Compare commits

..

No commits in common. "main" and "1.10.0-alpha.5" have entirely different histories.

681 changed files with 8825 additions and 40704 deletions

View File

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

View File

@ -1,25 +1,25 @@
{
"dependencies": {
"com.vrchat.avatars": {
"version": "3.7.4"
"version": "3.5.0"
},
"nadena.dev.ndmf": {
"version": "1.8.0-alpha.4"
"version": "1.4.0"
}
},
"locked": {
"com.vrchat.avatars": {
"version": "3.7.4",
"version": "3.5.0",
"dependencies": {
"com.vrchat.base": "3.7.4"
"com.vrchat.base": "3.5.0"
}
},
"com.vrchat.base": {
"version": "3.7.4",
"version": "3.5.0",
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.8.0-alpha.4"
"version": "1.5.0-beta.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@c062e08bd532815e2082a85e87e3ef29c3e6d191
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
generate_release_notes: true
tag_name: ${{ steps.version.outputs.prop }}
files: |
${{ env.zipFile }}
${{ env.zipFileSHA256 }}
package.json

View File

@ -23,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@367672c723960cd03bb7d8c2c4d89062a3fc1fac
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

View File

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

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,133 +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設定が正しくない問題を修正
- [#1555] VRC Animator Play Audioが、Audio Sourceまでの絶対パスで設定されている場合に、相対パスのMerge Animator
コンポーネントとマージされた場合、指定されたオブジェクトが存在しないことを検出し、参照を絶対パスとして扱うように修正
- 対象のパスにオブジェクトがある場合は、相対パスとして扱われます。安定性向上のためMerge Animatorコンポーネントと同じ
 指定方法を使用することをお勧めします。
### 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,135 +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
- [#1555] Fixed compatibility regression from 1.11.x: VRC Animator Play Audio, when configured with an absolute path
but merged with a relative-path merge animator component, will now detect that the indicated object does not
exist, and treat the reference as an absolute path.
- Note that if there is an object in the target path, then it will be treated as a relative path. Using
addressing for Play Audio behaviors consistent with Merge Animator settings is therefore recommended as it will be
more robust.
### 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,99 +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
- [#1555] VRC Animator Play Audioが、Audio Sourceまでの絶対パスで設定されている場合に、相対パスのMerge Animator
コンポーネントとマージされた場合、指定されたオブジェクトが存在しないことを検出し、参照を絶対パスとして扱うように修正
- 対象のパスにオブジェクトがある場合は、相対パスとして扱われます。安定性向上のためMerge Animatorコンポーネントと同じ
 指定方法を使用することをお勧めします。
### 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,105 +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
- [#1555] Fixed compatibility regression from 1.11.x: VRC Animator Play Audio, when configured with an absolute path
but merged with a relative-path merge animator component, will now detect that the indicated object does not
exist, and treat the reference as an absolute path.
- Note that if there is an object in the target path, then it will be treated as a relative path. Using
addressing for Play Audio behaviors consistent with Merge Animator settings is therefore recommended as it will be
more robust.
### 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,383 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// The animation database records the set of all clips which are used in the avatar, and which paths they
/// manipulate.
/// </summary>
internal class AnimationDatabase
{
internal class ClipHolder
{
private readonly AnimationDatabase ParentDatabase;
private Motion _currentClip;
internal Motion CurrentClip
{
get
{
ParentDatabase.InvalidateCaches();
return _currentClip;
}
set
{
ParentDatabase.InvalidateCaches();
_currentClip = value;
}
}
private Motion _originalClip;
internal Motion OriginalClip
{
get => _originalClip;
set
{
_originalClip = value;
IsProxyAnimation = value != null && Util.IsProxyAnimation(value);
}
}
internal bool IsProxyAnimation { private set; get; }
internal ClipHolder(AnimationDatabase parentDatabase, Motion clip)
{
ParentDatabase = parentDatabase;
CurrentClip = OriginalClip = clip;
}
/// <summary>
/// Returns the current clip without invalidating caches. Do not modify this clip without taking extra
/// steps to invalidate caches on the AnimationDatabase.
/// </summary>
/// <returns></returns>
internal Motion GetCurrentClipUnsafe()
{
return _currentClip;
}
public void SetCurrentNoInvalidate(Motion newMotion)
{
_currentClip = newMotion;
}
}
private BuildContext _context;
private List<Action> _clipCommitActions = new List<Action>();
private List<ClipHolder> _clips = new List<ClipHolder>();
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
private HashSet<VRCAnimatorPlayAudio> _playAudios = new HashSet<VRCAnimatorPlayAudio>();
#endif
private Dictionary<string, HashSet<ClipHolder>> _pathToClip = null;
internal AnimationDatabase()
{
Debug.Log("Creating animation database");
}
internal void Commit()
{
foreach (var clip in _clips)
{
if (clip.IsProxyAnimation) clip.CurrentClip = clip.OriginalClip;
}
foreach (var clip in _clips)
{
// Changing the "high quality curve" setting can result in behavior changes (but can happen accidentally
// as we manipulate curves)
if (clip.CurrentClip != clip.OriginalClip && clip.CurrentClip != null && clip.OriginalClip != null)
{
SerializedObject before = new SerializedObject(clip.OriginalClip);
SerializedObject after = new SerializedObject(clip.CurrentClip);
var before_prop = before.FindProperty("m_UseHighQualityCurve");
var after_prop = after.FindProperty("m_UseHighQualityCurve");
if (after_prop.boolValue != before_prop.boolValue)
{
after_prop.boolValue = before_prop.boolValue;
after.ApplyModifiedPropertiesWithoutUndo();
}
}
}
foreach (var action in _clipCommitActions)
{
action();
}
}
internal void OnActivate(BuildContext context)
{
_context = context;
AnimationUtil.CloneAllControllers(context);
#if MA_VRCSDK3_AVATARS
var avatarDescriptor = context.AvatarDescriptor;
foreach (var layer in avatarDescriptor.baseAnimationLayers)
{
BootstrapLayer(layer);
}
foreach (var layer in avatarDescriptor.specialAnimationLayers)
{
BootstrapLayer(layer);
}
void BootstrapLayer(VRCAvatarDescriptor.CustomAnimLayer layer)
{
if (!layer.isDefault && layer.animatorController is AnimatorController ac &&
context.IsTemporaryAsset(ac))
{
BuildReport.ReportingObject(ac, () =>
{
foreach (var state in Util.States(ac))
{
RegisterState(state);
}
});
}
}
#endif
}
/// <summary>
/// Registers a motion and all its reachable submotions with the animation database. The processClip callback,
/// if provided, will be invoked for each newly discovered clip.
/// </summary>
/// <param name="state"></param>
/// <param name="processClip"></param>
/// <exception cref="Exception"></exception>
internal void RegisterState(AnimatorState state, Action<ClipHolder> processClip = null)
{
Dictionary<Motion, ClipHolder> _originalToHolder = new Dictionary<Motion, ClipHolder>();
if (processClip == null) processClip = (_) => { };
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
foreach (var behavior in state.behaviours)
{
if (behavior is VRCAnimatorPlayAudio playAudio)
{
_playAudios.Add(playAudio);
}
}
#endif
if (state.motion == null) return;
var clipHolder = RegisterMotion(state.motion, state, processClip, _originalToHolder);
state.motion = clipHolder.CurrentClip;
_clipCommitActions.Add(() => { state.motion = clipHolder.CurrentClip; });
}
internal void ForeachClip(Action<ClipHolder> processClip)
{
foreach (var clipHolder in _clips)
{
processClip(clipHolder);
}
}
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
internal void ForeachPlayAudio(Action<VRCAnimatorPlayAudio> processPlayAudio)
{
foreach (var playAudioHolder in _playAudios)
{
processPlayAudio(playAudioHolder);
}
}
#endif
/// <summary>
/// Returns a list of clips which touched the given _original_ path. This path is subject to basepath remapping,
/// but not object movement remapping.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal ImmutableArray<ClipHolder> ClipsForPath(string path)
{
HydrateCaches();
if (_pathToClip.TryGetValue(path, out var clips))
{
return clips.ToImmutableArray();
}
else
{
return ImmutableArray<ClipHolder>.Empty;
}
}
private ClipHolder RegisterMotion(
Motion motion,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (motion == null)
{
return new ClipHolder(this, null);
}
if (originalToHolder.TryGetValue(motion, out var holder))
{
return holder;
}
InvalidateCaches();
Motion cloned = motion;
if (!_context.IsTemporaryAsset(motion))
{
// Protect the original animations from mutations by creating temporary clones; in the case of a proxy
// animation, we'll restore the original in a later pass
// cloned = Object.Instantiate(motion); - Object.Instantiate can't be used on AnimationClips and BlendTrees
cloned = (Motion)motion.GetType().GetConstructor(new Type[0]).Invoke(new object[0]);
EditorUtility.CopySerialized(motion, cloned);
ObjectRegistry.RegisterReplacedObject(motion, cloned);
}
switch (cloned)
{
case AnimationClip clip:
{
holder = new ClipHolder(this, clip);
processClip(holder);
_clips.Add(holder);
break;
}
case BlendTree tree:
{
holder = RegisterBlendtree(tree, state, processClip, originalToHolder);
break;
}
}
holder.OriginalClip = motion;
originalToHolder[motion] = holder;
return holder;
}
private void InvalidateCaches()
{
_pathToClip = null;
}
private void HydrateCaches()
{
if (_pathToClip == null)
{
_pathToClip = new Dictionary<string, HashSet<ClipHolder>>();
foreach (var clip in _clips)
{
recordPaths(clip);
}
}
}
private void recordPaths(ClipHolder holder)
{
var clip = holder.GetCurrentClipUnsafe() as AnimationClip;
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var path = binding.path;
AddPath(path);
}
void AddPath(string p0)
{
if (!_pathToClip.TryGetValue(p0, out var clips))
{
clips = new HashSet<ClipHolder>();
_pathToClip[p0] = clips;
}
clips.Add(holder);
}
}
private ClipHolder RegisterBlendtree(
BlendTree tree,
AnimatorState state,
Action<ClipHolder> processClip,
Dictionary<Motion, ClipHolder> originalToHolder
)
{
if (!_context.IsTemporaryAsset(tree))
{
throw new Exception("Blendtree must be a temporary asset");
}
var treeHolder = new ClipHolder(this, tree);
var children = tree.children;
var holders = new ClipHolder[children.Length];
for (int i = 0; i < children.Length; i++)
{
holders[i] = RegisterMotion(children[i].motion, state, processClip, originalToHolder);
children[i].motion = holders[i].CurrentClip;
}
tree.children = children;
_clipCommitActions.Add(() =>
{
var dirty = false;
for (int i = 0; i < children.Length; i++)
{
var curClip = holders[i].CurrentClip;
if (children[i].motion != curClip)
{
children[i].motion = curClip;
dirty = true;
}
}
if (dirty)
{
tree.children = children;
EditorUtility.SetDirty(tree);
}
});
return treeHolder;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1,46 +0,0 @@
#if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using VRC.SDK3.Avatars.Components;
namespace nadena.dev.modular_avatar.animation
{
[RunsOnPlatforms(WellKnownPlatforms.VRChatAvatar30)]
public class FixupAbsolutePlayAudioPass : Pass<FixupAbsolutePlayAudioPass>
{
protected override void Execute(BuildContext context)
{
// Older versions of modular avatar did not adjust Animator Play Audio paths when they were absolute paths.
// Replicate this behavior here.
// Note that this runs before any object movement.
var asc = context.Extension<AnimatorServicesContext>();
foreach (var mama in context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true))
{
if (!mama._wasRelative) continue;
var pathPrefix = asc.ObjectPathRemapper.GetVirtualPathForObject(mama.gameObject) + "/";
foreach (var state in asc.ControllerContext.Controllers[mama].AllReachableNodes()
.OfType<VirtualState>())
{
foreach (var behavior in state.Behaviours.OfType<VRCAnimatorPlayAudio>())
{
if (asc.ObjectPathRemapper.GetObjectForPath(behavior.SourcePath) != null) continue;
if (behavior.SourcePath.StartsWith(pathPrefix))
{
behavior.SourcePath = behavior.SourcePath.Substring(pathPrefix.Length);
}
}
}
}
}
}
}
#endif

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a763cbc4bac94063b6b084ea3f4d8206
timeCreated: 1744422528

View File

@ -1,128 +0,0 @@
#if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animator;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using BuildContext = nadena.dev.ndmf.BuildContext;
namespace nadena.dev.modular_avatar.animation
{
/// <summary>
/// This pass delays turning GameObjects OFF by one frame when those objects control a ReadableProperty. This
/// ensures that we don't expose hidden meshes when removing articles of clothing, for example.
/// </summary>
internal class GameObjectDelayDisablePass : Pass<GameObjectDelayDisablePass>
{
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;
// Filter any proxies not used in animator transitions
var usedProxies = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX]
.AllReachableNodes().OfType<VirtualTransitionBase>()
.SelectMany(t => t.Conditions)
.Select(c => c.parameter)
.ToHashSet();
foreach (var proxyBinding in activeProxies.ToList())
{
if (!usedProxies.Contains(proxyBinding.Value))
{
activeProxies.Remove(proxyBinding.Key);
}
}
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
if (fx == null) return;
var nullMotion = new AnimationClip();
nullMotion.name = "NullMotion";
var blendTree = new BlendTree();
blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false;
blendTree.children = activeProxies
.Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value)))
.ToArray();
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
var state = layer.StateMachine.AddState("DelayDisable");
layer.StateMachine.DefaultState = state;
state.WriteDefaultValues = true;
state.Motion = asc.ControllerContext.Clone(blendTree);
// Ensure the initial state of readable props matches the actual state of the gameobject
foreach (var controller in asc.ControllerContext.GetAllControllers())
{
foreach (var (binding, prop) in activeProxies)
{
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
{
p.defaultFloat = obj.activeSelf ? 1 : 0;
controller.Parameters = controller.Parameters.SetItem(prop, p);
}
}
}
}
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
{
var ecb = binding.Item1;
var prop = binding.Item2;
var motion = new AnimationClip();
var curve = new AnimationCurve();
curve.AddKey(0, 1);
AnimationUtility.SetEditorCurve(motion, ecb, curve);
// Occasionally, we'll have a very small value pop up, probably due to FP errors.
// To correct for this, instead of directly using the property in the direct blend tree,
// we'll use a 1D blend tree to give ourselves a buffer.
var bufferBlendTree = new BlendTree();
bufferBlendTree.blendType = BlendTreeType.Simple1D;
bufferBlendTree.useAutomaticThresholds = false;
bufferBlendTree.blendParameter = prop;
bufferBlendTree.children = new[]
{
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0
},
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0.01f
},
new ChildMotion
{
motion = motion,
timeScale = 1,
threshold = 1
}
};
return new ChildMotion
{
motion = bufferBlendTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1
};
}
}
}
#endif

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 9b3eb561f76b459fbfbcf29fc4484261
timeCreated: 1722222066

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,322 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor;
using UnityEngine;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
#endif
#endregion
namespace nadena.dev.modular_avatar.animation
{
#region
#endregion
/// <summary>
/// This extension context tracks when objects are renamed, and updates animations accordingly.
/// Users of this context need to be aware that, when creating new curves (or otherwise introducing new motions,
/// use context.ObjectPath to obtain a suitable path for the target objects).
/// </summary>
internal sealed class PathMappings
{
private AnimationDatabase _animationDatabase;
private Dictionary<GameObject, List<string>>
_objectToOriginalPaths = new Dictionary<GameObject, List<string>>();
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
private ImmutableDictionary<string, GameObject> _pathToObject = null;
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
{
_animationDatabase = animationDatabase;
_objectToOriginalPaths.Clear();
_transformLookthroughObjects.Clear();
ClearCache();
foreach (var xform in context.AvatarRootTransform.GetComponentsInChildren<Transform>(true))
{
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {xform.gameObject.AvatarRootPath()});
}
}
public void ClearCache()
{
_originalPathToMappedPath = null;
_transformOriginalPathToMappedPath = null;
_pathToObject = null;
}
/// <summary>
/// Sets the "transform lookthrough" flag for an object. Any transform animations on this object will be
/// redirected to its parent. This is used in Modular Avatar as part of bone merging logic.
/// </summary>
/// <param name="obj"></param>
public void MarkTransformLookthrough(GameObject obj)
{
_transformLookthroughObjects.Add(obj);
}
/// <summary>
/// Returns a path for use in dynamically generated animations for a given object. This can include objects not
/// present at the time of context activation; in this case, they will be assigned a randomly-generated internal
/// path and replaced during path remapping with the true path.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public string GetObjectIdentifier(GameObject obj)
{
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
return paths[0];
}
else
{
var internalPath = "_NewlyCreatedObject/" + GUID.Generate() + "/" + obj.AvatarRootPath();
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
return internalPath;
}
}
/// <summary>
/// Marks an object as having been removed. Its paths will be remapped to its parent.
/// </summary>
/// <param name="obj"></param>
public void MarkRemoved(GameObject obj)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
var parent = obj.transform.parent.gameObject;
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
{
parentPaths.AddRange(paths);
}
_objectToOriginalPaths.Remove(obj);
_transformLookthroughObjects.Remove(obj);
}
}
/// <summary>
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
/// by the new object. References originally to the new object will continue to point to the new object.
/// </summary>
/// <param name="old"></param>
/// <param name="newObject"></param>
public void ReplaceObject(GameObject old, GameObject newObject)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
{
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
{
newObjectPaths = new List<string>();
_objectToOriginalPaths.Add(newObject, newObjectPaths);
}
newObjectPaths.AddRange(paths);
_objectToOriginalPaths.Remove(old);
}
if (_transformLookthroughObjects.Contains(old))
{
_transformLookthroughObjects.Remove(old);
_transformLookthroughObjects.Add(newObject);
}
}
private ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
bool transformLookup)
{
if (cache != null) return cache;
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
foreach (var kvp in _objectToOriginalPaths)
{
var obj = kvp.Key;
var paths = kvp.Value;
if (transformLookup)
{
while (_transformLookthroughObjects.Contains(obj))
{
obj = obj.transform.parent.gameObject;
}
}
var newPath = obj.AvatarRootPath();
foreach (var origPath in paths)
{
if (!dict.ContainsKey(origPath))
{
dict = dict.Add(origPath, newPath);
}
}
}
cache = dict;
return cache;
}
public string MapPath(string path, bool isTransformMapping = false)
{
ImmutableDictionary<string, string> mappings;
if (isTransformMapping)
{
mappings = BuildMapping(ref _originalPathToMappedPath, true);
}
else
{
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
}
if (mappings.TryGetValue(path, out var mappedPath))
{
return mappedPath;
}
else
{
return path;
}
}
private string MapPath(UnityEditor.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);
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
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 = newClip.wrapMode;
newClip.legacy = newClip.legacy;
newClip.frameRate = newClip.frameRate;
newClip.localBounds = newClip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(originalClip));
if (clipCache != null)
{
clipCache.Add(originalClip, newClip);
}
return newClip;
}
internal void OnDeactivate(BuildContext context)
{
Dictionary<AnimationClip, AnimationClip> clipCache = new Dictionary<AnimationClip, AnimationClip>();
_animationDatabase.ForeachClip(holder =>
{
if (holder.CurrentClip is AnimationClip clip)
{
holder.CurrentClip = ApplyMappingsToClip(clip, clipCache);
}
});
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
_animationDatabase.ForeachPlayAudio(playAudio =>
{
if (playAudio == null) return;
playAudio.SourcePath = MapPath(playAudio.SourcePath, true);
});
#endif
foreach (var listener in context.AvatarRootObject.GetComponentsInChildren<IOnCommitObjectRenames>())
{
listener.OnCommitObjectRenames(context, this);
}
}
public GameObject PathToObject(string path)
{
if (_pathToObject == null)
{
_pathToObject = _objectToOriginalPaths.SelectMany(kvp => kvp.Value.Select(p => (p, kvp.Key)))
.ToImmutableDictionary(t => t.p, t => t.Key);
}
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,143 @@
using System;
using System.Collections.Generic;
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;
}
/// <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

