Compare commits

...

83 Commits
1.11.6 ... main

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
242 changed files with 14797 additions and 4047 deletions

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

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

View File

@ -4,7 +4,7 @@
"version": "3.7.4"
},
"nadena.dev.ndmf": {
"version": "1.6.0"
"version": "1.7.2-rc.0"
}
},
"locked": {
@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.6.0"
"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@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda
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@0efc272496735521e97d22ba9caa750c2781e257
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

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
{
_context.AssetSaver.SaveAsset(curClip);
}
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))
{
context.AssetSaver.SaveAsset(_combined);
}
_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)
{
_context.AssetSaver.SaveAsset(newTransition);
}
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,296 +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 BuildContext _context;
private bool _isSaved;
private UnityObject _combined;
public AnimatorOverrideController OverrideController { get; set; }
public DeepClone(BuildContext context)
{
_context = 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>();
using var scope = _context.OpenSerializationScope();
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))
{
scope.SaveAsset(obj);
}
return (T)obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
obj = UnityObject.Instantiate(original);
}
else
{
obj = (T)ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
ObjectRegistry.RegisterReplacedObject(original, obj);
if (_isSaved)
{
scope.SaveAsset(obj);
}
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)
{
_context.AssetSaver.SaveAsset(newClip);
}
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

@ -4,10 +4,7 @@ using VRC.SDK3.Avatars.ScriptableObjects;
#endif
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
@ -23,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 Object AssetContainer => PluginBuildContext.AssetContainer;
private bool SaveImmediate = false;
#if MA_VRCSDK3_AVATARS
@ -71,61 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
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))
@ -120,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)
{
@ -133,10 +141,14 @@ namespace nadena.dev.modular_avatar.core.editor
return original;
}
var newRatio = Math.Min(256f / original.width, 256f / original.height);
var newRatio = Math.Min(1, Math.Min(256f / original.width, 256f / original.height));
var newWidth = Math.Min(256, Mathf.RoundToInt(original.width * newRatio));
var newHeight = Math.Min(256, Mathf.RoundToInt(original.height * newRatio));
// Round up to a multiple of four
newWidth = (newWidth + 3) & ~3;
newHeight = (newHeight + 3) & ~3;
var newTex = new Texture2D(newWidth, newHeight, TextureFormat.RGBA32, true);
context.SaveAsset(newTex);

View File

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

