Compare commits

..

130 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
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
298 changed files with 19917 additions and 4942 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.5.6"
"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.6"
"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@01570a1f39cb168c169c802c3bceb9e93fb10974
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@992cc4e96422fb8ddf077281678373fe41e7736c
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

View File

@ -110,13 +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
https://vrchat.github.io/packages/index.json?download
- 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,430 +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;
var baseClip = ObjectRegistry.GetReference(value)?.Object as AnimationClip;
IsProxyAnimation = false;
if (value != null && Util.IsProxyAnimation(value))
{
IsProxyAnimation = true;
}
else if (baseClip != null && Util.IsProxyAnimation(baseClip))
{
// RenameParametersPass replaces proxy clips outside of the purview of the animation database,
// so trace this using ObjectRegistry and correct the reference.
IsProxyAnimation = true;
_originalClip = baseClip;
}
}
}
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;
if (!avatarDescriptor) return;
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,125 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Linq;
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
{
/// <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)
{
#if MA_VRCSDK3_AVATARS
if (!_context.AvatarDescriptor) return;
var fx = (AnimatorController)
_context.AvatarDescriptor.baseAnimationLayers
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
.animatorController;
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
#endif
}
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,224 +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
if (!context.AvatarDescriptor) return;
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,625 +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)
{
if (layerStateMachine == null) return null;
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

@ -2,6 +2,7 @@
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;
@ -18,14 +19,29 @@ 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";
@ -33,48 +49,31 @@ namespace nadena.dev.modular_avatar.animation
blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false;
blendTree.children = asc.BoundReadableProperties
.Select(prop => GenerateDelayChild(nullMotion, prop))
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[]
{
new ChildAnimatorState
{
state = state,
position = Vector3.zero
}
};
fx.layers = fx.layers.Append(new AnimatorControllerLayer
{
name = "DelayDisable",
stateMachine = asm,
defaultWeight = 1,
blendingMode = AnimatorLayerBlendingMode.Override
}).ToArray();
state.WriteDefaultValues = true;
state.Motion = asc.ControllerContext.Clone(blendTree);
// Ensure the initial state of readable props matches the actual state of the gameobject
var parameters = fx.parameters;
var paramToIndex = parameters.Select((p, i) => (p, i)).ToDictionary(x => x.p.name, x => x.i);
foreach (var (binding, prop) in asc.BoundReadableProperties)
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
var obj = asc.PathMappings.PathToObject(binding.path);
if (obj != null && paramToIndex.TryGetValue(prop, out var index))
foreach (var (binding, prop) in activeProxies)
{
parameters[index].defaultFloat = obj.activeSelf ? 1 : 0;
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
{
p.defaultFloat = obj.activeSelf ? 1 : 0;
controller.Parameters = controller.Parameters.SetItem(prop, p);
}
}
}
fx.parameters = parameters;
}
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)

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,418 +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();
#if MA_VRCSDK3_AVATARS
if (context.AvatarDescriptor)
{
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();
}
#endif
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

@ -5,6 +5,7 @@ using System;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor.Animations;
using UnityEngine;
@ -21,40 +22,34 @@ 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;
}
}
}

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;
@ -44,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))
@ -93,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++)
@ -115,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)
{
@ -128,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);
@ -163,4 +180,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
#endif
#endif

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

@ -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);
}
}
@ -371,6 +327,78 @@ namespace nadena.dev.modular_avatar.core.editor
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)
{
var list = new List<int>();

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);
}
@ -260,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"));
@ -274,7 +339,7 @@ namespace nadena.dev.modular_avatar.core.editor
_parameterGUI.DoGUI(true);
ShowInnateParameterGUI();
EditorGUILayout.EndVertical();
if (_texture != null)

View File

@ -1,4 +1,7 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Linq;
using System.Collections.Generic;
using nadena.dev.modular_avatar.ui;
using UnityEditor;
using UnityEngine;
@ -8,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;
foreach (var selected in selections)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var parent = avatarRoot.gameObject;
var createInstaller = true;
if (TryGetChildrenSourceSubmenu(selected, out var _))
{
parent = selected;
createInstaller = false;
}
CreateToggleImpl(selected, parent, createInstaller:createInstaller);
}
Selection.objects = null;
}
private static bool TryGetChildrenSourceSubmenu(GameObject target, out ModularAvatarMenuItem subMenu)
{
subMenu = null;
try
{
var selectedMenuItem = selected.GetComponent<ModularAvatarMenuItem>();
if (selectedMenuItem?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& selectedMenuItem.MenuSource == SubmenuSource.Children
var mami = target.GetComponent<ModularAvatarMenuItem>();
if (mami?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& mami.MenuSource == SubmenuSource.Children
)
{
parent = selected.transform;
createInstaller = false;
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,
};

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

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

@ -5,7 +5,6 @@ using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
@ -42,7 +41,7 @@ namespace nadena.dev.modular_avatar.core.editor
listView.selectionType = SelectionType.Multiple;
listView.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete)
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
{
serializedObject.Update();
@ -66,9 +65,9 @@ namespace nadena.dev.modular_avatar.core.editor
listView.SetSelectionWithoutNotify(indices);
};
}
evt.StopPropagation();
}
evt.StopPropagation();
}, TrickleDown.NoTrickleDown);
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
@ -211,18 +210,14 @@ namespace nadena.dev.modular_avatar.core.editor
default: pst = ParameterSyncType.Float; break;
}
if (!parameter.networkSynced)
{
pst = ParameterSyncType.NotSynced;
}
target.parameters.Add(new ParameterConfig()
{
internalParameter = false,
nameOrPrefix = parameter.name,
isPrefix = false,
remapTo = "",
syncType = pst,
syncType = pst,
localOnly = !parameter.networkSynced,
defaultValue = parameter.defaultValue,
saved = parameter.saved,
});
@ -260,4 +255,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif
#endif

View File