@ -1,11 +1,9 @@
#if MA_VRCSDK3_AVATARS
#region
#region
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 +11,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,36 +20,41 @@ 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 (!values.TryGetValue(name, out var defaultValue)) continue;
if (layer.isDefault || layer.animatorController == null) continue;
switch (parameter.type)
// We should have converted anything that's not an AnimationController by now
var controller = layer.animatorController as AnimatorController;
if (controller == null || !context.IsTemporaryAsset(controller))
{
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
}
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.5f;
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;
}
}
}
}
#endif

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)
public void OnPreprocessAvatar(BuildContext context)
{
return path == other.path && propertyName == other.propertyName;
}
public override bool Equals(object? obj)
{
return obj is SummaryBinding other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(path, propertyName);
}
}
public void OnPreprocessAvatar()
{
var avatarGameObject = _context.AvatarRootObject;
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
_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

@ -1,18 +1,23 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
#endif
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class BuildContext
{
internal readonly ndmf.BuildContext PluginBuildContext;
internal readonly nadena.dev.ndmf.BuildContext PluginBuildContext;
#if MA_VRCSDK3_AVATARS
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
@ -20,6 +25,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 UnityEngine.Object AssetContainer => PluginBuildContext.AssetContainer;
private bool SaveImmediate = false;
#if MA_VRCSDK3_AVATARS
@ -31,12 +44,13 @@ namespace nadena.dev.modular_avatar.core.editor
/// replace the source menu for the purposes of identifying any other MAMIs that might install to the same
/// menu asset.
/// </summary>
internal readonly Dictionary<Object, Action<VRCExpressionsMenu.Control>> PostProcessControls = new();
internal readonly Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> PostProcessControls
= new Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>();
#endif
public static implicit operator BuildContext(ndmf.BuildContext ctx) =>
ctx.Extension<ModularAvatarContext>().BuildContext;
public BuildContext(ndmf.BuildContext PluginBuildContext)
public BuildContext(nadena.dev.ndmf.BuildContext PluginBuildContext)
{
this.PluginBuildContext = PluginBuildContext;
}
@ -57,9 +71,64 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
PluginBuildContext.AssetSaver.SaveAsset(obj);
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
}
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

@ -2,13 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using nadena.dev.modular_avatar.ui;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
using static nadena.dev.modular_avatar.core.editor.Localization;
using System;
#endregion
@ -115,88 +113,44 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
public static class SetupOutfit
internal static class EasySetupOutfit
{
private static string[] errorMessageGroups;
private static string errorHeader;
[MenuItem(UnityMenuItems.GameObject_SetupOutfit, false, UnityMenuItems.GameObject_SetupOutfitOrder)]
internal static void SetupOutfitMenu(MenuCommand cmd)
internal static void SetupOutfit(MenuCommand cmd)
{
var outfitRoot = cmd.context as GameObject;
SetupOutfitUI(outfitRoot);
}
/// <summary>
/// Executes the `Setup Outfit` operation, as if the user selected `outfitRoot` and ran Setup Outfit from the
/// context menu. Any errors encountered will trigger a popup error window.
/// </summary>
/// <param name="outfitRoot"></param>
[PublicAPI]
public static void SetupOutfitUI(GameObject outfitRoot)
{
if (!ValidateSetupOutfit(outfitRoot))
if (!ValidateSetupOutfit())
{
ESOErrorWindow.Show(errorHeader, errorMessageGroups);
return;
}
if (!FindBones(outfitRoot,
if (!FindBones(cmd.context,
out var avatarRoot, out var avatarHips, out var outfitHips)
) return;
Undo.SetCurrentGroupName("Setup Outfit");
var outfitRoot = cmd.context as GameObject;
var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.transform.parent;
var merge = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (merge == null)
{
merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
} else {
Undo.RecordObject(merge, "");
}
if (merge.mergeTarget == null || merge.mergeTargetObject == null)
if (outfitArmature.GetComponent<ModularAvatarMergeArmature>() == null)
{
var merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
merge.mergeTarget = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.LockMode = ArmatureLockMode.BaseToMerge;
}
if (string.IsNullOrEmpty(merge.prefix) && string.IsNullOrEmpty(merge.suffix))
{
merge.InferPrefixSuffix();
}
PrefabUtility.RecordPrefabInstancePropertyModifications(merge);
var outfitAnimator = outfitRoot.GetComponent<Animator>();
var outfitHumanoidBones = GetOutfitHumanoidBones(outfitRoot.transform, outfitAnimator);
var avatarAnimator = avatarRoot.GetComponent<Animator>();
List<Transform> subRoots = new List<Transform>();
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots);
// If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
// help with this
foreach (var subRoot in subRoots)
{
var subConfig = subRoot.GetComponent<ModularAvatarMergeArmature>();
var subConfigMangleNames = false;
if (subConfig == null)
{
subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
}
else
{
Undo.RecordObject(subConfig, "");
subConfigMangleNames = subConfig.mangleNames;
}
if (subConfig.mergeTarget == null || subConfig.mergeTargetObject == null)
{
var subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
var parentTransform = subConfig.transform.parent;
var parentConfig = parentTransform.GetComponentInParent<ModularAvatarMergeArmature>();
var parentMapping = parentConfig.MapBone(parentTransform);
@ -207,9 +161,7 @@ namespace nadena.dev.modular_avatar.core.editor
subConfig.LockMode = ArmatureLockMode.BaseToMerge;
subConfig.prefix = merge.prefix;
subConfig.suffix = merge.suffix;
subConfig.mangleNames = subConfigMangleNames;
PrefabUtility.RecordPrefabInstancePropertyModifications(subConfig);
}
subConfig.mangleNames = false;
}
var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
@ -222,36 +174,21 @@ namespace nadena.dev.modular_avatar.core.editor
outfitArmature.name += ".1";
// Also make sure to refresh the avatar's animator humanoid bone cache.
var avatarAnimator = avatarRoot.GetComponent<Animator>();
var humanDescription = avatarAnimator.avatar;
avatarAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess
avatarAnimator.avatar = humanDescription;
}
}
FixAPose(avatarRoot, outfitArmature);
var meshSettings = outfitRoot.GetComponent<ModularAvatarMeshSettings>();
var mSInheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
var mSInheritBounds = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
if (outfitRoot != null)
{
if (meshSettings == null)
{
meshSettings = Undo.AddComponent<ModularAvatarMeshSettings>(outfitRoot.gameObject);
}
else
{
Undo.RecordObject(meshSettings, "");
mSInheritProbeAnchor = meshSettings.InheritProbeAnchor;
mSInheritBounds = meshSettings.InheritBounds;
}
}
if (meshSettings != null
&& (meshSettings.ProbeAnchor == null || meshSettings.ProbeAnchor.Get(meshSettings) == null
|| meshSettings.RootBone == null || meshSettings.RootBone.Get(meshSettings) == null))
if (outfitRoot != null
&& outfitRoot.GetComponent<ModularAvatarMeshSettings>() == null
&& outfitRoot.GetComponentInParent<ModularAvatarMeshSettings>() == null)
{
var meshSettings = Undo.AddComponent<ModularAvatarMeshSettings>(outfitRoot.gameObject);
Transform rootBone = null, probeAnchor = null;
Bounds bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
@ -267,8 +204,8 @@ namespace nadena.dev.modular_avatar.core.editor
rootBone = avatarRoot.transform;
}
meshSettings.InheritProbeAnchor = mSInheritProbeAnchor;
meshSettings.InheritBounds = mSInheritBounds;
meshSettings.InheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.Set;
meshSettings.InheritBounds = ModularAvatarMeshSettings.InheritMode.Set;
meshSettings.ProbeAnchor = new AvatarObjectReference();
meshSettings.ProbeAnchor.referencePath = RuntimeUtil.RelativePath(avatarRoot, probeAnchor.gameObject);
@ -276,43 +213,10 @@ namespace nadena.dev.modular_avatar.core.editor
meshSettings.RootBone = new AvatarObjectReference();
meshSettings.RootBone.referencePath = RuntimeUtil.RelativePath(avatarRoot, rootBone.gameObject);
meshSettings.Bounds = bounds;
PrefabUtility.RecordPrefabInstancePropertyModifications(meshSettings);
}
}
internal static Dictionary<Transform, HumanBodyBones> GetOutfitHumanoidBones(Transform outfitRoot, Animator outfitAnimator)
{
if (outfitAnimator != null)
{
var hipsCheck = outfitAnimator.isHuman ? outfitAnimator.GetBoneTransform(HumanBodyBones.Hips) : null;
if (hipsCheck != null && hipsCheck.parent == outfitRoot)
{
// Sometimes broken rigs can have the hips as a direct child of the root, instead of having
// an intermediate Armature object. We do not currently support this kind of rig, and so we'll
// assume the outfit's humanoid rig is broken and move on to heuristic matching.
outfitAnimator = null;
} else if (hipsCheck == null) {
outfitAnimator = null;
}
}
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null;
if (outfitAnimator != null)
{
outfitHumanoidBones = new Dictionary<Transform, HumanBodyBones>();
foreach (HumanBodyBones boneIndex in Enum.GetValues(typeof(HumanBodyBones)))
{
var bone = boneIndex != HumanBodyBones.LastBone ? outfitAnimator.GetBoneTransform(boneIndex) : null;
if (bone == null) continue;
outfitHumanoidBones[bone] = boneIndex;
}
}
return outfitHumanoidBones;
}
internal static void FixAPose(GameObject avatarRoot, Transform outfitArmature, bool strictMode = true)
private static void FixAPose(GameObject avatarRoot, Transform outfitArmature)
{
var mergeArmature = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (mergeArmature == null) return;
@ -332,7 +236,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
var lowerArm = (HumanBodyBones)((int)arm + 2);
// check if the rotation of the arm differs(, but distances and origin point are the same when strictMode)
// check if the rotation of the arm differs, but distances and origin point are the same
var avatarArm = rootAnimator.GetBoneTransform(arm);
var outfitArm = avatarToOutfit(avatarArm);
@ -342,8 +246,6 @@ namespace nadena.dev.modular_avatar.core.editor
if (outfitArm == null) return;
if (outfitLowerArm == null) return;
if (strictMode)
{
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
// check relative distance to lower arm as well
@ -351,18 +253,15 @@ namespace nadena.dev.modular_avatar.core.editor
var outfitArmLength = (outfitLowerArm.position - outfitArm.position).magnitude;
if (Mathf.Abs(avatarArmLength - outfitArmLength) > 0.001f) return;
} else {
if (Vector3.Dot((outfitLowerArm.position - outfitArm.position).normalized, (avatarLowerArm.position - avatarArm.position).normalized) > 0.999f) return;
}
// Rotate the outfit arm to ensure these two bone orientations match.
Undo.RecordObject(outfitArm, "Convert A/T Pose");
// Rotate the outfit arm to ensure these two points match.
var relRot = Quaternion.FromToRotation(
outfitLowerArm.position - outfitArm.position,
avatarLowerArm.position - avatarArm.position
);
outfitArm.rotation = relRot * outfitArm.rotation;
PrefabUtility.RecordPrefabInstancePropertyModifications(outfitArm);
EditorUtility.SetDirty(outfitArm);
}
Transform avatarToOutfit(Transform avBone)
@ -463,27 +362,14 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var obj in Selection.objects)
{
errorHeader = S_f("setup_outfit.err.header", obj.name);
if (!(obj is GameObject gameObj)) return false;
if (!ValidateSetupOutfit(gameObj)) return false;
}
return true;
}
private static bool ValidateSetupOutfit(GameObject gameObj)
{
if (gameObj == null)
{
errorHeader = S("setup_outfit.err.header.notarget");
errorMessageGroups = new string[] { S("setup_outfit.err.no_selection") };
return false;
}
errorHeader = S_f("setup_outfit.err.header", gameObj.name);
var xform = gameObj.transform;
if (!FindBones(gameObj, out var _, out var _, out var outfitHips)) return false;
if (!FindBones(obj, out var _, out var _, out var outfitHips))
{
return false;
}
// Some users have been accidentally running Setup Outfit on the avatar itself, and/or nesting avatar
// descriptors when transplanting outfits. Block this (and require that there be only one avdesc) by
@ -492,7 +378,7 @@ namespace nadena.dev.modular_avatar.core.editor
var nearestAvatarTransform = RuntimeUtil.FindAvatarTransformInParents(xform);
if (nearestAvatarTransform == null)
{
errorMessageGroups = new[]
errorMessageGroups = new string[]
{
S_f("setup_outfit.err.no_avatar_descriptor", xform.gameObject.name)
};
@ -501,7 +387,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (nearestAvatarTransform == xform)
{
errorMessageGroups = new[]
errorMessageGroups = new string[]
{ S_f("setup_outfit.err.run_on_avatar_itself", xform.gameObject.name) };
return false;
}
@ -509,12 +395,13 @@ namespace nadena.dev.modular_avatar.core.editor
var parent = nearestAvatarTransform.parent;
if (parent != null && RuntimeUtil.FindAvatarTransformInParents(parent) != null)
{
errorMessageGroups = new[]
errorMessageGroups = new string[]
{
S_f("setup_outfit.err.multiple_avatar_descriptors", xform.gameObject.name)
};
return false;
}
}
return true;
}
@ -578,7 +465,6 @@ namespace nadena.dev.modular_avatar.core.editor
}
var hipsCandidates = new List<string>();
var hipsExtraCandidateRoots = new List<Transform>();
if (outfitHips == null)
{
@ -587,23 +473,6 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (Transform child in outfitRoot.transform)
{
foreach (Transform tempHip in child)
{
if (tempHip.name.Contains(avatarHips.name))
{
outfitHips = tempHip.gameObject;
// Prefer the first hips we find
break;
}
hipsExtraCandidateRoots.Add(tempHip);
}
if (outfitHips != null) return true; // found an exact match, bail outgit
}
// Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips).
foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots)
{
foreach (Transform tempHip in extraCandidateRoot)
{
if (tempHip.name.Contains(avatarHips.name))
{
@ -617,7 +486,6 @@ namespace nadena.dev.modular_avatar.core.editor
}
hipsCandidates.Add(avatarHips.name);
hipsExtraCandidateRoots = new List<Transform>();
// If that doesn't work out, we'll check for heuristic bone mapper mappings.
foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips])
@ -638,25 +506,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
outfitHips = tempHip.gameObject;
}
hipsExtraCandidateRoots.Add(tempHip);
}
}
}
if (outfitHips == null)
{
// Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips).
foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots)
{
foreach (Transform tempHip in extraCandidateRoot)
{
foreach (var candidate in hipsCandidates)
{
if (HeuristicBoneMapper.NormalizeName(tempHip.name).Contains(candidate))
{
outfitHips = tempHip.gameObject;
}
}
}
}
}

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;
@ -18,8 +19,6 @@ namespace nadena.dev.modular_avatar.core.editor
internal static void FixupExpressionsMenu(BuildContext context)
{
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.customExpressions = true;
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;
@ -43,7 +42,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))
@ -92,11 +91,6 @@ namespace nadena.dev.modular_avatar.core.editor
control.icon = newIcon;
}
if (control.subMenu != null)
{
VisitMenu(control.subMenu);
}
if (control.labels != null)
{
for (int i = 0; i < control.labels.Length; i++)
@ -119,20 +113,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 +126,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

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

View File

@ -23,30 +23,30 @@ namespace nadena.dev.modular_avatar.core.editor
// Licensed under the MIT License
private static string[][] boneNamePatterns = new[]
{
new[] {"Hips", "Hip", "pelvis"},
new[] {"Hips", "Hip"},
new[]
{
"LeftUpperLeg", "UpperLeg_Left", "UpperLeg_L", "Leg_Left", "Leg_L", "ULeg_L", "Left leg", "LeftUpLeg",
"UpLeg.L", "Thigh_L"
"UpLeg.L"
},
new[]
{
"RightUpperLeg", "UpperLeg_Right", "UpperLeg_R", "Leg_Right", "Leg_R", "ULeg_R", "Right leg",
"RightUpLeg", "UpLeg.R", "Thigh_R"
"RightUpLeg", "UpLeg.R"
},
new[]
{
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L", "shin.L"
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg"
},
new[]
{
"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R", "LLeg_R", "Right knee",
"RightLeg", "leg_R", "shin.R"
"RightLeg"
},
new[] {"LeftFoot", "Foot_Left", "Foot_L", "Ankle_L", "Foot.L.001", "Left ankle", "heel.L", "heel"},
new[] {"RightFoot", "Foot_Right", "Foot_R", "Ankle_R", "Foot.R.001", "Right ankle", "heel.R", "heel"},
new[] {"Spine", "spine01"},
new[] {"Chest", "Bust", "spine02", "upper_chest"},
new[] {"Spine"},
new[] {"Chest", "Bust"},
new[] {"Neck"},
new[] {"Head"},
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
@ -60,8 +60,8 @@ namespace nadena.dev.modular_avatar.core.editor
"RightUpperArm", "UpperArm_Right", "UpperArm_R", "Arm_Right", "Arm_R", "UArm_R", "Right arm",
"UpperRightArm"
},
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L", "LArm_L", "Left elbow", "LeftForeArm", "Elbow_L", "forearm_L", "ForArm_L"},
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R", "LArm_R", "Right elbow", "RightForeArm", "Elbow_R", "forearm_R", "ForArm_R"},
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L", "LArm_L", "Left elbow", "LeftForeArm", "Elbow_L"},
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R", "LArm_R", "Right elbow", "RightForeArm", "Elbow_R"},
new[] {"LeftHand", "Hand_Left", "Hand_L", "Left wrist", "Wrist_L"},
new[] {"RightHand", "Hand_Right", "Hand_R", "Right wrist", "Wrist_R"},
new[]
@ -80,165 +80,162 @@ namespace nadena.dev.modular_avatar.core.editor
new[]
{
"LeftThumbProximal", "ProximalThumb_Left", "ProximalThumb_L", "Thumb1_L", "ThumbFinger1_L",
"LeftHandThumb1", "Thumb Proximal.L", "Thunb1_L", "finger01_01_L"
"LeftHandThumb1", "Thumb Proximal.L", "Thunb1_L"
},
new[]
{
"LeftThumbIntermediate", "IntermediateThumb_Left", "IntermediateThumb_L", "Thumb2_L", "ThumbFinger2_L",
"LeftHandThumb2", "Thumb Intermediate.L", "Thunb2_L", "finger01_02_L"
"LeftHandThumb2", "Thumb Intermediate.L", "Thunb2_L"
},
new[]
{
"LeftThumbDistal", "DistalThumb_Left", "DistalThumb_L", "Thumb3_L", "ThumbFinger3_L", "LeftHandThumb3",
"Thumb Distal.L", "Thunb3_L", "finger01_03_L"
"Thumb Distal.L", "Thunb3_L"
},
new[]
{
"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L", "Index1_L", "IndexFinger1_L",
"LeftHandIndex1", "Index Proximal.L", "finger02_01_L", "f_index.01.L"
"LeftHandIndex1", "Index Proximal.L"
},
new[]
{
"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L", "Index2_L", "IndexFinger2_L",
"LeftHandIndex2", "Index Intermediate.L", "finger02_02_L", "f_index.02.L"
"LeftHandIndex2", "Index Intermediate.L"
},
new[]
{
"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L", "Index3_L", "IndexFinger3_L", "LeftHandIndex3",
"Index Distal.L", "finger02_03_L", "f_index.03.L"
"Index Distal.L"
},
new[]
{
"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L", "Middle1_L", "MiddleFinger1_L",
"LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L", "f_middle.01.L"
"LeftHandMiddle1", "Middle Proximal.L"
},
new[]
{
"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L", "Middle2_L",
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L", "f_middle.02.L"
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L"
},
new[]
{
"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L", "Middle3_L", "MiddleFinger3_L",
"LeftHandMiddle3", "Middle Distal.L", "finger03_03_L", "f_middle.03.L"
"LeftHandMiddle3", "Middle Distal.L"
},
new[]
{
"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L", "Ring1_L", "RingFinger1_L", "LeftHandRing1",
"Ring Proximal.L", "finger04_01_L", "f_ring.01.L"
"Ring Proximal.L"
},
new[]
{
"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L", "Ring2_L", "RingFinger2_L",
"LeftHandRing2", "Ring Intermediate.L", "finger04_02_L", "f_ring.02.L"
"LeftHandRing2", "Ring Intermediate.L"
},
new[]
{
"LeftRingDistal", "DistalRing_Left", "DistalRing_L", "Ring3_L", "RingFinger3_L", "LeftHandRing3",
"Ring Distal.L", "finger04_03_L", "f_ring.03.L"
"Ring Distal.L"
},
new[]
{
"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L", "Little1_L", "LittleFinger1_L",
"LeftHandPinky1", "Little Proximal.L", "finger05_01_L", "f_pinky.01.L"
"LeftHandPinky1", "Little Proximal.L"
},
new[]
{
"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L", "Little2_L",
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L", "f_pinky.02.L"
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L"
},
new[]
{
"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L", "Little3_L", "LittleFinger3_L",
"LeftHandPinky3", "Little Distal.L", "finger05_03_L", "f_pinky.03.L"
"LeftHandPinky3", "Little Distal.L"
},
new[]
{
"RightThumbProximal", "ProximalThumb_Right", "ProximalThumb_R", "Thumb1_R", "ThumbFinger1_R",
"RightHandThumb1", "Thumb Proximal.R", "Thunb1_R", "finger01_01_R"
"RightHandThumb1", "Thumb Proximal.R", "Thunb1_R"
},
new[]
{
"RightThumbIntermediate", "IntermediateThumb_Right", "IntermediateThumb_R", "Thumb2_R",
"ThumbFinger2_R", "RightHandThumb2", "Thumb Intermediate.R", "Thunb2_R", "finger01_02_R"
"ThumbFinger2_R", "RightHandThumb2", "Thumb Intermediate.R", "Thunb2_R"
},
new[]
{
"RightThumbDistal", "DistalThumb_Right", "DistalThumb_R", "Thumb3_R", "ThumbFinger3_R",
"RightHandThumb3", "Thumb Distal.R", "Thunb3_R", "finger01_03_R"
"RightHandThumb3", "Thumb Distal.R", "Thunb3_R"
},
new[]
{
"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R", "Index1_R", "IndexFinger1_R",
"RightHandIndex1", "Index Proximal.R", "finger02_01_R", "f_index.01.R"
"RightHandIndex1", "Index Proximal.R"
},
new[]
{
"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R", "Index2_R",
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R", "f_index.02.R"
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R"
},
new[]
{
"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R", "Index3_R", "IndexFinger3_R",
"RightHandIndex3", "Index Distal.R", "finger02_03_R", "f_index.03.R"
"RightHandIndex3", "Index Distal.R"
},
new[]
{
"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R", "Middle1_R", "MiddleFinger1_R",
"RightHandMiddle1", "Middle Proximal.R", "finger03_01_R", "f_middle.01.R"
"RightHandMiddle1", "Middle Proximal.R"
},
new[]
{
"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R", "Middle2_R",
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R", "f_middle.02.R"
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R"
},
new[]
{
"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R", "Middle3_R", "MiddleFinger3_R",
"RightHandMiddle3", "Middle Distal.R", "finger03_03_R", "f_middle.03.R"
"RightHandMiddle3", "Middle Distal.R"
},
new[]
{
"RightRingProximal", "ProximalRing_Right", "ProximalRing_R", "Ring1_R", "RingFinger1_R",
"RightHandRing1", "Ring Proximal.R", "finger04_01_R", "f_ring.01.R"
"RightHandRing1", "Ring Proximal.R"
},
new[]
{
"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R", "Ring2_R", "RingFinger2_R",
"RightHandRing2", "Ring Intermediate.R", "finger04_02_R", "f_ring.02.R"
"RightHandRing2", "Ring Intermediate.R"
},
new[]
{
"RightRingDistal", "DistalRing_Right", "DistalRing_R", "Ring3_R", "RingFinger3_R", "RightHandRing3",
"Ring Distal.R", "finger04_03_R", "f_ring.03.R"
"Ring Distal.R"
},
new[]
{
"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R", "Little1_R", "LittleFinger1_R",
"RightHandPinky1", "Little Proximal.R", "finger05_01_R", "f_pinky.01.R"
"RightHandPinky1", "Little Proximal.R"
},
new[]
{
"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R", "Little2_R",
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R", "f_pinky.02.R"
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R"
},
new[]
{
"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R", "Little3_R", "LittleFinger3_R",
"RightHandPinky3", "Little Distal.R", "finger05_03_R", "f_pinky.03.R"
"RightHandPinky3", "Little Distal.R"
},
new[] {"UpperChest", "UChest"},
};
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
internal static ImmutableHashSet<string> AllBoneNames =
boneNamePatterns.SelectMany(x => x).Select(NormalizeName).ToImmutableHashSet();
internal static string NormalizeName(string name)
{
name = name.ToLowerInvariant();
name = Regex.Replace(name, "^bone_|[0-9 ._]", "");
name = Regex.Replace(name, "[0-9 ._]", "");
return name;
}
@ -246,14 +243,6 @@ namespace nadena.dev.modular_avatar.core.editor
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
[InitializeOnLoadMethod]
private static void InsertboneNamePatternsToRuntime()
{
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
}
static HeuristicBoneMapper()
{
var pat_end_side = new Regex(@"[_\.]([LR])$");
@ -317,9 +306,7 @@ namespace nadena.dev.modular_avatar.core.editor
GameObject src,
GameObject newParent,
List<Transform> skipped = null,
HashSet<Transform> unassigned = null,
Animator avatarAnimator = null,
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null
HashSet<Transform> unassigned = null
)
{
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
@ -368,55 +355,13 @@ namespace nadena.dev.modular_avatar.core.editor
var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
List<HumanBodyBones> bodyBones = null;
var isMapped = false;
if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
{
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
} else {
bodyBones = new List<HumanBodyBones> { outfitHumanoidBone };
}
} else {
bodyBones = new List<HumanBodyBones>() { outfitHumanoidBone };
}
}
if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out bodyBones))
if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out var bodyBones))
{
continue;
}
if (!isMapped)
{
foreach (var bodyBone in bodyBones)
{
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(bodyBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
break;
}
}
}
}
if (!isMapped)
{
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
@ -424,11 +369,9 @@ namespace nadena.dev.modular_avatar.core.editor
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
isMapped = true;
break;
}
}
}
if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null)
{
@ -445,7 +388,7 @@ namespace nadena.dev.modular_avatar.core.editor
return mappings;
}
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null, Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null, Animator avatarAnimator = null)
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return;
@ -456,7 +399,7 @@ namespace nadena.dev.modular_avatar.core.editor
void Traverse(Transform src, Transform dst)
{
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped);
foreach (var pair in mappings)
{

View File

@ -7,9 +7,6 @@ namespace nadena.dev.modular_avatar.core.editor
internal class AvatarObjectReferenceDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
label = EditorGUI.BeginProperty(position, label, property);
try
{
if (CustomGUI(position, property, label)) return;
@ -27,17 +24,11 @@ namespace nadena.dev.modular_avatar.core.editor
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
}
}
finally
{
EditorGUI.EndProperty();
}
}
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
{
var color = GUI.contentColor;
var targetObjectProp = property.FindPropertyRelative(nameof(AvatarObjectReference.targetObject));
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
try
@ -52,14 +43,6 @@ namespace nadena.dev.modular_avatar.core.editor
else if (isRoot) target = avatarTransform;
else target = avatarTransform.Find(property.stringValue);
if (targetObjectProp.objectReferenceValue is GameObject go &&
(go.transform == avatarTransform || go.transform.IsChildOf(avatarTransform)))
{
target = go.transform;
isNull = false;
isRoot = target == avatarTransform;
}
var labelRect = position;
position = EditorGUI.PrefixLabel(position, label);
labelRect.width = position.x - labelRect.x;
@ -90,8 +73,6 @@ namespace nadena.dev.modular_avatar.core.editor
property.stringValue = relPath;
}
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
}
}
else
@ -123,8 +104,6 @@ namespace nadena.dev.modular_avatar.core.editor
property.stringValue = relPath;
}
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
}
else
{

View File

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

View File

@ -20,14 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
private VisualElement _inner;
public new class UxmlFactory : UxmlFactory<LogoElement, UxmlTraits>
{
}
public new class UxmlTraits : VisualElement.UxmlTraits
{
}
private static void RegisterNode(LogoElement target)
{
if (_logoDisplayNode == null)

View File

@ -1,6 +1,5 @@
#region
using UnityEditor;
using UnityEngine.UIElements;
#endregion
@ -29,26 +28,11 @@ namespace nadena.dev.modular_avatar.core.editor
var image = new Image();
image.image = LogoDisplay.LOGO_ASSET;
SetImageSize(image);
image.style.width = new Length(LogoDisplay.ImageWidth(LogoDisplay.TARGET_HEIGHT), LengthUnit.Pixel);
image.style.height = new Length(LogoDisplay.TARGET_HEIGHT, LengthUnit.Pixel);
_inner.Add(image);
Add(_inner);
}
private static void SetImageSize(Image image, int maxTries = 10)
{
var targetHeight = LogoDisplay.TARGET_HEIGHT;
if (targetHeight == 0)
{
if (maxTries <= 0) return;
EditorApplication.delayCall += () => SetImageSize(image, maxTries - 1);
targetHeight = 45;
}
image.style.width = new Length(LogoDisplay.ImageWidth(targetHeight), LengthUnit.Pixel);
image.style.height = new Length(targetHeight, LengthUnit.Pixel);
}
}
}