@ -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;
@ -9,39 +12,170 @@ namespace nadena.dev.modular_avatar.core.editor
internal static class ToggleCreatorShortcut
{
[MenuItem(UnityMenuItems.GameObject_CreateToggleForSelection, false, UnityMenuItems.GameObject_CreateToggleForSelectionOrder)]
private static void CreateToggleForSelection(MenuCommand command) => CreateToggleImpl(command, true);
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(MenuCommand command) => CreateToggleImpl(command, false);
private static void CreateToggleImpl(MenuCommand command, bool forSelection)
private static void CreateToggle()
{
var selected = command.context as GameObject;
if (selected == null) return;
var selections = Selection.objects.OfType<GameObject>();
if (selections.Count() == 0) return;
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
foreach (var selected in selections)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
bool createInstaller = true;
Transform parent = avatarRoot;
var parent = avatarRoot.gameObject;
var createInstaller = true;
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 name = forSelection ? selected.name + " Toggle" : "New Toggle";
var mami = submenu.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control
{
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
name = submenuname,
};
submenu.AddComponent<ModularAvatarMenuInstaller>();
Selection.activeGameObject = submenu;
EditorGUIUtility.PingObject(submenu);
Undo.RegisterCreatedObjectUndo(submenu, "Create SubMenu");
return mami;
}
private static void CreateToggleImpl(IEnumerable<GameObject> selections, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
foreach (var selected in selections)
{
CreateToggleImpl(selected, parent, forSelection, createInstaller);
}
}
private static void CreateToggleImpl(GameObject selected, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var suffix = selected.activeSelf ? "OFF" : "ON";
var name = forSelection ? $"{selected.name} {suffix}" : "New Toggle";
var toggle = new GameObject(name);
@ -57,7 +191,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
toggle.transform.SetParent(parent, false);
toggle.transform.SetParent(parent.transform, false);
var mami = toggle.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();

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,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",
@ -154,6 +157,8 @@
"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",
@ -299,6 +304,7 @@
"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"
"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": "相対的パスのルート",
@ -150,6 +153,8 @@
"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がありますが、メッシュがありません。",
@ -278,6 +283,8 @@
"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": "頂点カラーを削除しない",
@ -287,5 +294,6 @@
"sync-param-sequence.parameters": "共用パラメーターアセット",
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
"sync-param-sequence.create-asset": "新規作成",
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します"
"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

@ -14,7 +14,7 @@
"menuinstall.menu_icon_too_large": "菜单图标过大,图标应小于 256 像素。",
"menuinstall.menu_icon_uncompressed": "菜单图标未设置压缩。",
"menuinstall.srcmenu": "要安装的菜单",
"params.syncmode.NotSynced": "仅 Animator(不同步)",
"params.syncmode.NotSynced": "仅 Animator 内部(不同步)",
"params.syncmode.Int": "Int",
"params.syncmode.Float": "Float",
"params.syncmode.Bool": "Bool",
@ -65,7 +65,7 @@
"merge_armature.mangle_names": "避免名称冲突",
"merge_armature.mangle_names.tooltip": "通过重命名新添加的骨骼来避免与其他资源发生名称冲突。",
"path_mode.Relative": "相对路径(基于当前对象)",
"path_mode.Absolute": "绝对路径(基于 Avatar 的 Root",
"path_mode.Absolute": "绝对路径(基于 Avatar 的根对象",
"merge_animator.animator": "要合并的 Animator",
"merge_animator.layer_type": "Layer 类型",
"merge_animator.delete_attached_animator": "删除额外的 Animator",
@ -93,7 +93,6 @@
"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.relative_path_root": "相对路径根对象",
@ -128,7 +127,7 @@
"mesh_settings.inherit_mode.Inherit": "继承",
"mesh_settings.inherit_mode.Set": "指定",
"mesh_settings.inherit_mode.DontSet": "不指定(保持原有的设置)",
"mesh_settings.inherit_mode.SetOrInherit": "父级设置优先,否则遵从指定设置",
"mesh_settings.inherit_mode.SetOrInherit": "父级设置优先,否则遵从指定设置",
"pb_blocker.help": "当前对象不会受到附加在父对象的 PhysBones 影响。",
"hint.bad_vrcsdk": "检测到不兼容的 VRCSDK 版本。\n\n请尝试升级 VRCSDK如果这不起作用请尝试更新到新版本的 Modular Avatar。",
"error.stack_trace": "Stack trace请在报告错误时提供此信息",
@ -244,7 +243,7 @@
"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_hips": "你的 Avatar 没有 Hips 骨骼。Setup Outfit 只能用于人形 (humanoid) 的 Avatar。",
"setup_outfit.err.no_outfit_hips": "无法识别服装的 Hips 骨骼,已尝试搜索包含以下名称的对象:",
"move_independently.group-header": "要一起移动的对象",
"scale_adjuster.scale": "调整比例",

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

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

@ -89,8 +89,8 @@ namespace nadena.dev.modular_avatar.core.editor
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);
if (updatedAspects.HasFlag(RenderAspects.Mesh)) return Task.FromResult<IRenderFilterNode>(null!);
if (_theMesh == null) return Task.FromResult<IRenderFilterNode>(null!);
return Task.FromResult<IRenderFilterNode>(this);
}

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);
@ -240,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
{
@ -254,8 +254,8 @@ namespace nadena.dev.modular_avatar.core.editor
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()
{
@ -301,6 +316,12 @@ namespace nadena.dev.modular_avatar.core.editor
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);
@ -308,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"
@ -333,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>)>();
@ -363,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)
{
@ -407,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));
@ -488,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>();
@ -574,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);
}
@ -586,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

@ -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;
@ -497,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)
{
@ -537,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>();
@ -653,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)
@ -710,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

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 19a3b07a9eeb413887792469f344c34d
timeCreated: 1741657804

View File