@ -2,6 +2,7 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
@ -83,8 +84,12 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
foreach (var elem in root.Query<TextElement>().Build())
{
// Prevent keypresses from bubbling up
elem.RegisterCallback<KeyDownEvent>(evt => evt.StopPropagation(), TrickleDown.NoTrickleDown);
// 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

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

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

@ -78,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.",
@ -93,7 +96,7 @@
"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",
@ -153,6 +156,9 @@
"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",
@ -284,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

@ -74,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": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
@ -89,7 +92,7 @@
"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": "相対的パスのルート",
@ -149,6 +152,9 @@
"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がありますが、メッシュがありません。",
@ -276,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": "調整子級的位置",
@ -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

@ -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,49 +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) return;
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>>();
@ -89,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(
@ -101,243 +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)
{
// Special case: A layer with a single state, which contains a blend tree, is ignored for WD analysis.
// This is because WD ON blend trees have different behavior from most WD ON states, and can be safely
// used in a WD OFF animator.
if (layer.stateMachine.states.Length == 1
&& layer.stateMachine.states[0].state.motion is BlendTree
&& layer.stateMachine.stateMachines.Length == 0
)
{
continue;
}
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

@ -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)
{
@ -135,7 +136,68 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
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)
@ -217,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
@ -294,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));
}
/**
@ -303,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)
{
@ -313,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;
@ -357,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);
}
@ -367,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);
}
@ -384,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)
@ -403,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;
}
}
@ -468,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,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
@ -187,16 +188,17 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (invertedRootBoneCache.TryGetValue(rootBone, out var cache)) { return cache; }
var cloned = Object.Instantiate(rootBone.gameObject, rootBone, true);
cloned.name = rootBone.gameObject.name + "-InvertedRootBone";
var invertedRootBone = new GameObject($"{rootBone.gameObject.name}-InvertedRootBone");
EditorUtility.CopySerialized(rootBone, invertedRootBone.transform);
invertedRootBone.transform.parent = rootBone;
var invertedRootBone = cloned.transform;
var scale = invertedRootBone.localScale;
var transform = invertedRootBone.transform;
var scale = transform.localScale;
scale.x *= -1;
invertedRootBone.localScale = scale;
transform.localScale = scale;
invertedRootBoneCache[rootBone] = invertedRootBone;
return invertedRootBone;
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

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor;
#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER
@ -6,7 +8,7 @@ using UnityEngine;
using UnityEngine.Animations;
using VRC.SDK3.Avatars;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf.animator;
using VRC.Dynamics;
#endif
@ -50,17 +52,18 @@ namespace nadena.dev.modular_avatar.core.editor
var constraintGameObjects = context.AvatarRootObject.GetComponentsInChildren<IConstraint>(true)
.Select(c => (c as Component)?.gameObject)
.Where(go => go != null)
.Distinct()
.Where(go => go.GetComponentsInParent<ModularAvatarConvertConstraints>(true)
.Where(go => go!.GetComponentsInParent<ModularAvatarConvertConstraints>(true)
.Select(c => c.gameObject)
.Any(converters.Contains)
).ToArray();
var targetConstraintComponents =
constraintGameObjects.SelectMany(go => go.GetComponents<IConstraint>()).ToArray();
constraintGameObjects.SelectMany(go => go!.GetComponents<IConstraint>()).ToArray();
AvatarDynamicsSetup.DoConvertUnityConstraints(targetConstraintComponents, null, false);
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
// Also look for preexisting VRCConstraints so we can go fix up any broken animation clips from people who
// clicked auto fix :(
@ -70,24 +73,20 @@ namespace nadena.dev.modular_avatar.core.editor
var targetPaths = constraintGameObjects
.Union(existingVRCConstraints)
.Select(c => asc.PathMappings.GetObjectIdentifier(c))
.Select(c => asc.ObjectPathRemapper.GetVirtualPathForObject(c!))
.ToHashSet();
// Update animation clips
var clips = targetPaths.SelectMany(tp => asc.AnimationDatabase.ClipsForPath(tp))
var clips = targetPaths.SelectMany(tp => asc.AnimationIndex.GetClipsForObjectPath(tp))
.ToHashSet();
foreach (var clip in clips) RemapSingleClip(clip, targetPaths);
}
private void RemapSingleClip(AnimationDatabase.ClipHolder clip, HashSet<string> targetPaths)
private void RemapSingleClip(VirtualClip clip, HashSet<string> targetPaths)
{
var motion = clip.CurrentClip as AnimationClip;
if (motion == null) return;
var bindings = AnimationUtility.GetCurveBindings(motion);
var toUpdateBindings = new List<EditorCurveBinding>();
var toUpdateCurves = new List<AnimationCurve>();
var bindings = clip.GetFloatCurveBindings().ToList();
foreach (var ecb in bindings)
{
@ -102,20 +101,11 @@ namespace nadena.dev.modular_avatar.core.editor
type = newType,
propertyName = newProp
};
var curve = AnimationUtility.GetEditorCurve(motion, ecb);
if (curve != null)
{
toUpdateBindings.Add(newBinding);
toUpdateCurves.Add(curve);
toUpdateBindings.Add(ecb);
toUpdateCurves.Add(null);
}
var curve = clip.GetFloatCurve(ecb);
clip.SetFloatCurve(newBinding, curve);
clip.SetFloatCurve(ecb, null);
}
}
if (toUpdateBindings.Count == 0) return;
AnimationUtility.SetEditorCurves(motion, toUpdateBindings.ToArray(), toUpdateCurves.ToArray());
}
#else

View File

@ -37,6 +37,7 @@ namespace nadena.dev.modular_avatar.core.editor
public static ImmutableHashSet<string> VRCSDKParameters = new string[]
{
"IsLocal",
"PreviewMode",
"Viseme",
"Voice",
"GestureLeft",
@ -155,7 +156,7 @@ namespace nadena.dev.modular_avatar.core.editor
case ModularAvatarMergeBlendTree mergeBlendTree:
{
WalkBlendTree(parameters, mergeBlendTree.BlendTree as BlendTree);
WalkBlendTree(parameters, mergeBlendTree.Motion as BlendTree);
break;
}

View File

@ -5,7 +5,9 @@ using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core.editor.plugin;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using nadena.dev.ndmf.fluent;
using nadena.dev.ndmf.util;
using UnityEngine;
using Object = UnityEngine.Object;
@ -35,8 +37,13 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
Sequence seq = InPhase(BuildPhase.Resolving);
seq.Run(ResolveObjectReferences.Instance);
// Protect against accidental destructive edits by cloning the input controllers ASAP
seq.Run("Clone animators", AnimationUtil.CloneAllControllers);
seq.WithRequiredExtension(typeof(AnimatorServicesContext), s =>
{
// Just activating the context is enough.
s.Run("Clone animators", _ => { });
});
seq = InPhase(BuildPhase.Transforming);
seq.Run("Validate configuration",
@ -44,42 +51,63 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
seq.WithRequiredExtension(typeof(ModularAvatarContext), _s1 =>
{
seq.Run(ClearEditorOnlyTags.Instance);
seq.Run(VRChatSettingsPass.Instance);
seq.Run(MeshSettingsPluginPass.Instance);
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
#if MA_VRCSDK3_AVATARS
seq.Run(ReactiveObjectPrepass.Instance);
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(ParameterAssignerPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
seq.Run(MergeAnimatorPluginPass.Instance);
seq.Run(ApplyAnimatorDefaultValuesPass.Instance);
#endif
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
seq.WithRequiredExtension(typeof(AnimatorServicesContext), _s2 =>
{
#if MA_VRCSDK3_AVATARS
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview());
seq.Run(MMDRelayEarlyPass.Instance);
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(ParameterAssignerPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
seq.Run(MergeAnimatorPluginPass.Instance);
seq.Run(ApplyAnimatorDefaultValuesPass.Instance);
seq.WithRequiredExtension(typeof(ReadablePropertyExtension), _s3 =>
{
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(),
new MaterialSetterPreview());
});
seq.Run(GameObjectDelayDisablePass.Instance);
// TODO: We currently run this above MergeArmaturePlugin, because Merge Armature might destroy
// game objects which contain Menu Installers. It'd probably be better however to teach Merge Armature
// to retain those objects? maybe?
seq.Run(MenuInstallPluginPass.Instance);
#endif
seq.Run(MergeArmaturePluginPass.Instance);
seq.Run(BoneProxyPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
#endif
seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx)
);
seq.Run(WorldScaleObjectPass.Instance);
seq.Run(ReplaceObjectPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(BlendshapeSyncAnimationPluginPass.Instance);
seq.Run(GameObjectDelayDisablePass.Instance);
#endif
seq.Run(ConstraintConverterPass.Instance);
seq.Run("Prune empty animator layers",
ctx => { ctx.Extension<AnimatorServicesContext>().RemoveEmptyLayers(); });
seq.Run("Harmonize animator parameter types",
ctx => { ctx.Extension<AnimatorServicesContext>().HarmonizeParameterTypes(); });
seq.Run(MMDRelayPass.Instance);
});
#if MA_VRCSDK3_AVATARS
seq.Run(PhysbonesBlockerPluginPass.Instance);
@ -88,7 +116,9 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
});
seq.Run(SyncParameterSequencePass.Instance);
#endif
seq.Run(RemoveVertexColorPass.Instance).PreviewingWith(new RemoveVertexColorPreview());
seq.Run(RebindHumanoidAvatarPass.Instance);
seq.Run("Purge ModularAvatar components", ctx =>
{
@ -238,7 +268,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
protected override void Execute(ndmf.BuildContext context)
{
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(MAContext(context));
new BlendshapeSyncAnimationProcessor(context).OnPreprocessAvatar();
}
}

View File

@ -1,4 +1,6 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Object = UnityEngine.Object;
@ -14,6 +16,8 @@ namespace nadena.dev.modular_avatar.core.editor
// Objects which trigger deletion of this shape key.
public List<ReactionRule> actionGroups = new List<ReactionRule>();
public object? overrideStaticState = null;
public AnimatedProperty(TargetProp key, float currentState)
{
TargetProp = key;
@ -32,7 +36,7 @@ namespace nadena.dev.modular_avatar.core.editor
TargetProp.Equals(other.TargetProp);
}
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;

View File

@ -31,7 +31,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (_asc != null)
{
return _asc.GetActiveSelfProxy(obj);
return _rpe.GetActiveSelfProxy(obj);
}
else
{
@ -137,7 +137,7 @@ namespace nadena.dev.modular_avatar.core.editor
var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates);
if (mami_condition != null &&
if (mami_condition != null && ForceMenuItems != null &&
ForceMenuItems.TryGetValue(mami_condition.Parameter, out var forcedMenuItem))
{
var enable = forcedMenuItem == mami;
@ -250,17 +250,12 @@ namespace nadena.dev.modular_avatar.core.editor
{
info = new AnimatedProperty(key, currentValue);
shapeKeys[key] = info;
// Add initial state
var agk = new ReactionRule(key, value);
agk.Value = currentValue;
info.actionGroups.Add(agk);
}
var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
info.currentState = currentValue;
if (info.actionGroups.Count == 0)
{

View File

@ -1,9 +1,11 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core.editor.Simulator;
using nadena.dev.ndmf.animator;
using nadena.dev.ndmf.preview;
using UnityEngine;
@ -17,7 +19,12 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly ComputeContext _computeContext;
private readonly ndmf.BuildContext _context;
private readonly AnimationServicesContext _asc;
private readonly AnimatorServicesContext _asc;
private readonly ReadablePropertyExtension _rpe;
private static readonly ImmutableHashSet<Type> ActiveObjectTypes =
new[] { typeof(AudioSource) }.ToImmutableHashSet();
private Dictionary<string, float> _simulationInitialStates;
public const string BlendshapePrefix = "blendShape.";
@ -34,7 +41,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
_computeContext = ComputeContext.NullContext;
_context = context;
_asc = context.Extension<AnimationServicesContext>();
_asc = context.Extension<AnimatorServicesContext>();
_rpe = context.Extension<ReadablePropertyExtension>();
_simulationInitialStates = null;
}
@ -111,6 +119,8 @@ namespace nadena.dev.modular_avatar.core.editor
FindObjectToggles(shapes, root);
FindMaterialSetters(shapes, root);
InjectActiveObjectFallbacks(shapes);
ApplyInitialStateOverrides(shapes);
AnalyzeConstants(shapes);
ResolveToggleInitialStates(shapes);
@ -120,6 +130,48 @@ namespace nadena.dev.modular_avatar.core.editor
return result;
}
private void InjectActiveObjectFallbacks(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var injectedComponents = new List<Behaviour>();
foreach (var targetProp in shapes.Keys)
{
if (targetProp.TargetObject is GameObject go && targetProp.PropertyName == "m_IsActive")
{
foreach (var ty in ActiveObjectTypes)
{
foreach (var c in go.GetComponentsInChildren(ty, true))
{
if (c is Behaviour b)
{
injectedComponents.Add(b);
}
}
}
}
}
foreach (var component in injectedComponents)
{
var tp = new TargetProp
{
TargetObject = component,
PropertyName = "m_Enabled"
};
if (!shapes.TryGetValue(tp, out var shape))
{
var currentState = component.enabled ? 1f : 0f;
shape = new AnimatedProperty(tp, currentState);
// Because we have no action groups, we'll reset current state in the base animation and otherwise
// not touch the state.
shapes[tp] = shape;
}
shape.overrideStaticState = 0f; // Static state is always off
}
}
private void ApplyInitialStateOverrides(Dictionary<TargetProp, AnimatedProperty> shapes)
{
foreach (var prop in shapes.Values)
@ -145,7 +197,7 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="shapes"></param>
private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = _context?.Extension<AnimationServicesContext>();
var asc = _context?.Extension<AnimatorServicesContext>();
HashSet<GameObject> toggledObjects = new();
if (asc == null) return;
@ -160,7 +212,10 @@ namespace nadena.dev.modular_avatar.core.editor
{
foreach (var condition in actionGroup.ControllingConditions)
if (condition.ReferenceObject != null && !toggledObjects.Contains(condition.ReferenceObject))
condition.IsConstant = asc.AnimationDatabase.ClipsForPath(asc.PathMappings.GetObjectIdentifier(condition.ReferenceObject)).IsEmpty;
condition.IsConstant = !asc.AnimationIndex.GetClipsForObjectPath(
asc.ObjectPathRemapper.GetVirtualPathForObject(condition.ReferenceObject) ??
"___NONEXISTENT___"
).Any();
// Remove redundant active conditions.
actionGroup.ControllingConditions.RemoveAll(c => c.IsConstant && c.InitiallyActive);
@ -175,9 +230,9 @@ namespace nadena.dev.modular_avatar.core.editor
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
}
// Remove shapes with no action groups
// Remove shapes with no action groups (unless we need to override static state)
foreach (var kvp in shapes.ToList())
if (kvp.Value.actionGroups.Count == 0)
if (kvp.Value.actionGroups.Count == 0 && kvp.Value.overrideStaticState == null)
shapes.Remove(kvp.Key);
}
@ -187,7 +242,7 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="groups"></param>
private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty> groups)
{
var asc = _context?.Extension<AnimationServicesContext>();
var asc = _context?.Extension<AnimatorServicesContext>();
Dictionary<string, float> propStates = new();
Dictionary<string, float> nextPropStates = new();
@ -283,7 +338,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var (key, info) in shapes.ToList())
{
if (info.actionGroups.Count == 0)
if (info.actionGroups.Count == 0 && info.overrideStaticState == null)
{
// never active control; ignore it entirely
if (OptimizeShapes) shapes.Remove(key);
@ -298,9 +353,9 @@ namespace nadena.dev.modular_avatar.core.editor
initialStates[key] = initialState;
// If we're now constant-on, we can skip animation generation
if (info.actionGroups[^1].IsConstant)
if (info.actionGroups.Count == 0 || info.actionGroups[^1].IsConstant)
{
if (OptimizeShapes) shapes.Remove(key);
if (OptimizeShapes && info.overrideStaticState == null) shapes.Remove(key);
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -23,8 +24,8 @@ namespace nadena.dev.modular_avatar.core.editor
// Properties that are being driven, either by foreign animations or Object Toggles
private HashSet<string> activeProps = new();
private AnimationClip _initialStateClip;
private VirtualClip _initialStateClip;
private bool _writeDefaults;
public ReactiveObjectPass(ndmf.BuildContext context)
@ -38,7 +39,14 @@ namespace nadena.dev.modular_avatar.core.editor
// Having a WD OFF layer after WD ON layers can break WD. We match the behavior of the existing states,
// and if mixed, use WD ON to maximize compatibility.
_writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true;
var asc = context.Extension<AnimatorServicesContext>();
_writeDefaults = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX]?.Layers.Any(
l => l.StateMachine.StateMachines.Any(
sm => sm.StateMachine.AllStates().Any(
s => s.WriteDefaultValues && s.Motion is not VirtualBlendTree
)
)
) ?? true;
var analysis = new ReactiveObjectAnalyzer(context).Analyze(context.AvatarRootObject);
@ -60,7 +68,7 @@ namespace nadena.dev.modular_avatar.core.editor
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = context.Extension<AnimationServicesContext>();
var rpe = context.Extension<ReadablePropertyExtension>();
foreach (var prop in shapes.Keys)
{
@ -68,7 +76,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
// Ensure a proxy exists for each object we're going to be toggling.
// TODO: is this still needed?
asc.GetActiveSelfProxy(go);
rpe.GetActiveSelfProxy(go);
}
}
}
@ -91,19 +99,19 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
var rpe = context.Extension<ReadablePropertyExtension>();
// We need to track _two_ initial states: the initial state we'll apply at build time (which applies
// when animations are disabled) and the animation base state. Confusingly, the animation base state
// should be the state that is currently applied to the object...
var clips = context.Extension<AnimationServicesContext>().AnimationDatabase;
var initialStateHolder = clips.ClipsForPath(ReactiveObjectPrepass.TAG_PATH).FirstOrDefault();
if (initialStateHolder == null) return;
_initialStateClip = new AnimationClip();
_initialStateClip.name = "Reactive Component Defaults";
initialStateHolder.CurrentClip = _initialStateClip;
var clips = asc.AnimationIndex;
_initialStateClip = clips.GetClipsForObjectPath(ReactiveObjectPrepass.TAG_PATH).FirstOrDefault();
if (_initialStateClip == null) return;
_initialStateClip.Name = "Reactive Component Defaults";
foreach (var (key, initialState) in initialStates)
{
@ -137,6 +145,11 @@ namespace nadena.dev.modular_avatar.core.editor
applied = true;
}
}
else if (key.TargetObject is Component c)
{
componentType = c.GetType();
path = RuntimeUtil.RelativePath(context.AvatarRootObject, c.gameObject);
}
else
{
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
@ -147,17 +160,19 @@ namespace nadena.dev.modular_avatar.core.editor
var serializedObject = new SerializedObject(key.TargetObject);
var prop = serializedObject.FindProperty(key.PropertyName);
var staticState = shapes.GetValueOrDefault(key)?.overrideStaticState ?? initialState;
if (prop != null)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Boolean:
animBaseState = prop.boolValue ? 1.0f : 0.0f;
prop.boolValue = ((float)initialState) > 0.5f;
prop.boolValue = (float)staticState > 0.5f;
break;
case SerializedPropertyType.Float:
animBaseState = prop.floatValue;
prop.floatValue = (float) initialState;
prop.floatValue = (float)staticState;
break;
case SerializedPropertyType.ObjectReference:
animBaseState = prop.objectReferenceValue;
@ -186,17 +201,17 @@ namespace nadena.dev.modular_avatar.core.editor
curve.AddKey(0, f);
curve.AddKey(1, f);
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
_initialStateClip.SetFloatCurve(binding, curve);
if (componentType == typeof(GameObject) && key.PropertyName == "m_IsActive")
{
binding = EditorCurveBinding.FloatCurve(
"",
typeof(Animator),
asc.GetActiveSelfProxy((GameObject)key.TargetObject)
rpe.GetActiveSelfProxy((GameObject)key.TargetObject)
);
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
_initialStateClip.SetFloatCurve(binding, curve);
}
}
else if (animBaseState is Object obj)
@ -206,8 +221,8 @@ namespace nadena.dev.modular_avatar.core.editor
componentType,
key.PropertyName
);
AnimationUtility.SetObjectReferenceCurve(_initialStateClip, binding, new []
_initialStateClip.SetObjectCurve(binding, new[]
{
new ObjectReferenceKeyframe()
{
@ -286,12 +301,27 @@ namespace nadena.dev.modular_avatar.core.editor
});
}
}
// Remove all deletedShape. props to avoid creating animator noise that'll confuse tools like AAO
foreach (var key in shapes.Keys.ToList())
{
if (key.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
{
shapes.Remove(key);
}
}
}
#endregion
private void ProcessShapeKey(AnimatedProperty info)
{
if (info.actionGroups.Count == 0)
{
// This is present only to override the static state; skip animation generation
return;
}
// TODO: prune non-animated keys
var asm = GenerateStateMachine(info);
ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name);
@ -299,7 +329,7 @@ namespace nadena.dev.modular_avatar.core.editor
private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info)
{
var asc = context.Extension<AnimationServicesContext>();
var asc = context.Extension<AnimatorServicesContext>();
var asm = new AnimatorStateMachine();
// Workaround for the warning: "'.' is not allowed in State name"
@ -310,10 +340,8 @@ namespace nadena.dev.modular_avatar.core.editor
var yInc = 60;
asm.anyStatePosition = new Vector3(-200, 0);
var initial = new AnimationClip();
var initialState = new AnimatorState();
initialState.motion = initial;
initialState.writeDefaultValues = _writeDefaults;
initialState.name = "<default>";
asm.defaultState = initialState;
@ -326,7 +354,6 @@ namespace nadena.dev.modular_avatar.core.editor
position = new Vector3(x, y),
state = initialState
});
asc.AnimationDatabase.RegisterState(states[^1].state);
var lastConstant = info.actionGroups.FindLastIndex(agk => agk.IsConstant);
var transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
@ -334,7 +361,13 @@ namespace nadena.dev.modular_avatar.core.editor
transitionBuffer.Add((initialState, new List<AnimatorStateTransition>()));
foreach (var group in info.actionGroups.Skip(lastConstant))
// Note: We need to generate a group for any base constant state as well; this is because we generate the
// scene initial value as a base animation curve in the base blend tree, which would be exposed in the
// default state. This is incorrect when there is a constant-on Object Toggle or similar changing the
// initial state of a property.
//
// We can, however, skip any groups _before_ that constant state, as they'll be overridden in all cases.
foreach (var group in info.actionGroups.Skip(Math.Max(0, lastConstant - 1)))
{
y += yInc;
@ -350,7 +383,7 @@ namespace nadena.dev.modular_avatar.core.editor
clip.name = "Property Overlay controlled by " + group.ControllingConditions[0].DebugName + " " +
group.Value;
var conditions = GetTransitionConditions(asc, group);
var conditions = GetTransitionConditions(group);
foreach (var (st, transitions) in transitionBuffer)
{
@ -394,7 +427,6 @@ namespace nadena.dev.modular_avatar.core.editor
position = new Vector3(x, y),
state = state
});
asc.AnimationDatabase.RegisterState(states[^1].state);
var transitionList = new List<AnimatorStateTransition>();
transitionBuffer.Add((state, transitionList));
@ -444,6 +476,15 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
if (initialState.motion == null)
{
// For some reason, if we set the state's motion multiple times, Unity will sometimes revert to the
// first motion set; as such, make sure to set the empty motion only if we really mean it.
var initial = new AnimationClip();
initial.name = "empty motion";
initialState.motion = initial;
}
foreach (var (st, transitions) in transitionBuffer) st.transitions = transitions.ToArray();
asm.states = states.ToArray();
@ -466,7 +507,7 @@ namespace nadena.dev.modular_avatar.core.editor
};
}
private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ReactionRule group)
private AnimatorCondition[] GetTransitionConditions(ReactionRule group)
{
var conditions = new List<AnimatorCondition>();
@ -552,8 +593,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (key.TargetObject is GameObject targetObject && key.PropertyName == "m_IsActive")
{
var asc = context.Extension<AnimationServicesContext>();
var propName = asc.GetActiveSelfProxy(targetObject);
var rpe = context.Extension<ReadablePropertyExtension>();
var propName = rpe.GetActiveSelfProxy(targetObject);
binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
@ -564,47 +605,27 @@ namespace nadena.dev.modular_avatar.core.editor
private void ApplyController(AnimatorStateMachine asm, string layerName)
{
var fx = FindFxController();
if (fx.animatorController == null)
var asc = context.Extension<AnimatorServicesContext>();
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
if (fx == null)
{
throw new InvalidOperationException("No FX layer found");
}
if (!context.IsTemporaryAsset(fx.animatorController))
{
throw new InvalidOperationException("FX layer is not a temporary asset");
}
if (!(fx.animatorController is AnimatorController animController))
foreach (var paramName in initialValues.Keys.Except(fx.Parameters.Keys))
{
throw new InvalidOperationException("FX layer is not an animator controller");
}
var paramList = animController.parameters.ToList();
var paramSet = paramList.Select(p => p.name).ToHashSet();
foreach (var paramName in initialValues.Keys.Except(paramSet))
{
paramList.Add(new AnimatorControllerParameter()
var parameter = new AnimatorControllerParameter
{
name = paramName,
type = AnimatorControllerParameterType.Float,
defaultFloat = initialValues[paramName], // TODO
});
paramSet.Add(paramName);
};
fx.Parameters = fx.Parameters.SetItem(paramName, parameter);
}
animController.parameters = paramList.ToArray();
animController.layers = animController.layers.Append(
new AnimatorControllerLayer
{
stateMachine = asm,
name = "RC " + layerName,
defaultWeight = 1
}
).ToArray();
fx.AddLayer(LayerPriority.Default, "RC " + layerName).StateMachine =
asc.ControllerContext.Clone(asm);
}
private VRCAvatarDescriptor.CustomAnimLayer FindFxController()

View File

@ -49,7 +49,7 @@ namespace nadena.dev.modular_avatar.core.editor
var obj = new GameObject("MA SC Defaults");
obj.transform.SetParent(context.AvatarRootTransform);
var mambt = obj.AddComponent<ModularAvatarMergeBlendTree>();
mambt.BlendTree = bt;
mambt.Motion = bt;
mambt.PathMode = MergeAnimatorPathMode.Absolute;
}
}

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor.Animations;
using nadena.dev.ndmf.animator;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
@ -54,6 +54,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (!context.AvatarDescriptor) return;
var paramIndex = 0;
var mappings = ParameterRenameMappings.Get(context);
var declaredParams = context.AvatarDescriptor.expressionParameters.parameters
.GroupBy(p => p.name).Select(l => l.First())
@ -72,7 +73,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (mami.Control == null) mami.Control = new VRCExpressionsMenu.Control();
mami.Control.parameter = new VRCExpressionsMenu.Control.Parameter
{
name = $"__MA/AutoParam/{mami.gameObject.name}${paramIndex++}"
name = mappings.Remap(mami, ParameterNamespace.Animator,
$"__MA/AutoParam/{mami.gameObject.name}")
};
}
@ -201,18 +203,20 @@ namespace nadena.dev.modular_avatar.core.editor
if (mamiWithRC.Count > 0)
{
// This make sures the parameters are correctly merged into the FX layer.
var mergeAnimator = context.AvatarRootObject.AddComponent<ModularAvatarMergeAnimator>();
mergeAnimator.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
mergeAnimator.deleteAttachedAnimator = false;
mergeAnimator.animator = new AnimatorController
var asc = context.Extension<AnimatorServicesContext>();
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
foreach (var (name, _) in mamiWithRC)
{
parameters = mamiWithRC.Select(kvp => new AnimatorControllerParameter
if (!fx.Parameters.ContainsKey(name))
{
name = kvp.Key,
type = AnimatorControllerParameterType.Float,
}).ToArray(),
};
fx.Parameters = fx.Parameters.SetItem(name, new()
{
name = name,
type = AnimatorControllerParameterType.Float,
});
}
}
}
}

View File

@ -0,0 +1,93 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class RemoveVertexColorPass : Pass<RemoveVertexColorPass>
{
protected override void Execute(ndmf.BuildContext context)
{
var removers = context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarRemoveVertexColor>(true)!;
Dictionary<Mesh, Mesh> conversionMap = new();
foreach (var remover in removers)
{
foreach (var smr in remover!.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
TryRemove(context.IsTemporaryAsset, smr, conversionMap);
}
foreach (var mf in remover.GetComponentsInChildren<MeshFilter>(true))
{
TryRemove(context.IsTemporaryAsset, mf, conversionMap);
}
}
}
private const string PropPath = "m_Mesh";
private static void TryRemove(
Func<Mesh, bool> isTempAsset,
Component c,
Dictionary<Mesh, Mesh> conversionMap
)
{
var nearestRemover = c.GetComponentInParent<ModularAvatarRemoveVertexColor>()!;
if (nearestRemover.Mode != ModularAvatarRemoveVertexColor.RemoveMode.Remove) return;
ForceRemove(isTempAsset, c, conversionMap);
}
internal static void ForceRemove(Func<Mesh, bool> isTempAsset, Component c,
Dictionary<Mesh, Mesh> conversionMap)
{
var obj = new SerializedObject(c);
var prop = obj.FindProperty("m_Mesh");
if (prop == null)
{
throw new Exception("Property not found: " + PropPath);
}
var mesh = prop.objectReferenceValue as Mesh;
if (mesh == null)
{
return;
}
var originalMesh = mesh;
if (conversionMap.TryGetValue(mesh, out var converted))
{
prop.objectReferenceValue = converted;
obj.ApplyModifiedPropertiesWithoutUndo();
return;
}
if (mesh.GetVertexAttributes().All(va => va.attribute != VertexAttribute.Color))
{
// no-op
return;
}
if (!isTempAsset(mesh))
{
mesh = Object.Instantiate(mesh);
prop.objectReferenceValue = mesh;
obj.ApplyModifiedPropertiesWithoutUndo();
}
mesh.colors = null;
conversionMap[originalMesh] = mesh;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a227da6f9f1548c3867b1ed113f28e9d
timeCreated: 1733008734

View File

@ -1,4 +1,4 @@
#if MA_VRCSDK3_AVATARS
#if MA_VRCSDK3_AVATARS
#region
@ -6,21 +6,19 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using System.Security.Cryptography;
using System.Text;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using UnityEngine.Profiling;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDK3.Dynamics.Contact.Components;
using VRC.SDK3.Dynamics.PhysBone.Components;
using Object = UnityEngine.Object;
using UnityObject = UnityEngine.Object;
#endregion
namespace nadena.dev.modular_avatar.core.editor
@ -32,18 +30,39 @@ namespace nadena.dev.modular_avatar.core.editor
return ctx.GetState<ParameterRenameMappings>();
}
public Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string> Remappings =
new Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string>();
private readonly HashSet<string> usedNames = new();
public Dictionary<(Component, ParameterNamespace, string), string> Remappings = new();
private int internalParamIndex;
public string Remap(ModularAvatarParameters p, ParameterNamespace ns, string s)
public string Remap(Component p, ParameterNamespace ns, string s)
{
var tuple = (p, ns, s);
if (Remappings.TryGetValue(tuple, out var mapping)) return mapping;
mapping = s + "$$Internal_" + internalParamIndex++;
var path = RuntimeUtil.AvatarRootPath(p.gameObject)!;
string pathHash;
using (var sha = SHA256.Create())
{
var hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(path));
StringBuilder sb = new();
for (var i = 0; i < 6; i++)
{
sb.AppendFormat("{0:x2}", hashBytes[i]);
}
pathHash = sb.ToString();
}
mapping = s + "$" + pathHash;
for (var i = 0; !usedNames.Add(mapping); i++)
{
mapping = s + "$" + mapping + "." + i;
}
Remappings[tuple] = mapping;
return mapping;
@ -55,7 +74,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
public ImmutableDictionary<string, float> InitialValueOverrides;
}
internal class RenameParametersHook
{
private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7";
@ -75,8 +94,8 @@ namespace nadena.dev.modular_avatar.core.editor
private static long encounterOrderCounter;
public ParameterConfig ResolvedParameter;
public List<UnityObject> TypeSources = new List<UnityObject>();
public List<UnityObject> DefaultSources = new List<UnityObject>();
public List<Object> TypeSources = new List<Object>();
public List<Object> DefaultSources = new List<Object>();
public ImmutableHashSet<float> ConflictingValues = ImmutableHashSet<float>.Empty;
public ImmutableHashSet<ParameterSyncType> ConflictingSyncTypes = ImmutableHashSet<ParameterSyncType>.Empty;
@ -163,7 +182,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (!context.AvatarDescriptor) return;
_context = context;
var syncParams = WalkTree(avatar);
SetExpressionParameters(avatar, syncParams);
@ -176,7 +195,7 @@ namespace nadena.dev.modular_avatar.core.editor
// clean up all parameters objects before the ParameterAssignerPass runs
foreach (var p in avatar.GetComponentsInChildren<ModularAvatarParameters>())
UnityObject.DestroyImmediate(p);
Object.DestroyImmediate(p);
}
private void SetExpressionParameters(GameObject avatarRoot, ImmutableDictionary<string, ParameterInfo> allParams)
@ -319,6 +338,8 @@ namespace nadena.dev.modular_avatar.core.editor
GameObject obj
)
{
var animServices = _context.PluginBuildContext.Extension<AnimatorServicesContext>();
var paramInfo = ndmf.ParameterInfo.ForContext(_context.PluginBuildContext);
ImmutableDictionary<string, ParameterInfo> rv = ImmutableDictionary<string, ParameterInfo>.Empty;
@ -366,14 +387,8 @@ namespace nadena.dev.modular_avatar.core.editor
break;
}
case ModularAvatarMergeAnimator merger:
case IVirtualizeAnimatorController or IVirtualizeMotion:
{
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = _context.ConvertAnimatorController(overrideController);
}
var mappings = paramInfo.GetParameterRemappingsAt(obj);
var remap = mappings.SelectMany(item =>
{
@ -387,25 +402,10 @@ namespace nadena.dev.modular_avatar.core.editor
);
}).ToImmutableDictionary();
if (merger.animator != null)
var controller = animServices.ControllerContext.Controllers[component];
if (controller != null)
{
Profiler.BeginSample("DeepCloneAnimator");
merger.animator = new DeepClone(_context.PluginBuildContext).DoClone(merger.animator);
Profiler.EndSample();
ProcessRuntimeAnimatorController(merger.animator, remap);
}
break;
}
case ModularAvatarMergeBlendTree merger:
{
var bt = merger.BlendTree as BlendTree;
if (bt != null)
{
merger.BlendTree = bt = new DeepClone(_context.PluginBuildContext).DoClone(bt);
ProcessBlendtree(bt, paramInfo.GetParameterRemappingsAt(obj));
ProcessVirtualAnimatorController(controller, remap);
}
break;
@ -439,6 +439,18 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
_context.PostProcessControls.Add(menuItem, control =>
{
control.parameter.name = remap(remaps, control.parameter.name);
foreach (var subParam in control.subParameters)
{
subParam.name = remap(remaps, subParam.name);
}
});
}
break;
}
}
@ -485,28 +497,6 @@ namespace nadena.dev.modular_avatar.core.editor
return rv;
}
private void ProcessRuntimeAnimatorController(RuntimeAnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap)
{
if (controller is AnimatorController ac)
{
ProcessAnimator(ac, remap);
}
else if (controller is AnimatorOverrideController aoc)
{
var list = new List<KeyValuePair<AnimationClip, AnimationClip>>();
aoc.GetOverrides(list);
for (var i = 0; i < list.Count; i++)
{
var kvp = list[i];
if (kvp.Value != null) ProcessClip(kvp.Value, remap);
}
ProcessRuntimeAnimatorController(aoc.runtimeAnimatorController, remap);
}
}
private void ProcessMenuInstaller(ModularAvatarMenuInstaller installer,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
@ -525,113 +515,70 @@ namespace nadena.dev.modular_avatar.core.editor
});
}
private void ProcessAnimator(AnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessVirtualAnimatorController(VirtualAnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap)
{
if (remaps.IsEmpty) return;
foreach (var node in controller.AllReachableNodes())
{
switch (node)
{
case VirtualStateMachine vsm: ProcessStateMachine(vsm, remap); break;
case VirtualState vs: ProcessState(vs, remap); break;
case VirtualTransitionBase vt: ProcessTransition(vt, remap); break;
case VirtualClip vc: ProcessClip(vc, remap); break;
case VirtualBlendTree bt: ProcessBlendtree(bt, remap); break;
}
}
var newParameters = controller.Parameters.Clear();
foreach (var (name, parameter) in controller.Parameters)
{
if (remap.TryGetValue((ParameterNamespace.Animator, name), out var newParam))
{
newParameters = newParameters.Add(newParam.ParameterName, parameter);
}
else
{
newParameters = newParameters.Add(name, parameter);
}
}
var visited = new HashSet<AnimatorStateMachine>();
var queue = new Queue<AnimatorStateMachine>();
var parameters = controller.parameters;
for (int i = 0; i < parameters.Length; i++)
{
if (remaps.TryGetValue((ParameterNamespace.Animator, parameters[i].name), out var newName))
{
parameters[i].name = newName.ParameterName;
}
}
controller.parameters = parameters;
foreach (var layer in controller.layers)
{
if (layer.stateMachine != null)
{
queue.Enqueue(layer.stateMachine);
}
}
Profiler.BeginSample("Walk animator graph");
while (queue.Count > 0)
{
var sm = queue.Dequeue();
if (visited.Contains(sm)) continue;
visited.Add(sm);
foreach (var behavior in sm.behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
foreach (var t in sm.anyStateTransitions)
{
ProcessTransition(t, remaps);
}
foreach (var t in sm.entryTransitions)
{
ProcessTransition(t, remaps);
}
foreach (var sub in sm.stateMachines)
{
queue.Enqueue(sub.stateMachine);
foreach (var t in sm.GetStateMachineTransitions(sub.stateMachine))
{
ProcessTransition(t, remaps);
}
}
foreach (var st in sm.states)
{
ProcessState(st.state, remaps);
}
}
Profiler.EndSample();
controller.Parameters = newParameters;
}
private void ProcessState(AnimatorState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessStateMachine(VirtualStateMachine vsm,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
state.mirrorParameter = remap(remaps, state.mirrorParameter);
state.timeParameter = remap(remaps, state.timeParameter);
state.speedParameter = remap(remaps, state.speedParameter);
state.cycleOffsetParameter = remap(remaps, state.cycleOffsetParameter);
foreach (var t in state.transitions)
{
ProcessTransition(t, remaps);
}
foreach (var behavior in state.behaviours)
foreach (var behavior in vsm.Behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
ProcessMotion(state.motion, remaps);
}
private void ProcessMotion(Motion motion,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessState(VirtualState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
if (motion is BlendTree blendTree) ProcessBlendtree(blendTree, remaps);
state.MirrorParameter = remap(remaps, state.MirrorParameter);
state.TimeParameter = remap(remaps, state.TimeParameter);
state.SpeedParameter = remap(remaps, state.SpeedParameter);
state.CycleOffsetParameter = remap(remaps, state.CycleOffsetParameter);
if (motion is AnimationClip clip) ProcessClip(clip, remaps);
foreach (var behavior in state.Behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
}
private void ProcessClip(AnimationClip clip,
private void ProcessClip(VirtualClip clip,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
var curveBindings = AnimationUtility.GetCurveBindings(clip);
var curveBindings = clip.GetFloatCurveBindings();
var bindingsToUpdate = new List<EditorCurveBinding>();
var newCurves = new List<AnimationCurve>();
@ -641,48 +588,30 @@ namespace nadena.dev.modular_avatar.core.editor
if (binding.path != "" || binding.type != typeof(Animator)) continue;
if (remaps.TryGetValue((ParameterNamespace.Animator, binding.propertyName), out var newBinding))
{
var curCurve = AnimationUtility.GetEditorCurve(clip, binding);
bindingsToUpdate.Add(binding);
newCurves.Add(null);
bindingsToUpdate.Add(new EditorCurveBinding
var curCurve = clip.GetFloatCurve(binding);
var newECB = new EditorCurveBinding
{
path = "",
type = typeof(Animator),
propertyName = newBinding.ParameterName
});
newCurves.Add(curCurve);
};
clip.SetFloatCurve(binding, null);
clip.SetFloatCurve(newECB, curCurve);
}
}
if (bindingsToUpdate.Any())
{
AnimationUtility.SetEditorCurves(clip, bindingsToUpdate.ToArray(), newCurves.ToArray());
// Workaround apparent unity bug where the clip's curves are not deleted
for (var i = 0; i < bindingsToUpdate.Count; i++)
if (newCurves[i] == null && AnimationUtility.GetEditorCurve(clip, bindingsToUpdate[i]) != null)
AnimationUtility.SetEditorCurve(clip, bindingsToUpdate[i], newCurves[i]);
}
}
private void ProcessBlendtree(BlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessBlendtree(VirtualBlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
blendTree.blendParameter = remap(remaps, blendTree.blendParameter);
blendTree.blendParameterY = remap(remaps, blendTree.blendParameterY);
blendTree.BlendParameter = remap(remaps, blendTree.BlendParameter);
blendTree.BlendParameterY = remap(remaps, blendTree.BlendParameterY);
var children = blendTree.children;
for (int i = 0; i < children.Length; i++)
var children = blendTree.Children;
foreach (var child in children)
{
var childMotion = children[i];
ProcessMotion(childMotion.motion, remaps);
childMotion.directBlendParameter = remap(remaps, childMotion.directBlendParameter);
children[i] = childMotion;
child.DirectBlendParameter = remap(remaps, child.DirectBlendParameter);
}
blendTree.children = children;
}
private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
@ -698,19 +627,17 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private void ProcessTransition(AnimatorTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessTransition(VirtualTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
bool dirty = false;
var conditions = t.conditions;
for (int i = 0; i < conditions.Length; i++)
{
var cond = conditions[i];
cond.parameter = remap(remaps, cond.parameter, ref dirty);
conditions[i] = cond;
}
if (dirty) t.conditions = conditions;
var conditions = t.Conditions
.Select(cond =>
{
cond.parameter = remap(remaps, cond.parameter, ref dirty);
return cond;
})
.ToImmutableList();
t.Conditions = conditions;
}
private ImmutableDictionary<string, ParameterInfo> CollectParameters(ModularAvatarParameters p,

View File

@ -1,14 +1,15 @@
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 Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
using UnityObject = UnityEngine.Object;
using UnityObject = Object;
// ReSharper disable once RedundantUsingDirective
using Object = System.Object;
@ -128,7 +129,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
_buildContext.Extension<AnimationServicesContext>().PathMappings
_buildContext.Extension<AnimatorServicesContext>().ObjectPathRemapper
.ReplaceObject(original, replacement);
// Destroy original

View File

@ -56,13 +56,16 @@ namespace nadena.dev.modular_avatar.core.editor
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
{
var scaleAdjusters = ctx.GetComponentsByType<ModularAvatarScaleAdjuster>();
var avatarToRenderer =
new Dictionary<GameObject, HashSet<Renderer>>();
foreach (var root in ctx.GetAvatarRoots())
{
if (ctx.ActiveInHierarchy(root) is false)
{
continue;
}
if (ctx.GetComponentsInChildren<ModularAvatarScaleAdjuster>(root, true).Length == 0)
{
continue;

View File

@ -0,0 +1,118 @@
#nullable enable
using System;
using System.Collections.Specialized;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.ModularAvatarSyncParameterSequence;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
public class SyncParameterSequencePass : Pass<SyncParameterSequencePass>
{
private static Platform? CurrentPlatform
{
get
{
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.Android: return Platform.Android;
case BuildTarget.iOS: return Platform.iOS;
case BuildTarget.StandaloneWindows64: return Platform.PC;
case BuildTarget.StandaloneLinux64: return Platform.PC; // for CI
default: return null;
}
}
}
protected override void Execute(ndmf.BuildContext context)
{
ExecuteStatic(context);
}
internal static void ExecuteStatic(ndmf.BuildContext context)
{
var avDesc = context.AvatarDescriptor;
var components = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarSyncParameterSequence>(true);
if (components.Length == 0) return;
if (components.Length > 1)
{
BuildReport.LogFatal("error.singleton", "Sync Parameter Sequence", components.Cast<object>().ToArray());
return;
}
var syncComponent = components[0];
if (syncComponent.Parameters == null) return;
if (avDesc.expressionParameters == null) return;
var avatarParams = avDesc.expressionParameters;
if (!context.IsTemporaryAsset(avatarParams))
{
avatarParams = Object.Instantiate(avatarParams);
avDesc.expressionParameters = avatarParams;
}
if (syncComponent.Parameters.parameters == null)
{
syncComponent.Parameters.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
EditorUtility.SetDirty(syncComponent.Parameters);
}
// If we're on the primary platform, add in any unknown parameters, and prune if we exceed the limit.
if (CurrentPlatform != null && CurrentPlatform == syncComponent.PrimaryPlatform)
{
var registered = new OrderedDictionary();
foreach (var param in syncComponent.Parameters.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
registered[param.name] = param;
}
foreach (var param in avatarParams.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
registered[param.name] = param;
}
syncComponent.Parameters.parameters = registered.Values.Cast<VRCExpressionParameters.Parameter>().ToArray();
if (!syncComponent.Parameters.IsWithinBudget())
{
var knownParams = avatarParams.parameters.Where(p => p != null).Select(p => p.name).ToHashSet();
syncComponent.Parameters.parameters = syncComponent.Parameters.parameters.Where(
p => p != null && knownParams.Contains(p.name)
).ToArray();
}
EditorUtility.SetDirty(syncComponent.Parameters);
}
// Now copy back...
OrderedDictionary finalParams = new();
foreach (var param in syncComponent.Parameters.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
finalParams[param.name] = param;
}
foreach (var param in avatarParams.parameters)
{
if (param == null) continue;
finalParams[param.name] = param;
}
avatarParams.parameters = finalParams.Values.Cast<VRCExpressionParameters.Parameter>().ToArray();
EditorUtility.SetDirty(avatarParams);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 756425df8aeb4926afceda71bedffa40
timeCreated: 1733011801

3
Editor/VRChat.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 476a3ae35e5046989e0c7276ae860e3c
timeCreated: 1742695540

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
namespace nadena.dev.modular_avatar.core.editor
{
internal class VRChatSettingsPass : Pass<VRChatSettingsPass>
{
protected override void Execute(ndmf.BuildContext context)
{
var settings = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarVRChatSettings>(true);
if (settings.Length > 1)
{
var objects = new List<object>();
objects.Add("MA VRChat Settings");
objects.AddRange(settings);
BuildReport.LogFatal("error.singleton", objects);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cc7a69c0f9aa469dbcf8d4b492f5a6d9
timeCreated: 1742695552

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -164,33 +165,34 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessAnimations()
{
var animdb = _context.AnimationDatabase;
var paths = _context.PathMappings;
var animdb = _context.PluginBuildContext.Extension<AnimatorServicesContext>();
var paths = animdb.ObjectPathRemapper;
Dictionary<string, string> pathMappings = new Dictionary<string, string>();
HashSet<VirtualClip> clips = new();
foreach (var kvp in _boneShims)
{
var orig = paths.GetObjectIdentifier(kvp.Key.gameObject);
var shim = paths.GetObjectIdentifier(kvp.Value.gameObject);
var orig = paths.GetVirtualPathForObject(kvp.Key.gameObject);
var shim = paths.GetVirtualPathForObject(kvp.Value.gameObject);
pathMappings[orig] = shim;
clips.UnionWith(animdb.AnimationIndex.GetClipsForObjectPath(orig));
}
animdb.ForeachClip(motion =>
foreach (var clip in clips)
{
if (!(motion.CurrentClip is AnimationClip clip)) return;
var bindings = AnimationUtility.GetCurveBindings(clip);
foreach (var binding in bindings)
foreach (var binding in clip.GetFloatCurveBindings())
{
if (binding.type != typeof(Transform)) continue;
if (!pathMappings.TryGetValue(binding.path, out var newPath)) continue;
var newBinding = binding;
newBinding.path = newPath;
AnimationUtility.SetEditorCurve(clip, newBinding, AnimationUtility.GetEditorCurve(clip, binding));
if (binding.type == typeof(Transform) && pathMappings.TryGetValue(binding.path, out var newPath))
{
clip.SetFloatCurve(
EditorCurveBinding.FloatCurve(newPath, typeof(Transform), binding.propertyName),
clip.GetFloatCurve(binding)
);
}
}
});
}
}
private Transform CreateShim(Transform target)

View File

@ -1,9 +1,14 @@
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
using VRC.Dynamics;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Dynamics.Constraint.Components;
#else
using UnityEngine.Animations;
#endif
namespace nadena.dev.modular_avatar.core.editor
{
@ -31,17 +36,6 @@ namespace nadena.dev.modular_avatar.core.editor
void Process(ModularAvatarWorldFixedObject target)
{
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
case BuildTarget.StandaloneLinux64: // for CI
break;
default:
BuildReport.Log(ErrorSeverity.NonFatal, "world_fixed_object.err.unsupported_platform");
return;
}
var retargeter = new ActiveAnimationRetargeter(
_context,
new BoneDatabase(),
@ -86,7 +80,30 @@ namespace nadena.dev.modular_avatar.core.editor
obj.transform.localRotation = Quaternion.identity;
obj.transform.localScale = Vector3.one;
var constraint = obj.AddComponent<ParentConstraint>();
CreateConstraint(obj, fixedGameObject);
_proxy = obj.transform;
return obj.transform;
}
#if MA_VRCSDK3_AVATARS
private static void CreateConstraint(GameObject target, GameObject fixedGameObject)
{
var constraint = target.AddComponent<VRCParentConstraint>();
constraint.Sources.Add(new VRCConstraintSource
{
Weight = 1.0f,
SourceTransform = fixedGameObject.transform,
ParentRotationOffset = Vector3.zero,
ParentPositionOffset = Vector3.zero
});
constraint.IsActive = true;
constraint.Locked = true;
}
#else
private static void CreateConstraint(GameObject target, GameObject fixedGameObject)
{
var constraint = target.AddComponent<ParentConstraint>();
constraint.AddSource(new ConstraintSource()
{
weight = 1.0f,
@ -96,10 +113,7 @@ namespace nadena.dev.modular_avatar.core.editor
constraint.locked = true;
constraint.rotationOffsets = new[] {Vector3.zero};
constraint.translationOffsets = new[] {Vector3.zero};
_proxy = obj.transform;
return obj.transform;
}
#endif
}
}

View File

@ -0,0 +1,46 @@
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.Dynamics;
using VRC.SDK3.Dynamics.Constraint.Components;
#else
using UnityEngine.Animations;
#endif
namespace nadena.dev.modular_avatar.core.editor
{
internal class WorldScaleObjectPass : Pass<WorldScaleObjectPass>
{
protected override void Execute(ndmf.BuildContext context)
{
var fixedPrefab =
AssetDatabase.LoadAssetAtPath<GameObject>(
"Packages/nadena.dev.modular-avatar/Assets/FixedPrefab.prefab"
);
var targets = context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarWorldScaleObject>(true);
foreach (var target in targets)
{
BuildReport.ReportingObject(target, () =>
{
#if MA_VRCSDK3_AVATARS
var c = target.gameObject.AddComponent<VRCScaleConstraint>();
c.Sources.Add(new VRCConstraintSource(fixedPrefab.transform, 1));
c.Locked = true;
c.IsActive = true;
#else
var c = target.gameObject.AddComponent<ScaleConstraint>();
c.AddSource(new ConstraintSource() {sourceTransform = fixedPrefab.transform, weight = 1});
c.locked = true;
c.constraintActive = true;
#endif
Object.DestroyImmediate(target);
});
}
}
}
}

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