View File

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

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 528c660b56905844ea2f88bc73837e9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -11,20 +11,7 @@ namespace nadena.dev.modular_avatar.core.editor
internal static class LogoDisplay
{
internal static readonly Texture2D LOGO_ASSET;
internal static float TARGET_HEIGHT
{
get {
try
{
return (EditorStyles.label?.lineHeight ?? 0) * 3;
}
catch (NullReferenceException)
{
// This can happen in early initialization...
return 0;
}
}
}
internal static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3;
internal static float ImageWidth(float height)
{

View File

@ -1,14 +0,0 @@
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarConvertConstraints))]
[CanEditMultipleObjects]
internal class MAConvertConstraintsEditor : MAEditorBase
{
protected override void OnInnerInspectorGUI()
{
// no UI
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 484ea04548b945ce9cf5fd6d49b50244
timeCreated: 1723778102

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,3 +0,0 @@
fileFormatVersion: 2
guid: 131d9706ddc04331bd09cf13b863c537
timeCreated: 1723334567

View File

@ -1,25 +0,0 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement name="root-box">
<ui:VisualElement name="group-box">
<ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/>
<ui:VisualElement name="ListViewContainer">
<ui:ListView virtualization-method="DynamicHeight"
reorder-mode="Animated"
reorderable="true"
show-add-remove-footer="true"
show-border="true"
show-foldout-header="false"
name="Shapes"
item-height="100"
binding-path="m_objects"
style="flex-grow: 1;"
/>
</ui:VisualElement>
</ui:VisualElement>
<ma:ROSimulatorButton/>
<ma:LanguageSwitcherElement/>
</ui:VisualElement>
</UXML>

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cd5c518316b2435d8a666911d4131903
timeCreated: 1723334567

View File

@ -1,81 +0,0 @@
#region
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomEditor(typeof(ModularAvatarMaterialSetter))]
public class MaterialSetterEditor : MAEditorBase
{
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
{
var root = uxml.CloneTree();
Localization.UI.Localize(root);
root.styleSheets.Add(uss);
root.Bind(serializedObject);
ROSimulatorButton.BindRefObject(root, target);
var listView = root.Q<ListView>("Shapes");
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarMaterialSetter;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarMaterialSetter>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarMaterialSetter targetComponent)
: base(targetElement, targetComponent) { }
protected override bool FilterGameObject(GameObject obj)
{
if (obj.TryGetComponent<Renderer>(out var renderer))
{
return renderer.sharedMaterials.Length > 0;
}
return false;
}
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Material Switch Objects");
foreach (var reference in references)
{
var materialSwitchObject = new MaterialSwitchObject { Object = reference, MaterialIndex = 0 };
TargetComponent.Objects.Add(materialSwitchObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}
}

View File

@ -1,13 +0,0 @@
fileFormatVersion: 2
guid: 339dd3848a2044b1aa04f543226de0e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- uss: {fileID: 7433441132597879392, guid: fce9f3fe74434b718abac5ea66775acb, type: 3}
- uxml: {fileID: 9197481963319205126, guid: cd5c518316b2435d8a666911d4131903, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,74 +0,0 @@
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
border-width: 3px;
border-left-color: rgba(0, 1, 0, 0.2);
border-top-color: rgba(0, 1, 0, 0.2);
border-right-color: rgba(0, 1, 0, 0.2);
border-bottom-color: rgba(0, 1, 0, 0.2);
border-radius: 4px;
/* background-color: rgba(0, 0, 0, 0.1); */
}
#group-box > Label {
-unity-font-style: bold;
}
#ListViewContainer {
margin-top: 4px;
}
.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.horizontal > Label {
height: auto;
}
.horizontal > PropertyField > * {
margin: 0;
}
#f-object {
flex-grow: 1;
}
#f-material-index {
display: none;
}
#f-material-index-dropdown {
width: 100px;
}
#f-material-index-original {
flex-grow: 1;
}
.horizontal > Label {
width: 100px;
}
#f-material {
flex-grow: 1;
}
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: fce9f3fe74434b718abac5ea66775acb
timeCreated: 1723334567