@ -29,7 +29,8 @@
"VRC.SDK3.Dynamics.Contact.Editor.dll",
"VRC.SDK3.Dynamics.PhysBone.dll",
"VRC.SDK3.Dynamics.PhysBone.Editor.dll",
"VRCCore-Editor.dll"
"VRCCore-Editor.dll",
"VRC.SDK3.Dynamics.Constraint.dll"
],
"autoReferenced": false,
"defineConstraints": [],

View File

@ -1,59 +0,0 @@
{
"name": "nadena.dev.modular-avatar.core.editor",
"rootNamespace": "",
"references": [
"nadena.dev.modular-avatar.core",
"VRC.SDK3A",
"VRC.SDKBase",
"nadena.dev.ndmf",
"nadena.dev.ndmf.vrchat",
"nadena.dev.ndmf.runtime",
"VRC.SDK3A.Editor",
"Unity.Burst"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"Newtonsoft.Json.dll",
"System.Collections.Immutable.dll",
"VRCSDKBase.dll",
"VRCSDKBase-Editor.dll",
"VRCSDK3A.dll",
"VRCSDK3A-Editor.dll",
"VRC.Dynamics.dll",
"VRC.SDK3.Dynamics.Contact.dll",
"VRC.SDK3.Dynamics.Contact.Editor.dll",
"VRC.SDK3.Dynamics.PhysBone.dll",
"VRC.SDK3.Dynamics.PhysBone.Editor.dll",
"VRCCore-Editor.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.anatawa12.avatar-optimizer",
"expression": "(,1.5.0-rc.8)",
"define": "LEGACY_AVATAR_OPTIMIZER"
},
{
"name": "com.vrchat.avatars",
"expression": "",
"define": "MA_VRCSDK3_AVATARS"
},
{
"name": "com.vrchat.avatars",
"expression": "3.5.2",
"define": "MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER"
},
{
"name": "com.vrchat.avatars",
"expression": "3.7.0-beta.2",
"define": "MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER"
}
],
"noEngineReferences": false
}

View File

