Compare commits

..

5 Commits
main ... 1.11.7

Author SHA1 Message Date
bd_
a18a915288 1.11.7 2025-03-08 21:27:14 -08:00
bd_
419f34baaa
fix: incorrect initial states for Shape Changer (#1463) 2025-03-02 18:20:18 -08:00
nadena-dev-ci
6768f3ef91
New Crowdin updates (#1409)
* New translations en-us.json (Chinese Traditional)

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

* New translations en-us.json (Chinese Simplified)
2025-03-02 17:43:01 -08:00
bd_
0ee8ec6385 1.11.6 2025-02-19 18:26:46 -08:00
bd_
930d25a4f6
fix: incorrect initial state for inverted Shape Changers (#1457) 2025-02-19 18:26:06 -08:00
255 changed files with 4035 additions and 16244 deletions

View File

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

View File

@ -4,7 +4,7 @@
"version": "3.7.4"
},
"nadena.dev.ndmf": {
"version": "1.8.0-alpha.4"
"version": "1.6.0"
}
},
"locked": {
@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.8.0-alpha.4"
"version": "1.6.0"
}
}
}

View File

@ -1,71 +0,0 @@
#!/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: $!";

View File

@ -1,22 +0,0 @@
#!/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;
}

91
.github/workflows/build-release.yml vendored Normal file
View File

@ -0,0 +1,91 @@
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,11 +23,6 @@ on:
description: 'build the latest release'
type: boolean
required: false
prerelease:
description: 'use prerelease changelog'
type: boolean
required: false
default: true
jobs:
build-docs:
@ -72,18 +67,6 @@ 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~

View File

@ -1,58 +0,0 @@
# 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,13 +40,11 @@ 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)
@ -55,7 +53,6 @@ jobs:
ref: main
path: dev
artifact: docs-dev
prerelease: true
deploy-docs:
name: Deploy documentation
@ -125,7 +122,7 @@ jobs:
workingDirectory: docs-site~
- name: Purge cache
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
uses: nathanvaughn/actions-cloudflare-purge@0efc272496735521e97d22ba9caa750c2781e257
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

View File

@ -1,231 +0,0 @@
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@v2
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

View File

