Compare commits

...

308 Commits

Author SHA1 Message Date
nadena.dev release bot
400960257e Release 1.12.2 2025-04-03 02:44:37 +00:00
bd_
f0fcbb66b1
fix: animated parameters in merge motion aren't renamed (#1537)
* fix: animated parameters in merge motion aren't renamed

Closes: #1536
2025-04-02 19:39:34 -07:00
nadena.dev release bot
7fd35bb49a Release 1.12.1 2025-04-02 02:22:57 +00:00
bd_
bc4c6628ee chore(deps): update NDMF dep 2025-04-01 19:16:16 -07:00
bd_
b733ce2e4d
fix: MA breaks in new VRCSDK projects (#1532) 2025-04-01 19:01:37 -07:00
bd_
3324d3f71b docs: fix CHANGELOG 2025-03-31 22:39:35 -07:00
nadena.dev release bot
c3d2cfb29f Release 1.12.0 2025-04-01 05:10:29 +00:00
bd_
7610020c3b chore(ci): fix typo 2025-03-31 22:05:48 -07:00
bd_
89de978c77 chore(ci): fix typo 2025-03-31 21:39:33 -07:00
bd_
db9389052c chore(deps): update NDMF dependency 2025-03-31 18:53:19 -07:00
bd_
81aed5b798
fix: compatibility issue with lilycalInventory (#1531)
The early animator cloning logic was dropped in refactoring, put it back.
2025-03-30 23:00:12 +00:00
bd_
706ce7aa2f
feat: use stable identifiers for MA Menu Items (#1530) 2025-03-30 18:07:48 +00:00
nadena.dev release bot
b75e74ef84 Release 1.12.0-rc.1 2025-03-28 02:15:53 +00:00
bd_
8ef4cf6328
feat: MA Parameters auto-rename now uses stable names (#1529)
Closes: #1527, #1430
2025-03-23 13:50:59 -07:00
nadena-dev-ci
c521bd7721
Update source file en-US.json (#1526) 2025-03-23 19:22:08 +00:00
bd_
e46e958f39
fix: match WD = off setting is not respected (#1528)
Closes: #1519
2025-03-23 18:34:19 +00:00
bd_
8a45515af0
chore(docs): update merge motion screenshots (#1525)
Closes: #1517
2025-03-23 02:54:15 +00:00
bd_
36b442f904
feat: allow opt-out from MMD handling (#1524)
Closes: #1518
2025-03-23 02:51:23 +00:00
bd_
124392c422
chore(docs): adjust docs translation (#1523)
Closes: #1521
2025-03-23 02:46:40 +00:00
bd_
dff7f03c2f
fix: convert constraints fails to convert animations (#1522)
Closes: #1520
2025-03-23 01:57:04 +00:00
nadena.dev release bot
713a0d3b1d Release 1.12.0-rc.0 2025-03-22 04:43:16 +00:00
nadena-dev-ci
e2a02982d5
New Crowdin updates (#1516)
* New translations en-us.json (Japanese)

* New translations en-us.json (Korean)

* New translations en-us.json (Chinese Simplified)

* New translations en-us.json (Chinese Traditional)

* Update source file en-US.json

* New translations en-us.json (Japanese)
2025-03-21 21:38:09 -07:00
bd_
672dd8b31f chore: update NDMF dependency 2025-03-21 21:34:28 -07:00
bd_
6175e20e46
fix: expressions menu icon compression breaks on iOS builds (#1513) 2025-03-22 03:25:00 +00:00
bd_
fce938820b
fix: compiler warnings (#1515) 2025-03-22 03:20:23 +00:00
bd_
34deac5681
feat: support merging animation clips in Merge Blend Tree (#1514)
This renames Merge Blend Tree to Merge Motion, and expands it to support arbitrary motions.

Closes: #1438
2025-03-22 03:10:45 +00:00
bd_
b49e5cb460
fix: non-divisible-by-four texture sizes breaks automatic expressions menu icon compression (#1508)
Closes: #1477
2025-03-19 03:25:30 +00:00
bd_
3165d471b5 chore(changelog): fix header after release 2025-03-16 21:14:45 -07:00
bd_
2d59c74066 fix(ci): perform-release workflow doesn't update JP CHANGELOGs 2025-03-16 21:13:33 -07:00
nadena.dev release bot
55d744885f Release 1.12.0-beta.0 2025-03-17 04:09:15 +00:00
bd_
318d65f3b5
fix: DelayDisable unnecessarily tracks no-longer-relevant game objects (#1504)
Closes: #1469
2025-03-17 03:58:05 +00:00
bd_
aac70873c5
chore(deps): Update NDMF dependency (#1503)
Closes: #1410
2025-03-17 03:37:02 +00:00
bd_
5a17d6ea9a
feat: World Fixed Object now uses VRCParentConstraint and supports Android builds (#1502)
Closes: #1417
2025-03-17 03:11:58 +00:00
bd_
62fd986fd0
fix: unity editor shortcuts break while editing MA Parameters text fields (#1501)
Closes: #1414
2025-03-17 02:58:37 +00:00
bd_
39b4df8367
fix: missing japanese docs for World Scale Object (#1500)
Closes: #1498
2025-03-15 04:02:21 +00:00
Ao_425
fc9b2683c8
chore: Create toggle with submenu in "CreateToggleForSelection" (#1437)
* chore: refactor and Ignore GameObjects with submenu for CreateToggleForSelection

* chore: create toggle with submenu in CreateToggleForSelection

* chore: use simple suffixes

* chore: update CHANGELOG

* fix: update CHANGELOG

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2025-03-15 03:54:15 +00:00
bd_
98311f11f8
fix: RC-toggled audio sources are always active when animations are blocked (#1499)
Closes: #1496
2025-03-15 03:49:28 +00:00
bd_
2557972461
feat: ensure that correct layers are toggled off in MMD worlds, even after messing with layer order (#1489)
We make the assumption that the MMD world will _specifically_
be disabling layers 1 and 2.
2025-03-14 20:44:50 -07:00
bd_
9510c56f7a
docs: add changelog to docs site (#1497)
Closes: #1479
2025-03-13 19:49:39 -07:00
dependabot[bot]
9418b00b54
chore(deps): bump @babel/runtime from 7.24.7 to 7.26.10 in /docs~ (#1493)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.24.7 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 04:10:55 +00:00
dependabot[bot]
61f11ff836
chore(deps): bump @babel/helpers from 7.24.7 to 7.26.10 in /docs~ (#1494)
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.24.7 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 04:10:41 +00:00
dependabot[bot]
2823696af9
chore(deps): bump @babel/runtime-corejs3 in /docs~ (#1495)
Bumps [@babel/runtime-corejs3](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs3) from 7.26.7 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime-corejs3)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs3"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 21:10:14 -07:00
bd_
45352296e9 Revert "ci: testing"
This reverts commit 4323b40362c357847603e65a7f34a33b26962987.
2025-03-12 21:10:02 -07:00
bd_
4323b40362 ci: testing 2025-03-12 21:09:18 -07:00
bd_
57f1851cdd ci: exclude dependabot from changelog checks 2025-03-12 21:08:05 -07:00
bd_
494ad1c4d9
ci: fix missing logo assets (#1492)
Closes: #1490
2025-03-13 03:24:35 +00:00
bd_
2e79c9b195 ci: enable CHANGELOG validation
Closes: #1478
2025-03-12 20:16:49 -07:00
bd_
7182baca47
fix: additional test failures (#1491) 2025-03-13 03:05:58 +00:00
nadena-dev-ci
71a0d82c66
New Crowdin updates (#1485)
* Update source file en-US.json

* New translations en-us.json (Japanese)
2025-03-13 02:34:28 +00:00
bd_
4c44c576de
fix: Merge Animator in replace mode breaks Merge Blend Tree (#1488) 2025-03-12 02:21:21 +00:00
bd_
ec73eb6225
fix: test failures in Merge Armature (#1487)
... apparently auto-merge wasn't set properly to check for unit test completion. oops.
2025-03-12 02:19:05 +00:00
bd_
295a46ec12
feat: support merging humanoid bones with PBs in limited cases (#1429)
* feat: support merging humanoid bones with PBs in limited cases

This change adds support for merging humanoid bones that are a target of PhysBones, provided that all humanoid children are excluded from that PhysBone (either with a direct ignores field, or using PB Blocker).

Note: Because this is a significant expansion of support, this will need to wait for a minor release to maintain semver semantics.

Closes: #1406
2025-03-12 01:51:47 +00:00
bd_
fa004b2db5
fix: Merge Animator should not adjust WD in single state or additive layers (#1483) 2025-03-10 21:09:27 -07:00
bd_
f6362cdbc2
chore: re-enable disabled test (#1484) 2025-03-10 21:09:11 -07:00
bd_
cbf7fa4233 chore: bump NDMF dependency 2025-03-10 21:01:42 -07:00
dependabot[bot]
a7906e4fd6
chore(deps): bump prismjs from 1.29.0 to 1.30.0 in /docs~ (#1480)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 20:43:30 -07:00
bd_
f0a61fe55a
feat: Merge Animator replacement mode (#1482)
Closes: #330
Closes: #1308
2025-03-10 20:37:29 -07:00
bd_
7abfb021e3 feat: World Scale Object 2025-03-10 20:36:17 -07:00
bd_
903f230633 docs(changelog): fix changelog issues 2025-03-10 19:13:28 -07:00
nadena.dev release bot
be729c8f53 Release 1.12.0-alpha.2 2025-03-10 05:19:45 +00:00
nadena.dev release bot
233521b029 Release 1.7.0-alpha.2 2025-03-10 03:56:45 +00:00
bd_
92ebc1c1ac ci: remove obsolete release script 2025-03-09 20:52:19 -07:00
bd_
1a20333b58 docs(changelog): CHANGELOG updates 2025-03-09 20:51:45 -07:00
bd_
5f19bad688 Revert "Release 1.12.0-alpha.2"
This reverts commit fdd3110a98315dae658fc876568e3f719752f7cb.
2025-03-09 20:51:13 -07:00
nadena.dev release bot
fdd3110a98 Release 1.12.0-alpha.2 2025-03-10 03:46:21 +00:00
bd_
c7a06e71a0 chore: update dependencies 2025-03-09 20:41:51 -07:00
bd_
cd5bb5ff4e refactor: use VirtualControllerContext.Controllers 2025-03-09 20:41:51 -07:00
bd_
e91b8ab6c3 refactor: use IVirtualize* 2025-03-09 20:41:51 -07:00
bd_
e3a01ff58b ci: fix incorrect release tag and name 2025-03-09 20:29:38 -07:00
bd_
5de63b3495 ci: fixing issues with releaser workflow 2025-03-09 20:12:49 -07:00
bd_
1c8477ba4a ci: new release workflow 2025-03-09 19:33:36 -07:00
bd_
cdf8d8400d
1.12.0-alpha.1 (#1474) 2025-03-08 21:03:17 -08:00
ColorlessColor
aaa448bf57
fix: Use localOnly instead of syncType = NotSynced when importing parameter asset (#1460) 2025-03-08 20:09:43 -08:00
Reina_Sakiria
e0f55ddc4f
fix: forget to process VirtualStateTransition (#1468)
* fix: forget to process VirtualStateTransition

* test: add test for renaming transition parameters

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2025-03-08 07:36:46 -08:00
dependabot[bot]
d4543a38c5
chore(deps): bump nathanvaughn/actions-cloudflare-purge (#1467)
Bumps [nathanvaughn/actions-cloudflare-purge](https://github.com/nathanvaughn/actions-cloudflare-purge) from 0efc272496735521e97d22ba9caa750c2781e257 to 784d555fc0fc48946a1e34873a43fc8cf634bcfa.
- [Release notes](https://github.com/nathanvaughn/actions-cloudflare-purge/releases)
- [Commits](0efc272496...784d555fc0)

---
updated-dependencies:
- dependency-name: nathanvaughn/actions-cloudflare-purge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 18:42:01 -08:00
bd_
e471df8860
fix: incorrect first layer weights in Merge Animator (#1473)
Unity ignores the weight of the first layer in a controller, and forces it to one; here, we replicate that behavior in Merge Animator.

Note that we choose to do this here rather than in NDMF - this is because NDMF-level APIs are trying to accurately represent the data in the animator, and should be able to round-trip even "garbage" data there.

Closes: https://github.com/bdunderscore/ndmf/issues/534
Closes: #1472
2025-03-07 18:41:26 -08:00
bd_
70dd38e970
fix: incorrect initial states for Shape Changer (#1464) 2025-03-02 20:52:38 -08:00
bd_
7e5d631b9d
fix: missing PreviewMode innate parameter (#1466) 2025-03-02 20:51:59 -08:00
nadena-dev-ci
3f57e17548
New Crowdin updates (#1462)
* New translations en-us.json (Chinese Simplified)

* New translations en-us.json (Chinese Traditional)
2025-03-02 18:20:22 -08:00
bd_
242a108703
fix: incorrect initial state for inverted Shape Changers (#1458) 2025-02-19 18:26:09 -08:00
bd_
07cce41329
fix: NRE in GetReplacementPath (#1452) 2025-02-19 18:17:11 -08:00
bd_
53c47bfb0b 1.12.0-alpha.0 2025-02-16 22:25:57 -08:00
bd_
7cafd314a4
chore: port MA to the new NDMF animation API (#1371) 2025-02-16 22:25:12 -08:00
bd_
d7e949239a 1.11.5 2025-02-16 18:40:48 -08:00
bd_
d3ae37c3cf fix: RCs can't be used in conjunction with custom animations
Closes: #1440
2025-02-16 18:40:15 -08:00
bd_
61d4b203ca docs: update outdated VHA docs
Closes: #1444
2025-02-16 16:18:04 -08:00
bd_
54f5bd1922 1.11.4 2025-02-03 06:15:17 -08:00
bd_
c6199ca183 1.11.3 2025-02-02 19:58:51 -08:00
bd_
8a1d2b77dd Revert "chore: sync object and menu"
This reverts commit fa4e714f307fce3cec4a13bd6cfbf2760134a710.

This is temporarily reverted until we have support in MenuItem to ensure
that `0` is always the default selection, even for toggles. This should
be reverted when #1433 is done (which will resolve #1434)
2025-02-02 19:52:33 -08:00
tliks
2849ea9183 chore: sync object and menu 2025-02-02 19:52:33 -08:00
tliks
eb7793d7c5 chore: rename "SetupToggle" to "CreateToggleForSelection" 2025-02-02 19:52:33 -08:00
tliks
de18e77e34 chore: use MenuCommand 2025-02-02 19:52:33 -08:00
tliks
19d8ebee68 chore: Active = !selected.activeSelf 2025-02-02 19:52:33 -08:00
tliks
54d85a5cef feat: support multiple selections 2025-02-02 19:52:33 -08:00
tliks
8e2650acdb feat: setup toggle 2025-02-02 19:52:33 -08:00
bd_
a6cde1fbe9 fix: menu assets referenced by Menu Installer aren't renamed correctly
Closes: #1414
2025-01-31 19:08:52 -08:00
あお
428a2cc4a3 docs: fix typo in MA Parameters docs 2025-01-31 17:18:57 -08:00
tliks
5175626b23 chore: remove unnecessary observing 2025-01-31 17:18:38 -08:00
tliks
8414d203e5 chore: ignore inactive root in GetTargetGroups 2025-01-31 17:18:38 -08:00
bd_
89d38c5371 fix: occasional NRE from RO analysis 2025-01-31 17:15:34 -08:00
bd_
2f32cb4351 fix: RO values from always-on components are sometimes ignored 2025-01-31 17:15:27 -08:00
dependabot[bot]
f799af4c03 chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /docs~
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 19:15:47 -08:00
bd_
75d3b5078a 1.11.2 2025-01-24 19:06:26 -08:00
bd_
18569ab556 fix: deletedShape animation curves are retained in final avatar animations
This results in downstream tools such as AAO improperly treating these objects as animated.
2025-01-24 19:02:09 -08:00
bd_
129ad4dc35 fix: high load caused by ObjectReferenceFixer when paths collide
Closes: #1411
2025-01-24 18:59:10 -08:00
anatawa12
909d7e66c4 fix: menu icon in submenu are not fixed 2025-01-24 18:58:54 -08:00
Rerigferl
3ceafb8e1f fix: add BeginProperty to AvatarObjectReferenceDrawer 2025-01-24 18:58:34 -08:00
dependabot[bot]
3a94498e45 chore(deps): bump softprops/action-gh-release in /.github/workflows
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.1.0 to 2.2.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](01570a1f39...c95fe14893)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 18:56:59 -08:00
dependabot[bot]
c1b2351537 chore(deps): bump nathanvaughn/actions-cloudflare-purge
Bumps [nathanvaughn/actions-cloudflare-purge](https://github.com/nathanvaughn/actions-cloudflare-purge) from 992cc4e96422fb8ddf077281678373fe41e7736c to 0efc272496735521e97d22ba9caa750c2781e257.
- [Release notes](https://github.com/nathanvaughn/actions-cloudflare-purge/releases)
- [Commits](992cc4e964...0efc272496)

---
updated-dependencies:
- dependency-name: nathanvaughn/actions-cloudflare-purge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 18:56:47 -08:00
bd_
71f428d804 docs: update dependency versions 2025-01-24 18:56:27 -08:00
bd_
be35de2018 ci: fix CI 2025-01-24 18:37:16 -08:00
bd_
cca9e22edd 1.11.1 2024-12-08 18:26:42 -08:00
nadena-dev-ci
cd0ed0f009 New translations en-us.json (Chinese Simplified) 2024-12-08 18:24:33 -08:00
nadena-dev-ci
1b8e40c747 New translations en-us.json (Chinese Simplified) 2024-12-08 18:24:33 -08:00
bd_
e0f9e95df3 fix: issues with MAMoveIndependentlyManager error handling
Closes: #1386
2024-12-08 18:24:21 -08:00
bd_
7967fcf121 fix: NullReferenceException in some cases when viewing MA Menu Item controls
Closes: #1383
2024-12-06 17:26:14 -08:00
bd_
f7dc99be7d 1.11.0 2024-12-01 16:54:02 -08:00
bd_
9ce8a209d8
Merge pull request #1008 from hai-vr/menu-richtext
feat: menu name may use string field
2024-12-01 15:48:54 -08:00
bd_
6dcd63dde7
Merge branch 'main' into menu-richtext 2024-12-01 15:41:30 -08:00
bd_
d91c69835c feat: add API to open Menu Installer's selection UI
Closes: #1327
2024-12-01 15:40:57 -08:00
bd_
5325c809a4 chore: update VRCSDK dependency 2024-12-01 15:36:53 -08:00
bd_
76eca08c22 feat: Sync Parameter Sequence 2024-12-01 15:36:53 -08:00
bd_
2c3e24333a
feat: Remove Vertex Color (#1378) 2024-12-01 13:54:43 -08:00
Rinna Koharu
f35283db51
fix: changed not to use Instantiate when creating inverse root bone (#1376)
Co-authored-by: Rerigferl <70315656+AshleyScarlet@users.noreply.github.com>
2024-12-01 07:10:48 -08:00
bd_
d538551fad 1.10.11 2024-11-28 17:00:25 -08:00
bd_
f3bf07b601
perf: switch to AssetSaver API (#1372) 2024-11-28 16:59:01 -08:00
bd_
4a65b9f2ac 1.10.10 2024-11-25 19:11:33 -08:00
bd_
80d17f8284
fix: heuristic matching for "びしょ濡れのしずくちゃん" (#1370) 2024-11-25 19:09:12 -08:00
bd_
a7ef0d6635
fix: multiple issues with MAMoveIndependently (#1369)
* fix: multiple issues with MAMoveIndependently

Fixed issues with nested Move Independently components (Closes #1367).

Fixed MAMoveIndependently not being saved (Supercedes #1366)

Reduce jittering when moving MAMI bones.

* chore: fix some namespaces

* chore: fix non-editor buil
2024-11-24 18:24:38 -08:00
Reina_Sakiria
0606311f51
fix: MA MeshSettings forget to keep the inverse root bone. (#1356)
* fix: Forget to keep the inverse root bone.

* fix: inverted root bone position is offset from original

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-11-24 18:02:10 -08:00
Reina_Sakiria
fd59c3e910
fix: MA MergeArmature forget to retaining the root bone. (#1355)
* fix: forget to retaining the root bone

* chore: fix test

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-11-24 16:41:20 -08:00
dependabot[bot]
5c084a8b8a
chore(deps): bump softprops/action-gh-release in /.github/workflows (#1358)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.9 to 2.1.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](e7a8f85e1c...01570a1f39)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 18:32:12 -08:00
dependabot[bot]
2a3da2fec3
chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /docs~ (#1364)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 18:31:59 -08:00
bd_
ef4304acf1 1.10.9 2024-11-19 19:05:11 -08:00
bd_
46f5296528
Revert "Use VRCParentConstraint instead of constraint hack for world fixed objects when available (#1326)" (#1363)
This reverts commit a984cf86735170d075f8224dd7c83fa760e54eed. The prior
behavior was to lock world fixed objects at their offset from the
origin; however with this change we ended up locking them at a location
relative to the avatar spawn location, breaking some gimmicks.

I tried experimenting a bit with VRCConstraint to try to replicate this
behavior, but it seems a bit non-trivial, so we'll revert this as a
hotfix for now.
2024-11-19 19:04:33 -08:00
bd_
9f4a7a6304
chore: update dependencies (#1354) 2024-11-16 20:12:47 -08:00
bd_
2c0b6df863 1.10.8 2024-11-16 19:46:41 -08:00
bd_
b7373b6584
fix: incorrect item name in mesh settings JP docs (#1352)
Closes: #1314
2024-11-16 19:45:30 -08:00
bd_
4b5cf06097
docs: update MA Parameters docs (#1351) 2024-11-16 19:43:19 -08:00
bd_
e68e176aa4
feat: allow multi-edit of "Is Default" when parameter names are empty (#1350) 2024-11-16 19:20:36 -08:00
bd_
d23b9f94a2
fix: empty MA Parameters names break auto param UI (#1346) 2024-11-16 19:02:37 -08:00
bd_
30512c26e8
fix: NotSynced parameters become synced when merging (#1347)
Closes: #1342
2024-11-16 19:02:32 -08:00
bd_
4405d7aa56
fix: MA Parameters bool items + MA Menu Item auto value is broken (#1345)
Closes: #1331
2024-11-16 19:02:24 -08:00
Jeremy Lam aka. Vistanz
a984cf8673
Use VRCParentConstraint instead of constraint hack for world fixed objects when available (#1326)
* Fixes error when merging same parameter with different type in RC menu item.

* Rollback ReactiveObjectPass, use another approach

* Set defaults to ModularAvatarMergeAnimator to prevent potential errors

* Use VRCParentConstraint instead of constraint hack
for world fixed objects when available

* Make sure the VRC constraint only applies on VRChat avatars

* Extract creation logic to external method

* Rearrange method

* Get rid of unit test

* Fix unit test build error

* Fix assert fail
2024-11-16 19:02:09 -08:00
dependabot[bot]
7980d933c2
chore(deps): bump softprops/action-gh-release in /.github/workflows (#1338)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.0.9.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](c062e08bd5...e7a8f85e1c)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-03 21:26:56 -08:00
bd_
ef0beec8ed 1.10.7 2024-11-03 18:33:03 -08:00
bd_
81ad82b765
fix: ParameterAssignerPass forces all parameters to float type (#1337)
Closes: #1335
2024-11-03 18:32:29 -08:00
bd_
973e7d2448
fix: suppress ObjectReferenceFixer in play mode (#1336)
Not sure if this will do anything, but maybe it'll help with the perf issues people have reported?
2024-11-03 18:32:22 -08:00
nadena-dev-ci
59ff119d20
New Crowdin updates (#1330)
* Update source file en-US.json

* New translations en-us.json (Japanese)
2024-11-03 18:32:15 -08:00
bd_
6fd8ac0cd7 1.10.6 2024-11-02 15:23:41 -07:00
Ao_425
29e2041312
chore: Add mesh settings regardless of parent presence in Setup Outfit (#1322)
* chore: remove parent check for adding meshsettings

* chore: a little refactoring
2024-11-02 15:20:13 -07:00
bd_
a3b9acba39
ui: Improvements to MA Parameters editor (#1329)
* Add import-from-asset feature (Closes: #880, #668, #410)
* Limit size of scrollable area to avoid double-scrolling
* Add support for pressing the "delete" key to delete parameters.
2024-11-02 15:17:58 -07:00
Sayamame-beans
497d16f89d
Improve InferPrefixSuffix / Support using Humanoid Rig on Setup Outfit (#1167)
* feat: inferring prefix/suffix now supports infer with HeuristicBoneMapper

* feat: inferring prefix/suffix is now triggered when prefix/suffix is empty and merge target changed

* chore: add comment for inferring prefix/suffix with HeuristicBoneMapper

* feat: support using Humanoid Rig on RenameBonesByHeuristic

* feat: support using cloth's Humanoid Rig on merge_armature.adjust_names

* feat: support outfits' hips in one more deep place

* chore: refine condition on Heuristic Bone Mapper's exact humanoid bone matching

* chore: unify the process for get outfit's humanoid bones

* chore: rename variable name to clarify means

* chore: use InitializeOnLoadMethod instead of reflection to get boneNamePattern from Editor Assembly

* test: add some tests for SetupOutfit and InferPrefixSuffix
2024-11-02 15:17:24 -07:00
bd_
e752762d21
chore: remove stray log statement (#1325) 2024-11-02 13:23:33 -07:00
bd_
32ea6678f7
feat: object references are now corrected when their target path is renamed/changes (#1323) 2024-10-27 10:07:23 -07:00
bd_
1153abd16e
fix: NRE when Move Independently is placed on a scene root (#1321)
Closes: #1317
2024-10-27 10:07:14 -07:00
kaikoga
efa263b551
chore: Fix non-VRChat support (for MA 1.10.5) (#1324)
* feat: add version defines for VRCSDK

* chore: early return if VRCSDK project but not VRChat avatar
2024-10-27 10:06:35 -07:00
Jeremy Lam aka. Vistanz
07b648dcc1
Fixes error when merging same parameter with different type in RC menu item (#1313) 2024-10-25 20:11:23 -07:00
bd_
131f54a713
docs: Update JP version of mesh-settings.md (#1315)
Closes: #1314
2024-10-25 19:53:09 -07:00
dependabot[bot]
1cce15590c
chore(deps): bump http-proxy-middleware from 2.0.6 to 2.0.7 in /docs~ (#1319)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 19:53:00 -07:00
bd_
e0702c5dcf 1.10.5 2024-10-19 20:09:52 -07:00
Haï~
3b067e4664
Make compatible with Unity 6 projects (#1232)
* Disable compilation for use in Unity 6 (6000.0.20f1):
- Do not compile some classes and code paths in non-VRChat projects.
- This has been tested in Unity 6 (6000.0.20f1).

* Fix hide internal components in Unity 6:
- [AddComponentMenu("")] does not work in Unity 6.
- Replace it with [AddComponentMenu("/")]
- This alternative is confirmed to also work in Unity 2022.

---------

Co-authored-by: Haï~ <hai-vr@users.noreply.github.com>
Co-authored-by: bd_ <bd_@nadena.dev>
2024-10-19 18:58:41 -07:00
Sayamame-beans
26153ea60d
feat: improve behavior when called setup outfit for setuped outfit (#1200) 2024-10-19 17:52:51 -07:00
bd_
5bafb0ba9d
fix: "extract menu to objects" does not set the target of its menu group properly (#1304)
Closes: #1297
2024-10-19 17:46:39 -07:00
bd_
11a62c88d4
fix: issues when handling VRCExpressionMenus with an uninitialized type field (#1303) 2024-10-19 17:46:31 -07:00
bd_
123523540e
fix: parameters lose their default value when others are moved around (#1302)
Closes: #1296
2024-10-19 17:15:43 -07:00
bd_
ab4d1fd2f4
fix: transient NRE when opening reaction debugger (#1301) 2024-10-19 17:15:33 -07:00
bd_
9dc342e81e
fix: always-on toggles fail to animate objects to off (#1300)
Closes: #1285
2024-10-19 17:15:23 -07:00
bd_
ae975506d7
fix: occasionally objects controlling reactive components "flicker" (#1298)
Reported-by: @whipnice
2024-10-19 17:15:14 -07:00
bd_
3ba0219430 1.10.4 2024-10-15 19:19:07 -07:00
bd_
8bf1d29bf3
test: add missing blendshape/RC tests (#1293) 2024-10-15 19:16:43 -07:00
dependabot[bot]
55ab65e22d
chore(deps): bump nathanvaughn/actions-cloudflare-purge (#1287)
Bumps [nathanvaughn/actions-cloudflare-purge](https://github.com/nathanvaughn/actions-cloudflare-purge) from cd4afdf666c2e6a6720048f27ac9cbdd664a673a to 992cc4e96422fb8ddf077281678373fe41e7736c.
- [Release notes](https://github.com/nathanvaughn/actions-cloudflare-purge/releases)
- [Commits](cd4afdf666...992cc4e964)

---
updated-dependencies:
- dependency-name: nathanvaughn/actions-cloudflare-purge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 19:13:24 -07:00
bd_
b73feb6b71
fix: inactive menu items don't appear in RO debugger (#1291) 2024-10-15 19:13:08 -07:00
bd_
662172c2e5
fix: NRE in RO simulator (#1292) 2024-10-15 19:13:01 -07:00
bd_
5d399dce4a ci: workaround VPM issues 2024-10-14 21:17:31 -07:00
nadena-dev-ci
766f728a8a
New Crowdin updates (#1284)
* Update source file en-US.json

* Update source file en-US.json

* New translations en-us.json (Japanese)
2024-10-13 18:28:53 -07:00
bd_
f40d02ceb9
fix: NRE when AnimatorControllerLayer.stateMachine is null (#1283)
Closes: #1056
2024-10-12 17:29:27 -07:00
bd_
7ae98d63b0
fix: missing localization strings for Replace Object errors (#1282)
Closes: #1281
2024-10-12 17:29:19 -07:00
nekobako
0b8cd3b3b6
feat: add support for drag-and-drop on MaterialSetter and ShapeChanger (#1271)
* feat: add support for drag-and-drop on MaterialSetter and ShapeChanger

* fix: allow adding known objects to MaterialSetter and ShapeChanger
2024-10-12 16:52:37 -07:00
Mooncake Sugar
7f9e65bcbc
docs: delete button group on top page (#1270) 2024-10-12 16:35:54 -07:00
Sayamame-beans
4a376f8723
Merge Armatureの"Reset position to base avatar"にA/Tポーズ変換オプションを追加 (#1188)
* feat: A/T Pose conversion on "Reset position to base avatar"

* chore: reorder posReset options

* chore: unify FixAPose functions into SetupOutfit.FixAPose
2024-10-12 16:34:30 -07:00
bd_
1024f626e8 1.10.3 2024-10-05 17:55:29 -07:00
bd_
828e6b4548
fix: propagate shape changer effects through BlendshapeSync (#1267)
Closes: #1259
2024-10-05 17:46:45 -07:00
bd_
656a401684
fix: proxy animations are cloned in Merge Animator (#1266)
This fixes issues with しゃがみ置き換え+α among other things.
2024-10-05 15:19:42 -07:00
Kisaragi
394601d4a7
docs: ドキュメントへの誘導を微調整 (#1265) 2024-10-05 15:19:32 -07:00
bd_
4da4ebc984 1.10.2 2024-10-03 20:18:00 -07:00
bd_
30cafb21e4
fix: incorrect handling of shape key deletion (#1258)
This change reworks delete handling to be more consistent with other properties,
by treating it as a virtual property (`deletedShape.{blendshapeName}`) instead of
a weird additional field of blendshape keys. This then fixes a number of issues
(e.g. broken preview for delete keys).

Fixes: #1253
2024-10-03 20:16:53 -07:00
bd_
c379d730ca chore: fix unit test broken by merge 2024-10-02 20:09:14 -07:00
bd_
d9c0a21f0d 1.10.1 2024-10-02 19:52:21 -07:00
bd_
816d2b28cb
fix: NRE from scale adjuster preview (#1251) 2024-10-02 19:51:17 -07:00
bd_
4ec36ca489
fix: shape changer preview overrides default blendshape values inappropriately (#1250)
Closes: #1227
2024-10-02 19:48:38 -07:00
bd_
409592f952
fix(rc): constant-off objects are not handled correctly (#1249)
Closes: #1233
2024-10-02 19:42:19 -07:00
bd_
02204c272f
Revert "fix: remove unnecessory exit transitions for reactive components (#1161)" (#1248)
This reverts commit 9dfa0dae23d9d4aa6e80e9c9d691e363dc297fdc. Those
transitions are needed when controlling the same object from multiple
parameters.

Closes: #1233
2024-10-02 19:03:44 -07:00
nekobako
36e035c8c7
fix: inverted reactive components on inactive objects didn't set defa… (#1246)
* fix: inverted reactive components on inactive objects didn't set default states

* chore: use `InitiallyActive` helper

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-10-02 19:03:03 -07:00
bd_
6c55185895 docs: fix reactive component help URL link 2024-10-01 20:19:14 -07:00
bd_
1c29af20fb docs: fix incorrect docusaurus directive 2024-10-01 20:16:29 -07:00
bd_
4b9d1128c6 chore: set harmony ID on UnpatchAll call 2024-10-01 20:16:17 -07:00
nekobako
2c9939dea8 chore: remove ShapeChanger migration for beta only 2024-10-01 20:15:24 -07:00
nadena-dev-ci
8150e05dd0 New translations en-us.json (Chinese Traditional) 2024-10-01 20:15:07 -07:00
bd_
f85d455c8f fix: NRE issued when viewing Menu Item editor outside of an avatar 2024-10-01 20:14:58 -07:00
bd_
cb2afcc3d5 fix: Menu Installers on the same object as Merge Armature are not processed
Changes to pass ordering caused Merge Armature to destroy menus before being
processed by Menu Installer; fix this by hoisting menu generation to occur within
the animation services context, before Merge Armature runs. This is safe because
the menu installer pass does not interact with the avatar's animator controllers
directly.
2024-10-01 20:13:02 -07:00
bd_
838f1dac7e fix: ignore blendtree-only layers when determing animator WD state 2024-10-01 20:12:52 -07:00
Haï~
94002e4594 review:
- Rebased to 1.10.0
- When editing multiple objects, always edit the label.
2024-10-01 21:28:54 +02:00
Haï~
c5e787045a review:
- Rebased to 1.10.0-rc.4 because the inspector of SubMenu of source Expressions Menu were broken in the base commit this branch initially started with, which was preventing testing some aspects raised during review.
- When this is being rendered as part of an SubMenu of source Expressions Menu, don't use any of the label logic, as menu items within such an Expressions Menu are not backed by any GameObject.
- Rename _isTryingRichLabel to _useLabel.
- Since switching to unlinked always overwrites the label field with the current ObjectName, and switching to linked always empties the label field, the state of _useLabel while the Inspector is open is implied by the value of the label field, or the previous state of the _useLabel field itself when the label field is being emptied out.
  - In addition, use the |= operator.
- When the name is linked, and the user begins typing the "<" character, set the label field, and do not apply the name. This will automatically switch to linked mode as the inspector will be reevaluated a second time.
  - If the original object name already contains a "<" character (i.e. it comes from a previous version of Modular Avatar), there will be no automatic conversion happening as long as the object name still contains the "<" character.
- Changed the localization keys to discard the rich text toggle aspect.
- Not addressed: When multiple Menu Item components are selected, the behaviour of the inspector currently edits the GameObject name, with no link button, and no automatic conversion when typing "<", regardless of the contents of the label field.
2024-10-01 09:52:39 +02:00
Haï~
776f08be3f review:
- Add a link/unlink icon to the right of the name field.
- Only show the rich text preview when there is the character '<' in the label field.
- Toggling link from ON to OFF will set the label to the current GameObject name.
2024-10-01 09:52:39 +02:00
Haï~
b01b280f79 feat: menu name may use string field 2024-10-01 09:52:39 +02:00
bd_
a71af7ae0a 1.10.0 2024-09-29 15:02:58 -07:00
bd_
6dafe72c3b fix: play audio path mapping is broken
Closes: #936
2024-09-29 14:28:44 -07:00
bd_
848f857728 docs: updates to outfit creator documentation 2024-09-29 13:34:15 -07:00
dependabot[bot]
01d75fb284 chore(deps): bump nathanvaughn/actions-cloudflare-purge
Bumps [nathanvaughn/actions-cloudflare-purge](https://github.com/nathanvaughn/actions-cloudflare-purge) from aa1121a867565ea71b60f445f441544df0c7b0b9 to cd4afdf666c2e6a6720048f27ac9cbdd664a673a.
- [Release notes](https://github.com/nathanvaughn/actions-cloudflare-purge/releases)
- [Commits](aa1121a867...cd4afdf666)

---
updated-dependencies:
- dependency-name: nathanvaughn/actions-cloudflare-purge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-29 12:50:13 -07:00
bd_
cd4cadf23f docs: add discord link 2024-09-29 12:50:06 -07:00
bd_
0f28cf7aba fix: warning is shown when using FPV on Android 2024-09-29 12:49:58 -07:00
nekobako
cc93c7e5ed feat: replace checkboxes in ObjectToggle with dropdowns 2024-09-29 06:26:12 -07:00
bd_
7040e3992b fix: ROSimulator UI refresh sometimes gets wedged
Closes: #1219
2024-09-28 20:05:53 -07:00
bd_
f3f2de3337 feat: add tracing points for the NDMF preview tracing system 2024-09-28 19:34:36 -07:00
bd_
b866628b24 chore: fix compiler warnings 2024-09-27 19:37:23 -07:00
bd_
7e5c227867 docs: update Visible Head Accessory docs 2024-09-27 18:57:50 -07:00
bd_
1fe8255f52 docs: update mesh settings docs 2024-09-27 18:57:50 -07:00
bd_
3f5d5a2013 1.10.0-rc.9 2024-09-27 18:35:19 -07:00
nadena-dev-ci
4f398d21c3
New Crowdin updates (#1212)
* New translations en-us.json (Japanese)

* New translations en-us.json (Korean)

* New translations en-us.json (Chinese Simplified)

* New translations en-us.json (Chinese Traditional)

* Update source file en-US.json

* New translations en-us.json (Japanese)
2024-09-27 18:06:53 -07:00
bd_
ee64cafe02
fix: remove ObjectIdentityComparer (#1211)
Apparently, it's safe to use Unity objects as keys in HashMaps, and doing
so actually fixes some edge cases where assets are recreated as a new C# object.
2024-09-25 21:39:01 -07:00
bd_
e63a34e2ba
fix: scale adjuster preview destroys proxy renderers inappropriately (#1213)
The scale adjuster preview system reparented proxy renderers under proxy
bones, in order to handle null root bones and MeshRenderers. However, it
then destroyed the entire proxy bone hierarchy, taking all proxy renderers
with it. This change instead tracks proxies, and reparents them back to the
root to avoid this issue.

Closes: #1177
2024-09-25 21:09:58 -07:00
bd_
a018df9219
chore: improve PropCache debuggability by adding a name property (#1209) 2024-09-25 20:01:59 -07:00
bd_
13b0ffe0b5
fix: missing observations in LocateReactions (#1210) 2024-09-25 20:01:55 -07:00
Sayamame-beans
de1744b080
chore: update some messages (#1208) 2024-09-25 19:35:25 -07:00
bd_
119c56878c 1.10.0-rc.8 2024-09-24 18:59:49 -07:00
nadena-dev-ci
d82c41c390
New translations en-us.json (Japanese) (#1204) 2024-09-24 18:56:57 -07:00
lilxyzw
2826c27d63
fix: cloned AnimatorController is not registered in ObjectRegistry #1205 (#1206) 2024-09-24 18:11:39 -07:00
bd_
51fedbd9b0
fix: ROSimulator registers multiple event handlers for state override buttons (#1203) 2024-09-23 20:27:56 -07:00
bd_
bf47a4c544
fix: reactive components break WD ON avatars (#1202)
Closes: #1199
2024-09-23 20:18:04 -07:00
Sayamame-beans
8e49df703f
fix: BoneProxy target couldn't be empty after assigned (#1192) 2024-09-23 19:14:26 -07:00
Sayamame-beans
032e7a692e
chore: fpvisible.NotUnderHead is now Warning, not Error (#1194) 2024-09-23 19:13:29 -07:00
Sayamame-beans
3b86822547
fix: Setup Outfit cause NRE when nothing selected (#1197) 2024-09-23 19:12:39 -07:00
bd_
fd3de6e680 1.10.0-rc.7 2024-09-22 18:18:52 -07:00
nekobako
2d8f5d764e
fix: resolve parameter type conflicts for menu items in params usage … (#1174)
* fix: resolve parameter type conflicts for menu items in params usage window

* Revert "fix: resolve parameter type conflicts for menu items in params usage window"

This reverts commit 4c6b41de4c92da5828219657b2927c90685e275a.

* fix: expand conflicting parameter types for menu item in introspection

* chore: update NDMF dependency

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-09-22 18:18:27 -07:00
bd_
a5e716cb3e
fix: incorrect auto parameter assignment when a non-auto item is set to 0 (#1189) 2024-09-22 18:12:26 -07:00
lilxyzw
54288ebd44
fix: MenuInstaller does not support cloning (#1173) (#1184)
* fix: MenuInstaller does not support cloning (#1173)

* fix: error when root menu is specified
2024-09-22 14:35:05 -07:00
nekobako
9dfa0dae23
fix: remove unnecessory exit transitions for reactive components (#1161) 2024-09-22 13:42:31 -07:00
bd_
5090d45cfe
prof: add some profiling annotations (#1183) 2024-09-20 20:46:35 -07:00
bd_
7bf5106246
fix: animation clips are not added to the persistent asset object on build (#1182)
This resulted in data loss when `AssetDatabase.StopAssetEditing()` was called, which can happen if VRCF triggers Poi lockdown.
2024-09-20 20:32:27 -07:00
bd_
c11a76642c 1.10.0-rc.6 2024-09-17 20:40:32 -07:00
bd_
71ddd257a3
test: add tests for PropCache (#1156)
* test: add tests for PropCache

* chore: update NDMF dependency
2024-09-17 20:40:09 -07:00
bd_
9b4e76e053
fix: Scale Adjuster preview breaks after changing scale of avatar root (#1172)
Closes: #1171
2024-09-17 20:26:26 -07:00
bd_
a98ef213ff
fix: performance issues with MAMenuItem (#1170)
Cache parameter introspection results (using PropCache) to avoid excessive
recomputation.

Closes: #1165
2024-09-17 20:25:47 -07:00
nadena-dev-ci
c2b6766a40
New Crowdin updates (#1169)
* New translations en-us.json (Japanese)

* New translations en-us.json (Japanese)
2024-09-17 20:25:41 -07:00
Sayamame-beans
8ed877c99c
fix: add pattern of "upper_chest" as a chest bone (#1168) 2024-09-17 19:56:15 -07:00
nekobako
56f1b67d31
fix: animator initial value type conversion (#1163) 2024-09-17 19:47:56 -07:00
bd_
3648348184
fix: ScaleAdjusterPreview breaks when avatar descriptors are nested (#1154) 2024-09-15 19:47:26 -07:00
bd_
9073ff8c2d
fix: blendshape sync reserializes prefab assets (#1153)
Hopefully fixes: #1148
2024-09-15 19:33:10 -07:00
nekobako
48b7d80f7c
Fix menu item float value (#1140)
* fix: menu item with float value incorrectly generates bool parameter

* fix: reactive components generate transitions with overlapping condition ranges

* chore: add tests for menu item parameter type

* fix: incorrect parameter type detemination for float values

* chore: add more tests for menu item parameter type

* refactor: unify logic to determine parameter type and rename confusing variable
2024-09-15 19:32:59 -07:00
nekobako
c80d24ea46
Fix parameter synced conflict (#1150)
* fix: parameter should be synced if any of sibling parameter is set to be synced

* fix: parameter should be saved/synced if any of menu item references same parameter is set to be saved/synced
2024-09-15 14:24:13 -07:00
nekobako
b83b89ce38
fix: incorrect default value for single menu item with automatic value (#1147) 2024-09-15 14:23:15 -07:00
dependabot[bot]
3b28ea2b14
chore(deps): bump express from 4.19.2 to 4.21.0 in /docs~ (#1144)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 19:48:28 -07:00
dependabot[bot]
65194fbc80
chore(deps): bump path-to-regexp from 6.2.1 to 6.3.0 in /docs-site~ (#1132)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 6.2.1 to 6.3.0.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v6.2.1...v6.3.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 19:41:44 -07:00
bd_
2ea9fb50b6 1.10.0-rc.5 2024-09-14 19:32:59 -07:00
bd_
4e3001ad65 fix: NullReferenceExceptions from ShapeChangerPreview 2024-09-14 18:42:01 -07:00
nekobako
faa8d210f2
Enhance default value field (#1125)
* feat: enhance default value field input

* fix: format default value when update type or value on parameters inspector

* fix: don't accept NaN and Infinity for default value setting
2024-09-14 17:53:00 -07:00
nekobako
f4d80b857d
fix: exception thrown when opening prefab override ui (#1141) 2024-09-14 17:51:43 -07:00
bd_
fed6a22d72
fix: multiple issues with auto parameter value assignment (#1136)
Closes: #1110
2024-09-13 19:27:01 -07:00
nadena-dev-ci
a9c2815106
New translations en-us.json (Chinese Traditional) (#1126) 2024-09-13 19:26:52 -07:00
nekobako
c0582a9961
fix: non-backwards-compatible changes to component initial values (#1114)
* fix: init menu item settings only when added manually from inspector

* fix: init menu item settings when added from some shortcuts

* fix: init menu item settings when reset from context menu

* fix: init merge animator settings only when added manually from inspector
2024-09-13 18:57:03 -07:00
Rinna Koharu
3eaf8bee6d
fix: Add null checks to ScaleAdjusterPreview (#1116)
* fix: Add null checks to ScaleAdjusterPreview

* chore: check for destroyed objects as well

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-09-13 18:54:20 -07:00
colloid
73755e7664
fix: add Rigify(metarig) bone mapping pattern (#1131)
* fix: add Rigify(metarig) bone mapping pattern

Rigifyで追加されるボーン名(metarigの物)を追加
ミコジンちゃん
https://booth.pm/ja/items/5699843
にて動作確認

* fix: add Rigify(metarig) finger bone mapping pattern

Rigifyで追加される手のボーン名(metarigの物)を追加
2024-09-13 18:07:02 -07:00
RayLight1732
4f77723906
Fix copy logic of sync layer (#1135) 2024-09-13 18:06:56 -07:00
nekobako
3be3cfb74a
Refine some UI (#1119)
* feat: warn by color when Shape Changer has an invalid blendshape

* feat: refine reactive components ui layout

* feat: refine ma parameters ui layout
2024-09-13 18:06:45 -07:00
nekobako
38384a3c70
fix: can't unset the default value for bool parameter (#1121) 2024-09-13 18:04:20 -07:00
nekobako
29177f2c5a
fix: NullReferenceException when opening RO Simulator for default Menu Item (#1107) 2024-09-13 17:55:49 -07:00
nekobako
106ba8c5ff
fix: previewing Object Toggle targets Renderers other than MeshRender… (#1112)
* fix: NullReferenceException when previewing Object Toggle with renderers other than MeshRenderer and SkinnedMeshRenderer

* fix: NullReferenceException when previewing Scale Adjuster with renderers other than MeshRenderer and SkinnedMeshRenderer
2024-09-13 17:52:45 -07:00
nekobako
2735adf55c
fix: warning when object name contains "." (#1123)
* fix: warning when object name contains "."

* chore: change RC state and layer names to not reference Shape Changer specifically

---------

Co-authored-by: bd_ <bd_@nadena.dev>
2024-09-13 17:49:58 -07:00
nekobako
566a030730
Fix nested parameter saved setting (#1130)
* fix: child parameters should not override parent saved setting

* fix: sibling parameters can override sibling saved setting
2024-09-13 17:49:20 -07:00
bd_
1163fac2e7 1.10.0-rc.4 2024-09-04 22:01:45 -07:00
bd_
4fa0621655
fix: menu items with no default fail to generate animator controller parameter entries (#1105) 2024-09-04 18:58:51 -07:00
bd_
acd6c50543
fix: menu item override is not shown in sim for autoprop boolean toggles (#1104) 2024-09-04 17:00:08 -07:00
bd_
89b4c8f921
fix: deactivating an inactive menu item doesn't work as expected (#1102) 2024-09-04 12:41:00 -07:00
bd_
389ae4f2cc
fix: performance issues when RO Simulator is open for too long (#1101)
Also fix an issue where the clear overrides button doesn't work for menu item overrides.

Closes: #1100
2024-09-04 12:40:48 -07:00
nadena-dev-ci
422ed5cfb1
New Crowdin updates (#1099)
* New translations en-us.json (Japanese)

* Update source file en-US.json
2024-09-03 21:24:00 -07:00
bd_
0ee291076f
feat: Menu Item automatic values (#1098) 2024-09-03 19:07:33 -07:00
nekobako
c63128095e
fix: ArgumentNullException when selecting multiple Menu Item with same settings (#1097) 2024-09-03 18:51:36 -07:00
bd_
d403f1b178
ui: improve handling of saved/synced checkboxes on MenuItems with sibling items (#1095)
We will now force the state of all related MenuItems to match when the
synced/saved checkboxes are updated on the Menu Item UI.
2024-09-03 16:05:18 -07:00
bd_
e07b18d87e
fix: Some MenuItemCoreGUI properties do not refresh when rendered in parent menu inspector (#1094)
Closes: #1091
2024-09-03 15:53:44 -07:00
bd_
668ab35b46
fix: avatar masks are not rewritten when merging animators (#1093)
Closes: #228
2024-09-03 15:44:29 -07:00
nekobako
f9a9f1f1ef
fix: clearing conflicted MenuItem.isDefault affects unrelated MenuItems (#1089) 2024-09-03 15:28:27 -07:00
bd_
22cff4ba3f
chore: set delete attached animator to true by default on MAMergeAnimator (#1092) 2024-09-03 15:26:52 -07:00
bd_
466017c102
feat: add support for drag-and-drop on the MA Object Toggle inspector (#1087) 2024-09-02 19:30:40 -07:00
bd_
1d58548013 1.10.0-rc.3 2024-09-02 17:59:17 -07:00
bd_
ae950ad938
fix: untranslated string in RO simulator UI (#1086) 2024-09-02 17:57:49 -07:00
bd_
371809f430
fix: incorrect handling of isDefault toggles for implicit parameters in UI (#1085)
Closes: #1079
2024-09-02 17:54:36 -07:00
dependabot[bot]
1aa6c03202
chore(deps): bump nathanvaughn/actions-cloudflare-purge (#1080)
Bumps [nathanvaughn/actions-cloudflare-purge](https://github.com/nathanvaughn/actions-cloudflare-purge) from 367672c723960cd03bb7d8c2c4d89062a3fc1fac to aa1121a867565ea71b60f445f441544df0c7b0b9.
- [Release notes](https://github.com/nathanvaughn/actions-cloudflare-purge/releases)
- [Commits](367672c723...aa1121a867)

---
updated-dependencies:
- dependency-name: nathanvaughn/actions-cloudflare-purge
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 17:54:27 -07:00
nadena-dev-ci
db06a6a492
New translations en-us.json (Chinese Traditional) (#1082) 2024-09-02 17:54:16 -07:00
bd_
7330cda42a
fix: previews do not update when invert option is changed (#1078) 2024-09-01 19:59:08 -07:00
bd_
ece8a6837a
fix: RO Simulator triggers all parameters (#1076) 2024-09-01 18:59:56 -07:00
bd_
c309d93bdd
fix: error during domain reload (#1077) 2024-09-01 18:59:44 -07:00
bd_
d33787a6b0 1.10.0-rc.2 2024-09-01 17:30:39 -07:00
bd_
0a6270bb43
feat: update AvatarObjectReference paths when target object is moved in scene (#1074)
Closes: #1037
2024-09-01 17:29:58 -07:00
bd_
682a0de0e0
perf: reimplement ModularAvatarScaleAdjuster (#1073)
Fixes a perf issue discovered when investigating #1055 .
Fixes: #1058 (probably)
2024-09-01 17:29:32 -07:00
bd_
b0032a09c0 chore: bump NDMF dependency version 2024-09-01 17:21:14 -07:00
bd_
28ed2e0ed1
fix: layer cross-references are broken by empty layer pruning in some cases (#1075)
Fixes: #830
2024-09-01 16:55:42 -07:00
bd_
c6e863d409
fix: MA Parameters does not update animator parameter curves (#1072)
Closes: #180
2024-08-31 20:47:13 -07:00
KOBAYASHI Yū
3bc090dc7d
Preserve local transform when rebinding humanoid avatar (#1062)
* Preserve local transform when rebinding humanoid avatar

* Check Animator.avatar

* Restore all transforms
2024-08-31 16:54:48 -07:00
kaikoga
2148ab0bfc
chore: Skip ParameterAssignerPass when not VRChat avatar (#1071) 2024-08-31 16:51:26 -07:00
bd_
818f16f839
docs: fix incorrect edit URL (#1070)
Closes: #1064
2024-08-30 20:17:26 -07:00
dependabot[bot]
7f3b0fec3e
chore(deps): bump webpack from 5.92.1 to 5.94.0 in /docs~ (#1059)
Bumps [webpack](https://github.com/webpack/webpack) from 5.92.1 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.92.1...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 19:34:44 -07:00
bd_
231feba3a2
docs: update unity 2019 deprecation page (#1069)
Closes: #978
2024-08-30 19:34:34 -07:00
bd_
13b9bf72e2
docs: document menu item autocreation of parameters (#1068)
Closes: #947
2024-08-30 19:19:56 -07:00
bd_
802fea09d9
feat: serialize Move Independently grouping (#1067)
Closed: #842
2024-08-30 19:19:46 -07:00
Kisaragi
f085ce07b6
docs(ja): fix potential typo on scale-adjuster.md (#1063) 2024-08-30 18:09:18 -07:00
nekobako
6cb249be44
fix: error when deleting Material Setter target (#1066) 2024-08-30 18:05:07 -07:00
nekobako
580cb2bfe9
Fix material setter index (#1061)
* fix: error when Material Setter has an invalid index

* feat: warn by color when Material Setter has an invalid index
2024-08-30 18:04:58 -07:00
464 changed files with 32179 additions and 7889 deletions

14
.github/CHANGELOG-HEADER.md vendored Normal file
View File

@ -0,0 +1,14 @@
## [Unreleased]
### Added
### Fixed
### Changed
### Removed
### Security
### Deprecated

View File

@ -1,25 +1,25 @@
{
"dependencies": {
"com.vrchat.avatars": {
"version": "3.7.0"
"version": "3.7.4"
},
"nadena.dev.ndmf": {
"version": "1.4.0"
"version": "1.7.2-rc.0"
}
},
"locked": {
"com.vrchat.avatars": {
"version": "3.7.0",
"version": "3.7.4",
"dependencies": {
"com.vrchat.base": "3.7.0"
"com.vrchat.base": "3.7.4"
}
},
"com.vrchat.base": {
"version": "3.7.0",
"version": "3.7.4",
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.5.0-rc.1"
"version": "1.7.2-rc.0"
}
}
}

71
.github/cut-changelog.pl vendored Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/perl
use strict;
use warnings;
my ($changelog_file, $header_file, $version, $excerpt_file) = @ARGV;
open my $changelog, '<', $changelog_file or die "Can't open $changelog_file: $!";
open my $header, '<', $header_file or die "Can't open $header_file: $!";
open my $new_changelog, '>', "$changelog_file.new" or die "Can't open $changelog_file.new: $!";
if (!$excerpt_file) {
$excerpt_file = '/dev/null';
}
open my $excerpt, '>', $excerpt_file or die "Can't open $excerpt_file: $!";
# Copy all lines before the first "## "
while (my $line = <$changelog>) {
last if $line =~ /^## /;
print $new_changelog $line;
}
# Copy header into the output changelog
while (my $line = <$header>) {
print $new_changelog $line;
}
# Generate new header: ## [version] - [YYYY-mm-DD]
my $date = `date +%Y-%m-%d`;
chomp $date;
print $new_changelog "## [$version] - [$date]\n";
# Copy all lines until the next ## into both the new changelog and $excerpt.
# Prune any ###-sections that contain no content
my @buffered;
while (my $line = <$changelog>) {
if ($line =~ /^### /) {
@buffered = ($line);
} elsif ($line =~ /^\s*$/) {
if (@buffered) {
push @buffered, $line;
} else {
print $new_changelog $line;
print $excerpt $line;
}
} elsif ($line =~ /^## /) {
@buffered = ();
print $new_changelog $line;
last;
} else {
for my $buffered_line (@buffered){
print $new_changelog $buffered_line;
print $excerpt $buffered_line;
}
@buffered = ();
print $new_changelog $line;
print $excerpt $line;
}
}
# Copy remainder of changelog into new changelog
while (my $line = <$changelog>) {
print $new_changelog $line;
}
rename "$changelog_file.new", $changelog_file or die "Can't rename $changelog_file.new to $changelog_file: $!";

22
.github/gen-docs-changelog.pl vendored Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/perl
use strict;
use warnings;
# We want to skip two sections - the main header, then up to the first version header.
# In a prerelease, we only want to skip the first section (not including the unreleased header)
if ($ENV{PRERELEASE} eq 'false') {
while (<>) {
if (/^\## /) { last; }
}
}
while (<>) {
if (/^## /) { print; last; }
}
while (<>) {
print;
}

View File

@ -1,91 +0,0 @@
name: Build Release
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
- refactor-structure
tags:
- '**'
env:
packageName: "nadena.dev.modular-avatar"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout logo assets
uses: actions/checkout@v4
if: startsWith(github.ref, 'refs/tags/')
with:
repository: bdunderscore/modular-avatar-images
path: .github/image-assets
- name: Inject logo assets
if: startsWith(github.ref, 'refs/tags/')
run: |
cp -f .github/image-assets/img/logo/ma_logo.png Editor/Images/logo.png
cp -f .github/image-assets/img/logo/ma_icon.png Runtime/Icons/Icon_MA_Script.png
- name: Check semver syntax
id: semver-check
if: startsWith(github.ref, 'refs/tags/')
env:
REF_NAME: ${{ github.ref }}
run: |
if echo $REF_NAME | grep '[a-z]-[0-9]' && ! echo $REF_NAME | grep '^refs/tags/1\.5\.0-'; then
echo "Tag name does not follow semver prerelease syntax: $REF_NAME"
exit 1
fi
- name: get version
id: version
uses: notiz-dev/github-action-json-property@a5a9c668b16513c737c3e1f8956772c99c73f6e8
with:
path: "package.json"
prop_path: "version"
- name: Check tag consistency
if: startsWith(github.ref, 'refs/tags/')
run: |
if [ "${{ steps.version.outputs.prop }}" != "${GITHUB_REF##*/}" ]; then
echo "Version in package.json does not match tag name: ${{ steps.version.outputs.prop }} != ${GITHUB_REF##*/}"
exit 1
fi
- run: echo ${{steps.version.outputs.prop}}
- name: Set Environment Variables
run: |
echo "zipFile=${{ env.packageName }}-${{ steps.version.outputs.prop }}".zip >> $GITHUB_ENV
echo "zipFileSHA256=${{ env.packageName }}-${{ steps.version.outputs.prop }}".zip.sha256 >> $GITHUB_ENV
echo "unityPackage=${{ env.packageName }}-${{ steps.version.outputs.prop }}.unitypackage" >> $GITHUB_ENV
- name: Create Zip
run: |
zip ".github/${{env.zipFile}}" ./* -r -x .github .git '.git/*' '*~/*' '*.ps1*'
mv ".github/${{env.zipFile}}" "${{env.zipFile}}"
sha256sum "${{env.zipFile}}" > "${{env.zipFileSHA256}}"
- uses: actions/upload-artifact@v4
with:
name: package-zip
path: ${{ env.zipFile }}
- name: Make Release
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
tag_name: ${{ steps.version.outputs.prop }}
files: |
${{ env.zipFile }}
${{ env.zipFileSHA256 }}
package.json

View File

@ -23,6 +23,11 @@ on:
description: 'build the latest release'
type: boolean
required: false
prerelease:
description: 'use prerelease changelog'
type: boolean
required: false
default: true
jobs:
build-docs:
@ -67,6 +72,18 @@ jobs:
BASEURL="/${{ inputs.path }}/" perl -i -p -e "s{baseUrl: '/'}{baseUrl: '\$ENV{BASEURL}'}" docs~/docusaurus.config.js
cat docs~/docusaurus.config.js
- name: Format changelogs
run: |
SUFFIX=""
export PRERELEASE=${{ inputs.prerelease && 'true' || 'false' }}
if [ ${{ inputs.prerelease }} == true ]; then
SUFFIX="-PRERELEASE"
fi
perl -n .github/gen-docs-changelog.pl < CHANGELOG$SUFFIX.md >> docs~/docs/changelog.md
perl -n .github/gen-docs-changelog.pl < CHANGELOG$SUFFIX''-jp.md >> docs~/i18n/ja/docusaurus-plugin-content-docs/current/changelog.md
- name: Build docs
run: |
cd docs~

58
.github/workflows/changelog-check.yml vendored Normal file
View File

@ -0,0 +1,58 @@
# From https://github.com/anatawa12/AvatarOptimizer/blob/ccb863243433019f323c23a3a2e24b27e15b2f6c/.github/workflows/changelog-check.yml
# Copyright 2022 anatawa12
# MIT license.
# this workflow checks CHANGELOG.md & CHANGELOG-SNAPSHOTS.md is updated correctly
# to skip this check, include `NO-CHANGELOG` for CHANGELOG.md
# and `NO-CHANGELOG-PRERELEASE` for CHANGELOG-PRERELEASE.md in tags of PR.
# also, this action ignores `dependencies` pull requests (expected to be generated by dependabot)
name: CHANGELOG check
on:
pull_request_target:
branches: [ main, main-* ]
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
releasenote-check:
if: ${{ ! github.event.pull_request.draft }}
runs-on: ubuntu-latest
strategy:
matrix:
file: [CHANGELOG.md, CHANGELOG-jp.md, CHANGELOG-PRERELEASE.md, CHANGELOG-PRERELEASE-jp.md]
env:
NO_CHANGELOG: ${{
contains(github.event.pull_request.labels.*.name, 'NO-CHANGELOG')
|| contains(github.event.pull_request.labels.*.name, 'documentation')
|| contains(github.event.pull_request.labels.*.name, 'localization')
|| contains(github.event.pull_request.labels.*.name, 'ci')
|| contains(github.event.pull_request.labels.*.name, 'refactor')
|| startsWith(github.event.pull_request.head.label, 'bdunderscore:dependabot/')
|| '' }}
SNAPSHOT_ONLY: ${{ contains(github.event.pull_request.labels.*.name, 'PRERELEASE-ONLY') || '' }}
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Fetch pull_request info
env:
GH_REPO: ${{ github.repositoryUrl }}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
PR_NUM: ${{ github.event.number }}
run: |
gh pr view $PR_NUM --json=files | jq --raw-output '.files[].path' > files.txt
- name: Changelog check for ${{ matrix.file }}
if: always() && !env.NO_CHANGELOG && (startsWith(matrix.file, 'CHANGELOG-PRERELEASE') || !env.SNAPSHOT_ONLY)
run: |
if ! grep -e '^${{ matrix.file }}$' < files.txt > /dev/null; then
echo "::error::An entry in ${{ matrix.file }} is required for this PR."
echo "If this change is only relevant between snapshot versions: Add the label 'PRERELEASE-ONLY' to this PR." >> $GITHUB_STEP_SUMMARY
echo "If this change does not warrant any release notes: Add the label 'NO-CHANGELOG' to this PR." >> $GITHUB_STEP_SUMMARY
exit 1
fi

View File

@ -40,11 +40,13 @@ jobs:
build-docs:
name: Build documentation (latest release)
# TODO - update to build-docs.yml
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
needs:
- snapshot-docs
with:
ref: docs-snapshot
prerelease: false
build-docs-dev:
name: Build documentation (main branch)
@ -53,6 +55,7 @@ jobs:
ref: main
path: dev
artifact: docs-dev
prerelease: true
deploy-docs:
name: Deploy documentation
@ -122,7 +125,7 @@ jobs:
workingDirectory: docs-site~
- name: Purge cache
uses: nathanvaughn/actions-cloudflare-purge@367672c723960cd03bb7d8c2c4d89062a3fc1fac
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

View File

@ -110,12 +110,15 @@ jobs:
fi
done
- uses: anatawa12/sh-actions/resolve-vpm-packages@master
name: Resolve VPM packages
if: ${{ matrix.sdk == 'vrcsdk' && steps.setup.outputs.should_test == 'true' }}
with:
repos: |
https://vpm.nadena.dev/vpm-prerelease.json
- uses: anatawa12/sh-actions/setup-vrc-get@master
- name: Resolve packages
if: ${{ steps.setup.outputs.should_test == 'true' }}
run: |
vrc-get repo add -- "https://vpm.nadena.dev/vpm-prerelease.json" || true
vrc-get repo add -- "https://vrchat.github.io/packages/index.json?download" || true
vrc-get resolve --project .
vrc-get info project --project .
- if: ${{ steps.setup.outputs.should_test == 'true' }}
name: "Debug: List project contents"

231
.github/workflows/perform-release.yml vendored Normal file
View File

@ -0,0 +1,231 @@
name: Perform Release
# Portions of this workflow are based on https://github.com/anatawa12/AvatarOptimizer/blob/master/.github/workflows/release.yml
on:
workflow_dispatch:
inputs:
release_kind:
type: choice
description: The type of release.
default: prerelease
required: true
options:
- prerelease
- stable
- adhoc
publish:
description: "True to publish release to git, vpm. if false, this creates release asset only"
type: boolean
required: false
version:
description: "Version to release"
type: string
required: false
env:
PKG_NAME: nadena.dev.modular-avatar
RELEASE_TYPE: ${{ github.event.inputs.release_kind }}
concurrency:
group: publish
cancel-in-progress: true
permissions: write-all
jobs:
check-gameci:
uses: bdunderscore/modular-avatar/.github/workflows/gameci.yml@main
permissions:
checks: write
contents: read
secrets: inherit
check-docs:
name: Build documentation (latest release)
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
create-release:
needs: [ check-gameci, check-docs ]
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
# https://github.com/orgs/community/discussions/13836#discussioncomment-8535364
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_PRIVATE_KEY }}
- name: Validate prerelease version
id: check-version
if: ${{ github.event.inputs.release_kind == 'prerelease' && !contains(github.event.inputs.version, '-') }}
run:
echo "Prerelease version must contain a hyphen"
exit 1
- name: Validate stable version
id: check-version-stable
if: ${{ github.event.inputs.release_kind == 'stable' && contains(github.event.inputs.version, '-') }}
run:
echo "Stable version must not contain a hyphen"
exit 1
- name: Validate adhoc
id: validate-adhoc
if: ${{ github.event.inputs.release_kind == 'adhoc' && github.event.inputs.publish == 'true' }}
run:
echo "Adhoc release cannot be published"
exit 1
- name: Set Environment Variables
run: |
echo "zipFile=${{ env.PKG_NAME }}-${{ github.event.inputs.version }}".zip >> $GITHUB_ENV
echo "unityPackage=${{ env.PKG_NAME }}-${{ github.event.inputs.version }}.unitypackage" >> $GITHUB_ENV
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
case "$RELEASE_TYPE" in
prerelease)
echo "PRERELEASE=true" >> $GITHUB_ENV
;;
stable)
echo "PRERELEASE=false" >> $GITHUB_ENV
;;
adhoc)
echo "PRERELEASE=true" >> $GITHUB_ENV
;;
esac
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
- name: Checkout logo assets
uses: actions/checkout@v4
if: ${{ github.event.inputs.release_kind != 'adhoc' }}
with:
repository: bdunderscore/modular-avatar-images
path: .github/image-assets
- name: Inject logo assets
if: ${{ github.event.inputs.release_kind != 'adhoc' }}
run: |
cp -f .github/image-assets/img/logo/ma_logo.png Editor/Images/logo.png
cp -f .github/image-assets/img/logo/ma_icon.png Runtime/Icons/Icon_MA_Script.png
- name: Check semver syntax
if: steps.check-tag.outputs.need-new-tag == 'true'
id: semver-check
run: |
chmod +x .github/workflows/*.sh
.github/workflows/check-semver-syntax.sh ${{ github.event.inputs.version }}
- name: Set git user and email
id: git-config
run: |
git config --global user.name "nadena.dev release bot"
git config --global user.email "ci@nadena.dev"
- name: Update version
id: update-version
run: |
jq '.version = env.VERSION' package.json > package.json.tmp
mv package.json.tmp package.json
env:
VERSION: ${{ github.event.inputs.version }}
- name: Update changelog
id: changelog
run: |
chmod +x .github/*.pl
if [ "${{ env.PRERELEASE }}" == "true" ]; then
./.github/cut-changelog.pl CHANGELOG-PRERELEASE.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-en.md
./.github/cut-changelog.pl CHANGELOG-PRERELEASE-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-jp.md
else
./.github/cut-changelog.pl CHANGELOG-PRERELEASE.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }}
./.github/cut-changelog.pl CHANGELOG-PRERELEASE-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }}
./.github/cut-changelog.pl CHANGELOG.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-en.md
./.github/cut-changelog.pl CHANGELOG-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-jp.md
fi
echo Version ${{ env.VERSION }} > release-note.md
echo >> release-note.md
if [ "${{ env.PRERELEASE }}" == "true" ]; then
echo '**This is a prerelease version.** There may be bugs, and API compatibility is not yet guaranteed.' >> release-note.md
echo 'Please: **BACK UP YOUR PROJECTS**' >> release-note.md
echo >> release-note.md
fi
echo '## Notable changes' >> release-note.md
cat .github/relnote-en.md >> release-note.md
echo >> release-note.md
echo '## 主な変更点' >> release-note.md
cat .github/relnote-jp.md >> release-note.md
- name: Upload CHANGELOG.md
if: ${{ github.event.inputs.release_kind == 'stable' }}
uses: actions/upload-artifact@v4
with:
name: CHANGELOG
path: CHANGELOG.md
- name: Upload CHANGELOG-PRERELEASE.md
if: ${{ github.event.inputs.release_kind == 'prerelease' }}
uses: actions/upload-artifact@v4
with:
name: CHANGELOG-PRERELEASE
path: CHANGELOG-PRERELEASE.md
- name: Upload release note
uses: actions/upload-artifact@v4
with:
name: changelog
path: release-note.md
- run: mv release-note.md .github
- name: Commit and tag version update
run: |
git commit -am "Release ${{ github.event.inputs.version }}"
git tag -a ${{ github.event.inputs.version }} -m "Release ${{ github.event.inputs.version }}"
- name: Publish tag
if: ${{ github.event.inputs.publish == 'true' }}
run: |
BRANCH_NAME=$(git branch --show-current)
git push origin $BRANCH_NAME && git push origin ${{ github.event.inputs.version }}
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Create Zip
run: |
zip ".github/${{env.zipFile}}" ./* -r -x .github .git '.git/*' '*~/*' '*.ps1*'
- name: Move zipfile
run: |
mv .github/${{env.zipFile}} ${{env.zipFile}}
- uses: actions/upload-artifact@v4
with:
name: package-zip
path: ${{ env.zipFile }}
- name: Dump release notes
run: |
cat .github/release-note.md
- name: Make Release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda
if: ${{ github.event.inputs.publish == 'true' }}
with:
draft: true
body_path: .github/release-note.md
tag_name: ${{ github.event.inputs.version }}
name: ${{ github.event.inputs.version }}
make_latest: ${{ github.event.inputs.release_kind == 'stable' }}
files: |
${{ env.zipFile }}
package.json

102
CHANGELOG-PRERELEASE-jp.md Normal file
View File

@ -0,0 +1,102 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.2] - [2025-04-03]
### Fixed
- [#1537] アニメーターパラメーターをアニメーションさせるカーブが、`Merge Motion` コンポーネントを使用して追加された場合、
`Rename Parameters` によって更新されない問題を修正``
## [1.12.1] - [2025-04-02]
### Fixed
- [#1532] Modular Avatarが新しく作成したプロジェクトでコンパイラエラーを出す問題を修正
## [1.12.0] - [2025-04-01]
### Fixed
- [#1531] lylicalInventoryとの互換性問題を修正
### Changed
- [#1530] `MA Menu Item`の自動パラメーター機能も、オブジェクトのパスに基づいて名前を割り当てるようになりました。
## [1.12.0-rc.1] - [2025-03-28]
### Added
- [#1524] MMDワールド対応をアバター全体で無効にする機能を追加
### Fixed
- [#1522] `Convert Constraints` がアニメーション参照を変換できない問題を修正
- [#1528] `Merge Animator``アバターのWrite Defaults設定に合わせる` 設定を無視し、常に合わせてしまう問題を修正
### Changed
- [#1529] `MA Parameters` の自動リネームは、オブジェクトのパスに基づいて新しい名前を割り当てるように変更されました。これにより、
`MA Sync Parameter Sequence` との互換性が向上します。
- `MA Sync Parameter Sequence` を使用している場合は、このバージョンに更新した後、SyncedParamsアセットを空にして、
すべてのプラットフォームを再アップロードすることをお勧めします。
## [1.12.0-rc.0] - [2025-03-22]
### Fixed
- [#1508] テクスチャのサイズが4の倍数でない場合に、エクスプレッションメニューアイコンの自動圧縮が失敗する問題を修正
- [#1513] iOSビルドでエクスプレッションメニューアイコンの圧縮が壊れる問題を修正
### Changed
- [#1514] `Merge Blend Tree``Merge Motion (Blend Tree)` に改名され、アニメーションクリップにも対応するようになりました
## [1.12.0-beta.0] - [2025-03-17]
### Added
- [#1497] CHANGELOGをドキュメンテーションサイトに追加
- [#1482] `Merge Animator` に既存のアニメーターコントローラーを置き換える機能を追加
- [#1481] [World Scale Object](https://m-a.nadena.dev/dev/ja/docs/reference/world-scale-object)を追加
- [#1489] [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を追加
### Fixed
- [#1492] 前回のプレリリースでアイコンとロゴアセットが間違っていた問題を修正
- [#1501] MA Parametersコンポーネントのテキスト入力欄を編集する際にUnityのキーボードショートカットが機能しない問題を修正
- [#1410] 同期レイヤー内のモーションオーバーライドがBone Proxy/Merge Armatureオブジェクトの移動に対して更新されない問題を修正
- [#1504] 一部の状況で内部の`DelayDisable`レイヤーが不要なオブジェクトを参照しないように変更
- これにより、オブジェクトがアニメーションされているかどうかを追跡するAAOなどのツールとの互換性が向上します
### Changed
- [#1483] Merge Animator の 「アバターの Write Defaults 設定に合わせる」設定では、Additiveなレイヤー、および単一Stateかつ遷移のないレイヤー
 に対してはWrite Defaultsを調整しないように変更。
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。
- 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
アニメーションがブロックされたときにオーディオソースを無効にするように変更。
- [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。
詳細は[ドキュメント](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を参照してください。
- [#1502] `World Fixed Object``VRCParentConstraint` を使用するようになり、Androidビルドで使用可能になりました。
## [1.12.0-alpha.2] - [2025-03-10]
### Added
- Added CHANGELOG files
### Changed
- [#1476] ModularAvatarMergeAnimator と ModularAvatarMergeParameter を新しい NDMF API (`IVirtualizeMotion``IVirtualizeAnimatorController`) を使用するように変更
## Older versions
Please see CHANGELOG.md

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f89fef1421c4126b6086156ff536d8f
timeCreated: 1741573199

103
CHANGELOG-PRERELEASE.md Normal file
View File

@ -0,0 +1,103 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.2] - [2025-04-03]
### Fixed
- [#1537] Curves which animated animator parameters, when added using a `Merge Motion` component, would not be updated by
`Rename Parameters`
## [1.12.1] - [2025-04-02]
### Fixed
- [#1532] Modular Avatar has compiler errors in a newly created project
## [1.12.0] - [2025-04-01]
### Fixed
- [#1531] Fix compatibility issue with lylicalInventory
### Changed
- [#1530] `MA Menu Item` auto parameters now also assign names based on object paths
## [1.12.0-rc.1] - [2025-03-28]
### Added
- [#1524] Added support for disabling MMD world handling at an avatar level
### Fixed
- [#1522] `Convert Constraints` failed to convert animation references
- [#1528] `Merge Animator` ignored the `Match Avatar Write Defaults` setting and always matched
### Changed
- [#1529] `MA Parameters` auto-rename now assigns new names based on the path of the object. This should improve
compatibility with `MA Sync Parameter Sequence`
- If you are using `MA Sync Parameter Sequence`, it's a good idea to empty your SyncedParams asset and reupload all
platforms after updating to this version.
## [1.12.0-rc.0] - [2025-03-22]
### Fixed
- [#1508] Fix an issue where automatic compression of expressions menu icons would fail when the texture dimensions were
not divisible by four.
- [#1513] Expression menu icon compression broke on iOS builds
### Changed
- [#1514] `Merge Blend Tree` is now `Merge Motion (Blend Tree)` and supports merging animation clips as well as blend trees
## [1.12.0-beta.0] - [2025-03-17]
### Added
- [#1497] Added changelog to docs site
- [#1482] Added support for replacing pre-existing animator controllers to `Merge Animator`
- [#1481] Added [World Scale Object](https://m-a.nadena.dev/dev/docs/reference/world-scale-object)
- [#1489] Added [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)
### Fixed
- [#1492] Fixed incorrect icon and logo assets in prior prerelease
- [#1489] Fixed compatibility issues between `Merge Blend Tree` or reactive components and MMD worlds.
See [documentation](https://modular-avatar.nadena.dev/docs/general-behavior/mmd) for details on the new handling.
- [#1501] Unity keyboard shortcuts don't work when editing text fields on the MA Parameters component
- [#1410] Motion overrides on synced layers are not updated for Bone Proxy/Merge Armature object movement
- [#1504] The internal `DelayDisable` layer no longer references unnecessary objects in some situations
- This helps improve compatibility with AAO and other tools that track whether objects are animated
### Changed
- [#1483] The Merge Animator "Match Avatar Write Defaults" option will no longer adjust write defaults on states in
additive layers, or layers with only one state and no transitions.
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
to avoid it unintentionally being constantly active.
- [#1502] `World Fixed Object` now uses `VRCParentConstraint` and is therefore compatible with Android builds
## [1.12.0-alpha.2] - [2025-03-10]
### Added
- Added CHANGELOG files
### Changed
- [#1476] Switch ModularAvatarMergeAnimator and ModularAvatarMergeParameter to use new NDMF APIs (`IVirtualizeMotion` and `IVirtualizeAnimatorController`)
## Older versions
Please see CHANGELOG.md

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cb586a9c85634b8b81015d16899d797b
timeCreated: 1741571222

72
CHANGELOG-jp.md Normal file
View File

@ -0,0 +1,72 @@
# Changelog
Modular Avatarの主な変更点をこのファイルで記録しています。
なお、プレリリース版の変更点は `CHANGELOG-PRERELEASE.md` に記録されます。
この形式は [Keep a Changelog](https://keepachangelog.com/ja/1.0.0/) に基づいており、
このプロジェクトは [Semantic Versioning](https://semver.org/lang/ja/) に従っています。
## [Unreleased]
### Added
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.2] - [2025-04-03]
### Fixed
- [#1537] アニメーターパラメーターをアニメーションさせるカーブが、`Merge Motion` コンポーネントを使用して追加された場合、
`Rename Parameters` によって更新されない問題を修正``
## [1.12.1] - [2025-04-02]
### Fixed
- [#1532] Modular Avatarが新しく作成したプロジェクトでコンパイラエラーを出す問題を修正
## [1.12.0] - [2025-04-01]
### Added
- CHANGELOGファイルを追加
- [#1482] `Merge Animator` に既存のアニメーターコントローラーを置き換える機能を追加
- [#1481] [World Scale Object](https://m-a.nadena.dev/ja/docs/reference/world-scale-object)を追加
- [#1489] [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を追加
### Fixed
- [#1460] パラメーターアセットをMA Parametersにインポートするとき、ローカルのみのパラメーターが間違ってアニメーターのみ扱いになる問題を修正
- [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。
- 詳細は[ドキュメント](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を参照してください。
- この動作を無効にするには、新しい `MA VRChat Settings` コンポーネントをアバターの適当なところに追加して、適切な設定を無効にしてください。
- [#1501] MA Parametersコンポーネントのテキスト入力欄を編集する際にUnityのキーボードショートカットが機能しない問題を修正
- [#1410] 同期レイヤー内のモーションオーバーライドがBone Proxy/Merge Armatureオブジェクトの移動に対して更新されない問題を修正
- [#1504] 一部の状況で内部の`DelayDisable`レイヤーが不要なオブジェクトを参照しないように変更
- これにより、オブジェクトがアニメーションされているかどうかを追跡するAAOなどのツールとの互換性が向上します
- [#1508] テクスチャのサイズが4の倍数でない場合に、エクスプレッションメニューアイコンの自動圧縮が失敗する問題を修正
- [#1513] iOSビルドでエクスプレッションメニューアイコンの圧縮処理が壊れる問題を修正
### Changed
- [#1529] `MA Parameters` の自動リネームと `MA Menu Item` の自動パラメーター機能は、オブジェクトのパスに基づいて名前
を割り当てるように変更されました。
- `MA Sync Parameter Sequence` を使用している場合は、このバージョンに更新した後、SyncedParamsアセットを空にして、
すべてのプラットフォームを再アップロードすることをお勧めします。
- [#1514] `Merge Blend Tree``Merge Motion (Blend Tree)` に改名され、アニメーションクリップにも対応するようになりました
- [#1476] ModularAvatarMergeAnimator と ModularAvatarMergeParameter を新しい NDMF API (`IVirtualizeMotion``IVirtualizeAnimatorController`) を使用するように変更
- [#1483] Merge Animator の 「アバターの Write Defaults 設定に合わせる」設定では、Additiveなレイヤー、および単一Stateかつ遷移のないレイヤー
 に対してはWrite Defaultsを調整しないように変更。
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。
- 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
アニメーションがブロックされたときにオーディオソースを無効にするように変更。
- [#1502] `World Fixed Object``VRCParentConstraint` を使用するようになり、Androidビルドで使用可能になりました。
## それより前
GitHubのリリースページをご確認ください: https://github.com/bdunderscore/modular-avatar/releases

7
CHANGELOG-jp.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b27815ff13397374abcf9547a36bfaf4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1 +1,76 @@
Temporary test release
# Changelog
All notable changes to this project will be documented in this file.
Changes between prerelease versions will be documented in `CHANGELOG-PRERELEASE.md` instead.
[日本語版はこちらです。](CHANGELOG-jp.md)
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.2] - [2025-04-03]
### Fixed
- [#1537] Curves which animated animator parameters, when added using a `Merge Motion` component, would not be updated by
`Rename Parameters`
## [1.12.1] - [2025-04-02]
### Fixed
- [#1532] Modular Avatar has compiler errors in a newly created project
## [1.12.0] - [2025-04-01]
### Added
- Added CHANGELOG files
- [#1482] Added support for replacing pre-existing animator controllers to `Merge Animator`
- [#1481] Added [World Scale Object](https://m-a.nadena.dev/docs/reference/world-scale-object)
- [#1489] Added [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)
### Fixed
- [#1460] When importing parameter assets in MA Parameters, "local only" parameters were incorrectly treated as
"animator only"
- [#1489] Fixed compatibility issues between `Merge Blend Tree` or reactive components and MMD worlds.
- See [documentation](https://modular-avatar.nadena.dev/docs/general-behavior/mmd) for details on the new handling.
- To disable this behavior, attach the new `MA VRChat Settings` component to any object on your avatar and disable the appropriate setting.
- [#1501] Unity keyboard shortcuts don't work when editing text fields on the MA Parameters component
- [#1410] Motion overrides on synced layers are not updated for Bone Proxy/Merge Armature object movement
- [#1504] The internal `DelayDisable` layer no longer references unnecessary objects in some situations
- This helps improve compatibility with AAO and other tools that track whether objects are animated
- [#1508] Fix an issue where automatic compression of expressions menu icons would fail when the texture dimensions were
not divisible by four.
- [#1513] Expression menu icon compression broke on iOS builds
### Changed
- [#1529] `MA Parameters` auto-rename and `MA Menu Item`'s automatic parameter feature now assign names based on the
path of the object. This should improve compatibility with `MA Sync Parameter Sequence`
- If you are using `MA Sync Parameter Sequence`, it's a good idea to empty your SyncedParams asset and reupload all
platforms after updating to this version.
- [#1514] `Merge Blend Tree` is now `Merge Motion (Blend Tree)` and supports merging animation clips as well as blend trees
- [#1476] Switch ModularAvatarMergeAnimator and ModularAvatarMergeParameter to use new NDMF APIs (`IVirtualizeMotion` and `IVirtualizeAnimatorController`)
- [#1483] The Merge Animator "Match Avatar Write Defaults" option will no longer adjust write defaults on states in
additive layers, or layers with only one state and no transitions.
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
to avoid it unintentionally being constantly active.
- [#1502] `World Fixed Object` now uses `VRCParentConstraint` and is therefore compatible with Android builds
## Older versions
Please see the github releases page at https://github.com/bdunderscore/modular-avatar/releases

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEngine;
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
@ -16,7 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly BuildContext _context;
private readonly BoneDatabase _boneDatabase;
private readonly PathMappings _pathMappings;
private readonly AnimatorServicesContext _asc;
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
/// <summary>
@ -55,15 +56,15 @@ namespace nadena.dev.modular_avatar.core.editor
{
_context = context;
_boneDatabase = boneDatabase;
_pathMappings = context.PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
while (root != null && !RuntimeUtil.IsAvatarRoot(root))
{
var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject);
System.Diagnostics.Debug.Assert(originalPath != null);
if (context.AnimationDatabase.ClipsForPath(originalPath).Any(clip =>
GetActiveBinding(clip.CurrentClip as AnimationClip, originalPath) != null
if (_asc.AnimationIndex.GetClipsForObjectPath(originalPath).Any(clip =>
GetActiveBinding(clip, originalPath) != null
))
{
_intermediateObjs.Add(new IntermediateObj
@ -118,7 +119,6 @@ namespace nadena.dev.modular_avatar.core.editor
// Ensure mesh retargeting looks through this
_boneDatabase.AddMergedBone(sourceBone.transform);
_boneDatabase.RetainMergedBone(sourceBone.transform);
_pathMappings.MarkTransformLookthrough(sourceBone);
}
return sourceBone;
@ -130,22 +130,14 @@ namespace nadena.dev.modular_avatar.core.editor
{
var path = intermediate.OriginalPath;
foreach (var holder in _context.AnimationDatabase.ClipsForPath(path))
foreach (var clip in _asc.AnimationIndex.GetClipsForObjectPath(path))
{
if (!_context.PluginBuildContext.IsTemporaryAsset(holder.CurrentClip))
{
holder.CurrentClip = Object.Instantiate(holder.CurrentClip);
}
var clip = holder.CurrentClip as AnimationClip;
if (clip == null) continue;
var curve = GetActiveBinding(clip, path);
if (curve != null)
{
foreach (var mapping in intermediate.Created)
{
clip.SetCurve(_pathMappings.GetObjectIdentifier(mapping), typeof(GameObject), "m_IsActive",
clip.SetFloatCurve(_asc.ObjectPathRemapper.GetVirtualPathForObject(mapping), typeof(GameObject), "m_IsActive",
curve);
}
}
@ -153,10 +145,9 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private AnimationCurve GetActiveBinding(AnimationClip clip, string path)
private AnimationCurve GetActiveBinding(VirtualClip clip, string path)
{
return AnimationUtility.GetEditorCurve(clip,
EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
return clip.GetFloatCurve(EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
}
}
}

View File

@ -1,383 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// The animation database records the set of all clips which are used in the avatar, and which paths they
/// manipulate.
/// </summary>
internal class AnimationDatabase
{
internal class ClipHolder
{
private readonly AnimationDatabase ParentDatabase;
private Motion _currentClip;
internal Motion CurrentClip
{
get
{
ParentDatabase.InvalidateCaches();
return _currentClip;
}
set
{
ParentDatabase.InvalidateCaches();
_currentClip = value;
}
}
private Motion _originalClip;
internal Motion OriginalClip
{
get => _originalClip;
set
{
_originalClip = value;
IsProxyAnimation = value != null && Util.IsProxyAnimation(value);
}
}
internal bool IsProxyAnimation { private set; get; }
internal ClipHolder(AnimationDatabase parentDatabase, Motion clip)
{
ParentDatabase = parentDatabase;
CurrentClip = OriginalClip = clip;
}
/// <summary>
/// Returns the current clip without invalidating caches. Do not modify this clip without taking extra
/// steps to invalidate caches on the AnimationDatabase.
/// </summary>
/// <returns></returns>
internal Motion GetCurrentClipUnsafe()
{
return _currentClip;
}
public void SetCurrentNoInvalidate(Motion newMotion)
{
_currentClip = newMotion;
}
}
private BuildContext _context;
private List<Action> _clipCommitActions = new List<Action>();
private List<ClipHolder> _clips = new List<ClipHolder>();
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
private HashSet<VRCAnimatorPlayAudio> _playAudios = new HashSet<VRCAnimatorPlayAudio>();
#endif
private Dictionary<string, HashSet<ClipHolder>> _pathToClip = null;
internal AnimationDatabase()
{
Debug.Log("Creating animation database");
}
internal void Commit()
{
foreach (var clip in _clips)
{
if (clip.IsProxyAnimation) clip.CurrentClip = clip.OriginalClip;
}
foreach (var clip in _clips)
{
// Changing the "high quality curve" setting can result in behavior changes (but can happen accidentally
// as we manipulate curves)
if (clip.CurrentClip != clip.OriginalClip && clip.CurrentClip != null && clip.OriginalClip != null)
{
SerializedObject before = new SerializedObject(clip.OriginalClip);
SerializedObject after = new SerializedObject(clip.CurrentClip);
var before_prop = before.FindProperty("m_UseHighQualityCurve");
var after_prop = after.FindProperty("m_UseHighQualityCurve");
if (after_prop.boolValue != before_prop.boolValue)
{
after_prop.boolValue = before_prop.boolValue;
after.ApplyModifiedPropertiesWithoutUndo();
}
}
}
foreach (var action in _clipCommitActions)
{
action();
}
}
internal void OnActivate(BuildContext context)
{
_context = context;
AnimationUtil.CloneAllControllers(context);
#if MA_VRCSDK3_AVATARS
var avatarDescriptor = context.AvatarDescriptor;
foreach (var layer in avatarDescriptor.baseAnimationLayers)
{
BootstrapLayer(layer);
}
foreach (var layer in avatarDescriptor.specialAnimationLayers)
{
BootstrapLayer(layer);
}
void BootstrapLayer(VRCAvatarDescriptor.CustomAnimLayer layer)
{
if (!layer.isDefault && layer.animatorController is AnimatorController ac &&
context.IsTemporaryAsset(ac))
{
BuildReport.ReportingObject(ac, () =>
{
foreach (var state in Util.States(ac))
{
RegisterState(state);
}
});
}
}
#endif
}
/// <summary>
/// Registers a motion and all its reachable submotions with the animation database. The processClip callback,
/// if provided, will be invoked for each newly discovered clip.
/// </summary>
/// <param name="state"></param>
/// <param name="processClip"></param>
/// <exception cref="Exception"></exception>
internal void RegisterState(AnimatorState state, Action<ClipHolder> processClip = null)
{
Dictionary<Motion, ClipHolder> _originalToHolder = new Dictionary<Motion, ClipHolder>();
if (processClip == null) processClip = (_) => { };
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
foreach (var behavior in state.behaviours)
{
if (behavior is VRCAnimatorPlayAudio playAudio)
{
_playAudios.Add(playAudio);
}
}
#endif
if (state.motion == null) return;
var clipHolder = RegisterMotion(state.motion, state, processClip, _originalToHolder);
state.motion = clipHolder.CurrentClip;
_clipCommitActions.Add(() => { state.motion = clipHolder.CurrentClip; });
}
internal void ForeachClip(Action<ClipHolder> processClip)
{
foreach (var clipHolder in _clips)
{
processClip(clipHolder);
}
}
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
internal void ForeachPlayAudio(Action<VRCAnimatorPlayAudio> processPlayAudio)
{
foreach (var playAudioHolder in _playAudios)
{
processPlayAudio(playAudioHolder);
}
}
#endif
/// <summary>
/// Returns a list of clips which touched the given _original_ path. This path is subject to basepath remapping,
/// but not object movement remapping.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal ImmutableArray<ClipHolder> ClipsForPath(string path)
{
HydrateCaches();
if (_pathToClip.TryGetValue(path, out var clips))
{
return clips.ToImmutableArray();
}
else
{
return ImmutableArray<ClipHolder>.Empty;
}
}
private ClipHolder RegisterMotion(
Motion motion,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (motion == null)
{
return new ClipHolder(this, null);
}
if (originalToHolder.TryGetValue(motion, out var holder))
{
return holder;
}
InvalidateCaches();
Motion cloned = motion;
if (!_context.IsTemporaryAsset(motion))
{
// Protect the original animations from mutations by creating temporary clones; in the case of a proxy
// animation, we'll restore the original in a later pass
// cloned = Object.Instantiate(motion); - Object.Instantiate can't be used on AnimationClips and BlendTrees
cloned = (Motion)motion.GetType().GetConstructor(new Type[0]).Invoke(new object[0]);
EditorUtility.CopySerialized(motion, cloned);
ObjectRegistry.RegisterReplacedObject(motion, cloned);
}
switch (cloned)
{
case AnimationClip clip:
{
holder = new ClipHolder(this, clip);
processClip(holder);
_clips.Add(holder);
break;
}
case BlendTree tree:
{
holder = RegisterBlendtree(tree, state, processClip, originalToHolder);
break;
}
}
holder.OriginalClip = motion;
originalToHolder[motion] = holder;
return holder;
}
private void InvalidateCaches()
{
_pathToClip = null;
}
private void HydrateCaches()
{
if (_pathToClip == null)
{
_pathToClip = new Dictionary<string, HashSet<ClipHolder>>();
foreach (var clip in _clips)
{
RecordPaths(clip);
}
}
}
private void RecordPaths(ClipHolder holder)
{
var clip = holder.GetCurrentClipUnsafe() as AnimationClip;
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
void AddPath(string p0)
{
if (!_pathToClip.TryGetValue(p0, out var clips))
{
clips = new HashSet<ClipHolder>();
_pathToClip[p0] = clips;
}
clips.Add(holder);
}
}
private ClipHolder RegisterBlendtree(
BlendTree tree,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (!_context.IsTemporaryAsset(tree))
{
throw new Exception("Blendtree must be a temporary asset");
}
var treeHolder = new ClipHolder(this, tree);
var children = tree.children;
var holders = new ClipHolder[children.Length];
for (int i = 0; i < children.Length; i++)
{
holders[i] = RegisterMotion(children[i].motion, state, processClip, originalToHolder);
children[i].motion = holders[i].CurrentClip;
}
tree.children = children;
_clipCommitActions.Add(() =>
{
var dirty = false;
for (int i = 0; i < children.Length; i++)
{
var curClip = holders[i].CurrentClip;
if (children[i].motion != curClip)
{
children[i].motion = curClip;
dirty = true;
}
}
if (dirty)
{
tree.children = children;
EditorUtility.SetDirty(tree);
}
});
return treeHolder;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 11130986120e452b8dc8db0d19aa71fc
timeCreated: 1671624207

View File

@ -1,119 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// This extension context amortizes a number of animation-related processing steps - notably,
/// collecting the set of all animation clips from the animators, and committing changes to them
/// in a deferred manner.
///
/// Restrictions: While this context is active, any changes to clips must be done by editing them via
/// ClipHolders in the AnimationDatabase. Any newly added clips must be registered in the AnimationDatabase,
/// and any new references to clips require setting appropriate ClipCommitActions.
///
/// New references to objects created in clips must use paths obtained from the
/// ObjectRenameTracker.GetObjectIdentifier method.
/// </summary>
internal sealed class AnimationServicesContext : IExtensionContext
{
private BuildContext _context;
private AnimationDatabase _animationDatabase;
private PathMappings _pathMappings;
private ReadableProperty _readableProperty;
private Dictionary<GameObject, string> _selfProxies = new();
public void OnActivate(BuildContext context)
{
_context = context;
_animationDatabase = new AnimationDatabase();
_animationDatabase.OnActivate(context);
_pathMappings = new PathMappings();
_pathMappings.OnActivate(context, _animationDatabase);
_readableProperty = new ReadableProperty(_context, _animationDatabase, this);
}
public void OnDeactivate(BuildContext context)
{
_pathMappings.OnDeactivate(context);
_animationDatabase.Commit();
_pathMappings = null;
_animationDatabase = null;
}
public AnimationDatabase AnimationDatabase
{
get
{
if (_animationDatabase == null)
{
throw new InvalidOperationException(
"AnimationDatabase is not available outside of the AnimationServicesContext");
}
return _animationDatabase;
}
}
public PathMappings PathMappings
{
get
{
if (_pathMappings == null)
{
throw new InvalidOperationException(
"ObjectRenameTracker is not available outside of the AnimationServicesContext");
}
return _pathMappings;
}
}
public IEnumerable<(EditorCurveBinding, string)> BoundReadableProperties => _readableProperty.BoundProperties;
// HACK: This is a temporary crutch until we rework the entire animator services system
public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
{
var fx = (AnimatorController)
_context.AvatarDescriptor.baseAnimationLayers
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
.animatorController;
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
}
public string GetActiveSelfProxy(GameObject obj)
{
if (_selfProxies.TryGetValue(obj, out var paramName) && !string.IsNullOrEmpty(paramName)) return paramName;
var path = PathMappings.GetObjectIdentifier(obj);
paramName = _readableProperty.ForActiveSelf(path);
_selfProxies[obj] = paramName;
return paramName;
}
public bool ObjectHasAnimations(GameObject obj)
{
var path = PathMappings.GetObjectIdentifier(obj);
var clips = AnimationDatabase.ClipsForPath(path);
return clips != null && !clips.IsEmpty;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: c2c26040d44d4dacb838aceced3b3e52
timeCreated: 1696063949

View File

@ -1,220 +0,0 @@
#region
using System;
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
internal static class AnimationUtil
{
private const string SAMPLE_PATH_PACKAGE =
"Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers";
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92";
public static AnimatorController DeepCloneAnimator(BuildContext context, RuntimeAnimatorController controller)
{
if (controller == null) return null;
var merger = new AnimatorCombiner(context, controller.name + " (cloned)");
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
return merger.Finish();
}
internal static void CloneAllControllers(BuildContext context)
{
// Ensure all of the controllers on the avatar descriptor point to temporary assets.
// This helps reduce the risk that we'll accidentally modify the original assets.
#if MA_VRCSDK3_AVATARS
context.AvatarDescriptor.baseAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.baseAnimationLayers);
context.AvatarDescriptor.specialAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.specialAnimationLayers);
#endif
}
#if MA_VRCSDK3_AVATARS
private static VRCAvatarDescriptor.CustomAnimLayer[] CloneLayers(
BuildContext context,
VRCAvatarDescriptor.CustomAnimLayer[] layers
)
{
if (layers == null) return null;
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
if (layer.animatorController != null && !context.IsTemporaryAsset(layer.animatorController))
{
layer.animatorController = DeepCloneAnimator(context, layer.animatorController);
}
layers[i] = layer;
}
return layers;
}
public static AnimatorController GetOrInitializeController(
this BuildContext context,
VRCAvatarDescriptor.AnimLayerType type)
{
return FindLayer(context.AvatarDescriptor.baseAnimationLayers)
?? FindLayer(context.AvatarDescriptor.specialAnimationLayers);
AnimatorController FindLayer(VRCAvatarDescriptor.CustomAnimLayer[] layers)
{
for (int i = 0; i < layers.Length; i++)
{
var layer = layers[i];
if (layer.type == type)
{
if (layer.animatorController == null || layer.isDefault)
{
layer.animatorController = ResolveLayerController(layer);
if (type == VRCAvatarDescriptor.AnimLayerType.Gesture)
{
layer.mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(
AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK)
);
}
layers[i] = layer;
}
return layer.animatorController as AnimatorController;
}
}
return null;
}
}
private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer)
{
AnimatorController controller = null;
if (!layer.isDefault && layer.animatorController != null &&
layer.animatorController is AnimatorController c)
{
controller = c;
}
else
{
string name;
switch (layer.type)
{
case VRCAvatarDescriptor.AnimLayerType.Action:
name = "Action";
break;
case VRCAvatarDescriptor.AnimLayerType.Additive:
name = "Idle";
break;
case VRCAvatarDescriptor.AnimLayerType.Base:
name = "Locomotion";
break;
case VRCAvatarDescriptor.AnimLayerType.Gesture:
name = "Hands";
break;
case VRCAvatarDescriptor.AnimLayerType.Sitting:
name = "Sitting";
break;
case VRCAvatarDescriptor.AnimLayerType.FX:
name = "Face";
break;
case VRCAvatarDescriptor.AnimLayerType.TPose:
name = "UtilityTPose";
break;
case VRCAvatarDescriptor.AnimLayerType.IKPose:
name = "UtilityIKPose";
break;
default:
name = null;
break;
}
if (name != null)
{
name = "/vrc_AvatarV3" + name + "Layer.controller";
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
}
}
}
return controller;
}
#endif
public static bool IsProxyAnimation(this Motion m)
{
var path = AssetDatabase.GetAssetPath(m);
// This is a fairly wide condition in order to deal with:
// 1. Future additions of proxy animations (so GUIDs are out)
// 2. Unitypackage based installations of the VRCSDK
// 3. VCC based installations of the VRCSDK
// 4. Very old VCC based installations of the VRCSDK where proxy animations were copied into Assets
return path.Contains("/AV3 Demo Assets/Animation/ProxyAnim/proxy")
|| path.Contains("/VRCSDK/Examples3/Animation/ProxyAnim/proxy")
|| path.StartsWith("Packages/com.vrchat.");
}
/// <summary>
/// Enumerates all state machines and sub-state machines starting from a specific starting ASM
/// </summary>
/// <param name="ac"></param>
/// <returns></returns>
internal static IEnumerable<AnimatorStateMachine> ReachableStateMachines(this AnimatorStateMachine asm)
{
HashSet<AnimatorStateMachine> visitedStateMachines = new HashSet<AnimatorStateMachine>();
Queue<AnimatorStateMachine> pending = new Queue<AnimatorStateMachine>();
pending.Enqueue(asm);
while (pending.Count > 0)
{
var next = pending.Dequeue();
if (visitedStateMachines.Contains(next)) continue;
visitedStateMachines.Add(next);
foreach (var child in next.stateMachines)
{
if (child.stateMachine != null) pending.Enqueue(child.stateMachine);
}
yield return next;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: af583e8ac3104fa4f8466741614219a0
timeCreated: 1691238553

View File

@ -1,615 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 bd_
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#region
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
internal class AnimatorCombiner
{
private readonly BuildContext _context;
private readonly AnimatorController _combined;
private readonly DeepClone _deepClone;
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
private Dictionary<String, AnimatorControllerParameter> _parameters =
new Dictionary<string, AnimatorControllerParameter>();
private Dictionary<String, AnimatorController> _parameterSource =
new Dictionary<string, AnimatorController>();
private Dictionary<KeyValuePair<String, Motion>, Motion> _motions =
new Dictionary<KeyValuePair<string, Motion>, Motion>();
private Dictionary<KeyValuePair<String, AnimatorStateMachine>, AnimatorStateMachine> _stateMachines =
new Dictionary<KeyValuePair<string, AnimatorStateMachine>, AnimatorStateMachine>();
private Dictionary<Object, Object> _cloneMap;
private int _controllerBaseLayer = 0;
#if MA_VRCSDK3_AVATARS
public VRC_AnimatorLayerControl.BlendableLayer? BlendableLayer;
#endif
public AnimatorCombiner(BuildContext context, String assetName)
{
_combined = new AnimatorController();
if (context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer))
{
AssetDatabase.AddObjectToAsset(_combined, context.AssetContainer);
}
_combined.name = assetName;
_context = context;
_deepClone = new DeepClone(context);
}
public AnimatorController Finish()
{
FixTransitionTypeConflicts();
PruneEmptyLayers();
_combined.parameters = _parameters.Values.ToArray();
_combined.layers = _layers.ToArray();
return _combined;
}
public void MergeTypes(Dictionary<string, AnimatorControllerParameterType> types)
{
foreach (var p in _parameters.ToList())
{
if (types.TryGetValue(p.Key, out var outerValue))
{
if (outerValue == p.Value.type) continue;
if (outerValue == AnimatorControllerParameterType.Trigger
|| p.Value.type == AnimatorControllerParameterType.Trigger)
{
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
p.Key,
p.Value.type,
outerValue
);
}
_parameters[p.Key].type = AnimatorControllerParameterType.Float;
types[p.Key] = AnimatorControllerParameterType.Float;
}
else
{
types.Add(p.Key, p.Value.type);
}
}
}
/// <summary>
/// When we merge multiple controllers with different types for the same parameter, we merge
/// them all into using floats; thanks to VRChat's implicit typecasting, we can do this even for
/// parameters registered as being ints or bools in the expressions parameter asset. However,
/// we do need to fix any transitions to use the right transition types after this conversion.
/// </summary>
private void FixTransitionTypeConflicts()
{
foreach (var layer in _layers)
{
foreach (var asm in layer.stateMachine.ReachableStateMachines())
{
foreach (ChildAnimatorState s in asm.states)
{
s.state.transitions = s.state.transitions.SelectMany(FixupTransition).ToArray();
}
asm.entryTransitions = asm.entryTransitions
.SelectMany(FixupTransition).ToArray();
asm.anyStateTransitions = asm.anyStateTransitions
.SelectMany(FixupTransition).ToArray();
foreach (var stateMachine in asm.stateMachines)
{
var ssm = stateMachine.stateMachine;
var stateMachineTransitions = asm.GetStateMachineTransitions(ssm);
if (stateMachineTransitions.Length > 0)
{
asm.SetStateMachineTransitions(ssm,
stateMachineTransitions.SelectMany(FixupTransition).ToArray());
}
}
}
}
}
private IEnumerable<T> FixupTransition<T>(T t) where T: AnimatorTransitionBase, new()
{
if (!NeedsFixing(t.conditions))
{
yield return t;
yield break;
}
AnimatorCondition[][][] combinations = t.conditions.Select(c => FixupCondition(c).ToArray()).ToArray();
// Generate the combinatorial explosion of conditions needed to emulate NotEquals with floats...
var conditions = ExplodeConditions(combinations).ToArray();
if (conditions.Length == 1)
{
t.conditions = conditions[0];
yield return t;
}
else
{
foreach (var conditionGroup in conditions)
{
t.conditions = conditionGroup;
yield return t;
var newTransition = new T();
EditorUtility.CopySerialized(t, newTransition);
if (_context.AssetContainer != null)
{
AssetDatabase.AddObjectToAsset(newTransition, _context.AssetContainer);
}
t = newTransition;
}
}
}
private bool NeedsFixing(AnimatorCondition[] conditions)
{
return conditions.Any(c =>
{
if (!_parameters.TryGetValue(c.parameter, out var param)) return false;
switch (c.mode)
{
case AnimatorConditionMode.If when param.type != AnimatorControllerParameterType.Bool:
case AnimatorConditionMode.IfNot when param.type != AnimatorControllerParameterType.Bool:
case AnimatorConditionMode.Equals when param.type != AnimatorControllerParameterType.Int:
case AnimatorConditionMode.NotEqual when param.type != AnimatorControllerParameterType.Int:
return true;
default:
return false;
}
});
}
private IEnumerable<AnimatorCondition[]> ExplodeConditions(AnimatorCondition[][][] conditions)
{
int[] indices = new int[conditions.Length];
while (true)
{
yield return conditions.SelectMany((group, i_) => group[indices[i_]]).ToArray();
// Increment the rightmost possible counter
int i;
for (i = indices.Length - 1; i >= 0; i--)
{
if (indices[i] < conditions[i].Length - 1)
{
indices[i]++;
// Unity 2019.....
// System.Array.Fill(indices, 0, i + 1, indices.Length - i - 1);
for (int j = i + 1; j < indices.Length; j++)
{
indices[j] = 0;
}
break;
}
}
if (i < 0) break;
}
}
private IEnumerable<AnimatorCondition[]> FixupCondition(AnimatorCondition c)
{
if (!_parameters.TryGetValue(c.parameter, out var paramDef))
{
// Parameter is undefined, don't touch this condition
yield return new[] { c };
yield break;
}
switch (c.mode)
{
case AnimatorConditionMode.If when paramDef.type == AnimatorControllerParameterType.Float:
{
c.mode = AnimatorConditionMode.Greater;
c.threshold = 0.5f;
yield return new[] { c };
break;
}
case AnimatorConditionMode.IfNot when paramDef.type == AnimatorControllerParameterType.Float:
{
c.mode = AnimatorConditionMode.Less;
c.threshold = 0.5f;
yield return new[] { c };
break;
}
case AnimatorConditionMode.Equals when paramDef.type == AnimatorControllerParameterType.Float:
{
var c1 = c;
var c2 = c;
c1.mode = AnimatorConditionMode.Greater;
c1.threshold -= 0.1f;
c2.mode = AnimatorConditionMode.Less;
c2.threshold += 0.1f;
yield return new[] { c1, c2 };
break;
}
case AnimatorConditionMode.NotEqual when paramDef.type == AnimatorControllerParameterType.Float:
{
var origThresh = c.threshold;
c.mode = AnimatorConditionMode.Greater;
c.threshold = origThresh + 0.1f;
yield return new[] { c };
c.mode = AnimatorConditionMode.Less;
c.threshold = origThresh - 0.1f;
yield return new[] { c };
break;
}
default:
yield return new[] { c };
break;
}
}
private void PruneEmptyLayers()
{
var originalLayers = _layers;
int[] layerIndexMappings = new int[originalLayers.Count];
List<AnimatorControllerLayer> newLayers = new List<AnimatorControllerLayer>();
for (int i = 0; i < originalLayers.Count; i++)
{
if (i > 0 && IsEmptyLayer(originalLayers[i]))
{
layerIndexMappings[i] = -1;
}
else
{
layerIndexMappings[i] = newLayers.Count;
newLayers.Add(originalLayers[i]);
}
}
foreach (var layer in newLayers)
{
if (layer.stateMachine == null) continue;
foreach (var asset in layer.stateMachine.ReferencedAssets(includeScene: false))
{
if (asset is AnimatorState alc)
{
alc.behaviours = AdjustStateBehaviors(alc.behaviours);
}
else if (asset is AnimatorStateMachine asm)
{
asm.behaviours = AdjustStateBehaviors(asm.behaviours);
}
}
}
_layers = newLayers;
StateMachineBehaviour[] AdjustStateBehaviors(StateMachineBehaviour[] behaviours)
{
if (behaviours.Length == 0) return behaviours;
var newBehaviors = new List<StateMachineBehaviour>();
foreach (var b in behaviours)
{
switch (b)
{
#if MA_VRCSDK3_AVATARS
case VRCAnimatorLayerControl alc when alc.playable == BlendableLayer:
int newLayer = -1;
if (alc.layer >= 0 && alc.layer < layerIndexMappings.Length)
{
newLayer = layerIndexMappings[alc.layer];
}
if (newLayer != -1)
{
alc.layer = newLayer;
newBehaviors.Add(alc);
}
break;
#endif
default:
newBehaviors.Add(b);
break;
}
}
return newBehaviors.ToArray();
}
}
private bool IsEmptyLayer(AnimatorControllerLayer layer)
{
if (layer.syncedLayerIndex >= 0) return false;
if (layer.avatarMask != null) return false;
return layer.stateMachine == null
|| (layer.stateMachine.states.Length == 0 && layer.stateMachine.stateMachines.Length == 0);
}
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults,
bool forceFirstLayerWeight = false)
{
_controllerBaseLayer = _layers.Count;
_cloneMap = new Dictionary<Object, Object>();
foreach (var param in controller.parameters)
{
if (_parameters.TryGetValue(param.name, out var acp))
{
if (acp.type == param.type) continue;
if (acp.type != param.type &&
(acp.type == AnimatorControllerParameterType.Trigger ||
param.type == AnimatorControllerParameterType.Trigger))
{
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
param.name,
acp.type.ToString(),
param.type.ToString(),
controller,
_parameterSource[param.name]
);
}
acp.type = AnimatorControllerParameterType.Float;
continue;
}
var clonedParameter = new AnimatorControllerParameter()
{
name = param.name,
type = param.type,
defaultBool = param.defaultBool,
defaultFloat = param.defaultFloat,
defaultInt = param.defaultInt
};
_parameters.Add(param.name, clonedParameter);
_parameterSource.Add(param.name, controller);
}
bool first = true;
var layers = controller.layers;
foreach (var layer in layers)
{
insertLayer(basePath, layer, first, writeDefaults, layers);
if (first && forceFirstLayerWeight)
{
_layers[_layers.Count - 1].defaultWeight = 1;
}
first = false;
}
}
public void AddOverrideController(string basePath, AnimatorOverrideController overrideController,
bool? writeDefaults)
{
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
if (controller == null) return;
_deepClone.OverrideController = overrideController;
try
{
this.AddController(basePath, controller, writeDefaults);
}
finally
{
}
}
private void insertLayer(
string basePath,
AnimatorControllerLayer layer,
bool first,
bool? writeDefaults,
AnimatorControllerLayer[] layers
)
{
var newLayer = new AnimatorControllerLayer()
{
name = layer.name,
avatarMask = layer.avatarMask, // TODO map transforms
blendingMode = layer.blendingMode,
defaultWeight = first ? 1 : layer.defaultWeight,
syncedLayerIndex = layer.syncedLayerIndex,
syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming,
iKPass = layer.iKPass,
stateMachine = mapStateMachine(basePath, layer.stateMachine),
};
UpdateWriteDefaults(newLayer.stateMachine, writeDefaults);
if (newLayer.syncedLayerIndex != -1 && newLayer.syncedLayerIndex >= 0 &&
newLayer.syncedLayerIndex < layers.Length)
{
// Transfer any motion overrides onto the new synced layer
var baseLayer = layers[newLayer.syncedLayerIndex];
foreach (var state in WalkAllStates(baseLayer.stateMachine))
{
var overrideMotion = layer.GetOverrideMotion(state);
if (overrideMotion != null)
{
newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], overrideMotion);
}
var overrideBehaviors = (StateMachineBehaviour[])layer.GetOverrideBehaviours(state)?.Clone();
if (overrideBehaviors != null)
{
for (int i = 0; i < overrideBehaviors.Length; i++)
{
overrideBehaviors[i] = _deepClone.DoClone(overrideBehaviors[i]);
AdjustBehavior(overrideBehaviors[i], basePath);
}
newLayer.SetOverrideBehaviours((AnimatorState)_cloneMap[state], overrideBehaviors);
}
}
newLayer.syncedLayerIndex += _controllerBaseLayer;
}
_layers.Add(newLayer);
}
IEnumerable<AnimatorState> WalkAllStates(AnimatorStateMachine animatorStateMachine)
{
HashSet<Object> visited = new HashSet<Object>();
foreach (var state in VisitStateMachine(animatorStateMachine))
{
yield return state;
}
IEnumerable<AnimatorState> VisitStateMachine(AnimatorStateMachine layerStateMachine)
{
if (!visited.Add(layerStateMachine)) yield break;
foreach (var state in layerStateMachine.states)
{
if (state.state == null) continue;
yield return state.state;
}
foreach (var child in layerStateMachine.stateMachines)
{
if (child.stateMachine == null) continue;
if (visited.Contains(child.stateMachine)) continue;
foreach (var state in VisitStateMachine(child.stateMachine))
{
yield return state;
}
}
}
}
private void UpdateWriteDefaults(AnimatorStateMachine stateMachine, bool? writeDefaults)
{
if (!writeDefaults.HasValue) return;
var queue = new Queue<AnimatorStateMachine>();
queue.Enqueue(stateMachine);
while (queue.Count > 0)
{
var sm = queue.Dequeue();
foreach (var state in sm.states)
{
state.state.writeDefaultValues = writeDefaults.Value;
}
foreach (var child in sm.stateMachines)
{
queue.Enqueue(child.stateMachine);
}
}
}
private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine)
{
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
if (_stateMachines.TryGetValue(cacheKey, out var asm))
{
return asm;
}
asm = _deepClone.DoClone(layerStateMachine, basePath, _cloneMap);
foreach (var state in WalkAllStates(asm))
{
foreach (var behavior in state.behaviours)
{
AdjustBehavior(behavior, basePath);
}
}
_stateMachines[cacheKey] = asm;
return asm;
}
private void AdjustBehavior(StateMachineBehaviour behavior, string basePath)
{
#if MA_VRCSDK3_AVATARS
switch (behavior)
{
case VRCAnimatorLayerControl layerControl:
{
// TODO - need to figure out how to handle cross-layer references. For now this will handle
// intra-animator cases.
layerControl.layer += _controllerBaseLayer;
break;
}
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
case VRCAnimatorPlayAudio playAudio:
{
if (!string.IsNullOrEmpty(playAudio.SourcePath) && !string.IsNullOrEmpty(basePath) && !playAudio.SourcePath.StartsWith(basePath))
{
playAudio.SourcePath = $"{basePath}/{playAudio.SourcePath}";
}
break;
}
#endif
}
#endif
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 614457d82b1a4b109788029754c9fc1a
timeCreated: 1703674134

View File

@ -1,216 +0,0 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.animation
{
using UnityObject = Object;
internal class DeepClone
{
private bool _isSaved;
private UnityObject _combined;
public AnimatorOverrideController OverrideController { get; set; }
public DeepClone(BuildContext context)
{
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
_combined = context.AssetContainer;
}
public T DoClone<T>(T original,
string basePath = null,
Dictionary<UnityObject, UnityObject> cloneMap = null
) where T : UnityObject
{
if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
System.Func<UnityObject, UnityObject> visitor = null;
if (basePath != null)
{
visitor = o => CloneWithPathMapping(o, basePath);
}
// We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes,
// MonoScripts...), so check for the types we care about here
switch (original)
{
// Any object referenced by an animator that we intend to mutate needs to be listed here.
case Motion _:
case AnimatorController _:
case AnimatorState _:
case AnimatorStateMachine _:
case AnimatorTransitionBase _:
case StateMachineBehaviour _:
break; // We want to clone these types
case AudioClip _: //Used in VRC Animator Play Audio State Behavior
// Leave textures, materials, and script definitions alone
case Texture2D _:
case MonoScript _:
case Material _:
return original;
// Also avoid copying unknown scriptable objects.
// This ensures compatibility with e.g. avatar remote, which stores state information in a state
// behaviour referencing a custom ScriptableObject
case ScriptableObject _:
return original;
default:
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
}
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
if (OverrideController != null && original is AnimationClip srcClip)
{
T overrideClip = OverrideController[srcClip] as T;
if (overrideClip != null)
{
original = overrideClip;
}
}
if (cloneMap.ContainsKey(original))
{
return (T)cloneMap[original];
}
var obj = visitor?.Invoke(original);
if (obj != null)
{
cloneMap[original] = obj;
if (obj != original)
{
ObjectRegistry.RegisterReplacedObject(original, obj);
}
return (T)obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
obj = UnityObject.Instantiate(original);
}
else
{
obj = (T)ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
ObjectRegistry.RegisterReplacedObject(original, obj);
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(obj, _combined);
}
SerializedObject so = new SerializedObject(obj);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = true;
switch (prop.propertyType)
{
case SerializedPropertyType.ObjectReference:
{
var newObj = DoClone(prop.objectReferenceValue, basePath, cloneMap);
prop.objectReferenceValue = newObj;
break;
}
// Iterating strings can get super slow...
case SerializedPropertyType.String:
enterChildren = false;
break;
}
}
so.ApplyModifiedPropertiesWithoutUndo();
return (T)obj;
}
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
{
if (o is AnimationClip clip)
{
// We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset
// from a persistent asset. If the asset is persistent, skip cases where path editing isn't required,
// or where this is one of the special VRC proxy animations.
if (EditorUtility.IsPersistent(o) && (basePath == "" || Util.IsProxyAnimation(clip))) return clip;
AnimationClip newClip = new AnimationClip();
newClip.name = "rebased " + clip.name;
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(newClip, _combined);
}
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = MapPath(binding, basePath);
// https://github.com/bdunderscore/modular-avatar/issues/950
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
// curves to be forgotten; use SetEditorCurve instead.
AnimationUtility.SetEditorCurve(newClip, newBinding,
AnimationUtility.GetEditorCurve(clip, binding));
}
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding, basePath);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
}
newClip.wrapMode = clip.wrapMode;
newClip.legacy = clip.legacy;
newClip.frameRate = clip.frameRate;
newClip.localBounds = clip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
return newClip;
}
else if (o is Texture)
{
return o;
}
else
{
return null;
}
}
private static string MapPath(EditorCurveBinding binding, string basePath)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
var newPath = binding.path == "" ? basePath : basePath + binding.path;
if (newPath.EndsWith("/"))
{
newPath = newPath.Substring(0, newPath.Length - 1);
}
return newPath;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b33090a3e763464ab05f3efe07e0cbd3
timeCreated: 1703148770

View File

@ -1,18 +0,0 @@
using System.Collections.Generic;
using UnityEditor;
namespace nadena.dev.modular_avatar.animation
{
internal class EditorCurveBindingComparer : IEqualityComparer<EditorCurveBinding>
{
public bool Equals(UnityEditor.EditorCurveBinding x, UnityEditor.EditorCurveBinding y)
{
return x.path == y.path && x.type == y.type && x.propertyName == y.propertyName;
}
public int GetHashCode(UnityEditor.EditorCurveBinding obj)
{
return obj.path.GetHashCode() ^ obj.type.GetHashCode() ^ obj.propertyName.GetHashCode();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e751f7889323485bbe202285a47cb0d4
timeCreated: 1719196767

View File

@ -1,9 +1,13 @@
using System.Linq;
#if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using BuildContext = nadena.dev.ndmf.BuildContext;
namespace nadena.dev.modular_avatar.animation
{
@ -15,46 +19,64 @@ namespace nadena.dev.modular_avatar.animation
{
protected override void Execute(BuildContext context)
{
var asc = context.Extension<AnimationServicesContext>();
if (!asc.BoundReadableProperties.Any()) return;
var asc = context.Extension<AnimatorServicesContext>();
var activeProxies = context.GetState<ReadablePropertyExtension.Retained>().proxyProps
.ToDictionary(kv => kv.Key, kv => kv.Value);
if (activeProxies.Count == 0) return;
var fx = (AnimatorController)context.AvatarDescriptor.baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController;
// Filter any proxies not used in animator transitions
var usedProxies = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX]
.AllReachableNodes().OfType<VirtualTransitionBase>()
.SelectMany(t => t.Conditions)
.Select(c => c.parameter)
.ToHashSet();
foreach (var proxyBinding in activeProxies.ToList())
{
if (!usedProxies.Contains(proxyBinding.Value))
{
activeProxies.Remove(proxyBinding.Key);
}
}
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
if (fx == null) return;
var nullMotion = new AnimationClip();
nullMotion.name = "NullMotion";
var blendTree = new BlendTree();
blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false;
blendTree.children = asc.BoundReadableProperties.Select(GenerateDelayChild).ToArray();
blendTree.children = activeProxies
.Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value)))
.ToArray();
var asm = new AnimatorStateMachine();
var state = new AnimatorState();
state.name = "DelayDisable";
state.motion = blendTree;
state.writeDefaultValues = true;
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
var state = layer.StateMachine.AddState("DelayDisable");
layer.StateMachine.DefaultState = state;
asm.defaultState = state;
asm.states = new[]
state.WriteDefaultValues = true;
state.Motion = asc.ControllerContext.Clone(blendTree);
// Ensure the initial state of readable props matches the actual state of the gameobject
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
new ChildAnimatorState
foreach (var (binding, prop) in activeProxies)
{
state = state,
position = Vector3.zero
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
{
p.defaultFloat = obj.activeSelf ? 1 : 0;
controller.Parameters = controller.Parameters.SetItem(prop, p);
}
}
}
};
fx.layers = fx.layers.Append(new AnimatorControllerLayer
{
name = "DelayDisable",
stateMachine = asm,
defaultWeight = 1,
blendingMode = AnimatorLayerBlendingMode.Override
}).ToArray();
}
private ChildMotion GenerateDelayChild((EditorCurveBinding, string) binding)
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
{
var ecb = binding.Item1;
var prop = binding.Item2;
@ -64,12 +86,43 @@ namespace nadena.dev.modular_avatar.animation
curve.AddKey(0, 1);
AnimationUtility.SetEditorCurve(motion, ecb, curve);
return new ChildMotion
// Occasionally, we'll have a very small value pop up, probably due to FP errors.
// To correct for this, instead of directly using the property in the direct blend tree,
// we'll use a 1D blend tree to give ourselves a buffer.
var bufferBlendTree = new BlendTree();
bufferBlendTree.blendType = BlendTreeType.Simple1D;
bufferBlendTree.useAutomaticThresholds = false;
bufferBlendTree.blendParameter = prop;
bufferBlendTree.children = new[]
{
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0
},
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0.01f
},
new ChildMotion
{
motion = motion,
directBlendParameter = prop,
timeScale = 1,
threshold = 1
}
};
return new ChildMotion
{
motion = bufferBlendTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1
};
}
}
}
#endif

View File

@ -1,17 +0,0 @@
#region
using nadena.dev.ndmf;
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// This interface tags components which supply additional animation controllers for merging. They will be given
/// an opportunity to apply animation path updates when the TrackObjectRenamesContext is committed.
/// </summary>
internal interface IOnCommitObjectRenames
{
void OnCommitObjectRenames(BuildContext buildContext, PathMappings renameContext);
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6a66f552b8b334a45a986bfcf6767200
timeCreated: 1692511752

View File

@ -0,0 +1,246 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
using BuildContext = nadena.dev.ndmf.BuildContext;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.animation
{
internal class MMDRelayState
{
internal HashSet<VirtualLayer> mmdAffectedOriginalLayers = new();
}
internal class MMDRelayEarlyPass : Pass<MMDRelayEarlyPass>
{
protected override void Execute(BuildContext context)
{
if (!MMDRelayPass.ShouldRun(context)) return;
var asc = context.Extension<AnimatorServicesContext>();
if (asc.ControllerContext.Controllers.TryGetValue(VRCAvatarDescriptor.AnimLayerType.FX, out var fx))
{
context.GetState<MMDRelayState>().mmdAffectedOriginalLayers = new HashSet<VirtualLayer>(
fx.Layers.Skip(1).Take(2)
);
}
}
}
/// <summary>
/// Many MMD worlds animate the first three FX layers to weight zero. When MA injects new layers, this can hit
/// unintended layers (eg the RC base state layer).
/// To work around this, we'll inject a layer which will relay its active state into a parameter; then, we add a
/// layer to relay this to layers which should be affected. Finally, any layer which _shouldn't_ be affected is
/// pushed out of the first three layers by injecting dummy layers.
/// </summary>
internal class MMDRelayPass : Pass<MMDRelayPass>
{
private const string MMDRelayParam = "__MA/Internal/MMDNotActive";
internal const string ControlLayerName = "Modular Avatar: MMD Control";
internal const string DummyLayerName = "Modular Avatar: MMD Dummy";
internal const string StateNameInitial = "Initial";
internal const string StateNameNotMMD = "NotMMD";
internal const string StateNameMMD = "MMD";
internal static bool ShouldRun(BuildContext context)
{
var settings = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarVRChatSettings>(true);
return settings.FirstOrDefault()?.MMDWorldSupport ?? true;
}
protected override void Execute(BuildContext context)
{
if (!ShouldRun(context)) return;
var asc = context.Extension<AnimatorServicesContext>();
if (!asc.ControllerContext.Controllers.TryGetValue(VRCAvatarDescriptor.AnimLayerType.FX, out var fx))
return;
var affectedLayers = context.GetState<MMDRelayState>().mmdAffectedOriginalLayers;
foreach (var layer in fx.Layers)
{
if (layer.StateMachine == null) continue;
var rootMMDModeBehaviors = layer.StateMachine.Behaviours
.OfType<ModularAvatarMMDLayerControl>()
.ToList();
if (rootMMDModeBehaviors.Count == 0) continue;
if (rootMMDModeBehaviors.Count > 1)
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
"error.mmd.multiple_mmd_mode_behaviors", layer.Name);
continue;
}
if (rootMMDModeBehaviors[0].DisableInMMDMode)
{
affectedLayers.Add(layer);
}
else
{
affectedLayers.Remove(layer);
}
layer.StateMachine.Behaviours = layer.StateMachine.Behaviours
.Where(b => b is not ModularAvatarMMDLayerControl).ToImmutableList();
Object.DestroyImmediate(rootMMDModeBehaviors[0]);
// check for child behaviors
// TODO: implement filtering on AllReachableNodes
foreach (var node in layer.AllReachableNodes())
{
if (node is VirtualState state)
{
if (state.Behaviours.Any(b => b is ModularAvatarMMDLayerControl))
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
"error.mmd.mmd_mode_in_child_state", layer.Name, state.Name);
}
}
else if (node is VirtualStateMachine vsm)
{
if (vsm.Behaviours.Any(b => b is ModularAvatarMMDLayerControl))
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
"error.mmd.mmd_mode_in_child_state_machine", layer.Name, vsm.Name);
}
}
}
}
var needsAdjustment = fx.Layers.Select((layer, index) => (layer, index))
.Any(pair => affectedLayers.Contains(pair.layer) != (pair.index < 3 && pair.index != 0));
if (!needsAdjustment) return;
var toDisable = fx.Layers.Where(l => affectedLayers.Contains(l))
.Select(l => l.VirtualLayerIndex)
.ToList();
fx.Parameters = fx.Parameters.Add(MMDRelayParam, new AnimatorControllerParameter
{
name = MMDRelayParam,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0
});
var currentLayers = fx.Layers.ToList();
var newLayers = new List<VirtualLayer>();
// Layer zero's weight can't be changed anyway, so leave it where it is.
newLayers.Add(currentLayers[0]);
currentLayers.RemoveAt(0);
newLayers.Add(CreateMMDLayer(fx, toDisable));
// Add a dummy layer
var dummy = fx.AddLayer(new LayerPriority(0), DummyLayerName);
var s = dummy.StateMachine!.DefaultState = dummy.StateMachine.AddState("Dummy");
s.Motion = VirtualClip.Create("empty");
newLayers.Add(dummy);
fx.Layers = newLayers.Concat(currentLayers);
}
private static VirtualLayer CreateMMDLayer(VirtualAnimatorController fx, List<int> virtualLayers)
{
// We'll reorder this later, so the layer priority doesn't matter
var mmdControl = fx.AddLayer(new LayerPriority(0), ControlLayerName);
var stateMachine = mmdControl.StateMachine ?? throw new Exception("No state machine on MMD Control layer");
var motion = VirtualClip.Create("MMDRelay");
motion.SetFloatCurve(EditorCurveBinding.FloatCurve("", typeof(Animator), MMDRelayParam),
AnimationCurve.Constant(0, 1, 1)
);
var state_initial = stateMachine.AddState(StateNameInitial);
state_initial.Motion = motion;
var state_notmmd = stateMachine.AddState(StateNameNotMMD);
state_notmmd.Motion = motion;
var state_mmd = stateMachine.AddState(StateNameMMD);
state_mmd.Motion = motion;
var t = VirtualStateTransition.Create();
t.SetDestination(state_mmd);
t.Conditions = ImmutableList.Create(new AnimatorCondition
{
mode = AnimatorConditionMode.Less,
parameter = MMDRelayParam,
threshold = 0.5f
});
state_notmmd.Transitions = ImmutableList.Create(t);
t = VirtualStateTransition.Create();
t.SetDestination(state_notmmd);
t.Conditions = ImmutableList.Create(new AnimatorCondition
{
mode = AnimatorConditionMode.Greater,
parameter = MMDRelayParam,
threshold = 0.5f
});
state_mmd.Transitions = ImmutableList.Create(t);
t = VirtualStateTransition.Create();
t.SetDestination(state_mmd);
t.Conditions = ImmutableList.Create(new AnimatorCondition
{
mode = AnimatorConditionMode.Less,
parameter = MMDRelayParam,
threshold = 0.5f
});
state_initial.Transitions = ImmutableList.Create(t);
stateMachine.DefaultState = state_initial;
var mmd_behaviors = ImmutableList.CreateBuilder<StateMachineBehaviour>();
var notmmd_behaviors = ImmutableList.CreateBuilder<StateMachineBehaviour>();
foreach (var index in virtualLayers)
{
var behavior = ScriptableObject.CreateInstance<VRCAnimatorLayerControl>();
behavior.layer = index;
behavior.playable = VRC_AnimatorLayerControl.BlendableLayer.FX;
behavior.goalWeight = 0;
behavior.blendDuration = 0;
mmd_behaviors.Add(behavior);
behavior = ScriptableObject.CreateInstance<VRCAnimatorLayerControl>();
behavior.layer = index;
behavior.playable = VRC_AnimatorLayerControl.BlendableLayer.FX;
behavior.goalWeight = 1;
behavior.blendDuration = 0;
notmmd_behaviors.Add(behavior);
}
state_notmmd.Behaviours = notmmd_behaviors.ToImmutable();
state_mmd.Behaviours = mmd_behaviors.ToImmutable();
return mmdControl;
}
internal static bool IsRelayLayer(string layerName)
{
return layerName == ControlLayerName || layerName == DummyLayerName;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 163fd3d0edea43d5969395079f561986
timeCreated: 1741745889

View File

@ -1,330 +0,0 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEngine;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
#region
#endregion
/// <summary>
/// This extension context tracks when objects are renamed, and updates animations accordingly.
/// Users of this context need to be aware that, when creating new curves (or otherwise introducing new motions,
/// use context.ObjectPath to obtain a suitable path for the target objects).
/// </summary>
internal sealed class PathMappings
{
private AnimationDatabase _animationDatabase;
private Dictionary<GameObject, List<string>>
_objectToOriginalPaths = new Dictionary<GameObject, List<string>>();
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
private ImmutableDictionary<string, GameObject> _pathToObject = null;
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
{
_animationDatabase = animationDatabase;
_objectToOriginalPaths.Clear();
_transformLookthroughObjects.Clear();
ClearCache();
foreach (var xform in context.AvatarRootTransform.GetComponentsInChildren<Transform>(true))
{
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {xform.gameObject.AvatarRootPath()});
}
}
public void ClearCache()
{
_originalPathToMappedPath = null;
_transformOriginalPathToMappedPath = null;
_pathToObject = null;
}
/// <summary>
/// Sets the "transform lookthrough" flag for an object. Any transform animations on this object will be
/// redirected to its parent. This is used in Modular Avatar as part of bone merging logic.
/// </summary>
/// <param name="obj"></param>
public void MarkTransformLookthrough(GameObject obj)
{
_transformLookthroughObjects.Add(obj);
}
/// <summary>
/// Returns a path for use in dynamically generated animations for a given object. This can include objects not
/// present at the time of context activation; in this case, they will be assigned a randomly-generated internal
/// path and replaced during path remapping with the true path.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public string GetObjectIdentifier(GameObject obj)
{
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
return paths[0];
}
else
{
var internalPath = "_NewlyCreatedObject/" + GUID.Generate() + "/" + obj.AvatarRootPath();
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
return internalPath;
}
}
/// <summary>
/// Marks an object as having been removed. Its paths will be remapped to its parent.
/// </summary>
/// <param name="obj"></param>
public void MarkRemoved(GameObject obj)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
var parent = obj.transform.parent.gameObject;
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
{
parentPaths.AddRange(paths);
}
_objectToOriginalPaths.Remove(obj);
_transformLookthroughObjects.Remove(obj);
}
}
/// <summary>
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
/// by the new object. References originally to the new object will continue to point to the new object.
/// </summary>
/// <param name="old"></param>
/// <param name="newObject"></param>
public void ReplaceObject(GameObject old, GameObject newObject)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
{
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
{
newObjectPaths = new List<string>();
_objectToOriginalPaths.Add(newObject, newObjectPaths);
}
newObjectPaths.AddRange(paths);
_objectToOriginalPaths.Remove(old);
}
if (_transformLookthroughObjects.Contains(old))
{
_transformLookthroughObjects.Remove(old);
_transformLookthroughObjects.Add(newObject);
}
}
private ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
bool transformLookup)
{
if (cache != null) return cache;
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
foreach (var kvp in _objectToOriginalPaths)
{
var obj = kvp.Key;
var paths = kvp.Value;
if (transformLookup)
{
while (_transformLookthroughObjects.Contains(obj))
{
obj = obj.transform.parent.gameObject;
}
}
var newPath = obj.AvatarRootPath();
foreach (var origPath in paths)
{
if (!dict.ContainsKey(origPath))
{
dict = dict.Add(origPath, newPath);
}
}
}
cache = dict;
return cache;
}
public string MapPath(string path, bool isTransformMapping = false)
{
ImmutableDictionary<string, string> mappings;
if (isTransformMapping)
{
mappings = BuildMapping(ref _originalPathToMappedPath, true);
}
else
{
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
}
if (mappings.TryGetValue(path, out var mappedPath))
{
return mappedPath;
}
else
{
return path;
}
}
private string MapPath(EditorCurveBinding binding)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
return MapPath(binding.path, binding.type == typeof(Transform));
}
}
private AnimationClip ApplyMappingsToClip(AnimationClip originalClip,
Dictionary<AnimationClip, AnimationClip> clipCache)
{
if (originalClip == null) return null;
if (clipCache != null && clipCache.TryGetValue(originalClip, out var cachedClip)) return cachedClip;
if (originalClip.IsProxyAnimation()) return originalClip;
var curveBindings = AnimationUtility.GetCurveBindings(originalClip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(originalClip);
bool hasMapping = false;
foreach (var binding in curveBindings.Concat(objectBindings))
{
if (MapPath(binding) != binding.path)
{
hasMapping = true;
break;
}
}
if (!hasMapping) return originalClip;
var newClip = new AnimationClip();
newClip.name = originalClip.name;
SerializedObject before = new SerializedObject(originalClip);
SerializedObject after = new SerializedObject(newClip);
var before_hqCurve = before.FindProperty("m_UseHighQualityCurve");
var after_hqCurve = after.FindProperty("m_UseHighQualityCurve");
after_hqCurve.boolValue = before_hqCurve.boolValue;
after.ApplyModifiedPropertiesWithoutUndo();
// TODO - should we use direct SerializedObject manipulation to avoid missing script issues?
foreach (var binding in curveBindings)
{
var newBinding = binding;
newBinding.path = MapPath(binding);
// https://github.com/bdunderscore/modular-avatar/issues/950
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
// curves to be forgotten; use SetEditorCurve instead.
AnimationUtility.SetEditorCurve(newClip, newBinding,
AnimationUtility.GetEditorCurve(originalClip, binding));
}
foreach (var objBinding in objectBindings)
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(originalClip, objBinding));
}
newClip.wrapMode = originalClip.wrapMode;
newClip.legacy = originalClip.legacy;
newClip.frameRate = originalClip.frameRate;
newClip.localBounds = originalClip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(originalClip));
if (clipCache != null)
{
clipCache.Add(originalClip, newClip);
}
return newClip;
}
internal void OnDeactivate(BuildContext context)
{
Dictionary<AnimationClip, AnimationClip> clipCache = new Dictionary<AnimationClip, AnimationClip>();
_animationDatabase.ForeachClip(holder =>
{
if (holder.CurrentClip is AnimationClip clip)
{
holder.CurrentClip = ApplyMappingsToClip(clip, clipCache);
}
});
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
_animationDatabase.ForeachPlayAudio(playAudio =>
{
if (playAudio == null) return;
playAudio.SourcePath = MapPath(playAudio.SourcePath, true);
});
#endif
foreach (var listener in context.AvatarRootObject.GetComponentsInChildren<IOnCommitObjectRenames>())
{
listener.OnCommitObjectRenames(context, this);
}
}
public GameObject PathToObject(string path)
{
if (_pathToObject == null)
{
var builder = ImmutableDictionary.CreateBuilder<string, GameObject>();
foreach (var kvp in _objectToOriginalPaths)
foreach (var p in kvp.Value)
builder[p] = kvp.Key;
_pathToObject = builder.ToImmutable();
}
if (_pathToObject.TryGetValue(path, out var obj))
{
return obj;
}
else
{
return null;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f60ee78d127fda546a84d5396edfc8b2
timeCreated: 1691237971

View File

@ -1,147 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.animation
{
internal class ReadableProperty
{
private readonly BuildContext _context;
private readonly AnimationDatabase _animDB;
private readonly AnimationServicesContext _asc;
private readonly Dictionary<EditorCurveBinding, string> _alreadyBound = new();
private long _nextIndex;
public ReadableProperty(BuildContext context, AnimationDatabase animDB, AnimationServicesContext asc)
{
_context = context;
_animDB = animDB;
_asc = asc;
}
public IEnumerable<(EditorCurveBinding, string)> BoundProperties =>
_alreadyBound.Select(kv => (kv.Key, kv.Value));
/// <summary>
/// Creates an animator parameter which tracks the effective value of a property on a component. This only
/// tracks FX layer properties.
/// </summary>
/// <param name="ecb"></param>
/// <returns></returns>
public string ForBinding(string path, Type componentType, string property)
{
var ecb = new EditorCurveBinding
{
path = path,
type = componentType,
propertyName = property
};
if (_alreadyBound.TryGetValue(ecb, out var reader))
{
return reader;
}
var lastComponent = path.Split("/")[^1];
var emuPropName = $"__MA/ReadableProp/{lastComponent}/{componentType}/{property}#{_nextIndex++}";
float initialValue = 0;
var gameObject = _asc.PathMappings.PathToObject(path);
Object component = componentType == typeof(GameObject)
? gameObject
: gameObject?.GetComponent(componentType);
if (component != null)
{
var so = new SerializedObject(component);
var prop = so.FindProperty(property);
if (prop != null)
switch (prop.propertyType)
{
case SerializedPropertyType.Boolean:
initialValue = prop.boolValue ? 1 : 0;
break;
case SerializedPropertyType.Float:
initialValue = prop.floatValue;
break;
case SerializedPropertyType.Integer:
initialValue = prop.intValue;
break;
default: throw new NotImplementedException($"Property type {prop.type} not supported");
}
}
_asc.AddPropertyDefinition(new AnimatorControllerParameter
{
defaultFloat = initialValue,
name = emuPropName,
type = AnimatorControllerParameterType.Float
});
BindProperty(ecb, emuPropName);
_alreadyBound[ecb] = emuPropName;
return emuPropName;
}
private void BindProperty(EditorCurveBinding ecb, string propertyName)
{
var boundProp = new EditorCurveBinding
{
path = "",
type = typeof(Animator),
propertyName = propertyName
};
foreach (var clip in _animDB.ClipsForPath(ecb.path)) ProcessAnyClip(clip);
void ProcessBlendTree(BlendTree blendTree)
{
foreach (var child in blendTree.children)
switch (child.motion)
{
case AnimationClip animationClip:
ProcessAnimationClip(animationClip);
break;
case BlendTree subBlendTree:
ProcessBlendTree(subBlendTree);
break;
}
}
void ProcessAnimationClip(AnimationClip animationClip)
{
var curve = AnimationUtility.GetEditorCurve(animationClip, ecb);
if (curve == null) return;
AnimationUtility.SetEditorCurve(animationClip, boundProp, curve);
}
void ProcessAnyClip(AnimationDatabase.ClipHolder clip)
{
switch (clip.CurrentClip)
{
case AnimationClip animationClip:
ProcessAnimationClip(animationClip);
break;
case BlendTree blendTree:
ProcessBlendTree(blendTree);
break;
}
}
}
public string ForActiveSelf(string path)
{
return ForBinding(path, typeof(GameObject), "m_IsActive");
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1074339e2a59465ba585cb8cbbc4a88c
timeCreated: 1719195449

View File

@ -0,0 +1,82 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.animation
{
[DependsOnContext(typeof(AnimatorServicesContext))]
internal class ReadablePropertyExtension : IExtensionContext
{
// This is a temporary hack for GameObjectDelayDisablePass
public class Retained
{
public Dictionary<EditorCurveBinding, string> proxyProps = new();
}
private AnimatorServicesContext? _asc;
private Retained _retained = null!;
private AnimatorServicesContext asc =>
_asc ?? throw new InvalidOperationException("ActiveSelfProxyExtension is not active");
private Dictionary<EditorCurveBinding, string> proxyProps => _retained.proxyProps;
private int index;
public IEnumerable<(EditorCurveBinding, string)> ActiveProxyProps =>
proxyProps.Select(kvp => (kvp.Key, kvp.Value));
public string GetActiveSelfProxy(GameObject obj)
{
var path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj);
var ecb = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive");
if (proxyProps.TryGetValue(ecb, out var prop)) return prop;
prop = $"__MA/ActiveSelfProxy/{obj.name}##{index++}";
proxyProps[ecb] = prop;
// Add prop to all animators
foreach (var animator in asc.ControllerContext.GetAllControllers())
{
animator.Parameters = animator.Parameters.SetItem(
prop,
new AnimatorControllerParameter
{
name = prop,
type = AnimatorControllerParameterType.Float,
defaultFloat = obj.activeSelf ? 1 : 0
}
);
}
return prop;
}
public void OnActivate(BuildContext context)
{
_asc = context.Extension<AnimatorServicesContext>();
_retained = context.GetState<Retained>();
}
public void OnDeactivate(BuildContext context)
{
asc.AnimationIndex.EditClipsByBinding(proxyProps.Keys, clip =>
{
foreach (var b in clip.GetFloatCurveBindings().ToList())
{
if (proxyProps.TryGetValue(b, out var proxyProp))
{
var curve = clip.GetFloatCurve(b);
clip.SetFloatCurve("", typeof(Animator), proxyProp, curve);
}
}
});
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 511cbc0373a2469192e0351e2222a203
timeCreated: 1732496091

View File

@ -1,9 +1,11 @@
#region
#if MA_VRCSDK3_AVATARS
#region
using System;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor.Animations;
using UnityEngine;
@ -20,41 +22,36 @@ namespace nadena.dev.modular_avatar.core.editor
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
?? ImmutableDictionary<string, float>.Empty;
foreach (var layer in context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers))
{
if (layer.isDefault || layer.animatorController == null) continue;
var asc = context.Extension<AnimatorServicesContext>();
// We should have converted anything that's not an AnimationController by now
var controller = layer.animatorController as AnimatorController;
if (controller == null || !context.IsTemporaryAsset(controller))
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
}
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
var parameters = controller.Parameters;
foreach (var (name, parameter) in parameters)
{
if (!values.TryGetValue(parameters[i].name, out var defaultValue)) continue;
if (!values.TryGetValue(name, out var defaultValue)) continue;
switch (parameters[i].type)
switch (parameter.type)
{
case AnimatorControllerParameterType.Bool:
parameters[i].defaultBool = defaultValue > 0.5f;
parameter.defaultBool = defaultValue != 0.0f;
break;
case AnimatorControllerParameterType.Int:
parameters[i].defaultInt = Mathf.RoundToInt(defaultValue);
parameter.defaultInt = Mathf.RoundToInt(defaultValue);
break;
case AnimatorControllerParameterType.Float:
parameters[i].defaultFloat = defaultValue;
parameter.defaultFloat = defaultValue;
break;
default:
continue; // unhandled type, e.g. trigger
}
parameters = parameters.SetItem(name, parameter);
}
controller.parameters = parameters;
controller.Parameters = parameters;
}
}
}
}
#endif

View File

@ -1,12 +1,14 @@
#if MA_VRCSDK3_AVATARS
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -17,11 +19,16 @@ namespace nadena.dev.modular_avatar.core.editor
*/
internal class BlendshapeSyncAnimationProcessor
{
private BuildContext _context;
private Dictionary<Motion, Motion> _motionCache;
private readonly ndmf.BuildContext _context;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
private struct SummaryBinding
internal BlendshapeSyncAnimationProcessor(ndmf.BuildContext context)
{
_context = context;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
}
private struct SummaryBinding : IEquatable<SummaryBinding>
{
private const string PREFIX = "blendShape.";
public string path;
@ -33,71 +40,76 @@ namespace nadena.dev.modular_avatar.core.editor
this.propertyName = PREFIX + blendShape;
}
public static SummaryBinding FromEditorBinding(EditorCurveBinding binding)
public static SummaryBinding? FromEditorBinding(EditorCurveBinding binding)
{
if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
{
return new SummaryBinding();
return null;
}
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
}
public EditorCurveBinding ToEditorCurveBinding()
{
return EditorCurveBinding.FloatCurve(
path,
typeof(SkinnedMeshRenderer),
propertyName
);
}
public void OnPreprocessAvatar(BuildContext context)
public bool Equals(SummaryBinding other)
{
_context = context;
var avatarGameObject = context.AvatarRootObject;
var animDb = _context.AnimationDatabase;
return path == other.path && propertyName == other.propertyName;
}
public override bool Equals(object? obj)
{
return obj is SummaryBinding other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(path, propertyName);
}
}
public void OnPreprocessAvatar()
{
var avatarGameObject = _context.AvatarRootObject;
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
var avatarDescriptor = context.AvatarDescriptor;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>();
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
if (components.Length == 0) return;
var layers = avatarDescriptor.baseAnimationLayers;
var fxIndex = -1;
AnimatorController controller = null;
for (int i = 0; i < layers.Length; i++)
{
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.FX && !layers[i].isDefault)
{
if (layers[i].animatorController is AnimatorController c && c != null)
{
fxIndex = i;
controller = c;
break;
}
}
}
if (controller == null)
{
// Nothing to do, return
}
foreach (var component in components)
{
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
}
// Walk and transform all clips
animDb.ForeachClip(clip =>
var clips = new HashSet<VirtualClip>();
foreach (var key in _bindingMappings.Keys)
{
if (clip.CurrentClip is AnimationClip anim)
{
BuildReport.ReportingObject(clip.CurrentClip,
() => { clip.CurrentClip = TransformMotion(anim); });
var ecb = key.ToEditorCurveBinding();
clips.UnionWith(animDb.GetClipsForBinding(ecb));
}
// Walk and transform all clips
foreach (var clip in clips)
{
ProcessClip(clip);
}
});
}
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
{
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
if (targetObj == null) return;
foreach (var binding in component.Bindings)
{
var refObj = binding.ReferenceMesh.Get(component);
@ -106,6 +118,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (refSmr == null) continue;
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
if (refPath == null) continue;
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
@ -123,108 +136,20 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
Motion TransformMotion(Motion motion)
private void ProcessClip(VirtualClip clip)
{
if (motion == null) return null;
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
switch (motion)
foreach (var binding in clip.GetFloatCurveBindings().ToList())
{
case AnimationClip clip:
{
motion = ProcessClip(clip);
break;
}
case BlendTree tree:
{
bool anyChanged = false;
var children = tree.children;
for (int i = 0; i < children.Length; i++)
{
var newM = TransformMotion(children[i].motion);
if (newM != children[i].motion)
{
anyChanged = true;
children[i].motion = newM;
}
}
if (anyChanged)
{
var newTree = new BlendTree();
EditorUtility.CopySerialized(tree, newTree);
_context.SaveAsset(newTree);
newTree.children = children;
motion = newTree;
}
break;
}
default:
Debug.LogWarning($"Ignoring unsupported motion type {motion.GetType()}");
break;
}
_motionCache[motion] = motion;
return motion;
}
AnimationClip ProcessClip(AnimationClip origClip)
{
var clip = origClip;
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);
foreach (var binding in bindings)
{
if (!_bindingMappings.TryGetValue(SummaryBinding.FromEditorBinding(binding), out var dstBindings))
var srcBinding = SummaryBinding.FromEditorBinding(binding);
if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
{
continue;
}
if (clip == origClip)
{
clip = Object.Instantiate(clip);
}
var curve = clip.GetFloatCurve(binding);
foreach (var dst in dstBindings)
{
clip.SetCurve(dst.path, typeof(SkinnedMeshRenderer), dst.propertyName,
AnimationUtility.GetEditorCurve(origClip, binding));
}
}
return clip;
}
IEnumerable<AnimatorState> AllStates(AnimatorController controller)
{
HashSet<AnimatorStateMachine> visitedStateMachines = new HashSet<AnimatorStateMachine>();
Queue<AnimatorStateMachine> stateMachines = new Queue<AnimatorStateMachine>();
foreach (var layer in controller.layers)
{
if (layer.stateMachine != null)
stateMachines.Enqueue(layer.stateMachine);
}
while (stateMachines.Count > 0)
{
var next = stateMachines.Dequeue();
if (visitedStateMachines.Contains(next)) continue;
visitedStateMachines.Add(next);
foreach (var state in next.states)
{
yield return state.state;
}
foreach (var sm in next.stateMachines)
{
stateMachines.Enqueue(sm.stateMachine);
clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
}
}
}

View File

@ -1,23 +1,18 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
#endif
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class BuildContext
{
internal readonly nadena.dev.ndmf.BuildContext PluginBuildContext;
internal readonly ndmf.BuildContext PluginBuildContext;
#if MA_VRCSDK3_AVATARS
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
@ -25,14 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
internal GameObject AvatarRootObject => PluginBuildContext.AvatarRootObject;
internal Transform AvatarRootTransform => PluginBuildContext.AvatarRootTransform;
internal AnimationDatabase AnimationDatabase =>
PluginBuildContext.Extension<AnimationServicesContext>().AnimationDatabase;
internal PathMappings PathMappings =>
PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
internal UnityEngine.Object AssetContainer => PluginBuildContext.AssetContainer;
private bool SaveImmediate = false;
#if MA_VRCSDK3_AVATARS
@ -44,13 +31,12 @@ namespace nadena.dev.modular_avatar.core.editor
/// replace the source menu for the purposes of identifying any other MAMIs that might install to the same
/// menu asset.
/// </summary>
internal readonly Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> PostProcessControls
= new Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>();
internal readonly Dictionary<Object, Action<VRCExpressionsMenu.Control>> PostProcessControls = new();
#endif
public static implicit operator BuildContext(ndmf.BuildContext ctx) =>
ctx.Extension<ModularAvatarContext>().BuildContext;
public BuildContext(nadena.dev.ndmf.BuildContext PluginBuildContext)
public BuildContext(ndmf.BuildContext PluginBuildContext)
{
this.PluginBuildContext = PluginBuildContext;
}
@ -71,64 +57,9 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
PluginBuildContext.AssetSaver.SaveAsset(obj);
}
public AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
SaveAsset(controller);
return controller;
}
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
if (controller == null) return null;
var merger = new AnimatorCombiner(PluginBuildContext, controller.name + " (clone)");
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
var result = merger.Finish();
ObjectRegistry.RegisterReplacedObject(controller, result);
return result;
}
public AnimatorController ConvertAnimatorController(RuntimeAnimatorController anyController)
{
switch (anyController)
{
case AnimatorController ac:
return ac;
case AnimatorOverrideController aoc:
var merger = new AnimatorCombiner(PluginBuildContext, anyController.name + " (clone)");
merger.AddOverrideController("", aoc, null);
return merger.Finish();
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + anyController.GetType());
}
}
#if MA_VRCSDK3_AVATARS
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)

View File

@ -6,7 +6,6 @@ using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using VRC.SDK3.Avatars.ScriptableObjects;
using Object = UnityEngine.Object;
@ -19,6 +18,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal static void FixupExpressionsMenu(BuildContext context)
{
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.customExpressions = true;
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;
@ -42,7 +43,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
var parameters = context.AvatarDescriptor.expressionParameters.parameters
?? new VRCExpressionParameters.Parameter[0];
?? Array.Empty<VRCExpressionParameters.Parameter>();
var parameterNames = parameters.Select(p => p.name).ToImmutableHashSet();
if (!context.PluginBuildContext.IsTemporaryAsset(expressionsMenu))
@ -91,6 +92,11 @@ namespace nadena.dev.modular_avatar.core.editor
control.icon = newIcon;
}
if (control.subMenu != null)
{
VisitMenu(control.subMenu);
}
if (control.labels != null)
{
for (int i = 0; i < control.labels.Length; i++)
@ -113,11 +119,20 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
#if UNITY_ANDROID
private const TextureFormat TargetFormat = TextureFormat.ASTC_4x4;
#else
private const TextureFormat TargetFormat = TextureFormat.DXT5;
#endif
internal static TextureFormat TargetFormat
{
get
{
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.StandaloneWindows64:
return TextureFormat.DXT5;
default:
return TextureFormat.ASTC_4x4;
}
}
}
private static Texture2D MaybeScaleIcon(BuildContext context, Texture2D original)
{
@ -126,10 +141,14 @@ namespace nadena.dev.modular_avatar.core.editor
return original;
}
var newRatio = Math.Min(256f / original.width, 256f / original.height);
var newRatio = Math.Min(1, Math.Min(256f / original.width, 256f / original.height));
var newWidth = Math.Min(256, Mathf.RoundToInt(original.width * newRatio));
var newHeight = Math.Min(256, Mathf.RoundToInt(original.height * newRatio));
// Round up to a multiple of four
newWidth = (newWidth + 3) & ~3;
newHeight = (newHeight + 3) & ~3;
var newTex = new Texture2D(newWidth, newHeight, TextureFormat.RGBA32, true);
context.SaveAsset(newTex);

View File

@ -11,6 +11,8 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
internal class PatchLoader
{
private const string HarmonyId = "nadena.dev.modular_avatar";
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
{
//HierarchyViewPatches.Patch,
@ -19,7 +21,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
[InitializeOnLoadMethod]
static void ApplyPatches()
{
var harmony = new Harmony("nadena.dev.modular_avatar");
var harmony = new Harmony(HarmonyId);
foreach (var patch in patches)
{
@ -33,7 +35,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
}
}
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(); };
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(HarmonyId); };
}
}
}

View File

@ -36,17 +36,17 @@ namespace nadena.dev.modular_avatar.core.editor
},
new[]
{
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L"
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L", "shin.L"
},
new[]
{
"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R", "LLeg_R", "Right knee",
"RightLeg", "leg_R"
"RightLeg", "leg_R", "shin.R"
},
new[] {"LeftFoot", "Foot_Left", "Foot_L", "Ankle_L", "Foot.L.001", "Left ankle", "heel.L", "heel"},
new[] {"RightFoot", "Foot_Right", "Foot_R", "Ankle_R", "Foot.R.001", "Right ankle", "heel.R", "heel"},
new[] {"Spine", "spine01"},
new[] {"Chest", "Bust", "spine02"},
new[] {"Chest", "Bust", "spine02", "upper_chest"},
new[] {"Neck"},
new[] {"Head"},
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
@ -95,62 +95,62 @@ namespace nadena.dev.modular_avatar.core.editor
new[]
{
"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L", "Index1_L", "IndexFinger1_L",
"LeftHandIndex1", "Index Proximal.L", "finger02_01_L"
"LeftHandIndex1", "Index Proximal.L", "finger02_01_L", "f_index.01.L"
},
new[]
{
"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L", "Index2_L", "IndexFinger2_L",
"LeftHandIndex2", "Index Intermediate.L", "finger02_02_L"
"LeftHandIndex2", "Index Intermediate.L", "finger02_02_L", "f_index.02.L"
},
new[]
{
"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L", "Index3_L", "IndexFinger3_L", "LeftHandIndex3",
"Index Distal.L", "finger02_03_L"
"Index Distal.L", "finger02_03_L", "f_index.03.L"
},
new[]
{
"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L", "Middle1_L", "MiddleFinger1_L",
"LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L"
"LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L", "f_middle.01.L"
},
new[]
{
"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L", "Middle2_L",
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L"
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L", "f_middle.02.L"
},
new[]
{
"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L", "Middle3_L", "MiddleFinger3_L",
"LeftHandMiddle3", "Middle Distal.L", "finger03_03_L"
"LeftHandMiddle3", "Middle Distal.L", "finger03_03_L", "f_middle.03.L"
},
new[]
{
"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L", "Ring1_L", "RingFinger1_L", "LeftHandRing1",
"Ring Proximal.L", "finger04_01_L"
"Ring Proximal.L", "finger04_01_L", "f_ring.01.L"
},
new[]
{
"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L", "Ring2_L", "RingFinger2_L",
"LeftHandRing2", "Ring Intermediate.L", "finger04_02_L"
"LeftHandRing2", "Ring Intermediate.L", "finger04_02_L", "f_ring.02.L"
},
new[]
{
"LeftRingDistal", "DistalRing_Left", "DistalRing_L", "Ring3_L", "RingFinger3_L", "LeftHandRing3",
"Ring Distal.L", "finger04_03_L"
"Ring Distal.L", "finger04_03_L", "f_ring.03.L"
},
new[]
{
"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L", "Little1_L", "LittleFinger1_L",
"LeftHandPinky1", "Little Proximal.L", "finger05_01_L"
"LeftHandPinky1", "Little Proximal.L", "finger05_01_L", "f_pinky.01.L"
},
new[]
{
"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L", "Little2_L",
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L"
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L", "f_pinky.02.L"
},
new[]
{
"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L", "Little3_L", "LittleFinger3_L",
"LeftHandPinky3", "Little Distal.L", "finger05_03_L"
"LeftHandPinky3", "Little Distal.L", "finger05_03_L", "f_pinky.03.L"
},
new[]
{
@ -170,68 +170,71 @@ namespace nadena.dev.modular_avatar.core.editor
new[]
{
"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R", "Index1_R", "IndexFinger1_R",
"RightHandIndex1", "Index Proximal.R", "finger02_01_R"
"RightHandIndex1", "Index Proximal.R", "finger02_01_R", "f_index.01.R"
},
new[]
{
"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R", "Index2_R",
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R"
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R", "f_index.02.R"
},
new[]
{
"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R", "Index3_R", "IndexFinger3_R",
"RightHandIndex3", "Index Distal.R", "finger02_03_R"
"RightHandIndex3", "Index Distal.R", "finger02_03_R", "f_index.03.R"
},
new[]
{
"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R", "Middle1_R", "MiddleFinger1_R",
"RightHandMiddle1", "Middle Proximal.R", "finger03_01_R"
"RightHandMiddle1", "Middle Proximal.R", "finger03_01_R", "f_middle.01.R"
},
new[]
{
"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R", "Middle2_R",
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R"
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R", "f_middle.02.R"
},
new[]
{
"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R", "Middle3_R", "MiddleFinger3_R",
"RightHandMiddle3", "Middle Distal.R", "finger03_03_R"
"RightHandMiddle3", "Middle Distal.R", "finger03_03_R", "f_middle.03.R"
},
new[]
{
"RightRingProximal", "ProximalRing_Right", "ProximalRing_R", "Ring1_R", "RingFinger1_R",
"RightHandRing1", "Ring Proximal.R", "finger04_01_R"
"RightHandRing1", "Ring Proximal.R", "finger04_01_R", "f_ring.01.R"
},
new[]
{
"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R", "Ring2_R", "RingFinger2_R",
"RightHandRing2", "Ring Intermediate.R", "finger04_02_R"
"RightHandRing2", "Ring Intermediate.R", "finger04_02_R", "f_ring.02.R"
},
new[]
{
"RightRingDistal", "DistalRing_Right", "DistalRing_R", "Ring3_R", "RingFinger3_R", "RightHandRing3",
"Ring Distal.R", "finger04_03_R"
"Ring Distal.R", "finger04_03_R", "f_ring.03.R"
},
new[]
{
"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R", "Little1_R", "LittleFinger1_R",
"RightHandPinky1", "Little Proximal.R", "finger05_01_R"
"RightHandPinky1", "Little Proximal.R", "finger05_01_R", "f_pinky.01.R"
},
new[]
{
"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R", "Little2_R",
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R"
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R", "f_pinky.02.R"
},
new[]
{
"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R", "Little3_R", "LittleFinger3_R",
"RightHandPinky3", "Little Distal.R", "finger05_03_R"
"RightHandPinky3", "Little Distal.R", "finger05_03_R", "f_pinky.03.R"
},
new[] {"UpperChest", "UChest"},
};
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
internal static ImmutableHashSet<string> AllBoneNames =
boneNamePatterns.SelectMany(x => x).Select(NormalizeName).ToImmutableHashSet();
internal static string NormalizeName(string name)
{
name = name.ToLowerInvariant();
@ -243,6 +246,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
[InitializeOnLoadMethod]
private static void InsertboneNamePatternsToRuntime()
{
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
}
static HeuristicBoneMapper()
{
var pat_end_side = new Regex(@"[_\.]([LR])$");
@ -306,7 +317,9 @@ namespace nadena.dev.modular_avatar.core.editor
GameObject src,
GameObject newParent,
List<Transform> skipped = null,
HashSet<Transform> unassigned = null
HashSet<Transform> unassigned = null,
Animator avatarAnimator = null,
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null
)
{
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
@ -355,13 +368,55 @@ namespace nadena.dev.modular_avatar.core.editor
var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
List<HumanBodyBones> bodyBones = null;
var isMapped = false;
if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out var bodyBones))
if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
{
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
} else {
bodyBones = new List<HumanBodyBones> { outfitHumanoidBone };
}
} else {
bodyBones = new List<HumanBodyBones>() { outfitHumanoidBone };
}
}
if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out bodyBones))
{
continue;
}
if (!isMapped)
{
foreach (var bodyBone in bodyBones)
{
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(bodyBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
break;
}
}
}
}
if (!isMapped)
{
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
@ -369,9 +424,11 @@ namespace nadena.dev.modular_avatar.core.editor
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
isMapped = true;
break;
}
}
}
if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null)
{
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
return mappings;
}
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null)
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null, Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null, Animator avatarAnimator = null)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return;
@ -399,7 +456,7 @@ namespace nadena.dev.modular_avatar.core.editor
void Traverse(Transform src, Transform dst)
{
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped);
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
foreach (var pair in mappings)
{

View File

@ -7,6 +7,9 @@ namespace nadena.dev.modular_avatar.core.editor
internal class AvatarObjectReferenceDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
label = EditorGUI.BeginProperty(position, label, property);
try
{
if (CustomGUI(position, property, label)) return;
@ -24,6 +27,11 @@ namespace nadena.dev.modular_avatar.core.editor
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
}
}
finally
{
EditorGUI.EndProperty();
}
}
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
{

View File

@ -95,7 +95,7 @@ namespace nadena.dev.modular_avatar.core.editor
var t = (ModularAvatarBoneProxy) targets[i];
Undo.RecordObjects(targets, "Set targets");
var xform = ((TempObjRef) objRefs[i]).target;
if (RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
if (xform != null && RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
t.target = xform;
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace nadena.dev.modular_avatar.core.editor
{
internal abstract class DragAndDropManipulator<T> : PointerManipulator where T : Component, IHaveObjReferences
{
private const string DragActiveClassName = "drop-area--drag-active";
public T TargetComponent { get; set; }
protected virtual bool AllowKnownObjects => true;
private Transform _avatarRoot;
private GameObject[] _draggingObjects = Array.Empty<GameObject>();
public DragAndDropManipulator(VisualElement targetElement, T targetComponent)
{
target = targetElement;
TargetComponent = targetComponent;
}
protected sealed override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
target.RegisterCallback<DragExitedEvent>(OnDragExited);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
}
protected sealed override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
target.UnregisterCallback<DragExitedEvent>(OnDragExited);
target.UnregisterCallback<DragUpdatedEvent>(OnDragUpdated);
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
}
private void OnDragEnter(DragEnterEvent _)
{
if (TargetComponent == null) return;
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
if (_avatarRoot == null) return;
var knownObjects = TargetComponent.GetObjectReferences().Select(x => x.Get(TargetComponent)).ToHashSet();
_draggingObjects = DragAndDrop.objectReferences.OfType<GameObject>()
.Where(x => AllowKnownObjects || !knownObjects.Contains(x))
.Where(x => RuntimeUtil.FindAvatarTransformInParents(x.transform) == _avatarRoot)
.Where(FilterGameObject)
.ToArray();
if (_draggingObjects.Length == 0) return;
target.AddToClassList(DragActiveClassName);
}
private void OnDragLeave(DragLeaveEvent _)
{
_draggingObjects = Array.Empty<GameObject>();
target.RemoveFromClassList(DragActiveClassName);
}
private void OnDragExited(DragExitedEvent _)
{
_draggingObjects = Array.Empty<GameObject>();
target.RemoveFromClassList(DragActiveClassName);
}
private void OnDragUpdated(DragUpdatedEvent _)
{
if (TargetComponent == null) return;
if (_avatarRoot == null) return;
if (_draggingObjects.Length == 0) return;
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
}
private void OnDragPerform(DragPerformEvent _)
{
if (TargetComponent == null) return;
if (_avatarRoot == null) return;
if (_draggingObjects.Length == 0) return;
AddObjectReferences(_draggingObjects
.Select(x =>
{
var reference = new AvatarObjectReference();
reference.Set(x);
return reference;
})
.ToArray());
}
protected virtual bool FilterGameObject(GameObject obj)
{
return true;
}
protected abstract void AddObjectReferences(AvatarObjectReference[] references);
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d86c7d257d78fff4d8fdf56e2954a5c9
guid: 528c660b56905844ea2f88bc73837e9f
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -1,4 +1,5 @@
using UnityEditor;
#if MA_VRCSDK3_AVATARS
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor
{
@ -19,12 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
var target = (ModularAvatarVisibleHeadAccessory) this.target;
#if UNITY_ANDROID
EditorGUILayout.HelpBox(Localization.S("fpvisible.quest"), MessageType.Warning);
#else
if (_validation != null)
{
var status = _validation.Validate(target);
@ -35,6 +30,9 @@ namespace nadena.dev.modular_avatar.core.editor
case VisibleHeadAccessoryValidation.ReadyStatus.ParentMarked:
EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info);
break;
case VisibleHeadAccessoryValidation.ReadyStatus.NotUnderHead:
EditorGUILayout.HelpBox(Localization.S("fpvisible.NotUnderHead"), MessageType.Warning);
break;
default:
{
var label = "fpvisible." + status;
@ -44,9 +42,9 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
#endif
Localization.ShowLanguageUI();
}
}
}
#endif

View File

@ -18,7 +18,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
return (EditorStyles.label?.lineHeight ?? 0) * 3;
}
catch (NullReferenceException e)
catch (NullReferenceException)
{
// This can happen in early initialization...
return 0;

View File

@ -0,0 +1,30 @@
using UnityEditor;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarMMDLayerControl))]
internal class MMDModeEditor : MAEditorBase
{
private SerializedProperty m_p_DisableInMMDMode;
private void OnEnable()
{
m_p_DisableInMMDMode =
serializedObject.FindProperty(nameof(ModularAvatarMMDLayerControl.m_DisableInMMDMode));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
LogoDisplay.DisplayLogo();
EditorGUILayout.PropertyField(m_p_DisableInMMDMode, G("mmd_mode.disable_in_mmd_mode"));
ShowLanguageUI();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a1a682db3a3b491fa27980adfeeacffd
timeCreated: 1741836147

View File

@ -16,10 +16,11 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
throw new NotImplementedException();
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
@ -37,7 +38,44 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarMaterialSetter;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarMaterialSetter>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarMaterialSetter targetComponent)
: base(targetElement, targetComponent) { }
protected override bool FilterGameObject(GameObject obj)
{
if (obj.TryGetComponent<Renderer>(out var renderer))
{
return renderer.sharedMaterials.Length > 0;
}
return false;
}
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Material Switch Objects");
foreach (var reference in references)
{
var materialSwitchObject = new MaterialSwitchObject { Object = reference, MaterialIndex = 0 };
TargetComponent.Objects.Add(materialSwitchObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}
}

View File

@ -1,7 +1,4 @@
VisualElement {
}
#group-box {
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
@ -14,21 +11,36 @@
/* background-color: rgba(0, 0, 0, 0.1); */
}
#ListViewContainer {
margin-top: 4px;
}
#group-box > Label {
-unity-font-style: bold;
}
.horizontal {
flex-direction: row;
#ListViewContainer {
margin-top: 4px;
}
.horizontal #f-object {
.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.horizontal > Label {
height: auto;
}
.horizontal > PropertyField > * {
margin: 0;
}
#f-object {
flex-grow: 1;
margin-bottom: 1px;
}
#f-material-index {
@ -43,14 +55,20 @@
flex-grow: 1;
}
.horizontal > Label {
width: 100px;
}
#f-material {
flex-grow: 1;
}
.horizontal > Label {
align-self: center;
width: 100px;
height: 19px;
margin: 1px -2px 1px 3px;
-unity-text-align: middle-left;
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -3,7 +3,6 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
#endregion
@ -78,7 +77,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
var idx = int.Parse(idx_s);
if (idx < 0 || idx >= sharedMaterials.Length)
{
return $"Element {idx_s}: <???>";
return $"<color=\"red\">Element {idx_s}: <???></color>";
}
else if (sharedMaterials[idx] == null)
{
@ -89,7 +88,20 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
return $"Element {idx_s}: {sharedMaterials[idx].name}";
}
};
f_material_index_dropdown.formatSelectedValueCallback = idx_s => $"Element {idx_s}";
f_material_index_dropdown.formatSelectedValueCallback = idx_s =>
{
if (string.IsNullOrWhiteSpace(idx_s)) return "";
var idx = int.Parse(idx_s);
if (idx < 0 || idx >= sharedMaterials.Length)
{
return $"<color=\"red\">Element {idx_s}</color>";
}
else
{
return $"Element {idx_s}";
}
};
}
else
{
@ -135,7 +147,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
return targetObject?.GetComponent<Renderer>()?.sharedMaterials;
}
catch (MissingComponentException e)
catch (MissingComponentException)
{
return null;
}

View File

@ -1,16 +1,16 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="toggled-object-editor">
<ui:VisualElement class="horizontal">
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
</ui:VisualElement>
<ui:VisualElement class="horizontal">
<ed:IntegerField name="f-material-index" binding-path="MaterialIndex"/>
<ui:DropdownField name="f-material-index-dropdown"/>
<ed:ObjectField name="f-material-index-original"/>
</ui:VisualElement>
<ui:VisualElement class="horizontal">
<ui:Label text="reactive_object.material-setter.set-to" class="ndmf-tr"/>
<ed:PropertyField binding-path="Material" label="" name="f-material"/>
</ui:VisualElement>
<ed:PropertyField name="f-material" binding-path="Material" label=""/>
</ui:VisualElement>
</UXML>

View File

@ -33,6 +33,29 @@ namespace nadena.dev.modular_avatar.core.editor
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
private static Editor _cachedEditor;
[InitializeOnLoadMethod]
private static void Init()
{
ModularAvatarMenuInstaller._openSelectMenu = OpenSelectInstallTargetMenu;
}
private static void OpenSelectInstallTargetMenu(ModularAvatarMenuInstaller installer)
{
CreateCachedEditor(installer, typeof(MenuInstallerEditor), ref _cachedEditor);
var editor = (MenuInstallerEditor)_cachedEditor;
editor.OnEnable();
var serializedObject = editor.serializedObject;
var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu));
var root = editor.FindCommonAvatar();
editor.OpenSelectMenu(root, installTo);
}
private void OnEnable()
{
_installer = (ModularAvatarMenuInstaller) target;
@ -215,74 +238,7 @@ namespace nadena.dev.modular_avatar.core.editor
var avatar = commonAvatar;
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
{
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
{
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
{
DestroyInstallTargets();
}
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
{
// Menu, ContextCallback
menu = vt.Item1;
}
if (menu is ModularAvatarMenuItem item)
{
if (item.MenuSource == SubmenuSource.MenuAsset)
{
menu = item.Control.subMenu;
}
else
{
var menuParent = item.menuSource_otherObjectChildren != null
? item.menuSource_otherObjectChildren
: item.gameObject;
menu = new MenuNodesUnder(menuParent);
}
}
else if (menu is ModularAvatarMenuGroup group)
{
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
else menu = new MenuNodesUnder(group.gameObject);
}
if (menu is VRCExpressionsMenu expMenu)
{
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
else installTo.objectReferenceValue = expMenu;
}
else if (menu is RootMenu)
{
installTo.objectReferenceValue = null;
}
else if (menu is MenuNodesUnder nodesUnder)
{
installTo.objectReferenceValue = null;
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
{
var installer = (ModularAvatarMenuInstaller) target;
var child = new GameObject();
Undo.RegisterCreatedObjectUndo(child, "Set install target");
child.transform.SetParent(nodesUnder.root.transform, false);
child.name = installer.gameObject.name;
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
targetComponent.installer = installer;
EditorGUIUtility.PingObject(child);
}
}
serializedObject.ApplyModifiedProperties();
VirtualMenu.InvalidateCaches();
Repaint();
});
OpenSelectMenu(avatar, installTo);
}
}
@ -368,7 +324,79 @@ namespace nadena.dev.modular_avatar.core.editor
serializedObject.ApplyModifiedProperties();
Localization.ShowLanguageUI();
ShowLanguageUI();
}
private void OpenSelectMenu(VRCAvatarDescriptor avatar, SerializedProperty installTo)
{
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
{
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
{
DestroyInstallTargets();
}
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
{
// Menu, ContextCallback
menu = vt.Item1;
}
if (menu is ModularAvatarMenuItem item)
{
if (item.MenuSource == SubmenuSource.MenuAsset)
{
menu = item.Control.subMenu;
}
else
{
var menuParent = item.menuSource_otherObjectChildren != null
? item.menuSource_otherObjectChildren
: item.gameObject;
menu = new MenuNodesUnder(menuParent);
}
}
else if (menu is ModularAvatarMenuGroup group)
{
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
else menu = new MenuNodesUnder(group.gameObject);
}
if (menu is VRCExpressionsMenu expMenu)
{
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
else installTo.objectReferenceValue = expMenu;
}
else if (menu is RootMenu)
{
installTo.objectReferenceValue = null;
}
else if (menu is MenuNodesUnder nodesUnder)
{
installTo.objectReferenceValue = null;
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
{
var installer = (ModularAvatarMenuInstaller)target;
var child = new GameObject();
Undo.RegisterCreatedObjectUndo(child, "Set install target");
child.transform.SetParent(nodesUnder.root.transform, false);
child.name = installer.gameObject.name;
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
targetComponent.installer = installer;
EditorGUIUtility.PingObject(child);
}
}
serializedObject.ApplyModifiedProperties();
VirtualMenu.InvalidateCaches();
Repaint();
});
}
private string ObjectHierarchyOrder(Component arg)
@ -415,6 +443,9 @@ namespace nadena.dev.modular_avatar.core.editor
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
var menuRoot = new GameObject();
menuRoot.name = "Menu";
group.targetObject = menuRoot;
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
menuRoot.transform.SetParent(group.transform, false);
foreach (var control in menu.controls)

View File

@ -2,10 +2,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using nadena.dev.modular_avatar.core.menu;
using nadena.dev.ndmf;
using nadena.dev.ndmf.preview;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -21,8 +23,44 @@ namespace nadena.dev.modular_avatar.core.editor
protected override string localizationPrefix => "submenu_source";
}
internal static class ParameterIntrospectionCache
{
internal static PropCache<GameObject, ImmutableList<ProvidedParameter>> ProvidedParameterCache =
new("GetParametersForObject", GetParametersForObject_miss);
internal static PropCache<GameObject, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>>
ParameterRemappingCache = new("GetParameterRemappingsAt", GetParameterRemappingsAt_miss);
private static ImmutableList<ProvidedParameter> GetParametersForObject_miss(ComputeContext ctx, GameObject obj)
{
if (obj == null) return ImmutableList<ProvidedParameter>.Empty;
return ParameterInfo.ForPreview(ctx).GetParametersForObject(obj).ToImmutableList();
}
private static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>
GetParameterRemappingsAt_miss(ComputeContext ctx, GameObject obj)
{
if (obj == null) return ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>.Empty;
return ParameterInfo.ForPreview(ctx).GetParameterRemappingsAt(obj);
}
internal static ImmutableList<ProvidedParameter> GetParametersForObject(GameObject avatar)
{
return ProvidedParameterCache.Get(ComputeContext.NullContext, avatar);
}
internal static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> GetParameterRemappingsAt(GameObject avatar)
{
return ParameterRemappingCache.Get(ComputeContext.NullContext, avatar);
}
}
internal class MenuItemCoreGUI
{
private const string ImpliesRichText = "<";
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
private readonly GameObject _parameterReference;
private readonly Action _redraw;
@ -54,12 +92,16 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _prop_isSynced;
private readonly SerializedProperty _prop_isSaved;
private readonly SerializedProperty _prop_isDefault;
private readonly SerializedProperty _prop_automaticValue;
private readonly SerializedProperty _prop_label;
public bool AlwaysExpandContents = false;
public bool ExpandContents = false;
private readonly Dictionary<string, ProvidedParameter> _knownParameters = new();
private bool _parameterSourceNotDetermined;
private bool _useLabel;
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
{
@ -105,6 +147,9 @@ namespace nadena.dev.modular_avatar.core.editor
_prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
_prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue));
_prop_label = obj.FindProperty(nameof(ModularAvatarMenuItem.label));
_previewGUI = new MenuPreviewGUI(redraw);
}
@ -132,12 +177,17 @@ namespace nadena.dev.modular_avatar.core.editor
Dictionary<string, ProvidedParameter> rootParameters = new();
foreach (var param in ParameterInfo.ForUI.GetParametersForObject(parentAvatar.gameObject)
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
.Where(p => p.Namespace == ParameterNamespace.Animator)
)
{
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
{
rootParameters[param.EffectiveName] = param;
}
}
var remaps = ParameterInfo.ForUI.GetParameterRemappingsAt(paramRef);
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
foreach (var remap in remaps)
{
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
@ -180,6 +230,7 @@ namespace nadena.dev.modular_avatar.core.editor
_prop_isSynced = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isDefault));
_prop_automaticValue = null;
_prop_submenuSource = null;
_prop_otherObjSource = null;
@ -202,35 +253,88 @@ namespace nadena.dev.modular_avatar.core.editor
if (forceMixedValues != null) EditorGUI.showMixedValue = forceMixedValues.Value;
if (forceValue != null)
{
EditorGUI.ToggleLeft(rect, label, forceValue.Value);
}
else
{
EditorGUI.BeginChangeCheck();
var value = EditorGUI.ToggleLeft(rect, label, prop.boolValue);
var value = EditorGUI.ToggleLeft(rect, label, forceValue ?? prop.boolValue);
if (EditorGUI.EndChangeCheck()) prop.boolValue = value;
}
EditorGUI.EndProperty();
}
public void DoGUI()
{
if (_obj != null) _obj.UpdateIfRequiredOrScript();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
if (_prop_label == null)
{
EditorGUI.BeginChangeCheck();
if (_obj != null && _obj.isEditingMultipleObjects)
{
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
}
else
{
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
}
if (EditorGUI.EndChangeCheck())
{
_name.serializedObject.ApplyModifiedProperties();
}
}
else
{
_useLabel |= !string.IsNullOrEmpty(_prop_label.stringValue);
if (!_useLabel)
{
EditorGUI.BeginChangeCheck();
var previousName = _name.stringValue;
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
if (EditorGUI.EndChangeCheck())
{
if (!previousName.Contains(ImpliesRichText) && _name.stringValue.Contains(ImpliesRichText))
{
_prop_label.stringValue = _name.stringValue;
}
else
{
_name.serializedObject.ApplyModifiedProperties();
}
}
}
else
{
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
}
var linkIcon = EditorGUIUtility.IconContent(_useLabel ? "UnLinked" : "Linked").image;
var guiIcon = new GUIContent(linkIcon, S(_useLabel ? "menuitem.label.gameobject_name.tooltip" : "menuitem.label.long_name.tooltip"));
if (GUILayout.Button(guiIcon, GUILayout.Height(EditorGUIUtility.singleLineHeight), GUILayout.Width(25)))
{
_prop_label.stringValue = !_useLabel ? _name.stringValue : "";
_useLabel = !_useLabel;
}
}
EditorGUILayout.EndHorizontal();
if (_useLabel && _prop_label.stringValue.Contains(ImpliesRichText))
{
var style = new GUIStyle(EditorStyles.textField);
style.richText = true;
style.alignment = TextAnchor.MiddleCenter;
EditorGUILayout.LabelField(" ", _prop_label.stringValue, style, GUILayout.Height(EditorGUIUtility.singleLineHeight * 3));
}
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
DoValueField();
_parameterGUI.DoGUI(true);
@ -267,10 +371,9 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.BeginVertical();
if (_type.hasMultipleDifferentValues) return;
VRCExpressionsMenu.Control.ControlType type =
(VRCExpressionsMenu.Control.ControlType) Enum
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
.GetValue(_type.enumValueIndex);
var controlTypeArray = Enum.GetValues(typeof(VRCExpressionsMenu.Control.ControlType));
var index = Math.Clamp(_type.enumValueIndex, 0, controlTypeArray.Length - 1);
var type = (VRCExpressionsMenu.Control.ControlType)controlTypeArray.GetValue(index);
switch (type)
{
@ -436,22 +539,44 @@ namespace nadena.dev.modular_avatar.core.editor
// For now, don't show the UI in this case.
return;
var multipleSelections = _obj.targetObjects.Length > 1;
var paramName = _parameterName.stringValue;
var siblings = FindSiblingMenuItems(_obj);
EditorGUILayout.BeginHorizontal();
bool? forceMixedValues = _parameterName.hasMultipleDifferentValues ? true : null;
var forceMixedValues = _parameterName.hasMultipleDifferentValues;
var syncedIsMixed = forceMixedValues || _prop_isSynced.hasMultipleDifferentValues ||
siblings != null && siblings.Any(s => s.isSynced != _prop_isSynced.boolValue);
var savedIsMixed = forceMixedValues || _prop_isSaved.hasMultipleDifferentValues ||
siblings != null && siblings.Any(s => s.isSaved != _prop_isSaved.boolValue);
var knownParameter = _parameterName.hasMultipleDifferentValues
? null
: _knownParameters.GetValueOrDefault(paramName);
var knownSource = knownParameter?.Source;
var externalSource = knownSource != null && knownSource is not ModularAvatarMenuItem;
if (externalSource) savedIsMixed = true; // NDMF doesn't yet support querying for the saved state
var forceSyncedValue = externalSource ? knownParameter?.WantSynced : null;
var knownParamDefault = knownParameter?.DefaultValue;
var isDefaultByKnownParam =
knownParamDefault != null ? _value.floatValue == knownParamDefault : (bool?)null;
if (knownParameter != null && knownParameter.Source is ModularAvatarMenuItem)
isDefaultByKnownParam = null;
if (_prop_automaticValue?.boolValue == true) isDefaultByKnownParam = null;
Object controller = knownParameter?.Source;
var controllerIsElsewhere = controller != null && !(controller is ModularAvatarMenuItem);
// If we can't figure out what to reference the parameter names to, disable the UI
controllerIsElsewhere = controllerIsElsewhere || _parameterSourceNotDetermined;
// If we can't figure out what to reference the parameter names to, or if they're controlled by something
// other than the Menu Item component itself, disable the UI
var controllerIsElsewhere = externalSource || _parameterSourceNotDetermined;
using (new EditorGUI.DisabledScope(
_parameterName.hasMultipleDifferentValues || controllerIsElsewhere)
@ -460,22 +585,47 @@ namespace nadena.dev.modular_avatar.core.editor
// If we have multiple menu items selected, it probably doesn't make sense to make them all default.
// But, we do want to see if _any_ are default.
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
var multipleSelections = _obj.targetObjects.Length > 1;
var mixedIsDefault = multipleSelections && anyIsDefault;
using (new EditorGUI.DisabledScope(multipleSelections))
var allAreAutoParams = !_parameterName.hasMultipleDifferentValues &&
string.IsNullOrWhiteSpace(_parameterName.stringValue);
using (new EditorGUI.DisabledScope((!allAreAutoParams && multipleSelections) ||
isDefaultByKnownParam != null))
{
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
multipleSelections ? false : isDefaultByKnownParam);
isDefaultByKnownParam);
if (EditorGUI.EndChangeCheck())
{
_obj.ApplyModifiedProperties();
ClearConflictingDefaults(siblings);
}
}
GUILayout.FlexibleSpace();
var isSavedMixed = forceMixedValues ??
(_parameterName.hasMultipleDifferentValues || controllerIsElsewhere ? true : null);
DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), isSavedMixed);
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), savedIsMixed);
if (EditorGUI.EndChangeCheck() && siblings != null)
foreach (var sibling in siblings)
{
sibling.isSaved = _prop_isSaved.boolValue;
EditorUtility.SetDirty(sibling);
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
}
GUILayout.FlexibleSpace();
DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), forceMixedValues,
knownParameter?.WantSynced);
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), syncedIsMixed,
forceSyncedValue);
if (EditorGUI.EndChangeCheck() && siblings != null)
foreach (var sibling in siblings)
{
sibling.isSynced = _prop_isSynced.boolValue;
EditorUtility.SetDirty(sibling);
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
}
}
if (controllerIsElsewhere)
@ -514,6 +664,111 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.EndHorizontal();
}
private void DoValueField()
{
var value_label = G("menuitem.prop.value");
var auto_label = G("menuitem.prop.automatic_value");
if (_prop_automaticValue == null)
{
EditorGUILayout.PropertyField(_value, value_label);
return;
}
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
var autoLabelSize = EditorStyles.label.CalcSize(auto_label);
var style = EditorStyles.numberField;
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, style);
var valueRect = rect;
valueRect.xMax -= toggleSize.x + autoLabelSize.x + 4;
var autoRect = rect;
autoRect.xMin = valueRect.xMax + 4;
var suppressValue = _prop_automaticValue.boolValue || _prop_automaticValue.hasMultipleDifferentValues;
using (new EditorGUI.DisabledScope(suppressValue))
{
if (suppressValue)
{
EditorGUI.TextField(valueRect, value_label, "", style);
}
else
{
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(valueRect, _value, value_label);
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = false;
}
}
EditorGUI.BeginProperty(autoRect, auto_label, _prop_automaticValue);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = _prop_automaticValue.hasMultipleDifferentValues;
var autoValue = EditorGUI.ToggleLeft(autoRect, auto_label, _prop_automaticValue.boolValue);
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = autoValue;
EditorGUI.EndProperty();
}
private List<ModularAvatarMenuItem> FindSiblingMenuItems(SerializedObject serializedObject)
{
if (serializedObject == null || serializedObject.isEditingMultipleObjects) return null;
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
if (myMenuItem == null) return null;
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
if (avatarRoot == null) return null;
var myParameterName = myMenuItem.Control.parameter.name;
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
var myMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(myMenuItem.gameObject);
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
myParameterName = myReplacement.ParameterName;
var siblings = new List<ModularAvatarMenuItem>();
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))
{
if (otherMenuItem == myMenuItem) continue;
var otherParameterName = otherMenuItem.Control.parameter.name;
if (string.IsNullOrEmpty(otherParameterName)) continue;
var otherMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(otherMenuItem.gameObject);
if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName),
out var otherReplacement))
otherParameterName = otherReplacement.ParameterName;
if (otherParameterName != myParameterName) continue;
siblings.Add(otherMenuItem);
}
return siblings;
}
private void ClearConflictingDefaults(List<ModularAvatarMenuItem> siblingItems)
{
var siblings = siblingItems;
if (siblings == null) return;
foreach (var otherMenuItem in siblings)
{
if (otherMenuItem.isDefault)
{
Undo.RecordObject(otherMenuItem, "");
otherMenuItem.isDefault = false;
EditorUtility.SetDirty(otherMenuItem);
PrefabUtility.RecordPrefabInstancePropertyModifications(otherMenuItem);
}
}
}
private void EnsureLabelCount(int i)
{
if (_labels == null || _labelsRoot.arraySize < i || _labels.Length < i)

View File

@ -212,7 +212,10 @@ namespace nadena.dev.modular_avatar.core.editor
var newChild = new GameObject();
newChild.name = "New item";
newChild.transform.SetParent(nodesUnder.root.transform, false);
newChild.AddComponent<ModularAvatarMenuItem>();
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
Undo.RegisterCreatedObjectUndo(newChild, "Added menu item");
}
@ -223,13 +226,12 @@ namespace nadena.dev.modular_avatar.core.editor
newChild.transform.SetParent(nodesUnder.root.transform, false);
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control()
{
type = VRCExpressionsMenu.Control.ControlType.Toggle,
value = 1,
};
mami.isSaved = true;
mami.isSynced = true;
newChild.AddComponent<ModularAvatarObjectToggle>();

View File

@ -1,4 +1,8 @@
using nadena.dev.modular_avatar.ui;
#if MA_VRCSDK3_AVATARS
using System;
using System.Linq;
using System.Collections.Generic;
using nadena.dev.modular_avatar.ui;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
@ -7,43 +11,194 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal static class ToggleCreatorShortcut
{
[MenuItem(UnityMenuItems.GameObject_CreateToggleForSelection, false, UnityMenuItems.GameObject_CreateToggleForSelectionOrder)]
private static void CreateToggleForSelection()
{
var forSelection = true;
var selections = Selection.objects.OfType<GameObject>();
// Ignore GameObjects with submenu in the context of CreateToggleForSelection.
selections = selections.Where(s => !TryGetChildrenSourceSubmenu(s, out var _));
if (selections.Count() == 0) return;
// Grouping according to parent
var groups = new Dictionary<GameObject, HashSet<GameObject>>();
foreach (var selected in selections)
{
var parent = selected.transform.parent?.gameObject;
if (parent == null) continue;
if (!groups.ContainsKey(parent))
{
groups[parent] = new();
}
groups[parent].Add(selected);
}
foreach (var group in groups)
{
var parent = group.Key;
var targets = group.Value;
if (parent == null) continue;
if (targets == null || targets.Count() == 0) continue;
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(parent.transform);
if (avatarRoot == null) continue;
var subMenuName = parent.name + " Toggles";
// Try to find target submenu that should be the parent of toggles
ModularAvatarMenuItem targetSubMenu = null;
if (TryGetChildrenSourceSubmenu(parent, out var subMenu))
{
// If parent has subMenu, use it as target submenu.
targetSubMenu = subMenu;
}
else
{
// If parent hasn't subMenu, get submenus at the same level
var subMenus = new List<ModularAvatarMenuItem>();
foreach (Transform sibling in parent.transform)
{
if (TryGetChildrenSourceSubmenu(sibling.gameObject, out var m)) { subMenus.Add(m); }
}
// Filter to submenus with the same name
subMenus = subMenus.Where(m => m.gameObject.name == subMenuName).ToList();
// If only one submenu as target is found, use it as target submenu.
if (subMenus.Count() == 1) targetSubMenu = subMenus.First();
}
if (targetSubMenu != null) // If target SubMenu is found, add the toggles as children of it.
{
parent = targetSubMenu.gameObject;
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
}
else
{
if (targets.Count() > 1) // Create a submenu and add the toggles as children of it.
{
parent = CreateSubMenu(parent, subMenuName).gameObject;
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
}
else // Create a single toggle with installer.
{
var target = targets.First();
CreateToggleImpl(target, parent, forSelection, createInstaller:true);
}
}
}
Selection.objects = null;
}
[MenuItem(UnityMenuItems.GameObject_CreateToggle, false, UnityMenuItems.GameObject_CreateToggleOrder)]
private static void CreateToggle()
{
var selected = Selection.activeGameObject;
if (selected == null) return;
var selections = Selection.objects.OfType<GameObject>();
if (selections.Count() == 0) return;
foreach (var selected in selections)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
bool createInstaller = true;
Transform parent = avatarRoot;
var parent = avatarRoot.gameObject;
var createInstaller = true;
try
if (TryGetChildrenSourceSubmenu(selected, out var _))
{
var selectedMenuItem = selected.GetComponent<ModularAvatarMenuItem>();
if (selectedMenuItem?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& selectedMenuItem.MenuSource == SubmenuSource.Children
)
{
parent = selected.transform;
parent = selected;
createInstaller = false;
}
CreateToggleImpl(selected, parent, createInstaller:createInstaller);
}
catch (MissingComponentException e)
Selection.objects = null;
}
private static bool TryGetChildrenSourceSubmenu(GameObject target, out ModularAvatarMenuItem subMenu)
{
subMenu = null;
try
{
var mami = target.GetComponent<ModularAvatarMenuItem>();
if (mami?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& mami.MenuSource == SubmenuSource.Children
)
{
subMenu = mami;
return true;
}
}
catch (MissingComponentException)
{
// ignore
}
return false;
}
var toggle = new GameObject("New Toggle");
private static ModularAvatarMenuItem CreateSubMenu(GameObject parent, string submenuname)
{
var submenu = new GameObject(submenuname);
submenu.transform.SetParent(parent.transform);
var mami = submenu.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control
{
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
name = submenuname,
};
submenu.AddComponent<ModularAvatarMenuInstaller>();
Selection.activeGameObject = submenu;
EditorGUIUtility.PingObject(submenu);
Undo.RegisterCreatedObjectUndo(submenu, "Create SubMenu");
return mami;
}
private static void CreateToggleImpl(IEnumerable<GameObject> selections, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
foreach (var selected in selections)
{
CreateToggleImpl(selected, parent, forSelection, createInstaller);
}
}
private static void CreateToggleImpl(GameObject selected, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var suffix = selected.activeSelf ? "OFF" : "ON";
var name = forSelection ? $"{selected.name} {suffix}" : "New Toggle";
var toggle = new GameObject(name);
var objToggle = toggle.AddComponent<ModularAvatarObjectToggle>();
if (forSelection)
{
var path = RuntimeUtil.RelativePath(avatarRoot.gameObject, selected);
objToggle.Objects.Add(new ToggledObject
{
Object = new AvatarObjectReference(){ referencePath = path },
Active = !selected.activeSelf
});
}
toggle.transform.SetParent(parent, false);
toggle.AddComponent<ModularAvatarMenuItem>().Control = new VRCExpressionsMenu.Control
toggle.transform.SetParent(parent.transform, false);
var mami = toggle.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control
{
type = VRCExpressionsMenu.Control.ControlType.Toggle,
name = "New Toggle",
name = name,
value = 1,
};
@ -60,3 +215,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif

View File

@ -11,6 +11,12 @@ namespace nadena.dev.modular_avatar.core.editor
protected override string localizationPrefix => "path_mode";
}
[CustomPropertyDrawer(typeof(MergeAnimatorMode))]
internal class MergeModeDrawer : EnumDrawer<MergeAnimatorMode>
{
protected override string localizationPrefix => "merge_animator.merge_mode";
}
[CustomEditor(typeof(ModularAvatarMergeAnimator))]
class MergeAnimationEditor : MAEditorBase
{
@ -20,7 +26,8 @@ namespace nadena.dev.modular_avatar.core.editor
prop_pathMode,
prop_matchAvatarWriteDefaults,
prop_relativePathRoot,
prop_layerPriority;
prop_layerPriority,
prop_mergeMode;
private void OnEnable()
{
@ -34,6 +41,7 @@ namespace nadena.dev.modular_avatar.core.editor
prop_relativePathRoot =
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.relativePathRoot));
prop_layerPriority = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.layerPriority));
prop_mergeMode = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.mergeAnimatorMode));
}
protected override void OnInnerInspectorGUI()
@ -47,8 +55,12 @@ namespace nadena.dev.modular_avatar.core.editor
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
EditorGUILayout.PropertyField(prop_layerPriority, G("merge_animator.layer_priority"));
EditorGUILayout.PropertyField(prop_mergeMode, G("merge_animator.merge_mode"));
using (new EditorGUI.DisabledScope(prop_mergeMode.enumValueIndex == (int)MergeAnimatorMode.Replace))
{
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
G("merge_animator.match_avatar_write_defaults"));
}
serializedObject.ApplyModifiedProperties();

View File

@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
private bool posResetOptionFoldout = false;
private bool posReset_convertATPose = true;
private bool posReset_adjustRotation = false;
private bool posReset_adjustScale = false;
private bool posReset_heuristicRootScale = true;
@ -99,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
serializedObject.ApplyModifiedProperties();
if (target.mergeTargetObject != null && priorMergeTarget == null
if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject
&& string.IsNullOrEmpty(target.prefix)
&& string.IsNullOrEmpty(target.suffix))
{
@ -114,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (GUILayout.Button(G("merge_armature.adjust_names")))
{
HeuristicBoneMapper.RenameBonesByHeuristic(target);
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(target.mergeTarget.Get(target).transform);
var avatarAnimator = avatarRoot != null ? avatarRoot.GetComponent<Animator>() : null;
// Search Outfit Root Animator
var outfitRoot = ((ModularAvatarMergeArmature)serializedObject.targetObject).transform;
Animator outfitAnimator = null;
while (outfitRoot != null)
{
if (outfitRoot == avatarRoot)
{
outfitAnimator = null;
break;
}
outfitAnimator = outfitRoot.GetComponent<Animator>();
if (outfitAnimator != null && outfitAnimator.isHuman) break;
outfitAnimator = null;
outfitRoot = outfitRoot.parent;
}
var outfitHumanoidBones = SetupOutfit.GetOutfitHumanoidBones(outfitRoot, outfitAnimator);
HeuristicBoneMapper.RenameBonesByHeuristic(target, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
}
}
@ -134,14 +155,17 @@ namespace nadena.dev.modular_avatar.core.editor
MessageType.Info
);
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.heuristic_scale"),
posReset_heuristicRootScale);
posReset_convertATPose = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.convert_atpose"),
posReset_convertATPose);
posReset_adjustRotation = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.adjust_rotation"),
posReset_adjustRotation);
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
posReset_adjustScale);
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.heuristic_scale"),
posReset_heuristicRootScale);
if (GUILayout.Button(G("merge_armature.reset_pos.execute")))
{
@ -188,6 +212,11 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
if (posReset_convertATPose)
{
SetupOutfit.FixAPose(RuntimeUtil.FindAvatarTransformInParents(mergeTarget.transform).gameObject, mama.transform, false);
}
if (posReset_heuristicRootScale && !suppressRootScale)
{
AdjustRootScale();

View File

@ -1,7 +1,7 @@
#if MA_VRCSDK3_AVATARS
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
@ -15,7 +15,9 @@ namespace nadena.dev.modular_avatar.core.editor
private void OnEnable()
{
#pragma warning disable CS0618 // Type or member is obsolete
_blendTree = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.BlendTree));
#pragma warning restore CS0618 // Type or member is obsolete
_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.PathMode));
_relativePathRoot = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.RelativePathRoot));
}
@ -24,7 +26,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
serializedObject.Update();
EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), G("merge_blend_tree.blend_tree"));
EditorGUILayout.ObjectField(_blendTree, typeof(Motion), G("merge_blend_tree.motion"));
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
{

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.ArmatureAwase;
using nadena.dev.ndmf.preview;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
@ -15,15 +15,38 @@ namespace nadena.dev.modular_avatar.core.editor
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private ComputeContext _ctx;
private VisualElement _root;
private TransformChildrenNode _groupedNodesElem;
protected override void OnInnerInspectorGUI()
{
throw new System.NotImplementedException();
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
{
_root = new VisualElement();
RebuildInnerGUI();
return _root;
}
private void RebuildInnerGUI()
{
_root.Clear();
_ctx = new ComputeContext("MoveIndependentlyEditor");
_root.Add(BuildInnerGUI(_ctx));
}
private VisualElement BuildInnerGUI(ComputeContext ctx)
{
if (this.target == null) return new VisualElement();
_ctx.InvokeOnInvalidate(this, editor => editor.RebuildInnerGUI());
#pragma warning disable CS0618 // Type or member is obsolete
var root = uxml.Localize();
#pragma warning restore CS0618 // Type or member is obsolete
@ -32,9 +55,14 @@ namespace nadena.dev.modular_avatar.core.editor
var container = root.Q<VisualElement>("group-container");
MAMoveIndependently target = (MAMoveIndependently) this.target;
var grouped = (target.GroupedBones ?? Array.Empty<GameObject>())
// Note: We specifically _don't_ use an ImmutableHashSet here as we want to update the previously-returned
// set in place to avoid rebuilding GUI elements after the user changes the grouping.
var grouped = ctx.Observe(target,
t => (t.GroupedBones ?? Array.Empty<GameObject>())
.Select(obj => obj.transform)
.ToImmutableHashSet();
.ToHashSet(),
(x, y) => x.SetEquals(y)
);
_groupedNodesElem = new TransformChildrenNode(target.transform, grouped);
_groupedNodesElem.AddToClassList("group-root");
@ -43,6 +71,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
Undo.RecordObject(target, "Toggle grouped nodes");
target.GroupedBones = _groupedNodesElem.Active().Select(t => t.gameObject).ToArray();
grouped.Clear();
grouped.UnionWith(target.GroupedBones.Select(obj => obj.transform));
PrefabUtility.RecordPrefabInstancePropertyModifications(target);
};

View File

@ -1,6 +1,7 @@
#region
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
@ -16,10 +17,11 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
throw new NotImplementedException();
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
@ -37,7 +39,37 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
: base(targetElement, targetComponent) { }
protected override bool AllowKnownObjects => false;
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
foreach (var reference in references)
{
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}
}

View File

@ -1,7 +1,4 @@
VisualElement {
}
#group-box {
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
@ -14,101 +11,52 @@
/* background-color: rgba(0, 0, 0, 0.1); */
}
#ListViewContainer {
margin-top: 4px;
}
#group-box > Label {
-unity-font-style: bold;
}
.group-root {
#ListViewContainer {
margin-top: 4px;
}
.group-root Toggle {
margin-left: 0;
}
.group-children {
padding-left: 10px;
}
.left-toggle {
display: flex;
.horizontal {
flex-direction: row;
}
.toggled-object-editor {
flex-direction: row;
justify-content: center;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.toggled-object-editor #f-object {
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.horizontal > Label {
height: auto;
}
.horizontal > PropertyField > * {
margin: 0;
}
#f-object {
flex-grow: 1;
height: 100%;
}
#f-active > Toggle {
margin-top: 0;
margin-bottom: 0;
margin-left: -12px;
margin-right: 3px;
}
.toggled-object-editor PropertyField Label {
#f-active {
display: none;
}
#f-change-type {
width: 75px;
#f-active-dropdown {
width: 60px;
}
.f-value {
width: 40px;
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
#f-value-delete {
display: none;
}
.change-type-delete #f-value {
display: none;
}
.change-type-delete #f-value-delete {
display: flex;
}
/* Add shape window */
.add-shape-popup {
margin: 2px;
}
.vline {
width: 100%;
height: 4px;
border-top-width: 4px;
margin-top: 2px;
margin-bottom: 2px;
border-top-color: rgba(0, 0, 0, 0.2);
}
.add-shape-row {
flex-direction: row;
}
.add-shape-row Button {
flex-grow: 0;
}
.add-shape-popup ScrollView Label.placeholder {
-unity-text-align: middle-center;
}
.add-shape-row Label {
flex-grow: 1;
-unity-text-align: middle-left;
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -15,6 +15,9 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
private const string V_On = "ON";
private const string V_Off = "OFF";
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
@ -24,6 +27,21 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
uxml.styleSheets.Add(uss);
uxml.BindProperty(property);
var f_active = uxml.Q<Toggle>("f-active");
var f_active_dropdown = uxml.Q<DropdownField>("f-active-dropdown");
f_active_dropdown.choices.Add(V_On);
f_active_dropdown.choices.Add(V_Off);
f_active.RegisterValueChangedCallback(evt =>
{
f_active_dropdown.SetValueWithoutNotify(evt.newValue ? V_On : V_Off);
});
f_active_dropdown.RegisterValueChangedCallback(evt =>
{
f_active.value = evt.newValue == V_On;
});
return uxml;
}
}

View File

@ -1,6 +1,7 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="toggled-object-editor">
<ed:PropertyField binding-path="Active" label="" name="f-active"/>
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ui:VisualElement class="horizontal">
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
<ui:Toggle name="f-active" binding-path="Active" label=""/>
<ui:DropdownField name="f-active-dropdown"/>
</ui:VisualElement>
</UXML>

View File

@ -6,7 +6,10 @@ using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Button = UnityEngine.UIElements.Button;
using Image = UnityEngine.UIElements.Image;
namespace nadena.dev.modular_avatar.core.editor
{
@ -35,6 +38,37 @@ namespace nadena.dev.modular_avatar.core.editor
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
listView.selectionType = SelectionType.Multiple;
listView.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
{
serializedObject.Update();
var prop = serializedObject.FindProperty("parameters");
var indices = listView.selectedIndices.ToList();
foreach (var index in indices.OrderByDescending(i => i))
{
prop.DeleteArrayElementAtIndex(index);
}
serializedObject.ApplyModifiedProperties();
if (indices.Count == 0)
{
EditorApplication.delayCall += () =>
{
// Works around an issue where the inner text boxes are auto-selected, preventing you from
// just hitting delete over and over
listView.SetSelectionWithoutNotify(indices);
};
}
evt.StopPropagation();
}
}, TrickleDown.NoTrickleDown);
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
@ -129,9 +163,68 @@ namespace nadena.dev.modular_avatar.core.editor
}
};
var importProp = root.Q<ObjectField>("p_import");
importProp.RegisterValueChangedCallback(evt =>
{
ImportValues(importProp);
importProp.SetValueWithoutNotify(null);
});
importProp.objectType = typeof(VRCExpressionParameters);
importProp.allowSceneObjects = false;
return root;
}
private void ImportValues(ObjectField importProp)
{
var known = new HashSet<string>();
var target = (ModularAvatarParameters)this.target;
foreach (var parameter in target.parameters)
{
if (!parameter.isPrefix)
{
known.Add(parameter.nameOrPrefix);
}
}
Undo.RecordObject(target, "Import parameters");
var source = (VRCExpressionParameters)importProp.value;
if (source == null)
{
return;
}
foreach (var parameter in source.parameters)
{
if (!known.Contains(parameter.name))
{
ParameterSyncType pst;
switch (parameter.valueType)
{
case VRCExpressionParameters.ValueType.Bool: pst = ParameterSyncType.Bool; break;
case VRCExpressionParameters.ValueType.Float: pst = ParameterSyncType.Float; break;
case VRCExpressionParameters.ValueType.Int: pst = ParameterSyncType.Int; break;
default: pst = ParameterSyncType.Float; break;
}
target.parameters.Add(new ParameterConfig()
{
internalParameter = false,
nameOrPrefix = parameter.name,
isPrefix = false,
remapTo = "",
syncType = pst,
localOnly = !parameter.networkSynced,
defaultValue = parameter.defaultValue,
saved = parameter.saved,
});
}
}
}
private void DetectParameters()
{
var known = new HashSet<string>();

View File

@ -1,6 +1,4 @@
using System.Globalization;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
@ -12,97 +10,125 @@ namespace nadena.dev.modular_avatar.core.editor
{
}
private const string V_None = " ";
private const string V_True = "ON";
private const string V_False = "OFF";
private readonly TextField _visibleField;
private readonly FloatField _defaultValueField;
private readonly Toggle _hasExplicitDefaultValueField;
private readonly TextField _numberField;
private readonly DropdownField _boolField;
private readonly Toggle _hasExplicitDefaultSetField;
private ParameterSyncType _syncType;
private bool _hasInitialBinding;
public DefaultValueField()
{
// Hidden binding elements
_defaultValueField = new FloatField();
_hasExplicitDefaultSetField = new Toggle();
_boolField = new DropdownField();
_defaultValueField.style.display = DisplayStyle.None;
_defaultValueField.bindingPath = nameof(ParameterConfig.defaultValue);
_defaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultValueField.value));
_hasExplicitDefaultValueField = new Toggle();
_hasExplicitDefaultValueField.style.display = DisplayStyle.None;
_hasExplicitDefaultValueField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue);
_hasExplicitDefaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(_defaultValueField.value, evt.newValue));
_boolField.choices.Add("");
// Visible elements for input
_numberField = new TextField();
_numberField.isDelayed = true;
_numberField.RegisterValueChangedCallback(evt => OnUpdateNumberValue(evt.newValue));
_boolField = new DropdownField();
_boolField.choices.Add(V_None);
_boolField.choices.Add(V_True);
_boolField.choices.Add(V_False);
_boolField.RegisterValueChangedCallback(evt => OnUpdateBoolValue(evt.newValue));
_defaultValueField.RegisterValueChangedCallback(
evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultSetField.value));
_defaultValueField.bindingPath = nameof(ParameterConfig.defaultValue);
_hasExplicitDefaultSetField.RegisterValueChangedCallback(
evt => UpdateVisibleField(_defaultValueField.value, evt.newValue));
_hasExplicitDefaultSetField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue);
_visibleField = new TextField();
_visibleField.RegisterValueChangedCallback(evt =>
{
if (string.IsNullOrWhiteSpace(evt.newValue))
{
_hasExplicitDefaultSetField.value = false;
_defaultValueField.value = 0;
}
else
{
_hasExplicitDefaultSetField.value = true;
_defaultValueField.value = float.Parse(evt.newValue, CultureInfo.InvariantCulture);
}
});
_defaultValueField.style.width = 0;
_defaultValueField.SetEnabled(false);
_hasExplicitDefaultSetField.style.width = 0;
_hasExplicitDefaultSetField.SetEnabled(false);
_boolField.RegisterValueChangedCallback(evt =>
{
if (evt.newValue == V_True)
_defaultValueField.value = 1;
else
_defaultValueField.value = 0;
_hasExplicitDefaultSetField.value = evt.newValue != "";
});
style.flexDirection = FlexDirection.Row;
Add(_visibleField);
Add(_boolField);
Add(_defaultValueField);
Add(_hasExplicitDefaultSetField);
Add(_hasExplicitDefaultValueField);
Add(_numberField);
Add(_boolField);
}
public void ManualBindProperty(SerializedProperty property)
public void OnUpdateSyncType(ParameterSyncType syncType)
{
_defaultValueField.BindProperty(property);
_hasExplicitDefaultSetField.BindProperty(property);
_syncType = syncType;
if (syncType != ParameterSyncType.Bool)
{
_numberField.style.display = DisplayStyle.Flex;
_boolField.style.display = DisplayStyle.None;
OnUpdateNumberValue(_numberField.value, true);
}
else
{
_numberField.style.display = DisplayStyle.None;
_boolField.style.display = DisplayStyle.Flex;
OnUpdateBoolValue(_boolField.value, true);
}
}
private void OnUpdateNumberValue(string value, bool implicitUpdate = false)
{
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
// In this case, suppress the update to avoid losing data.
if (implicitUpdate && !_hasInitialBinding) return;
var theValue = _defaultValueField.value;
if (string.IsNullOrWhiteSpace(value))
{
if (!implicitUpdate)
{
_defaultValueField.value = 0;
}
theValue = _defaultValueField.value;
_hasExplicitDefaultValueField.value = false;
}
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
&& !float.IsNaN(parsed)
&& !float.IsInfinity(parsed))
{
theValue = _defaultValueField.value = _syncType switch
{
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
ParameterSyncType.Bool => parsed != 0 ? 1 : 0,
_ => parsed,
};
_hasExplicitDefaultValueField.value = true;
}
UpdateVisibleField(theValue, _hasExplicitDefaultValueField.value);
}
private void OnUpdateBoolValue(string value, bool implicitUpdate = false)
{
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
// In this case, suppress the update to avoid losing data.
if (implicitUpdate && !_hasInitialBinding) return;
_defaultValueField.value = value == V_True ? 1 : 0;
_hasExplicitDefaultValueField.value = value != V_None;
UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value);
}
private void UpdateVisibleField(float value, bool hasExplicitValue)
{
if (Mathf.Abs(value) > 0.0000001)
_hasInitialBinding = true;
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
{
hasExplicitValue = true;
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));
_boolField.SetValueWithoutNotify(value != 0 ? V_True : V_False);
}
var str = hasExplicitValue ? value.ToString(CultureInfo.InvariantCulture) : "";
_visibleField.SetValueWithoutNotify(str);
string boolStr;
if (!hasExplicitValue)
boolStr = "";
else if (value > 0.5)
boolStr = V_True;
else
boolStr = V_False;
_boolField.SetValueWithoutNotify(boolStr);
{
_numberField.SetValueWithoutNotify(string.Empty);
_boolField.SetValueWithoutNotify(V_None);
}
}
}
}

View File

@ -2,6 +2,7 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
@ -20,15 +21,14 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
Localization.UI.Localize(root);
root.styleSheets.Add(uss);
var proot = root.Q<VisualElement>("Root");
var type_field = proot.Q<DropdownField>("f-type");
var f_sync_type = proot.Q<VisualElement>("f-sync-type");
var f_type = root.Q<DropdownField>("f-type");
var f_sync_type = root.Q<DropdownField>("f-sync-type");
var f_is_prefix = root.Q<VisualElement>("f-is-prefix");
SetupPairedDropdownField(
proot,
type_field,
root,
f_type,
f_sync_type,
proot.Q<VisualElement>("f-is-prefix"),
f_is_prefix,
("Bool", "False", "params.syncmode.Bool"),
("Float", "False", "params.syncmode.Float"),
("Int", "False", "params.syncmode.Int"),
@ -36,54 +36,61 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
(null, "True", "params.syncmode.PhysBonesPrefix")
);
f_sync_type.Q<DropdownField>().RegisterValueChangedCallback(evt =>
{
var is_anim_only = evt.newValue == "Not Synced";
var f_default = root.Q<DefaultValueField>();
f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index);
f_sync_type.RegisterValueChangedCallback(evt => f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index));
if (is_anim_only)
proot.AddToClassList("st-anim-only");
else
proot.RemoveFromClassList("st-anim-only");
});
var f_synced = proot.Q<Toggle>("f-synced");
var f_local_only = proot.Q<Toggle>("f-local-only");
var f_synced = root.Q<Toggle>("f-synced");
var f_local_only = root.Q<Toggle>("f-local-only");
// Invert f_local_only and f_synced
f_local_only.RegisterValueChangedCallback(evt => { f_synced.SetValueWithoutNotify(!evt.newValue); });
f_synced.RegisterValueChangedCallback(evt => { f_local_only.value = !evt.newValue; });
var internalParamAccessor = proot.Q<Toggle>("f-internal-parameter");
var internalParamAccessor = root.Q<Toggle>("f-internal-parameter");
internalParamAccessor.RegisterValueChangedCallback(evt =>
{
if (evt.newValue)
proot.AddToClassList("st-internal-parameter");
root.AddToClassList("st-internal-parameter");
else
proot.RemoveFromClassList("st-internal-parameter");
root.RemoveFromClassList("st-internal-parameter");
});
var remapTo = proot.Q<TextField>("f-remap-to");
var defaultParam = proot.Q<Label>("f-default-param");
var name = proot.Q<TextField>("f-name");
var remapToInner = remapTo.Q<TextElement>();
root.Q<VisualElement>("remap-to-group-disabled").SetEnabled(false);
Action updateDefaultParam = () =>
var name = root.Q<TextField>("f-name");
var remapTo = root.Q<TextField>("f-remap-to");
var remapToInner = remapTo.Q<TextElement>();
var remapToPlaceholder = root.Q<Label>("f-remap-to-placeholder");
remapToPlaceholder.pickingMode = PickingMode.Ignore;
Action updateRemapToPlaceholder = () =>
{
if (string.IsNullOrWhiteSpace(remapTo.value))
defaultParam.text = name.value;
remapToPlaceholder.text = name.value;
else
defaultParam.text = "";
remapToPlaceholder.text = "";
};
name.RegisterValueChangedCallback(evt => { updateDefaultParam(); });
name.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
remapTo.RegisterValueChangedCallback(evt => { updateDefaultParam(); });
remapTo.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
defaultParam.RemoveFromHierarchy();
remapToInner.Add(defaultParam);
remapToPlaceholder.RemoveFromHierarchy();
remapToInner.Add(remapToPlaceholder);
updateDefaultParam();
updateRemapToPlaceholder();
foreach (var elem in root.Query<TextElement>().Build())
{
// Prevent delete keypresses from bubbling up if we're in a text field
elem.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
evt.StopPropagation();
});
}
return root;
}
@ -158,9 +165,6 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
var p_type = GetAccessor(v_type);
var p_prefix = GetAccessor(v_pbPrefix);
v_type.style.display = DisplayStyle.None;
v_pbPrefix.style.display = DisplayStyle.None;
for (var i = 0; i < choices.Length; i++) target.choices.Add("" + i);
target.formatListItemCallback = s_n =>

View File

@ -1,48 +1,38 @@
<ui:UXML
xmlns:ui="UnityEngine.UIElements"
xmlns:ma="nadena.dev.modular_avatar.core.editor"
editor-extension-mode="False"
>
<ui:VisualElement name="Root">
<ui:VisualElement class="horizontal no-label">
<ui:TextField binding-path="nameOrPrefix" label="merge_parameter.ui.name" name="f-name" class="ndmf-tr"/>
<ui:DropdownField name="f-type"/>
</ui:VisualElement>
<ui:Toggle binding-path="isPrefix" name="f-is-prefix"/>
<ui:DropdownField binding-path="syncType" name="f-sync-type"/>
<ui:VisualElement class="horizontal small-label">
<ui:Toggle binding-path="internalParameter" name="f-internal-parameter"
text="merge_parameter.ui.internalParameter" class="ndmf-tr no-left-margin"/>
<ui:VisualElement class="v-separator hide-with-internal-param">
<ui:VisualElement/>
</ui:VisualElement>
<ui:Label text="merge_parameter.ui.remapTo"
class="ndmf-tr inner-label hide-with-internal-param no-left-margin"/>
<ui:TextField name="f-remap-to" binding-path="remapTo" class="hide-with-internal-param"/>
<ui:Label name="f-default-param" text="test test test"/>
</ui:VisualElement>
<ui:VisualElement class="horizontal small-label st-pb-prefix__hide ">
<ui:UXML xmlns:ui="UnityEngine.UIElements"
xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement class="horizontal">
<ui:Label text="merge_parameter.ui.defaultValue" class="ndmf-tr no-left-margin"/>
<ui:TextField name="f-name" binding-path="nameOrPrefix" label=""/>
<ui:DropdownField name="f-type"/>
<ui:DropdownField name="f-sync-type" binding-path="syncType"/>
<ui:Toggle name="f-is-prefix" binding-path="isPrefix"/>
</ui:VisualElement>
<ui:VisualElement class="horizontal">
<ui:VisualElement name="remap-to-group-disabled" class="horizontal">
<ui:Label text="merge_parameter.ui.remapTo" class="ndmf-tr"/>
<ui:TextField name="f-remap-to-disabled"/>
</ui:VisualElement>
<ui:VisualElement name="remap-to-group" class="horizontal">
<ui:Label text="merge_parameter.ui.remapTo" class="ndmf-tr"/>
<ui:TextField name="f-remap-to" binding-path="remapTo"/>
<ui:Label name="f-remap-to-placeholder"/>
</ui:VisualElement>
<ui:Toggle name="f-internal-parameter" binding-path="internalParameter"
text="merge_parameter.ui.internalParameter" class="ndmf-tr"/>
</ui:VisualElement>
<ui:VisualElement class="horizontal st-pb-prefix__hide">
<ui:VisualElement class="horizontal">
<ui:Label text="merge_parameter.ui.defaultValue" class="ndmf-tr"/>
<ma:DefaultValueField/>
</ui:VisualElement>
<ui:VisualElement class="horizontal st-anim-only__hide">
<ui:VisualElement class="v-separator">
<ui:VisualElement/>
<ui:Toggle binding-path="saved"
text="merge_parameter.ui.saved" class="ndmf-tr st-anim-only__hide"/>
<ui:Toggle name="f-local-only" binding-path="localOnly"
text="merge_parameter.ui.localOnly" class="ndmf-tr st-anim-only__hide"/>
<ui:Toggle name="f-synced"
text="merge_parameter.ui.synced" class="ndmf-tr st-anim-only__hide"/>
<ui:Toggle binding-path="m_overrideAnimatorDefaults"
text="merge_parameter.ui.overrideAnimatorDefaults" class="ndmf-tr st-anim-only__hide"/>
</ui:VisualElement>
<ui:Toggle binding-path="saved" text="merge_parameter.ui.saved"
class="ndmf-tr st-pb-prefix__first-retained"/>
<ui:Toggle binding-path="localOnly" text="merge_parameter.ui.localOnly" class="ndmf-tr"
name="f-local-only"/>
<ui:Toggle text="merge_parameter.ui.synced" class="ndmf-tr" name="f-synced"/>
<ui:Toggle binding-path="m_overrideAnimatorDefaults" text="merge_parameter.ui.overrideAnimatorDefaults"
class="ndmf-tr"/>
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@ -1,120 +1,41 @@
VisualElement {}
/* I hate CSS precedence rules... */
.horizontal .no-left-margin {
margin-left: 0 !important;
#ListViewContainer {
margin-top: 4px;
max-height: 500px;
}
.horizontal .no-left-margin.unity-label {
margin-left: 0 !important;
.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.horizontal .no-left-margin Label.unity-label {
margin-left: 0 !important;
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.horizontal > Label {
height: auto;
}
.horizontal > * {
margin-top: 0;
margin-bottom: 0;
.horizontal > PropertyField > * {
margin: 0;
}
.v-separator {
width: 1px;
height: 100%;
margin-left: 8px;
margin-right: 8px;
justify-content: center;
align-content: center;
flex-shrink: 0;
}
.v-separator VisualElement {
width: 100%;
height: 80%;
background-color: rgba(0, 0, 0, 0.4);
}
.horizontal TextField {
margin-left: 0px;
}
.horizontal TextField Label.unity-label {
margin-left: 0px !important;
}
.horizontal Label {
padding-top: 0;
padding-bottom: 0;
align-self: center;
}
.horizontal {
flex-direction: row;
align-content: center;
margin-top: 1px;
}
.horizontal > * {
height: 100%;
}
.no-label Label.unity-base-field__label {
display: none;
}
#Root .horizontal #f-rename-destination {
#f-name {
flex-grow: 1;
}
.inner-label > Label {
margin-left: 6px;
#f-sync-type {
display: none;
}
.small-label Label.unity-label {
min-width: 0;
margin-left: 4px;
}
VisualElement.small-label > * {
flex-grow: 0;
}
VisualElement.small-label > PropertyField {
flex-direction: row;
}
#Root #f-name {
flex-grow: 1;
}
#Root DefaultValueField {
width: 60px;
flex-grow: 0;
}
.st-internal-parameter .hide-with-internal-param {
.st-ty-Not-Synced .st-anim-only__hide {
display: none;
}
DefaultValueField DropdownField {
display: none;
}
.st-ty-Bool DefaultValueField DropdownField {
display: flex;
}
.st-ty-Bool DefaultValueField TextField {
display: none;
}
.st-ty-NotSynced DefaultValueField {
#f-is-prefix {
display: none;
}
@ -122,44 +43,55 @@ DefaultValueField DropdownField {
display: none;
}
.st-anim-only .st-anim-only__hide {
#f-remap-to, #f-remap-to-disabled {
flex-grow: 1;
}
#f-remap-to-placeholder {
width: 100%;
height: 100%;
color: rgba(255, 255, 255, 0.4);
}
#f-internal-parameter {
margin-left: 3px;
}
#remap-to-group {
display: flex;
flex-grow: 1;
}
#remap-to-group-disabled {
display: none;
flex-grow: 1;
}
.st-internal-parameter #remap-to-group {
display: none;
}
.st-anim-only .st-pb-prefix__first-retained {
margin-left: 0;
.st-internal-parameter #remap-to-group-disabled {
display: flex;
}
.st-anim-only .st-pb-prefix__first-retained Label.unity-label {
margin-left: 0;
.horizontal > .horizontal {
flex-shrink: 0;
margin: 0;
}
#f-remap-to {
flex-grow: 1;
DefaultValueField > * {
width: 60px;
height: 100%;
margin: 0;
}
#f-local-only {
display: none;
}
DefaultValueField TextInput {
min-width: 30px;
}
/** Ghostly text for the renameTo text box **/
Label#f-default-param {
position: absolute;
width: 100%;
height: 100%;
margin: 0 0 0 0;
overflow: hidden;
color: rgba(255, 255, 255, 0.4) !important;
}
.DetectedParameter {
flex-direction: row;
margin-top: 2px;
margin-bottom: 2px;
}
.DetectedParameter > Label {
@ -168,10 +100,7 @@ Label#f-default-param {
}
.SourceButton {
flex-grow: 0;
align-self: flex-end;
height: 24px;
width: 24px;
height: 24px;
padding: 1px;
}

View File

@ -12,7 +12,6 @@
show-border="true"
show-foldout-header="false"
name="Parameters"
item-height="100"
binding-path="parameters"
style="flex-grow: 1;"
/>
@ -33,5 +32,7 @@
/>
</ui:Foldout>
<editor:ObjectField name="p_import" label="merge_parameter.ui.importFromAsset" class="ndmf-tr"/>
<ma:LanguageSwitcherElement/>
</UXML>

View File

@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
using UnityEditor;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomPropertyDrawer(typeof(ModularAvatarRemoveVertexColor.RemoveMode))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal class RVCModeDrawer : EnumDrawer<ModularAvatarRemoveVertexColor.RemoveMode>
{
protected override string localizationPrefix => "remove-vertex-color.mode";
}
[CustomEditor(typeof(ModularAvatarRemoveVertexColor))]
internal class RemoveVertexColorEditor : MAEditorBase
{
private SerializedProperty _p_mode;
protected void OnEnable()
{
_p_mode = serializedObject.FindProperty(nameof(ModularAvatarRemoveVertexColor.Mode));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_p_mode, G("remove-vertex-color.mode"));
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bfcaf601e9f94ba2900e66d66f469037
timeCreated: 1733085477

View File

@ -64,7 +64,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
.Select(x => mesh.GetBlendShapeName(x))
.ToList();
}
catch (MissingComponentException e)
catch (MissingComponentException)
{
shapeNames = null;
}
@ -72,7 +72,23 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
f_shape_name.SetEnabled(shapeNames != null);
f_shape_name.choices = shapeNames ?? new();
f_shape_name.formatListItemCallback = name => shapeNames != null ? name : "<Missing SkinnedMeshRenderer>";
f_shape_name.formatListItemCallback = name =>
{
if (string.IsNullOrWhiteSpace(name)) return "";
if (shapeNames == null)
{
return $"<Missing SkinnedMeshRenderer>";
}
else if (!shapeNames.Contains(name))
{
return $"<color=\"red\">{name}</color>";
}
else
{
return name;
}
};
f_shape_name.formatSelectedValueCallback = f_shape_name.formatListItemCallback;
}
}

View File

@ -1,13 +1,12 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="changed-shape-editor">
<ui:VisualElement class="horizontal">
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ed:PropertyField binding-path="ChangeType" label="" name="f-change-type"/>
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
<ed:PropertyField name="f-change-type" binding-path="ChangeType" label=""/>
</ui:VisualElement>
<ui:VisualElement class="horizontal">
<ui:DropdownField name="f-shape-name" binding-path="ShapeName"/>
<ed:PropertyField binding-path="Value" label="" name="f-value" class="f-value"/>
<ui:VisualElement name="f-value-delete" class="f-value"/>
</ui:VisualElement>
<ed:PropertyField name="f-value" binding-path="Value" label=""/>
<ui:VisualElement name="f-value-delete"/>
</ui:VisualElement>
</UXML>

View File

@ -19,11 +19,12 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
private BlendshapeSelectWindow _window;
protected override void OnInnerInspectorGUI()
{
throw new NotImplementedException();
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
@ -41,6 +42,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarShapeChanger);
// The Add button callback isn't exposed publicly for some reason...
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
var addButton = (Button)field_addButton.GetValue(listView);
@ -50,6 +53,41 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarShapeChanger;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarShapeChanger>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarShapeChanger targetComponent)
: base(targetElement, targetComponent) { }
protected override bool FilterGameObject(GameObject obj)
{
if (obj.TryGetComponent<SkinnedMeshRenderer>(out var smr))
{
return smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0;
}
return false;
}
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Changed Shapes");
foreach (var reference in references)
{
var changedShape = new ChangedShape { Object = reference, ShapeName = string.Empty };
TargetComponent.Shapes.Add(changedShape);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
private void OnDisable()
{
if (_window != null) DestroyImmediate(_window);

View File

@ -1,7 +1,4 @@
VisualElement {
}
#group-box {
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
@ -14,65 +11,54 @@
/* background-color: rgba(0, 0, 0, 0.1); */
}
#ListViewContainer {
margin-top: 4px;
}
#group-box > Label {
-unity-font-style: bold;
}
.group-root {
#ListViewContainer {
margin-top: 4px;
}
.group-root Toggle {
margin-left: 0;
}
.group-children {
padding-left: 10px;
}
.left-toggle {
display: flex;
.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.changed-shape-editor .horizontal {
flex-direction: row;
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.changed-shape-editor #f-object {
.horizontal > Label {
height: auto;
}
.horizontal > PropertyField > * {
margin: 0;
}
#f-object {
flex-grow: 1;
}
.changed-shape-editor #f-shape-name {
flex-grow: 1;
}
.changed-shape-editor #f-change-type {
flex-grow: 0;
}
.changed-shape-editor #f-value {
flex-grow: 0;
}
.changed-shape-editor PropertyField Label {
display: none;
}
#f-change-type {
width: 75px;
width: 60px;
}
.f-value {
width: 75px;
#f-shape-name {
flex-grow: 1;
}
#f-value {
display: flex;
width: 60px;
}
#f-value-delete {
display: none;
width: 60px;
}
.change-type-delete #f-value {
@ -81,37 +67,14 @@
.change-type-delete #f-value-delete {
display: flex;
height: 20px;
}
/* Add shape window */
.add-shape-popup {
margin: 2px;
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.vline {
width: 100%;
height: 4px;
border-top-width: 4px;
margin-top: 2px;
margin-bottom: 2px;
border-top-color: rgba(0, 0, 0, 0.2);
}
.add-shape-row {
flex-direction: row;
}
.add-shape-row Button {
flex-grow: 0;
}
.add-shape-popup ScrollView Label.placeholder {
-unity-text-align: middle-center;
}
.add-shape-row Label {
flex-grow: 1;
-unity-text-align: middle-left;
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -0,0 +1,101 @@
using System;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarSyncParameterSequence))]
[CanEditMultipleObjects]
public class SyncParameterSequenceEditor : MAEditorBase
{
private SerializedProperty _p_platform;
private SerializedProperty _p_parameters;
private void OnEnable()
{
_p_platform = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.PrimaryPlatform));
_p_parameters = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.Parameters));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
#if MA_VRCSDK3_AVATARS
var disable = false;
#else
bool disable = true;
#endif
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (disable)
// ReSharper disable HeuristicUnreachableCode
{
EditorGUILayout.HelpBox(S("general.vrcsdk-required"), MessageType.Warning);
}
// ReSharper restore HeuristicUnreachableCode
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
using (new EditorGUI.DisabledGroupScope(disable))
{
EditorGUILayout.PropertyField(_p_platform, G("sync-param-sequence.platform"));
GUILayout.BeginHorizontal();
var label = G("sync-param-sequence.parameters");
var sizeCalc = EditorStyles.objectField.CalcSize(label);
EditorGUILayout.PropertyField(_p_parameters, label);
if (GUILayout.Button(G("sync-param-sequence.create-asset"),
GUILayout.ExpandWidth(false),
GUILayout.Height(sizeCalc.y)
))
{
CreateParameterAsset();
}
GUILayout.EndHorizontal();
}
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
ShowLanguageUI();
}
private void CreateParameterAsset()
{
#if MA_VRCSDK3_AVATARS
Transform avatarRoot = null;
if (targets.Length == 1)
{
avatarRoot =
RuntimeUtil.FindAvatarTransformInParents(((ModularAvatarSyncParameterSequence)target).transform);
}
var assetName = "Avatar";
if (avatarRoot != null) assetName = avatarRoot.gameObject.name;
assetName += " SyncedParams";
var file = EditorUtility.SaveFilePanelInProject("Create new parameter asset", assetName, "asset",
"Create a new parameter asset");
var obj = CreateInstance<VRCExpressionParameters>();
obj.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
obj.isEmpty = true;
AssetDatabase.CreateAsset(obj, file);
Undo.RegisterCreatedObjectUndo(obj, "Create parameter asset");
_p_parameters.objectReferenceValue = obj;
serializedObject.ApplyModifiedProperties();
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bf6030b7fa704997885767897d1acba0
timeCreated: 1733090792

View File

@ -0,0 +1,12 @@
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarWorldScaleObject))]
internal class WorldScaleObjectEditor : MAEditorBase
{
protected override void OnInnerInspectorGUI()
{
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9b8b83586074bd7a6441b4cd7539dc9
timeCreated: 1741658287

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d59587969bdd48f4ba16883ee3b30d4d
timeCreated: 1742695977

View File

@ -0,0 +1,27 @@
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarVRChatSettings))]
internal class VRChatSettingsEditor : MAEditorBase
{
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(
serializedObject.FindProperty(nameof(ModularAvatarVRChatSettings.m_mmdWorldSupport)),
Localization.G("platform.vrchat.settings.mmd_world_support")
);
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
Localization.ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1ffd87033f4d441b849ad147b6e2a6ef
timeCreated: 1742695986

View File

@ -51,6 +51,7 @@
"merge_parameter.ui.add_button": "Add",
"merge_parameter.ui.details": "Parameter Configuration",
"merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults",
"merge_parameter.ui.importFromAsset": "Import from asset",
"merge_armature.merge_target": "Merge Target",
"merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into",
"merge_armature.prefix": "Prefix",
@ -77,6 +78,9 @@
"merge_animator.relative_path_root.tooltip": "The root object to use when interpreting relative paths. If not specified, the object this component is attached to will be used.",
"merge_animator.layer_priority": "Layer Priority",
"merge_animator.layer_priority.tooltip": "Controls the order in which layers are merged into the animator - lower to higher. Negative values are merged before the original layer on the avatar descriptor, while zero and positive numbers are merged after.",
"merge_animator.merge_mode": "Merge Mode",
"merge_animator.merge_mode.Append": "Append to Animator",
"merge_animator.merge_mode.Replace": "Replace Existing Animator",
"merge_armature.lockmode": "Position sync mode",
"merge_armature.lockmode.not_locked.title": "Not locked",
"merge_armature.lockmode.not_locked.body": "Merged armature does not sync its position with the base avatar.",
@ -86,12 +90,13 @@
"merge_armature.lockmode.bidirectional.body": "The base armature and the merged armature will always have the same position. This is useful when creating animations that are meant to target the base armature. In order to activate this, your armatures must already be in the exact same position.",
"merge_armature.reset_pos": "Reset position to base avatar",
"merge_armature.reset_pos.info": "This command will force the position of all bones in the outfit to match that of the base avatar. This can be helpful as a starting point for installing outfits not set up for your current avatar.",
"merge_armature.reset_pos.convert_atpose": "Convert A-Pose/T-Pose to match base avatar",
"merge_armature.reset_pos.adjust_rotation": "Also set rotation to base avatar",
"merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar",
"merge_armature.reset_pos.execute": "Do it!",
"merge_armature.reset_pos.heuristic_scale": "Adjust outfit overall scale to match base avatar",
"merge_armature.reset_pos.heuristic_scale.tooltip": "Will set the overall scale of the outfit as a whole based on armspan measurements. Recommended for setting up outfits.",
"merge_blend_tree.blend_tree": "Blend Tree",
"merge_blend_tree.motion": "Motion (or Blend Tree) to merge",
"merge_blend_tree.path_mode": "Path Mode",
"merge_blend_tree.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
"merge_blend_tree.relative_path_root": "Relative Path Root",
@ -99,7 +104,7 @@
"worldfixed.quest": "This component is not compatible with Android builds and will have no effect.",
"worldfixed.normal": "This object will be fixed to world unless you fixed to avatar with constraint.",
"fpvisible.normal": "This object will be visible in your first person view.",
"fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.",
"fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.\nIf this will be placed under the head bone through Bone Proxy etc., this warning can be ignored.",
"fpvisible.quest": "This component is not compatible with Android builds and will have no effect.",
"fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead.",
"blendshape.mesh": "Mesh",
@ -149,6 +154,11 @@
"error.rename_params.default_value_conflict:hint": "To avoid unpredictable behavior, leave the default value field blank in all but on MA Parameters component. If multiple values are present, Modular Avatar will select the first default value specified in the hierarchy order.",
"error.replace_object.null_target": "[MA-0008] No target specified",
"error.replace_object.null_target:hint": "Replace object needs a target object to replace. Try setting one.",
"error.replace_object.replacing_replacement": "[MA-0009] The same target object cannot be specified in multiple Replace Object components",
"error.replace_object.parent_of_target": "[MA-0010] The target object cannot be a parent of this object",
"error.singleton": "[MA-0011] Only one instance of {0} is allowed in an avatar",
"error.merge_animator.multiple_replacements": "[MA-0012] Multiple Merge Animators are trying to replace the same layer",
"error.merge_animator.multiple_replacements:hint": "Because the 'Replace' mode of Merge Animator replaces the entire animator with a different one, it's not clear which of these you wanted to have win. Try either deleting all but one of these Merge Animators, or setting them to Append mode.",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] No renderer found on this object",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync acts on a Skinned Mesh Renderer on the same GameObject. Did you attach it to the right object?",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
@ -187,6 +197,8 @@
"menuitem.prop.type.tooltip": "The type of this item",
"menuitem.prop.value": "Value",
"menuitem.prop.value.tooltip": "The value to set the parameter to when this control is used",
"menuitem.prop.automatic_value": "Auto",
"menuitem.prop.automatic_value.tooltip": "Automatically set this control to a unique value",
"menuitem.prop.parameter": "Parameter",
"menuitem.prop.label": "Label",
"menuitem.prop.submenu_asset": "Submenu Asset",
@ -238,7 +250,7 @@
"setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0}'s parents. Make sure your outfit is placed inside your avatar.",
"setup_outfit.err.no_animator": "Your avatar does not have an Animator component.",
"setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.",
"setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names:",
"setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names(case-insensitive):",
"move_independently.group-header": "Objects to move together",
"scale_adjuster.scale": "Scale adjustment",
"scale_adjuster.adjust_children": "Adjust position of child objects",
@ -278,5 +290,21 @@
"ro_sim.effect_group.rule_inverted": "This rule is inverted",
"ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met",
"ro_sim.effect_group.conditions": "Conditions"
"ro_sim.effect_group.conditions": "Conditions",
"menuitem.label.long_name.tooltip": "Use a long name which may contain rich text and line breaks.",
"menuitem.label.gameobject_name.tooltip": "Use the GameObject name.",
"remove-vertex-color.mode": "Mode",
"remove-vertex-color.mode.Remove": "Remove Vertex Colors",
"remove-vertex-color.mode.DontRemove": "Keep Vertex Colors",
"general.vrcsdk-required": "This component requires the VRCSDK to function.",
"sync-param-sequence.platform": "Primary Platform",
"sync-param-sequence.platform.tooltip": "When building for this platform, Modular Avatar will record all expression parameters for use on other platform builds",
"sync-param-sequence.parameters": "Common parameters asset",
"sync-param-sequence.parameters.tooltip": "The asset to store common parameters in. Do not use the same Expression Parameters that you have set in your avatar descriptor.",
"sync-param-sequence.create-asset": "New",
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset",
"platform.vrchat.settings.mmd_world_support": "MMD world support"
}

View File

@ -9,7 +9,7 @@
"menuinstall.showcontents": "メニュー内容を表示",
"menuinstall.showcontents.notselected": "メニューが選択されていません",
"menuinstall.devoptions": "プレハブ開発者向け設定",
"menuinstall.menu_icon_too_large": "メニューに設定されているアイコンが256ピクセルより大きすぎます。",
"menuinstall.menu_icon_too_large": "メニューに設定されているアイコンが256x256より大きいです。",
"menuinstall.menu_icon_uncompressed": "メニューに設定されているアイコンが圧縮設定されていません。",
"menuinstall.srcmenu": "インストールされるメニュー",
"params.syncmode.NotSynced": "Animatorのみ",
@ -31,32 +31,33 @@
"merge_parameter.ui.name": "パラメーター名",
"merge_parameter.ui.prefix": "PhysBone 接頭辞",
"merge_parameter.ui.remapTo": "名前を変更",
"merge_parameter.ui.remapTo.tooltip": "ここに新しい名前を入れることで、このパラメーターの名前を変更できます。これで名前かぶりを回避したり、あるいはあえて複数のギミックを連動できます。",
"merge_parameter.ui.remapTo.tooltip": "ここに新しい名前を入れることで、このパラメーターの名前を変更できます。これで名前かぶりを回避したり、複数のギミックを連動させたりすることができます。",
"merge_parameter.ui.remapTo.automatic": "(自動的に設定)",
"merge_parameter.ui.defaultValue": "初期値",
"merge_parameter.ui.defaultValue.tooltip": "アバターがリセット、または最初に着た時にこの値が採用されます。",
"merge_parameter.ui.defaultValue.tooltip": "アバターをリセットした時、または最初に着た時にこの値が採用されます。",
"merge_parameter.ui.saved": "保存する",
"merge_parameter.ui.saved.tooltip": "保存されたパラメーターは、アバター変更やワールド移動保持されます",
"merge_parameter.ui.saved.tooltip": "保存されたパラメーターは、アバター変更やワールド移動をしても保持されます",
"merge_parameter.ui.internalParameter": "自動リネーム",
"merge_parameter.ui.internalParameter.tooltip": "有効にすると、名前かぶりを回避するために自動的に名前を変更します",
"merge_parameter.ui.isPrefix": "PhysBone 接頭辞",
"merge_parameter.ui.syncType": "パラメーター型",
"merge_parameter.ui.synced": "同期する",
"merge_parameter.ui.synced.tooltip": "有効にすると、ネットワーク上同期されます",
"merge_parameter.ui.synced.tooltip": "有効にすると、このパラメーターは同期されます",
"merge_parameter.ui.unregistered_foldout": "未登録パラメーター",
"merge_parameter.ui.add_button": "追加",
"merge_parameter.ui.details": "パラメーターの詳細設定",
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターの初期値を設定",
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定",
"merge_parameter.ui.importFromAsset": "アセットからインポートする",
"merge_armature.merge_target": "統合先",
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
"merge_armature.prefix": "接頭辞",
"merge_armature.prefix.tooltip": "マージするボーンに付いている接頭辞",
"merge_armature.prefix.tooltip": "統合されるボーンに付いている接頭辞",
"merge_armature.suffix": "接尾辞",
"merge_armature.suffix.tooltip": "マージするボーンに付いている接尾辞",
"merge_armature.suffix.tooltip": "統合されるボーンに付いている接尾辞",
"merge_armature.locked": "位置を固定",
"merge_armature.locked.tooltip": "このオブジェクトのボーンを統合先のボーンに常に相互的に位置を合わせる。アニメーション制作向け",
"merge_armature.locked.tooltip": "このオブジェクトのボーンと統合先のボーンの位置を常に合わせます。アニメーション作成時に便利です。",
"merge_armature.adjust_names": "ボーン名を統合先に合わせる",
"merge_armature.adjust_names.tooltip": "統合先のボーン名に合わせて、衣装のボーン名を合わせて変更します。統合先アバターに非対応の衣装導入向け機能です。",
"merge_armature.adjust_names.tooltip": "衣装のボーン名をアバターのボーン名に合わせて変更します。アバターに非対応の衣装を導入する時に便利です。",
"merge_armature.mangle_names": "名前かぶりを回避",
"merge_armature.mangle_names.tooltip": "ほかのアセットとの名前かぶりを裂けるため、新規ボーンの名前を自動で変更する",
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
@ -66,13 +67,16 @@
"merge_animator.delete_attached_animator": "付属アニメーターを削除",
"merge_animator.delete_attached_animator.tooltip": "統合後、このオブジェクトについているアニメーターを削除します",
"merge_animator.path_mode": "パスモード",
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
"merge_animator.relative_path_root": "相対的パスのルート",
"merge_animator.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
"merge_animator.layer_priority": "レイヤー優先度",
"merge_animator.layer_priority": "レイヤー統合優先度",
"merge_animator.layer_priority.tooltip": "アニメーターにレイヤーが統合される順番を制御します。低い値から高い値の順に統合されます。マイナスの場合は元々のAvatar Descriptorについているコントローラーより前に統合され、ゼロ以上の場合はそのあとに統合されます。",
"merge_animator.merge_mode": "統合モード",
"merge_animator.merge_mode.Append": "アニメーターに追加",
"merge_animator.merge_mode.Replace": "既存アニメーターを置き換える",
"merge_armature.lockmode": "位置追従モード",
"merge_armature.lockmode.not_locked.title": "追従なし",
"merge_armature.lockmode.not_locked.body": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
@ -81,28 +85,29 @@
"merge_armature.lockmode.bidirectional.title": "アバター <=====> オブジェクト (双方向)",
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
"merge_armature.reset_pos.info": "このコマンドは、衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入するとき、アバウトに合わせるために便利です。",
"merge_armature.reset_pos.info": "衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入する際、アバウトに位置を合わせるのに便利です。",
"merge_armature.reset_pos.convert_atpose": "Aポーズ/Tポーズを合わせる",
"merge_armature.reset_pos.adjust_rotation": "回転も合わせる",
"merge_armature.reset_pos.adjust_scale": "スケールも合わせる",
"merge_armature.reset_pos.execute": "実行",
"merge_armature.reset_pos.heuristic_scale": "衣装の全体的なスケールをアバターに合わせる",
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入するときは推奨です。",
"merge_blend_tree.blend_tree": "ブレンドツリー",
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入する時にお勧めです。",
"merge_blend_tree.motion": "結合するモーション(またはブレンドツリー)",
"merge_blend_tree.path_mode": "パスモード",
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
"merge_blend_tree.relative_path_root": "相対的パスのルート",
"merge_blend_tree.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
"worldfixed.quest": "このコンポーネントはアンドロイドビルド非対応のため無効となっています。",
"worldfixed.quest": "このコンポーネントはアンドロイドビルドに非対応であるため、無効となっています。",
"worldfixed.normal": "このオブジェクトはConstraint等でアバターに追従させない限りワールドに固定されます。",
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
"fpvisible.NotUnderHead": "このコンポーネントはヘッドボーン外では効果がありません。",
"fpvisible.quest": "このコンポーネントはアンドロイドビルド非対応のため無効となっています。",
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。",
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
"fpvisible.NotUnderHead": "このコンポーネントは頭ボーンの配下でないと効果がありません。\nBone Proxyなどで頭ボーンの配下に配置される場合は効果あります",
"fpvisible.quest": "このコンポーネントはアンドロイドビルドに非対応であるため、無効となっています。",
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneで制御されているため、一人称視点でうまく表示させることができません。PhysBoneの始点を指定してください。",
"blendshape.mesh": "メッシュ",
"blendshape.source": "元メッシュのブレンドシェプ",
"blendshape.target": "このメッシュのブレンドシェプ",
"hint.not_in_avatar": "このコンポーネントが正しく動作するには、アバター内に配置する必要があります。",
"boneproxy.err.MovingTarget": "他のモジュラーアバターコンポーネントで移動されるオブジェクト指定できません。",
"blendshape.source": "元メッシュのブレンドシェプ",
"blendshape.target": "このメッシュのブレンドシェプ",
"hint.not_in_avatar": "このコンポーネントを正しく動作させるには、アバター内に配置する必要があります。",
"boneproxy.err.MovingTarget": "他のモジュラーアバターコンポーネントで移動されるオブジェクト指定できません。",
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。",
"boneproxy.attachment": "配置モード",
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
@ -111,28 +116,28 @@
"boneproxy.attachment.AsChildKeepRotation": "子として・ワールド向きを維持",
"mesh_settings.header_probe_anchor": "Anchor Override 設定",
"mesh_settings.inherit_probe_anchor": "設定モード",
"mesh_settings.probe_anchor": "アンカーオーバーライド",
"mesh_settings.probe_anchor.tooltip": "このオブジェクトとその子のレンダラーのAnchorOverrideを設定します。",
"mesh_settings.probe_anchor": "Anchor Override",
"mesh_settings.probe_anchor.tooltip": "このオブジェクトとその子のレンダラーのAnchor Overrideを設定します。",
"mesh_settings.header_bounds": "Bounds 設定",
"mesh_settings.inherit_bounds": "設定モード",
"mesh_settings.root_bone": "ルートボーン",
"mesh_settings.root_bone.tooltip": "このオブジェクトとその子のメッシュで設定されるルートボーン。メッシュのバウンズを計算するための参照点として使用されます。",
"mesh_settings.bounds": "バウンズ",
"mesh_settings.root_bone.tooltip": "このオブジェクトとその子のメッシュで設定されるルートボーン。メッシュのBoundsを計算する際の基準として使用されます。",
"mesh_settings.bounds": "Bounds",
"mesh_settings.bounds.tooltip": "このオブジェクトとその子のメッシュで設定されるバウンズ。画面外のメッシュのレンダリングを省略するかどうかを決定するために使用されます。",
"mesh_settings.inherit_mode.Inherit": "継承",
"mesh_settings.inherit_mode.Set": "設定",
"mesh_settings.inherit_mode.DontSet": "設定しない(メッシュ本体の設定のまま)",
"mesh_settings.inherit_mode.SetOrInherit": "親が指定されてる時は継承、または設定",
"mesh_settings.inherit_mode.SetOrInherit": "親で指定されている時は継承、それ以外では設定",
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでも駄目な場合、Modular Avatarにも最新版が出ていないか確認してみてください。",
"error.stack_trace": "スタックトレース(バグを報告する時は必ず添付してください!)",
"error.merge_armature.circular_dependency": "[MA-0001] Merge Armatureに循環参照があります",
"error.merge_armature.circular_dependency:description": "Merge Armature コンポーネントは、自分自身、または自分の子をマージターゲットとして参照しています。",
"error.merge_armature.circular_dependency:hint": "Merge Armature は通常、ターゲットフィールドにアバター本体の Armature オブジェクトを指定する必要があります。衣装自体を指定しないでください。",
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] ヒューマイドボーンにPhysBoneコンポーネントを検出",
"error.merge_armature.physbone_on_humanoid_bone:hint": "一部のヒューマイドボーンは、PhysBonesによって制御されています。 マージ対象の対応するヒューマイドボーンとは位置が異なるため、適切にマージすることはできません。マージするにはヒューマイドボーンのPhysBonesを取り除く必要があります。",
"error.merge_armature.circular_dependency:description": "Merge Armatureコンポーネントの統合先として、自分自身、または自分の子が参照されています。",
"error.merge_armature.circular_dependency:hint": "通常、Merge Armatureは統合先としてアバター本体のArmatureオブジェクトを指定する必要があります。衣装自体を指定しないように注意してください。",
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] HumanoidボーンにPhysBoneコンポーネントがついています。",
"error.merge_armature.physbone_on_humanoid_bone:hint": "一部のHumanoidボーンがPhysBoneによって制御されています。対応する統合先のHumanoidボーンと位置が異なるため、適切に統合することができません。統合するにはHumanoidボーンについているPhysBoneを取り除く必要があります。",
"error.merge_blend_tree.missing_tree": "[MA-0009] ブレンドツリーが指定されていません",
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Treeが動作するには、どのブレンドツリーを統合するか指定する必要があります。「ブレンドツリー」欄を設定してみてください。",
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Treeが動作するには、どのブレンドツリーを統合するか指定する必要があります。「ブレンドツリー」欄を設定しているかご確認ください。",
"error.internal_error": "[MA-9999] 内部エラーが発生しました:{0}\n以下のオブジェクトの処理中に発生しました",
"error.merge_animator.param_type_mismatch": "[MA-0003] パラメータの型が競合しています",
"error.merge_animator.param_type_mismatch:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
@ -142,37 +147,42 @@
"error.rename_params.type_conflict:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
"error.rename_params.default_value_conflict": "[MA-0007] 初期値の競合",
"error.rename_params.default_value_conflict:description": "パラメータ {0} には複数の初期値が指定されています: {1} != {2}",
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるために、MAパラメータコンポーネントのデフォルト値フィールドはパラメーター名毎に一個のコンポーネント以外空白のままにしてください。 複数の値が存在する場合、Modular Avatarは階層順に指定された最初のデフォルト値を選択します。",
"error.replace_object.null_target": "[MA-0008] ターゲットが指定されていません",
"error.replace_object.null_target:hint": "Replace Object には置き換えるべきターゲットオブジェクトが必要です。設定してみてください。",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] このオブジェクトにはSkinnedMeshRendererがありません。",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上のSkinned Mesh Rendererに作用します。正しいオブジェクトに追加しましたか",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] このオブジェクトにはSkinnedMeshRendererがありますが、メッシュがありません。",
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるため、MA Parametersコンポーネントの初期値フィールドはパラメーター名毎に1つだけしか指定しないようにし、他のコンポーネントでは空白のままにしてください。複数の値が存在する場合、Modular Avatarは階層順で最初に指定された初期値を採用します。",
"error.replace_object.null_target": "[MA-0008] 置き換え先が指定されていません",
"error.replace_object.null_target:hint": "Replace Objectは置き換え先のオブジェクトを指定する必要があります。",
"error.replace_object.replacing_replacement": "[MA-0009] 複数のReplace Objectコンポーネントで、同じ置き換え先を指定できません",
"error.replace_object.parent_of_target": "[MA-0010] このオブジェクトの親を置き換え先に指定できません",
"error.singleton": "[MA-0011] {0} はアバターに一個しか存在できません",
"error.merge_animator.multiple_replacements": "[MA-0012] 複数のMerge Animatorが同じレイヤーを置き換えようとしています",
"error.merge_animator.multiple_replacements:hint": "Merge Animator の「既存アニメーターを置き換える」設定は丸ごと置き換えるので、複数があるとどれに置き換えたいかわかりません。一つを残してMerge Animatorを削除するか、一つを除いて「アニメーターに追加」にしてみましょう。",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] このオブジェクトにはSkinned Mesh Rendererがありません。",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上のSkinned Mesh Rendererに作用します。コンポーネントが正しいオブジェクトに追加されているか確認してください。",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] このオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
"validation.blendshape_sync.no_local_mesh:hint": "このオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
"validation.blendshape_sync.no_bindings": "[MA-1002] このBlendshapeSyncにはバインドが設定されていません。",
"validation.blendshape_sync.no_bindings": "[MA-1002] このBlendshape Syncにはバインドが設定されていません。",
"validation.blendshape_sync.no_bindings:hint": "Blendshape Syncは、どのブレンドシェイプを同期するかを知る必要があります。追加するには、「+」ボタンをクリックしてください。",
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 同期先のメッシュに該当するブレンドシェープ「{0}」がありません。",
"validation.blendshape_sync.missing_local_shape:description": "ローカルブレンドシェイプがありません: {0}",
"validation.blendshape_sync.missing_local_shape:hint": "ターゲットオブジェクトから値を受け取るように設定されたブレンドシェイプがありません。赤で示されているブレンドシェイプ名を変更してみてください。",
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 同期先メッシュにブレンドシェープ「{0}」が見つかりません",
"validation.blendshape_sync.missing_target_shape:description": "ターゲットブレンドシェイプがありません: {0}",
"validation.blendshape_sync.missing_target_shape:hint": "ローカルオブジェクトに値を「送る」ように設定されたブレンドシェイプがありません。赤で示されているブレンドシェイプ名を変更してみてください。",
"validation.blendshape_sync.no_target": "[MA-1005] このBlendshapeSyncには同期元が設定されていないバインドがあります。",
"validation.blendshape_sync.no_target:hint": "どのオブジェクトからBlendshapeを同期するかを教える必要があります。メッシュを設定してみてください。",
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 同期元のオブジェクトにSkinnedMeshRendererがありません。",
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Syncは、対象オブジェクトのSkinned Mesh Rendererからブレンド形状の値を受け取ります。正しいオブジェクトに追加しましたか",
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 同期元のオブジェクトにはSkinnedMeshRendererがありますが、メッシュがありません。",
"validation.blendshape_sync.missing_target_mesh:hint": "ターゲットオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
"validation.bone_proxy.no_target": "[MA-1100] ターゲットオブジェクトが未設定もしくは存在しません。",
"validation.bone_proxy.no_target:hint": "ボーンプロキシがどのオブジェクトに追尾するかを知る必要があります。追尾すべきオブジェクトをターゲットフィールドに設定してみてください。",
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 同期先のメッシュにブレンドシェイプ「{0}」がありません。",
"validation.blendshape_sync.missing_local_shape:description": "ブレンドシェイプ「{0}」がありません。",
"validation.blendshape_sync.missing_local_shape:hint": "値を受け取るように設定されたブレンドシェイプが同期先のメッシュに存在しません。赤で示されているブレンドシェイプ名を変更してみてください。",
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 同期元のメッシュにブレンドシェイプ「{0}」がありません。",
"validation.blendshape_sync.missing_target_shape:description": "ブレンドシェイプ「{0}」がありません。",
"validation.blendshape_sync.missing_target_shape:hint": "値を「送る」ように設定されたブレンドシェイプが同期元のメッシュに存在しません。赤で示されているブレンドシェイプ名を変更してみてください。",
"validation.blendshape_sync.no_target": "[MA-1005] このBlendshape Syncには同期元が設定されていないバインドがあります。",
"validation.blendshape_sync.no_target:hint": "どのオブジェクトからBlendshapeを同期するか指定する必要があります。メッシュを設定してください。",
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 同期元のオブジェクトにSkinned Mesh Rendererがありません。",
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Syncは、同期元のSkinned Mesh Rendererからブレンドシェイプの値を受け取ります。コンポーネントが正しいオブジェクトに追加されているか確認してください。",
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 同期元のオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
"validation.blendshape_sync.missing_target_mesh:hint": "同期元のオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
"validation.bone_proxy.no_target": "[MA-1100] ターゲットオブジェクトが未設定であるか、存在しません。",
"validation.bone_proxy.no_target:hint": "Bone Proxyはどのオブジェクトに追従させるかを指定する必要があります。追従させたい対象のオブジェクトをターゲット欄に設定してみてください。",
"validation.menu_installer.no_menu": "[MA-1200] インストールするメニューがありません。",
"validation.menu_installer.no_menu:hint": "Menu Installer は、どのメニューをインストールするべきか設定する必要があります。 「Prefab開発者向けオプション」内の「インストールするメニュー」フィールドを設定するか、MA Menu Itemコンポーネントを追加してみてください。",
"validation.merge_animator.no_animator": "[MA-1300] Animator Controllerが設定されていません",
"validation.merge_animator.no_animator:hint": "Animatorをマージするには、どのアニメーターを統合するか設定する必要があります。「統合されるアニメーター」を設定してみてください。",
"validation.merge_armature.no_target": "[MA-1400] 統合先が未設定もしくは存在しません。",
"validation.merge_animator.no_animator": "[MA-1300] Animator Controllerが未設定であるか、存在しません。",
"validation.merge_animator.no_animator:hint": "どのアニメーターを統合するか設定する必要があります。「統合されるアニメーター」を設定してみてください。",
"validation.merge_armature.no_target": "[MA-1400] 統合先が未設定であるか、存在しません。",
"validation.merge_armature.no_target:hint": "Merge Armatureはどこに統合するか設定する必要があります。「統合先」を設定してみてください。",
"validation.merge_armature.target_is_child": "[MA-1500] 統合先をこのオブジェクトの子にすることができません",
"validation.merge_armature.target_is_child:hint": "Merge Armature は自身の子に統合できません。「統合先」を別のオブジェクトに設定してみてください。",
"validation.merge_armature.target_is_child": "[MA-1500] 統合先として自身の子を指定することはできません。",
"validation.merge_armature.target_is_child:hint": "Merge Armatureで自身の子に統合することはできません。「統合先」を別のオブジェクトに設定してみてください。",
"submenu_source.Children": "子オブジェクトから生成",
"submenu_source.MenuAsset": "Expressions Menu アセットを指定",
"menuitem.showcontents": "メニュー内容を表示",
@ -182,25 +192,27 @@
"menuitem.prop.type": "タイプ",
"menuitem.prop.type.tooltip": "この項目の種別",
"menuitem.prop.value": "パラメーター値",
"menuitem.prop.value.tooltip": "この項目が操作されたとき、パラメーターが設定される値",
"menuitem.prop.value.tooltip": "この項目が操作されたとき、パラメーターに設定される値",
"menuitem.prop.automatic_value": "自動",
"menuitem.prop.automatic_value.tooltip": "かぶらない値を自動的に割り振る",
"menuitem.prop.parameter": "パラメーター名",
"menuitem.prop.label": "表示名",
"menuitem.prop.submenu_asset": "サブメニューアセット",
"menuitem.prop.submenu_asset.tooltip": "サブメニューとして引用するアセット",
"menuitem.prop.submenu_source": "サブメニュー引用元",
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきか指定",
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきか指定します。",
"menuitem.prop.source_override": "引用元オブジェクト",
"menuitem.prop.source_override.tooltip": "指定した場合、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
"menuitem.prop.source_override.tooltip": "指定した場合、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
"menuitem.prop.is_default": "初期設定にする",
"menuitem.prop.is_default.tooltip": "ONの場合、アバター初期化の際にこのメニューアイテムを選択します",
"menuitem.prop.is_default.tooltip": "有効になっていると、アバター初期化の際にこのメニューアイテムを選択した状態にします",
"menuitem.prop.is_saved": "保存する",
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動をしてもこの設定が保持されます。",
"menuitem.prop.is_synced": "同期する",
"menuitem.prop.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
"menuitem.param.rotation": "回転パラメーター名",
"menuitem.param.rotation.tooltip": "このメニューアイテムの回転に連動するべきパラメーター",
"menuitem.prop.is_synced.tooltip": "有効になっていると、メニューがほかのプレイヤーに同期されます。",
"menuitem.param.rotation": "ラジアルメニュー用パラメーター名",
"menuitem.param.rotation.tooltip": "このラジアルメニューの操作と連動するべきパラメーター",
"menuitem.param.horizontal": "横パラメーター名",
"menuitem.param.horizontal.tooltip": "操作に連動するパラメーター名",
"menuitem.param.horizontal.tooltip": "左右操作に連動するパラメーター名",
"menuitem.param.vertical": "縦パラメーター名",
"menuitem.param.vertical.tooltip": "上下操作に連動するパラメーター名",
"menuitem.label.control_labels_and_params": "表示名・パラメーター",
@ -216,29 +228,29 @@
"control_group.foldout.actions": "アクション",
"control_group.foldout.menu_items": "関連付けされたメニューアイテム",
"control_group.is_saved": "保存する",
"control_group.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
"control_group.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動をしてもこの設定が保持されます。",
"control_group.is_synced": "同期する",
"control_group.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
"control_group.is_synced.tooltip": "有効になっていると、メニューがほかのプレイヤーに同期されます。",
"control_group.default_value": "初期値",
"control_group.default_value.unset": "(どれも選択されない)",
"animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}",
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定に指定されたメニューアイテムが複数あります。",
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定にするとして指定されたメニューアイテムが複数あります。",
"menuitem.misc.add_item": "メニューアイテムを追加",
"replace_object.target_object": "上書き先",
"setup_outfit.err.header.notarget": "Setup outfit の処理が失敗しました",
"setup_outfit.err.header": "Setup outfit が「{0}」を処理中に失敗しました。",
"replace_object.target_object": "置き換え先",
"setup_outfit.err.header.notarget": "Setup Outfit の処理に失敗しました",
"setup_outfit.err.header": "Setup Outfit が「{0}」を処理中に失敗しました。",
"setup_outfit.err.unknown": "原因不明のエラーが発生しました。",
"setup_outfit.err.no_selection": "オブジェクトが選択されていません。",
"setup_outfit.err.run_on_avatar_itself": "Setup Outfitはアバター自体ではなく、衣装のほうで実行してください。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。",
"setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親に、複数のavatar descriptorを発見しました。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。",
"setup_outfit.err.no_avatar_descriptor": "「{0}」の親に、avatar descriptorが見つかりませんでした。衣装のオブジェクトをアバターの中に配置してください。",
"setup_outfit.err.run_on_avatar_itself": "Setup Outfitはアバター自体ではなく、衣装の方で実行してください。\n\nキメラアバターを作る場合は、中の方のAvatar Descriptorを消して、衣装として扱ってください。",
"setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親で複数のAvatar Descriptorを発見しました。\n\nキメラアバターを作る場合は、中の方のAvatar Descriptorを消して、衣装として扱ってください。",
"setup_outfit.err.no_avatar_descriptor": "「{0}」の親にAvatar Descriptorが見つかりませんでした。衣装のオブジェクトをアバターの中に配置してください。",
"setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。",
"setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup Outfitはヒューマノイドアバター以外には対応していません。",
"setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。以下の名前を含むボーンを探しました:",
"move_independently.group-header": "一緒に動かすオブジェクト",
"setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup OutfitはHumanoidアバター以外には対応していません。",
"setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。アクセサリー等に対してSetup Outfitを試みた場合は、代わりにBone Proxyコンポーネントを使って配置してみてください。\n以下の名前を含むボーンを(大文字・小文字を区別つけずに)探しました:",
"move_independently.group-header": "同時に動かすオブジェクト",
"scale_adjuster.scale": "Scale調整値",
"scale_adjuster.adjust_children": "子オブジェクトの位置を調整",
"world_fixed_object.err.unsupported_platform": "World Fixed Objectこのプラットフォームに対応していません",
"world_fixed_object.err.unsupported_platform": "World Fixed Objectこのプラットフォームに対応していません",
"world_fixed_object.err.unsupported_platform:description": "World Fixed ObjectはAndroid向けビルドには対応していないため、動作しません。",
"ma_info.param_usage_ui.header": "Expressions Parameter 使用状況",
"ma_info.param_usage_ui.other_objects": "このアバター内の他のオブジェクト",
@ -248,9 +260,8 @@
"reactive_object.inverse": "条件を反転",
"reactive_object.material-setter.set-to": "変更先のマテリアル ",
"menuitem.misc.add_toggle": "トグルを追加",
"ro_sim.open_debugger_button": "リアクションデバッグツールを開く",
"ro_sim.window.title": "MA リアクションデバッグツール",
"ro_sim.open_debugger_button": "Reaction デバッガーを開く",
"ro_sim.window.title": "MA Reaction デバッガー",
"ro_sim.header.inspecting": "表示中のオブジェクト",
"ro_sim.header.clear_overrides": "すべてのオーバーライドを解除",
"ro_sim.header.object_state": "オブジェクトのアクティブ状態",
@ -259,19 +270,30 @@
"ro_sim.header.override_gameobject_state": "GameObject のアクティブ状態をオーバーライド",
"ro_sim.header.override_menuitem_state": "MenuItem の選択状態をオーバーライト",
"ro_sim.affected_by.title": "以下のルールに影響されています",
"ro_sim.effect_group.component": "Reactive Component",
"ro_sim.effect_group.controls_obj_state": "オブジェクトのアクティブ状態を設定する➡",
"ro_sim.effect_group.controls_obj_state": "オブジェクトのアクティブ状態を次に設定する➡",
"ro_sim.effect_group.target_component": "コンポーネント",
"ro_sim.effect_group.target_component.tooltip": "Reactive Componentに影響されるコンポーネント",
"ro_sim.effect_group.property": "プロパティ",
"ro_sim.effect_group.property.tooltip": "設定されるプロパティ",
"ro_sim.effect_group.value": "値",
"ro_sim.effect_group.value.tooltip": "上記 Reactive Component が活性状態の時に設定される値",
"ro_sim.effect_group.value.tooltip": "上記の Reactive Component がアクティブな時に設定される値",
"ro_sim.effect_group.material": "マテリアル",
"ro_sim.effect_group.material.tooltip": "上記 Reactive Component が活性状態の時に設定されるマテリアル",
"ro_sim.effect_group.rule_inverted": "このルールの条件が反転されています",
"ro_sim.effect_group.material.tooltip": "上記の Reactive Component がアクティブな時に設定されるマテリアル",
"ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています",
"ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます",
"ro_sim.effect_group.conditions": "条件"
"ro_sim.effect_group.conditions": "条件",
"menuitem.label.long_name.tooltip": "リッチテキスト、改行を含む長い名前にする。",
"menuitem.label.gameobject_name.tooltip": "ゲームオブジェクトの名前を採用する。",
"remove-vertex-color.mode": "モード",
"remove-vertex-color.mode.Remove": "頂点カラーを削除する",
"remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない",
"general.vrcsdk-required": "このコンポーネントにはVRCSDKが必要です。",
"sync-param-sequence.platform": "主要プラットホーム",
"sync-param-sequence.platform.tooltip": "このプラットホームでビルドすると、他のプラットホームを合わせるためにパラメーターを記録します。",
"sync-param-sequence.parameters": "共用パラメーターアセット",
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
"sync-param-sequence.create-asset": "新規作成",
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します",
"platform.vrchat.settings.mmd_world_support": "MMDワールド対応"
}

View File

@ -88,7 +88,6 @@
"merge_armature.reset_pos.execute": "실행",
"merge_armature.reset_pos.heuristic_scale": "의상의 전체적인 스케일을 원본 아바타에 맞춤",
"merge_armature.reset_pos.heuristic_scale.tooltip": "원본 아바타의 팔 길이를 참조하여, 의상의 전체 스케일을 이에 맞춥니다. 전용 의상이 아닌 경우 도움이 됩니다.",
"merge_blend_tree.blend_tree": "블랜드 트리",
"merge_blend_tree.path_mode": "경로 모드",
"merge_blend_tree.path_mode.tooltip": "애니메이션에서 경로를 해석하는 방법에 대해 설명합니다. 상대 모드를 사용하면 이 오브젝트의 애니메이터에서 애니메이션을 기록할 수 있습니다.",
"merge_blend_tree.relative_path_root": "상대적 경로",
@ -96,7 +95,6 @@
"worldfixed.quest": "이 컴포넌트는 안드로이드 빌드를 대응하지 않습니다.",
"worldfixed.normal": "이 오브젝트는 Constraint 을 사용하여 아바타에 고정하지 않는 이상 월드축에 고정됩니다.",
"fpvisible.normal": "이 오브젝트는 일인칭 시점에서 보일 것입니다.",
"fpvisible.NotUnderHead": "Head 본(Bone) 내의 오브젝트가 아닌 경우 작동하지 않습니다.",
"fpvisible.quest": "이 컴포넌트는 오큘러스 퀘스트 단독 버전과 호환되지 않으며 영향을 미치지 않습니다.",
"fpvisible.InPhysBoneChain": "이 객체는 Physics Bone 체인에 의해 컨트롤되므로 체인의 세부적인 선택이 불가능합니다. 대신 체인의 시작 부분을 선택하세요.",
"blendshape.mesh": "메시",

View File

@ -1,108 +1,114 @@
{
"test0.test_a": "test_a",
"test0.test_b": "test_b",
"boneproxy.foldout.advanced": "高级设置",
"boneproxy.target": "目标",
"menuinstall.help.hint_set_menu": "此预制件的菜单默认会安装到 Avatar 的顶部菜单中。如果不需要,可以选择其他菜单或取消勾选此组件。",
"menuinstall.help.hint_bad_menu": "选择的菜单不属于此 Avatar。",
"menuinstall.installto": "安装到",
"menuinstall.installto.tooltip": "当前预制件的菜单将会安装到此菜单",
"menuinstall.installto.tooltip": "当前预制件包含的菜单将会安装到此菜单",
"menuinstall.selectmenu": "选择菜单",
"menuinstall.showcontents": "显示菜单内容",
"menuinstall.showcontents.notselected": "未选择菜单",
"menuinstall.devoptions": "预制件开发者选项",
"menuinstall.menu_icon_too_large": "菜单图标过大,图标应小于 256 像素。",
"menuinstall.menu_icon_uncompressed": "菜单图标未设置压缩。",
"menuinstall.srcmenu": "安装的菜单",
"params.syncmode.NotSynced": "仅动画控制器(不同步)",
"menuinstall.srcmenu": "安装的菜单",
"params.syncmode.NotSynced": "仅 Animator 内部(不同步)",
"params.syncmode.Int": "Int",
"params.syncmode.Float": "Float",
"params.syncmode.Bool": "Bool",
"params.syncmode.PhysBonesPrefix": "PB 前缀",
"params.__comment__": "=== Unity 2019 only strings ===",
"params.autodetect_header": " 自动检测参数 ",
"params.internal": "内部",
"params.pb_prefix": "PhysBones 前缀",
"params.syncmode": "参数模式",
"params.saved": "保存",
"params.synced": "同步",
"params.syncmode": "参数类型",
"params.saved": "保存",
"params.synced": "同步",
"params.default": "默认值",
"params.fieldname": "字段名",
"params.remapto": "字段名映射到",
"params.remapto.tooltip": "输入新的字段名以防止参数名称冲突",
"params.remapto.tooltip": "输入新的以防止参数名称冲突",
"params.devmode": "显示预制件开发者选项",
"params.__comment1__": "=== Unity 2022 only strings ===",
"merge_parameter.ui.name": "参数名称",
"merge_parameter.ui.prefix": "PhysBone 前缀名称",
"merge_parameter.ui.remapTo": "将名称更改为",
"merge_parameter.ui.remapTo.tooltip": "在这里输入一个新名称以重命名此参数或前缀。这可以用来解决名称冲突或用以连接各种功能模块。",
"merge_parameter.ui.remapTo.tooltip": "在这里输入一个新名称以重命名此参数或前缀。这可以用来解决名称冲突或链接多种功能模块。",
"merge_parameter.ui.remapTo.automatic": "(自动锁定)",
"merge_parameter.ui.defaultValue": "默认值",
"merge_parameter.ui.defaultValue.tooltip": "当重置 Avatar 或第一次使用 Avatar 时,此参数将会被设置为该值",
"merge_parameter.ui.defaultValue.tooltip": "首次使用或重置 Avatar 时,参数将会被设置为此值",
"merge_parameter.ui.saved": "保存",
"merge_parameter.ui.saved.tooltip": "如果勾选,当您更换 Avatar 或房间时,此参数的值将会被保存。",
"merge_parameter.ui.saved.tooltip": "如果勾选,当你更换 Avatar 或房间时,参数的值将被保存。",
"merge_parameter.ui.internalParameter": "自动重命名",
"merge_parameter.ui.internalParameter.tooltip": "如果勾选,此参数将会被自动重命名以防止参数名称冲突",
"merge_parameter.ui.isPrefix": "是 PhysBone 的前缀",
"merge_parameter.ui.syncType": "参数模式",
"merge_parameter.ui.internalParameter.tooltip": "如果勾选,此参数将会自动重命名以防止其名称和其他参数冲突",
"merge_parameter.ui.isPrefix": "是 PhysBone 前缀",
"merge_parameter.ui.syncType": "参数类型",
"merge_parameter.ui.synced": "同步",
"merge_parameter.ui.synced.tooltip": "如果勾选,参数将会在网络上同步",
"merge_parameter.ui.unregistered_foldout": "未注册的参数",
"merge_parameter.ui.add_button": "添加",
"merge_parameter.ui.details": "参数设置",
"merge_parameter.ui.overrideAnimatorDefaults": "覆盖动画控制器的默认值",
"merge_parameter.ui.overrideAnimatorDefaults": "覆盖 Animator 默认值",
"merge_parameter.ui.importFromAsset": "从 Assets 导入",
"merge_armature.merge_target": "合并目标",
"merge_armature.merge_target.tooltip": "将当前对象合并到合并目标中",
"merge_armature.merge_target.tooltip": "当前对象要合并到的骨架(或其子级)",
"merge_armature.prefix": "骨骼前缀",
"merge_armature.prefix.tooltip": "合并目标的骨骼前缀",
"merge_armature.prefix.tooltip": "当前对象里,要合并的骨骼前缀",
"merge_armature.suffix": "骨骼后缀",
"merge_armature.suffix.tooltip": "合并目标的骨骼后缀",
"merge_armature.suffix.tooltip": "当前对象里,要合并的骨骼后缀",
"merge_armature.locked": "锁定位置",
"merge_armature.locked.tooltip": "将当前对象的骨架与合并目标的骨架锁定,用于动画的创建。",
"merge_armature.locked.tooltip": "将当前对象的骨架与合并目标的骨架锁定(反之亦然)用于创建动画。",
"merge_armature.adjust_names": "根据合并目标调整骨骼名称",
"merge_armature.adjust_names.tooltip": "根据合并目标调整骨骼名称,通常用于不适配当前 Avatar 的服装。",
"merge_armature.mangle_names": "避免名冲突",
"merge_armature.mangle_names.tooltip": "通过重命名新添加的骨骼来避免与其他资源发生名冲突。",
"merge_armature.mangle_names": "避免冲突",
"merge_armature.mangle_names.tooltip": "通过重命名新添加的骨骼来避免与其他资源发生名冲突。",
"path_mode.Relative": "相对路径(基于当前对象)",
"path_mode.Absolute": "绝对路径(基于 Avatar 的 Root",
"merge_animator.animator": "合并的目标控制器",
"path_mode.Absolute": "绝对路径(基于 Avatar 的根对象",
"merge_animator.animator": "要合并的 Animator",
"merge_animator.layer_type": "Layer 类型",
"merge_animator.delete_attached_animator": "删除额外的动画控制器",
"merge_animator.delete_attached_animator.tooltip": "合并后删除当前对象上的动画控制器",
"merge_animator.delete_attached_animator": "删除额外的 Animator",
"merge_animator.delete_attached_animator.tooltip": "合并后删除当前对象上的 Animator 组件",
"merge_animator.path_mode": "路径模式",
"merge_animator.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让在当前对象上录制动画。",
"merge_animator.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让在当前对象上录制动画。",
"merge_animator.match_avatar_write_defaults": "匹配 Avatar 的 Write Defaults 设置",
"merge_animator.match_avatar_write_defaults.tooltip": "使用与 Avatar 一样的 Write Defaults 设置。\n如果设置存在冲突,将保留目标控制器的设置。",
"merge_animator.match_avatar_write_defaults.tooltip": "使用与 Avatar 一样的 Write Defaults 设置。\n如果 Avatar 将 WD 混用Animator 的 WD 设置将维持不变。",
"merge_animator.relative_path_root": "相对路径根对象",
"merge_animator.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定则使用当前对象。",
"merge_animator.layer_priority": "动画层合并优先级",
"merge_animator.layer_priority.tooltip": "控制动画层合并到目标控制器中的顺序\n由小到大。负值将合并于原有动画层之前\n0 或正值将使其合并于原有动画层之后。",
"merge_animator.layer_priority": "Animator layer 的合并优先级",
"merge_animator.layer_priority.tooltip": "控制动画层合并后在 Animator 里的位置\n由小到大。负值将合并于原有动画层之前\n0 或正值将合并于之后。",
"merge_armature.lockmode": "位置同步模式",
"merge_armature.lockmode.not_locked.title": "不同步",
"merge_armature.lockmode.not_locked.body": "Avatar 骨骼和合并目标骨骼不进行位置同步",
"merge_armature.lockmode.base_to_merge.title": "Avatar =====> 合并目标(单向)",
"merge_armature.lockmode.base_to_merge.body": "Avatar 骨骼位置改变,合并目标骨骼也会改变。\n合并目标骨骼位置改变Avatar 骨骼不会改变。\n建议一般服装使用此模式因为此模式允许调整合并目标的骨骼。",
"merge_armature.lockmode.bidirectional.title": "Avatar <=====> 合并目标(双向)",
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和合并目标骨骼的位置始终相同。\n此模式对创建基于 Avatar 骨骼的动画时非常有用。\n启用此模式要求 Avatar 骨骼与合并目标骨骼位置完全相同。",
"merge_armature.lockmode.not_locked.body": "当前对象的骨骼不会和 Avatar 骨骼位置同步",
"merge_armature.lockmode.base_to_merge.title": "Avatar =====> 目标(单向)",
"merge_armature.lockmode.base_to_merge.body": "Avatar 骨骼位置改变,当前对象的骨骼也会改变。\n但当当前对象的骨骼位置改变时Avatar 骨骼不会改变。\n建议一般服装使用此模式因为此模式允许调整服装的骨骼。",
"merge_armature.lockmode.bidirectional.title": "Avatar <=====> 目标(双向)",
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和当前对象的骨骼的位置始终相同。\n此模式对创建基于 Avatar 骨骼的动画时非常有用。\n启用此模式要求 Avatar 骨骼和当前对象的骨骼位置完全相同。",
"merge_armature.reset_pos": "将位置与 Avatar 进行对齐",
"merge_armature.reset_pos.info": "此命令将强制服装骨骼与 Avatar 骨骼进行对齐,在穿戴不适配当前 Avatar 的服装时可能有帮助。",
"merge_armature.reset_pos.convert_atpose": "转换 A-Pose/T-Pose 以匹配 Base Avatar",
"merge_armature.reset_pos.adjust_rotation": "也对齐旋转",
"merge_armature.reset_pos.adjust_scale": "也对齐缩放",
"merge_armature.reset_pos.execute": "执行",
"merge_armature.reset_pos.execute": "执行",
"merge_armature.reset_pos.heuristic_scale": "根据 Avatar 调整服装的整体比例",
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作为参考,调整服装的整体比例。\n推荐用于不适配当前 Avatar 的服装。",
"merge_blend_tree.blend_tree": "BlendTree",
"merge_blend_tree.path_mode": "路径模式",
"merge_blend_tree.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让在当前对象上录制动画。",
"merge_blend_tree.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让在当前对象上录制动画。",
"merge_blend_tree.relative_path_root": "相对路径根对象",
"merge_blend_tree.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定则使用当前对象。",
"worldfixed.quest": "此组件未生效,因为它与 Oculus Quest 不兼容。",
"worldfixed.normal": "当前对象将会固定于世界,除非使用约束将它绑定在 Avatar 内。",
"worldfixed.quest": "此组件未生效,因为它与 Android 环境不兼容。",
"worldfixed.normal": "当前对象将会固定于世界,除非使用约束将它绑定在 Avatar 内。",
"fpvisible.normal": "当前对象将在第一人称视角中可见。",
"fpvisible.NotUnderHead": "此组件未生效,因为它需要放置在 Head 骨骼下。",
"fpvisible.quest": "此组件未生效,因为它与 Oculus Quest 不兼容。",
"fpvisible.InPhysBoneChain": "当前对象由 PhysicsBone 控制,可能无法在第一人称视角中可见;请指定 PhysicsBone 链的起点。",
"fpvisible.NotUnderHead": "当这个组件不被放置在“Head”骨骼下方时不会生效。\n如果这个组件将通过 Bone Proxy 等方式放置在 “Head”骨骼下那么这个警告也许是可以被忽略的。",
"fpvisible.quest": "此组件未生效,因为它与 Android 环境不兼容。",
"fpvisible.InPhysBoneChain": "当前对象由 PhysBones 控制,可能无法在第一人称视角中可见;请指定 Physics Bone 链的起点。",
"blendshape.mesh": "网格",
"blendshape.source": "源 blendshape",
"blendshape.target": "目标 blendshape",
"hint.not_in_avatar": "此组件需要放置于您的 Avatar 内才能工作",
"boneproxy.err.MovingTarget": "不能指定将由其他 Modular Avatar 组件移动的目标对象",
"boneproxy.err.NotInAvatar": "必须指定一个在 Avatar 内的对象",
"hint.not_in_avatar": "此组件需要放置于你的 Avatar 内才能工作。",
"boneproxy.err.MovingTarget": "不能指定将由其他 Modular Avatar 组件移动的对象",
"boneproxy.err.NotInAvatar": "必须指定一个在 Avatar 内的对象",
"boneproxy.attachment": "附加模式",
"boneproxy.attachment.AsChildAtRoot": "作为子级,放置于 Root",
"boneproxy.attachment.AsChildKeepWorldPose": "作为子级,保持原有位置和旋转",
@ -111,64 +117,70 @@
"mesh_settings.header_probe_anchor": "锚点覆盖设置",
"mesh_settings.inherit_probe_anchor": "锚点覆盖模式",
"mesh_settings.probe_anchor": "锚点覆盖",
"mesh_settings.probe_anchor.tooltip": "Anchor Override\n设置用于当前对象和子对象内的渲染器的锚点覆盖",
"mesh_settings.header_bounds": "网格边界设置",
"mesh_settings.inherit_bounds": "网格边界模式",
"mesh_settings.probe_anchor.tooltip": "设置用于当前对象和子对象内的渲染器的锚点覆盖",
"mesh_settings.header_bounds": "网格边界覆盖设置",
"mesh_settings.inherit_bounds": "网格边界覆盖模式",
"mesh_settings.root_bone": "根骨骼",
"mesh_settings.root_bone.tooltip": "网格的根骨骼Root bone\n这被用作计算网格边界的参考点。",
"mesh_settings.bounds": "网格边界",
"mesh_settings.bounds.tooltip": "网格的边界Bounds\n用于确定何时要略过屏幕外的网格渲染。",
"mesh_settings.inherit_mode.Inherit": "继承",
"mesh_settings.inherit_mode.Set": "设置",
"mesh_settings.inherit_mode.DontSet": "不设置(保持原有的设置)",
"pb_blocker.help": "当前对象不会受到附加到父对象的 PhysBones 的影响。",
"hint.bad_vrcsdk": "检测到不兼容的 VRCSDK 版本。\n\n请尝试升级 VRCSDK如果这不起作用请尝试更新 Modular Avatar。",
"mesh_settings.inherit_mode.Set": "指定",
"mesh_settings.inherit_mode.DontSet": "不指定(保持原有的设置)",
"mesh_settings.inherit_mode.SetOrInherit": "父级设置优先,否则遵从指定的设置",
"pb_blocker.help": "当前对象不会受到附加在父对象的 PhysBones 影响。",
"hint.bad_vrcsdk": "检测到不兼容的 VRCSDK 版本。\n\n请尝试升级 VRCSDK如果这不起作用请尝试更新到新版本的 Modular Avatar。",
"error.stack_trace": "Stack trace请在报告错误时提供此信息",
"error.merge_armature.circular_dependency": "[MA-0001] 在 Merge armature 组件中存在循环引用",
"error.merge_armature.circular_dependency:description": "您的 Merge Armature 组件正在将自身或其子级作为合并目标。",
"error.merge_armature.circular_dependency:hint": "通常应在 Merge Armature 的合并目标字段指定 Avatar 本身的骨骼。不要指定服装本身!",
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] 在 Humanoid 骨骼上有 PhysBones 组件",
"error.merge_armature.physbone_on_humanoid_bone:hint": "在要合并的骨架中,某些 Humanoid 骨骼受到 PhysBones 控制,其位置与合并目标中相应的 Humanoid 骨骼不同。您应该在要合并的骨架中,移除这些 Humanoid 骨骼上的 PhysBones。",
"error.merge_armature.circular_dependency:description": "你的 Merge Armature 组件正在将自身或其子级作为合并目标。",
"error.merge_armature.circular_dependency:hint": "通常应在 Merge Armature 组件的「合并目标」字段上指定 Avatar 本身的 Armture。不要指定服装本身",
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] 在 Humanoid 骨骼上存在 PhysBones 组件",
"error.merge_armature.physbone_on_humanoid_bone:hint": "在要合并的骨架中,某些 Humanoid 骨骼受到 PhysBones 控制,其位置与合并目标中相应的 Humanoid 骨骼不同。你应该在要合并的骨架中,移除这些 Humanoid 骨骼上的 PhysBones。",
"error.merge_blend_tree.missing_tree": "[MA-0009] 未指定 Blend Tree",
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Tree 需要知道要合并到哪个 Blend Tree。请尝试在「Blend Tree」中设置一个对象。",
"error.internal_error": "[MA-9999] 发生内部错误:{0}\nwhen processing:",
"error.merge_animator.param_type_mismatch": "[MA-0003] 参数类型不符",
"error.merge_animator.param_type_mismatch:description": "参数 {0} 具有多种类型:{1} != {2}",
"error.rename_params.too_many_synced_params": "同步参数超出限制Cost {0} > {1}",
"error.rename_params.too_many_synced_params:description": "您在 Avatar 中使用了太多的同步参数。您已使用 {0} bits 的参数,但限制为 {1}。",
"error.rename_params.too_many_synced_params:description": "你在 Avatar 中使用了太多的同步参数。你已使用 {0} bits 的参数,但限制为 {1}。",
"error.rename_params.type_conflict": "[MA-0006] 参数类型冲突",
"error.rename_params.type_conflict:description": "参数 {0} 指定了多种类型:{1} != {2}",
"error.rename_params.default_value_conflict": "[MA-0007] 默认值冲突",
"error.rename_params.default_value_conflict:description": "参数 {0} 指定了多个默认值:{1} != {2}",
"error.rename_params.default_value_conflict:hint": "为了避免不可预测的行为,请将 MA Parameters 组件中所有默认值留空只留一个。如果存在多个值Modular Avatar 将选择第一个默认值。",
"error.replace_object.null_target": "[MA-0008] 未指定替代对象",
"error.replace_object.null_target:hint": "Replace object 需要一个对象来替代。尝试指定一个。",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此对象上找不到 Renderer",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用于相同对象上的 Skinned Mesh Renderer。您是否将它附加到正确的对象上",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此对象的 Renderer 上找不到网格Mesh",
"error.rename_params.default_value_conflict:hint": "为了避免不可预知的行为,请将 MA Parameters 组件中所有重复的默认值置空只留一个相关值即可。如果存在多个值Modular Avatar 将选择第一个默认值。",
"error.replace_object.null_target": "[MA-0008] 未指定要替换的对象",
"error.replace_object.null_target:hint": "Replace object 需要知道要替换掉哪个对象。尝试指定一个对象再试试。",
"error.replace_object.replacing_replacement": "[MA-0009] 不能在多个 Replace Object 组件中指定相同的目标对象",
"error.replace_object.parent_of_target": "[MA-0010] 目标对象不能是此对象的父级",
"error.singleton": "[MA-0011] 一个 Avatar 中只允许存在一个 {0} 组件",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此部件上找不到 Renderer",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用于所在对象上的 Skinned Mesh Renderer。你是否将它附加到了正确的对象上",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此部件的 Renderer 上找不到网格Mesh",
"validation.blendshape_sync.no_local_mesh:hint": "当前对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
"validation.blendshape_sync.no_bindings": "[MA-1002] 在此对象上找不到 BlendShape",
"validation.blendshape_sync.no_bindings:hint": "Blendshape Sync 必须知道要同步哪些 blendshapes。点击「+」新增。",
"validation.blendshape_sync.no_bindings": "[MA-1002] 在此部件上找不到 BlendShape",
"validation.blendshape_sync.no_bindings:hint": "Blendshape Sync 必须知道要同步哪些 blendshape。点击「+」新增。",
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 找不到目标 BlendShape「{0}」",
"validation.blendshape_sync.missing_local_shape:description": "找不到目标 BlendShape{0}",
"validation.blendshape_sync.missing_local_shape:hint": "配置为从目标对象「接收」值的 BlendShape 缺失。请尝试更换被标红的 Blendshape。",
"validation.blendshape_sync.missing_local_shape:hint": "找不到「接收」数值的 BlendShape。请更换被标为红色的 Blendshape。",
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 找不到源 BlendShape「{0}」",
"validation.blendshape_sync.missing_target_shape:description": "找不到源 BlendShape{0}",
"validation.blendshape_sync.missing_target_shape:hint": "配置为向本地对象「发送」值的 BlendShape 缺失。请尝试更换被标红的 Blendshape。",
"validation.blendshape_sync.missing_target_shape:hint": "找不到向当前部件「发送」数值的 BlendShape。请更换被标为红色的 Blendshape。",
"validation.blendshape_sync.no_target": "[MA-1005] 未指定目标对象(网格)",
"validation.blendshape_sync.no_target:hint": "Blendshape Sync 需要知道要从哪个对象同步 Blendshape。请尝试在「网格」中设置一个对象。",
"validation.blendshape_sync.no_target:hint": "Blendshape Sync 必须知道要从哪个对象同步 Blendshape。请尝试在「网格」中指定一个对象。",
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 在目标对象上找不到 Renderer",
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Sync 会从目标对象上的 Skinned Mesh Renderer 接收 blendshape 的值。您将它附加到正确的对象上了吗",
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 在目标对象的渲染器上找不到网格Mesh",
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Sync 会从目标对象上的 Skinned Mesh Renderer 接收 blendshape 的值。你有没有将它附加到正确的对象上呢",
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 在目标对象的 Renderer 上找不到网格Mesh",
"validation.blendshape_sync.missing_target_mesh:hint": "目标对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
"validation.bone_proxy.no_target": "[MA-1100] 未指定目标对象,或未找到目标对象",
"validation.bone_proxy.no_target:hint": "Bone Proxy 需要知道要将当前对象绑定到哪个目标对象。请尝试在「目标」中设置一个当前对象应该绑定到的目标对象。",
"validation.menu_installer.no_menu": "[MA-1200] 未指定安装的菜单",
"validation.menu_installer.no_menu:hint": "Menu Installer 需要知道要将此预制件安装到哪个菜单。请先配置「预制件开发者选项」里的「安装的菜单」或是新增一个「MA Menu Item」组件。",
"validation.merge_animator.no_animator": "[MA-1300] 未指定要合并的目标动画器",
"validation.merge_animator.no_animator:hint": "Merge Animator 需要知道要合并到哪个动画控制器。请尝试在「合并的目标控制器」中设置一个对象。",
"validation.bone_proxy.no_target": "[MA-1100] 未指定目标对象,或目标对象未找到",
"validation.bone_proxy.no_target:hint": "Bone Proxy 必须知道要将当前对象绑定到哪个对象。请在「目标」中指定。",
"validation.menu_installer.no_menu": "[MA-1200] 未指定安装的菜单",
"validation.menu_installer.no_menu:hint": "Menu Installer 必须知道要安装哪个菜单。请在「预制件开发者选项」里的「要安装的菜单」指定或是新增一个「MA Menu Item」组件。",
"validation.merge_animator.no_animator": "[MA-1300] 未指定要合并的 Animator",
"validation.merge_animator.no_animator:hint": "Merge Animator 必须知道要合并哪个 Animator。请在「要合并的 Animator」中指定一个。",
"validation.merge_armature.no_target": "[MA-1400] 未指定合并目标",
"validation.merge_armature.no_target:hint": "Merge Armature 需要知道要合并到哪个骨架。请尝试在「合并目标」中设置一个对象。",
"validation.merge_armature.no_target:hint": "Merge Armature 必须知道相应骨骼要合并到哪个骨架里。请在「合并目标」中指定一个。",
"validation.merge_armature.target_is_child": "[MA-1500] 合并目标不能是此对象的子级",
"validation.merge_armature.target_is_child:hint": "Merge Armature 不能合并一个骨架到其自身。请尝试将「合并目标」设置为其他对象。",
"validation.merge_armature.target_is_child:hint": "Merge Armature 不能合并骨架到其自身。请将「合并目标」设置为其他对象。",
"submenu_source.Children": "子对象",
"submenu_source.MenuAsset": "菜单资源文件 (Expressions Menu)",
"menuitem.showcontents": "显示菜单内容",
@ -178,63 +190,107 @@
"menuitem.prop.type": "类型",
"menuitem.prop.type.tooltip": "此菜单项的类型",
"menuitem.prop.value": "参数值",
"menuitem.prop.value.tooltip": "菜单项触发时设置的参数值",
"menuitem.prop.value.tooltip": "设置菜单项触发时的参数值",
"menuitem.prop.automatic_value": "自动",
"menuitem.prop.automatic_value.tooltip": "自动将此控制项设置为唯一值",
"menuitem.prop.parameter": "参数",
"menuitem.prop.label": "名称",
"menuitem.prop.label": "标签",
"menuitem.prop.submenu_asset": "子菜单资源",
"menuitem.prop.submenu_asset.tooltip": "用作子菜单的资源文件",
"menuitem.prop.submenu_asset.tooltip": "用作子菜单的资源文件 (Expressions Menu)",
"menuitem.prop.submenu_source": "子菜单来源",
"menuitem.prop.submenu_source.tooltip": "寻找子菜单的菜单项的方式",
"menuitem.prop.source_override": "源对象",
"menuitem.prop.source_override.tooltip": "如果指定,此对象将被用作子菜单内容的来源。\n否则将使用此菜单项目的子级。",
"menuitem.prop.submenu_source.tooltip": "寻找子菜单内菜单项的方式",
"menuitem.prop.source_override": "源部件覆盖",
"menuitem.prop.source_override.tooltip": "如果指定,这个对象将被用作子菜单内容的来源。\n否则将使用此菜单项的子级菜单作为来源。",
"menuitem.prop.is_default": "默认启用",
"menuitem.prop.is_default.tooltip": "如果勾选,初次使用或重置 Avatar 时会启用此菜单项",
"menuitem.prop.is_saved": "保存",
"menuitem.prop.is_saved.tooltip": "如果勾选,当您更换 Avatar 或房间时,此菜单项目的值将被保存。",
"menuitem.prop.is_saved.tooltip": "如果勾选,当你更换 Avatar 或房间时,此菜单项的值将被保存。",
"menuitem.prop.is_synced": "同步",
"menuitem.prop.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
"menuitem.prop.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
"menuitem.param.rotation": "参数: 旋转 (Rotation)",
"menuitem.param.rotation.tooltip": "基于此菜单项的旋转设置的参数。",
"menuitem.param.rotation.tooltip": "基于此菜单项的旋转设置的参数。",
"menuitem.param.horizontal": "参数: 水平 (Horizontal)",
"menuitem.param.horizontal.tooltip": "基于摇杆的水平位置设置的参数。",
"menuitem.param.vertical": "参数: 垂直 (Vertical)",
"menuitem.param.vertical.tooltip": "基于摇杆的垂直位置设置的参数。",
"menuitem.label.control_labels_and_params": "设置名称和参数",
"menuitem.label.control_labels": "设置名称",
"menuitem.misc.multiple": "复数设置)",
"menuitem.misc.multiple": "多重设置)",
"menuitem.misc.no_icon": "(无图标)",
"menuitem.misc.extract": "提取到对象",
"menuitem.misc.extract": "提取为部件",
"menuitem.label.parameters": "参数",
"action.toggle_object.header.object": "要显示 / 隐藏的对象",
"action.toggle_object.header.show": "显示",
"menu_tree.title": "选择菜单",
"menuitem.param.controlled_by_action": "<controlled by action>",
"control_group.foldout.actions": "Actions",
"control_group.foldout.menu_items": "相关菜单项",
"control_group.is_saved": "保存",
"control_group.is_saved.tooltip": "如果勾选,当更换 Avatar 或世界时,此菜单项的值将被保存。",
"control_group.is_saved.tooltip": "如果勾选,当更换 Avatar 或世界时,此菜单项的值将被保存。",
"control_group.is_synced": "同步",
"control_group.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
"control_group.default_value": "初始设置",
"control_group.default_value.unset": "(未选择)",
"animation_gen.duplicate_binding": "来自不同控制组的控制项尝试修改动画相同的参数。参数:{0}",
"animation_gen.multiple_defaults": "在同一个控制组中找到多个默认的菜单项。",
"animation_gen.duplicate_binding": "来自不同部件组的部件尝试动画化相同的参数。参数:{0}",
"animation_gen.multiple_defaults": "在同一个部件组中找到多个默认的菜单项。",
"menuitem.misc.add_item": "添加菜单项",
"replace_object.target_object": "要替换的对象",
"setup_outfit.err.header.notarget": "Setup Outfit 失败",
"setup_outfit.err.header": "对 {0} 进行 Setup Outfit 操作失败",
"setup_outfit.err.unknown": "未知错误",
"setup_outfit.err.no_selection": "没有选择对象。",
"setup_outfit.err.run_on_avatar_itself": "Setup outfit 必须在服装对象上进行,而不是在 Avatar 本身。\n\n您要制作「混合 Avatar」吗如果是请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
"setup_outfit.err.multiple_avatar_descriptors": "在 {0} 和其父级中有多个 Avatar descriptor。\n\n要制作「混合 Avatar」吗如果是请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父级中找不到 VRC Avatar Descriptor。请确保您的服装放置在 Avatar 内。",
"setup_outfit.err.no_animator": "您的 Avatar 没有动画控制器 (Animator) 组件。",
"setup_outfit.err.no_hips": "的 Avatar 没有 Hips 骨骼。Setup Outfit 只能用于 humanoid Avatars。",
"setup_outfit.err.no_outfit_hips": "无法识别服装的 Hips,已搜索包含以下名称的对象:",
"setup_outfit.err.run_on_avatar_itself": "Setup outfit 必须在服装对象上运行,而不是在 Avatar 本身。\n\n你是要制作「混合 Avatar」吗如果是请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
"setup_outfit.err.multiple_avatar_descriptors": "在 {0} 和其父级中有多个 Avatar descriptor。\n\n你是要制作「混合 Avatar」吗如果是请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父级中找不到 VRC Avatar Descriptor。请确认你的服装是否被正确放置在 Avatar 里。",
"setup_outfit.err.no_animator": "你的 Avatar 没有 Animator 组件。",
"setup_outfit.err.no_hips": "的 Avatar 没有 Hips 骨骼。Setup Outfit 只能用于人形 (humanoid) 的 Avatar。",
"setup_outfit.err.no_outfit_hips": "无法识别服装的 Hips 骨骼,已尝试搜索包含以下名称的对象:",
"move_independently.group-header": "要一起移动的对象",
"scale_adjuster.scale": "调整比例",
"scale_adjuster.adjust_children": "调整子级的位置",
"world_fixed_object.err.unsupported_platform": "此平台不支持 World Fixed Object 组件。",
"world_fixed_object.err.unsupported_platform:description": "此组件未生效。因为在 Android 平台下不支持 World Fixed Object。",
"world_fixed_object.err.unsupported_platform:description": "此组件未生效。因为在 Android 环境下不支持 World Fixed Object。",
"ma_info.param_usage_ui.header": "Expressions 参数使用情况",
"ma_info.param_usage_ui.other_objects": "此 Avatar 上的其他对象",
"ma_info.param_usage_ui.free_space": "未使用的参数空间 ({0} bits)",
"ma_info.param_usage_ui.bits_template": "{0} ({1} bits)"
"ma_info.param_usage_ui.other_objects": "此 Avatar 中的其他东西",
"ma_info.param_usage_ui.free_space": "未使用的参数 ({0} bits)",
"ma_info.param_usage_ui.bits_template": "{0} ({1} bits)",
"ma_info.param_usage_ui.no_data": "【无信息】",
"reactive_object.inverse": "反转条件",
"reactive_object.material-setter.set-to": "将材质设置为:",
"menuitem.misc.add_toggle": "新增开关",
"ro_sim.open_debugger_button": "开启 Reaction 调试工具",
"ro_sim.window.title": "MA 响应调试工具",
"ro_sim.header.inspecting": "检查部件",
"ro_sim.header.clear_overrides": "清除所有覆盖",
"ro_sim.header.object_state": "对象状态",
"ro_sim.state.active": "启用",
"ro_sim.state.inactive": "停用",
"ro_sim.header.override_gameobject_state": "覆写对象状态",
"ro_sim.header.override_menuitem_state": "覆写菜单项状态",
"ro_sim.affected_by.title": "受到以下影响:",
"ro_sim.effect_group.component": "接收组件",
"ro_sim.effect_group.controls_obj_state": "控制对象状态为:",
"ro_sim.effect_group.target_component": "目标组件",
"ro_sim.effect_group.target_component.tooltip": "受到 Reactive Component 影响的组件",
"ro_sim.effect_group.property": "属性",
"ro_sim.effect_group.property.tooltip": "受到 Reactive Component 影响的目标组件属性",
"ro_sim.effect_group.value": "值",
"ro_sim.effect_group.value.tooltip": "Reactive Component 启用时,属性将被设为此值",
"ro_sim.effect_group.material": "材质",
"ro_sim.effect_group.material.tooltip": "Reactive Component 启用时,将被设在目标组件上的材质",
"ro_sim.effect_group.rule_inverted": "规则的条件已反转",
"ro_sim.effect_group.rule_inverted.tooltip": "这条规则将在未达成其任一条件时应用。",
"ro_sim.effect_group.conditions": "条件",
"menuitem.label.long_name.tooltip": "使用可能包含富文本和间隔线的长名称。",
"menuitem.label.gameobject_name.tooltip": "使用对象的名称。",
"remove-vertex-color.mode": "模式",
"remove-vertex-color.mode.Remove": "删除顶点颜色",
"remove-vertex-color.mode.DontRemove": "保留顶点颜色",
"general.vrcsdk-required": "此组件需要 VRCSDK 才能工作。",
"sync-param-sequence.platform": "主平台",
"sync-param-sequence.platform.tooltip": "此平台构建时Modular Avatar将记录所有参数供其他平台使用",
"sync-param-sequence.parameters": "共用参数的 Asset",
"sync-param-sequence.parameters.tooltip": "用于存储共用参数的 Assets。请勿使用已经在 Avatar descriptor 中设置的相同参数 。",
"sync-param-sequence.create-asset": "新增",
"sync-param-sequence.create-asset.tooltip": "创建一个新的 expression parameters"
}

View File

@ -51,6 +51,7 @@
"merge_parameter.ui.add_button": "添加",
"merge_parameter.ui.details": "參數設定",
"merge_parameter.ui.overrideAnimatorDefaults": "覆蓋 Animator 預設值",
"merge_parameter.ui.importFromAsset": "從 Assets 匯入",
"merge_armature.merge_target": "合併目標",
"merge_armature.merge_target.tooltip": "當前物件要合併到的骨架(或其子級)",
"merge_armature.prefix": "骨骼前綴",
@ -86,12 +87,12 @@
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和當前物件的骨骼的位置始終相同。\n此模式對創建基於 Avatar 骨骼的動畫時非常有用。\n啟用此模式要求 Avatar 骨骼和當前物件的骨骼位置完全相同。",
"merge_armature.reset_pos": "將位置與 Avatar 進行對齊",
"merge_armature.reset_pos.info": "此命令將強制服裝骨骼與 Avatar 骨骼進行對齊,在使用非 Avatar 對應的服裝時可能有幫助。",
"merge_armature.reset_pos.convert_atpose": "轉換 A-Pose/T-Pose 以符合角色",
"merge_armature.reset_pos.adjust_rotation": "也對齊旋轉",
"merge_armature.reset_pos.adjust_scale": "也對齊縮放",
"merge_armature.reset_pos.execute": "執行",
"merge_armature.reset_pos.heuristic_scale": "根據 Avatar 調整服裝的整體比例",
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作為參考,調整服裝的整體比例。\n推薦用於非 Avatar 對應的服裝。",
"merge_blend_tree.blend_tree": "Blend Tree",
"merge_blend_tree.path_mode": "路徑模式",
"merge_blend_tree.path_mode.tooltip": "在動畫中路徑的工作模式。\n使用相對路徑可以讓你在當前物件上錄制動畫。",
"merge_blend_tree.relative_path_root": "相對路徑根物件",
@ -99,7 +100,7 @@
"worldfixed.quest": "此元件未生效,因為它與 Android 環境不相容。",
"worldfixed.normal": "當前物件將會固定於世界,除非你使用約束將它綁在 Avatar 內。",
"fpvisible.normal": "當前物件將在第一人稱視角中可見。",
"fpvisible.NotUnderHead": "此元件未生效,因為它需要放置在 Head 骨骼下。",
"fpvisible.NotUnderHead": "此元件在未放置於 Head 骨骼下時不會生效。\n如果是透過 Bone Proxy 等方式將其放於 Head 骨骼下,則可忽略此警告。",
"fpvisible.quest": "此元件未生效,因為它與 Android 環境不相容。",
"fpvisible.InPhysBoneChain": "當前物件由 Physics Bone 控制,可能無法在第一人稱視角中可見;請指定 Physics Bone 鏈的起點。",
"blendshape.mesh": "網格",
@ -149,6 +150,9 @@
"error.rename_params.default_value_conflict:hint": "為了避免不可預測的行為,請將 MA Parameters 元件中所有預設值留空只留一個。如果存在多個值Modular Avatar 將選擇第一個預設值。",
"error.replace_object.null_target": "[MA-0008] 未指定要替換的物件",
"error.replace_object.null_target:hint": "Replace object 需要知道要替換掉哪個物件。嘗試指定一個。",
"error.replace_object.replacing_replacement": "[MA-0009] 不能在多個 Replace Object 元件中指定相同的目標物件",
"error.replace_object.parent_of_target": "[MA-0010] 目標物件不能是此物件的父級",
"error.singleton": "[MA-0011] 在一個 Avatar 中只允許存在單個 {0} 元件",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此物件上找不到 Renderer",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用於所在物件上的 Skinned Mesh Renderer。你是否將它附加到正確的物件上",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此物件的 Renderer 上找不到網格Mesh",
@ -187,6 +191,8 @@
"menuitem.prop.type.tooltip": "此選單項的類型",
"menuitem.prop.value": "參數值",
"menuitem.prop.value.tooltip": "設定選單項觸發時的參數值",
"menuitem.prop.automatic_value": "自動",
"menuitem.prop.automatic_value.tooltip": "自動將此控制設定為唯一值",
"menuitem.prop.parameter": "參數",
"menuitem.prop.label": "名稱",
"menuitem.prop.submenu_asset": "子選單資源",
@ -238,7 +244,7 @@
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父級中找不到 VRC Avatar Descriptor。請確保你的服裝放置在 Avatar 裡。",
"setup_outfit.err.no_animator": "你的 Avatar 沒有 Animator 元件。",
"setup_outfit.err.no_hips": "你的 Avatar 沒有 Hips 骨骼。Setup Outfit 只能用於 humanoid Avatars。",
"setup_outfit.err.no_outfit_hips": "識別不到服裝的 Hips,已搜尋含以下名稱的物件:",
"setup_outfit.err.no_outfit_hips": "識別不到服裝的 Hips 骨骼,已嘗試搜尋含以下名稱的骨骼物件(不區分大小寫)",
"move_independently.group-header": "要一起移動的物件",
"scale_adjuster.scale": "調整比例",
"scale_adjuster.adjust_children": "調整子級的位置",
@ -252,5 +258,38 @@
"reactive_object.inverse": "反轉條件",
"reactive_object.material-setter.set-to": "將材質設定為:",
"menuitem.misc.add_toggle": "新增開關",
"ro_sim.effect_group.material": "材質"
"ro_sim.open_debugger_button": "開啟 Reaction 除錯工具",
"ro_sim.window.title": "MA 響應除錯工具",
"ro_sim.header.inspecting": "檢視物件",
"ro_sim.header.clear_overrides": "清除所有覆寫",
"ro_sim.header.object_state": "物件狀態",
"ro_sim.state.active": "啟用",
"ro_sim.state.inactive": "停用",
"ro_sim.header.override_gameobject_state": "覆寫物件狀態",
"ro_sim.header.override_menuitem_state": "覆寫選單項狀態",
"ro_sim.affected_by.title": "受到以下影響:",
"ro_sim.effect_group.controls_obj_state": "控制物件狀態為:",
"ro_sim.effect_group.target_component": "目標元件",
"ro_sim.effect_group.target_component.tooltip": "受到 Reactive Component 影響的元件",
"ro_sim.effect_group.property": "屬性",
"ro_sim.effect_group.property.tooltip": "受到 Reactive Component 影響的目標元件屬性",
"ro_sim.effect_group.value": "值",
"ro_sim.effect_group.value.tooltip": "Reactive Component 啟用時,屬性將被設為此值",
"ro_sim.effect_group.material": "材質",
"ro_sim.effect_group.material.tooltip": "Reactive Component 啟用時,將被設在目標元件上的材質",
"ro_sim.effect_group.rule_inverted": "規則的條件已反轉",
"ro_sim.effect_group.rule_inverted.tooltip": "這條規則將在未達成其任一條件時套用。",
"ro_sim.effect_group.conditions": "條件",
"menuitem.label.long_name.tooltip": "使用可能含有富文本和換行符號的長名稱。",
"menuitem.label.gameobject_name.tooltip": "使用物件的名稱。",
"remove-vertex-color.mode": "模式",
"remove-vertex-color.mode.Remove": "移除頂點顏色",
"remove-vertex-color.mode.DontRemove": "保留頂點顏色",
"general.vrcsdk-required": "此元件需要 VRCSDK 才能運作。",
"sync-param-sequence.platform": "主平台",
"sync-param-sequence.platform.tooltip": "此平台進行建置時Modular Avatar 將記錄所有參數,供其他平台使用。",
"sync-param-sequence.parameters": "共用的參數資源",
"sync-param-sequence.parameters.tooltip": "用來儲存共用參數的資源檔。請勿使用已在 Avatar Descriptor 中設定的相同參數。",
"sync-param-sequence.create-asset": "新增",
"sync-param-sequence.create-asset.tooltip": "創建一個新的 Expression Parameters"
}

View File

@ -119,9 +119,11 @@ namespace nadena.dev.modular_avatar.core.editor
internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
{
var type = c.type != 0 ? c.type : VRCExpressionsMenu.Control.ControlType.Button;
return new VRCExpressionsMenu.Control()
{
type = c.type,
type = type,
name = c.name,
icon = c.icon,
parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name },

View File

@ -7,9 +7,11 @@ using System.Linq;
using JetBrains.Annotations;
using nadena.dev.modular_avatar.core.menu;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor.menu
{
@ -40,9 +42,9 @@ namespace nadena.dev.modular_avatar.core.editor.menu
private readonly ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>>
_menuToInstallerMap;
private readonly ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>
private readonly ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>
_postProcessControls
= ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>.Empty;
= ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>.Empty;
private readonly VirtualMenuNode _node;
private readonly NodeForDelegate _nodeFor;
@ -75,7 +77,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
VirtualMenuNode node,
NodeForDelegate nodeFor,
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerMap,
ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> postProcessControls,
ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>> postProcessControls,
Action<VRCExpressionsMenu> visitedMenu,
Action<VRCExpressionsMenu.Control> postprocessor
)
@ -102,7 +104,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
PushControl(control);
}
if (_menuToInstallerMap.TryGetValue(expMenu, out var installers))
if (_menuToInstallerMap.TryGetValue(ObjectRegistry.GetReference(expMenu), out var installers))
{
foreach (var installer in installers)
{
@ -127,7 +129,13 @@ namespace nadena.dev.modular_avatar.core.editor.menu
if (_visited.Contains(source)) return;
_visited.Add(source);
BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this));
var sourceObj = source as Object;
var postProcessor = sourceObj != null ? _postProcessControls.GetValueOrDefault(sourceObj) : default;
using (new PostprocessorContext(this, postProcessor))
{
BuildReport.ReportingObject(source as Object, () => source.Visit(this));
}
_visited.Remove(source);
}
@ -228,9 +236,9 @@ namespace nadena.dev.modular_avatar.core.editor.menu
private Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
private ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>
private ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>
_postprocessControlsHooks =
ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>.Empty;
ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>.Empty;
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
@ -311,7 +319,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
// initial validation
if (installer.menuToAppend == null && installer.GetComponent<MenuSource>() == null) return;
var target = installer.installTargetMenu ? (object) installer.installTargetMenu : RootMenuKey;
var target = installer.installTargetMenu ? (object) ObjectRegistry.GetReference(installer.installTargetMenu) : RootMenuKey;
if (!_targetMenuToInstaller.TryGetValue(target, out var targets))
{
targets = new List<ModularAvatarMenuInstaller>();
@ -366,7 +374,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
}
// Some menu installers may be bound to the root menu _asset_ directly.
if (menuToInstallerFiltered.TryGetValue(menu, out var installers))
if (menuToInstallerFiltered.TryGetValue(ObjectRegistry.GetReference(menu), out var installers))
{
foreach (var installer in installers)
{
@ -405,7 +413,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
_pendingGeneration.Enqueue(() =>
{
BuildReport.ReportingObject(key as UnityEngine.Object, () =>
BuildReport.ReportingObject(key as Object, () =>
{
var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered,
_postprocessControlsHooks,
@ -430,7 +438,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
}
}
internal VRCExpressionsMenu SerializeMenu(Action<UnityEngine.Object> SaveAsset)
internal VRCExpressionsMenu SerializeMenu(Action<Object> SaveAsset)
{
Dictionary<object, VRCExpressionsMenu> serializedMenus = new Dictionary<object, VRCExpressionsMenu>();

View File

@ -27,47 +27,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class MergeAnimatorProcessor
{
private const string SAMPLE_PATH_PACKAGE =
"Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers";
private AnimatorServicesContext _asc;
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
[InitializeOnLoadMethod]
private static void Init()
{
ModularAvatarMergeAnimator.GetMotionBasePathCallback = (merge, objectBuildContext) =>
{
if (merge.pathMode == MergeAnimatorPathMode.Absolute) return "";
private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92";
var context = (ndmf.BuildContext)objectBuildContext;
private BuildContext _context;
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
private Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?> writeDefaults_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?>();
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
return relativePath != "" ? relativePath : "";
};
}
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{
_context = context;
defaultControllers_.Clear();
mergeSessions.Clear();
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
@ -88,10 +81,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
ProcessLayerType(context, entry.Key, entry.Value);
}
descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers);
descriptor.specialAnimationLayers = FinishSessions(descriptor.specialAnimationLayers);
descriptor.customizeAnimationLayers = true;
}
private void ProcessLayerType(
@ -100,229 +89,124 @@ namespace nadena.dev.modular_avatar.core.editor
List<ModularAvatarMergeAnimator> toMerge
)
{
// Stable sort
var sorted = toMerge.OrderBy(x => x.layerPriority)
.ToList();
var beforeOriginal = sorted.Where(x => x.layerPriority < 0)
.ToList();
var afterOriginal = sorted.Where(x => x.layerPriority >= 0)
// Layer priority sorting is handled by NDMF, so we just need to worry about replace mode going first
var sorted = toMerge.OrderBy(x => x.mergeAnimatorMode == MergeAnimatorMode.Append)
.ToList();
var session = new AnimatorCombiner(context.PluginBuildContext, layerType.ToString() + " (merged)");
mergeSessions[layerType] = session;
mergeSessions[layerType].BlendableLayer = BlendableLayerFor(layerType);
var controller = _asc.ControllerContext.Controllers[layerType];
foreach (var component in beforeOriginal)
var replacements = sorted.Count(x => x.mergeAnimatorMode == MergeAnimatorMode.Replace);
if (replacements > 1)
{
MergeSingle(context, session, component);
BuildReport.LogFatal("error.merge_animator.multiple_replacements",
sorted.Where(x => x.mergeAnimatorMode == MergeAnimatorMode.Replace).ToArray<object>());
}
if (defaultControllers_.TryGetValue(layerType, out var defaultController) &&
defaultController.layers.Length > 0)
else if (replacements == 1)
{
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
// Delete all pre-existing layers.
// Retain the blend tree layer, since that will generally be placed as the first layer in the animator
controller.RemoveLayers(l => l.Name != MergeBlendTreePass.BlendTreeLayerName);
// Merge just the first controller (the one that replaces)
MergeSingle(context, controller, sorted.First(), null);
sorted.RemoveAt(0);
// We'll now continue processing the rest as normal.
}
foreach (var component in afterOriginal)
bool? writeDefaults = null;
var wdStateCounter = controller.Layers.SelectMany(l => l.StateMachine.AllStates())
.Select(s => s.WriteDefaultValues)
.GroupBy(b => b)
.ToDictionary(g => g.Key, g => g.Count());
if (wdStateCounter.Count == 1) writeDefaults = wdStateCounter.First().Key;
foreach (var component in sorted)
{
MergeSingle(context, session, component);
MergeSingle(context, controller, component, writeDefaults);
}
}
private void MergeSingle(BuildContext context, AnimatorCombiner session, ModularAvatarMergeAnimator merge)
private void MergeSingle(BuildContext context, VirtualAnimatorController targetController,
ModularAvatarMergeAnimator merge, bool? initialWriteDefaults)
{
if (merge.animator == null)
{
return;
}
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
if (!merge.matchAvatarWriteDefaults)
{
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
else
{
basePath = "";
initialWriteDefaults = null;
}
var writeDefaults = merge.matchAvatarWriteDefaults
? writeDefaults_.GetValueOrDefault(merge.layerType)
: null;
var controller = _context.ConvertAnimatorController(merge.animator);
session.AddController(basePath, controller, writeDefaults);
var vac = context.PluginBuildContext.Extension<VirtualControllerContext>();
if (!vac.Controllers.TryGetValue(merge, out var clonedController)) return;
var firstLayer = clonedController.Layers.FirstOrDefault();
// the first layer in an animator controller always has weight 1.0f (regardless of what is serialized)
if (firstLayer != null) firstLayer.DefaultWeight = 1.0f;
foreach (var l in clonedController.Layers)
{
if (initialWriteDefaults != null && !IsWriteDefaultsSafeLayer(l))
{
foreach (var s in l.StateMachine?.AllStates() ?? Array.Empty<VirtualState>())
{
s.WriteDefaultValues = initialWriteDefaults.Value;
}
}
targetController.AddLayer(new LayerPriority(merge.layerPriority), l);
}
foreach (var (name, parameter) in clonedController.Parameters)
{
if (targetController.Parameters.TryGetValue(name, out var existingParam))
{
if (existingParam.type != parameter.type)
{
// Force to float
switch (parameter.type)
{
case AnimatorControllerParameterType.Bool:
existingParam.defaultFloat = existingParam.defaultBool ? 1.0f : 0.0f;
break;
case AnimatorControllerParameterType.Int:
existingParam.defaultFloat = existingParam.defaultInt;
break;
}
existingParam.type = AnimatorControllerParameterType.Float;
targetController.Parameters = targetController.Parameters.SetItem(name, existingParam);
}
continue;
}
targetController.Parameters = targetController.Parameters.Add(name, parameter);
}
if (merge.deleteAttachedAnimator)
{
var animator = merge.GetComponent<Animator>();
if (animator != null) Object.DestroyImmediate(animator);
}
Object.DestroyImmediate(merge);
}
private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions(
VRCAvatarDescriptor.CustomAnimLayer[] layers
)
private bool IsWriteDefaultsSafeLayer(VirtualLayer virtualLayer)
{
layers = (VRCAvatarDescriptor.CustomAnimLayer[])layers.Clone();
if (virtualLayer.BlendingMode == AnimatorLayerBlendingMode.Additive) return true;
var sm = virtualLayer.StateMachine;
// Ensure types are consistent across layers
Dictionary<string, AnimatorControllerParameterType> types =
new Dictionary<string, AnimatorControllerParameterType>();
// Learn types...
foreach (var session in mergeSessions.Values)
{
session.MergeTypes(types);
}
// And propagate them
foreach (var session in mergeSessions.Values)
{
session.MergeTypes(types);
}
for (int i = 0; i < layers.Length; i++)
{
if (mergeSessions.TryGetValue(layers[i].type, out var session))
{
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.Gesture && layers[i].isDefault)
{
// We need to set the mask field for the gesture layer on initial configuration
layers[i].mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(
AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK)
);
}
layers[i].isDefault = false;
layers[i].animatorController = session.Finish();
}
}
return layers;
}
private void InitSessions(VRCAvatarDescriptor.CustomAnimLayer[] layers)
{
foreach (var layer in layers)
{
var controller = ResolveLayerController(layer);
if (controller == null) controller = new AnimatorController();
defaultControllers_[layer.type] = controller;
writeDefaults_[layer.type] = ProbeWriteDefaults(controller);
if (!layer.isDefault)
{
// For non-default layers, ensure we always clone the controller for the benefit of subsequent
// processing phases
mergeSessions[layer.type] =
new AnimatorCombiner(_context.PluginBuildContext, layer.type.ToString());
mergeSessions[layer.type].BlendableLayer = BlendableLayerFor(layer.type);
mergeSessions[layer.type].AddController("", controller, null);
}
}
}
private VRC_AnimatorLayerControl.BlendableLayer? BlendableLayerFor(VRCAvatarDescriptor.AnimLayerType layerType)
{
if (Enum.TryParse(layerType.ToString(), out VRC_AnimatorLayerControl.BlendableLayer result))
{
return result;
}
else
{
return null;
}
}
private bool? ProbeWriteDefaults(AnimatorController controller)
{
bool hasWDOn = false;
bool hasWDOff = false;
var stateMachineQueue = new Queue<AnimatorStateMachine>();
foreach (var layer in controller.layers)
{
stateMachineQueue.Enqueue(layer.stateMachine);
}
while (stateMachineQueue.Count > 0)
{
var stateMachine = stateMachineQueue.Dequeue();
foreach (var state in stateMachine.states)
{
if (state.state.writeDefaultValues) hasWDOn = true;
else hasWDOff = true;
}
foreach (var child in stateMachine.stateMachines)
{
stateMachineQueue.Enqueue(child.stateMachine);
}
}
if (hasWDOn == hasWDOff) return null;
return hasWDOn;
}
private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer)
{
AnimatorController controller = null;
if (!layer.isDefault && layer.animatorController != null &&
layer.animatorController is AnimatorController c)
{
controller = c;
}
else
{
string name;
switch (layer.type)
{
case VRCAvatarDescriptor.AnimLayerType.Action:
name = "Action";
break;
case VRCAvatarDescriptor.AnimLayerType.Additive:
name = "Idle";
break;
case VRCAvatarDescriptor.AnimLayerType.Base:
name = "Locomotion";
break;
case VRCAvatarDescriptor.AnimLayerType.Gesture:
name = "Hands";
break;
case VRCAvatarDescriptor.AnimLayerType.Sitting:
name = "Sitting";
break;
case VRCAvatarDescriptor.AnimLayerType.FX:
name = "Face";
break;
case VRCAvatarDescriptor.AnimLayerType.TPose:
name = "UtilityTPose";
break;
case VRCAvatarDescriptor.AnimLayerType.IKPose:
name = "UtilityIKPose";
break;
default:
name = null;
break;
}
if (name != null)
{
name = "/vrc_AvatarV3" + name + "Layer.controller";
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
}
}
}
return controller;
if (sm.StateMachines.Count != 0) return false;
return sm.States.Count == 1 && sm.AnyStateTransitions.Count == 0 &&
sm.DefaultState.Transitions.Count == 0;
}
}
}

View File

@ -1,4 +1,4 @@
/*
/*
* MIT License
*
* Copyright (c) 2022 bd_
@ -31,8 +31,8 @@ using VRC.SDK3.Dynamics.PhysBone.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEngine;
using UnityEngine.Animations;
@ -54,13 +54,14 @@ namespace nadena.dev.modular_avatar.core.editor
#endif
private BoneDatabase BoneDatabase = new BoneDatabase();
private PathMappings PathMappings => frameworkContext.Extension<AnimationServicesContext>()
.PathMappings;
private AnimatorServicesContext AnimatorServices => frameworkContext.Extension<AnimatorServicesContext>();
private HashSet<Transform> humanoidBones = new HashSet<Transform>();
private HashSet<Transform> mergedObjects = new HashSet<Transform>();
private readonly HashSet<Transform> prunePBsObjects = new();
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
private HashSet<Transform> transformLookthrough = new HashSet<Transform>();
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
{
this.frameworkContext = context;
@ -117,7 +118,86 @@ namespace nadena.dev.modular_avatar.core.editor
RetainBoneReferences(c as Component);
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
foreach (var smr in avatarGameObject.transform.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
// If the root bone has been offset, or has a different sign for its scale, we need to retain it.
// see https://github.com/bdunderscore/modular-avatar/pull/1355
// (we avoid retaining otherwise to avoid excess bone transforms)
if (smr.rootBone == null || smr.rootBone.parent == null) continue;
var root = smr.rootBone;
var parent = root.parent;
if ((parent.position - root.position).sqrMagnitude > 0.000001f
|| Vector3.Dot(parent.localScale.normalized, root.localScale.normalized) < 0.9999f)
{
BoneDatabase.RetainMergedBone(smr.rootBone);
}
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, AnimatorServices);
ProcessTransformLookthrough();
}
private void ProcessTransformLookthrough()
{
var asc = frameworkContext.Extension<AnimatorServicesContext>();
transformLookthrough.RemoveWhere(t => !t);
var clipsToEdit = transformLookthrough.SelectMany(
xform =>
{
var path = asc.ObjectPathRemapper.GetVirtualPathForObject(xform);
return asc.AnimationIndex.GetClipsForObjectPath(path);
});
Dictionary<string, string> parentCache = new();
foreach (var clip in clipsToEdit)
{
foreach (var binding in clip.GetFloatCurveBindings())
{
if (binding.type == typeof(Transform))
{
var newPath = GetReplacementPath(binding.path);
var newBinding = EditorCurveBinding.FloatCurve(newPath, binding.type, binding.propertyName);
clip.SetFloatCurve(newBinding, clip.GetFloatCurve(binding));
clip.SetFloatCurve(binding, null);
}
}
}
string GetReplacementPath(string bindingPath)
{
if (parentCache.TryGetValue(bindingPath, out var cached))
{
return cached;
}
var obj = asc.ObjectPathRemapper.GetObjectForPath(bindingPath)?.transform;
while (obj != null && transformLookthrough.Contains(obj))
{
obj = obj.parent;
}
string path;
if (obj == null)
{
path = bindingPath;
}
else
{
path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj);
}
parentCache[bindingPath] = path;
return path;
}
}
private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures)
@ -199,7 +279,7 @@ namespace nadena.dev.modular_avatar.core.editor
BuildReport.ReportingObject(config, () =>
{
mergedObjects.Clear();
prunePBsObjects.Clear();
thisPassAdded.Clear();
MergeArmature(config, target);
#if MA_VRCSDK3_AVATARS
@ -276,6 +356,7 @@ namespace nadena.dev.modular_avatar.core.editor
_activeRetargeter.FixupAnimations();
thisPassAdded.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
transformLookthrough.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
}
/**
@ -285,7 +366,8 @@ namespace nadena.dev.modular_avatar.core.editor
private void RecursiveMerge(ModularAvatarMergeArmature config,
GameObject src,
GameObject newParent,
bool zipMerge)
bool zipMerge
)
{
if (src == newParent)
{
@ -295,7 +377,6 @@ namespace nadena.dev.modular_avatar.core.editor
if (zipMerge)
{
mergedObjects.Add(src.transform);
thisPassAdded.Add(src.transform);
}
@ -339,7 +420,7 @@ namespace nadena.dev.modular_avatar.core.editor
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
BoneDatabase.RetainMergedBone(mergedSrcBone.transform);
PathMappings.MarkTransformLookthrough(mergedSrcBone);
transformLookthrough.Add(mergedSrcBone.transform);
thisPassAdded.Add(mergedSrcBone.transform);
}
@ -349,12 +430,28 @@ namespace nadena.dev.modular_avatar.core.editor
src.name = src.name + "$" + Guid.NewGuid();
}
src.GetOrAddComponent<ModularAvatarPBBlocker>();
mergedSrcBone = src;
if (zipMerge)
HashSet<Transform> childPhysBonesBlockedSet = null;
#if MA_VRCSDK3_AVATARS
src.GetOrAddComponent<ModularAvatarPBBlocker>();
if (physBoneByRootBone.TryGetValue(src.transform, out var pb)
&& !NotAffectedByPhysBoneOrSimilarChainsAsTarget(src.transform, newParent.transform))
{
PathMappings.MarkTransformLookthrough(src);
childPhysBonesBlockedSet = new HashSet<Transform>(pb.ignoreTransforms);
}
else if (zipMerge)
{
prunePBsObjects.Add(src.transform);
}
#endif
// If we're zipping, and the current object is not being used for PBs, we can remove it later.
if (zipMerge && childPhysBonesBlockedSet == null)
{
transformLookthrough.Add(src.transform);
BoneDatabase.AddMergedBone(src.transform);
}
@ -366,6 +463,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (zipMerge)
{
var reportedHumanoidBoneError = false;
foreach (Transform child in children)
{
if (child.GetComponent <ModularAvatarMergeArmature>() != null)
@ -385,21 +484,32 @@ namespace nadena.dev.modular_avatar.core.editor
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
var targetObject = newParent.transform.Find(targetObjectName);
if (childPhysBonesBlockedSet != null
&& !childPhysBonesBlockedSet.Contains(child)
&& !child.TryGetComponent<ModularAvatarPBBlocker>(out _))
{
// This object is potentially impacted by the parent's physbones; is it humanoid?
if (!reportedHumanoidBoneError && targetObject != null &&
humanoidBones.Contains(targetObject.transform))
{
// If so, fail the build, as we won't properly apply this to humanoid children.
BuildReport.LogFatal(
"error.merge_armature.physbone_on_humanoid_bone", new string[0], config);
reportedHumanoidBoneError = true;
}
// Don't move this child object
continue;
}
// Zip merge bones if the names match and the outfit side is not affected by its own PhysBone.
// Also zip merge when it seems to have been copied from avatar side by checking the dinstance.
if (targetObject != null)
{
if (NotAffectedByPhysBoneOrSimilarChainsAsTarget(child, targetObject))
{
childNewParent = targetObject.gameObject;
shouldZip = true;
}
else if (humanoidBones.Contains(targetObject))
{
BuildReport.LogFatal(
"error.merge_armature.physbone_on_humanoid_bone", new string[0], config);
}
}
}
RecursiveMerge(config, childGameObject, childNewParent, shouldZip);
@ -450,7 +560,7 @@ namespace nadena.dev.modular_avatar.core.editor
*/
private void PruneDuplicatePhysBones()
{
foreach (var obj in mergedObjects)
foreach (var obj in prunePBsObjects)
{
if (obj.GetComponent<VRCPhysBone>() == null) continue;
var baseObj = FindOriginalParent(obj);

View File

@ -4,12 +4,13 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
#endregion
@ -20,16 +21,32 @@ namespace nadena.dev.modular_avatar.core.editor
internal const string ALWAYS_ONE = "__ModularAvatarInternal/One";
internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree";
private AnimatorController _controller;
private BlendTree _rootBlendTree;
private GameObject _mergeHost;
private AnimatorServicesContext _asc;
private VirtualBlendTree _rootBlendTree;
private HashSet<string> _parameterNames;
[InitializeOnLoadMethod]
private static void Init()
{
ModularAvatarMergeBlendTree.GetMotionBasePathCallback = (mbt, objectBuildContext) =>
{
if (mbt.PathMode == MergeAnimatorPathMode.Absolute) return "";
var buildContext = (ndmf.BuildContext)objectBuildContext;
var root = mbt.RelativePathRoot.Get(buildContext.AvatarRootTransform);
if (root == null) root = mbt.gameObject;
return RuntimeUtil.AvatarRootPath(root);
};
}
protected override void Execute(ndmf.BuildContext context)
{
_asc = context.Extension<AnimatorServicesContext>();
_rootBlendTree = null;
_parameterNames = new HashSet<string>();
_controller = new AnimatorController();
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
foreach (var component in
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
@ -37,12 +54,8 @@ namespace nadena.dev.modular_avatar.core.editor
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
}
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_parameterNames.Count + 1);
if (_mergeHost != null)
{
_parameterNames.Remove(ALWAYS_ONE);
parameters.Add(new AnimatorControllerParameter()
// always add the ALWAYS_ONE parameter
fx.Parameters = fx.Parameters.SetItem(ALWAYS_ONE, new AnimatorControllerParameter()
{
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
@ -51,133 +64,90 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var name in _parameterNames)
{
parameters.Add(new AnimatorControllerParameter()
if (fx.Parameters.ContainsKey(name)) continue;
fx.Parameters = fx.Parameters.SetItem(name, new AnimatorControllerParameter()
{
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0
defaultFloat = 0.0f
});
}
var paramsAnimator = new AnimatorController();
paramsAnimator.parameters = parameters.ToArray();
var paramsComponent = _mergeHost.AddComponent<ModularAvatarMergeAnimator>();
paramsComponent.animator = paramsAnimator;
paramsComponent.layerPriority = Int32.MaxValue;
}
}
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component)
{
BlendTree componentBlendTree = component.BlendTree as BlendTree;
var virtualBlendTree = _asc.ControllerContext.GetVirtualizedMotion(component);
if (componentBlendTree == null)
if (virtualBlendTree == null)
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.NonFatal, "error.merge_blend_tree.missing_tree");
return;
}
string basePath = null;
if (component.PathMode == MergeAnimatorPathMode.Relative)
{
var root = component.RelativePathRoot.Get(context.AvatarRootTransform);
if (root == null) root = component.gameObject;
var rootBlend = GetRootBlendTree();
basePath = RuntimeUtil.AvatarRootPath(root) + "/";
rootBlend.Children = rootBlend.Children.Add(new()
{
Motion = virtualBlendTree,
DirectBlendParameter = ALWAYS_ONE,
Threshold = 1,
CycleOffset = 1,
TimeScale = 1,
});
foreach (var asset in virtualBlendTree.AllReachableNodes())
{
if (asset is VirtualBlendTree bt2)
{
if (!string.IsNullOrEmpty(bt2.BlendParameter) && bt2.BlendType != BlendTreeType.Direct)
{
_parameterNames.Add(bt2.BlendParameter);
}
var bt = new DeepClone(context).DoClone(componentBlendTree, basePath);
var rootBlend = GetRootBlendTree(context);
rootBlend.AddChild(bt);
var children = rootBlend.children;
children[children.Length - 1].directBlendParameter = ALWAYS_ONE;
rootBlend.children = children;
foreach (var asset in bt.ReferencedAssets(includeScene: false))
if (bt2.BlendType != BlendTreeType.Direct && bt2.BlendType != BlendTreeType.Simple1D)
{
if (asset is BlendTree bt2)
if (!string.IsNullOrEmpty(bt2.BlendParameterY))
{
if (!string.IsNullOrEmpty(bt2.blendParameter) && bt2.blendType != BlendTreeType.Direct)
{
_parameterNames.Add(bt2.blendParameter);
}
if (bt2.blendType != BlendTreeType.Direct && bt2.blendType != BlendTreeType.Simple1D)
{
if (!string.IsNullOrEmpty(bt2.blendParameterY))
{
_parameterNames.Add(bt2.blendParameterY);
_parameterNames.Add(bt2.BlendParameterY);
}
}
if (bt2.blendType == BlendTreeType.Direct)
if (bt2.BlendType == BlendTreeType.Direct)
{
foreach (var childMotion in bt2.children)
foreach (var childMotion in bt2.Children)
{
if (!string.IsNullOrEmpty(childMotion.directBlendParameter))
if (!string.IsNullOrEmpty(childMotion.DirectBlendParameter))
{
_parameterNames.Add(childMotion.directBlendParameter);
}
_parameterNames.Add(childMotion.DirectBlendParameter);
}
}
}
}
}
private BlendTree GetRootBlendTree(ndmf.BuildContext context)
Object.DestroyImmediate(component);
}
private VirtualBlendTree GetRootBlendTree()
{
if (_rootBlendTree != null) return _rootBlendTree;
var newController = new AnimatorController();
var newStateMachine = new AnimatorStateMachine();
var newState = new AnimatorState();
_rootBlendTree = new BlendTree();
_controller = newController;
newController.layers = new[]
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
var controller = fx.AddLayer(new LayerPriority(int.MinValue), BlendTreeLayerName);
var stateMachine = controller.StateMachine;
if (fx == null)
{
new AnimatorControllerLayer
{
blendingMode = AnimatorLayerBlendingMode.Override,
defaultWeight = 1,
name = BlendTreeLayerName,
stateMachine = newStateMachine
throw new Exception("FX layer not found");
}
};
newStateMachine.name = "ModularAvatarMergeBlendTree";
newStateMachine.states = new[]
{
new ChildAnimatorState
{
state = newState,
position = Vector3.zero
}
};
newStateMachine.defaultState = newState;
_rootBlendTree = VirtualBlendTree.Create("Root");
var state = stateMachine.AddState("State", _rootBlendTree);
stateMachine.DefaultState = state;
state.WriteDefaultValues = true;
newState.writeDefaultValues = true;
newState.motion = _rootBlendTree;
_rootBlendTree.blendType = BlendTreeType.Direct;
_rootBlendTree.blendParameter = ALWAYS_ONE;
var mergeObject = new GameObject("ModularAvatarMergeBlendTree");
var merger = mergeObject.AddComponent<ModularAvatarMergeAnimator>();
merger.animator = newController;
merger.pathMode = MergeAnimatorPathMode.Absolute;
merger.matchAvatarWriteDefaults = false;
merger.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
merger.deleteAttachedAnimator = false;
merger.layerPriority = Int32.MinValue;
mergeObject.transform.SetParent(context.AvatarRootTransform, false);
mergeObject.transform.SetSiblingIndex(0);
_mergeHost = mergeObject;
_rootBlendTree.BlendType = BlendTreeType.Direct;
_rootBlendTree.BlendParameter = ALWAYS_ONE;
return _rootBlendTree;
}

Some files were not shown because too many files have changed in this diff Show More