@ -23,9 +23,9 @@
*/
using System;
using nadena.dev.ndmf;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDKBase;
#endif
namespace nadena.dev.modular_avatar.core
@ -35,7 +35,7 @@ namespace nadena.dev.modular_avatar.core
/// inherited by user classes, and will be removed in Modular Avatar 2.0.
/// </summary>
[DefaultExecutionOrder(-9999)] // run before av3emu
public abstract class AvatarTagComponent : MonoBehaviour, IEditorOnly
public abstract class AvatarTagComponent : MonoBehaviour, INDMFEditorOnly
{
internal static event Action OnChangeAction;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -16,8 +16,6 @@ namespace nadena.dev.modular_avatar.core
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/move-independently?lang=auto")]
class MAMoveIndependently : MonoBehaviour, IEditorOnly
{
private float EPSILON = 0.0000001f;
[SerializeField]
private GameObject[] m_groupedBones;

View File

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA MMD Layer Control")]
[DisallowMultipleComponent]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/mmd-layer-control?lang=auto")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
// ReSharper disable once RequiredBaseTypesIsNotInherited (false positive)
public sealed class ModularAvatarMMDLayerControl : StateMachineBehaviour
{
[SerializeField] internal bool m_DisableInMMDMode;
[PublicAPI]
public bool DisableInMMDMode
{
get => m_DisableInMMDMode;
set => m_DisableInMMDMode = value;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d1d979d3cedd4ddd969f414e2ea04fb8
timeCreated: 1741836107

View File

@ -25,6 +25,7 @@
#if MA_VRCSDK3_AVATARS
using System;
using nadena.dev.ndmf.animator;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -36,10 +37,19 @@ namespace nadena.dev.modular_avatar.core
Absolute
}
public enum MergeAnimatorMode
{
Append,
Replace
}
[AddComponentMenu("Modular Avatar/MA Merge Animator")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-animator?lang=auto")]
public class ModularAvatarMergeAnimator : AvatarTagComponent
public class ModularAvatarMergeAnimator : AvatarTagComponent, IVirtualizeAnimatorController
{
internal static Func<ModularAvatarMergeAnimator, object, string> GetMotionBasePathCallback =
(_, _) => "";
public RuntimeAnimatorController animator;
public VRCAvatarDescriptor.AnimLayerType layerType = VRCAvatarDescriptor.AnimLayerType.FX;
public bool deleteAttachedAnimator;
@ -47,7 +57,8 @@ namespace nadena.dev.modular_avatar.core
public bool matchAvatarWriteDefaults;
public AvatarObjectReference relativePathRoot = new AvatarObjectReference();
public int layerPriority = 0;
public MergeAnimatorMode mergeAnimatorMode = MergeAnimatorMode.Append;
public override void ResolveReferences()
{
// no-op
@ -67,6 +78,22 @@ namespace nadena.dev.modular_avatar.core
{
deleteAttachedAnimator = true;
}
RuntimeAnimatorController IVirtualizeAnimatorController.AnimatorController
{
get => animator;
set => animator = value;
}
string IVirtualizeAnimatorController.GetMotionBasePath(object ndmfBuildContext, bool clearPath)
{
var path = GetMotionBasePathCallback(this, ndmfBuildContext);
if (clearPath) pathMode = MergeAnimatorPathMode.Absolute;
return path;
}
object IVirtualizeAnimatorController.TargetControllerKey => layerType;
}
}

View File

@ -1,17 +1,61 @@
#if MA_VRCSDK3_AVATARS
using System;
using nadena.dev.ndmf.animator;
using JetBrains.Annotations;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Merge Blend Tree")]
[AddComponentMenu("Modular Avatar/MA Merge Motion (Blend Tree)")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-blend-tree?lang=auto")]
public sealed class ModularAvatarMergeBlendTree : AvatarTagComponent
public sealed class ModularAvatarMergeBlendTree : AvatarTagComponent, IVirtualizeMotion
{
// We can't actually reference a BlendTree here because it's not available when building a player build
public UnityEngine.Object BlendTree;
internal static Func<ModularAvatarMergeBlendTree, object, string> GetMotionBasePathCallback
= (_, _) => "";
// Previous versions of this component expected a BlendTree, which is not available in player builds, so this
// field was made an Object. This can now become a Motion, but unfortunately that would be a breaking change.
/// <summary>
/// The blend tree or other motion to merge.
/// </summary>
[Obsolete("Use Motion property instead; this field will be removed in 2.0")] [PublicAPI]
public Object BlendTree;
[PublicAPI]
public MergeAnimatorPathMode PathMode = MergeAnimatorPathMode.Relative;
[PublicAPI]
public AvatarObjectReference RelativePathRoot = new AvatarObjectReference();
[PublicAPI]
public Motion Motion
{
get => ((IVirtualizeMotion)this).Motion;
set => ((IVirtualizeMotion)this).Motion = value;
}
Motion IVirtualizeMotion.Motion
{
#pragma warning disable CS0618 // Type or member is obsolete
get => (Motion)BlendTree;
set => BlendTree = value;
#pragma warning restore CS0618 // Type or member is obsolete
}
string IVirtualizeMotion.GetMotionBasePath(object ndmfBuildContext, bool clearPath)
{
var path = GetMotionBasePathCallback(this, ndmfBuildContext);
if (clearPath)
{
PathMode = MergeAnimatorPathMode.Absolute;
}
return path;
}
}
}

View File

@ -0,0 +1,20 @@
using JetBrains.Annotations;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA VRChat Settings")]
[DisallowMultipleComponent]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/vrchat-settings?lang=auto")]
public class ModularAvatarVRChatSettings : AvatarTagComponent
{
[SerializeField] internal bool m_mmdWorldSupport = true;
[PublicAPI]
public bool MMDWorldSupport
{
get => m_mmdWorldSupport;
set => m_mmdWorldSupport = value;
}
}
}

View File

@ -1,11 +1,11 @@
fileFormatVersion: 2
guid: d86c7d257d78fff4d8fdf56e2954a5c9
guid: 89c938d7d8a741df99f2eda501b3a6fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA World Scale Object")]
[DisallowMultipleComponent]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/world-scale-object?lang=auto")]
public class ModularAvatarWorldScaleObject : AvatarTagComponent
{
// no configuration
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e113c01563a14226b5e863befe6fe769
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +1,14 @@
#if MA_VRCSDK3_AVATARS
using System.Linq;
using modular_avatar_tests;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf.animator;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
public class ActiveAnimationRetargeterTests : TestBase
@ -17,8 +20,7 @@ public class ActiveAnimationRetargeterTests : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
var pathMappings = buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>()
.PathMappings;
var asc = buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
// get game objects
var changedChild = avatar.transform.Find("Toggled/Child");
@ -29,18 +31,16 @@ public class ActiveAnimationRetargeterTests : TestBase
var created = retargeter.CreateIntermediateObjects(newParent.gameObject);
retargeter.FixupAnimations();
// commit
buildContext.AnimationDatabase.Commit();
var clip = findFxClip(avatar, layerName: "retarget");
var curveBindings = AnimationUtility.GetCurveBindings(clip);
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX]!;
var clip = (VirtualClip) fx.Layers.First(l => l.Name == "retarget").StateMachine.DefaultState!.Motion;
var curveBindings = clip!.GetFloatCurveBindings();
// Intermediate object must be created
Assert.That(created, Is.Not.EqualTo(newParent.gameObject));
// The created animation must have m_IsActive of intermediate object
Assert.That(curveBindings, Does.Contain(EditorCurveBinding.FloatCurve(
pathMappings.GetObjectIdentifier(created), typeof(GameObject), "m_IsActive")));
asc.ObjectPathRemapper.GetVirtualPathForObject(created), typeof(GameObject), "m_IsActive")));
}
}