@ -1,129 +0,0 @@
# 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.13.0-alpha.1] - [2025-04-10]
### Fixed
- [#1552] Merge Blend Treeにて、メインアバターFXレイヤーと同じ名前のintやboolパラメーターがBlend Treeに含まれている場合、
パラメーター型が修正されない問題を修正
- [#1553] リアクティブコンポーネントが生成するステートに、WD設定が正しくない問題を修正
### Changed
- [#1551] Merge Animatorは、遷移のない単一のstateを持つブレンドツリーのレイヤーに対して常にWDをONに設定します。
- 一部、以前の挙動に依存したアセットとの互換性を向上させるための変更です。
## [1.13.0-alpha.0] - [2025-04-08]
### Added
- (実験的機能) VRC以外のプラットフォームのサポートを有効化
## [1.12.3] - [2025-04-05]
### Fixed
- Additiveレイヤーの問題を修正NDMFバージョンアップグレードによって修正
### Changed
- [#1542] Merge Animatorは、アニメーションクリップを含む単一のstateを持つレイヤーに対してWD設定を一致させるが、
  ブレンドツリーを含む場合は一致させないように変更されました。
- これにより、1.12で導入された互換性の問題が修正されます1.12.0では、単一のstateアニメーションクリップに対してWD設定
と一致しないように変更されました)。
## [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

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

View File

@ -1,129 +0,0 @@
# 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.13.0-alpha.1] - [2025-04-10]
### Fixed
- [#1552] Merge Blend Tree failed to correct parameter types when the main avatar FX layer contained an int or bool
parameter with the same name as one used in the blend tree.
- [#1553] Reactive components might generate states with incorrect write default settings
### Changed
- [#1551] Merge Animator will always set WD ON for single-state blendtree layers with no any state transitions.
- This fixes compatibility issues with assets which relied on the prior behavior.
## [1.13.0-alpha.0] - [2025-04-08]
### Added
- (Experimental feature) Enabled support for non-VRC platforms
## [1.12.3] - [2025-04-05]
### Fixed
- Fixed issues with additive layers (via NDMF version upgrade)
### Changed
- [#1542] Merge Animator now will match WD settings for layers with a single state containing an animation clip,
but not if it contains a blend tree. This fixes some compatibility issues introduced in 1.12 (where the behavior
was changed to not match WD settings for single-state animation clips).
## [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

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

View File

@ -1,95 +0,0 @@
# 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
- (実験的機能) VRC以外のプラットフォームのサポートを有効化
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.4] - [2025-04-10]
### Fixed
- [#1552] Merge Blend Treeにて、メインアバターFXレイヤーと同じ名前のintやboolパラメーターがBlend Treeに含まれている場合、
パラメーター型が修正されない問題を修正
- [#1553] リアクティブコンポーネントが生成するステートに、WD設定が正しくない問題を修正
### Changed
- [#1551] Merge Animatorは、遷移のない単一のstateを持つブレンドツリーのレイヤーに対して常にWDをONに設定します。
- 一部、以前の挙動に依存したアセットとの互換性を向上させるための変更です。
## [1.12.3] - [2025-04-05]
### Fixed
- Additiveレイヤーの問題を修正NDMFバージョンアップグレードによって修正
### Changed
- [#1542] Merge Animatorは、アニメーションクリップを含む単一のstateを持つレイヤーに対してWD設定を一致させるが、
  ブレンドツリーを含む場合は一致させないように変更されました。
- これにより、1.12で導入された互換性の問題が修正されます1.12.0では、単一のstateアニメーションクリップに対してWD設定
と一致しないように変更されました)。
## [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

View File

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

View File

@ -1,99 +1 @@
# 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
- (Experimental feature) Enabled support for non-VRC platforms
### Fixed
### Changed
### Removed
### Security
### Deprecated
## [1.12.4] - [2025-04-10]
### Fixed
- [#1552] Merge Blend Tree failed to correct parameter types when the main avatar FX layer contained an int or bool
parameter with the same name as one used in the blend tree.
- [#1553] Reactive components might generate states with incorrect write default settings
### Changed
- [#1551] Merge Animator will always set WD ON for single-state blendtree layers with no any state transitions.
- This fixes compatibility issues with assets which relied on the prior behavior.
## [1.12.3] - [2025-04-05]
### Fixed
- Fixed issues with additive layers (via NDMF version upgrade)
### Changed
- [#1542] Merge Animator now will match WD settings for layers with a single state containing an animation clip,
but not if it contains a blend tree. This fixes some compatibility issues introduced in 1.12 (where the behavior
was changed to not match WD settings for single-state animation clips).
- [#1551] Merge Animator will always set WD ON for single-state blendtree layers with no any state transitions.
## [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
Temporary test release

View File

@ -2,7 +2,6 @@ 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;
@ -17,7 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly BuildContext _context;
private readonly BoneDatabase _boneDatabase;
private readonly AnimatorServicesContext _asc;
private readonly PathMappings _pathMappings;
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
/// <summary>
@ -56,15 +55,15 @@ namespace nadena.dev.modular_avatar.core.editor
{
_context = context;
_boneDatabase = boneDatabase;
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
_pathMappings = context.PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
while (root != null && !RuntimeUtil.IsAvatarRoot(root))
{
var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject);
System.Diagnostics.Debug.Assert(originalPath != null);
if (_asc.AnimationIndex.GetClipsForObjectPath(originalPath).Any(clip =>
GetActiveBinding(clip, originalPath) != null
if (context.AnimationDatabase.ClipsForPath(originalPath).Any(clip =>
GetActiveBinding(clip.CurrentClip as AnimationClip, originalPath) != null
))
{
_intermediateObjs.Add(new IntermediateObj
@ -119,6 +118,7 @@ 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,14 +130,22 @@ namespace nadena.dev.modular_avatar.core.editor
{
var path = intermediate.OriginalPath;
foreach (var clip in _asc.AnimationIndex.GetClipsForObjectPath(path))
foreach (var holder in _context.AnimationDatabase.ClipsForPath(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.SetFloatCurve(_asc.ObjectPathRemapper.GetVirtualPathForObject(mapping), typeof(GameObject), "m_IsActive",
clip.SetCurve(_pathMappings.GetObjectIdentifier(mapping), typeof(GameObject), "m_IsActive",
curve);
}
}
@ -145,9 +153,10 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private AnimationCurve GetActiveBinding(VirtualClip clip, string path)
private AnimationCurve GetActiveBinding(AnimationClip clip, string path)
{
return clip.GetFloatCurve(EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
return AnimationUtility.GetEditorCurve(clip,
EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
}
}
}

View File

@ -0,0 +1,430 @@
#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

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

View File

@ -0,0 +1,125 @@
#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

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

View File

@ -0,0 +1,224 @@
#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

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

View File

@ -0,0 +1,625 @@
/*
* 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

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

View File

@ -0,0 +1,296 @@
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

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

View File

@ -0,0 +1,18 @@
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

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

View File

@ -2,7 +2,6 @@
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;
@ -19,29 +18,14 @@ namespace nadena.dev.modular_avatar.animation
{
protected override void Execute(BuildContext context)
{
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 asc = context.Extension<AnimationServicesContext>();
if (!asc.BoundReadableProperties.Any()) return;
// 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();
var fx = (AnimatorController)context.AvatarDescriptor.baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController;
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";
@ -49,31 +33,48 @@ namespace nadena.dev.modular_avatar.animation
blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false;
blendTree.children = activeProxies
.Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value)))
blendTree.children = asc.BoundReadableProperties
.Select(prop => GenerateDelayChild(nullMotion, prop))
.ToArray();
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
var state = layer.StateMachine.AddState("DelayDisable");
layer.StateMachine.DefaultState = state;
var asm = new AnimatorStateMachine();
var state = new AnimatorState();
state.name = "DelayDisable";
state.motion = blendTree;
state.writeDefaultValues = true;
state.WriteDefaultValues = true;
state.Motion = asc.ControllerContext.Clone(blendTree);
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();
// Ensure the initial state of readable props matches the actual state of the gameobject
foreach (var controller in asc.ControllerContext.GetAllControllers())
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 (binding, prop) in activeProxies)
{
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
var obj = asc.PathMappings.PathToObject(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);
}
if (obj != null && paramToIndex.TryGetValue(prop, out var index))
{
parameters[index].defaultFloat = obj.activeSelf ? 1 : 0;
}
}
fx.parameters = parameters;
}
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)

View File

@ -0,0 +1,17 @@
#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

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

View File

@ -1,248 +0,0 @@
#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();
}
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
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>
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
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

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

View File

@ -0,0 +1,418 @@
#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

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

View File

@ -0,0 +1,147 @@
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

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

View File

@ -1,82 +0,0 @@
#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

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

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor.Animations;
using UnityEngine;
@ -13,7 +12,6 @@ using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
internal class ApplyAnimatorDefaultValuesPass : Pass<ApplyAnimatorDefaultValuesPass>
{
protected override void Execute(ndmf.BuildContext context)
@ -23,34 +21,40 @@ namespace nadena.dev.modular_avatar.core.editor
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
?? ImmutableDictionary<string, float>.Empty;
var asc = context.Extension<AnimatorServicesContext>();
foreach (var controller in asc.ControllerContext.GetAllControllers())
foreach (var layer in context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers))
{
var parameters = controller.Parameters;
foreach (var (name, parameter) in parameters)
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))
{
if (!values.TryGetValue(name, out var defaultValue)) continue;
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
}
switch (parameter.type)
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)
{
case AnimatorControllerParameterType.Bool:
parameter.defaultBool = defaultValue != 0.0f;
parameters[i].defaultBool = defaultValue != 0.0f;
break;
case AnimatorControllerParameterType.Int:
parameter.defaultInt = Mathf.RoundToInt(defaultValue);
parameters[i].defaultInt = Mathf.RoundToInt(defaultValue);
break;
case AnimatorControllerParameterType.Float:
parameter.defaultFloat = defaultValue;
parameters[i].defaultFloat = defaultValue;
break;
default:
continue; // unhandled type, e.g. trigger
}
parameters = parameters.SetItem(name, parameter);
}
controller.Parameters = parameters;
controller.parameters = parameters;
}
}
}

View File

@ -1,14 +1,12 @@
#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
{
@ -19,16 +17,11 @@ namespace nadena.dev.modular_avatar.core.editor
*/
internal class BlendshapeSyncAnimationProcessor
{
private readonly ndmf.BuildContext _context;
private BuildContext _context;
private Dictionary<Motion, Motion> _motionCache;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
internal BlendshapeSyncAnimationProcessor(ndmf.BuildContext context)
{
_context = context;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
}
private struct SummaryBinding : IEquatable<SummaryBinding>
private struct SummaryBinding
{
private const string PREFIX = "blendShape.";
public string path;
@ -40,76 +33,71 @@ 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 null;
return new SummaryBinding();
}
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()
public void OnPreprocessAvatar(BuildContext context)
{
var avatarGameObject = _context.AvatarRootObject;
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
_context = context;
var avatarGameObject = context.AvatarRootObject;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = context.AvatarDescriptor;
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>();
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
if (components.Length == 0) return;
var layers = avatarDescriptor.baseAnimationLayers;
var fxIndex = -1;
AnimatorController controller = null;
for (int i = 0; i < layers.Length; i++)
{
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.FX && !layers[i].isDefault)
{
if (layers[i].animatorController is AnimatorController c && c != null)
{
fxIndex = i;
controller = c;
break;
}
}
}
if (controller == null)
{
// Nothing to do, return
}
foreach (var component in components)
{
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
}
var clips = new HashSet<VirtualClip>();
foreach (var key in _bindingMappings.Keys)
{
var ecb = key.ToEditorCurveBinding();
clips.UnionWith(animDb.GetClipsForBinding(ecb));
}
// Walk and transform all clips
foreach (var clip in clips)
animDb.ForeachClip(clip =>
{
ProcessClip(clip);
}
if (clip.CurrentClip is AnimationClip anim)
{
BuildReport.ReportingObject(clip.CurrentClip,
() => { clip.CurrentClip = TransformMotion(anim); });
}
});
}
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);
@ -118,7 +106,6 @@ 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);
@ -136,20 +123,108 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private void ProcessClip(VirtualClip clip)
Motion TransformMotion(Motion motion)
{
foreach (var binding in clip.GetFloatCurveBindings().ToList())
if (motion == null) return null;
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
switch (motion)
{
var srcBinding = SummaryBinding.FromEditorBinding(binding);
if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
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))
{
continue;
}
var curve = clip.GetFloatCurve(binding);
if (clip == origClip)
{
clip = Object.Instantiate(clip);
}
foreach (var dst in dstBindings)
{
clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
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);
}
}
}

View File

@ -4,7 +4,10 @@ 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;
@ -20,6 +23,14 @@ 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
@ -60,6 +71,61 @@ 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,6 +6,7 @@ using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using VRC.SDK3.Avatars.ScriptableObjects;
using Object = UnityEngine.Object;
@ -43,7 +44,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
var parameters = context.AvatarDescriptor.expressionParameters.parameters
?? Array.Empty<VRCExpressionParameters.Parameter>();
?? new VRCExpressionParameters.Parameter[0];
var parameterNames = parameters.Select(p => p.name).ToImmutableHashSet();
if (!context.PluginBuildContext.IsTemporaryAsset(expressionsMenu))
@ -119,20 +120,11 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
internal static TextureFormat TargetFormat
{
get
{
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.StandaloneWindows64:
return TextureFormat.DXT5;
default:
return TextureFormat.ASTC_4x4;
}
}
}
#if UNITY_ANDROID
private const TextureFormat TargetFormat = TextureFormat.ASTC_4x4;
#else
private const TextureFormat TargetFormat = TextureFormat.DXT5;
#endif
private static Texture2D MaybeScaleIcon(BuildContext context, Texture2D original)
{
@ -141,14 +133,10 @@ namespace nadena.dev.modular_avatar.core.editor
return original;
}
var newRatio = Math.Min(1, Math.Min(256f / original.width, 256f / original.height));
var newRatio = 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

@ -1,30 +0,0 @@
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

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

View File

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

View File

@ -11,12 +11,6 @@ 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
{
@ -26,8 +20,7 @@ namespace nadena.dev.modular_avatar.core.editor
prop_pathMode,
prop_matchAvatarWriteDefaults,
prop_relativePathRoot,
prop_layerPriority,
prop_mergeMode;
prop_layerPriority;
private void OnEnable()
{
@ -41,7 +34,6 @@ 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()
@ -55,12 +47,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
EditorGUILayout.PropertyField(prop_layerPriority, G("merge_animator.layer_priority"));
EditorGUILayout.PropertyField(prop_mergeMode, G("merge_animator.merge_mode"));
using (new EditorGUI.DisabledScope(prop_mergeMode.enumValueIndex == (int)MergeAnimatorMode.Replace))
{
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
G("merge_animator.match_avatar_write_defaults"));
}
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 UnityEngine;
using UnityEditor.Animations;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
@ -15,9 +15,7 @@ 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));
}
@ -26,7 +24,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
serializedObject.Update();
EditorGUILayout.ObjectField(_blendTree, typeof(Motion), G("merge_blend_tree.motion"));
EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), G("merge_blend_tree.blend_tree"));
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
{

View File

@ -5,6 +5,7 @@ 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;
@ -41,7 +42,7 @@ namespace nadena.dev.modular_avatar.core.editor
listView.selectionType = SelectionType.Multiple;
listView.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
if (evt.keyCode == KeyCode.Delete)
{
serializedObject.Update();
@ -65,9 +66,9 @@ namespace nadena.dev.modular_avatar.core.editor
listView.SetSelectionWithoutNotify(indices);
};
}
evt.StopPropagation();
}
evt.StopPropagation();
}, TrickleDown.NoTrickleDown);
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
@ -210,14 +211,18 @@ 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,
localOnly = !parameter.networkSynced,
syncType = pst,
defaultValue = parameter.defaultValue,
saved = parameter.saved,
});
@ -255,4 +260,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif
#endif

View File

@ -2,7 +2,6 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
@ -84,12 +83,8 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
foreach (var elem in root.Query<TextElement>().Build())
{
// Prevent delete keypresses from bubbling up if we're in a text field
elem.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
evt.StopPropagation();
});
// Prevent keypresses from bubbling up
elem.RegisterCallback<KeyDownEvent>(evt => evt.StopPropagation(), TrickleDown.NoTrickleDown);
}
return root;

View File

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

View File

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

View File

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

View File

@ -1,27 +0,0 @@
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

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

View File

@ -78,9 +78,6 @@
"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.",
@ -96,7 +93,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.motion": "Motion (or Blend Tree) to merge",
"merge_blend_tree.blend_tree": "Blend Tree",
"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",
@ -157,8 +154,6 @@
"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",
@ -304,7 +299,6 @@
"sync-param-sequence.parameters": "Common parameters asset",
"sync-param-sequence.parameters.tooltip": "The asset to store common parameters in. Do not use the same Expression Parameters that you have set in your avatar descriptor.",
"sync-param-sequence.create-asset": "New",
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset",
"platform.vrchat.settings.mmd_world_support": "MMD world support"
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset"
}

View File

@ -74,9 +74,6 @@
"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": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
@ -92,7 +89,7 @@
"merge_armature.reset_pos.execute": "実行",
"merge_armature.reset_pos.heuristic_scale": "衣装の全体的なスケールをアバターに合わせる",
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入する時にお勧めです。",
"merge_blend_tree.motion": "結合するモーション(またはブレンドツリー)",
"merge_blend_tree.blend_tree": "ブレンドツリー",
"merge_blend_tree.path_mode": "パスモード",
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
"merge_blend_tree.relative_path_root": "相対的パスのルート",
@ -153,8 +150,6 @@
"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がありますが、メッシュがありません。",
@ -283,8 +278,6 @@
"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": "頂点カラーを削除しない",
@ -294,6 +287,5 @@
"sync-param-sequence.parameters": "共用パラメーターアセット",
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
"sync-param-sequence.create-asset": "新規作成",
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します",
"platform.vrchat.settings.mmd_world_support": "MMDワールド対応"
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します"
}

View File

@ -88,6 +88,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.path_mode": "경로 모드",
"merge_blend_tree.path_mode.tooltip": "애니메이션에서 경로를 해석하는 방법에 대해 설명합니다. 상대 모드를 사용하면 이 오브젝트의 애니메이터에서 애니메이션을 기록할 수 있습니다.",
"merge_blend_tree.relative_path_root": "상대적 경로",

View File

@ -93,6 +93,7 @@
"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": "相对路径根对象",

View File

@ -93,6 +93,7 @@
"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": "相對路徑根物件",

View File

@ -27,41 +27,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf.animator;
using nadena.dev.modular_avatar.animation;
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 AnimatorServicesContext _asc;
private const string SAMPLE_PATH_PACKAGE =
"Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers";
[InitializeOnLoadMethod]
private static void Init()
{
ModularAvatarMergeAnimator.GetMotionBasePathCallback = (merge, objectBuildContext) =>
{
if (merge.pathMode == MergeAnimatorPathMode.Absolute) return "";
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
var context = (ndmf.BuildContext)objectBuildContext;
private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92";
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
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 relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
return relativePath != "" ? relativePath : "";
};
}
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
_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);
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
= new Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>>();
@ -81,6 +89,10 @@ 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(
@ -89,141 +101,243 @@ namespace nadena.dev.modular_avatar.core.editor
List<ModularAvatarMergeAnimator> toMerge
)
{
// 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)
// 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)
.ToList();
var controller = _asc.ControllerContext.Controllers[layerType];
var session = new AnimatorCombiner(context.PluginBuildContext, layerType.ToString() + " (merged)");
mergeSessions[layerType] = session;
mergeSessions[layerType].BlendableLayer = BlendableLayerFor(layerType);
var replacements = sorted.Count(x => x.mergeAnimatorMode == MergeAnimatorMode.Replace);
if (replacements > 1)
foreach (var component in beforeOriginal)
{
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.
MergeSingle(context, session, component);
}
var writeDefaults = AnalyzeLayerWriteDefaults(controller);
foreach (var component in sorted)
if (defaultControllers_.TryGetValue(layerType, out var defaultController) &&
defaultController.layers.Length > 0)
{
MergeSingle(context, controller, component, writeDefaults);
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
}
foreach (var component in afterOriginal)
{
MergeSingle(context, session, component);
}
}
internal static bool? AnalyzeLayerWriteDefaults(VirtualAnimatorController controller)
{
bool? writeDefaults = null;
var wdStateCounter = controller.Layers
.Where(l => !IsWriteDefaultsSafeLayer(l))
.SelectMany(l => l.StateMachine.AllStates())
.Select(s => s.WriteDefaultValues)
.GroupBy(b => b)
.ToDictionary(g => g.Key, g => g.Count());
if (wdStateCounter.Count == 1) writeDefaults = wdStateCounter.First().Key;
return writeDefaults;
}
private void MergeSingle(BuildContext context, VirtualAnimatorController targetController,
ModularAvatarMergeAnimator merge, bool? initialWriteDefaults)
private void MergeSingle(BuildContext context, AnimatorCombiner session, ModularAvatarMergeAnimator merge)
{
if (merge.animator == null)
{
return;
}
if (!merge.matchAvatarWriteDefaults)
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
initialWriteDefaults = null;
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
var vac = context.PluginBuildContext.Extension<VirtualControllerContext>();
if (!vac.Controllers.TryGetValue(merge, out var clonedController)) return;
var firstLayer = clonedController.Layers.FirstOrDefault();
// the first layer in an animator controller always has weight 1.0f (regardless of what is serialized)
if (firstLayer != null) firstLayer.DefaultWeight = 1.0f;
foreach (var l in clonedController.Layers)
else
{
if (initialWriteDefaults != null && !IsWriteDefaultsSafeLayer(l))
{
foreach (var s in l.StateMachine?.AllStates() ?? Array.Empty<VirtualState>())
{
s.WriteDefaultValues = initialWriteDefaults.Value;
}
}
if (l.StateMachine?.DefaultState?.Motion is VirtualBlendTree
&& l.StateMachine.States.Count == 1
&& l.StateMachine.StateMachines.Count == 0
&& l.StateMachine.AnyStateTransitions.Count == 0)
{
// Force WD on for single state blendtree layers
l.StateMachine.DefaultState.WriteDefaultValues = true;
}
targetController.AddLayer(new LayerPriority(merge.layerPriority), l);
basePath = "";
}
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;
}
var writeDefaults = merge.matchAvatarWriteDefaults
? writeDefaults_.GetValueOrDefault(merge.layerType)
: null;
var controller = _context.ConvertAnimatorController(merge.animator);
session.AddController(basePath, controller, writeDefaults);
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 static bool IsWriteDefaultsSafeLayer(VirtualLayer virtualLayer)
private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions(
VRCAvatarDescriptor.CustomAnimLayer[] layers
)
{
if (virtualLayer.BlendingMode == AnimatorLayerBlendingMode.Additive) return true;
var sm = virtualLayer.StateMachine;
layers = (VRCAvatarDescriptor.CustomAnimLayer[])layers.Clone();
if (sm.StateMachines.Count != 0) return false;
return sm.States.Count == 1 && sm.AnyStateTransitions.Count == 0 &&
sm.DefaultState?.Transitions.Count == 0 && sm.DefaultState.Motion is VirtualBlendTree;
// 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;
}
}
}

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,13 +54,12 @@ namespace nadena.dev.modular_avatar.core.editor
#endif
private BoneDatabase BoneDatabase = new BoneDatabase();
private AnimatorServicesContext AnimatorServices => frameworkContext.Extension<AnimatorServicesContext>();
private PathMappings PathMappings => frameworkContext.Extension<AnimationServicesContext>()
.PathMappings;
private HashSet<Transform> humanoidBones = new HashSet<Transform>();
private readonly HashSet<Transform> prunePBsObjects = new();
private HashSet<Transform> mergedObjects = new HashSet<Transform>();
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
private HashSet<Transform> transformLookthrough = new HashSet<Transform>();
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
{
@ -136,68 +135,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
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;
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
}
private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures)
@ -279,7 +217,7 @@ namespace nadena.dev.modular_avatar.core.editor
BuildReport.ReportingObject(config, () =>
{
prunePBsObjects.Clear();
mergedObjects.Clear();
thisPassAdded.Clear();
MergeArmature(config, target);
#if MA_VRCSDK3_AVATARS
@ -356,7 +294,6 @@ 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));
}
/**
@ -366,8 +303,7 @@ namespace nadena.dev.modular_avatar.core.editor
private void RecursiveMerge(ModularAvatarMergeArmature config,
GameObject src,
GameObject newParent,
bool zipMerge
)
bool zipMerge)
{
if (src == newParent)
{
@ -377,9 +313,10 @@ 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;
@ -420,7 +357,7 @@ namespace nadena.dev.modular_avatar.core.editor
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
BoneDatabase.RetainMergedBone(mergedSrcBone.transform);
transformLookthrough.Add(mergedSrcBone.transform);
PathMappings.MarkTransformLookthrough(mergedSrcBone);
thisPassAdded.Add(mergedSrcBone.transform);
}
@ -430,28 +367,12 @@ namespace nadena.dev.modular_avatar.core.editor
src.name = src.name + "$" + Guid.NewGuid();
}
src.GetOrAddComponent<ModularAvatarPBBlocker>();
mergedSrcBone = src;
HashSet<Transform> childPhysBonesBlockedSet = null;
#if MA_VRCSDK3_AVATARS
src.GetOrAddComponent<ModularAvatarPBBlocker>();
if (physBoneByRootBone.TryGetValue(src.transform, out var pb)
&& !NotAffectedByPhysBoneOrSimilarChainsAsTarget(src.transform, newParent.transform))
if (zipMerge)
{
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);
PathMappings.MarkTransformLookthrough(src);
BoneDatabase.AddMergedBone(src.transform);
}
@ -463,8 +384,6 @@ namespace nadena.dev.modular_avatar.core.editor
if (zipMerge)
{
var reportedHumanoidBoneError = false;
foreach (Transform child in children)
{
if (child.GetComponent <ModularAvatarMergeArmature>() != null)
@ -484,31 +403,20 @@ 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)
{
childNewParent = targetObject.gameObject;
shouldZip = true;
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);
}
}
}
@ -560,7 +468,7 @@ namespace nadena.dev.modular_avatar.core.editor
*/
private void PruneDuplicatePhysBones()
{
foreach (var obj in prunePBsObjects)
foreach (var obj in mergedObjects)
{
if (obj.GetComponent<VRCPhysBone>() == null) continue;
var baseObj = FindOriginalParent(obj);

View File

@ -4,169 +4,181 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using nadena.dev.ndmf.util;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
internal class MergeBlendTreePass : Pass<MergeBlendTreePass>
{
internal const string ALWAYS_ONE = "__ModularAvatarInternal/One";
internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree";
private AnimatorServicesContext _asc;
private VirtualBlendTree _rootBlendTree;
private AnimatorController _controller;
private BlendTree _rootBlendTree;
private GameObject _mergeHost;
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>();
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
_controller = new AnimatorController();
foreach (var component in
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
{
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
}
// always add the ALWAYS_ONE parameter
fx.Parameters = fx.Parameters.SetItem(ALWAYS_ONE, new AnimatorControllerParameter()
{
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1
});
foreach (var name in _parameterNames)
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_parameterNames.Count + 1);
if (_mergeHost != null)
{
if (fx.Parameters.TryGetValue(name, out var existingParameter))
{
switch (existingParameter.type)
{
case AnimatorControllerParameterType.Bool:
existingParameter.defaultFloat = existingParameter.defaultBool ? 1 : 0;
break;
case AnimatorControllerParameterType.Int:
existingParameter.defaultFloat = existingParameter.defaultInt;
break;
}
_parameterNames.Remove(ALWAYS_ONE);
existingParameter.type = AnimatorControllerParameterType.Float;
}
else
parameters.Add(new AnimatorControllerParameter()
{
existingParameter = new AnimatorControllerParameter
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1
});
foreach (var name in _parameterNames)
{
parameters.Add(new AnimatorControllerParameter()
{
name = name,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0.0f
};
defaultFloat = 0
});
}
fx.Parameters = fx.Parameters.SetItem(name, existingParameter);
var paramsAnimator = new AnimatorController();
paramsAnimator.parameters = parameters.ToArray();
var paramsComponent = _mergeHost.AddComponent<ModularAvatarMergeAnimator>();
paramsComponent.animator = paramsAnimator;
paramsComponent.layerPriority = Int32.MaxValue;
}
}
private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component)
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
{
var virtualBlendTree = _asc.ControllerContext.GetVirtualizedMotion(component);
if (virtualBlendTree == null)
BlendTree componentBlendTree = component.BlendTree as BlendTree;
if (componentBlendTree == null)
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.NonFatal, "error.merge_blend_tree.missing_tree");
return;
}
var rootBlend = GetRootBlendTree();
string basePath = null;
if (component.PathMode == MergeAnimatorPathMode.Relative)
{
var root = component.RelativePathRoot.Get(context.AvatarRootTransform);
if (root == null) root = component.gameObject;
basePath = RuntimeUtil.AvatarRootPath(root) + "/";
}
rootBlend.Children = rootBlend.Children.Add(new()
{
Motion = virtualBlendTree,
DirectBlendParameter = ALWAYS_ONE,
Threshold = 1,
CycleOffset = 1,
TimeScale = 1,
});
var bt = new DeepClone(context).DoClone(componentBlendTree, basePath);
var rootBlend = GetRootBlendTree(context);
rootBlend.AddChild(bt);
var children = rootBlend.children;
children[children.Length - 1].directBlendParameter = ALWAYS_ONE;
rootBlend.children = children;
foreach (var asset in virtualBlendTree.AllReachableNodes())
foreach (var asset in bt.ReferencedAssets(includeScene: false))
{
if (asset is VirtualBlendTree bt2)
if (asset is BlendTree 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 VirtualBlendTree GetRootBlendTree()
private BlendTree GetRootBlendTree(ndmf.BuildContext context)
{
if (_rootBlendTree != null) return _rootBlendTree;
var newController = new AnimatorController();
var newStateMachine = new AnimatorStateMachine();
var newState = new AnimatorState();
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
var controller = fx.AddLayer(new LayerPriority(int.MinValue), BlendTreeLayerName);
var stateMachine = controller.StateMachine;
if (fx == null)
_rootBlendTree = new BlendTree();
_controller = newController;
newController.layers = new[]
{
throw new Exception("FX layer not found");
}
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;
_rootBlendTree = VirtualBlendTree.Create("Root");
var state = stateMachine.AddState("State", _rootBlendTree);
stateMachine.DefaultState = state;
state.WriteDefaultValues = true;
newState.writeDefaultValues = true;
newState.motion = _rootBlendTree;
_rootBlendTree.blendType = BlendTreeType.Direct;
_rootBlendTree.blendParameter = ALWAYS_ONE;
_rootBlendTree.BlendType = BlendTreeType.Direct;
_rootBlendTree.BlendParameter = ALWAYS_ONE;
var mergeObject = new GameObject("ModularAvatarMergeBlendTree");
var merger = mergeObject.AddComponent<ModularAvatarMergeAnimator>();
merger.animator = newController;
merger.pathMode = MergeAnimatorPathMode.Absolute;
merger.matchAvatarWriteDefaults = false;
merger.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
merger.deleteAttachedAnimator = false;
merger.layerPriority = Int32.MinValue;
mergeObject.transform.SetParent(context.AvatarRootTransform, false);
mergeObject.transform.SetSiblingIndex(0);
_mergeHost = mergeObject;
return _rootBlendTree;
}
}

View File

@ -27,7 +27,6 @@ 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
@ -85,15 +84,13 @@ namespace nadena.dev.modular_avatar.core.editor
internal class RetargetMeshes
{
private BoneDatabase _boneDatabase;
private AnimationIndex _animationIndex;
private ObjectPathRemapper _pathRemapper;
private PathMappings _pathTracker;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
AnimatorServicesContext pathMappings)
PathMappings pathMappings)
{
this._boneDatabase = boneDatabase;
this._animationIndex = pathMappings.AnimationIndex;
this._pathRemapper = pathMappings.ObjectPathRemapper;
this._pathTracker = pathMappings;
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
@ -156,8 +153,7 @@ namespace nadena.dev.modular_avatar.core.editor
child.SetParent(destBone, true);
}
// Remap any animation clips that reference this bone into its parent
_pathRemapper.ReplaceObject(sourceBone.gameObject, sourceBone.transform.parent.gameObject);
_pathTracker.MarkRemoved(sourceBone.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,6 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor;
#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER
@ -8,13 +6,12 @@ using UnityEngine;
using UnityEngine.Animations;
using VRC.SDK3.Avatars;
using System.Linq;
using nadena.dev.ndmf.animator;
using nadena.dev.modular_avatar.animation;
using VRC.Dynamics;
#endif
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
internal class ConstraintConverterPass : Pass<ConstraintConverterPass>
{
#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER
@ -53,18 +50,17 @@ 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<AnimatorServicesContext>();
var asc = context.Extension<AnimationServicesContext>();
// Also look for preexisting VRCConstraints so we can go fix up any broken animation clips from people who
// clicked auto fix :(
@ -74,20 +70,24 @@ namespace nadena.dev.modular_avatar.core.editor
var targetPaths = constraintGameObjects
.Union(existingVRCConstraints)
.Select(c => asc.ObjectPathRemapper.GetVirtualPathForObject(c!))
.Select(c => asc.PathMappings.GetObjectIdentifier(c))
.ToHashSet();
// Update animation clips
var clips = targetPaths.SelectMany(tp => asc.AnimationIndex.GetClipsForObjectPath(tp))
var clips = targetPaths.SelectMany(tp => asc.AnimationDatabase.ClipsForPath(tp))
.ToHashSet();
foreach (var clip in clips) RemapSingleClip(clip, targetPaths);
}
private void RemapSingleClip(VirtualClip clip, HashSet<string> targetPaths)
private void RemapSingleClip(AnimationDatabase.ClipHolder clip, HashSet<string> targetPaths)
{
var bindings = clip.GetFloatCurveBindings().ToList();
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>();
foreach (var ecb in bindings)
{
@ -102,11 +102,20 @@ namespace nadena.dev.modular_avatar.core.editor
type = newType,
propertyName = newProp
};
var curve = clip.GetFloatCurve(ecb);
clip.SetFloatCurve(newBinding, curve);
clip.SetFloatCurve(ecb, null);
var curve = AnimationUtility.GetEditorCurve(motion, ecb);
if (curve != null)
{
toUpdateBindings.Add(newBinding);
toUpdateCurves.Add(curve);
toUpdateBindings.Add(ecb);
toUpdateCurves.Add(null);
}
}
}
if (toUpdateBindings.Count == 0) return;
AnimationUtility.SetEditorCurves(motion, toUpdateBindings.ToArray(), toUpdateCurves.ToArray());
}
#else