View File

@ -1,157 +0,0 @@
#region
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomPropertyDrawer(typeof(MaterialSwitchObject))]
public class MaterialSwitchObjectEditor : PropertyDrawer
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/MaterialSetter/";
private const string UxmlPath = Root + "MaterialSwitchObjectEditor.uxml";
private const string UssPath = Root + "MaterialSetterStyles.uss";
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
Localization.UI.Localize(uxml);
uxml.styleSheets.Add(uss);
uxml.BindProperty(property);
var f_material_index = uxml.Q<IntegerField>("f-material-index");
var f_material_index_dropdown = uxml.Q<DropdownField>("f-material-index-dropdown");
var f_material_index_original = uxml.Q<ObjectField>("f-material-index-original");
var f_object = uxml.Q<PropertyField>("f-object");
f_object.RegisterValueChangeCallback(evt =>
{
EditorApplication.delayCall += UpdateMaterialDropdown;
});
UpdateMaterialDropdown();
// Link dropdown and original field to material index field
f_material_index.RegisterValueChangedCallback(evt =>
{
f_material_index_dropdown.SetValueWithoutNotify(evt.newValue.ToString());
UpdateOriginalMaterial();
});
f_material_index_dropdown.RegisterValueChangedCallback(evt =>
{
if (evt.newValue != null && int.TryParse(evt.newValue, out var i))
{
f_material_index.value = i;
}
});
f_material_index_original.SetEnabled(false);
return uxml;
void UpdateMaterialDropdown()
{
var sharedMaterials = GetSharedMaterials();
if (sharedMaterials != null)
{
var matCount = sharedMaterials.Length;
f_material_index_dropdown.SetEnabled(true);
f_material_index_dropdown.choices.Clear();
for (int i = 0; i < matCount; i++)
{
f_material_index_dropdown.choices.Add(i.ToString());
}
f_material_index_dropdown.formatListItemCallback = idx_s =>
{
if (string.IsNullOrWhiteSpace(idx_s)) return "";
var idx = int.Parse(idx_s);
if (idx < 0 || idx >= sharedMaterials.Length)
{
return $"<color=\"red\">Element {idx_s}: <???></color>";
}
else if (sharedMaterials[idx] == null)
{
return $"Element {idx_s}: <None>";
}
else
{
return $"Element {idx_s}: {sharedMaterials[idx].name}";
}
};
f_material_index_dropdown.formatSelectedValueCallback = idx_s =>
{
if (string.IsNullOrWhiteSpace(idx_s)) return "";
var idx = int.Parse(idx_s);
if (idx < 0 || idx >= sharedMaterials.Length)
{
return $"<color=\"red\">Element {idx_s}</color>";
}
else
{
return $"Element {idx_s}";
}
};
}
else
{
f_material_index_dropdown.SetEnabled(false);
if (f_material_index_dropdown.choices.Count == 0)
{
f_material_index_dropdown.choices.Add("0");
}
f_material_index_dropdown.formatListItemCallback = idx_s => "<Missing Renderer>";
f_material_index_dropdown.formatSelectedValueCallback = f_material_index_dropdown.formatListItemCallback;
}
UpdateOriginalMaterial();
}
void UpdateOriginalMaterial()
{
var sharedMaterials = GetSharedMaterials();
if (sharedMaterials != null)
{
var idx = f_material_index.value;
if (idx < 0 || idx >= sharedMaterials.Length)
{
f_material_index_original.SetValueWithoutNotify(null);
}
else
{
f_material_index_original.SetValueWithoutNotify(sharedMaterials[idx]);
}
}
else
{
f_material_index_original.SetValueWithoutNotify(null);
}
}
Material[] GetSharedMaterials()
{
var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
try
{
return targetObject?.GetComponent<Renderer>()?.sharedMaterials;
}
catch (MissingComponentException)
{
return null;
}
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6361a17f884644988ef3ece7fbe73ab7
timeCreated: 1723334567

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 55b5e53f6c364089a1871b68e0de17c6
timeCreated: 1723334567

View File

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

View File

@ -1,19 +1,13 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using nadena.dev.modular_avatar.core.menu;
using nadena.dev.ndmf;
using nadena.dev.ndmf.preview;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -23,44 +17,8 @@ namespace nadena.dev.modular_avatar.core.editor
protected override string localizationPrefix => "submenu_source";
}
internal static class ParameterIntrospectionCache
{
internal static PropCache<GameObject, ImmutableList<ProvidedParameter>> ProvidedParameterCache =
new("GetParametersForObject", GetParametersForObject_miss);
internal static PropCache<GameObject, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>>
ParameterRemappingCache = new("GetParameterRemappingsAt", GetParameterRemappingsAt_miss);
private static ImmutableList<ProvidedParameter> GetParametersForObject_miss(ComputeContext ctx, GameObject obj)
{
if (obj == null) return ImmutableList<ProvidedParameter>.Empty;
return ParameterInfo.ForPreview(ctx).GetParametersForObject(obj).ToImmutableList();
}
private static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>
GetParameterRemappingsAt_miss(ComputeContext ctx, GameObject obj)
{
if (obj == null) return ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>.Empty;
return ParameterInfo.ForPreview(ctx).GetParameterRemappingsAt(obj);
}
internal static ImmutableList<ProvidedParameter> GetParametersForObject(GameObject avatar)
{
return ProvidedParameterCache.Get(ComputeContext.NullContext, avatar);
}
internal static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> GetParameterRemappingsAt(GameObject avatar)
{
return ParameterRemappingCache.Get(ComputeContext.NullContext, avatar);
}
}
internal class MenuItemCoreGUI
{
private const string ImpliesRichText = "<";
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
private readonly GameObject _parameterReference;
private readonly Action _redraw;
@ -74,7 +32,6 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _submenu;
private readonly ParameterGUI _parameterGUI;
private readonly SerializedProperty _parameterName;
private readonly SerializedProperty _subParamsRoot;
private readonly SerializedProperty _labelsRoot;
@ -89,20 +46,9 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _prop_submenuSource;
private readonly SerializedProperty _prop_otherObjSource;
private readonly SerializedProperty _prop_isSynced;
private readonly SerializedProperty _prop_isSaved;
private readonly SerializedProperty _prop_isDefault;
private readonly SerializedProperty _prop_automaticValue;
private readonly SerializedProperty _prop_label;
public bool AlwaysExpandContents = false;
public bool ExpandContents = false;
private readonly Dictionary<string, ProvidedParameter> _knownParameters = new();
private bool _parameterSourceNotDetermined;
private bool _useLabel;
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
{
_obj = obj;
@ -116,11 +62,9 @@ namespace nadena.dev.modular_avatar.core.editor
_parameterReference = parameterReference;
_redraw = redraw;
InitKnownParameters();
var gameObjects = new SerializedObject(
obj.targetObjects.Select(o =>
(Object) ((ModularAvatarMenuItem) o).gameObject
(UnityEngine.Object) ((ModularAvatarMenuItem) o).gameObject
).ToArray()
);
@ -130,76 +74,21 @@ namespace nadena.dev.modular_avatar.core.editor
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
_parameterName = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
var parameter = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
_submenu = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
_subParamsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
_prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
_prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue));
_prop_label = obj.FindProperty(nameof(ModularAvatarMenuItem.label));
_previewGUI = new MenuPreviewGUI(redraw);
}
private void InitKnownParameters()
{
var paramRef = _parameterReference;
if (_parameterReference == null)
// TODO: This could give incorrect results in some cases when we have multiple objects selected with
// different rename contexts.
paramRef = (_obj.targetObjects[0] as Component)?.gameObject;
if (paramRef == null)
{
_parameterSourceNotDetermined = true;
return;
}
var parentAvatar = RuntimeUtil.FindAvatarInParents(paramRef.transform);
if (parentAvatar == null)
{
_parameterSourceNotDetermined = true;
return;
}
Dictionary<string, ProvidedParameter> rootParameters = new();
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
.Where(p => p.Namespace == ParameterNamespace.Animator)
)
{
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
{
rootParameters[param.EffectiveName] = param;
}
}
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
foreach (var remap in remaps)
{
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
if (rootParameters.ContainsKey(remap.Value.ParameterName))
_knownParameters[remap.Key.Item2] = rootParameters[remap.Value.ParameterName];
}
foreach (var rootParam in rootParameters)
if (!remaps.ContainsKey((ParameterNamespace.Animator, rootParam.Key)))
_knownParameters[rootParam.Key] = rootParam.Value;
}
/// <summary>
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
/// </summary>
@ -210,136 +99,43 @@ namespace nadena.dev.modular_avatar.core.editor
{
_obj = _control.serializedObject;
_parameterReference = parameterReference;
InitKnownParameters();
_redraw = redraw;
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
_type = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
_parameterName = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
var parameter = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
_labelsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
_prop_isSynced = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isDefault));
_prop_automaticValue = null;
_prop_submenuSource = null;
_prop_otherObjSource = null;
_previewGUI = new MenuPreviewGUI(redraw);
}
private void DrawHorizontalToggleProp(
SerializedProperty prop,
GUIContent label,
bool? forceMixedValues = null,
bool? forceValue = null
)
{
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
var labelSize = EditorStyles.label.CalcSize(label);
var width = toggleSize.x + labelSize.x + 4;
var rect = EditorGUILayout.GetControlRect(GUILayout.Width(width));
EditorGUI.BeginProperty(rect, label, prop);
if (forceMixedValues != null) EditorGUI.showMixedValue = forceMixedValues.Value;
EditorGUI.BeginChangeCheck();
var value = EditorGUI.ToggleLeft(rect, label, forceValue ?? prop.boolValue);
if (EditorGUI.EndChangeCheck()) prop.boolValue = value;
EditorGUI.EndProperty();
}
public void DoGUI()
{
if (_obj != null) _obj.UpdateIfRequiredOrScript();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
if (_prop_label == null)
{
EditorGUI.BeginChangeCheck();
if (_obj != null && _obj.isEditingMultipleObjects)
{
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
}
else
{
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
}
if (EditorGUI.EndChangeCheck())
{
_name.serializedObject.ApplyModifiedProperties();
}
}
else
{
_useLabel |= !string.IsNullOrEmpty(_prop_label.stringValue);
if (!_useLabel)
{
EditorGUI.BeginChangeCheck();
var previousName = _name.stringValue;
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
if (EditorGUI.EndChangeCheck())
{
if (!previousName.Contains(ImpliesRichText) && _name.stringValue.Contains(ImpliesRichText))
{
_prop_label.stringValue = _name.stringValue;
}
else
{
_name.serializedObject.ApplyModifiedProperties();
}
}
}
else
{
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
}
var linkIcon = EditorGUIUtility.IconContent(_useLabel ? "UnLinked" : "Linked").image;
var guiIcon = new GUIContent(linkIcon, S(_useLabel ? "menuitem.label.gameobject_name.tooltip" : "menuitem.label.long_name.tooltip"));
if (GUILayout.Button(guiIcon, GUILayout.Height(EditorGUIUtility.singleLineHeight), GUILayout.Width(25)))
{
_prop_label.stringValue = !_useLabel ? _name.stringValue : "";
_useLabel = !_useLabel;
}
}
EditorGUILayout.EndHorizontal();
if (_useLabel && _prop_label.stringValue.Contains(ImpliesRichText))
{
var style = new GUIStyle(EditorStyles.textField);
style.richText = true;
style.alignment = TextAnchor.MiddleCenter;
EditorGUILayout.LabelField(" ", _prop_label.stringValue, style, GUILayout.Height(EditorGUIUtility.singleLineHeight * 3));
}
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
DoValueField();
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
_parameterGUI.DoGUI(true);
ShowInnateParameterGUI();
EditorGUILayout.EndVertical();
if (_texture != null)
@ -371,9 +167,10 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.BeginVertical();
if (_type.hasMultipleDifferentValues) return;
var controlTypeArray = Enum.GetValues(typeof(VRCExpressionsMenu.Control.ControlType));
var index = Math.Clamp(_type.enumValueIndex, 0, controlTypeArray.Length - 1);
var type = (VRCExpressionsMenu.Control.ControlType)controlTypeArray.GetValue(index);
VRCExpressionsMenu.Control.ControlType type =
(VRCExpressionsMenu.Control.ControlType) Enum
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
.GetValue(_type.enumValueIndex);
switch (type)
{
@ -532,243 +329,6 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private void ShowInnateParameterGUI()
{
if (_prop_isDefault == null)
// This is probably coming from a VRC Expressions menu asset.
// For now, don't show the UI in this case.
return;
var multipleSelections = _obj.targetObjects.Length > 1;
var paramName = _parameterName.stringValue;
var siblings = FindSiblingMenuItems(_obj);
EditorGUILayout.BeginHorizontal();
var forceMixedValues = _parameterName.hasMultipleDifferentValues;
var syncedIsMixed = forceMixedValues || _prop_isSynced.hasMultipleDifferentValues ||
siblings != null && siblings.Any(s => s.isSynced != _prop_isSynced.boolValue);
var savedIsMixed = forceMixedValues || _prop_isSaved.hasMultipleDifferentValues ||
siblings != null && siblings.Any(s => s.isSaved != _prop_isSaved.boolValue);
var knownParameter = _parameterName.hasMultipleDifferentValues
? null
: _knownParameters.GetValueOrDefault(paramName);
var knownSource = knownParameter?.Source;
var externalSource = knownSource != null && knownSource is not ModularAvatarMenuItem;
if (externalSource) savedIsMixed = true; // NDMF doesn't yet support querying for the saved state
var forceSyncedValue = externalSource ? knownParameter?.WantSynced : null;
var knownParamDefault = knownParameter?.DefaultValue;
var isDefaultByKnownParam =
knownParamDefault != null ? _value.floatValue == knownParamDefault : (bool?)null;
if (knownParameter != null && knownParameter.Source is ModularAvatarMenuItem)
isDefaultByKnownParam = null;
if (_prop_automaticValue?.boolValue == true) isDefaultByKnownParam = null;
Object controller = knownParameter?.Source;
// If we can't figure out what to reference the parameter names to, or if they're controlled by something
// other than the Menu Item component itself, disable the UI
var controllerIsElsewhere = externalSource || _parameterSourceNotDetermined;
using (new EditorGUI.DisabledScope(
_parameterName.hasMultipleDifferentValues || controllerIsElsewhere)
)
{
// If we have multiple menu items selected, it probably doesn't make sense to make them all default.
// But, we do want to see if _any_ are default.
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
var mixedIsDefault = multipleSelections && anyIsDefault;
var allAreAutoParams = !_parameterName.hasMultipleDifferentValues &&
string.IsNullOrWhiteSpace(_parameterName.stringValue);
using (new EditorGUI.DisabledScope((!allAreAutoParams && multipleSelections) ||
isDefaultByKnownParam != null))
{
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
isDefaultByKnownParam);
if (EditorGUI.EndChangeCheck())
{
_obj.ApplyModifiedProperties();
ClearConflictingDefaults(siblings);
}
}
GUILayout.FlexibleSpace();
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), savedIsMixed);
if (EditorGUI.EndChangeCheck() && siblings != null)
foreach (var sibling in siblings)
{
sibling.isSaved = _prop_isSaved.boolValue;
EditorUtility.SetDirty(sibling);
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
}
GUILayout.FlexibleSpace();
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), syncedIsMixed,
forceSyncedValue);
if (EditorGUI.EndChangeCheck() && siblings != null)
foreach (var sibling in siblings)
{
sibling.isSynced = _prop_isSynced.boolValue;
EditorUtility.SetDirty(sibling);
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
}
}
if (controllerIsElsewhere)
{
var refStyle = EditorStyles.toggle;
var refContent = new GUIContent("test");
var refRect = refStyle.CalcSize(refContent);
var height = refRect.y + EditorStyles.toggle.margin.top + EditorStyles.toggle.margin.bottom;
GUILayout.FlexibleSpace();
var style = new GUIStyle(EditorStyles.miniButton);
style.fixedWidth = 0;
style.fixedHeight = 0;
style.stretchHeight = true;
style.stretchWidth = true;
style.imagePosition = ImagePosition.ImageOnly;
var icon = EditorGUIUtility.FindTexture("d_Search Icon");
var rect = GUILayoutUtility.GetRect(new GUIContent(), style, GUILayout.ExpandWidth(false),
GUILayout.Width(height), GUILayout.Height(height));
if (GUI.Button(rect, new GUIContent(), style))
{
if (controller is VRCAvatarDescriptor desc) controller = desc.expressionParameters;
Selection.activeObject = controller;
EditorGUIUtility.PingObject(controller);
}
rect.xMin += 2;
rect.yMin += 2;
rect.xMax -= 2;
rect.yMax -= 2;
GUI.DrawTexture(rect, icon);
}
EditorGUILayout.EndHorizontal();
}
private void DoValueField()
{
var value_label = G("menuitem.prop.value");
var auto_label = G("menuitem.prop.automatic_value");
if (_prop_automaticValue == null)
{
EditorGUILayout.PropertyField(_value, value_label);
return;
}
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
var autoLabelSize = EditorStyles.label.CalcSize(auto_label);
var style = EditorStyles.numberField;
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, style);
var valueRect = rect;
valueRect.xMax -= toggleSize.x + autoLabelSize.x + 4;
var autoRect = rect;
autoRect.xMin = valueRect.xMax + 4;
var suppressValue = _prop_automaticValue.boolValue || _prop_automaticValue.hasMultipleDifferentValues;
using (new EditorGUI.DisabledScope(suppressValue))
{
if (suppressValue)
{
EditorGUI.TextField(valueRect, value_label, "", style);
}
else
{
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(valueRect, _value, value_label);
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = false;
}
}
EditorGUI.BeginProperty(autoRect, auto_label, _prop_automaticValue);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = _prop_automaticValue.hasMultipleDifferentValues;
var autoValue = EditorGUI.ToggleLeft(autoRect, auto_label, _prop_automaticValue.boolValue);
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = autoValue;
EditorGUI.EndProperty();
}
private List<ModularAvatarMenuItem> FindSiblingMenuItems(SerializedObject serializedObject)
{
if (serializedObject == null || serializedObject.isEditingMultipleObjects) return null;
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
if (myMenuItem == null) return null;
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
if (avatarRoot == null) return null;
var myParameterName = myMenuItem.Control.parameter.name;
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
var myMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(myMenuItem.gameObject);
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
myParameterName = myReplacement.ParameterName;
var siblings = new List<ModularAvatarMenuItem>();
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))
{
if (otherMenuItem == myMenuItem) continue;
var otherParameterName = otherMenuItem.Control.parameter.name;
if (string.IsNullOrEmpty(otherParameterName)) continue;
var otherMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(otherMenuItem.gameObject);
if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName),
out var otherReplacement))
otherParameterName = otherReplacement.ParameterName;
if (otherParameterName != myParameterName) continue;
siblings.Add(otherMenuItem);
}
return siblings;
}
private void ClearConflictingDefaults(List<ModularAvatarMenuItem> siblingItems)
{
var siblings = siblingItems;
if (siblings == null) return;
foreach (var otherMenuItem in siblings)
{
if (otherMenuItem.isDefault)
{
Undo.RecordObject(otherMenuItem, "");
otherMenuItem.isDefault = false;
EditorUtility.SetDirty(otherMenuItem);
PrefabUtility.RecordPrefabInstancePropertyModifications(otherMenuItem);
}
}
}
private void EnsureLabelCount(int i)
{
if (_labels == null || _labelsRoot.arraySize < i || _labels.Length < i)

View File

@ -206,40 +206,14 @@ namespace nadena.dev.modular_avatar.core.editor
if (source is MenuNodesUnder nodesUnder)
{
GUILayout.BeginHorizontal();
if (GUILayout.Button(G("menuitem.misc.add_item")))
{
var newChild = new GameObject();
newChild.name = "New item";
newChild.transform.SetParent(nodesUnder.root.transform, false);
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
newChild.AddComponent<ModularAvatarMenuItem>();
Undo.RegisterCreatedObjectUndo(newChild, "Added menu item");
}
if (GUILayout.Button(G("menuitem.misc.add_toggle")))
{
var newChild = new GameObject();
newChild.name = "New toggle";
newChild.transform.SetParent(nodesUnder.root.transform, false);
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control()
{
type = VRCExpressionsMenu.Control.ControlType.Toggle,
value = 1,
};
newChild.AddComponent<ModularAvatarObjectToggle>();
Selection.activeObject = newChild;
Undo.RegisterCreatedObjectUndo(newChild, "Added menu toggle");
}
GUILayout.EndHorizontal();
}
}
}

