Compare commits

..

218 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
397 changed files with 27009 additions and 7349 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.8"
"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@aa1121a867565ea71b60f445f441544df0c7b0b9
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,415 +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 UnityEngine.Profiling;
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()
{
Profiler.BeginSample("AnimationDatabase.Commit");
foreach (var clip in _clips)
{
if (clip.IsProxyAnimation) clip.CurrentClip = clip.OriginalClip;
}
Profiler.BeginSample("UpdateClipProperties");
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();
}
}
}
Profiler.EndSample();
Profiler.BeginSample("ClipCommitActions");
foreach (var action in _clipCommitActions)
{
action();
}
Profiler.EndSample();
Profiler.EndSample();
}
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;
MaybeSaveClip(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;
}
MaybeSaveClip(curClip);
}
if (dirty)
{
tree.children = children;
EditorUtility.SetDirty(tree);
}
});
return treeHolder;
}
private void MaybeSaveClip(Motion curClip)
{
Profiler.BeginSample("MaybeSaveClip");
if (curClip != null && !EditorUtility.IsPersistent(curClip) && EditorUtility.IsPersistent(_context.AssetContainer) && _context.AssetContainer != null)
{
try
{
AssetDatabase.AddObjectToAsset(curClip, _context.AssetContainer);
}
catch (Exception e)
{
Debug.LogException(e);
throw;
}
}
Profiler.EndSample();
}
}
}

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,222 +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());
}
var clone = merger.Finish();
ObjectRegistry.RegisterReplacedObject(controller, clone);
return clone;
}
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,623 +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()
{
#if MA_VRCSDK3_AVATARS
// We can't safely correct the layer index of a VRCAnimatorLayerControl without knowing if it refers to
// _this_ animator controller, so just skip this. We'll do the empty layer pruning later when we merge
// everything together.
if (BlendableLayer == null) return;
#endif
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 = _deepClone.DoClone(layer.avatarMask, basePath, _cloneMap),
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)
{
var newMotion = _deepClone.DoClone(overrideMotion, basePath, _cloneMap);
newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], newMotion);
}
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,294 +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>();
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 _:
case AvatarMask _:
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);
}
if (_isSaved && !EditorUtility.IsPersistent(obj))
{
AssetDatabase.AddObjectToAsset(obj, _combined);
}
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:
{
if (prop.objectReferenceValue != null && prop.objectReferenceValue != obj)
{
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;
}
// internal for testing
internal static AvatarMask CloneAvatarMask(AvatarMask mask, string basePath)
{
if (basePath.EndsWith("/")) basePath = basePath.Substring(0, basePath.Length - 1);
var newMask = new AvatarMask();
// Transfer first the humanoid mask data
EditorUtility.CopySerialized(mask, newMask);
var srcSo = new SerializedObject(mask);
var dstSo = new SerializedObject(newMask);
var srcElements = srcSo.FindProperty("m_Elements");
if (basePath == "" || srcElements.arraySize == 0) return newMask; // no changes required
// We now need to prefix the elements of basePath (with weight zero)
var newElements = new List<string>();
var accum = "";
foreach (var element in basePath.Split("/"))
{
if (accum != "") accum += "/";
accum += element;
newElements.Add(accum);
}
var dstElements = dstSo.FindProperty("m_Elements");
// We'll need to create new array elements by using DuplicateCommand. We'll then rewrite the whole
// list to keep things in traversal order.
for (var i = 0; i < newElements.Count; i++) dstElements.GetArrayElementAtIndex(0).DuplicateCommand();
var totalElements = srcElements.arraySize + newElements.Count;
for (var i = 0; i < totalElements; i++)
{
var dstElem = dstElements.GetArrayElementAtIndex(i);
var dstPath = dstElem.FindPropertyRelative("m_Path");
var dstWeight = dstElem.FindPropertyRelative("m_Weight");
var srcIndex = i - newElements.Count;
if (srcIndex < 0)
{
dstPath.stringValue = newElements[i];
dstWeight.floatValue = 0;
}
else
{
var srcElem = srcElements.GetArrayElementAtIndex(srcIndex);
dstPath.stringValue = basePath + "/" + srcElem.FindPropertyRelative("m_Path").stringValue;
dstWeight.floatValue = srcElem.FindPropertyRelative("m_Weight").floatValue;
}
}
dstSo.ApplyModifiedPropertiesWithoutUndo();
return newMask;
}
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
{
if (o is AvatarMask mask)
{
return CloneAvatarMask(mask, 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);
fx.layers = fx.layers.Append(new AnimatorControllerLayer
{
name = "DelayDisable",
stateMachine = asm,
defaultWeight = 1,
blendingMode = AnimatorLayerBlendingMode.Override
}).ToArray();
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
{
p.defaultFloat = obj.activeSelf ? 1 : 0;
controller.Parameters = controller.Parameters.SetItem(prop, p);
}
}
}
}
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);
// 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,
timeScale = 1,
threshold = 1
}
};
return new ChildMotion
{
motion = motion,
directBlendParameter = prop,
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,413 +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 UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Profiling;
#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;
}
private void ApplyMappingsToAvatarMask(AvatarMask mask)
{
if (mask == null) return;
var maskSo = new SerializedObject(mask);
var seenTransforms = new Dictionary<string, float>();
var transformOrder = new List<string>();
var m_Elements = maskSo.FindProperty("m_Elements");
var elementCount = m_Elements.arraySize;
for (var i = 0; i < elementCount; i++)
{
var element = m_Elements.GetArrayElementAtIndex(i);
var path = element.FindPropertyRelative("m_Path").stringValue;
var weight = element.FindPropertyRelative("m_Weight").floatValue;
path = MapPath(path);
// ensure all parent elements are present
EnsureParentsPresent(path);
if (!seenTransforms.ContainsKey(path)) transformOrder.Add(path);
seenTransforms[path] = weight;
}
transformOrder.Sort();
m_Elements.arraySize = transformOrder.Count;
for (var i = 0; i < transformOrder.Count; i++)
{
var element = m_Elements.GetArrayElementAtIndex(i);
var path = transformOrder[i];
element.FindPropertyRelative("m_Path").stringValue = path;
element.FindPropertyRelative("m_Weight").floatValue = seenTransforms[path];
}
maskSo.ApplyModifiedPropertiesWithoutUndo();
void EnsureParentsPresent(string path)
{
var nextSlash = -1;
while ((nextSlash = path.IndexOf('/', nextSlash + 1)) != -1)
{
var parentPath = path.Substring(0, nextSlash);
if (!seenTransforms.ContainsKey(parentPath))
{
seenTransforms[parentPath] = 0;
transformOrder.Add(parentPath);
}
}
}
}
internal void OnDeactivate(BuildContext context)
{
Profiler.BeginSample("PathMappings.OnDeactivate");
Dictionary<AnimationClip, AnimationClip> clipCache = new Dictionary<AnimationClip, AnimationClip>();
Profiler.BeginSample("ApplyMappingsToClip");
_animationDatabase.ForeachClip(holder =>
{
if (holder.CurrentClip is AnimationClip clip)
{
holder.CurrentClip = ApplyMappingsToClip(clip, clipCache);
}
});
Profiler.EndSample();
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
Profiler.BeginSample("MapPlayAudio");
_animationDatabase.ForeachPlayAudio(playAudio =>
{
if (playAudio == null) return;
playAudio.SourcePath = MapPath(playAudio.SourcePath, true);
});
Profiler.EndSample();
#endif
Profiler.BeginSample("InvokeIOnCommitObjectRenamesCallbacks");
foreach (var listener in context.AvatarRootObject.GetComponentsInChildren<IOnCommitObjectRenames>())
{
listener.OnCommitObjectRenames(context, this);
}
Profiler.EndSample();
var layers = context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers);
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
foreach (var layer in layers)
{
ApplyMappingsToAvatarMask(layer.mask);
if (layer.animatorController is AnimatorController ac)
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
// AnimatorController so we can safely modify the controller in-place.
foreach (var acLayer in ac.layers)
ApplyMappingsToAvatarMask(acLayer.avatarMask);
}
Profiler.EndSample();
Profiler.EndSample();
}
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))
var asc = context.Extension<AnimatorServicesContext>();
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
if (layer.isDefault || layer.animatorController == null) continue;
// We should have converted anything that's not an AnimationController by now
var controller = layer.animatorController as AnimatorController;
if (controller == null || !context.IsTemporaryAsset(controller))
var parameters = controller.Parameters;
foreach (var (name, parameter) in parameters)
{
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
}
if (!values.TryGetValue(name, out var defaultValue)) continue;
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
if (!values.TryGetValue(parameters[i].name, out var defaultValue)) continue;
switch (parameters[i].type)
switch (parameter.type)
{
case AnimatorControllerParameterType.Bool:
parameters[i].defaultBool = defaultValue != 0.0f;
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 bool Equals(SummaryBinding other)
{
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(BuildContext context)
public void OnPreprocessAvatar()
{
_context = context;
var avatarGameObject = context.AvatarRootObject;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = context.AvatarDescriptor;
var avatarGameObject = _context.AvatarRootObject;
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
_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);
@ -161,4 +180,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
#endif
#endif

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

@ -231,6 +231,9 @@ namespace nadena.dev.modular_avatar.core.editor
};
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)
{
@ -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,21 +368,65 @@ 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);
if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out var bodyBones))
List<HumanBodyBones> bodyBones = null;
var isMapped = false;
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;
}
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
if (!isMapped)
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
foreach (var bodyBone in bodyBones)
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
break;
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))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
isMapped = true;
break;
}
}
}
@ -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