View File

@ -5,7 +5,6 @@ using nadena.dev.ndmf;
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
internal class PruneParametersPass : Pass<PruneParametersPass>
{
protected override void Execute(ndmf.BuildContext context)

View File

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

View File

@ -5,10 +5,7 @@ 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.model;
using nadena.dev.ndmf.util;
using UnityEngine;
using Object = UnityEngine.Object;
@ -20,7 +17,6 @@ using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor.plugin
{
[RunsOnAllPlatforms]
class PluginDefinition : Plugin<PluginDefinition>
{
public override string QualifiedName => "nadena.dev.modular-avatar";
@ -39,13 +35,8 @@ 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.WithRequiredExtension(typeof(AnimatorServicesContext), s =>
{
// Just activating the context is enough.
s.Run("Clone animators", _ => { });
});
seq.Run("Clone animators", AnimationUtil.CloneAllControllers);
seq = InPhase(BuildPhase.Transforming);
seq.Run("Validate configuration",
@ -53,76 +44,49 @@ 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(AnimatorServicesContext), _s2 =>
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
#if MA_VRCSDK3_AVATARS
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 =>
{
// TODO - refactor out VRChat-specific bits
seq.Run("Reactive Components", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(),
new MaterialSetterPreview());
});
seq.Run(GameObjectDelayDisablePass.Instance);
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview());
// 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.OnPlatforms(new[] { WellKnownPlatforms.VRChatAvatar30 }, _seq =>
{
seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx)
);
});
seq.Run(WorldScaleObjectPass.Instance);
seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx)
);
seq.Run(ReplaceObjectPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(BlendshapeSyncAnimationPluginPass.Instance);
seq.Run(ConstraintConverterPass.Instance);
seq.Run(GameObjectDelayDisablePass.Instance);
#endif
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);
seq.Run(ConstraintConverterPass.Instance);
});
#if MA_VRCSDK3_AVATARS
seq.Run(PhysbonesBlockerPluginPass.Instance);
seq.OnPlatforms(new[] { WellKnownPlatforms.VRChatAvatar30 }, _seq =>
seq.Run("Fixup Expressions Menu", ctx =>
{
seq.Run("Fixup Expressions Menu", ctx =>
{
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
});
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
});
seq.Run(SyncParameterSequencePass.Instance);
#endif
@ -220,7 +184,6 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
}
}
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
class MergeAnimatorPluginPass : MAPass<MergeAnimatorPluginPass>
{
protected override void Execute(ndmf.BuildContext context)
@ -229,7 +192,6 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
}
}
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
class MenuInstallPluginPass : MAPass<MenuInstallPluginPass>
{
protected override void Execute(ndmf.BuildContext context)
@ -256,7 +218,6 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
}
#if MA_VRCSDK3_AVATARS
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
class VisibleHeadAccessoryPluginPass : MAPass<VisibleHeadAccessoryPluginPass>
{
protected override void Execute(ndmf.BuildContext context)
@ -275,15 +236,14 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
}
#if MA_VRCSDK3_AVATARS
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)] // TODO - support other platforms
class BlendshapeSyncAnimationPluginPass : MAPass<BlendshapeSyncAnimationPluginPass>
{
protected override void Execute(ndmf.BuildContext context)
{
new BlendshapeSyncAnimationProcessor(context).OnPreprocessAvatar();
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(MAContext(context));
}
}
class PhysbonesBlockerPluginPass : MAPass<PhysbonesBlockerPluginPass>
{
protected override void Execute(ndmf.BuildContext context)

View File

@ -1,6 +1,4 @@
#nullable enable
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Object = UnityEngine.Object;
@ -16,8 +14,6 @@ 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;
@ -36,7 +32,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 _rpe.GetActiveSelfProxy(obj);
return _asc.GetActiveSelfProxy(obj);
}
else
{

View File

@ -1,11 +1,9 @@
#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;
@ -19,12 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
private readonly ComputeContext _computeContext;
private readonly ndmf.BuildContext _context;
private readonly AnimatorServicesContext _asc;
private readonly ReadablePropertyExtension _rpe;
private static readonly ImmutableHashSet<Type> ActiveObjectTypes =
new[] { typeof(AudioSource) }.ToImmutableHashSet();
private readonly AnimationServicesContext _asc;
private Dictionary<string, float> _simulationInitialStates;
public const string BlendshapePrefix = "blendShape.";
@ -41,8 +34,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
_computeContext = ComputeContext.NullContext;
_context = context;
_asc = context.Extension<AnimatorServicesContext>();
_rpe = context.Extension<ReadablePropertyExtension>();
_asc = context.Extension<AnimationServicesContext>();
_simulationInitialStates = null;
}
@ -119,8 +111,6 @@ namespace nadena.dev.modular_avatar.core.editor
FindObjectToggles(shapes, root);
FindMaterialSetters(shapes, root);
InjectActiveObjectFallbacks(shapes);
ApplyInitialStateOverrides(shapes);
AnalyzeConstants(shapes);
ResolveToggleInitialStates(shapes);
@ -130,48 +120,6 @@ 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)
@ -197,7 +145,7 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="shapes"></param>
private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = _context?.Extension<AnimatorServicesContext>();
var asc = _context?.Extension<AnimationServicesContext>();
HashSet<GameObject> toggledObjects = new();
if (asc == null) return;
@ -212,10 +160,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
foreach (var condition in actionGroup.ControllingConditions)
if (condition.ReferenceObject != null && !toggledObjects.Contains(condition.ReferenceObject))
condition.IsConstant = !asc.AnimationIndex.GetClipsForObjectPath(
asc.ObjectPathRemapper.GetVirtualPathForObject(condition.ReferenceObject) ??
"___NONEXISTENT___"
).Any();
condition.IsConstant = asc.AnimationDatabase.ClipsForPath(asc.PathMappings.GetObjectIdentifier(condition.ReferenceObject)).IsEmpty;
// Remove redundant active conditions.
actionGroup.ControllingConditions.RemoveAll(c => c.IsConstant && c.InitiallyActive);
@ -230,9 +175,9 @@ namespace nadena.dev.modular_avatar.core.editor
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
}
// Remove shapes with no action groups (unless we need to override static state)
// Remove shapes with no action groups
foreach (var kvp in shapes.ToList())
if (kvp.Value.actionGroups.Count == 0 && kvp.Value.overrideStaticState == null)
if (kvp.Value.actionGroups.Count == 0)
shapes.Remove(kvp.Key);
}
@ -242,7 +187,7 @@ namespace nadena.dev.modular_avatar.core.editor
/// <param name="groups"></param>
private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty> groups)
{
var asc = _context?.Extension<AnimatorServicesContext>();
var asc = _context?.Extension<AnimationServicesContext>();
Dictionary<string, float> propStates = new();
Dictionary<string, float> nextPropStates = new();
@ -338,7 +283,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var (key, info) in shapes.ToList())
{
if (info.actionGroups.Count == 0 && info.overrideStaticState == null)
if (info.actionGroups.Count == 0)
{
// never active control; ignore it entirely
if (OptimizeShapes) shapes.Remove(key);
@ -353,9 +298,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.Count == 0 || info.actionGroups[^1].IsConstant)
if (info.actionGroups[^1].IsConstant)
{
if (OptimizeShapes && info.overrideStaticState == null) shapes.Remove(key);
if (OptimizeShapes) shapes.Remove(key);
}
}
}