View File

@ -275,11 +275,10 @@ namespace nadena.dev.modular_avatar.core.editor
}
var desc = node?.GetComponent<VRCAvatarDescriptor>();
if (desc?.expressionParameters?.parameters != null)
if (desc != null)
{
foreach (var param in desc.expressionParameters.parameters)
{
if (param == null) continue;
if (emitted.Add(param.name)) yield return (node, param.name);
}
}

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 7e15fef260544783af5ff1fd5f13acd3
timeCreated: 1723341065

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"));
}
serializedObject.ApplyModifiedProperties();

View File

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

View File

@ -1,7 +1,7 @@
#if MA_VRCSDK3_AVATARS
using UnityEditor;
using 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

@ -54,7 +54,7 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.LabelField(G("mesh_settings.header_probe_anchor"), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_prop_inherit_probe_anchor, G("mesh_settings.inherit_probe_anchor"));
if (_prop_inherit_probe_anchor.enumValueIndex is (int) ModularAvatarMeshSettings.InheritMode.Set or (int) ModularAvatarMeshSettings.InheritMode.SetOrInherit)
if (_prop_inherit_probe_anchor.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
{
EditorGUILayout.PropertyField(_prop_probe_anchor, G("mesh_settings.probe_anchor"));
}
@ -72,7 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.LabelField(G("mesh_settings.header_bounds"), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_prop_inherit_bounds, G("mesh_settings.inherit_bounds"));
if (_prop_inherit_bounds.enumValueIndex is (int) ModularAvatarMeshSettings.InheritMode.Set or (int) ModularAvatarMeshSettings.InheritMode.SetOrInherit)
if (_prop_inherit_bounds.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
{
EditorGUILayout.PropertyField(_prop_root_bone, G("mesh_settings.root_bone"));
EditorGUILayout.PropertyField(_prop_bounds, G("mesh_settings.bounds"));

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 61045dcdc7f24658a5b47fb0b67ab9fe
timeCreated: 1722736548

View File

@ -1,25 +0,0 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement name="root-box">
<ui:VisualElement name="group-box">
<ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/>
<ui:VisualElement name="ListViewContainer">
<ui:ListView virtualization-method="DynamicHeight"
reorder-mode="Animated"
reorderable="true"
show-add-remove-footer="true"
show-border="true"
show-foldout-header="false"
name="Shapes"
item-height="100"
binding-path="m_objects"
style="flex-grow: 1;"
/>
</ui:VisualElement>
</ui:VisualElement>
<ma:ROSimulatorButton/>
<ma:LanguageSwitcherElement/>
</ui:VisualElement>
</UXML>

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 02f9cb4b3be34457870f111d73e2fd2f
timeCreated: 1722736548

View File

@ -1,75 +0,0 @@
#region
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomEditor(typeof(ModularAvatarObjectToggle))]
public class ObjectSwitcherEditor : MAEditorBase
{
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
}
protected override VisualElement CreateInnerInspectorGUI()
{
var root = uxml.CloneTree();
Localization.UI.Localize(root);
root.styleSheets.Add(uss);
root.Bind(serializedObject);
ROSimulatorButton.BindRefObject(root, target);
var listView = root.Q<ListView>("Shapes");
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
: base(targetElement, targetComponent) { }
protected override bool AllowKnownObjects => false;
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
foreach (var reference in references)
{
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}
}

View File

@ -1,13 +0,0 @@
fileFormatVersion: 2
guid: a77f3b5f35d04831a7896261cabd3370
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- uss: {fileID: 7433441132597879392, guid: b7559b81cea245b68c66602ea0cbbbcf, type: 3}
- uxml: {fileID: 9197481963319205126, guid: 02f9cb4b3be34457870f111d73e2fd2f, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,62 +0,0 @@
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
border-width: 3px;
border-left-color: rgba(0, 1, 0, 0.2);
border-top-color: rgba(0, 1, 0, 0.2);
border-right-color: rgba(0, 1, 0, 0.2);
border-bottom-color: rgba(0, 1, 0, 0.2);
border-radius: 4px;
/* background-color: rgba(0, 0, 0, 0.1); */
}
#group-box > Label {
-unity-font-style: bold;
}
#ListViewContainer {
margin-top: 4px;
}
.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 1px 0;
}
.horizontal > * {
height: 18px;
margin: 0 1px;
}
.horizontal > Label {
height: auto;
}
.horizontal > PropertyField > * {
margin: 0;
}
#f-object {
flex-grow: 1;
}
#f-active {
display: none;
}
#f-active-dropdown {
width: 60px;
}
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b7559b81cea245b68c66602ea0cbbbcf
timeCreated: 1722736548