@ -8,20 +8,28 @@ namespace nadena.dev.modular_avatar.core.editor
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (CustomGUI(position, property, label)) return;
var xButtonSize = EditorStyles.miniButtonRight.CalcSize(new GUIContent("x"));
var xButtonRect = new Rect(position.xMax - xButtonSize.x, position.y, xButtonSize.x, position.height);
position = new Rect(position.x, position.y, position.width - xButtonSize.x, position.height);
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
position = EditorGUI.PrefixLabel(position, label);
using (var scope = new ZeroIndentScope())
{
EditorGUI.LabelField(position,
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
label = EditorGUI.BeginProperty(position, label, property);
try
{
if (CustomGUI(position, property, label)) return;
var xButtonSize = EditorStyles.miniButtonRight.CalcSize(new GUIContent("x"));
var xButtonRect = new Rect(position.xMax - xButtonSize.x, position.y, xButtonSize.x, position.height);
position = new Rect(position.x, position.y, position.width - xButtonSize.x, position.height);
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
position = EditorGUI.PrefixLabel(position, label);
using (var scope = new ZeroIndentScope())
{
EditorGUI.LabelField(position,
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
}
}
finally
{
EditorGUI.EndProperty();
}
}

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);
@ -47,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,6 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
@ -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

@ -62,3 +62,13 @@
#f-material {
flex-grow: 1;
}
.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
@ -148,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