View File

@ -1,58 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using modular_avatar_tests;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEditor.Animations;
using UnityEngine;
namespace _ModularAvatar.EditModeTests
{
public class AnimationDatabaseCloningTest : TestBase
{
[Test]
public void TestAnimationDatabaseCloningLogic()
{
var root = CreateRoot("root");
var context = CreateContext(root);
var origController = LoadAsset<AnimatorController>("ac.controller");
var state = origController.layers[0].stateMachine.defaultState;
var clonedState = Object.Instantiate(state);
var origAnimation = LoadAsset<AnimationClip>("anim.anim");
using (new ObjectRegistryScope(new ObjectRegistry(root.transform)))
{
var db = new AnimationDatabase();
db.OnActivate(context);
db.RegisterState(clonedState);
var newBlendTree = clonedState.motion as BlendTree;
var origBlendTree = state.motion as BlendTree;
Assert.NotNull(newBlendTree);
Assert.NotNull(origBlendTree);
Assert.AreNotSame(newBlendTree, origBlendTree);
Assert.AreNotSame(newBlendTree.children[1].motion, origBlendTree.children[1].motion);
// Before commit, proxy animations are replaced.
Assert.AreNotSame(newBlendTree.children[0].motion, origBlendTree.children[0].motion);
Assert.AreSame(ObjectRegistry.GetReference(origAnimation),
ObjectRegistry.GetReference(newBlendTree.children[1].motion));
db.Commit();
Assert.AreNotSame(newBlendTree, origBlendTree);
Assert.AreNotSame(newBlendTree.children[1].motion, origBlendTree.children[1].motion);
// After commit, proxy animations are restored to the original assets.
Assert.AreSame(newBlendTree.children[0].motion, origBlendTree.children[0].motion);
}
}
}
}

View File

@ -108,9 +108,10 @@ namespace modular_avatar_tests
Assert.Greater(animRootIndex, parentIndex);
Assert.Greater(bodyIndex, animRootIndex);
// Body is still enabled; the injected parent and parent/anim-root are not
// Body is still enabled; the injected parent is not. anim-root should be enabled, since the test mask has
// the root element enabled.
Assert.IsTrue(state.transformMaskElements[parentIndex].Item2 < 0.5f);
Assert.IsTrue(state.transformMaskElements[animRootIndex].Item2 < 0.5f);
Assert.IsTrue(state.transformMaskElements[animRootIndex].Item2 > 0.5f);
Assert.IsTrue(state.transformMaskElements[bodyIndex].Item2 > 0.5f);
// Original paths are removed

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