View File

@ -5,7 +5,6 @@ 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;
@ -24,8 +23,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 VirtualClip _initialStateClip;
private AnimationClip _initialStateClip;
private bool _writeDefaults;
public ReactiveObjectPass(ndmf.BuildContext context)
@ -39,12 +38,7 @@ 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.
var asc = context.Extension<AnimatorServicesContext>();
var fxLayer = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
if (fxLayer != null)
{
_writeDefaults = MergeAnimatorProcessor.AnalyzeLayerWriteDefaults(fxLayer) ?? true;
}
_writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true;
var analysis = new ReactiveObjectAnalyzer(context).Analyze(context.AvatarRootObject);
@ -66,7 +60,7 @@ namespace nadena.dev.modular_avatar.core.editor
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
{
var rpe = context.Extension<ReadablePropertyExtension>();
var asc = context.Extension<AnimationServicesContext>();
foreach (var prop in shapes.Keys)
{
@ -74,7 +68,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?
rpe.GetActiveSelfProxy(go);
asc.GetActiveSelfProxy(go);
}
}
}
@ -97,19 +91,19 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
{
var asc = context.Extension<AnimatorServicesContext>();
var rpe = context.Extension<ReadablePropertyExtension>();
var asc = context.Extension<AnimationServicesContext>();
// 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;
var clips = asc.AnimationIndex;
_initialStateClip = clips.GetClipsForObjectPath(ReactiveObjectPrepass.TAG_PATH).FirstOrDefault();
if (_initialStateClip == null) return;
_initialStateClip.Name = "Reactive Component Defaults";
_initialStateClip = new AnimationClip();
_initialStateClip.name = "Reactive Component Defaults";
initialStateHolder.CurrentClip = _initialStateClip;
foreach (var (key, initialState) in initialStates)
{
@ -143,11 +137,6 @@ 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);
@ -158,19 +147,17 @@ 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)staticState > 0.5f;
prop.boolValue = ((float)initialState) > 0.5f;
break;
case SerializedPropertyType.Float:
animBaseState = prop.floatValue;
prop.floatValue = (float)staticState;
prop.floatValue = (float) initialState;
break;
case SerializedPropertyType.ObjectReference:
animBaseState = prop.objectReferenceValue;
@ -199,17 +186,17 @@ namespace nadena.dev.modular_avatar.core.editor
curve.AddKey(0, f);
curve.AddKey(1, f);
_initialStateClip.SetFloatCurve(binding, curve);
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
if (componentType == typeof(GameObject) && key.PropertyName == "m_IsActive")
{
binding = EditorCurveBinding.FloatCurve(
"",
typeof(Animator),
rpe.GetActiveSelfProxy((GameObject)key.TargetObject)
asc.GetActiveSelfProxy((GameObject)key.TargetObject)
);
_initialStateClip.SetFloatCurve(binding, curve);
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
}
}
else if (animBaseState is Object obj)
@ -219,8 +206,8 @@ namespace nadena.dev.modular_avatar.core.editor
componentType,
key.PropertyName
);
_initialStateClip.SetObjectCurve(binding, new[]
AnimationUtility.SetObjectReferenceCurve(_initialStateClip, binding, new []
{
new ObjectReferenceKeyframe()
{
@ -314,12 +301,6 @@ 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);
@ -327,7 +308,7 @@ namespace nadena.dev.modular_avatar.core.editor
private AnimatorStateMachine GenerateStateMachine(AnimatedProperty info)
{
var asc = context.Extension<AnimatorServicesContext>();
var asc = context.Extension<AnimationServicesContext>();
var asm = new AnimatorStateMachine();
// Workaround for the warning: "'.' is not allowed in State name"
@ -352,6 +333,7 @@ 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>)>();
@ -381,7 +363,7 @@ namespace nadena.dev.modular_avatar.core.editor
clip.name = "Property Overlay controlled by " + group.ControllingConditions[0].DebugName + " " +
group.Value;
var conditions = GetTransitionConditions(group);
var conditions = GetTransitionConditions(asc, group);
foreach (var (st, transitions) in transitionBuffer)
{
@ -425,6 +407,7 @@ 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));
@ -505,7 +488,7 @@ namespace nadena.dev.modular_avatar.core.editor
};
}
private AnimatorCondition[] GetTransitionConditions(ReactionRule group)
private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ReactionRule group)
{
var conditions = new List<AnimatorCondition>();
@ -591,8 +574,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (key.TargetObject is GameObject targetObject && key.PropertyName == "m_IsActive")
{
var rpe = context.Extension<ReadablePropertyExtension>();
var propName = rpe.GetActiveSelfProxy(targetObject);
var asc = context.Extension<AnimationServicesContext>();
var propName = asc.GetActiveSelfProxy(targetObject);
binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
@ -603,27 +586,47 @@ namespace nadena.dev.modular_avatar.core.editor
private void ApplyController(AnimatorStateMachine asm, string layerName)
{
var asc = context.Extension<AnimatorServicesContext>();
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
if (fx == null)
var fx = FindFxController();
if (fx.animatorController == null)
{
throw new InvalidOperationException("No FX layer found");
}
foreach (var paramName in initialValues.Keys.Except(fx.Parameters.Keys))
if (!context.IsTemporaryAsset(fx.animatorController))
{
var parameter = new AnimatorControllerParameter
throw new InvalidOperationException("FX layer is not a temporary asset");
}
if (!(fx.animatorController is AnimatorController animController))
{
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()
{
name = paramName,
type = AnimatorControllerParameterType.Float,
defaultFloat = initialValues[paramName], // TODO
};
fx.Parameters = fx.Parameters.SetItem(paramName, parameter);
});
paramSet.Add(paramName);
}
fx.AddLayer(LayerPriority.Default, "RC " + layerName).StateMachine =
asc.ControllerContext.Clone(asm);
animController.parameters = paramList.ToArray();
animController.layers = animController.layers.Append(
new AnimatorControllerLayer
{
stateMachine = asm,
name = "RC " + layerName,
defaultWeight = 1
}
).ToArray();
}
private VRCAvatarDescriptor.CustomAnimLayer FindFxController()