@ -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

@ -59,6 +59,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal class MenuItemCoreGUI
{
private const string ImpliesRichText = "<";
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
private readonly GameObject _parameterReference;
private readonly Action _redraw;
@ -92,11 +94,14 @@ namespace nadena.dev.modular_avatar.core.editor
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)
{
@ -144,6 +149,8 @@ namespace nadena.dev.modular_avatar.core.editor
_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);
}
@ -169,11 +176,16 @@ namespace nadena.dev.modular_avatar.core.editor
}
Dictionary<string, ProvidedParameter> rootParameters = new();
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
.Where(p => p.Namespace == ParameterNamespace.Animator)
)
rootParameters[param.EffectiveName] = param;
{
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
{
rootParameters[param.EffectiveName] = param;
}
}
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
foreach (var remap in remaps)
@ -255,11 +267,69 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
if (EditorGUI.EndChangeCheck())
EditorGUILayout.BeginHorizontal();
if (_prop_label == null)
{
_name.serializedObject.ApplyModifiedProperties();
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"));
@ -269,7 +339,7 @@ namespace nadena.dev.modular_avatar.core.editor
_parameterGUI.DoGUI(true);
ShowInnateParameterGUI();
EditorGUILayout.EndVertical();
if (_texture != null)
@ -301,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)
{
@ -517,7 +586,12 @@ namespace nadena.dev.modular_avatar.core.editor
// But, we do want to see if _any_ are default.
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
var mixedIsDefault = multipleSelections && anyIsDefault;
using (new EditorGUI.DisabledScope(multipleSelections || isDefaultByKnownParam != null))
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,
@ -646,6 +720,9 @@ namespace nadena.dev.modular_avatar.core.editor
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>();
@ -653,7 +730,6 @@ namespace nadena.dev.modular_avatar.core.editor
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
myParameterName = myReplacement.ParameterName;
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
var siblings = new List<ModularAvatarMenuItem>();
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))

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,46 +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 avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var selections = Selection.objects.OfType<GameObject>();
if (selections.Count() == 0) return;
bool createInstaller = true;
Transform parent = avatarRoot;
try
foreach (var selected in selections)
{
var selectedMenuItem = selected.GetComponent<ModularAvatarMenuItem>();
if (selectedMenuItem?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& selectedMenuItem.MenuSource == SubmenuSource.Children
)
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var parent = avatarRoot.gameObject;
var createInstaller = true;
if (TryGetChildrenSourceSubmenu(selected, out var _))
{
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;
}
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("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.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,
};
@ -62,4 +214,5 @@ namespace nadena.dev.modular_avatar.core.editor
Undo.RegisterCreatedObjectUndo(toggle, "Create Toggle");
}
}
}
}
#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_matchAvatarWriteDefaults,
G("merge_animator.match_avatar_write_defaults"));
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();
@ -279,4 +308,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}

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,6 @@
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;