View File

@ -1,48 +0,0 @@
#region
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomPropertyDrawer(typeof(ToggledObject))]
public class ToggledObjectEditor : PropertyDrawer
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ObjectToggle/";
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
private const string V_On = "ON";
private const string V_Off = "OFF";
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
Localization.UI.Localize(uxml);
uxml.styleSheets.Add(uss);
uxml.BindProperty(property);
var f_active = uxml.Q<Toggle>("f-active");
var f_active_dropdown = uxml.Q<DropdownField>("f-active-dropdown");
f_active_dropdown.choices.Add(V_On);
f_active_dropdown.choices.Add(V_Off);
f_active.RegisterValueChangedCallback(evt =>
{
f_active_dropdown.SetValueWithoutNotify(evt.newValue ? V_On : V_Off);
});
f_active_dropdown.RegisterValueChangedCallback(evt =>
{
f_active.value = evt.newValue == V_On;
});
return uxml;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 0d18528c5f704d3daf1160d9672bd09e
timeCreated: 1722736548

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 565803cb95a04d1f98f7050c18234cdd
timeCreated: 1722736548

View File

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

View File

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

View File

@ -2,9 +2,8 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using Toggle = UnityEngine.UIElements.Toggle;
namespace nadena.dev.modular_avatar.core.editor.Parameters
{
@ -21,251 +20,105 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
Localization.UI.Localize(root);
root.styleSheets.Add(uss);
var f_type = root.Q<DropdownField>("f-type");
var f_sync_type = root.Q<DropdownField>("f-sync-type");
var f_is_prefix = root.Q<VisualElement>("f-is-prefix");
SetupPairedDropdownField(
root,
f_type,
f_sync_type,
f_is_prefix,
("Bool", "False", "params.syncmode.Bool"),
("Float", "False", "params.syncmode.Float"),
("Int", "False", "params.syncmode.Int"),
("Not Synced", "False", "params.syncmode.NotSynced"),
(null, "True", "params.syncmode.PhysBonesPrefix")
);
var f_default = root.Q<DefaultValueField>();
f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index);
f_sync_type.RegisterValueChangedCallback(evt => f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index));
var f_synced = root.Q<Toggle>("f-synced");
var f_local_only = root.Q<Toggle>("f-local-only");
// Invert f_local_only and f_synced
f_local_only.RegisterValueChangedCallback(evt => { f_synced.SetValueWithoutNotify(!evt.newValue); });
f_synced.RegisterValueChangedCallback(evt => { f_local_only.value = !evt.newValue; });
var internalParamAccessor = root.Q<Toggle>("f-internal-parameter");
internalParamAccessor.RegisterValueChangedCallback(evt =>
var foldout = root.Q<Foldout>();
var foldoutLabel = foldout?.Q<Label>();
if (foldoutLabel != null)
{
if (evt.newValue)
root.AddToClassList("st-internal-parameter");
else
root.RemoveFromClassList("st-internal-parameter");
});
foldoutLabel.bindingPath = "nameOrPrefix";
}
root.Q<VisualElement>("remap-to-group-disabled").SetEnabled(false);
var miniDisplay = root.Q<VisualElement>("MiniDisplay");
miniDisplay.RemoveFromHierarchy();
foldoutLabel.parent.Add(miniDisplay);
miniDisplay.styleSheets.Add(uss);
var name = root.Q<TextField>("f-name");
var remapTo = root.Q<TextField>("f-remap-to");
var remapToInner = remapTo.Q<TextElement>();
var remapToPlaceholder = root.Q<Label>("f-remap-to-placeholder");
remapToPlaceholder.pickingMode = PickingMode.Ignore;
Action updateRemapToPlaceholder = () =>
var isPrefixProp = root.Q<PropertyField>("isPrefix");
bool isPrefix = false;
Action evaluateMiniDisplay = () =>
{
if (string.IsNullOrWhiteSpace(remapTo.value))
remapToPlaceholder.text = name.value;
else
remapToPlaceholder.text = "";
miniDisplay.style.display = (isPrefix || foldout.value) ? DisplayStyle.None : DisplayStyle.Flex;
};
name.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
remapTo.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
foldout.RegisterValueChangedCallback(evt => evaluateMiniDisplay());
remapToPlaceholder.RemoveFromHierarchy();
remapToInner.Add(remapToPlaceholder);
updateRemapToPlaceholder();
foreach (var elem in root.Query<TextElement>().Build())
isPrefixProp.RegisterValueChangeCallback(evt =>
{
// Prevent delete keypresses from bubbling up if we're in a text field
elem.RegisterCallback<KeyDownEvent>(evt =>
var value = evt.changedProperty.boolValue;
if (value)
{
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
evt.StopPropagation();
});
root.AddToClassList("ParameterConfig__isPrefix_true");
root.RemoveFromClassList("ParameterConfig__isPrefix_false");
}
else
{
root.AddToClassList("ParameterConfig__isPrefix_false");
root.RemoveFromClassList("ParameterConfig__isPrefix_true");
}
isPrefix = value;
evaluateMiniDisplay();
});
var syncTypeProp = root.Q<PropertyField>("syncType");
// TODO: This callback is not actually invoked on initial bind...
syncTypeProp.RegisterValueChangeCallback(evt =>
{
var value = (ParameterSyncType) evt.changedProperty.enumValueIndex;
if (value == ParameterSyncType.NotSynced)
{
root.AddToClassList("ParameterConfig__animatorOnly_true");
root.RemoveFromClassList("ParameterConfig__animatorOnly_false");
}
else
{
root.AddToClassList("ParameterConfig__animatorOnly_false");
root.RemoveFromClassList("ParameterConfig__animatorOnly_true");
}
});
/*
var overridePlaceholder = root.Q<Toggle>("overridePlaceholder");
overridePlaceholder.labelElement.AddToClassList("ndmf-tr");
overridePlaceholder.SetEnabled(false);
*/
var remapTo = root.Q<PropertyField>("remapTo");
var remapToPlaceholder = root.Q<TextField>("remapToPlaceholder");
remapToPlaceholder.labelElement.AddToClassList("ndmf-tr");
remapToPlaceholder.SetEnabled(false);
Localization.UI.Localize(remapToPlaceholder.labelElement);
root.Q<PropertyField>("internalParameter").RegisterValueChangeCallback(evt =>
{
remapTo.style.display = evt.changedProperty.boolValue ? DisplayStyle.None : DisplayStyle.Flex;
remapToPlaceholder.style.display = evt.changedProperty.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
});
// This is a bit of a hack, but I'm not sure of another way to properly align property labels with a custom
// field, when we only want to manipulate a subset of fields on an object...
var defaultValueField = root.Q<VisualElement>("innerDefaultValueField"); // create ahead of time so it's bound...
// Then move it into the property field once the property field has created its inner controls
var defaultValueProp = root.Q<PropertyField>("defaultValueProp");
defaultValueProp.RegisterCallback<GeometryChangedEvent>(evt =>
{
var floatField = defaultValueProp.Q<FloatField>();
var innerField = floatField?.Q<DefaultValueField>();
if (floatField != null && innerField == null)
{
defaultValueField.RemoveFromHierarchy();
floatField.contentContainer.Add(defaultValueField);
}
});
return root;
}
private interface Accessor
{
Action<string> OnValueChanged { get; set; }
string Value { get; set; }
}
private class ToggleAccessor : Accessor
{
private readonly Toggle _toggle;
public ToggleAccessor(Toggle toggle)
{
_toggle = toggle;
_toggle.RegisterValueChangedCallback(evt => OnValueChanged?.Invoke(evt.newValue.ToString()));
}
public Action<string> OnValueChanged { get; set; }
public string Value
{
get => _toggle.value.ToString();
set => _toggle.value = value == "True";
}
}
private class DropdownAccessor : Accessor
{
private readonly DropdownField _dropdown;
public DropdownAccessor(DropdownField dropdown)
{
_dropdown = dropdown;
_dropdown.RegisterValueChangedCallback(evt => OnValueChanged?.Invoke(evt.newValue));
}
public Action<string> OnValueChanged { get; set; }
public string Value
{
get => _dropdown.value;
set => _dropdown.value = value;
}
}
private Accessor GetAccessor(VisualElement elem)
{
var toggle = elem.Q<Toggle>();
if (toggle != null) return new ToggleAccessor(toggle);
var dropdown = elem.Q<DropdownField>();
if (dropdown != null)
{
return new DropdownAccessor(dropdown);
}
throw new ArgumentException("Unsupported element type");
}
private void SetupPairedDropdownField(
VisualElement root,
DropdownField target,
VisualElement v_type,
VisualElement v_pbPrefix,
// p1, p2, localization key
params (string, string, string)[] choices
)
{
var p_type = GetAccessor(v_type);
var p_prefix = GetAccessor(v_pbPrefix);
for (var i = 0; i < choices.Length; i++) target.choices.Add("" + i);
target.formatListItemCallback = s_n =>
{
if (int.TryParse(s_n, out var n) && n >= 0 && n < choices.Length)
{
return Localization.S(choices[n].Item3);
}
else
{
return "";
}
};
target.formatSelectedValueCallback = target.formatListItemCallback;
var inLoop = false;
string current_type_class = null;
target.RegisterValueChangedCallback(evt =>
{
if (inLoop) return;
if (int.TryParse(evt.newValue, out var n) && n >= 0 && n < choices.Length)
{
p_type.Value = choices[n].Item1;
p_prefix.Value = choices[n].Item2;
}
else
{
p_type.Value = "";
p_prefix.Value = "";
}
});
p_type.OnValueChanged = s =>
{
inLoop = true;
try
{
if (!string.IsNullOrWhiteSpace(s))
{
var new_class = "st-ty-" + s.Replace(" ", "-");
root.RemoveFromClassList(current_type_class);
current_type_class = null;
root.AddToClassList(new_class);
current_type_class = new_class;
}
if (string.IsNullOrEmpty(s)) return;
for (var i = 0; i < choices.Length; i++)
if (choices[i].Item1 == s && (choices[i].Item2 == null || choices[i].Item2 == p_prefix.Value))
{
target.SetValueWithoutNotify("" + i);
break;
}
}
finally
{
inLoop = false;
}
};
p_prefix.OnValueChanged = s =>
{
inLoop = true;
try
{
if (string.IsNullOrEmpty(s)) return;
if (bool.TryParse(s, out var b))
{
if (b) root.AddToClassList("st-pb-prefix");
else root.RemoveFromClassList("st-pb-prefix");
}
for (var i = 0; i < choices.Length; i++)
if ((choices[i].Item1 == null || choices[i].Item1 == p_type.Value) && choices[i].Item2 == s)
{
target.SetValueWithoutNotify("" + i);
break;
}
}
finally
{
inLoop = false;
}
};
inLoop = true;
for (var i = 0; i < choices.Length; i++)
if (choices[i].Item1 == p_type.Value && choices[i].Item2 == p_prefix.Value)
{
target.SetValueWithoutNotify("" + i);
break;
}
inLoop = false;
}
}
}
#endif

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