View File

@ -9,7 +9,6 @@ namespace nadena.dev.modular_avatar.core.editor
/// Reserve an animator layer for reactive object use. We do this here so that we can take advantage of MergeAnimator's
/// layer reference correction logic; this can go away once we have a more unified animation services API.
/// </summary>
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
internal class ReactiveObjectPrepass : Pass<ReactiveObjectPrepass>
{
internal const string TAG_PATH = "__MA/ShapeChanger/PrepassPlaceholder";
@ -50,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.Motion = bt;
mambt.BlendTree = bt;
mambt.PathMode = MergeAnimatorPathMode.Absolute;
}
}

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
@ -54,7 +54,6 @@ 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())
@ -73,8 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (mami.Control == null) mami.Control = new VRCExpressionsMenu.Control();
mami.Control.parameter = new VRCExpressionsMenu.Control.Parameter
{
name = mappings.Remap(mami, ParameterNamespace.Animator,
$"__MA/AutoParam/{mami.gameObject.name}")
name = $"__MA/AutoParam/{mami.gameObject.name}${paramIndex++}"
};
}
@ -203,20 +201,18 @@ namespace nadena.dev.modular_avatar.core.editor
if (mamiWithRC.Count > 0)
{
var asc = context.Extension<AnimatorServicesContext>();
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
foreach (var (name, _) in mamiWithRC)
// 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
{
if (!fx.Parameters.ContainsKey(name))
parameters = mamiWithRC.Select(kvp => new AnimatorControllerParameter
{
fx.Parameters = fx.Parameters.SetItem(name, new()
{
name = name,
type = AnimatorControllerParameterType.Float,
});
}
}
name = kvp.Key,
type = AnimatorControllerParameterType.Float,
}).ToArray(),
};
}
}