View File

@ -35,14 +35,12 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
ROSimulatorButton.BindRefObject(root, target);
var listView = root.Q<ListView>("Shapes");
_dragAndDropManipulator = new DragAndDropManipulator(listView)
{
TargetComponent = target as ModularAvatarObjectToggle
};
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
return root;
}
@ -52,91 +50,25 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
}
private class DragAndDropManipulator : PointerManipulator
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
{
public ModularAvatarObjectToggle TargetComponent;
private GameObject[] _nowDragging = Array.Empty<GameObject>();
private Transform _avatarRoot;
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
: base(targetElement, targetComponent) { }
private readonly VisualElement _parentElem;
protected override bool AllowKnownObjects => false;
public DragAndDropManipulator(VisualElement target)
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
this.target = target;
_parentElem = target.parent;
}
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
}
private void OnDragEnter(DragEnterEvent evt)
{
if (TargetComponent == null) return;
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
if (_avatarRoot == null) return;
_nowDragging = DragAndDrop.objectReferences.OfType<GameObject>()
.Where(o => RuntimeUtil.FindAvatarTransformInParents(o.transform) == _avatarRoot)
.ToArray();
if (_nowDragging.Length > 0)
foreach (var reference in references)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
_parentElem.AddToClassList("drop-area--drag-active");
}
}
private void OnDragUpdate(DragUpdatedEvent _)
{
if (_nowDragging.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
}
private void OnDragLeave(DragLeaveEvent evt)
{
_nowDragging = Array.Empty<GameObject>();
_parentElem.RemoveFromClassList("drop-area--drag-active");
}
private void OnDragPerform(DragPerformEvent evt)
{
if (_nowDragging.Length > 0 && TargetComponent != null && _avatarRoot != null)
{
var knownObjs = TargetComponent.Objects.Select(o => o.Object.Get(TargetComponent)).ToHashSet();
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
foreach (var obj in _nowDragging)
{
if (knownObjs.Contains(obj)) continue;
var aor = new AvatarObjectReference();
aor.Set(obj);
var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
_nowDragging = Array.Empty<GameObject>();
_parentElem.RemoveFromClassList("drop-area--drag-active");
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}

View File

@ -39,14 +39,24 @@
margin: 0;
}
#f-active {
justify-content: center;
}
#f-object {
flex-grow: 1;
}
.drop-area--drag-active > ListView ScrollView {
background-color: rgba(0, 255, 255, 0.1);
#f-active {
display: none;
}
#f-active-dropdown {
width: 60px;
}
.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

@ -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="horizontal">
<ed:PropertyField name="f-active" binding-path="Active" label=""/>
<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");
@ -128,10 +162,69 @@ namespace nadena.dev.modular_avatar.core.editor
EditorApplication.delayCall += DetectParameters;
}
};
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>();
@ -162,4 +255,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif
#endif

View File

@ -20,7 +20,8 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly DropdownField _boolField;
private ParameterSyncType _syncType;
private bool _hasInitialBinding;
public DefaultValueField()
{
// Hidden binding elements
@ -57,28 +58,39 @@ namespace nadena.dev.modular_avatar.core.editor
{
_numberField.style.display = DisplayStyle.Flex;
_boolField.style.display = DisplayStyle.None;
OnUpdateNumberValue(_numberField.value);
OnUpdateNumberValue(_numberField.value, true);
}
else
{
_numberField.style.display = DisplayStyle.None;
_boolField.style.display = DisplayStyle.Flex;
OnUpdateBoolValue(_boolField.value);
OnUpdateBoolValue(_boolField.value, true);
}
}
private void OnUpdateNumberValue(string value)
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))
{
_defaultValueField.value = 0;
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))
{
_defaultValueField.value = _syncType switch
theValue = _defaultValueField.value = _syncType switch
{
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
@ -88,11 +100,15 @@ namespace nadena.dev.modular_avatar.core.editor
_hasExplicitDefaultValueField.value = true;
}
UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value);
UpdateVisibleField(theValue, _hasExplicitDefaultValueField.value);
}
private void OnUpdateBoolValue(string 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;
@ -101,6 +117,8 @@ namespace nadena.dev.modular_avatar.core.editor
private void UpdateVisibleField(float value, bool hasExplicitValue)
{
_hasInitialBinding = true;
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
{
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));

View File

@ -2,6 +2,7 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
@ -81,6 +82,16 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
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;
}

View File

@ -1,5 +1,6 @@
#ListViewContainer {
margin-top: 4px;
max-height: 500px;
}
.horizontal {

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;
}

View File

@ -19,6 +19,7 @@ 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()
@ -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

@ -68,3 +68,13 @@
.change-type-delete #f-value-delete {
display: flex;
}
.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

@ -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",
@ -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",
@ -280,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

@ -47,6 +47,7 @@
"merge_parameter.ui.add_button": "追加",
"merge_parameter.ui.details": "パラメーターの詳細設定",
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定",
"merge_parameter.ui.importFromAsset": "アセットからインポートする",
"merge_armature.merge_target": "統合先",
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
"merge_armature.prefix": "接頭辞",
@ -73,6 +74,9 @@
"merge_animator.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
"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": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
@ -82,12 +86,13 @@
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
"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_blend_tree.motion": "結合するモーション(またはブレンドツリー)",
"merge_blend_tree.path_mode": "パスモード",
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
"merge_blend_tree.relative_path_root": "相対的パスのルート",
@ -145,6 +150,11 @@
"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がありますが、メッシュがありません。",
@ -272,5 +282,18 @@
"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": "상대적 경로",

View File

@ -1,107 +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.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": "作为子级,保持原有位置和旋转",
@ -110,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": "显示菜单内容",
@ -177,62 +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.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,6 +100,7 @@
"worldfixed.quest": "此元件未生效,因為它與 Android 環境不相容。",
"worldfixed.normal": "當前物件將會固定於世界,除非你使用約束將它綁在 Avatar 內。",
"fpvisible.normal": "當前物件將在第一人稱視角中可見。",
"fpvisible.NotUnderHead": "此元件在未放置於 Head 骨骼下時不會生效。\n如果是透過 Bone Proxy 等方式將其放於 Head 骨骼下,則可忽略此警告。",
"fpvisible.quest": "此元件未生效,因為它與 Android 環境不相容。",
"fpvisible.InPhysBoneChain": "當前物件由 Physics Bone 控制,可能無法在第一人稱視角中可見;請指定 Physics Bone 鏈的起點。",
"blendshape.mesh": "網格",
@ -148,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 +192,7 @@
"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,6 +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 骨骼,已嘗試搜尋含有以下名稱的骨骼物件(不區分大小寫):",
"move_independently.group-header": "要一起移動的物件",
"scale_adjuster.scale": "調整比例",
"scale_adjuster.adjust_children": "調整子級的位置",
@ -251,7 +258,7 @@
"reactive_object.inverse": "反轉條件",
"reactive_object.material-setter.set-to": "將材質設定為:",
"menuitem.misc.add_toggle": "新增開關",
"ro_sim.open_debugger_button": "開啟響應除錯工具",
"ro_sim.open_debugger_button": "開啟 Reaction 除錯工具",
"ro_sim.window.title": "MA 響應除錯工具",
"ro_sim.header.inspecting": "檢視物件",
"ro_sim.header.clear_overrides": "清除所有覆寫",
@ -272,5 +279,17 @@
"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": "此平台進行建置時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