View File

@ -1,4 +1,4 @@
#if MA_VRCSDK3_AVATARS
#if MA_VRCSDK3_AVATARS
#region
@ -6,19 +6,21 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using nadena.dev.modular_avatar.animation;
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
@ -30,39 +32,18 @@ namespace nadena.dev.modular_avatar.core.editor
return ctx.GetState<ParameterRenameMappings>();
}
private readonly HashSet<string> usedNames = new();
public Dictionary<(Component, ParameterNamespace, string), string> Remappings = new();
public Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string> Remappings =
new Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string>();
private int internalParamIndex;
public string Remap(Component p, ParameterNamespace ns, string s)
public string Remap(ModularAvatarParameters p, ParameterNamespace ns, string s)
{
var tuple = (p, ns, s);
if (Remappings.TryGetValue(tuple, out var mapping)) return mapping;
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;
}
mapping = s + "$$Internal_" + internalParamIndex++;
Remappings[tuple] = mapping;
return mapping;
@ -74,7 +55,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";
@ -94,8 +75,8 @@ namespace nadena.dev.modular_avatar.core.editor
private static long encounterOrderCounter;
public ParameterConfig ResolvedParameter;
public List<Object> TypeSources = new List<Object>();
public List<Object> DefaultSources = new List<Object>();
public List<UnityObject> TypeSources = new List<UnityObject>();
public List<UnityObject> DefaultSources = new List<UnityObject>();
public ImmutableHashSet<float> ConflictingValues = ImmutableHashSet<float>.Empty;
public ImmutableHashSet<ParameterSyncType> ConflictingSyncTypes = ImmutableHashSet<ParameterSyncType>.Empty;
@ -182,7 +163,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (!context.AvatarDescriptor) return;
_context = context;
var syncParams = WalkTree(avatar);
SetExpressionParameters(avatar, syncParams);
@ -195,7 +176,7 @@ namespace nadena.dev.modular_avatar.core.editor
// clean up all parameters objects before the ParameterAssignerPass runs
foreach (var p in avatar.GetComponentsInChildren<ModularAvatarParameters>())
Object.DestroyImmediate(p);
UnityObject.DestroyImmediate(p);
}
private void SetExpressionParameters(GameObject avatarRoot, ImmutableDictionary<string, ParameterInfo> allParams)
@ -338,8 +319,6 @@ 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;
@ -387,8 +366,14 @@ namespace nadena.dev.modular_avatar.core.editor
break;
}
case IVirtualizeAnimatorController or IVirtualizeMotion:
case ModularAvatarMergeAnimator merger:
{
// 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 =>
{
@ -402,10 +387,25 @@ namespace nadena.dev.modular_avatar.core.editor
);
}).ToImmutableDictionary();
var controller = animServices.ControllerContext.Controllers[component];
if (controller != null)
if (merger.animator != null)
{
ProcessVirtualAnimatorController(controller, remap);
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));
}
break;
@ -497,6 +497,28 @@ 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)
{
@ -515,70 +537,113 @@ namespace nadena.dev.modular_avatar.core.editor
});
}
private void ProcessVirtualAnimatorController(VirtualAnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap)
private void ProcessAnimator(AnimatorController controller,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
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);
}
}
if (remaps.IsEmpty) return;
controller.Parameters = newParameters;
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();
}
private void ProcessStateMachine(VirtualStateMachine vsm,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessState(AnimatorState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
foreach (var behavior in vsm.Behaviours)
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)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
ProcessMotion(state.motion, remaps);
}
private void ProcessState(VirtualState state, 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 behavior in state.Behaviours)
{
if (behavior is VRCAvatarParameterDriver driver)
{
ProcessDriver(driver, remaps);
}
}
}
private void ProcessClip(VirtualClip clip,
private void ProcessMotion(Motion motion,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
var curveBindings = clip.GetFloatCurveBindings();
if (motion is BlendTree blendTree) ProcessBlendtree(blendTree, remaps);
if (motion is AnimationClip clip) ProcessClip(clip, remaps);
}
private void ProcessClip(AnimationClip clip,
ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
var curveBindings = AnimationUtility.GetCurveBindings(clip);
var bindingsToUpdate = new List<EditorCurveBinding>();
var newCurves = new List<AnimationCurve>();
@ -588,30 +653,48 @@ 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 = clip.GetFloatCurve(binding);
var newECB = new EditorCurveBinding
var curCurve = AnimationUtility.GetEditorCurve(clip, binding);
bindingsToUpdate.Add(binding);
newCurves.Add(null);
bindingsToUpdate.Add(new EditorCurveBinding
{
path = "",
type = typeof(Animator),
propertyName = newBinding.ParameterName
};
clip.SetFloatCurve(binding, null);
clip.SetFloatCurve(newECB, curCurve);
});
newCurves.Add(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(VirtualBlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessBlendtree(BlendTree 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;
foreach (var child in children)
var children = blendTree.children;
for (int i = 0; i < children.Length; i++)
{
child.DirectBlendParameter = remap(remaps, child.DirectBlendParameter);
var childMotion = children[i];
ProcessMotion(childMotion.motion, remaps);
childMotion.directBlendParameter = remap(remaps, childMotion.directBlendParameter);
children[i] = childMotion;
}
blendTree.children = children;
}
private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
@ -627,17 +710,19 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private void ProcessTransition(VirtualTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
private void ProcessTransition(AnimatorTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps)
{
bool dirty = false;
var conditions = t.Conditions
.Select(cond =>
{
cond.parameter = remap(remaps, cond.parameter, ref dirty);
return cond;
})
.ToImmutableList();
t.Conditions = conditions;
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;
}
private ImmutableDictionary<string, ParameterInfo> CollectParameters(ModularAvatarParameters p,

View File

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

View File

@ -12,7 +12,6 @@ using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
public class SyncParameterSequencePass : Pass<SyncParameterSequencePass>
{
private static Platform? CurrentPlatform

View File

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

View File

@ -1,24 +0,0 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
namespace nadena.dev.modular_avatar.core.editor
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
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

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

View File

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

View File

@ -1,14 +1,9 @@
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
{
@ -36,6 +31,17 @@ 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(),
@ -80,30 +86,7 @@ namespace nadena.dev.modular_avatar.core.editor
obj.transform.localRotation = Quaternion.identity;
obj.transform.localScale = Vector3.one;
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>();
var constraint = obj.AddComponent<ParentConstraint>();
constraint.AddSource(new ConstraintSource()
{
weight = 1.0f,
@ -113,7 +96,10 @@ private static void CreateConstraint(GameObject target, GameObject fixedGameObje
constraint.locked = true;
constraint.rotationOffsets = new[] {Vector3.zero};
constraint.translationOffsets = new[] {Vector3.zero};
_proxy = obj.transform;
return obj.transform;
}
#endif
}
}

View File

@ -1,47 +0,0 @@
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
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
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

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

View File

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

View File

@ -0,0 +1,59 @@
{
"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, INDMFEditorOnly
public abstract class AvatarTagComponent : MonoBehaviour, IEditorOnly
{
internal static event Action OnChangeAction;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 547 B

View File

@ -16,6 +16,8 @@ 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

@ -1,23 +0,0 @@
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

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

View File

@ -25,7 +25,6 @@
#if MA_VRCSDK3_AVATARS
using System;
using nadena.dev.ndmf.animator;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -37,19 +36,10 @@ 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, IVirtualizeAnimatorController
public class ModularAvatarMergeAnimator : AvatarTagComponent
{
internal static Func<ModularAvatarMergeAnimator, object, string> GetMotionBasePathCallback =
(_, _) => "";
public RuntimeAnimatorController animator;
public VRCAvatarDescriptor.AnimLayerType layerType = VRCAvatarDescriptor.AnimLayerType.FX;
public bool deleteAttachedAnimator;
@ -57,8 +47,7 @@ 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
@ -78,22 +67,6 @@ 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,61 +1,17 @@
#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 Motion (Blend Tree)")]
[AddComponentMenu("Modular Avatar/MA Merge Blend Tree")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-blend-tree?lang=auto")]
public sealed class ModularAvatarMergeBlendTree : AvatarTagComponent, IVirtualizeMotion
public sealed class ModularAvatarMergeBlendTree : AvatarTagComponent
{
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]
// We can't actually reference a BlendTree here because it's not available when building a player build
public UnityEngine.Object BlendTree;
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

@ -1,20 +0,0 @@
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 +0,0 @@
fileFormatVersion: 2
guid: 89c938d7d8a741df99f2eda501b3a6fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,12 +0,0 @@
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

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

View File

@ -1,14 +1,11 @@
#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
@ -20,7 +17,8 @@ public class ActiveAnimationRetargeterTests : TestBase
// initialize context
var buildContext = new BuildContext(avatar);
var asc = buildContext.PluginBuildContext.ActivateExtensionContextRecursive<AnimatorServicesContext>();
var pathMappings = buildContext.PluginBuildContext.ActivateExtensionContext<AnimationServicesContext>()
.PathMappings;
// get game objects
var changedChild = avatar.transform.Find("Toggled/Child");
@ -31,16 +29,18 @@ public class ActiveAnimationRetargeterTests : TestBase
var created = retargeter.CreateIntermediateObjects(newParent.gameObject);
retargeter.FixupAnimations();
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();
// commit
buildContext.AnimationDatabase.Commit();
var clip = findFxClip(avatar, layerName: "retarget");
var curveBindings = AnimationUtility.GetCurveBindings(clip);
// 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(
asc.ObjectPathRemapper.GetVirtualPathForObject(created), typeof(GameObject), "m_IsActive")));
pathMappings.GetObjectIdentifier(created), typeof(GameObject), "m_IsActive")));
}
}

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