@ -11,6 +11,7 @@ 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
{
@ -41,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;
@ -76,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
)
@ -128,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);
}
@ -229,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>();
@ -406,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,
@ -431,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,48 +27,41 @@
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;
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 targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
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
= new Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>>();
@ -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,231 +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>());
}
else if (replacements == 1)
{
// 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.
}
if (defaultControllers_.TryGetValue(layerType, out var defaultController) &&
defaultController.layers.Length > 0)
{
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
}
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());
foreach (var component in afterOriginal)
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;
}
if (!merge.matchAvatarWriteDefaults)
{
initialWriteDefaults = null;
}
var vac = context.PluginBuildContext.Extension<VirtualControllerContext>();
if (!vac.Controllers.TryGetValue(merge, out var clonedController)) return;
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
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;
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
else
foreach (var l in clonedController.Layers)
{
basePath = "";
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);
}
var writeDefaults = merge.matchAvatarWriteDefaults
? writeDefaults_.GetValueOrDefault(merge.layerType)
: null;
var controller = _context.ConvertAnimatorController(merge.animator);
session.AddController(basePath, controller, writeDefaults);
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;
}
}
internal static bool? ProbeWriteDefaults(AnimatorController controller)
{
if (controller == null) return null;
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,12 +54,13 @@ 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)
{
@ -116,8 +117,87 @@ namespace nadena.dev.modular_avatar.core.editor
{
RetainBoneReferences(c as Component);
}
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)
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
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,10 +377,9 @@ namespace nadena.dev.modular_avatar.core.editor
if (zipMerge)
{
mergedObjects.Add(src.transform);
thisPassAdded.Add(src.transform);
}
bool retain = HasAdditionalComponents(src) || !zipMerge;
zipMerge = zipMerge && src.GetComponent<IConstraint>() == null;
@ -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,20 +484,31 @@ 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);
}
childNewParent = targetObject.gameObject;
shouldZip = true;
}
}
@ -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,165 +21,134 @@ 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))
{
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
}
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_parameterNames.Count + 1);
if (_mergeHost != null)
// always add the ALWAYS_ONE parameter
fx.Parameters = fx.Parameters.SetItem(ALWAYS_ONE, new AnimatorControllerParameter()
{
_parameterNames.Remove(ALWAYS_ONE);
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1
});
parameters.Add(new AnimatorControllerParameter()
{
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1
});
foreach (var name in _parameterNames)
{
if (fx.Parameters.ContainsKey(name)) continue;
foreach (var name in _parameterNames)
fx.Parameters = fx.Parameters.SetItem(name, new AnimatorControllerParameter()
{
parameters.Add(new AnimatorControllerParameter()
{
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0
});
}
var paramsAnimator = new AnimatorController();
paramsAnimator.parameters = parameters.ToArray();
var paramsComponent = _mergeHost.AddComponent<ModularAvatarMergeAnimator>();
paramsComponent.animator = paramsAnimator;
paramsComponent.layerPriority = Int32.MaxValue;
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0.0f
});
}
}
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component)
{
BlendTree componentBlendTree = component.BlendTree as BlendTree;
if (componentBlendTree == null)
var virtualBlendTree = _asc.ControllerContext.GetVirtualizedMotion(component);
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 rootBlend = GetRootBlendTree();
rootBlend.Children = rootBlend.Children.Add(new()
{
var root = component.RelativePathRoot.Get(context.AvatarRootTransform);
if (root == null) root = component.gameObject;
basePath = RuntimeUtil.AvatarRootPath(root) + "/";
}
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;
Motion = virtualBlendTree,
DirectBlendParameter = ALWAYS_ONE,
Threshold = 1,
CycleOffset = 1,
TimeScale = 1,
});
foreach (var asset in bt.ReferencedAssets(includeScene: false))
foreach (var asset in virtualBlendTree.AllReachableNodes())
{
if (asset is BlendTree bt2)
if (asset is VirtualBlendTree bt2)
{
if (!string.IsNullOrEmpty(bt2.blendParameter) && bt2.blendType != BlendTreeType.Direct)
if (!string.IsNullOrEmpty(bt2.BlendParameter) && bt2.BlendType != BlendTreeType.Direct)
{
_parameterNames.Add(bt2.blendParameter);
_parameterNames.Add(bt2.BlendParameter);
}
if (bt2.blendType != BlendTreeType.Direct && bt2.blendType != BlendTreeType.Simple1D)
if (bt2.BlendType != BlendTreeType.Direct && bt2.BlendType != BlendTreeType.Simple1D)
{
if (!string.IsNullOrEmpty(bt2.blendParameterY))
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);
}
}
}
}
}
Object.DestroyImmediate(component);
}
private BlendTree GetRootBlendTree(ndmf.BuildContext context)
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
}
};
newStateMachine.name = "ModularAvatarMergeBlendTree";
newStateMachine.states = new[]
{
new ChildAnimatorState
{
state = newState,
position = Vector3.zero
}
};
newStateMachine.defaultState = newState;
throw new Exception("FX layer not found");
}
newState.writeDefaultValues = true;
newState.motion = _rootBlendTree;
_rootBlendTree.blendType = BlendTreeType.Direct;
_rootBlendTree.blendParameter = ALWAYS_ONE;
_rootBlendTree = VirtualBlendTree.Create("Root");
var state = stateMachine.AddState("State", _rootBlendTree);
stateMachine.DefaultState = state;
state.WriteDefaultValues = true;
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;
_rootBlendTree.BlendType = BlendTreeType.Direct;
_rootBlendTree.BlendParameter = ALWAYS_ONE;
mergeObject.transform.SetParent(context.AvatarRootTransform, false);
mergeObject.transform.SetSiblingIndex(0);
_mergeHost = mergeObject;
return _rootBlendTree;
}
}

View File

@ -27,6 +27,7 @@ using System.Linq;
using JetBrains.Annotations;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
@ -84,13 +85,15 @@ namespace nadena.dev.modular_avatar.core.editor
internal class RetargetMeshes
{
private BoneDatabase _boneDatabase;
private PathMappings _pathTracker;
private AnimationIndex _animationIndex;
private ObjectPathRemapper _pathRemapper;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
PathMappings pathMappings)
AnimatorServicesContext pathMappings)
{
this._boneDatabase = boneDatabase;
this._pathTracker = pathMappings;
this._animationIndex = pathMappings.AnimationIndex;
this._pathRemapper = pathMappings.ObjectPathRemapper;
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
@ -153,7 +156,8 @@ namespace nadena.dev.modular_avatar.core.editor
child.SetParent(destBone, true);
}
_pathTracker.MarkRemoved(sourceBone.gameObject);
// Remap any animation clips that reference this bone into its parent
_pathRemapper.ReplaceObject(sourceBone.gameObject, sourceBone.transform.parent.gameObject);
UnityEngine.Object.DestroyImmediate(sourceBone.gameObject);
}
}

View File

@ -1,5 +1,9 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -50,12 +54,12 @@ namespace nadena.dev.modular_avatar.core.editor
or ModularAvatarMeshSettings.InheritMode.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit), _):
throw new System.InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
throw new InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
case (_, not (ModularAvatarMeshSettings.InheritMode.Set
or ModularAvatarMeshSettings.InheritMode.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit)):
throw new System.ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
throw new ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
// If current value is came from Set or DontSet, it should not be changed
case (ModularAvatarMeshSettings.InheritMode.Set, _):
@ -144,9 +148,57 @@ namespace nadena.dev.modular_avatar.core.editor
if (newMesh) context.SaveAsset(newMesh);
}
smr.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds;
var settingsRootBone = settings.RootBone;
settingsRootBone = settingsRootBone == null ? smr.transform : settingsRootBone;
var smrRootBone = smr.rootBone;
smrRootBone = smrRootBone == null ? smr.transform : smrRootBone;
if (IsInverted(smrRootBone) != IsInverted(settingsRootBone))
{
smr.rootBone = GetInvertedRootBone(settingsRootBone);
var bounds = settings.Bounds;
var center = bounds.center;
center.x *= -1;
bounds.center = center;
smr.localBounds = bounds;
}
else
{
smr.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds;
}
}
}
private bool IsInverted(Transform bone)
{
var inverseCount = 0;
var scale = bone.lossyScale;
if (scale.x < 0) inverseCount += 1;
if (scale.y < 0) inverseCount += 1;
if (scale.z < 0) inverseCount += 1;
return (inverseCount % 2) != 0;
}
private Dictionary<Transform, Transform> invertedRootBoneCache = new();
private Transform GetInvertedRootBone(Transform rootBone)
{
if (invertedRootBoneCache.TryGetValue(rootBone, out var cache)) { return cache; }
var invertedRootBone = new GameObject($"{rootBone.gameObject.name}-InvertedRootBone");
EditorUtility.CopySerialized(rootBone, invertedRootBone.transform);
invertedRootBone.transform.parent = rootBone;
var transform = invertedRootBone.transform;
var scale = transform.localScale;
scale.x *= -1;
transform.localScale = scale;
invertedRootBoneCache[rootBone] = transform;
return transform;
}
}
}
}

3
Editor/MiscPreview.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea61a438a5d54a289c6abbb1e05c56da
timeCreated: 1733085642

View File

@ -0,0 +1,122 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.ndmf.preview;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal class RemoveVertexColorPreview : IRenderFilter
{
private static string ToPathString(ComputeContext ctx, Transform t)
{
return string.Join("/", ctx.ObservePath(t).Select(t2 => t2.gameObject.name).Reverse());
}
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
{
var roots = context.GetAvatarRoots()
.Where(r => context.ActiveInHierarchy(r) is true);
var removers = roots
.SelectMany(r => context.GetComponentsInChildren<ModularAvatarRemoveVertexColor>(r, true))
.Select(rvc => (ToPathString(context, rvc.transform),
context.Observe(rvc, r => r.Mode) == ModularAvatarRemoveVertexColor.RemoveMode.Remove))
.OrderBy(pair => pair.Item1)
.ToList();
var targets = roots.SelectMany(
r => context.GetComponentsInChildren<SkinnedMeshRenderer>(r, true)
.Concat(
context.GetComponentsInChildren<MeshFilter>(r, true)
.SelectMany(mf => context.GetComponents<Renderer>(mf.gameObject))
)
);
targets = targets.Where(target =>
{
var stringPath = ToPathString(context, target.transform);
var index = removers.BinarySearch((stringPath, true));
if (index >= 0)
{
// There is a component on this mesh
return true;
}
var priorIndex = ~index - 1;
if (priorIndex < 0) return false; // no match
var (maybeParent, mode) = removers[priorIndex];
if (!stringPath.StartsWith(maybeParent)) return false; // no parent matched
return mode;
});
return targets.Select(RenderGroup.For).ToImmutableList();
}
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context)
{
Dictionary<Mesh, Mesh> conversionMap = new();
foreach (var (_, proxy) in proxyPairs)
{
Component c = proxy;
if (!(c is SkinnedMeshRenderer))
{
c = context.GetComponent<MeshFilter>(proxy.gameObject);
}
if (c == null) continue;
RemoveVertexColorPass.ForceRemove(_ => false, c, conversionMap);
}
return Task.FromResult<IRenderFilterNode>(new Node(conversionMap.Values.FirstOrDefault()));
}
private class Node : IRenderFilterNode
{
private readonly Mesh? _theMesh;
public Node(Mesh? theMesh)
{
_theMesh = theMesh;
}
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context,
RenderAspects updatedAspects)
{
if (updatedAspects.HasFlag(RenderAspects.Mesh)) return Task.FromResult<IRenderFilterNode>(null!);
if (_theMesh == null) return Task.FromResult<IRenderFilterNode>(null!);
return Task.FromResult<IRenderFilterNode>(this);
}
public RenderAspects WhatChanged => RenderAspects.Mesh;
public void Dispose()
{
if (_theMesh != null) Object.DestroyImmediate(_theMesh);
}
public void OnFrame(Renderer original, Renderer proxy)
{
if (_theMesh == null) return;
switch (proxy)
{
case SkinnedMeshRenderer smr: smr.sharedMesh = _theMesh; break;
default:
{
var mf = proxy.GetComponent<MeshFilter>();
if (mf != null) mf.sharedMesh = _theMesh;
break;
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b05d5c04f86b4924bf8acdd135448463
timeCreated: 1733085648

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