mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-04-06 04:28:59 +08:00
Compare commits
255 Commits
1.10.0-rc.
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
897d168137 | ||
|
400960257e | ||
|
f0fcbb66b1 | ||
|
7fd35bb49a | ||
|
bc4c6628ee | ||
|
b733ce2e4d | ||
|
3324d3f71b | ||
|
c3d2cfb29f | ||
|
7610020c3b | ||
|
89de978c77 | ||
|
db9389052c | ||
|
81aed5b798 | ||
|
706ce7aa2f | ||
|
b75e74ef84 | ||
|
8ef4cf6328 | ||
|
c521bd7721 | ||
|
e46e958f39 | ||
|
8a45515af0 | ||
|
36b442f904 | ||
|
124392c422 | ||
|
dff7f03c2f | ||
|
713a0d3b1d | ||
|
e2a02982d5 | ||
|
672dd8b31f | ||
|
6175e20e46 | ||
|
fce938820b | ||
|
34deac5681 | ||
|
b49e5cb460 | ||
|
3165d471b5 | ||
|
2d59c74066 | ||
|
55d744885f | ||
|
318d65f3b5 | ||
|
aac70873c5 | ||
|
5a17d6ea9a | ||
|
62fd986fd0 | ||
|
39b4df8367 | ||
|
fc9b2683c8 | ||
|
98311f11f8 | ||
|
2557972461 | ||
|
9510c56f7a | ||
|
9418b00b54 | ||
|
61f11ff836 | ||
|
2823696af9 | ||
|
45352296e9 | ||
|
4323b40362 | ||
|
57f1851cdd | ||
|
494ad1c4d9 | ||
|
2e79c9b195 | ||
|
7182baca47 | ||
|
71a0d82c66 | ||
|
4c44c576de | ||
|
ec73eb6225 | ||
|
295a46ec12 | ||
|
fa004b2db5 | ||
|
f6362cdbc2 | ||
|
cbf7fa4233 | ||
|
a7906e4fd6 | ||
|
f0a61fe55a | ||
|
7abfb021e3 | ||
|
903f230633 | ||
|
be729c8f53 | ||
|
233521b029 | ||
|
92ebc1c1ac | ||
|
1a20333b58 | ||
|
5f19bad688 | ||
|
fdd3110a98 | ||
|
c7a06e71a0 | ||
|
cd5bb5ff4e | ||
|
e91b8ab6c3 | ||
|
e3a01ff58b | ||
|
5de63b3495 | ||
|
1c8477ba4a | ||
|
cdf8d8400d | ||
|
aaa448bf57 | ||
|
e0f55ddc4f | ||
|
d4543a38c5 | ||
|
e471df8860 | ||
|
70dd38e970 | ||
|
7e5d631b9d | ||
|
3f57e17548 | ||
|
242a108703 | ||
|
07cce41329 | ||
|
53c47bfb0b | ||
|
7cafd314a4 | ||
|
d7e949239a | ||
|
d3ae37c3cf | ||
|
61d4b203ca | ||
|
54f5bd1922 | ||
|
c6199ca183 | ||
|
8a1d2b77dd | ||
|
2849ea9183 | ||
|
eb7793d7c5 | ||
|
de18e77e34 | ||
|
19d8ebee68 | ||
|
54d85a5cef | ||
|
8e2650acdb | ||
|
a6cde1fbe9 | ||
|
428a2cc4a3 | ||
|
5175626b23 | ||
|
8414d203e5 | ||
|
89d38c5371 | ||
|
2f32cb4351 | ||
|
f799af4c03 | ||
|
75d3b5078a | ||
|
18569ab556 | ||
|
129ad4dc35 | ||
|
909d7e66c4 | ||
|
3ceafb8e1f | ||
|
3a94498e45 | ||
|
c1b2351537 | ||
|
71f428d804 | ||
|
be35de2018 | ||
|
cca9e22edd | ||
|
cd0ed0f009 | ||
|
1b8e40c747 | ||
|
e0f9e95df3 | ||
|
7967fcf121 | ||
|
f7dc99be7d | ||
|
9ce8a209d8 | ||
|
6dcd63dde7 | ||
|
d91c69835c | ||
|
5325c809a4 | ||
|
76eca08c22 | ||
|
2c3e24333a | ||
|
f35283db51 | ||
|
d538551fad | ||
|
f3bf07b601 | ||
|
4a65b9f2ac | ||
|
80d17f8284 | ||
|
a7ef0d6635 | ||
|
0606311f51 | ||
|
fd59c3e910 | ||
|
5c084a8b8a | ||
|
2a3da2fec3 | ||
|
ef4304acf1 | ||
|
46f5296528 | ||
|
9f4a7a6304 | ||
|
2c0b6df863 | ||
|
b7373b6584 | ||
|
4b5cf06097 | ||
|
e68e176aa4 | ||
|
d23b9f94a2 | ||
|
30512c26e8 | ||
|
4405d7aa56 | ||
|
a984cf8673 | ||
|
7980d933c2 | ||
|
ef0beec8ed | ||
|
81ad82b765 | ||
|
973e7d2448 | ||
|
59ff119d20 | ||
|
6fd8ac0cd7 | ||
|
29e2041312 | ||
|
a3b9acba39 | ||
|
497d16f89d | ||
|
e752762d21 | ||
|
32ea6678f7 | ||
|
1153abd16e | ||
|
efa263b551 | ||
|
07b648dcc1 | ||
|
131f54a713 | ||
|
1cce15590c | ||
|
e0702c5dcf | ||
|
3b067e4664 | ||
|
26153ea60d | ||
|
5bafb0ba9d | ||
|
11a62c88d4 | ||
|
123523540e | ||
|
ab4d1fd2f4 | ||
|
9dc342e81e | ||
|
ae975506d7 | ||
|
3ba0219430 | ||
|
8bf1d29bf3 | ||
|
55ab65e22d | ||
|
b73feb6b71 | ||
|
662172c2e5 | ||
|
5d399dce4a | ||
|
766f728a8a | ||
|
f40d02ceb9 | ||
|
7ae98d63b0 | ||
|
0b8cd3b3b6 | ||
|
7f9e65bcbc | ||
|
4a376f8723 | ||
|
1024f626e8 | ||
|
828e6b4548 | ||
|
656a401684 | ||
|
394601d4a7 | ||
|
4da4ebc984 | ||
|
30cafb21e4 | ||
|
c379d730ca | ||
|
d9c0a21f0d | ||
|
816d2b28cb | ||
|
4ec36ca489 | ||
|
409592f952 | ||
|
02204c272f | ||
|
36e035c8c7 | ||
|
6c55185895 | ||
|
1c29af20fb | ||
|
4b9d1128c6 | ||
|
2c9939dea8 | ||
|
8150e05dd0 | ||
|
f85d455c8f | ||
|
cb2afcc3d5 | ||
|
838f1dac7e | ||
|
94002e4594 | ||
|
c5e787045a | ||
|
776f08be3f | ||
|
b01b280f79 | ||
|
a71af7ae0a | ||
|
6dafe72c3b | ||
|
848f857728 | ||
|
01d75fb284 | ||
|
cd4cadf23f | ||
|
0f28cf7aba | ||
|
cc93c7e5ed | ||
|
7040e3992b | ||
|
f3f2de3337 | ||
|
b866628b24 | ||
|
7e5c227867 | ||
|
1fe8255f52 | ||
|
3f5d5a2013 | ||
|
4f398d21c3 | ||
|
ee64cafe02 | ||
|
e63a34e2ba | ||
|
a018df9219 | ||
|
13b0ffe0b5 | ||
|
de1744b080 | ||
|
119c56878c | ||
|
d82c41c390 | ||
|
2826c27d63 | ||
|
51fedbd9b0 | ||
|
bf47a4c544 | ||
|
8e49df703f | ||
|
032e7a692e | ||
|
3b86822547 | ||
|
fd3de6e680 | ||
|
2d8f5d764e | ||
|
a5e716cb3e | ||
|
54288ebd44 | ||
|
9dfa0dae23 | ||
|
5090d45cfe | ||
|
7bf5106246 | ||
|
c11a76642c | ||
|
71ddd257a3 | ||
|
9b4e76e053 | ||
|
a98ef213ff | ||
|
c2b6766a40 | ||
|
8ed877c99c | ||
|
56f1b67d31 | ||
|
3648348184 | ||
|
9073ff8c2d | ||
|
48b7d80f7c | ||
|
c80d24ea46 | ||
|
b83b89ce38 | ||
|
3b28ea2b14 | ||
|
65194fbc80 |
14
.github/CHANGELOG-HEADER.md
vendored
Normal file
14
.github/CHANGELOG-HEADER.md
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
12
.github/ProjectRoot/vpm-manifest-2022.json
vendored
12
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.vrchat.avatars": {
|
"com.vrchat.avatars": {
|
||||||
"version": "3.7.0"
|
"version": "3.7.4"
|
||||||
},
|
},
|
||||||
"nadena.dev.ndmf": {
|
"nadena.dev.ndmf": {
|
||||||
"version": "1.4.0"
|
"version": "1.7.2-rc.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"com.vrchat.avatars": {
|
"com.vrchat.avatars": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.vrchat.base": "3.7.0"
|
"com.vrchat.base": "3.7.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"com.vrchat.base": {
|
"com.vrchat.base": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.4",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"nadena.dev.ndmf": {
|
"nadena.dev.ndmf": {
|
||||||
"version": "1.5.0-rc.6"
|
"version": "1.7.2-rc.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
71
.github/cut-changelog.pl
vendored
Executable file
71
.github/cut-changelog.pl
vendored
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
my ($changelog_file, $header_file, $version, $excerpt_file) = @ARGV;
|
||||||
|
|
||||||
|
open my $changelog, '<', $changelog_file or die "Can't open $changelog_file: $!";
|
||||||
|
open my $header, '<', $header_file or die "Can't open $header_file: $!";
|
||||||
|
open my $new_changelog, '>', "$changelog_file.new" or die "Can't open $changelog_file.new: $!";
|
||||||
|
|
||||||
|
if (!$excerpt_file) {
|
||||||
|
$excerpt_file = '/dev/null';
|
||||||
|
}
|
||||||
|
|
||||||
|
open my $excerpt, '>', $excerpt_file or die "Can't open $excerpt_file: $!";
|
||||||
|
|
||||||
|
# Copy all lines before the first "## "
|
||||||
|
while (my $line = <$changelog>) {
|
||||||
|
last if $line =~ /^## /;
|
||||||
|
print $new_changelog $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy header into the output changelog
|
||||||
|
while (my $line = <$header>) {
|
||||||
|
print $new_changelog $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate new header: ## [version] - [YYYY-mm-DD]
|
||||||
|
|
||||||
|
my $date = `date +%Y-%m-%d`;
|
||||||
|
chomp $date;
|
||||||
|
|
||||||
|
print $new_changelog "## [$version] - [$date]\n";
|
||||||
|
|
||||||
|
# Copy all lines until the next ## into both the new changelog and $excerpt.
|
||||||
|
# Prune any ###-sections that contain no content
|
||||||
|
|
||||||
|
my @buffered;
|
||||||
|
|
||||||
|
while (my $line = <$changelog>) {
|
||||||
|
if ($line =~ /^### /) {
|
||||||
|
@buffered = ($line);
|
||||||
|
} elsif ($line =~ /^\s*$/) {
|
||||||
|
if (@buffered) {
|
||||||
|
push @buffered, $line;
|
||||||
|
} else {
|
||||||
|
print $new_changelog $line;
|
||||||
|
print $excerpt $line;
|
||||||
|
}
|
||||||
|
} elsif ($line =~ /^## /) {
|
||||||
|
@buffered = ();
|
||||||
|
print $new_changelog $line;
|
||||||
|
last;
|
||||||
|
} else {
|
||||||
|
for my $buffered_line (@buffered){
|
||||||
|
print $new_changelog $buffered_line;
|
||||||
|
print $excerpt $buffered_line;
|
||||||
|
}
|
||||||
|
@buffered = ();
|
||||||
|
print $new_changelog $line;
|
||||||
|
print $excerpt $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy remainder of changelog into new changelog
|
||||||
|
while (my $line = <$changelog>) {
|
||||||
|
print $new_changelog $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
rename "$changelog_file.new", $changelog_file or die "Can't rename $changelog_file.new to $changelog_file: $!";
|
22
.github/gen-docs-changelog.pl
vendored
Executable file
22
.github/gen-docs-changelog.pl
vendored
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
# We want to skip two sections - the main header, then up to the first version header.
|
||||||
|
# In a prerelease, we only want to skip the first section (not including the unreleased header)
|
||||||
|
|
||||||
|
if ($ENV{PRERELEASE} eq 'false') {
|
||||||
|
while (<>) {
|
||||||
|
if (/^\## /) { last; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while (<>) {
|
||||||
|
if (/^## /) { print; last; }
|
||||||
|
}
|
||||||
|
|
||||||
|
while (<>) {
|
||||||
|
print;
|
||||||
|
}
|
91
.github/workflows/build-release.yml
vendored
91
.github/workflows/build-release.yml
vendored
@ -1,91 +0,0 @@
|
|||||||
name: Build Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- refactor-structure
|
|
||||||
tags:
|
|
||||||
- '**'
|
|
||||||
|
|
||||||
env:
|
|
||||||
packageName: "nadena.dev.modular-avatar"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Checkout logo assets
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
repository: bdunderscore/modular-avatar-images
|
|
||||||
path: .github/image-assets
|
|
||||||
- name: Inject logo assets
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
run: |
|
|
||||||
cp -f .github/image-assets/img/logo/ma_logo.png Editor/Images/logo.png
|
|
||||||
cp -f .github/image-assets/img/logo/ma_icon.png Runtime/Icons/Icon_MA_Script.png
|
|
||||||
|
|
||||||
- name: Check semver syntax
|
|
||||||
id: semver-check
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
env:
|
|
||||||
REF_NAME: ${{ github.ref }}
|
|
||||||
run: |
|
|
||||||
if echo $REF_NAME | grep '[a-z]-[0-9]' && ! echo $REF_NAME | grep '^refs/tags/1\.5\.0-'; then
|
|
||||||
echo "Tag name does not follow semver prerelease syntax: $REF_NAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: get version
|
|
||||||
id: version
|
|
||||||
uses: notiz-dev/github-action-json-property@a5a9c668b16513c737c3e1f8956772c99c73f6e8
|
|
||||||
with:
|
|
||||||
path: "package.json"
|
|
||||||
prop_path: "version"
|
|
||||||
|
|
||||||
- name: Check tag consistency
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.version.outputs.prop }}" != "${GITHUB_REF##*/}" ]; then
|
|
||||||
echo "Version in package.json does not match tag name: ${{ steps.version.outputs.prop }} != ${GITHUB_REF##*/}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- run: echo ${{steps.version.outputs.prop}}
|
|
||||||
|
|
||||||
- name: Set Environment Variables
|
|
||||||
run: |
|
|
||||||
echo "zipFile=${{ env.packageName }}-${{ steps.version.outputs.prop }}".zip >> $GITHUB_ENV
|
|
||||||
echo "zipFileSHA256=${{ env.packageName }}-${{ steps.version.outputs.prop }}".zip.sha256 >> $GITHUB_ENV
|
|
||||||
echo "unityPackage=${{ env.packageName }}-${{ steps.version.outputs.prop }}.unitypackage" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Zip
|
|
||||||
run: |
|
|
||||||
zip ".github/${{env.zipFile}}" ./* -r -x .github .git '.git/*' '*~/*' '*.ps1*'
|
|
||||||
mv ".github/${{env.zipFile}}" "${{env.zipFile}}"
|
|
||||||
sha256sum "${{env.zipFile}}" > "${{env.zipFileSHA256}}"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: package-zip
|
|
||||||
path: ${{ env.zipFile }}
|
|
||||||
|
|
||||||
- name: Make Release
|
|
||||||
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
draft: true
|
|
||||||
generate_release_notes: true
|
|
||||||
tag_name: ${{ steps.version.outputs.prop }}
|
|
||||||
files: |
|
|
||||||
${{ env.zipFile }}
|
|
||||||
${{ env.zipFileSHA256 }}
|
|
||||||
package.json
|
|
17
.github/workflows/build-test-docs.yml
vendored
17
.github/workflows/build-test-docs.yml
vendored
@ -23,6 +23,11 @@ on:
|
|||||||
description: 'build the latest release'
|
description: 'build the latest release'
|
||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
required: false
|
||||||
|
prerelease:
|
||||||
|
description: 'use prerelease changelog'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docs:
|
build-docs:
|
||||||
@ -67,6 +72,18 @@ jobs:
|
|||||||
BASEURL="/${{ inputs.path }}/" perl -i -p -e "s{baseUrl: '/'}{baseUrl: '\$ENV{BASEURL}'}" docs~/docusaurus.config.js
|
BASEURL="/${{ inputs.path }}/" perl -i -p -e "s{baseUrl: '/'}{baseUrl: '\$ENV{BASEURL}'}" docs~/docusaurus.config.js
|
||||||
cat 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
|
- name: Build docs
|
||||||
run: |
|
run: |
|
||||||
cd docs~
|
cd docs~
|
||||||
|
58
.github/workflows/changelog-check.yml
vendored
Normal file
58
.github/workflows/changelog-check.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# From https://github.com/anatawa12/AvatarOptimizer/blob/ccb863243433019f323c23a3a2e24b27e15b2f6c/.github/workflows/changelog-check.yml
|
||||||
|
# Copyright 2022 anatawa12
|
||||||
|
# MIT license.
|
||||||
|
|
||||||
|
# this workflow checks CHANGELOG.md & CHANGELOG-SNAPSHOTS.md is updated correctly
|
||||||
|
# to skip this check, include `NO-CHANGELOG` for CHANGELOG.md
|
||||||
|
# and `NO-CHANGELOG-PRERELEASE` for CHANGELOG-PRERELEASE.md in tags of PR.
|
||||||
|
# also, this action ignores `dependencies` pull requests (expected to be generated by dependabot)
|
||||||
|
|
||||||
|
name: CHANGELOG check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches: [ main, main-* ]
|
||||||
|
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
releasenote-check:
|
||||||
|
if: ${{ ! github.event.pull_request.draft }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
file: [CHANGELOG.md, CHANGELOG-jp.md, CHANGELOG-PRERELEASE.md, CHANGELOG-PRERELEASE-jp.md]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NO_CHANGELOG: ${{
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'NO-CHANGELOG')
|
||||||
|
|| contains(github.event.pull_request.labels.*.name, 'documentation')
|
||||||
|
|| contains(github.event.pull_request.labels.*.name, 'localization')
|
||||||
|
|| contains(github.event.pull_request.labels.*.name, 'ci')
|
||||||
|
|| contains(github.event.pull_request.labels.*.name, 'refactor')
|
||||||
|
|| startsWith(github.event.pull_request.head.label, 'bdunderscore:dependabot/')
|
||||||
|
|| '' }}
|
||||||
|
SNAPSHOT_ONLY: ${{ contains(github.event.pull_request.labels.*.name, 'PRERELEASE-ONLY') || '' }}
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
- name: Dump GitHub context
|
||||||
|
env:
|
||||||
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
|
run: echo "$GITHUB_CONTEXT"
|
||||||
|
- name: Fetch pull_request info
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repositoryUrl }}
|
||||||
|
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
PR_NUM: ${{ github.event.number }}
|
||||||
|
run: |
|
||||||
|
gh pr view $PR_NUM --json=files | jq --raw-output '.files[].path' > files.txt
|
||||||
|
- name: Changelog check for ${{ matrix.file }}
|
||||||
|
if: always() && !env.NO_CHANGELOG && (startsWith(matrix.file, 'CHANGELOG-PRERELEASE') || !env.SNAPSHOT_ONLY)
|
||||||
|
run: |
|
||||||
|
if ! grep -e '^${{ matrix.file }}$' < files.txt > /dev/null; then
|
||||||
|
echo "::error::An entry in ${{ matrix.file }} is required for this PR."
|
||||||
|
echo "If this change is only relevant between snapshot versions: Add the label 'PRERELEASE-ONLY' to this PR." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "If this change does not warrant any release notes: Add the label 'NO-CHANGELOG' to this PR." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
5
.github/workflows/deploy-pages.yml
vendored
5
.github/workflows/deploy-pages.yml
vendored
@ -40,11 +40,13 @@ jobs:
|
|||||||
|
|
||||||
build-docs:
|
build-docs:
|
||||||
name: Build documentation (latest release)
|
name: Build documentation (latest release)
|
||||||
|
# TODO - update to build-docs.yml
|
||||||
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
|
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
|
||||||
needs:
|
needs:
|
||||||
- snapshot-docs
|
- snapshot-docs
|
||||||
with:
|
with:
|
||||||
ref: docs-snapshot
|
ref: docs-snapshot
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
build-docs-dev:
|
build-docs-dev:
|
||||||
name: Build documentation (main branch)
|
name: Build documentation (main branch)
|
||||||
@ -53,6 +55,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
path: dev
|
path: dev
|
||||||
artifact: docs-dev
|
artifact: docs-dev
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
name: Deploy documentation
|
name: Deploy documentation
|
||||||
@ -122,7 +125,7 @@ jobs:
|
|||||||
workingDirectory: docs-site~
|
workingDirectory: docs-site~
|
||||||
|
|
||||||
- name: Purge cache
|
- name: Purge cache
|
||||||
uses: nathanvaughn/actions-cloudflare-purge@aa1121a867565ea71b60f445f441544df0c7b0b9
|
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
cf_zone: ${{ secrets.CF_ZONE_ID }}
|
cf_zone: ${{ secrets.CF_ZONE_ID }}
|
||||||
|
15
.github/workflows/gameci.yml
vendored
15
.github/workflows/gameci.yml
vendored
@ -110,12 +110,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- uses: anatawa12/sh-actions/resolve-vpm-packages@master
|
- uses: anatawa12/sh-actions/setup-vrc-get@master
|
||||||
name: Resolve VPM packages
|
|
||||||
if: ${{ matrix.sdk == 'vrcsdk' && steps.setup.outputs.should_test == 'true' }}
|
- name: Resolve packages
|
||||||
with:
|
if: ${{ steps.setup.outputs.should_test == 'true' }}
|
||||||
repos: |
|
run: |
|
||||||
https://vpm.nadena.dev/vpm-prerelease.json
|
vrc-get repo add -- "https://vpm.nadena.dev/vpm-prerelease.json" || true
|
||||||
|
vrc-get repo add -- "https://vrchat.github.io/packages/index.json?download" || true
|
||||||
|
vrc-get resolve --project .
|
||||||
|
vrc-get info project --project .
|
||||||
|
|
||||||
- if: ${{ steps.setup.outputs.should_test == 'true' }}
|
- if: ${{ steps.setup.outputs.should_test == 'true' }}
|
||||||
name: "Debug: List project contents"
|
name: "Debug: List project contents"
|
||||||
|
231
.github/workflows/perform-release.yml
vendored
Normal file
231
.github/workflows/perform-release.yml
vendored
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
name: Perform Release
|
||||||
|
|
||||||
|
# Portions of this workflow are based on https://github.com/anatawa12/AvatarOptimizer/blob/master/.github/workflows/release.yml
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_kind:
|
||||||
|
type: choice
|
||||||
|
description: The type of release.
|
||||||
|
default: prerelease
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
- prerelease
|
||||||
|
- stable
|
||||||
|
- adhoc
|
||||||
|
publish:
|
||||||
|
description: "True to publish release to git, vpm. if false, this creates release asset only"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
version:
|
||||||
|
description: "Version to release"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
PKG_NAME: nadena.dev.modular-avatar
|
||||||
|
RELEASE_TYPE: ${{ github.event.inputs.release_kind }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: publish
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-gameci:
|
||||||
|
uses: bdunderscore/modular-avatar/.github/workflows/gameci.yml@main
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
contents: read
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
check-docs:
|
||||||
|
name: Build documentation (latest release)
|
||||||
|
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
|
||||||
|
|
||||||
|
create-release:
|
||||||
|
needs: [ check-gameci, check-docs ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Dump GitHub context
|
||||||
|
env:
|
||||||
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
|
run: echo "$GITHUB_CONTEXT"
|
||||||
|
|
||||||
|
# https://github.com/orgs/community/discussions/13836#discussioncomment-8535364
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.RELEASER_APP_ID }}
|
||||||
|
private-key: ${{ secrets.RELEASER_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Validate prerelease version
|
||||||
|
id: check-version
|
||||||
|
if: ${{ github.event.inputs.release_kind == 'prerelease' && !contains(github.event.inputs.version, '-') }}
|
||||||
|
run:
|
||||||
|
echo "Prerelease version must contain a hyphen"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Validate stable version
|
||||||
|
id: check-version-stable
|
||||||
|
if: ${{ github.event.inputs.release_kind == 'stable' && contains(github.event.inputs.version, '-') }}
|
||||||
|
run:
|
||||||
|
echo "Stable version must not contain a hyphen"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Validate adhoc
|
||||||
|
id: validate-adhoc
|
||||||
|
if: ${{ github.event.inputs.release_kind == 'adhoc' && github.event.inputs.publish == 'true' }}
|
||||||
|
run:
|
||||||
|
echo "Adhoc release cannot be published"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Set Environment Variables
|
||||||
|
run: |
|
||||||
|
echo "zipFile=${{ env.PKG_NAME }}-${{ github.event.inputs.version }}".zip >> $GITHUB_ENV
|
||||||
|
echo "unityPackage=${{ env.PKG_NAME }}-${{ github.event.inputs.version }}.unitypackage" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
case "$RELEASE_TYPE" in
|
||||||
|
prerelease)
|
||||||
|
echo "PRERELEASE=true" >> $GITHUB_ENV
|
||||||
|
;;
|
||||||
|
stable)
|
||||||
|
echo "PRERELEASE=false" >> $GITHUB_ENV
|
||||||
|
;;
|
||||||
|
adhoc)
|
||||||
|
echo "PRERELEASE=true" >> $GITHUB_ENV
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Checkout logo assets
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
if: ${{ github.event.inputs.release_kind != 'adhoc' }}
|
||||||
|
with:
|
||||||
|
repository: bdunderscore/modular-avatar-images
|
||||||
|
path: .github/image-assets
|
||||||
|
- name: Inject logo assets
|
||||||
|
if: ${{ github.event.inputs.release_kind != 'adhoc' }}
|
||||||
|
run: |
|
||||||
|
cp -f .github/image-assets/img/logo/ma_logo.png Editor/Images/logo.png
|
||||||
|
cp -f .github/image-assets/img/logo/ma_icon.png Runtime/Icons/Icon_MA_Script.png
|
||||||
|
|
||||||
|
- name: Check semver syntax
|
||||||
|
if: steps.check-tag.outputs.need-new-tag == 'true'
|
||||||
|
id: semver-check
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/*.sh
|
||||||
|
.github/workflows/check-semver-syntax.sh ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
|
- name: Set git user and email
|
||||||
|
id: git-config
|
||||||
|
run: |
|
||||||
|
git config --global user.name "nadena.dev release bot"
|
||||||
|
git config --global user.email "ci@nadena.dev"
|
||||||
|
- name: Update version
|
||||||
|
id: update-version
|
||||||
|
run: |
|
||||||
|
jq '.version = env.VERSION' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.inputs.version }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Update changelog
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
chmod +x .github/*.pl
|
||||||
|
|
||||||
|
if [ "${{ env.PRERELEASE }}" == "true" ]; then
|
||||||
|
./.github/cut-changelog.pl CHANGELOG-PRERELEASE.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-en.md
|
||||||
|
./.github/cut-changelog.pl CHANGELOG-PRERELEASE-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-jp.md
|
||||||
|
else
|
||||||
|
./.github/cut-changelog.pl CHANGELOG-PRERELEASE.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }}
|
||||||
|
./.github/cut-changelog.pl CHANGELOG-PRERELEASE-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }}
|
||||||
|
./.github/cut-changelog.pl CHANGELOG.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-en.md
|
||||||
|
./.github/cut-changelog.pl CHANGELOG-jp.md .github/CHANGELOG-HEADER.md ${{ env.VERSION }} .github/relnote-jp.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo Version ${{ env.VERSION }} > release-note.md
|
||||||
|
echo >> release-note.md
|
||||||
|
if [ "${{ env.PRERELEASE }}" == "true" ]; then
|
||||||
|
echo '**This is a prerelease version.** There may be bugs, and API compatibility is not yet guaranteed.' >> release-note.md
|
||||||
|
echo 'Please: **BACK UP YOUR PROJECTS**' >> release-note.md
|
||||||
|
echo >> release-note.md
|
||||||
|
fi
|
||||||
|
echo '## Notable changes' >> release-note.md
|
||||||
|
cat .github/relnote-en.md >> release-note.md
|
||||||
|
echo >> release-note.md
|
||||||
|
echo '## 主な変更点' >> release-note.md
|
||||||
|
cat .github/relnote-jp.md >> release-note.md
|
||||||
|
|
||||||
|
- name: Upload CHANGELOG.md
|
||||||
|
if: ${{ github.event.inputs.release_kind == 'stable' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: CHANGELOG
|
||||||
|
path: CHANGELOG.md
|
||||||
|
- name: Upload CHANGELOG-PRERELEASE.md
|
||||||
|
if: ${{ github.event.inputs.release_kind == 'prerelease' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: CHANGELOG-PRERELEASE
|
||||||
|
path: CHANGELOG-PRERELEASE.md
|
||||||
|
- name: Upload release note
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: changelog
|
||||||
|
path: release-note.md
|
||||||
|
- run: mv release-note.md .github
|
||||||
|
|
||||||
|
- name: Commit and tag version update
|
||||||
|
run: |
|
||||||
|
git commit -am "Release ${{ github.event.inputs.version }}"
|
||||||
|
git tag -a ${{ github.event.inputs.version }} -m "Release ${{ github.event.inputs.version }}"
|
||||||
|
- name: Publish tag
|
||||||
|
if: ${{ github.event.inputs.publish == 'true' }}
|
||||||
|
run: |
|
||||||
|
BRANCH_NAME=$(git branch --show-current)
|
||||||
|
git push origin $BRANCH_NAME && git push origin ${{ github.event.inputs.version }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Create Zip
|
||||||
|
run: |
|
||||||
|
zip ".github/${{env.zipFile}}" ./* -r -x .github .git '.git/*' '*~/*' '*.ps1*'
|
||||||
|
|
||||||
|
- name: Move zipfile
|
||||||
|
run: |
|
||||||
|
mv .github/${{env.zipFile}} ${{env.zipFile}}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: package-zip
|
||||||
|
path: ${{ env.zipFile }}
|
||||||
|
|
||||||
|
- name: Dump release notes
|
||||||
|
run: |
|
||||||
|
cat .github/release-note.md
|
||||||
|
|
||||||
|
- name: Make Release
|
||||||
|
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda
|
||||||
|
if: ${{ github.event.inputs.publish == 'true' }}
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
body_path: .github/release-note.md
|
||||||
|
tag_name: ${{ github.event.inputs.version }}
|
||||||
|
name: ${{ github.event.inputs.version }}
|
||||||
|
make_latest: ${{ github.event.inputs.release_kind == 'stable' }}
|
||||||
|
files: |
|
||||||
|
${{ env.zipFile }}
|
||||||
|
package.json
|
102
CHANGELOG-PRERELEASE-jp.md
Normal file
102
CHANGELOG-PRERELEASE-jp.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
## [1.12.2] - [2025-04-03]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1537] アニメーターパラメーターをアニメーションさせるカーブが、`Merge Motion` コンポーネントを使用して追加された場合、
|
||||||
|
`Rename Parameters` によって更新されない問題を修正``
|
||||||
|
|
||||||
|
## [1.12.1] - [2025-04-02]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1532] Modular Avatarが新しく作成したプロジェクトでコンパイラエラーを出す問題を修正
|
||||||
|
|
||||||
|
## [1.12.0] - [2025-04-01]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1531] lylicalInventoryとの互換性問題を修正
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1530] `MA Menu Item`の自動パラメーター機能も、オブジェクトのパスに基づいて名前を割り当てるようになりました。
|
||||||
|
|
||||||
|
## [1.12.0-rc.1] - [2025-03-28]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- [#1524] MMDワールド対応をアバター全体で無効にする機能を追加
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1522] `Convert Constraints` がアニメーション参照を変換できない問題を修正
|
||||||
|
- [#1528] `Merge Animator` が `アバターのWrite Defaults設定に合わせる` 設定を無視し、常に合わせてしまう問題を修正
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1529] `MA Parameters` の自動リネームは、オブジェクトのパスに基づいて新しい名前を割り当てるように変更されました。これにより、
|
||||||
|
`MA Sync Parameter Sequence` との互換性が向上します。
|
||||||
|
- `MA Sync Parameter Sequence` を使用している場合は、このバージョンに更新した後、SyncedParamsアセットを空にして、
|
||||||
|
すべてのプラットフォームを再アップロードすることをお勧めします。
|
||||||
|
|
||||||
|
## [1.12.0-rc.0] - [2025-03-22]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1508] テクスチャのサイズが4の倍数でない場合に、エクスプレッションメニューアイコンの自動圧縮が失敗する問題を修正
|
||||||
|
- [#1513] iOSビルドでエクスプレッションメニューアイコンの圧縮が壊れる問題を修正
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1514] `Merge Blend Tree` は `Merge Motion (Blend Tree)` に改名され、アニメーションクリップにも対応するようになりました
|
||||||
|
|
||||||
|
## [1.12.0-beta.0] - [2025-03-17]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- [#1497] CHANGELOGをドキュメンテーションサイトに追加
|
||||||
|
- [#1482] `Merge Animator` に既存のアニメーターコントローラーを置き換える機能を追加
|
||||||
|
- [#1481] [World Scale Object](https://m-a.nadena.dev/dev/ja/docs/reference/world-scale-object)を追加
|
||||||
|
- [#1489] [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を追加
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1492] 前回のプレリリースでアイコンとロゴアセットが間違っていた問題を修正
|
||||||
|
- [#1501] MA Parametersコンポーネントのテキスト入力欄を編集する際にUnityのキーボードショートカットが機能しない問題を修正
|
||||||
|
- [#1410] 同期レイヤー内のモーションオーバーライドがBone Proxy/Merge Armatureオブジェクトの移動に対して更新されない問題を修正
|
||||||
|
- [#1504] 一部の状況で内部の`DelayDisable`レイヤーが不要なオブジェクトを参照しないように変更
|
||||||
|
- これにより、オブジェクトがアニメーションされているかどうかを追跡するAAOなどのツールとの互換性が向上します
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1483] Merge Animator の 「アバターの Write Defaults 設定に合わせる」設定では、Additiveなレイヤー、および単一Stateかつ遷移のないレイヤー
|
||||||
|
に対してはWrite Defaultsを調整しないように変更。
|
||||||
|
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマノイドボーンをマージできるようになりました。
|
||||||
|
- 具体的には、子ヒューマノイドボーンがある場合はPhysBoneから除外される必要があります。
|
||||||
|
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
|
||||||
|
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
|
||||||
|
アニメーションがブロックされたときにオーディオソースを無効にするように変更。
|
||||||
|
- [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。
|
||||||
|
詳細は[ドキュメント](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を参照してください。
|
||||||
|
- [#1502] `World Fixed Object` は `VRCParentConstraint` を使用するようになり、Androidビルドで使用可能になりました。
|
||||||
|
|
||||||
|
## [1.12.0-alpha.2] - [2025-03-10]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added CHANGELOG files
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1476] ModularAvatarMergeAnimator と ModularAvatarMergeParameter を新しい NDMF API (`IVirtualizeMotion` と `IVirtualizeAnimatorController`) を使用するように変更
|
||||||
|
|
||||||
|
## Older versions
|
||||||
|
|
||||||
|
Please see CHANGELOG.md
|
3
CHANGELOG-PRERELEASE-jp.md.meta
Normal file
3
CHANGELOG-PRERELEASE-jp.md.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2f89fef1421c4126b6086156ff536d8f
|
||||||
|
timeCreated: 1741573199
|
103
CHANGELOG-PRERELEASE.md
Normal file
103
CHANGELOG-PRERELEASE.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
## [1.12.2] - [2025-04-03]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1537] Curves which animated animator parameters, when added using a `Merge Motion` component, would not be updated by
|
||||||
|
`Rename Parameters`
|
||||||
|
|
||||||
|
## [1.12.1] - [2025-04-02]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1532] Modular Avatar has compiler errors in a newly created project
|
||||||
|
|
||||||
|
## [1.12.0] - [2025-04-01]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1531] Fix compatibility issue with lylicalInventory
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1530] `MA Menu Item` auto parameters now also assign names based on object paths
|
||||||
|
|
||||||
|
## [1.12.0-rc.1] - [2025-03-28]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- [#1524] Added support for disabling MMD world handling at an avatar level
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1522] `Convert Constraints` failed to convert animation references
|
||||||
|
- [#1528] `Merge Animator` ignored the `Match Avatar Write Defaults` setting and always matched
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1529] `MA Parameters` auto-rename now assigns new names based on the path of the object. This should improve
|
||||||
|
compatibility with `MA Sync Parameter Sequence`
|
||||||
|
- If you are using `MA Sync Parameter Sequence`, it's a good idea to empty your SyncedParams asset and reupload all
|
||||||
|
platforms after updating to this version.
|
||||||
|
|
||||||
|
## [1.12.0-rc.0] - [2025-03-22]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1508] Fix an issue where automatic compression of expressions menu icons would fail when the texture dimensions were
|
||||||
|
not divisible by four.
|
||||||
|
- [#1513] Expression menu icon compression broke on iOS builds
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1514] `Merge Blend Tree` is now `Merge Motion (Blend Tree)` and supports merging animation clips as well as blend trees
|
||||||
|
|
||||||
|
## [1.12.0-beta.0] - [2025-03-17]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- [#1497] Added changelog to docs site
|
||||||
|
- [#1482] Added support for replacing pre-existing animator controllers to `Merge Animator`
|
||||||
|
- [#1481] Added [World Scale Object](https://m-a.nadena.dev/dev/docs/reference/world-scale-object)
|
||||||
|
- [#1489] Added [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1492] Fixed incorrect icon and logo assets in prior prerelease
|
||||||
|
- [#1489] Fixed compatibility issues between `Merge Blend Tree` or reactive components and MMD worlds.
|
||||||
|
See [documentation](https://modular-avatar.nadena.dev/docs/general-behavior/mmd) for details on the new handling.
|
||||||
|
- [#1501] Unity keyboard shortcuts don't work when editing text fields on the MA Parameters component
|
||||||
|
- [#1410] Motion overrides on synced layers are not updated for Bone Proxy/Merge Armature object movement
|
||||||
|
- [#1504] The internal `DelayDisable` layer no longer references unnecessary objects in some situations
|
||||||
|
- This helps improve compatibility with AAO and other tools that track whether objects are animated
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1483] The Merge Animator "Match Avatar Write Defaults" option will no longer adjust write defaults on states in
|
||||||
|
additive layers, or layers with only one state and no transitions.
|
||||||
|
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
|
||||||
|
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
|
||||||
|
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
|
||||||
|
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
|
||||||
|
to avoid it unintentionally being constantly active.
|
||||||
|
- [#1502] `World Fixed Object` now uses `VRCParentConstraint` and is therefore compatible with Android builds
|
||||||
|
|
||||||
|
## [1.12.0-alpha.2] - [2025-03-10]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added CHANGELOG files
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1476] Switch ModularAvatarMergeAnimator and ModularAvatarMergeParameter to use new NDMF APIs (`IVirtualizeMotion` and `IVirtualizeAnimatorController`)
|
||||||
|
|
||||||
|
## Older versions
|
||||||
|
|
||||||
|
Please see CHANGELOG.md
|
3
CHANGELOG-PRERELEASE.md.meta
Normal file
3
CHANGELOG-PRERELEASE.md.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cb586a9c85634b8b81015d16899d797b
|
||||||
|
timeCreated: 1741571222
|
72
CHANGELOG-jp.md
Normal file
72
CHANGELOG-jp.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Modular Avatarの主な変更点をこのファイルで記録しています。
|
||||||
|
なお、プレリリース版の変更点は `CHANGELOG-PRERELEASE.md` に記録されます。
|
||||||
|
|
||||||
|
この形式は [Keep a Changelog](https://keepachangelog.com/ja/1.0.0/) に基づいており、
|
||||||
|
このプロジェクトは [Semantic Versioning](https://semver.org/lang/ja/) に従っています。
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
## [1.12.2] - [2025-04-03]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1537] アニメーターパラメーターをアニメーションさせるカーブが、`Merge Motion` コンポーネントを使用して追加された場合、
|
||||||
|
`Rename Parameters` によって更新されない問題を修正``
|
||||||
|
|
||||||
|
## [1.12.1] - [2025-04-02]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1532] Modular Avatarが新しく作成したプロジェクトでコンパイラエラーを出す問題を修正
|
||||||
|
|
||||||
|
## [1.12.0] - [2025-04-01]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- CHANGELOGファイルを追加
|
||||||
|
- [#1482] `Merge Animator` に既存のアニメーターコントローラーを置き換える機能を追加
|
||||||
|
- [#1481] [World Scale Object](https://m-a.nadena.dev/ja/docs/reference/world-scale-object)を追加
|
||||||
|
- [#1489] [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を追加
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1460] パラメーターアセットをMA Parametersにインポートするとき、ローカルのみのパラメーターが間違ってアニメーターのみ扱いになる問題を修正
|
||||||
|
- [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。
|
||||||
|
- 詳細は[ドキュメント](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)を参照してください。
|
||||||
|
- この動作を無効にするには、新しい `MA VRChat Settings` コンポーネントをアバターの適当なところに追加して、適切な設定を無効にしてください。
|
||||||
|
- [#1501] MA Parametersコンポーネントのテキスト入力欄を編集する際にUnityのキーボードショートカットが機能しない問題を修正
|
||||||
|
- [#1410] 同期レイヤー内のモーションオーバーライドがBone Proxy/Merge Armatureオブジェクトの移動に対して更新されない問題を修正
|
||||||
|
- [#1504] 一部の状況で内部の`DelayDisable`レイヤーが不要なオブジェクトを参照しないように変更
|
||||||
|
- これにより、オブジェクトがアニメーションされているかどうかを追跡するAAOなどのツールとの互換性が向上します
|
||||||
|
- [#1508] テクスチャのサイズが4の倍数でない場合に、エクスプレッションメニューアイコンの自動圧縮が失敗する問題を修正
|
||||||
|
- [#1513] iOSビルドでエクスプレッションメニューアイコンの圧縮処理が壊れる問題を修正
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1529] `MA Parameters` の自動リネームと `MA Menu Item` の自動パラメーター機能は、オブジェクトのパスに基づいて名前
|
||||||
|
を割り当てるように変更されました。
|
||||||
|
- `MA Sync Parameter Sequence` を使用している場合は、このバージョンに更新した後、SyncedParamsアセットを空にして、
|
||||||
|
すべてのプラットフォームを再アップロードすることをお勧めします。
|
||||||
|
- [#1514] `Merge Blend Tree` は `Merge Motion (Blend Tree)` に改名され、アニメーションクリップにも対応するようになりました
|
||||||
|
- [#1476] ModularAvatarMergeAnimator と ModularAvatarMergeParameter を新しい NDMF API (`IVirtualizeMotion` と `IVirtualizeAnimatorController`) を使用するように変更
|
||||||
|
- [#1483] Merge Animator の 「アバターの Write Defaults 設定に合わせる」設定では、Additiveなレイヤー、および単一Stateかつ遷移のないレイヤー
|
||||||
|
に対してはWrite Defaultsを調整しないように変更。
|
||||||
|
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマノイドボーンをマージできるようになりました。
|
||||||
|
- 具体的には、子ヒューマノイドボーンがある場合はPhysBoneから除外される必要があります。
|
||||||
|
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
|
||||||
|
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
|
||||||
|
アニメーションがブロックされたときにオーディオソースを無効にするように変更。
|
||||||
|
- [#1502] `World Fixed Object` は `VRCParentConstraint` を使用するようになり、Androidビルドで使用可能になりました。
|
||||||
|
|
||||||
|
## それより前
|
||||||
|
|
||||||
|
GitHubのリリースページをご確認ください: https://github.com/bdunderscore/modular-avatar/releases
|
7
CHANGELOG-jp.md.meta
Normal file
7
CHANGELOG-jp.md.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b27815ff13397374abcf9547a36bfaf4
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
77
CHANGELOG.md
77
CHANGELOG.md
@ -1 +1,76 @@
|
|||||||
Temporary test release
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
Changes between prerelease versions will be documented in `CHANGELOG-PRERELEASE.md` instead.
|
||||||
|
|
||||||
|
[日本語版はこちらです。](CHANGELOG-jp.md)
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
## [1.12.2] - [2025-04-03]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1537] Curves which animated animator parameters, when added using a `Merge Motion` component, would not be updated by
|
||||||
|
`Rename Parameters`
|
||||||
|
|
||||||
|
## [1.12.1] - [2025-04-02]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1532] Modular Avatar has compiler errors in a newly created project
|
||||||
|
|
||||||
|
## [1.12.0] - [2025-04-01]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added CHANGELOG files
|
||||||
|
- [#1482] Added support for replacing pre-existing animator controllers to `Merge Animator`
|
||||||
|
- [#1481] Added [World Scale Object](https://m-a.nadena.dev/docs/reference/world-scale-object)
|
||||||
|
- [#1489] Added [`MA MMD Layer Control`](https://modular-avatar.nadena.dev/docs/general-behavior/mmd)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [#1460] When importing parameter assets in MA Parameters, "local only" parameters were incorrectly treated as
|
||||||
|
"animator only"
|
||||||
|
- [#1489] Fixed compatibility issues between `Merge Blend Tree` or reactive components and MMD worlds.
|
||||||
|
- See [documentation](https://modular-avatar.nadena.dev/docs/general-behavior/mmd) for details on the new handling.
|
||||||
|
- To disable this behavior, attach the new `MA VRChat Settings` component to any object on your avatar and disable the appropriate setting.
|
||||||
|
- [#1501] Unity keyboard shortcuts don't work when editing text fields on the MA Parameters component
|
||||||
|
- [#1410] Motion overrides on synced layers are not updated for Bone Proxy/Merge Armature object movement
|
||||||
|
- [#1504] The internal `DelayDisable` layer no longer references unnecessary objects in some situations
|
||||||
|
- This helps improve compatibility with AAO and other tools that track whether objects are animated
|
||||||
|
- [#1508] Fix an issue where automatic compression of expressions menu icons would fail when the texture dimensions were
|
||||||
|
not divisible by four.
|
||||||
|
- [#1513] Expression menu icon compression broke on iOS builds
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [#1529] `MA Parameters` auto-rename and `MA Menu Item`'s automatic parameter feature now assign names based on the
|
||||||
|
path of the object. This should improve compatibility with `MA Sync Parameter Sequence`
|
||||||
|
- If you are using `MA Sync Parameter Sequence`, it's a good idea to empty your SyncedParams asset and reupload all
|
||||||
|
platforms after updating to this version.
|
||||||
|
- [#1514] `Merge Blend Tree` is now `Merge Motion (Blend Tree)` and supports merging animation clips as well as blend trees
|
||||||
|
- [#1476] Switch ModularAvatarMergeAnimator and ModularAvatarMergeParameter to use new NDMF APIs (`IVirtualizeMotion` and `IVirtualizeAnimatorController`)
|
||||||
|
- [#1483] The Merge Animator "Match Avatar Write Defaults" option will no longer adjust write defaults on states in
|
||||||
|
additive layers, or layers with only one state and no transitions.
|
||||||
|
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
|
||||||
|
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
|
||||||
|
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
|
||||||
|
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
|
||||||
|
to avoid it unintentionally being constantly active.
|
||||||
|
- [#1502] `World Fixed Object` now uses `VRCParentConstraint` and is therefore compatible with Android builds
|
||||||
|
|
||||||
|
## Older versions
|
||||||
|
|
||||||
|
Please see the github releases page at https://github.com/bdunderscore/modular-avatar/releases
|
||||||
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.animation;
|
using nadena.dev.modular_avatar.animation;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||||
@ -16,7 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
private readonly BuildContext _context;
|
private readonly BuildContext _context;
|
||||||
private readonly BoneDatabase _boneDatabase;
|
private readonly BoneDatabase _boneDatabase;
|
||||||
private readonly PathMappings _pathMappings;
|
private readonly AnimatorServicesContext _asc;
|
||||||
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
|
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -55,15 +56,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_boneDatabase = boneDatabase;
|
_boneDatabase = boneDatabase;
|
||||||
_pathMappings = context.PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
|
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
|
||||||
|
|
||||||
while (root != null && !RuntimeUtil.IsAvatarRoot(root))
|
while (root != null && !RuntimeUtil.IsAvatarRoot(root))
|
||||||
{
|
{
|
||||||
var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject);
|
var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject);
|
||||||
System.Diagnostics.Debug.Assert(originalPath != null);
|
System.Diagnostics.Debug.Assert(originalPath != null);
|
||||||
|
|
||||||
if (context.AnimationDatabase.ClipsForPath(originalPath).Any(clip =>
|
if (_asc.AnimationIndex.GetClipsForObjectPath(originalPath).Any(clip =>
|
||||||
GetActiveBinding(clip.CurrentClip as AnimationClip, originalPath) != null
|
GetActiveBinding(clip, originalPath) != null
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
_intermediateObjs.Add(new IntermediateObj
|
_intermediateObjs.Add(new IntermediateObj
|
||||||
@ -118,7 +119,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
// Ensure mesh retargeting looks through this
|
// Ensure mesh retargeting looks through this
|
||||||
_boneDatabase.AddMergedBone(sourceBone.transform);
|
_boneDatabase.AddMergedBone(sourceBone.transform);
|
||||||
_boneDatabase.RetainMergedBone(sourceBone.transform);
|
_boneDatabase.RetainMergedBone(sourceBone.transform);
|
||||||
_pathMappings.MarkTransformLookthrough(sourceBone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceBone;
|
return sourceBone;
|
||||||
@ -130,22 +130,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
var path = intermediate.OriginalPath;
|
var path = intermediate.OriginalPath;
|
||||||
|
|
||||||
foreach (var holder in _context.AnimationDatabase.ClipsForPath(path))
|
foreach (var clip in _asc.AnimationIndex.GetClipsForObjectPath(path))
|
||||||
{
|
{
|
||||||
if (!_context.PluginBuildContext.IsTemporaryAsset(holder.CurrentClip))
|
|
||||||
{
|
|
||||||
holder.CurrentClip = Object.Instantiate(holder.CurrentClip);
|
|
||||||
}
|
|
||||||
|
|
||||||
var clip = holder.CurrentClip as AnimationClip;
|
|
||||||
if (clip == null) continue;
|
|
||||||
|
|
||||||
var curve = GetActiveBinding(clip, path);
|
var curve = GetActiveBinding(clip, path);
|
||||||
if (curve != null)
|
if (curve != null)
|
||||||
{
|
{
|
||||||
foreach (var mapping in intermediate.Created)
|
foreach (var mapping in intermediate.Created)
|
||||||
{
|
{
|
||||||
clip.SetCurve(_pathMappings.GetObjectIdentifier(mapping), typeof(GameObject), "m_IsActive",
|
clip.SetFloatCurve(_asc.ObjectPathRemapper.GetVirtualPathForObject(mapping), typeof(GameObject), "m_IsActive",
|
||||||
curve);
|
curve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,10 +145,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AnimationCurve GetActiveBinding(AnimationClip clip, string path)
|
private AnimationCurve GetActiveBinding(VirtualClip clip, string path)
|
||||||
{
|
{
|
||||||
return AnimationUtility.GetEditorCurve(clip,
|
return clip.GetFloatCurve(EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
|
||||||
EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,383 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using nadena.dev.modular_avatar.core.editor;
|
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 11130986120e452b8dc8db0d19aa71fc
|
|
||||||
timeCreated: 1671624207
|
|
@ -1,119 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using VRC.SDK3.Avatars.Components;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This extension context amortizes a number of animation-related processing steps - notably,
|
|
||||||
/// collecting the set of all animation clips from the animators, and committing changes to them
|
|
||||||
/// in a deferred manner.
|
|
||||||
///
|
|
||||||
/// Restrictions: While this context is active, any changes to clips must be done by editing them via
|
|
||||||
/// ClipHolders in the AnimationDatabase. Any newly added clips must be registered in the AnimationDatabase,
|
|
||||||
/// and any new references to clips require setting appropriate ClipCommitActions.
|
|
||||||
///
|
|
||||||
/// New references to objects created in clips must use paths obtained from the
|
|
||||||
/// ObjectRenameTracker.GetObjectIdentifier method.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class AnimationServicesContext : IExtensionContext
|
|
||||||
{
|
|
||||||
private BuildContext _context;
|
|
||||||
private AnimationDatabase _animationDatabase;
|
|
||||||
private PathMappings _pathMappings;
|
|
||||||
private ReadableProperty _readableProperty;
|
|
||||||
|
|
||||||
private Dictionary<GameObject, string> _selfProxies = new();
|
|
||||||
|
|
||||||
public void OnActivate(BuildContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
|
|
||||||
_animationDatabase = new AnimationDatabase();
|
|
||||||
_animationDatabase.OnActivate(context);
|
|
||||||
|
|
||||||
_pathMappings = new PathMappings();
|
|
||||||
_pathMappings.OnActivate(context, _animationDatabase);
|
|
||||||
|
|
||||||
_readableProperty = new ReadableProperty(_context, _animationDatabase, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnDeactivate(BuildContext context)
|
|
||||||
{
|
|
||||||
_pathMappings.OnDeactivate(context);
|
|
||||||
_animationDatabase.Commit();
|
|
||||||
|
|
||||||
_pathMappings = null;
|
|
||||||
_animationDatabase = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnimationDatabase AnimationDatabase
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_animationDatabase == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"AnimationDatabase is not available outside of the AnimationServicesContext");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _animationDatabase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PathMappings PathMappings
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_pathMappings == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"ObjectRenameTracker is not available outside of the AnimationServicesContext");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _pathMappings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<(EditorCurveBinding, string)> BoundReadableProperties => _readableProperty.BoundProperties;
|
|
||||||
|
|
||||||
// HACK: This is a temporary crutch until we rework the entire animator services system
|
|
||||||
public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
|
|
||||||
{
|
|
||||||
var fx = (AnimatorController)
|
|
||||||
_context.AvatarDescriptor.baseAnimationLayers
|
|
||||||
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
|
|
||||||
.animatorController;
|
|
||||||
|
|
||||||
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetActiveSelfProxy(GameObject obj)
|
|
||||||
{
|
|
||||||
if (_selfProxies.TryGetValue(obj, out var paramName) && !string.IsNullOrEmpty(paramName)) return paramName;
|
|
||||||
|
|
||||||
var path = PathMappings.GetObjectIdentifier(obj);
|
|
||||||
|
|
||||||
paramName = _readableProperty.ForActiveSelf(path);
|
|
||||||
_selfProxies[obj] = paramName;
|
|
||||||
|
|
||||||
return paramName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ObjectHasAnimations(GameObject obj)
|
|
||||||
{
|
|
||||||
var path = PathMappings.GetObjectIdentifier(obj);
|
|
||||||
var clips = AnimationDatabase.ClipsForPath(path);
|
|
||||||
return clips != null && !clips.IsEmpty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c2c26040d44d4dacb838aceced3b3e52
|
|
||||||
timeCreated: 1696063949
|
|
@ -1,220 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
using VRC.SDK3.Avatars.Components;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
internal static class AnimationUtil
|
|
||||||
{
|
|
||||||
private const string SAMPLE_PATH_PACKAGE =
|
|
||||||
"Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers";
|
|
||||||
|
|
||||||
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
|
|
||||||
|
|
||||||
private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92";
|
|
||||||
|
|
||||||
|
|
||||||
public static AnimatorController DeepCloneAnimator(BuildContext context, RuntimeAnimatorController controller)
|
|
||||||
{
|
|
||||||
if (controller == null) return null;
|
|
||||||
|
|
||||||
var merger = new AnimatorCombiner(context, controller.name + " (cloned)");
|
|
||||||
switch (controller)
|
|
||||||
{
|
|
||||||
case AnimatorController ac:
|
|
||||||
merger.AddController("", ac, null);
|
|
||||||
break;
|
|
||||||
case AnimatorOverrideController oac:
|
|
||||||
merger.AddOverrideController("", oac, null);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: af583e8ac3104fa4f8466741614219a0
|
|
||||||
timeCreated: 1691238553
|
|
@ -1,623 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2022 bd_
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#region
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using nadena.dev.ndmf.util;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using Object = UnityEngine.Object;
|
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
using VRC.SDK3.Avatars.Components;
|
|
||||||
using VRC.SDKBase;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
internal class AnimatorCombiner
|
|
||||||
{
|
|
||||||
private readonly BuildContext _context;
|
|
||||||
private readonly AnimatorController _combined;
|
|
||||||
|
|
||||||
private readonly DeepClone _deepClone;
|
|
||||||
|
|
||||||
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
|
|
||||||
|
|
||||||
private Dictionary<String, AnimatorControllerParameter> _parameters =
|
|
||||||
new Dictionary<string, AnimatorControllerParameter>();
|
|
||||||
|
|
||||||
private Dictionary<String, AnimatorController> _parameterSource =
|
|
||||||
new Dictionary<string, AnimatorController>();
|
|
||||||
|
|
||||||
private Dictionary<KeyValuePair<String, Motion>, Motion> _motions =
|
|
||||||
new Dictionary<KeyValuePair<string, Motion>, Motion>();
|
|
||||||
|
|
||||||
private Dictionary<KeyValuePair<String, AnimatorStateMachine>, AnimatorStateMachine> _stateMachines =
|
|
||||||
new Dictionary<KeyValuePair<string, AnimatorStateMachine>, AnimatorStateMachine>();
|
|
||||||
|
|
||||||
private Dictionary<Object, Object> _cloneMap;
|
|
||||||
|
|
||||||
private int _controllerBaseLayer = 0;
|
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
public VRC_AnimatorLayerControl.BlendableLayer? BlendableLayer;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public AnimatorCombiner(BuildContext context, String assetName)
|
|
||||||
{
|
|
||||||
_combined = new AnimatorController();
|
|
||||||
if (context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer))
|
|
||||||
{
|
|
||||||
AssetDatabase.AddObjectToAsset(_combined, context.AssetContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_combined.name = assetName;
|
|
||||||
|
|
||||||
_context = context;
|
|
||||||
_deepClone = new DeepClone(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnimatorController Finish()
|
|
||||||
{
|
|
||||||
FixTransitionTypeConflicts();
|
|
||||||
PruneEmptyLayers();
|
|
||||||
|
|
||||||
_combined.parameters = _parameters.Values.ToArray();
|
|
||||||
_combined.layers = _layers.ToArray();
|
|
||||||
return _combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MergeTypes(Dictionary<string, AnimatorControllerParameterType> types)
|
|
||||||
{
|
|
||||||
foreach (var p in _parameters.ToList())
|
|
||||||
{
|
|
||||||
if (types.TryGetValue(p.Key, out var outerValue))
|
|
||||||
{
|
|
||||||
if (outerValue == p.Value.type) continue;
|
|
||||||
|
|
||||||
if (outerValue == AnimatorControllerParameterType.Trigger
|
|
||||||
|| p.Value.type == AnimatorControllerParameterType.Trigger)
|
|
||||||
{
|
|
||||||
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
|
|
||||||
p.Key,
|
|
||||||
p.Value.type,
|
|
||||||
outerValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_parameters[p.Key].type = AnimatorControllerParameterType.Float;
|
|
||||||
types[p.Key] = AnimatorControllerParameterType.Float;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
types.Add(p.Key, p.Value.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When we merge multiple controllers with different types for the same parameter, we merge
|
|
||||||
/// them all into using floats; thanks to VRChat's implicit typecasting, we can do this even for
|
|
||||||
/// parameters registered as being ints or bools in the expressions parameter asset. However,
|
|
||||||
/// we do need to fix any transitions to use the right transition types after this conversion.
|
|
||||||
/// </summary>
|
|
||||||
private void FixTransitionTypeConflicts()
|
|
||||||
{
|
|
||||||
foreach (var layer in _layers)
|
|
||||||
{
|
|
||||||
foreach (var asm in layer.stateMachine.ReachableStateMachines())
|
|
||||||
{
|
|
||||||
foreach (ChildAnimatorState s in asm.states)
|
|
||||||
{
|
|
||||||
s.state.transitions = s.state.transitions.SelectMany(FixupTransition).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
asm.entryTransitions = asm.entryTransitions
|
|
||||||
.SelectMany(FixupTransition).ToArray();
|
|
||||||
asm.anyStateTransitions = asm.anyStateTransitions
|
|
||||||
.SelectMany(FixupTransition).ToArray();
|
|
||||||
|
|
||||||
foreach (var stateMachine in asm.stateMachines)
|
|
||||||
{
|
|
||||||
var ssm = stateMachine.stateMachine;
|
|
||||||
|
|
||||||
var stateMachineTransitions = asm.GetStateMachineTransitions(ssm);
|
|
||||||
if (stateMachineTransitions.Length > 0)
|
|
||||||
{
|
|
||||||
asm.SetStateMachineTransitions(ssm,
|
|
||||||
stateMachineTransitions.SelectMany(FixupTransition).ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<T> FixupTransition<T>(T t) where T: AnimatorTransitionBase, new()
|
|
||||||
{
|
|
||||||
if (!NeedsFixing(t.conditions))
|
|
||||||
{
|
|
||||||
yield return t;
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatorCondition[][][] combinations = t.conditions.Select(c => FixupCondition(c).ToArray()).ToArray();
|
|
||||||
|
|
||||||
// Generate the combinatorial explosion of conditions needed to emulate NotEquals with floats...
|
|
||||||
var conditions = ExplodeConditions(combinations).ToArray();
|
|
||||||
|
|
||||||
if (conditions.Length == 1)
|
|
||||||
{
|
|
||||||
t.conditions = conditions[0];
|
|
||||||
yield return t;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var conditionGroup in conditions)
|
|
||||||
{
|
|
||||||
t.conditions = conditionGroup;
|
|
||||||
yield return t;
|
|
||||||
|
|
||||||
var newTransition = new T();
|
|
||||||
EditorUtility.CopySerialized(t, newTransition);
|
|
||||||
if (_context.AssetContainer != null)
|
|
||||||
{
|
|
||||||
AssetDatabase.AddObjectToAsset(newTransition, _context.AssetContainer);
|
|
||||||
}
|
|
||||||
t = newTransition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool NeedsFixing(AnimatorCondition[] conditions)
|
|
||||||
{
|
|
||||||
return conditions.Any(c =>
|
|
||||||
{
|
|
||||||
if (!_parameters.TryGetValue(c.parameter, out var param)) return false;
|
|
||||||
|
|
||||||
switch (c.mode)
|
|
||||||
{
|
|
||||||
case AnimatorConditionMode.If when param.type != AnimatorControllerParameterType.Bool:
|
|
||||||
case AnimatorConditionMode.IfNot when param.type != AnimatorControllerParameterType.Bool:
|
|
||||||
case AnimatorConditionMode.Equals when param.type != AnimatorControllerParameterType.Int:
|
|
||||||
case AnimatorConditionMode.NotEqual when param.type != AnimatorControllerParameterType.Int:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<AnimatorCondition[]> ExplodeConditions(AnimatorCondition[][][] conditions)
|
|
||||||
{
|
|
||||||
int[] indices = new int[conditions.Length];
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
yield return conditions.SelectMany((group, i_) => group[indices[i_]]).ToArray();
|
|
||||||
|
|
||||||
// Increment the rightmost possible counter
|
|
||||||
int i;
|
|
||||||
for (i = indices.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (indices[i] < conditions[i].Length - 1)
|
|
||||||
{
|
|
||||||
indices[i]++;
|
|
||||||
// Unity 2019.....
|
|
||||||
// System.Array.Fill(indices, 0, i + 1, indices.Length - i - 1);
|
|
||||||
for (int j = i + 1; j < indices.Length; j++)
|
|
||||||
{
|
|
||||||
indices[j] = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < 0) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<AnimatorCondition[]> FixupCondition(AnimatorCondition c)
|
|
||||||
{
|
|
||||||
if (!_parameters.TryGetValue(c.parameter, out var paramDef))
|
|
||||||
{
|
|
||||||
// Parameter is undefined, don't touch this condition
|
|
||||||
yield return new[] { c };
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (c.mode)
|
|
||||||
{
|
|
||||||
case AnimatorConditionMode.If when paramDef.type == AnimatorControllerParameterType.Float:
|
|
||||||
{
|
|
||||||
c.mode = AnimatorConditionMode.Greater;
|
|
||||||
c.threshold = 0.5f;
|
|
||||||
yield return new[] { c };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AnimatorConditionMode.IfNot when paramDef.type == AnimatorControllerParameterType.Float:
|
|
||||||
{
|
|
||||||
c.mode = AnimatorConditionMode.Less;
|
|
||||||
c.threshold = 0.5f;
|
|
||||||
yield return new[] { c };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AnimatorConditionMode.Equals when paramDef.type == AnimatorControllerParameterType.Float:
|
|
||||||
{
|
|
||||||
var c1 = c;
|
|
||||||
var c2 = c;
|
|
||||||
c1.mode = AnimatorConditionMode.Greater;
|
|
||||||
c1.threshold -= 0.1f;
|
|
||||||
c2.mode = AnimatorConditionMode.Less;
|
|
||||||
c2.threshold += 0.1f;
|
|
||||||
yield return new[] { c1, c2 };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AnimatorConditionMode.NotEqual when paramDef.type == AnimatorControllerParameterType.Float:
|
|
||||||
{
|
|
||||||
var origThresh = c.threshold;
|
|
||||||
c.mode = AnimatorConditionMode.Greater;
|
|
||||||
c.threshold = origThresh + 0.1f;
|
|
||||||
yield return new[] { c };
|
|
||||||
|
|
||||||
c.mode = AnimatorConditionMode.Less;
|
|
||||||
c.threshold = origThresh - 0.1f;
|
|
||||||
yield return new[] { c };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
yield return new[] { c };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PruneEmptyLayers()
|
|
||||||
{
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
// We can't safely correct the layer index of a VRCAnimatorLayerControl without knowing if it refers to
|
|
||||||
// _this_ animator controller, so just skip this. We'll do the empty layer pruning later when we merge
|
|
||||||
// everything together.
|
|
||||||
if (BlendableLayer == null) return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var originalLayers = _layers;
|
|
||||||
int[] layerIndexMappings = new int[originalLayers.Count];
|
|
||||||
|
|
||||||
List<AnimatorControllerLayer> newLayers = new List<AnimatorControllerLayer>();
|
|
||||||
|
|
||||||
for (int i = 0; i < originalLayers.Count; i++)
|
|
||||||
{
|
|
||||||
if (i > 0 && IsEmptyLayer(originalLayers[i]))
|
|
||||||
{
|
|
||||||
layerIndexMappings[i] = -1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
layerIndexMappings[i] = newLayers.Count;
|
|
||||||
newLayers.Add(originalLayers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var layer in newLayers)
|
|
||||||
{
|
|
||||||
if (layer.stateMachine == null) continue;
|
|
||||||
|
|
||||||
foreach (var asset in layer.stateMachine.ReferencedAssets(includeScene: false))
|
|
||||||
{
|
|
||||||
if (asset is AnimatorState alc)
|
|
||||||
{
|
|
||||||
alc.behaviours = AdjustStateBehaviors(alc.behaviours);
|
|
||||||
}
|
|
||||||
else if (asset is AnimatorStateMachine asm)
|
|
||||||
{
|
|
||||||
asm.behaviours = AdjustStateBehaviors(asm.behaviours);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_layers = newLayers;
|
|
||||||
|
|
||||||
StateMachineBehaviour[] AdjustStateBehaviors(StateMachineBehaviour[] behaviours)
|
|
||||||
{
|
|
||||||
if (behaviours.Length == 0) return behaviours;
|
|
||||||
|
|
||||||
var newBehaviors = new List<StateMachineBehaviour>();
|
|
||||||
foreach (var b in behaviours)
|
|
||||||
{
|
|
||||||
switch (b)
|
|
||||||
{
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
case VRCAnimatorLayerControl alc when alc.playable == BlendableLayer:
|
|
||||||
int newLayer = -1;
|
|
||||||
if (alc.layer >= 0 && alc.layer < layerIndexMappings.Length)
|
|
||||||
{
|
|
||||||
newLayer = layerIndexMappings[alc.layer];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLayer != -1)
|
|
||||||
{
|
|
||||||
alc.layer = newLayer;
|
|
||||||
newBehaviors.Add(alc);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
default:
|
|
||||||
newBehaviors.Add(b);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBehaviors.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsEmptyLayer(AnimatorControllerLayer layer)
|
|
||||||
{
|
|
||||||
if (layer.syncedLayerIndex >= 0) return false;
|
|
||||||
if (layer.avatarMask != null) return false;
|
|
||||||
|
|
||||||
return layer.stateMachine == null
|
|
||||||
|| (layer.stateMachine.states.Length == 0 && layer.stateMachine.stateMachines.Length == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults,
|
|
||||||
bool forceFirstLayerWeight = false)
|
|
||||||
{
|
|
||||||
_controllerBaseLayer = _layers.Count;
|
|
||||||
_cloneMap = new Dictionary<Object, Object>();
|
|
||||||
|
|
||||||
foreach (var param in controller.parameters)
|
|
||||||
{
|
|
||||||
if (_parameters.TryGetValue(param.name, out var acp))
|
|
||||||
{
|
|
||||||
if (acp.type == param.type) continue;
|
|
||||||
|
|
||||||
if (acp.type != param.type &&
|
|
||||||
(acp.type == AnimatorControllerParameterType.Trigger ||
|
|
||||||
param.type == AnimatorControllerParameterType.Trigger))
|
|
||||||
{
|
|
||||||
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
|
|
||||||
param.name,
|
|
||||||
acp.type.ToString(),
|
|
||||||
param.type.ToString(),
|
|
||||||
controller,
|
|
||||||
_parameterSource[param.name]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acp.type = AnimatorControllerParameterType.Float;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var clonedParameter = new AnimatorControllerParameter()
|
|
||||||
{
|
|
||||||
name = param.name,
|
|
||||||
type = param.type,
|
|
||||||
defaultBool = param.defaultBool,
|
|
||||||
defaultFloat = param.defaultFloat,
|
|
||||||
defaultInt = param.defaultInt
|
|
||||||
};
|
|
||||||
|
|
||||||
_parameters.Add(param.name, clonedParameter);
|
|
||||||
_parameterSource.Add(param.name, controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool first = true;
|
|
||||||
var layers = controller.layers;
|
|
||||||
foreach (var layer in layers)
|
|
||||||
{
|
|
||||||
insertLayer(basePath, layer, first, writeDefaults, layers);
|
|
||||||
if (first && forceFirstLayerWeight)
|
|
||||||
{
|
|
||||||
_layers[_layers.Count - 1].defaultWeight = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddOverrideController(string basePath, AnimatorOverrideController overrideController,
|
|
||||||
bool? writeDefaults)
|
|
||||||
{
|
|
||||||
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
|
|
||||||
if (controller == null) return;
|
|
||||||
_deepClone.OverrideController = overrideController;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.AddController(basePath, controller, writeDefaults);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertLayer(
|
|
||||||
string basePath,
|
|
||||||
AnimatorControllerLayer layer,
|
|
||||||
bool first,
|
|
||||||
bool? writeDefaults,
|
|
||||||
AnimatorControllerLayer[] layers
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var newLayer = new AnimatorControllerLayer()
|
|
||||||
{
|
|
||||||
name = layer.name,
|
|
||||||
avatarMask = _deepClone.DoClone(layer.avatarMask, basePath, _cloneMap),
|
|
||||||
blendingMode = layer.blendingMode,
|
|
||||||
defaultWeight = first ? 1 : layer.defaultWeight,
|
|
||||||
syncedLayerIndex = layer.syncedLayerIndex,
|
|
||||||
syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming,
|
|
||||||
iKPass = layer.iKPass,
|
|
||||||
stateMachine = mapStateMachine(basePath, layer.stateMachine),
|
|
||||||
};
|
|
||||||
|
|
||||||
UpdateWriteDefaults(newLayer.stateMachine, writeDefaults);
|
|
||||||
|
|
||||||
if (newLayer.syncedLayerIndex != -1 && newLayer.syncedLayerIndex >= 0 &&
|
|
||||||
newLayer.syncedLayerIndex < layers.Length)
|
|
||||||
{
|
|
||||||
// Transfer any motion overrides onto the new synced layer
|
|
||||||
var baseLayer = layers[newLayer.syncedLayerIndex];
|
|
||||||
foreach (var state in WalkAllStates(baseLayer.stateMachine))
|
|
||||||
{
|
|
||||||
var overrideMotion = layer.GetOverrideMotion(state);
|
|
||||||
if (overrideMotion != null)
|
|
||||||
{
|
|
||||||
var newMotion = _deepClone.DoClone(overrideMotion, basePath, _cloneMap);
|
|
||||||
newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], newMotion);
|
|
||||||
}
|
|
||||||
|
|
||||||
var overrideBehaviors = (StateMachineBehaviour[])layer.GetOverrideBehaviours(state)?.Clone();
|
|
||||||
if (overrideBehaviors != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < overrideBehaviors.Length; i++)
|
|
||||||
{
|
|
||||||
overrideBehaviors[i] = _deepClone.DoClone(overrideBehaviors[i]);
|
|
||||||
AdjustBehavior(overrideBehaviors[i], basePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
newLayer.SetOverrideBehaviours((AnimatorState)_cloneMap[state], overrideBehaviors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newLayer.syncedLayerIndex += _controllerBaseLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_layers.Add(newLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<AnimatorState> WalkAllStates(AnimatorStateMachine animatorStateMachine)
|
|
||||||
{
|
|
||||||
HashSet<Object> visited = new HashSet<Object>();
|
|
||||||
|
|
||||||
foreach (var state in VisitStateMachine(animatorStateMachine))
|
|
||||||
{
|
|
||||||
yield return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<AnimatorState> VisitStateMachine(AnimatorStateMachine layerStateMachine)
|
|
||||||
{
|
|
||||||
if (!visited.Add(layerStateMachine)) yield break;
|
|
||||||
|
|
||||||
foreach (var state in layerStateMachine.states)
|
|
||||||
{
|
|
||||||
if (state.state == null) continue;
|
|
||||||
|
|
||||||
yield return state.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in layerStateMachine.stateMachines)
|
|
||||||
{
|
|
||||||
if (child.stateMachine == null) continue;
|
|
||||||
|
|
||||||
if (visited.Contains(child.stateMachine)) continue;
|
|
||||||
foreach (var state in VisitStateMachine(child.stateMachine))
|
|
||||||
{
|
|
||||||
yield return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateWriteDefaults(AnimatorStateMachine stateMachine, bool? writeDefaults)
|
|
||||||
{
|
|
||||||
if (!writeDefaults.HasValue) return;
|
|
||||||
|
|
||||||
var queue = new Queue<AnimatorStateMachine>();
|
|
||||||
queue.Enqueue(stateMachine);
|
|
||||||
while (queue.Count > 0)
|
|
||||||
{
|
|
||||||
var sm = queue.Dequeue();
|
|
||||||
foreach (var state in sm.states)
|
|
||||||
{
|
|
||||||
state.state.writeDefaultValues = writeDefaults.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in sm.stateMachines)
|
|
||||||
{
|
|
||||||
queue.Enqueue(child.stateMachine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine)
|
|
||||||
{
|
|
||||||
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
|
|
||||||
|
|
||||||
if (_stateMachines.TryGetValue(cacheKey, out var asm))
|
|
||||||
{
|
|
||||||
return asm;
|
|
||||||
}
|
|
||||||
|
|
||||||
asm = _deepClone.DoClone(layerStateMachine, basePath, _cloneMap);
|
|
||||||
|
|
||||||
foreach (var state in WalkAllStates(asm))
|
|
||||||
{
|
|
||||||
foreach (var behavior in state.behaviours)
|
|
||||||
{
|
|
||||||
AdjustBehavior(behavior, basePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_stateMachines[cacheKey] = asm;
|
|
||||||
return asm;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AdjustBehavior(StateMachineBehaviour behavior, string basePath)
|
|
||||||
{
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
|
||||||
switch (behavior)
|
|
||||||
{
|
|
||||||
case VRCAnimatorLayerControl layerControl:
|
|
||||||
{
|
|
||||||
// TODO - need to figure out how to handle cross-layer references. For now this will handle
|
|
||||||
// intra-animator cases.
|
|
||||||
layerControl.layer += _controllerBaseLayer;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
|
|
||||||
case VRCAnimatorPlayAudio playAudio:
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(playAudio.SourcePath) && !string.IsNullOrEmpty(basePath) && !playAudio.SourcePath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
playAudio.SourcePath = $"{basePath}/{playAudio.SourcePath}";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 614457d82b1a4b109788029754c9fc1a
|
|
||||||
timeCreated: 1703674134
|
|
@ -1,289 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using nadena.dev.modular_avatar.core.editor;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using BuildContext = nadena.dev.ndmf.BuildContext;
|
|
||||||
using Object = UnityEngine.Object;
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
using UnityObject = Object;
|
|
||||||
|
|
||||||
internal class DeepClone
|
|
||||||
{
|
|
||||||
private bool _isSaved;
|
|
||||||
private UnityObject _combined;
|
|
||||||
|
|
||||||
public AnimatorOverrideController OverrideController { get; set; }
|
|
||||||
|
|
||||||
public DeepClone(BuildContext context)
|
|
||||||
{
|
|
||||||
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
|
|
||||||
_combined = context.AssetContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T DoClone<T>(T original,
|
|
||||||
string basePath = null,
|
|
||||||
Dictionary<UnityObject, UnityObject> cloneMap = null
|
|
||||||
) where T : UnityObject
|
|
||||||
{
|
|
||||||
if (original == null) return null;
|
|
||||||
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
|
|
||||||
|
|
||||||
Func<UnityObject, UnityObject> visitor = null;
|
|
||||||
if (basePath != null)
|
|
||||||
{
|
|
||||||
visitor = o => CloneWithPathMapping(o, basePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes,
|
|
||||||
// MonoScripts...), so check for the types we care about here
|
|
||||||
switch (original)
|
|
||||||
{
|
|
||||||
// Any object referenced by an animator that we intend to mutate needs to be listed here.
|
|
||||||
case Motion _:
|
|
||||||
case AnimatorController _:
|
|
||||||
case AnimatorState _:
|
|
||||||
case AnimatorStateMachine _:
|
|
||||||
case AnimatorTransitionBase _:
|
|
||||||
case StateMachineBehaviour _:
|
|
||||||
case AvatarMask _:
|
|
||||||
break; // We want to clone these types
|
|
||||||
|
|
||||||
case AudioClip _: //Used in VRC Animator Play Audio State Behavior
|
|
||||||
// Leave textures, materials, and script definitions alone
|
|
||||||
case Texture2D _:
|
|
||||||
case MonoScript _:
|
|
||||||
case Material _:
|
|
||||||
return original;
|
|
||||||
|
|
||||||
// Also avoid copying unknown scriptable objects.
|
|
||||||
// This ensures compatibility with e.g. avatar remote, which stores state information in a state
|
|
||||||
// behaviour referencing a custom ScriptableObject
|
|
||||||
case ScriptableObject _:
|
|
||||||
return original;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
|
|
||||||
if (OverrideController != null && original is AnimationClip srcClip)
|
|
||||||
{
|
|
||||||
T overrideClip = OverrideController[srcClip] as T;
|
|
||||||
if (overrideClip != null)
|
|
||||||
{
|
|
||||||
original = overrideClip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cloneMap.ContainsKey(original))
|
|
||||||
{
|
|
||||||
return (T)cloneMap[original];
|
|
||||||
}
|
|
||||||
|
|
||||||
var obj = visitor?.Invoke(original);
|
|
||||||
if (obj != null)
|
|
||||||
{
|
|
||||||
cloneMap[original] = obj;
|
|
||||||
if (obj != original)
|
|
||||||
{
|
|
||||||
ObjectRegistry.RegisterReplacedObject(original, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T)obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
|
|
||||||
if (ctor == null || original is ScriptableObject)
|
|
||||||
{
|
|
||||||
obj = UnityObject.Instantiate(original);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
obj = (T)ctor.Invoke(Array.Empty<object>());
|
|
||||||
EditorUtility.CopySerialized(original, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneMap[original] = obj;
|
|
||||||
ObjectRegistry.RegisterReplacedObject(original, obj);
|
|
||||||
|
|
||||||
if (_isSaved)
|
|
||||||
{
|
|
||||||
AssetDatabase.AddObjectToAsset(obj, _combined);
|
|
||||||
}
|
|
||||||
|
|
||||||
SerializedObject so = new SerializedObject(obj);
|
|
||||||
SerializedProperty prop = so.GetIterator();
|
|
||||||
|
|
||||||
bool enterChildren = true;
|
|
||||||
while (prop.Next(enterChildren))
|
|
||||||
{
|
|
||||||
enterChildren = true;
|
|
||||||
switch (prop.propertyType)
|
|
||||||
{
|
|
||||||
case SerializedPropertyType.ObjectReference:
|
|
||||||
{
|
|
||||||
if (prop.objectReferenceValue != null && prop.objectReferenceValue != obj)
|
|
||||||
{
|
|
||||||
var newObj = DoClone(prop.objectReferenceValue, basePath, cloneMap);
|
|
||||||
prop.objectReferenceValue = newObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Iterating strings can get super slow...
|
|
||||||
case SerializedPropertyType.String:
|
|
||||||
enterChildren = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
so.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
|
|
||||||
return (T)obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal for testing
|
|
||||||
internal static AvatarMask CloneAvatarMask(AvatarMask mask, string basePath)
|
|
||||||
{
|
|
||||||
if (basePath.EndsWith("/")) basePath = basePath.Substring(0, basePath.Length - 1);
|
|
||||||
|
|
||||||
var newMask = new AvatarMask();
|
|
||||||
|
|
||||||
// Transfer first the humanoid mask data
|
|
||||||
EditorUtility.CopySerialized(mask, newMask);
|
|
||||||
|
|
||||||
var srcSo = new SerializedObject(mask);
|
|
||||||
var dstSo = new SerializedObject(newMask);
|
|
||||||
var srcElements = srcSo.FindProperty("m_Elements");
|
|
||||||
|
|
||||||
if (basePath == "" || srcElements.arraySize == 0) return newMask; // no changes required
|
|
||||||
|
|
||||||
// We now need to prefix the elements of basePath (with weight zero)
|
|
||||||
|
|
||||||
var newElements = new List<string>();
|
|
||||||
|
|
||||||
var accum = "";
|
|
||||||
foreach (var element in basePath.Split("/"))
|
|
||||||
{
|
|
||||||
if (accum != "") accum += "/";
|
|
||||||
accum += element;
|
|
||||||
|
|
||||||
newElements.Add(accum);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dstElements = dstSo.FindProperty("m_Elements");
|
|
||||||
|
|
||||||
// We'll need to create new array elements by using DuplicateCommand. We'll then rewrite the whole
|
|
||||||
// list to keep things in traversal order.
|
|
||||||
for (var i = 0; i < newElements.Count; i++) dstElements.GetArrayElementAtIndex(0).DuplicateCommand();
|
|
||||||
|
|
||||||
var totalElements = srcElements.arraySize + newElements.Count;
|
|
||||||
for (var i = 0; i < totalElements; i++)
|
|
||||||
{
|
|
||||||
var dstElem = dstElements.GetArrayElementAtIndex(i);
|
|
||||||
var dstPath = dstElem.FindPropertyRelative("m_Path");
|
|
||||||
var dstWeight = dstElem.FindPropertyRelative("m_Weight");
|
|
||||||
|
|
||||||
var srcIndex = i - newElements.Count;
|
|
||||||
if (srcIndex < 0)
|
|
||||||
{
|
|
||||||
dstPath.stringValue = newElements[i];
|
|
||||||
dstWeight.floatValue = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var srcElem = srcElements.GetArrayElementAtIndex(srcIndex);
|
|
||||||
dstPath.stringValue = basePath + "/" + srcElem.FindPropertyRelative("m_Path").stringValue;
|
|
||||||
dstWeight.floatValue = srcElem.FindPropertyRelative("m_Weight").floatValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dstSo.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
|
|
||||||
return newMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
|
|
||||||
{
|
|
||||||
if (o is AvatarMask mask)
|
|
||||||
{
|
|
||||||
return CloneAvatarMask(mask, basePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (o is AnimationClip clip)
|
|
||||||
{
|
|
||||||
// We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset
|
|
||||||
// from a persistent asset. If the asset is persistent, skip cases where path editing isn't required,
|
|
||||||
// or where this is one of the special VRC proxy animations.
|
|
||||||
if (EditorUtility.IsPersistent(o) && (basePath == "" || Util.IsProxyAnimation(clip))) return clip;
|
|
||||||
|
|
||||||
AnimationClip newClip = new AnimationClip();
|
|
||||||
newClip.name = "rebased " + clip.name;
|
|
||||||
if (_isSaved)
|
|
||||||
{
|
|
||||||
AssetDatabase.AddObjectToAsset(newClip, _combined);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
|
|
||||||
{
|
|
||||||
var newBinding = binding;
|
|
||||||
newBinding.path = MapPath(binding, basePath);
|
|
||||||
// https://github.com/bdunderscore/modular-avatar/issues/950
|
|
||||||
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
|
|
||||||
// curves to be forgotten; use SetEditorCurve instead.
|
|
||||||
AnimationUtility.SetEditorCurve(newClip, newBinding,
|
|
||||||
AnimationUtility.GetEditorCurve(clip, binding));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
|
|
||||||
{
|
|
||||||
var newBinding = objBinding;
|
|
||||||
newBinding.path = MapPath(objBinding, basePath);
|
|
||||||
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
|
|
||||||
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
|
|
||||||
}
|
|
||||||
|
|
||||||
newClip.wrapMode = clip.wrapMode;
|
|
||||||
newClip.legacy = clip.legacy;
|
|
||||||
newClip.frameRate = clip.frameRate;
|
|
||||||
newClip.localBounds = clip.localBounds;
|
|
||||||
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
|
|
||||||
|
|
||||||
return newClip;
|
|
||||||
}
|
|
||||||
else if (o is Texture)
|
|
||||||
{
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string MapPath(EditorCurveBinding binding, string basePath)
|
|
||||||
{
|
|
||||||
if (binding.type == typeof(Animator) && binding.path == "")
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newPath = binding.path == "" ? basePath : basePath + binding.path;
|
|
||||||
if (newPath.EndsWith("/"))
|
|
||||||
{
|
|
||||||
newPath = newPath.Substring(0, newPath.Length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b33090a3e763464ab05f3efe07e0cbd3
|
|
||||||
timeCreated: 1703148770
|
|
@ -1,18 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEditor;
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
internal class EditorCurveBindingComparer : IEqualityComparer<EditorCurveBinding>
|
|
||||||
{
|
|
||||||
public bool Equals(UnityEditor.EditorCurveBinding x, UnityEditor.EditorCurveBinding y)
|
|
||||||
{
|
|
||||||
return x.path == y.path && x.type == y.type && x.propertyName == y.propertyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(UnityEditor.EditorCurveBinding obj)
|
|
||||||
{
|
|
||||||
return obj.path.GetHashCode() ^ obj.type.GetHashCode() ^ obj.propertyName.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e751f7889323485bbe202285a47cb0d4
|
|
||||||
timeCreated: 1719196767
|
|
@ -1,9 +1,13 @@
|
|||||||
using System.Linq;
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
using System.Linq;
|
||||||
|
using nadena.dev.modular_avatar.core.editor;
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using BuildContext = nadena.dev.ndmf.BuildContext;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
namespace nadena.dev.modular_avatar.animation
|
||||||
{
|
{
|
||||||
@ -15,46 +19,64 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
{
|
{
|
||||||
protected override void Execute(BuildContext context)
|
protected override void Execute(BuildContext context)
|
||||||
{
|
{
|
||||||
var asc = context.Extension<AnimationServicesContext>();
|
var asc = context.Extension<AnimatorServicesContext>();
|
||||||
if (!asc.BoundReadableProperties.Any()) return;
|
var activeProxies = context.GetState<ReadablePropertyExtension.Retained>().proxyProps
|
||||||
|
.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||||
|
if (activeProxies.Count == 0) return;
|
||||||
|
|
||||||
var fx = (AnimatorController)context.AvatarDescriptor.baseAnimationLayers
|
// Filter any proxies not used in animator transitions
|
||||||
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX).animatorController;
|
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;
|
if (fx == null) return;
|
||||||
|
|
||||||
|
var nullMotion = new AnimationClip();
|
||||||
|
nullMotion.name = "NullMotion";
|
||||||
|
|
||||||
var blendTree = new BlendTree();
|
var blendTree = new BlendTree();
|
||||||
blendTree.blendType = BlendTreeType.Direct;
|
blendTree.blendType = BlendTreeType.Direct;
|
||||||
blendTree.useAutomaticThresholds = false;
|
blendTree.useAutomaticThresholds = false;
|
||||||
|
|
||||||
blendTree.children = asc.BoundReadableProperties.Select(GenerateDelayChild).ToArray();
|
blendTree.children = activeProxies
|
||||||
|
.Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
var asm = new AnimatorStateMachine();
|
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
|
||||||
var state = new AnimatorState();
|
var state = layer.StateMachine.AddState("DelayDisable");
|
||||||
state.name = "DelayDisable";
|
layer.StateMachine.DefaultState = state;
|
||||||
state.motion = blendTree;
|
|
||||||
state.writeDefaultValues = true;
|
|
||||||
|
|
||||||
asm.defaultState = state;
|
state.WriteDefaultValues = true;
|
||||||
asm.states = new[]
|
state.Motion = asc.ControllerContext.Clone(blendTree);
|
||||||
|
|
||||||
|
// Ensure the initial state of readable props matches the actual state of the gameobject
|
||||||
|
foreach (var controller in asc.ControllerContext.GetAllControllers())
|
||||||
{
|
{
|
||||||
new ChildAnimatorState
|
foreach (var (binding, prop) in activeProxies)
|
||||||
{
|
{
|
||||||
state = state,
|
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
|
||||||
position = Vector3.zero
|
|
||||||
|
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
|
||||||
|
{
|
||||||
|
p.defaultFloat = obj.activeSelf ? 1 : 0;
|
||||||
|
controller.Parameters = controller.Parameters.SetItem(prop, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
fx.layers = fx.layers.Append(new AnimatorControllerLayer
|
|
||||||
{
|
|
||||||
name = "DelayDisable",
|
|
||||||
stateMachine = asm,
|
|
||||||
defaultWeight = 1,
|
|
||||||
blendingMode = AnimatorLayerBlendingMode.Override
|
|
||||||
}).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChildMotion GenerateDelayChild((EditorCurveBinding, string) binding)
|
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
|
||||||
{
|
{
|
||||||
var ecb = binding.Item1;
|
var ecb = binding.Item1;
|
||||||
var prop = binding.Item2;
|
var prop = binding.Item2;
|
||||||
@ -64,12 +86,43 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
curve.AddKey(0, 1);
|
curve.AddKey(0, 1);
|
||||||
AnimationUtility.SetEditorCurve(motion, ecb, curve);
|
AnimationUtility.SetEditorCurve(motion, ecb, curve);
|
||||||
|
|
||||||
return new ChildMotion
|
// 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,
|
motion = motion,
|
||||||
directBlendParameter = prop,
|
timeScale = 1,
|
||||||
|
threshold = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ChildMotion
|
||||||
|
{
|
||||||
|
motion = bufferBlendTree,
|
||||||
|
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
||||||
timeScale = 1
|
timeScale = 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -1,17 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This interface tags components which supply additional animation controllers for merging. They will be given
|
|
||||||
/// an opportunity to apply animation path updates when the TrackObjectRenamesContext is committed.
|
|
||||||
/// </summary>
|
|
||||||
internal interface IOnCommitObjectRenames
|
|
||||||
{
|
|
||||||
void OnCommitObjectRenames(BuildContext buildContext, PathMappings renameContext);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6a66f552b8b334a45a986bfcf6767200
|
|
||||||
timeCreated: 1692511752
|
|
246
Editor/Animation/MMDRelayPass.cs
Normal file
246
Editor/Animation/MMDRelayPass.cs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using nadena.dev.modular_avatar.core;
|
||||||
|
using nadena.dev.modular_avatar.core.editor;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.Animations;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using VRC.SDKBase;
|
||||||
|
using BuildContext = nadena.dev.ndmf.BuildContext;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.animation
|
||||||
|
{
|
||||||
|
internal class MMDRelayState
|
||||||
|
{
|
||||||
|
internal HashSet<VirtualLayer> mmdAffectedOriginalLayers = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MMDRelayEarlyPass : Pass<MMDRelayEarlyPass>
|
||||||
|
{
|
||||||
|
protected override void Execute(BuildContext context)
|
||||||
|
{
|
||||||
|
if (!MMDRelayPass.ShouldRun(context)) return;
|
||||||
|
|
||||||
|
var asc = context.Extension<AnimatorServicesContext>();
|
||||||
|
if (asc.ControllerContext.Controllers.TryGetValue(VRCAvatarDescriptor.AnimLayerType.FX, out var fx))
|
||||||
|
{
|
||||||
|
context.GetState<MMDRelayState>().mmdAffectedOriginalLayers = new HashSet<VirtualLayer>(
|
||||||
|
fx.Layers.Skip(1).Take(2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Many MMD worlds animate the first three FX layers to weight zero. When MA injects new layers, this can hit
|
||||||
|
/// unintended layers (eg the RC base state layer).
|
||||||
|
/// To work around this, we'll inject a layer which will relay its active state into a parameter; then, we add a
|
||||||
|
/// layer to relay this to layers which should be affected. Finally, any layer which _shouldn't_ be affected is
|
||||||
|
/// pushed out of the first three layers by injecting dummy layers.
|
||||||
|
/// </summary>
|
||||||
|
internal class MMDRelayPass : Pass<MMDRelayPass>
|
||||||
|
{
|
||||||
|
private const string MMDRelayParam = "__MA/Internal/MMDNotActive";
|
||||||
|
internal const string ControlLayerName = "Modular Avatar: MMD Control";
|
||||||
|
internal const string DummyLayerName = "Modular Avatar: MMD Dummy";
|
||||||
|
internal const string StateNameInitial = "Initial";
|
||||||
|
internal const string StateNameNotMMD = "NotMMD";
|
||||||
|
internal const string StateNameMMD = "MMD";
|
||||||
|
|
||||||
|
internal static bool ShouldRun(BuildContext context)
|
||||||
|
{
|
||||||
|
var settings = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarVRChatSettings>(true);
|
||||||
|
return settings.FirstOrDefault()?.MMDWorldSupport ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Execute(BuildContext context)
|
||||||
|
{
|
||||||
|
if (!ShouldRun(context)) return;
|
||||||
|
|
||||||
|
var asc = context.Extension<AnimatorServicesContext>();
|
||||||
|
if (!asc.ControllerContext.Controllers.TryGetValue(VRCAvatarDescriptor.AnimLayerType.FX, out var fx))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var affectedLayers = context.GetState<MMDRelayState>().mmdAffectedOriginalLayers;
|
||||||
|
|
||||||
|
foreach (var layer in fx.Layers)
|
||||||
|
{
|
||||||
|
if (layer.StateMachine == null) continue;
|
||||||
|
|
||||||
|
var rootMMDModeBehaviors = layer.StateMachine.Behaviours
|
||||||
|
.OfType<ModularAvatarMMDLayerControl>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (rootMMDModeBehaviors.Count == 0) continue;
|
||||||
|
if (rootMMDModeBehaviors.Count > 1)
|
||||||
|
{
|
||||||
|
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
|
||||||
|
"error.mmd.multiple_mmd_mode_behaviors", layer.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootMMDModeBehaviors[0].DisableInMMDMode)
|
||||||
|
{
|
||||||
|
affectedLayers.Add(layer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
affectedLayers.Remove(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.StateMachine.Behaviours = layer.StateMachine.Behaviours
|
||||||
|
.Where(b => b is not ModularAvatarMMDLayerControl).ToImmutableList();
|
||||||
|
Object.DestroyImmediate(rootMMDModeBehaviors[0]);
|
||||||
|
|
||||||
|
// check for child behaviors
|
||||||
|
// TODO: implement filtering on AllReachableNodes
|
||||||
|
foreach (var node in layer.AllReachableNodes())
|
||||||
|
{
|
||||||
|
if (node is VirtualState state)
|
||||||
|
{
|
||||||
|
if (state.Behaviours.Any(b => b is ModularAvatarMMDLayerControl))
|
||||||
|
{
|
||||||
|
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
|
||||||
|
"error.mmd.mmd_mode_in_child_state", layer.Name, state.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (node is VirtualStateMachine vsm)
|
||||||
|
{
|
||||||
|
if (vsm.Behaviours.Any(b => b is ModularAvatarMMDLayerControl))
|
||||||
|
{
|
||||||
|
ErrorReport.ReportError(Localization.L, ErrorSeverity.Error,
|
||||||
|
"error.mmd.mmd_mode_in_child_state_machine", layer.Name, vsm.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsAdjustment = fx.Layers.Select((layer, index) => (layer, index))
|
||||||
|
.Any(pair => affectedLayers.Contains(pair.layer) != (pair.index < 3 && pair.index != 0));
|
||||||
|
if (!needsAdjustment) return;
|
||||||
|
|
||||||
|
var toDisable = fx.Layers.Where(l => affectedLayers.Contains(l))
|
||||||
|
.Select(l => l.VirtualLayerIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
fx.Parameters = fx.Parameters.Add(MMDRelayParam, new AnimatorControllerParameter
|
||||||
|
{
|
||||||
|
name = MMDRelayParam,
|
||||||
|
type = AnimatorControllerParameterType.Float,
|
||||||
|
defaultFloat = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
var currentLayers = fx.Layers.ToList();
|
||||||
|
var newLayers = new List<VirtualLayer>();
|
||||||
|
|
||||||
|
// Layer zero's weight can't be changed anyway, so leave it where it is.
|
||||||
|
newLayers.Add(currentLayers[0]);
|
||||||
|
currentLayers.RemoveAt(0);
|
||||||
|
newLayers.Add(CreateMMDLayer(fx, toDisable));
|
||||||
|
|
||||||
|
// Add a dummy layer
|
||||||
|
var dummy = fx.AddLayer(new LayerPriority(0), DummyLayerName);
|
||||||
|
var s = dummy.StateMachine!.DefaultState = dummy.StateMachine.AddState("Dummy");
|
||||||
|
s.Motion = VirtualClip.Create("empty");
|
||||||
|
newLayers.Add(dummy);
|
||||||
|
|
||||||
|
fx.Layers = newLayers.Concat(currentLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualLayer CreateMMDLayer(VirtualAnimatorController fx, List<int> virtualLayers)
|
||||||
|
{
|
||||||
|
// We'll reorder this later, so the layer priority doesn't matter
|
||||||
|
var mmdControl = fx.AddLayer(new LayerPriority(0), ControlLayerName);
|
||||||
|
var stateMachine = mmdControl.StateMachine ?? throw new Exception("No state machine on MMD Control layer");
|
||||||
|
|
||||||
|
var motion = VirtualClip.Create("MMDRelay");
|
||||||
|
motion.SetFloatCurve(EditorCurveBinding.FloatCurve("", typeof(Animator), MMDRelayParam),
|
||||||
|
AnimationCurve.Constant(0, 1, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
var state_initial = stateMachine.AddState(StateNameInitial);
|
||||||
|
state_initial.Motion = motion;
|
||||||
|
|
||||||
|
var state_notmmd = stateMachine.AddState(StateNameNotMMD);
|
||||||
|
state_notmmd.Motion = motion;
|
||||||
|
|
||||||
|
var state_mmd = stateMachine.AddState(StateNameMMD);
|
||||||
|
state_mmd.Motion = motion;
|
||||||
|
|
||||||
|
var t = VirtualStateTransition.Create();
|
||||||
|
t.SetDestination(state_mmd);
|
||||||
|
t.Conditions = ImmutableList.Create(new AnimatorCondition
|
||||||
|
{
|
||||||
|
mode = AnimatorConditionMode.Less,
|
||||||
|
parameter = MMDRelayParam,
|
||||||
|
threshold = 0.5f
|
||||||
|
});
|
||||||
|
|
||||||
|
state_notmmd.Transitions = ImmutableList.Create(t);
|
||||||
|
|
||||||
|
t = VirtualStateTransition.Create();
|
||||||
|
t.SetDestination(state_notmmd);
|
||||||
|
t.Conditions = ImmutableList.Create(new AnimatorCondition
|
||||||
|
{
|
||||||
|
mode = AnimatorConditionMode.Greater,
|
||||||
|
parameter = MMDRelayParam,
|
||||||
|
threshold = 0.5f
|
||||||
|
});
|
||||||
|
|
||||||
|
state_mmd.Transitions = ImmutableList.Create(t);
|
||||||
|
|
||||||
|
t = VirtualStateTransition.Create();
|
||||||
|
t.SetDestination(state_mmd);
|
||||||
|
t.Conditions = ImmutableList.Create(new AnimatorCondition
|
||||||
|
{
|
||||||
|
mode = AnimatorConditionMode.Less,
|
||||||
|
parameter = MMDRelayParam,
|
||||||
|
threshold = 0.5f
|
||||||
|
});
|
||||||
|
|
||||||
|
state_initial.Transitions = ImmutableList.Create(t);
|
||||||
|
|
||||||
|
stateMachine.DefaultState = state_initial;
|
||||||
|
|
||||||
|
var mmd_behaviors = ImmutableList.CreateBuilder<StateMachineBehaviour>();
|
||||||
|
var notmmd_behaviors = ImmutableList.CreateBuilder<StateMachineBehaviour>();
|
||||||
|
|
||||||
|
foreach (var index in virtualLayers)
|
||||||
|
{
|
||||||
|
var behavior = ScriptableObject.CreateInstance<VRCAnimatorLayerControl>();
|
||||||
|
behavior.layer = index;
|
||||||
|
behavior.playable = VRC_AnimatorLayerControl.BlendableLayer.FX;
|
||||||
|
behavior.goalWeight = 0;
|
||||||
|
behavior.blendDuration = 0;
|
||||||
|
|
||||||
|
mmd_behaviors.Add(behavior);
|
||||||
|
|
||||||
|
behavior = ScriptableObject.CreateInstance<VRCAnimatorLayerControl>();
|
||||||
|
behavior.layer = index;
|
||||||
|
behavior.playable = VRC_AnimatorLayerControl.BlendableLayer.FX;
|
||||||
|
behavior.goalWeight = 1;
|
||||||
|
behavior.blendDuration = 0;
|
||||||
|
|
||||||
|
notmmd_behaviors.Add(behavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
state_notmmd.Behaviours = notmmd_behaviors.ToImmutable();
|
||||||
|
state_mmd.Behaviours = mmd_behaviors.ToImmutable();
|
||||||
|
|
||||||
|
return mmdControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsRelayLayer(string layerName)
|
||||||
|
{
|
||||||
|
return layerName == ControlLayerName || layerName == DummyLayerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Animation/MMDRelayPass.cs.meta
Normal file
3
Editor/Animation/MMDRelayPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 163fd3d0edea43d5969395079f561986
|
||||||
|
timeCreated: 1741745889
|
@ -1,401 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using nadena.dev.ndmf.util;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
#region
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This extension context tracks when objects are renamed, and updates animations accordingly.
|
|
||||||
/// Users of this context need to be aware that, when creating new curves (or otherwise introducing new motions,
|
|
||||||
/// use context.ObjectPath to obtain a suitable path for the target objects).
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class PathMappings
|
|
||||||
{
|
|
||||||
private AnimationDatabase _animationDatabase;
|
|
||||||
|
|
||||||
private Dictionary<GameObject, List<string>>
|
|
||||||
_objectToOriginalPaths = new Dictionary<GameObject, List<string>>();
|
|
||||||
|
|
||||||
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
|
|
||||||
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
|
|
||||||
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
|
|
||||||
private ImmutableDictionary<string, GameObject> _pathToObject = null;
|
|
||||||
|
|
||||||
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
|
|
||||||
{
|
|
||||||
_animationDatabase = animationDatabase;
|
|
||||||
_objectToOriginalPaths.Clear();
|
|
||||||
_transformLookthroughObjects.Clear();
|
|
||||||
ClearCache();
|
|
||||||
|
|
||||||
foreach (var xform in context.AvatarRootTransform.GetComponentsInChildren<Transform>(true))
|
|
||||||
{
|
|
||||||
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {xform.gameObject.AvatarRootPath()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearCache()
|
|
||||||
{
|
|
||||||
_originalPathToMappedPath = null;
|
|
||||||
_transformOriginalPathToMappedPath = null;
|
|
||||||
_pathToObject = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the "transform lookthrough" flag for an object. Any transform animations on this object will be
|
|
||||||
/// redirected to its parent. This is used in Modular Avatar as part of bone merging logic.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj"></param>
|
|
||||||
public void MarkTransformLookthrough(GameObject obj)
|
|
||||||
{
|
|
||||||
_transformLookthroughObjects.Add(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a path for use in dynamically generated animations for a given object. This can include objects not
|
|
||||||
/// present at the time of context activation; in this case, they will be assigned a randomly-generated internal
|
|
||||||
/// path and replaced during path remapping with the true path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string GetObjectIdentifier(GameObject obj)
|
|
||||||
{
|
|
||||||
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
|
|
||||||
{
|
|
||||||
return paths[0];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var internalPath = "_NewlyCreatedObject/" + GUID.Generate() + "/" + obj.AvatarRootPath();
|
|
||||||
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
|
|
||||||
return internalPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks an object as having been removed. Its paths will be remapped to its parent.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj"></param>
|
|
||||||
public void MarkRemoved(GameObject obj)
|
|
||||||
{
|
|
||||||
ClearCache();
|
|
||||||
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
|
|
||||||
{
|
|
||||||
var parent = obj.transform.parent.gameObject;
|
|
||||||
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
|
|
||||||
{
|
|
||||||
parentPaths.AddRange(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
_objectToOriginalPaths.Remove(obj);
|
|
||||||
_transformLookthroughObjects.Remove(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
|
|
||||||
/// by the new object. References originally to the new object will continue to point to the new object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="old"></param>
|
|
||||||
/// <param name="newObject"></param>
|
|
||||||
public void ReplaceObject(GameObject old, GameObject newObject)
|
|
||||||
{
|
|
||||||
ClearCache();
|
|
||||||
|
|
||||||
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
|
|
||||||
{
|
|
||||||
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
|
|
||||||
{
|
|
||||||
newObjectPaths = new List<string>();
|
|
||||||
_objectToOriginalPaths.Add(newObject, newObjectPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
newObjectPaths.AddRange(paths);
|
|
||||||
|
|
||||||
_objectToOriginalPaths.Remove(old);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (_transformLookthroughObjects.Contains(old))
|
|
||||||
{
|
|
||||||
_transformLookthroughObjects.Remove(old);
|
|
||||||
_transformLookthroughObjects.Add(newObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
|
|
||||||
bool transformLookup)
|
|
||||||
{
|
|
||||||
if (cache != null) return cache;
|
|
||||||
|
|
||||||
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
|
|
||||||
|
|
||||||
foreach (var kvp in _objectToOriginalPaths)
|
|
||||||
{
|
|
||||||
var obj = kvp.Key;
|
|
||||||
var paths = kvp.Value;
|
|
||||||
|
|
||||||
if (transformLookup)
|
|
||||||
{
|
|
||||||
while (_transformLookthroughObjects.Contains(obj))
|
|
||||||
{
|
|
||||||
obj = obj.transform.parent.gameObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPath = obj.AvatarRootPath();
|
|
||||||
foreach (var origPath in paths)
|
|
||||||
{
|
|
||||||
if (!dict.ContainsKey(origPath))
|
|
||||||
{
|
|
||||||
dict = dict.Add(origPath, newPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cache = dict;
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string MapPath(string path, bool isTransformMapping = false)
|
|
||||||
{
|
|
||||||
ImmutableDictionary<string, string> mappings;
|
|
||||||
|
|
||||||
if (isTransformMapping)
|
|
||||||
{
|
|
||||||
mappings = BuildMapping(ref _originalPathToMappedPath, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappings.TryGetValue(path, out var mappedPath))
|
|
||||||
{
|
|
||||||
return mappedPath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string MapPath(EditorCurveBinding binding)
|
|
||||||
{
|
|
||||||
if (binding.type == typeof(Animator) && binding.path == "")
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return MapPath(binding.path, binding.type == typeof(Transform));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AnimationClip ApplyMappingsToClip(AnimationClip originalClip,
|
|
||||||
Dictionary<AnimationClip, AnimationClip> clipCache)
|
|
||||||
{
|
|
||||||
if (originalClip == null) return null;
|
|
||||||
if (clipCache != null && clipCache.TryGetValue(originalClip, out var cachedClip)) return cachedClip;
|
|
||||||
|
|
||||||
if (originalClip.IsProxyAnimation()) return originalClip;
|
|
||||||
|
|
||||||
var curveBindings = AnimationUtility.GetCurveBindings(originalClip);
|
|
||||||
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(originalClip);
|
|
||||||
|
|
||||||
bool hasMapping = false;
|
|
||||||
foreach (var binding in curveBindings.Concat(objectBindings))
|
|
||||||
{
|
|
||||||
if (MapPath(binding) != binding.path)
|
|
||||||
{
|
|
||||||
hasMapping = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasMapping) return originalClip;
|
|
||||||
|
|
||||||
|
|
||||||
var newClip = new AnimationClip();
|
|
||||||
newClip.name = originalClip.name;
|
|
||||||
|
|
||||||
SerializedObject before = new SerializedObject(originalClip);
|
|
||||||
SerializedObject after = new SerializedObject(newClip);
|
|
||||||
|
|
||||||
var before_hqCurve = before.FindProperty("m_UseHighQualityCurve");
|
|
||||||
var after_hqCurve = after.FindProperty("m_UseHighQualityCurve");
|
|
||||||
|
|
||||||
after_hqCurve.boolValue = before_hqCurve.boolValue;
|
|
||||||
after.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
|
|
||||||
// TODO - should we use direct SerializedObject manipulation to avoid missing script issues?
|
|
||||||
foreach (var binding in curveBindings)
|
|
||||||
{
|
|
||||||
var newBinding = binding;
|
|
||||||
newBinding.path = MapPath(binding);
|
|
||||||
// https://github.com/bdunderscore/modular-avatar/issues/950
|
|
||||||
// It's reported that sometimes using SetObjectReferenceCurve right after SetCurve might cause the
|
|
||||||
// curves to be forgotten; use SetEditorCurve instead.
|
|
||||||
AnimationUtility.SetEditorCurve(newClip, newBinding,
|
|
||||||
AnimationUtility.GetEditorCurve(originalClip, binding));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var objBinding in objectBindings)
|
|
||||||
{
|
|
||||||
var newBinding = objBinding;
|
|
||||||
newBinding.path = MapPath(objBinding);
|
|
||||||
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
|
|
||||||
AnimationUtility.GetObjectReferenceCurve(originalClip, objBinding));
|
|
||||||
}
|
|
||||||
|
|
||||||
newClip.wrapMode = originalClip.wrapMode;
|
|
||||||
newClip.legacy = originalClip.legacy;
|
|
||||||
newClip.frameRate = originalClip.frameRate;
|
|
||||||
newClip.localBounds = originalClip.localBounds;
|
|
||||||
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(originalClip));
|
|
||||||
|
|
||||||
if (clipCache != null)
|
|
||||||
{
|
|
||||||
clipCache.Add(originalClip, newClip);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newClip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyMappingsToAvatarMask(AvatarMask mask)
|
|
||||||
{
|
|
||||||
if (mask == null) return;
|
|
||||||
|
|
||||||
var maskSo = new SerializedObject(mask);
|
|
||||||
|
|
||||||
var seenTransforms = new Dictionary<string, float>();
|
|
||||||
var transformOrder = new List<string>();
|
|
||||||
var m_Elements = maskSo.FindProperty("m_Elements");
|
|
||||||
var elementCount = m_Elements.arraySize;
|
|
||||||
|
|
||||||
for (var i = 0; i < elementCount; i++)
|
|
||||||
{
|
|
||||||
var element = m_Elements.GetArrayElementAtIndex(i);
|
|
||||||
var path = element.FindPropertyRelative("m_Path").stringValue;
|
|
||||||
var weight = element.FindPropertyRelative("m_Weight").floatValue;
|
|
||||||
|
|
||||||
path = MapPath(path);
|
|
||||||
|
|
||||||
// ensure all parent elements are present
|
|
||||||
EnsureParentsPresent(path);
|
|
||||||
|
|
||||||
if (!seenTransforms.ContainsKey(path)) transformOrder.Add(path);
|
|
||||||
seenTransforms[path] = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformOrder.Sort();
|
|
||||||
m_Elements.arraySize = transformOrder.Count;
|
|
||||||
|
|
||||||
for (var i = 0; i < transformOrder.Count; i++)
|
|
||||||
{
|
|
||||||
var element = m_Elements.GetArrayElementAtIndex(i);
|
|
||||||
var path = transformOrder[i];
|
|
||||||
|
|
||||||
element.FindPropertyRelative("m_Path").stringValue = path;
|
|
||||||
element.FindPropertyRelative("m_Weight").floatValue = seenTransforms[path];
|
|
||||||
}
|
|
||||||
|
|
||||||
maskSo.ApplyModifiedPropertiesWithoutUndo();
|
|
||||||
|
|
||||||
void EnsureParentsPresent(string path)
|
|
||||||
{
|
|
||||||
var nextSlash = -1;
|
|
||||||
|
|
||||||
while ((nextSlash = path.IndexOf('/', nextSlash + 1)) != -1)
|
|
||||||
{
|
|
||||||
var parentPath = path.Substring(0, nextSlash);
|
|
||||||
if (!seenTransforms.ContainsKey(parentPath))
|
|
||||||
{
|
|
||||||
seenTransforms[parentPath] = 0;
|
|
||||||
transformOrder.Add(parentPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void OnDeactivate(BuildContext context)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
var layers = context.AvatarDescriptor.baseAnimationLayers
|
|
||||||
.Concat(context.AvatarDescriptor.specialAnimationLayers);
|
|
||||||
|
|
||||||
foreach (var layer in layers)
|
|
||||||
{
|
|
||||||
ApplyMappingsToAvatarMask(layer.mask);
|
|
||||||
|
|
||||||
if (layer.animatorController is AnimatorController ac)
|
|
||||||
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
|
|
||||||
// AnimatorController so we can safely modify the controller in-place.
|
|
||||||
foreach (var acLayer in ac.layers)
|
|
||||||
ApplyMappingsToAvatarMask(acLayer.avatarMask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameObject PathToObject(string path)
|
|
||||||
{
|
|
||||||
if (_pathToObject == null)
|
|
||||||
{
|
|
||||||
var builder = ImmutableDictionary.CreateBuilder<string, GameObject>();
|
|
||||||
|
|
||||||
foreach (var kvp in _objectToOriginalPaths)
|
|
||||||
foreach (var p in kvp.Value)
|
|
||||||
builder[p] = kvp.Key;
|
|
||||||
|
|
||||||
_pathToObject = builder.ToImmutable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pathToObject.TryGetValue(path, out var obj))
|
|
||||||
{
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f60ee78d127fda546a84d5396edfc8b2
|
|
||||||
timeCreated: 1691237971
|
|
@ -1,147 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using Object = UnityEngine.Object;
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.animation
|
|
||||||
{
|
|
||||||
internal class ReadableProperty
|
|
||||||
{
|
|
||||||
private readonly BuildContext _context;
|
|
||||||
private readonly AnimationDatabase _animDB;
|
|
||||||
private readonly AnimationServicesContext _asc;
|
|
||||||
private readonly Dictionary<EditorCurveBinding, string> _alreadyBound = new();
|
|
||||||
private long _nextIndex;
|
|
||||||
|
|
||||||
public ReadableProperty(BuildContext context, AnimationDatabase animDB, AnimationServicesContext asc)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_animDB = animDB;
|
|
||||||
_asc = asc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<(EditorCurveBinding, string)> BoundProperties =>
|
|
||||||
_alreadyBound.Select(kv => (kv.Key, kv.Value));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an animator parameter which tracks the effective value of a property on a component. This only
|
|
||||||
/// tracks FX layer properties.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ecb"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string ForBinding(string path, Type componentType, string property)
|
|
||||||
{
|
|
||||||
var ecb = new EditorCurveBinding
|
|
||||||
{
|
|
||||||
path = path,
|
|
||||||
type = componentType,
|
|
||||||
propertyName = property
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_alreadyBound.TryGetValue(ecb, out var reader))
|
|
||||||
{
|
|
||||||
return reader;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastComponent = path.Split("/")[^1];
|
|
||||||
var emuPropName = $"__MA/ReadableProp/{lastComponent}/{componentType}/{property}#{_nextIndex++}";
|
|
||||||
|
|
||||||
float initialValue = 0;
|
|
||||||
var gameObject = _asc.PathMappings.PathToObject(path);
|
|
||||||
Object component = componentType == typeof(GameObject)
|
|
||||||
? gameObject
|
|
||||||
: gameObject?.GetComponent(componentType);
|
|
||||||
if (component != null)
|
|
||||||
{
|
|
||||||
var so = new SerializedObject(component);
|
|
||||||
var prop = so.FindProperty(property);
|
|
||||||
if (prop != null)
|
|
||||||
switch (prop.propertyType)
|
|
||||||
{
|
|
||||||
case SerializedPropertyType.Boolean:
|
|
||||||
initialValue = prop.boolValue ? 1 : 0;
|
|
||||||
break;
|
|
||||||
case SerializedPropertyType.Float:
|
|
||||||
initialValue = prop.floatValue;
|
|
||||||
break;
|
|
||||||
case SerializedPropertyType.Integer:
|
|
||||||
initialValue = prop.intValue;
|
|
||||||
break;
|
|
||||||
default: throw new NotImplementedException($"Property type {prop.type} not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_asc.AddPropertyDefinition(new AnimatorControllerParameter
|
|
||||||
{
|
|
||||||
defaultFloat = initialValue,
|
|
||||||
name = emuPropName,
|
|
||||||
type = AnimatorControllerParameterType.Float
|
|
||||||
});
|
|
||||||
|
|
||||||
BindProperty(ecb, emuPropName);
|
|
||||||
|
|
||||||
_alreadyBound[ecb] = emuPropName;
|
|
||||||
|
|
||||||
return emuPropName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BindProperty(EditorCurveBinding ecb, string propertyName)
|
|
||||||
{
|
|
||||||
var boundProp = new EditorCurveBinding
|
|
||||||
{
|
|
||||||
path = "",
|
|
||||||
type = typeof(Animator),
|
|
||||||
propertyName = propertyName
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var clip in _animDB.ClipsForPath(ecb.path)) ProcessAnyClip(clip);
|
|
||||||
|
|
||||||
void ProcessBlendTree(BlendTree blendTree)
|
|
||||||
{
|
|
||||||
foreach (var child in blendTree.children)
|
|
||||||
switch (child.motion)
|
|
||||||
{
|
|
||||||
case AnimationClip animationClip:
|
|
||||||
ProcessAnimationClip(animationClip);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BlendTree subBlendTree:
|
|
||||||
ProcessBlendTree(subBlendTree);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessAnimationClip(AnimationClip animationClip)
|
|
||||||
{
|
|
||||||
var curve = AnimationUtility.GetEditorCurve(animationClip, ecb);
|
|
||||||
if (curve == null) return;
|
|
||||||
|
|
||||||
AnimationUtility.SetEditorCurve(animationClip, boundProp, curve);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessAnyClip(AnimationDatabase.ClipHolder clip)
|
|
||||||
{
|
|
||||||
switch (clip.CurrentClip)
|
|
||||||
{
|
|
||||||
case AnimationClip animationClip:
|
|
||||||
ProcessAnimationClip(animationClip);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BlendTree blendTree:
|
|
||||||
ProcessBlendTree(blendTree);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ForActiveSelf(string path)
|
|
||||||
{
|
|
||||||
return ForBinding(path, typeof(GameObject), "m_IsActive");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1074339e2a59465ba585cb8cbbc4a88c
|
|
||||||
timeCreated: 1719195449
|
|
82
Editor/Animation/ReadablePropertyExtension.cs
Normal file
82
Editor/Animation/ReadablePropertyExtension.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.animation
|
||||||
|
{
|
||||||
|
[DependsOnContext(typeof(AnimatorServicesContext))]
|
||||||
|
internal class ReadablePropertyExtension : IExtensionContext
|
||||||
|
{
|
||||||
|
// This is a temporary hack for GameObjectDelayDisablePass
|
||||||
|
public class Retained
|
||||||
|
{
|
||||||
|
public Dictionary<EditorCurveBinding, string> proxyProps = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimatorServicesContext? _asc;
|
||||||
|
private Retained _retained = null!;
|
||||||
|
|
||||||
|
private AnimatorServicesContext asc =>
|
||||||
|
_asc ?? throw new InvalidOperationException("ActiveSelfProxyExtension is not active");
|
||||||
|
|
||||||
|
private Dictionary<EditorCurveBinding, string> proxyProps => _retained.proxyProps;
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public IEnumerable<(EditorCurveBinding, string)> ActiveProxyProps =>
|
||||||
|
proxyProps.Select(kvp => (kvp.Key, kvp.Value));
|
||||||
|
|
||||||
|
public string GetActiveSelfProxy(GameObject obj)
|
||||||
|
{
|
||||||
|
var path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj);
|
||||||
|
var ecb = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive");
|
||||||
|
|
||||||
|
if (proxyProps.TryGetValue(ecb, out var prop)) return prop;
|
||||||
|
|
||||||
|
prop = $"__MA/ActiveSelfProxy/{obj.name}##{index++}";
|
||||||
|
proxyProps[ecb] = prop;
|
||||||
|
|
||||||
|
// Add prop to all animators
|
||||||
|
foreach (var animator in asc.ControllerContext.GetAllControllers())
|
||||||
|
{
|
||||||
|
animator.Parameters = animator.Parameters.SetItem(
|
||||||
|
prop,
|
||||||
|
new AnimatorControllerParameter
|
||||||
|
{
|
||||||
|
name = prop,
|
||||||
|
type = AnimatorControllerParameterType.Float,
|
||||||
|
defaultFloat = obj.activeSelf ? 1 : 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnActivate(BuildContext context)
|
||||||
|
{
|
||||||
|
_asc = context.Extension<AnimatorServicesContext>();
|
||||||
|
_retained = context.GetState<Retained>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDeactivate(BuildContext context)
|
||||||
|
{
|
||||||
|
asc.AnimationIndex.EditClipsByBinding(proxyProps.Keys, clip =>
|
||||||
|
{
|
||||||
|
foreach (var b in clip.GetFloatCurveBindings().ToList())
|
||||||
|
{
|
||||||
|
if (proxyProps.TryGetValue(b, out var proxyProp))
|
||||||
|
{
|
||||||
|
var curve = clip.GetFloatCurve(b);
|
||||||
|
clip.SetFloatCurve("", typeof(Animator), proxyProp, curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Animation/ReadablePropertyExtension.cs.meta
Normal file
3
Editor/Animation/ReadablePropertyExtension.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 511cbc0373a2469192e0351e2222a203
|
||||||
|
timeCreated: 1732496091
|
@ -1,9 +1,11 @@
|
|||||||
#region
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
#region
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@ -20,41 +22,36 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
|
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
|
||||||
?? ImmutableDictionary<string, float>.Empty;
|
?? ImmutableDictionary<string, float>.Empty;
|
||||||
|
|
||||||
foreach (var layer in context.AvatarDescriptor.baseAnimationLayers
|
var asc = context.Extension<AnimatorServicesContext>();
|
||||||
.Concat(context.AvatarDescriptor.specialAnimationLayers))
|
|
||||||
{
|
|
||||||
if (layer.isDefault || layer.animatorController == null) continue;
|
|
||||||
|
|
||||||
// We should have converted anything that's not an AnimationController by now
|
foreach (var controller in asc.ControllerContext.GetAllControllers())
|
||||||
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;
|
||||||
}
|
foreach (var (name, parameter) in parameters)
|
||||||
|
|
||||||
var parameters = controller.parameters;
|
|
||||||
for (int i = 0; i < parameters.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (!values.TryGetValue(parameters[i].name, out var defaultValue)) continue;
|
if (!values.TryGetValue(name, out var defaultValue)) continue;
|
||||||
|
|
||||||
switch (parameters[i].type)
|
switch (parameter.type)
|
||||||
{
|
{
|
||||||
case AnimatorControllerParameterType.Bool:
|
case AnimatorControllerParameterType.Bool:
|
||||||
parameters[i].defaultBool = defaultValue > 0.5f;
|
parameter.defaultBool = defaultValue != 0.0f;
|
||||||
break;
|
break;
|
||||||
case AnimatorControllerParameterType.Int:
|
case AnimatorControllerParameterType.Int:
|
||||||
parameters[i].defaultInt = Mathf.RoundToInt(defaultValue);
|
parameter.defaultInt = Mathf.RoundToInt(defaultValue);
|
||||||
break;
|
break;
|
||||||
case AnimatorControllerParameterType.Float:
|
case AnimatorControllerParameterType.Float:
|
||||||
parameters[i].defaultFloat = defaultValue;
|
parameter.defaultFloat = defaultValue;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
continue; // unhandled type, e.g. trigger
|
continue; // unhandled type, e.g. trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parameters = parameters.SetItem(name, parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.parameters = parameters;
|
controller.Parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -1,12 +1,14 @@
|
|||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
|
||||||
using Object = UnityEngine.Object;
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
@ -17,11 +19,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
*/
|
*/
|
||||||
internal class BlendshapeSyncAnimationProcessor
|
internal class BlendshapeSyncAnimationProcessor
|
||||||
{
|
{
|
||||||
private BuildContext _context;
|
private readonly ndmf.BuildContext _context;
|
||||||
private Dictionary<Motion, Motion> _motionCache;
|
|
||||||
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
|
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
|
||||||
|
|
||||||
private struct SummaryBinding
|
internal BlendshapeSyncAnimationProcessor(ndmf.BuildContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SummaryBinding : IEquatable<SummaryBinding>
|
||||||
{
|
{
|
||||||
private const string PREFIX = "blendShape.";
|
private const string PREFIX = "blendShape.";
|
||||||
public string path;
|
public string path;
|
||||||
@ -33,71 +40,76 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
this.propertyName = PREFIX + blendShape;
|
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))
|
if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
|
||||||
{
|
{
|
||||||
return new SummaryBinding();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
|
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EditorCurveBinding ToEditorCurveBinding()
|
||||||
|
{
|
||||||
|
return EditorCurveBinding.FloatCurve(
|
||||||
|
path,
|
||||||
|
typeof(SkinnedMeshRenderer),
|
||||||
|
propertyName
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPreprocessAvatar(BuildContext context)
|
public bool Equals(SummaryBinding other)
|
||||||
{
|
{
|
||||||
_context = context;
|
return path == other.path && propertyName == other.propertyName;
|
||||||
var avatarGameObject = context.AvatarRootObject;
|
}
|
||||||
var animDb = _context.AnimationDatabase;
|
|
||||||
|
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;
|
||||||
|
|
||||||
var avatarDescriptor = context.AvatarDescriptor;
|
|
||||||
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
||||||
_motionCache = new Dictionary<Motion, Motion>();
|
|
||||||
|
|
||||||
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
|
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
|
||||||
if (components.Length == 0) return;
|
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)
|
foreach (var component in components)
|
||||||
{
|
{
|
||||||
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
|
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk and transform all clips
|
var clips = new HashSet<VirtualClip>();
|
||||||
animDb.ForeachClip(clip =>
|
foreach (var key in _bindingMappings.Keys)
|
||||||
{
|
{
|
||||||
if (clip.CurrentClip is AnimationClip anim)
|
var ecb = key.ToEditorCurveBinding();
|
||||||
{
|
clips.UnionWith(animDb.GetClipsForBinding(ecb));
|
||||||
BuildReport.ReportingObject(clip.CurrentClip,
|
}
|
||||||
() => { clip.CurrentClip = TransformMotion(anim); });
|
|
||||||
|
// Walk and transform all clips
|
||||||
|
foreach (var clip in clips)
|
||||||
|
{
|
||||||
|
ProcessClip(clip);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
|
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
|
||||||
{
|
{
|
||||||
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
|
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
|
||||||
|
|
||||||
|
if (targetObj == null) return;
|
||||||
|
|
||||||
foreach (var binding in component.Bindings)
|
foreach (var binding in component.Bindings)
|
||||||
{
|
{
|
||||||
var refObj = binding.ReferenceMesh.Get(component);
|
var refObj = binding.ReferenceMesh.Get(component);
|
||||||
@ -106,6 +118,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (refSmr == null) continue;
|
if (refSmr == null) continue;
|
||||||
|
|
||||||
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
|
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
|
||||||
|
if (refPath == null) continue;
|
||||||
|
|
||||||
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
|
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
|
||||||
|
|
||||||
@ -123,108 +136,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Motion TransformMotion(Motion motion)
|
private void ProcessClip(VirtualClip clip)
|
||||||
{
|
{
|
||||||
if (motion == null) return null;
|
foreach (var binding in clip.GetFloatCurveBindings().ToList())
|
||||||
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
|
|
||||||
|
|
||||||
switch (motion)
|
|
||||||
{
|
{
|
||||||
case AnimationClip clip:
|
var srcBinding = SummaryBinding.FromEditorBinding(binding);
|
||||||
{
|
if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clip == origClip)
|
var curve = clip.GetFloatCurve(binding);
|
||||||
{
|
|
||||||
clip = Object.Instantiate(clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var dst in dstBindings)
|
foreach (var dst in dstBindings)
|
||||||
{
|
{
|
||||||
clip.SetCurve(dst.path, typeof(SkinnedMeshRenderer), dst.propertyName,
|
clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
using System;
|
#if MA_VRCSDK3_AVATARS
|
||||||
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.Components;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
#endif
|
#endif
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
internal class BuildContext
|
internal class BuildContext
|
||||||
{
|
{
|
||||||
internal readonly nadena.dev.ndmf.BuildContext PluginBuildContext;
|
internal readonly ndmf.BuildContext PluginBuildContext;
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
|
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
|
||||||
@ -25,14 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal GameObject AvatarRootObject => PluginBuildContext.AvatarRootObject;
|
internal GameObject AvatarRootObject => PluginBuildContext.AvatarRootObject;
|
||||||
internal Transform AvatarRootTransform => PluginBuildContext.AvatarRootTransform;
|
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;
|
private bool SaveImmediate = false;
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
@ -44,13 +31,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
/// replace the source menu for the purposes of identifying any other MAMIs that might install to the same
|
/// replace the source menu for the purposes of identifying any other MAMIs that might install to the same
|
||||||
/// menu asset.
|
/// menu asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> PostProcessControls
|
internal readonly Dictionary<Object, Action<VRCExpressionsMenu.Control>> PostProcessControls = new();
|
||||||
= new Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>();
|
|
||||||
#endif
|
#endif
|
||||||
public static implicit operator BuildContext(ndmf.BuildContext ctx) =>
|
public static implicit operator BuildContext(ndmf.BuildContext ctx) =>
|
||||||
ctx.Extension<ModularAvatarContext>().BuildContext;
|
ctx.Extension<ModularAvatarContext>().BuildContext;
|
||||||
|
|
||||||
public BuildContext(nadena.dev.ndmf.BuildContext PluginBuildContext)
|
public BuildContext(ndmf.BuildContext PluginBuildContext)
|
||||||
{
|
{
|
||||||
this.PluginBuildContext = PluginBuildContext;
|
this.PluginBuildContext = PluginBuildContext;
|
||||||
}
|
}
|
||||||
@ -71,64 +57,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
|
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
|
||||||
|
|
||||||
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
|
PluginBuildContext.AssetSaver.SaveAsset(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AnimatorController CreateAnimator(AnimatorController toClone = null)
|
|
||||||
{
|
|
||||||
AnimatorController controller;
|
|
||||||
if (toClone != null)
|
|
||||||
{
|
|
||||||
controller = Object.Instantiate(toClone);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
controller = new AnimatorController();
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveAsset(controller);
|
|
||||||
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
|
|
||||||
{
|
|
||||||
if (controller == null) return null;
|
|
||||||
|
|
||||||
var merger = new AnimatorCombiner(PluginBuildContext, controller.name + " (clone)");
|
|
||||||
switch (controller)
|
|
||||||
{
|
|
||||||
case AnimatorController ac:
|
|
||||||
merger.AddController("", ac, null);
|
|
||||||
break;
|
|
||||||
case AnimatorOverrideController oac:
|
|
||||||
merger.AddOverrideController("", oac, null);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = merger.Finish();
|
|
||||||
|
|
||||||
ObjectRegistry.RegisterReplacedObject(controller, result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnimatorController ConvertAnimatorController(RuntimeAnimatorController anyController)
|
|
||||||
{
|
|
||||||
switch (anyController)
|
|
||||||
{
|
|
||||||
case AnimatorController ac:
|
|
||||||
return ac;
|
|
||||||
case AnimatorOverrideController aoc:
|
|
||||||
var merger = new AnimatorCombiner(PluginBuildContext, anyController.name + " (clone)");
|
|
||||||
merger.AddOverrideController("", aoc, null);
|
|
||||||
return merger.Finish();
|
|
||||||
default:
|
|
||||||
throw new Exception("Unknown RuntimeAnimatorContoller type " + anyController.GetType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||||
|
@ -6,7 +6,6 @@ using System.Collections.Immutable;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Experimental.Rendering;
|
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
@ -19,6 +18,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
internal static void FixupExpressionsMenu(BuildContext context)
|
internal static void FixupExpressionsMenu(BuildContext context)
|
||||||
{
|
{
|
||||||
|
if (!context.AvatarDescriptor) return;
|
||||||
|
|
||||||
context.AvatarDescriptor.customExpressions = true;
|
context.AvatarDescriptor.customExpressions = true;
|
||||||
|
|
||||||
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;
|
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;
|
||||||
@ -42,7 +43,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parameters = context.AvatarDescriptor.expressionParameters.parameters
|
var parameters = context.AvatarDescriptor.expressionParameters.parameters
|
||||||
?? new VRCExpressionParameters.Parameter[0];
|
?? Array.Empty<VRCExpressionParameters.Parameter>();
|
||||||
var parameterNames = parameters.Select(p => p.name).ToImmutableHashSet();
|
var parameterNames = parameters.Select(p => p.name).ToImmutableHashSet();
|
||||||
|
|
||||||
if (!context.PluginBuildContext.IsTemporaryAsset(expressionsMenu))
|
if (!context.PluginBuildContext.IsTemporaryAsset(expressionsMenu))
|
||||||
@ -91,6 +92,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
control.icon = newIcon;
|
control.icon = newIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (control.subMenu != null)
|
||||||
|
{
|
||||||
|
VisitMenu(control.subMenu);
|
||||||
|
}
|
||||||
|
|
||||||
if (control.labels != null)
|
if (control.labels != null)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < control.labels.Length; i++)
|
for (int i = 0; i < control.labels.Length; i++)
|
||||||
@ -113,11 +119,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if UNITY_ANDROID
|
internal static TextureFormat TargetFormat
|
||||||
private const TextureFormat TargetFormat = TextureFormat.ASTC_4x4;
|
{
|
||||||
#else
|
get
|
||||||
private const TextureFormat TargetFormat = TextureFormat.DXT5;
|
{
|
||||||
#endif
|
switch (EditorUserBuildSettings.activeBuildTarget)
|
||||||
|
{
|
||||||
|
case BuildTarget.StandaloneWindows64:
|
||||||
|
return TextureFormat.DXT5;
|
||||||
|
default:
|
||||||
|
return TextureFormat.ASTC_4x4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Texture2D MaybeScaleIcon(BuildContext context, Texture2D original)
|
private static Texture2D MaybeScaleIcon(BuildContext context, Texture2D original)
|
||||||
{
|
{
|
||||||
@ -126,10 +141,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRatio = Math.Min(256f / original.width, 256f / original.height);
|
var newRatio = Math.Min(1, Math.Min(256f / original.width, 256f / original.height));
|
||||||
var newWidth = Math.Min(256, Mathf.RoundToInt(original.width * newRatio));
|
var newWidth = Math.Min(256, Mathf.RoundToInt(original.width * newRatio));
|
||||||
var newHeight = Math.Min(256, Mathf.RoundToInt(original.height * 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);
|
var newTex = new Texture2D(newWidth, newHeight, TextureFormat.RGBA32, true);
|
||||||
context.SaveAsset(newTex);
|
context.SaveAsset(newTex);
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
|||||||
{
|
{
|
||||||
internal class PatchLoader
|
internal class PatchLoader
|
||||||
{
|
{
|
||||||
|
private const string HarmonyId = "nadena.dev.modular_avatar";
|
||||||
|
|
||||||
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
|
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
|
||||||
{
|
{
|
||||||
//HierarchyViewPatches.Patch,
|
//HierarchyViewPatches.Patch,
|
||||||
@ -19,7 +21,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
|||||||
[InitializeOnLoadMethod]
|
[InitializeOnLoadMethod]
|
||||||
static void ApplyPatches()
|
static void ApplyPatches()
|
||||||
{
|
{
|
||||||
var harmony = new Harmony("nadena.dev.modular_avatar");
|
var harmony = new Harmony(HarmonyId);
|
||||||
|
|
||||||
foreach (var patch in patches)
|
foreach (var patch in patches)
|
||||||
{
|
{
|
||||||
@ -33,7 +35,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(); };
|
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(HarmonyId); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
new[] {"LeftFoot", "Foot_Left", "Foot_L", "Ankle_L", "Foot.L.001", "Left ankle", "heel.L", "heel"},
|
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[] {"RightFoot", "Foot_Right", "Foot_R", "Ankle_R", "Foot.R.001", "Right ankle", "heel.R", "heel"},
|
||||||
new[] {"Spine", "spine01"},
|
new[] {"Spine", "spine01"},
|
||||||
new[] {"Chest", "Bust", "spine02"},
|
new[] {"Chest", "Bust", "spine02", "upper_chest"},
|
||||||
new[] {"Neck"},
|
new[] {"Neck"},
|
||||||
new[] {"Head"},
|
new[] {"Head"},
|
||||||
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
|
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
|
||||||
@ -232,6 +232,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
|
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)
|
internal static string NormalizeName(string name)
|
||||||
{
|
{
|
||||||
name = name.ToLowerInvariant();
|
name = name.ToLowerInvariant();
|
||||||
@ -243,6 +246,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
|
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
|
||||||
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
|
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
|
||||||
|
|
||||||
|
[InitializeOnLoadMethod]
|
||||||
|
private static void InsertboneNamePatternsToRuntime()
|
||||||
|
{
|
||||||
|
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
|
||||||
|
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
|
||||||
|
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
|
||||||
|
}
|
||||||
|
|
||||||
static HeuristicBoneMapper()
|
static HeuristicBoneMapper()
|
||||||
{
|
{
|
||||||
var pat_end_side = new Regex(@"[_\.]([LR])$");
|
var pat_end_side = new Regex(@"[_\.]([LR])$");
|
||||||
@ -306,7 +317,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
GameObject src,
|
GameObject src,
|
||||||
GameObject newParent,
|
GameObject newParent,
|
||||||
List<Transform> skipped = null,
|
List<Transform> skipped = null,
|
||||||
HashSet<Transform> unassigned = null
|
HashSet<Transform> unassigned = null,
|
||||||
|
Animator avatarAnimator = null,
|
||||||
|
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
|
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
|
||||||
@ -355,13 +368,55 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var childName = child.gameObject.name;
|
var childName = child.gameObject.name;
|
||||||
var targetObjectName = childName.Substring(config.prefix.Length,
|
var targetObjectName = childName.Substring(config.prefix.Length,
|
||||||
childName.Length - config.prefix.Length - config.suffix.Length);
|
childName.Length - config.prefix.Length - config.suffix.Length);
|
||||||
|
List<HumanBodyBones> bodyBones = null;
|
||||||
|
var isMapped = false;
|
||||||
|
|
||||||
if (!NameToBoneMap.TryGetValue(
|
if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
|
||||||
NormalizeName(targetObjectName), out var bodyBones))
|
{
|
||||||
|
if (avatarAnimator != null)
|
||||||
|
{
|
||||||
|
var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone);
|
||||||
|
if (avatarBone != null && unassigned.Contains(avatarBone))
|
||||||
|
{
|
||||||
|
mappings[child] = avatarBone;
|
||||||
|
unassigned.Remove(avatarBone);
|
||||||
|
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
|
||||||
|
isMapped = true;
|
||||||
|
} else {
|
||||||
|
bodyBones = new List<HumanBodyBones> { outfitHumanoidBone };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyBones = new List<HumanBodyBones>() { outfitHumanoidBone };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue(
|
||||||
|
NormalizeName(targetObjectName), out bodyBones))
|
||||||
{
|
{
|
||||||
continue;
|
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]))
|
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
|
||||||
{
|
{
|
||||||
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
||||||
@ -369,9 +424,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
mappings[child] = targetObject;
|
mappings[child] = targetObject;
|
||||||
unassigned.Remove(targetObject);
|
unassigned.Remove(targetObject);
|
||||||
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
||||||
|
isMapped = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null)
|
if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null)
|
||||||
{
|
{
|
||||||
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return mappings;
|
return mappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null)
|
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null, Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null, Animator avatarAnimator = null)
|
||||||
{
|
{
|
||||||
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
|
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
@ -399,7 +456,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
void Traverse(Transform src, Transform dst)
|
void Traverse(Transform src, Transform dst)
|
||||||
{
|
{
|
||||||
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped);
|
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
|
||||||
|
|
||||||
foreach (var pair in mappings)
|
foreach (var pair in mappings)
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal class AvatarObjectReferenceDrawer : PropertyDrawer
|
internal class AvatarObjectReferenceDrawer : PropertyDrawer
|
||||||
{
|
{
|
||||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||||
|
{
|
||||||
|
label = EditorGUI.BeginProperty(position, label, property);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (CustomGUI(position, property, label)) return;
|
if (CustomGUI(position, property, label)) return;
|
||||||
|
|
||||||
@ -24,6 +27,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EditorGUI.EndProperty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
|
private bool CustomGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||||
{
|
{
|
||||||
|
@ -95,7 +95,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var t = (ModularAvatarBoneProxy) targets[i];
|
var t = (ModularAvatarBoneProxy) targets[i];
|
||||||
Undo.RecordObjects(targets, "Set targets");
|
Undo.RecordObjects(targets, "Set targets");
|
||||||
var xform = ((TempObjRef) objRefs[i]).target;
|
var xform = ((TempObjRef) objRefs[i]).target;
|
||||||
if (RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
|
if (xform != null && RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
|
||||||
t.target = xform;
|
t.target = xform;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
Editor/Inspector/DragAndDropManipulator.cs
Normal file
106
Editor/Inspector/DragAndDropManipulator.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
internal abstract class DragAndDropManipulator<T> : PointerManipulator where T : Component, IHaveObjReferences
|
||||||
|
{
|
||||||
|
private const string DragActiveClassName = "drop-area--drag-active";
|
||||||
|
|
||||||
|
public T TargetComponent { get; set; }
|
||||||
|
|
||||||
|
protected virtual bool AllowKnownObjects => true;
|
||||||
|
|
||||||
|
private Transform _avatarRoot;
|
||||||
|
private GameObject[] _draggingObjects = Array.Empty<GameObject>();
|
||||||
|
|
||||||
|
public DragAndDropManipulator(VisualElement targetElement, T targetComponent)
|
||||||
|
{
|
||||||
|
target = targetElement;
|
||||||
|
TargetComponent = targetComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override void RegisterCallbacksOnTarget()
|
||||||
|
{
|
||||||
|
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
|
||||||
|
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
|
||||||
|
target.RegisterCallback<DragExitedEvent>(OnDragExited);
|
||||||
|
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
|
||||||
|
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override void UnregisterCallbacksFromTarget()
|
||||||
|
{
|
||||||
|
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
|
||||||
|
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
|
||||||
|
target.UnregisterCallback<DragExitedEvent>(OnDragExited);
|
||||||
|
target.UnregisterCallback<DragUpdatedEvent>(OnDragUpdated);
|
||||||
|
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragEnter(DragEnterEvent _)
|
||||||
|
{
|
||||||
|
if (TargetComponent == null) return;
|
||||||
|
|
||||||
|
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
|
||||||
|
if (_avatarRoot == null) return;
|
||||||
|
|
||||||
|
var knownObjects = TargetComponent.GetObjectReferences().Select(x => x.Get(TargetComponent)).ToHashSet();
|
||||||
|
_draggingObjects = DragAndDrop.objectReferences.OfType<GameObject>()
|
||||||
|
.Where(x => AllowKnownObjects || !knownObjects.Contains(x))
|
||||||
|
.Where(x => RuntimeUtil.FindAvatarTransformInParents(x.transform) == _avatarRoot)
|
||||||
|
.Where(FilterGameObject)
|
||||||
|
.ToArray();
|
||||||
|
if (_draggingObjects.Length == 0) return;
|
||||||
|
|
||||||
|
target.AddToClassList(DragActiveClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragLeave(DragLeaveEvent _)
|
||||||
|
{
|
||||||
|
_draggingObjects = Array.Empty<GameObject>();
|
||||||
|
target.RemoveFromClassList(DragActiveClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragExited(DragExitedEvent _)
|
||||||
|
{
|
||||||
|
_draggingObjects = Array.Empty<GameObject>();
|
||||||
|
target.RemoveFromClassList(DragActiveClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragUpdated(DragUpdatedEvent _)
|
||||||
|
{
|
||||||
|
if (TargetComponent == null) return;
|
||||||
|
if (_avatarRoot == null) return;
|
||||||
|
if (_draggingObjects.Length == 0) return;
|
||||||
|
|
||||||
|
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragPerform(DragPerformEvent _)
|
||||||
|
{
|
||||||
|
if (TargetComponent == null) return;
|
||||||
|
if (_avatarRoot == null) return;
|
||||||
|
if (_draggingObjects.Length == 0) return;
|
||||||
|
|
||||||
|
AddObjectReferences(_draggingObjects
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
var reference = new AvatarObjectReference();
|
||||||
|
reference.Set(x);
|
||||||
|
return reference;
|
||||||
|
})
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool FilterGameObject(GameObject obj)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void AddObjectReferences(AvatarObjectReference[] references);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: d86c7d257d78fff4d8fdf56e2954a5c9
|
guid: 528c660b56905844ea2f88bc73837e9f
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
@ -1,4 +1,5 @@
|
|||||||
using UnityEditor;
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
@ -19,12 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
var target = (ModularAvatarVisibleHeadAccessory) this.target;
|
var target = (ModularAvatarVisibleHeadAccessory) this.target;
|
||||||
|
|
||||||
|
|
||||||
#if UNITY_ANDROID
|
|
||||||
EditorGUILayout.HelpBox(Localization.S("fpvisible.quest"), MessageType.Warning);
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
if (_validation != null)
|
if (_validation != null)
|
||||||
{
|
{
|
||||||
var status = _validation.Validate(target);
|
var status = _validation.Validate(target);
|
||||||
@ -35,6 +30,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
case VisibleHeadAccessoryValidation.ReadyStatus.ParentMarked:
|
case VisibleHeadAccessoryValidation.ReadyStatus.ParentMarked:
|
||||||
EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info);
|
EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info);
|
||||||
break;
|
break;
|
||||||
|
case VisibleHeadAccessoryValidation.ReadyStatus.NotUnderHead:
|
||||||
|
EditorGUILayout.HelpBox(Localization.S("fpvisible.NotUnderHead"), MessageType.Warning);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var label = "fpvisible." + status;
|
var label = "fpvisible." + status;
|
||||||
@ -44,9 +42,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Localization.ShowLanguageUI();
|
Localization.ShowLanguageUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -18,7 +18,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
return (EditorStyles.label?.lineHeight ?? 0) * 3;
|
return (EditorStyles.label?.lineHeight ?? 0) * 3;
|
||||||
}
|
}
|
||||||
catch (NullReferenceException e)
|
catch (NullReferenceException)
|
||||||
{
|
{
|
||||||
// This can happen in early initialization...
|
// This can happen in early initialization...
|
||||||
return 0;
|
return 0;
|
||||||
|
30
Editor/Inspector/MMDModeEditor.cs
Normal file
30
Editor/Inspector/MMDModeEditor.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(ModularAvatarMMDLayerControl))]
|
||||||
|
internal class MMDModeEditor : MAEditorBase
|
||||||
|
{
|
||||||
|
private SerializedProperty m_p_DisableInMMDMode;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
m_p_DisableInMMDMode =
|
||||||
|
serializedObject.FindProperty(nameof(ModularAvatarMMDLayerControl.m_DisableInMMDMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInnerInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
LogoDisplay.DisplayLogo();
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(m_p_DisableInMMDMode, G("mmd_mode.disable_in_mmd_mode"));
|
||||||
|
|
||||||
|
ShowLanguageUI();
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/MMDModeEditor.cs.meta
Normal file
3
Editor/Inspector/MMDModeEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a1a682db3a3b491fa27980adfeeacffd
|
||||||
|
timeCreated: 1741836147
|
@ -16,6 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
[SerializeField] private StyleSheet uss;
|
[SerializeField] private StyleSheet uss;
|
||||||
[SerializeField] private VisualTreeAsset uxml;
|
[SerializeField] private VisualTreeAsset uxml;
|
||||||
|
|
||||||
|
private DragAndDropManipulator _dragAndDropManipulator;
|
||||||
|
|
||||||
protected override void OnInnerInspectorGUI()
|
protected override void OnInnerInspectorGUI()
|
||||||
{
|
{
|
||||||
@ -37,7 +38,44 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
listView.showBoundCollectionSize = false;
|
listView.showBoundCollectionSize = false;
|
||||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||||
|
|
||||||
|
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
|
||||||
|
|
||||||
return root;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -62,3 +62,13 @@
|
|||||||
#f-material {
|
#f-material {
|
||||||
flex-grow: 1;
|
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);
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.UIElements;
|
using UnityEditor.UIElements;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -148,7 +147,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
{
|
{
|
||||||
return targetObject?.GetComponent<Renderer>()?.sharedMaterials;
|
return targetObject?.GetComponent<Renderer>()?.sharedMaterials;
|
||||||
}
|
}
|
||||||
catch (MissingComponentException e)
|
catch (MissingComponentException)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,29 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
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()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
_installer = (ModularAvatarMenuInstaller) target;
|
_installer = (ModularAvatarMenuInstaller) target;
|
||||||
@ -215,74 +238,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var avatar = commonAvatar;
|
var avatar = commonAvatar;
|
||||||
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
|
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||||
{
|
{
|
||||||
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
OpenSelectMenu(avatar, installTo);
|
||||||
{
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +324,79 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
|
||||||
Localization.ShowLanguageUI();
|
ShowLanguageUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSelectMenu(VRCAvatarDescriptor avatar, SerializedProperty installTo)
|
||||||
|
{
|
||||||
|
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||||
|
{
|
||||||
|
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
|
||||||
|
|
||||||
|
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
|
||||||
|
{
|
||||||
|
DestroyInstallTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
|
||||||
|
{
|
||||||
|
// Menu, ContextCallback
|
||||||
|
menu = vt.Item1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu is ModularAvatarMenuItem item)
|
||||||
|
{
|
||||||
|
if (item.MenuSource == SubmenuSource.MenuAsset)
|
||||||
|
{
|
||||||
|
menu = item.Control.subMenu;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var menuParent = item.menuSource_otherObjectChildren != null
|
||||||
|
? item.menuSource_otherObjectChildren
|
||||||
|
: item.gameObject;
|
||||||
|
|
||||||
|
menu = new MenuNodesUnder(menuParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (menu is ModularAvatarMenuGroup group)
|
||||||
|
{
|
||||||
|
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
|
||||||
|
else menu = new MenuNodesUnder(group.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu is VRCExpressionsMenu expMenu)
|
||||||
|
{
|
||||||
|
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
|
||||||
|
else installTo.objectReferenceValue = expMenu;
|
||||||
|
}
|
||||||
|
else if (menu is RootMenu)
|
||||||
|
{
|
||||||
|
installTo.objectReferenceValue = null;
|
||||||
|
}
|
||||||
|
else if (menu is MenuNodesUnder nodesUnder)
|
||||||
|
{
|
||||||
|
installTo.objectReferenceValue = null;
|
||||||
|
|
||||||
|
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
|
||||||
|
{
|
||||||
|
var installer = (ModularAvatarMenuInstaller)target;
|
||||||
|
var child = new GameObject();
|
||||||
|
Undo.RegisterCreatedObjectUndo(child, "Set install target");
|
||||||
|
child.transform.SetParent(nodesUnder.root.transform, false);
|
||||||
|
child.name = installer.gameObject.name;
|
||||||
|
|
||||||
|
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||||
|
targetComponent.installer = installer;
|
||||||
|
|
||||||
|
EditorGUIUtility.PingObject(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
VirtualMenu.InvalidateCaches();
|
||||||
|
Repaint();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ObjectHierarchyOrder(Component arg)
|
private string ObjectHierarchyOrder(Component arg)
|
||||||
@ -415,6 +443,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
|
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
|
||||||
var menuRoot = new GameObject();
|
var menuRoot = new GameObject();
|
||||||
menuRoot.name = "Menu";
|
menuRoot.name = "Menu";
|
||||||
|
|
||||||
|
group.targetObject = menuRoot;
|
||||||
|
|
||||||
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
|
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
|
||||||
menuRoot.transform.SetParent(group.transform, false);
|
menuRoot.transform.SetParent(group.transform, false);
|
||||||
foreach (var control in menu.controls)
|
foreach (var control in menu.controls)
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using nadena.dev.modular_avatar.core.menu;
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
|
using nadena.dev.ndmf.preview;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
@ -21,8 +23,44 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
protected override string localizationPrefix => "submenu_source";
|
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
|
internal class MenuItemCoreGUI
|
||||||
{
|
{
|
||||||
|
private const string ImpliesRichText = "<";
|
||||||
|
|
||||||
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
|
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
|
||||||
private readonly GameObject _parameterReference;
|
private readonly GameObject _parameterReference;
|
||||||
private readonly Action _redraw;
|
private readonly Action _redraw;
|
||||||
@ -56,11 +94,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private readonly SerializedProperty _prop_isDefault;
|
private readonly SerializedProperty _prop_isDefault;
|
||||||
private readonly SerializedProperty _prop_automaticValue;
|
private readonly SerializedProperty _prop_automaticValue;
|
||||||
|
|
||||||
|
private readonly SerializedProperty _prop_label;
|
||||||
|
|
||||||
public bool AlwaysExpandContents = false;
|
public bool AlwaysExpandContents = false;
|
||||||
public bool ExpandContents = false;
|
public bool ExpandContents = false;
|
||||||
|
|
||||||
private readonly Dictionary<string, ProvidedParameter> _knownParameters = new();
|
private readonly Dictionary<string, ProvidedParameter> _knownParameters = new();
|
||||||
private bool _parameterSourceNotDetermined;
|
private bool _parameterSourceNotDetermined;
|
||||||
|
private bool _useLabel;
|
||||||
|
|
||||||
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
||||||
{
|
{
|
||||||
@ -108,6 +149,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
|
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
|
||||||
_prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue));
|
_prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue));
|
||||||
|
|
||||||
|
_prop_label = obj.FindProperty(nameof(ModularAvatarMenuItem.label));
|
||||||
|
|
||||||
_previewGUI = new MenuPreviewGUI(redraw);
|
_previewGUI = new MenuPreviewGUI(redraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,12 +177,17 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
Dictionary<string, ProvidedParameter> rootParameters = new();
|
Dictionary<string, ProvidedParameter> rootParameters = new();
|
||||||
|
|
||||||
foreach (var param in ParameterInfo.ForUI.GetParametersForObject(parentAvatar.gameObject)
|
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
|
||||||
.Where(p => p.Namespace == ParameterNamespace.Animator)
|
.Where(p => p.Namespace == ParameterNamespace.Animator)
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
|
||||||
|
{
|
||||||
rootParameters[param.EffectiveName] = param;
|
rootParameters[param.EffectiveName] = param;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var remaps = ParameterInfo.ForUI.GetParameterRemappingsAt(paramRef);
|
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
|
||||||
foreach (var remap in remaps)
|
foreach (var remap in remaps)
|
||||||
{
|
{
|
||||||
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
|
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
|
||||||
@ -219,12 +267,70 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
EditorGUILayout.BeginHorizontal();
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
|
||||||
EditorGUILayout.BeginVertical();
|
EditorGUILayout.BeginVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
|
||||||
|
if (_prop_label == null)
|
||||||
|
{
|
||||||
EditorGUI.BeginChangeCheck();
|
EditorGUI.BeginChangeCheck();
|
||||||
|
if (_obj != null && _obj.isEditingMultipleObjects)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||||
|
}
|
||||||
if (EditorGUI.EndChangeCheck())
|
if (EditorGUI.EndChangeCheck())
|
||||||
{
|
{
|
||||||
_name.serializedObject.ApplyModifiedProperties();
|
_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(_texture, G("menuitem.prop.icon"));
|
||||||
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
|
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
|
||||||
@ -265,10 +371,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
EditorGUILayout.BeginVertical();
|
EditorGUILayout.BeginVertical();
|
||||||
|
|
||||||
if (_type.hasMultipleDifferentValues) return;
|
if (_type.hasMultipleDifferentValues) return;
|
||||||
VRCExpressionsMenu.Control.ControlType type =
|
var controlTypeArray = Enum.GetValues(typeof(VRCExpressionsMenu.Control.ControlType));
|
||||||
(VRCExpressionsMenu.Control.ControlType) Enum
|
var index = Math.Clamp(_type.enumValueIndex, 0, controlTypeArray.Length - 1);
|
||||||
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
|
var type = (VRCExpressionsMenu.Control.ControlType)controlTypeArray.GetValue(index);
|
||||||
.GetValue(_type.enumValueIndex);
|
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
@ -481,7 +586,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
// But, we do want to see if _any_ are default.
|
// But, we do want to see if _any_ are default.
|
||||||
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
|
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
|
||||||
var mixedIsDefault = multipleSelections && anyIsDefault;
|
var mixedIsDefault = multipleSelections && anyIsDefault;
|
||||||
using (new EditorGUI.DisabledScope(multipleSelections || isDefaultByKnownParam != null))
|
|
||||||
|
var allAreAutoParams = !_parameterName.hasMultipleDifferentValues &&
|
||||||
|
string.IsNullOrWhiteSpace(_parameterName.stringValue);
|
||||||
|
|
||||||
|
using (new EditorGUI.DisabledScope((!allAreAutoParams && multipleSelections) ||
|
||||||
|
isDefaultByKnownParam != null))
|
||||||
{
|
{
|
||||||
EditorGUI.BeginChangeCheck();
|
EditorGUI.BeginChangeCheck();
|
||||||
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
|
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
|
||||||
@ -610,14 +720,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
|
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
|
||||||
if (myMenuItem == null) return null;
|
if (myMenuItem == null) return null;
|
||||||
|
|
||||||
|
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
|
||||||
|
if (avatarRoot == null) return null;
|
||||||
|
|
||||||
var myParameterName = myMenuItem.Control.parameter.name;
|
var myParameterName = myMenuItem.Control.parameter.name;
|
||||||
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
|
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
|
||||||
|
|
||||||
var myMappings = ParameterInfo.ForUI.GetParameterRemappingsAt(myMenuItem.gameObject);
|
var myMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(myMenuItem.gameObject);
|
||||||
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
|
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
|
||||||
myParameterName = myReplacement.ParameterName;
|
myParameterName = myReplacement.ParameterName;
|
||||||
|
|
||||||
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
|
|
||||||
var siblings = new List<ModularAvatarMenuItem>();
|
var siblings = new List<ModularAvatarMenuItem>();
|
||||||
|
|
||||||
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))
|
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))
|
||||||
@ -627,7 +739,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var otherParameterName = otherMenuItem.Control.parameter.name;
|
var otherParameterName = otherMenuItem.Control.parameter.name;
|
||||||
if (string.IsNullOrEmpty(otherParameterName)) continue;
|
if (string.IsNullOrEmpty(otherParameterName)) continue;
|
||||||
|
|
||||||
var otherMappings = ParameterInfo.ForUI.GetParameterRemappingsAt(otherMenuItem.gameObject);
|
var otherMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(otherMenuItem.gameObject);
|
||||||
if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName),
|
if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName),
|
||||||
out var otherReplacement))
|
out var otherReplacement))
|
||||||
otherParameterName = otherReplacement.ParameterName;
|
otherParameterName = otherReplacement.ParameterName;
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
using nadena.dev.modular_avatar.ui;
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using nadena.dev.modular_avatar.ui;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
@ -7,46 +11,194 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
internal static class ToggleCreatorShortcut
|
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)]
|
[MenuItem(UnityMenuItems.GameObject_CreateToggle, false, UnityMenuItems.GameObject_CreateToggleOrder)]
|
||||||
private static void CreateToggle()
|
private static void CreateToggle()
|
||||||
{
|
{
|
||||||
var selected = Selection.activeGameObject;
|
var selections = Selection.objects.OfType<GameObject>();
|
||||||
if (selected == null) return;
|
if (selections.Count() == 0) return;
|
||||||
|
|
||||||
|
foreach (var selected in selections)
|
||||||
|
{
|
||||||
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
|
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
|
||||||
if (avatarRoot == null) return;
|
if (avatarRoot == null) return;
|
||||||
|
|
||||||
bool createInstaller = true;
|
var parent = avatarRoot.gameObject;
|
||||||
Transform parent = avatarRoot;
|
var createInstaller = true;
|
||||||
|
|
||||||
try
|
if (TryGetChildrenSourceSubmenu(selected, out var _))
|
||||||
{
|
{
|
||||||
var selectedMenuItem = selected.GetComponent<ModularAvatarMenuItem>();
|
parent = selected;
|
||||||
if (selectedMenuItem?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
|
|
||||||
&& selectedMenuItem.MenuSource == SubmenuSource.Children
|
|
||||||
)
|
|
||||||
{
|
|
||||||
parent = selected.transform;
|
|
||||||
createInstaller = false;
|
createInstaller = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateToggleImpl(selected, parent, createInstaller:createInstaller);
|
||||||
}
|
}
|
||||||
catch (MissingComponentException e)
|
|
||||||
|
Selection.objects = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetChildrenSourceSubmenu(GameObject target, out ModularAvatarMenuItem subMenu)
|
||||||
|
{
|
||||||
|
subMenu = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mami = target.GetComponent<ModularAvatarMenuItem>();
|
||||||
|
if (mami?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
|
||||||
|
&& mami.MenuSource == SubmenuSource.Children
|
||||||
|
)
|
||||||
|
{
|
||||||
|
subMenu = mami;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (MissingComponentException)
|
||||||
{
|
{
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var toggle = new GameObject("New Toggle");
|
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>();
|
var objToggle = toggle.AddComponent<ModularAvatarObjectToggle>();
|
||||||
|
if (forSelection)
|
||||||
|
{
|
||||||
|
var path = RuntimeUtil.RelativePath(avatarRoot.gameObject, selected);
|
||||||
|
objToggle.Objects.Add(new ToggledObject
|
||||||
|
{
|
||||||
|
Object = new AvatarObjectReference(){ referencePath = path },
|
||||||
|
Active = !selected.activeSelf
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggle.transform.SetParent(parent, false);
|
|
||||||
|
toggle.transform.SetParent(parent.transform, false);
|
||||||
|
|
||||||
var mami = toggle.AddComponent<ModularAvatarMenuItem>();
|
var mami = toggle.AddComponent<ModularAvatarMenuItem>();
|
||||||
mami.InitSettings();
|
mami.InitSettings();
|
||||||
mami.Control = new VRCExpressionsMenu.Control
|
mami.Control = new VRCExpressionsMenu.Control
|
||||||
{
|
{
|
||||||
type = VRCExpressionsMenu.Control.ControlType.Toggle,
|
type = VRCExpressionsMenu.Control.ControlType.Toggle,
|
||||||
name = "New Toggle",
|
name = name,
|
||||||
value = 1,
|
value = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,3 +215,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -11,6 +11,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
protected override string localizationPrefix => "path_mode";
|
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))]
|
[CustomEditor(typeof(ModularAvatarMergeAnimator))]
|
||||||
class MergeAnimationEditor : MAEditorBase
|
class MergeAnimationEditor : MAEditorBase
|
||||||
{
|
{
|
||||||
@ -20,7 +26,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
prop_pathMode,
|
prop_pathMode,
|
||||||
prop_matchAvatarWriteDefaults,
|
prop_matchAvatarWriteDefaults,
|
||||||
prop_relativePathRoot,
|
prop_relativePathRoot,
|
||||||
prop_layerPriority;
|
prop_layerPriority,
|
||||||
|
prop_mergeMode;
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
@ -34,6 +41,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
prop_relativePathRoot =
|
prop_relativePathRoot =
|
||||||
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.relativePathRoot));
|
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.relativePathRoot));
|
||||||
prop_layerPriority = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.layerPriority));
|
prop_layerPriority = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.layerPriority));
|
||||||
|
prop_mergeMode = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.mergeAnimatorMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInnerInspectorGUI()
|
protected override void OnInnerInspectorGUI()
|
||||||
@ -47,8 +55,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
||||||
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
|
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
|
||||||
EditorGUILayout.PropertyField(prop_layerPriority, G("merge_animator.layer_priority"));
|
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,
|
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
|
||||||
G("merge_animator.match_avatar_write_defaults"));
|
G("merge_animator.match_avatar_write_defaults"));
|
||||||
|
}
|
||||||
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool posResetOptionFoldout = false;
|
private bool posResetOptionFoldout = false;
|
||||||
|
private bool posReset_convertATPose = true;
|
||||||
private bool posReset_adjustRotation = false;
|
private bool posReset_adjustRotation = false;
|
||||||
private bool posReset_adjustScale = false;
|
private bool posReset_adjustScale = false;
|
||||||
private bool posReset_heuristicRootScale = true;
|
private bool posReset_heuristicRootScale = true;
|
||||||
@ -99,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
|
||||||
if (target.mergeTargetObject != null && priorMergeTarget == null
|
if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject
|
||||||
&& string.IsNullOrEmpty(target.prefix)
|
&& string.IsNullOrEmpty(target.prefix)
|
||||||
&& string.IsNullOrEmpty(target.suffix))
|
&& string.IsNullOrEmpty(target.suffix))
|
||||||
{
|
{
|
||||||
@ -114,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
if (GUILayout.Button(G("merge_armature.adjust_names")))
|
if (GUILayout.Button(G("merge_armature.adjust_names")))
|
||||||
{
|
{
|
||||||
HeuristicBoneMapper.RenameBonesByHeuristic(target);
|
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(target.mergeTarget.Get(target).transform);
|
||||||
|
var avatarAnimator = avatarRoot != null ? avatarRoot.GetComponent<Animator>() : null;
|
||||||
|
|
||||||
|
// Search Outfit Root Animator
|
||||||
|
var outfitRoot = ((ModularAvatarMergeArmature)serializedObject.targetObject).transform;
|
||||||
|
Animator outfitAnimator = null;
|
||||||
|
while (outfitRoot != null)
|
||||||
|
{
|
||||||
|
if (outfitRoot == avatarRoot)
|
||||||
|
{
|
||||||
|
outfitAnimator = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outfitAnimator = outfitRoot.GetComponent<Animator>();
|
||||||
|
if (outfitAnimator != null && outfitAnimator.isHuman) break;
|
||||||
|
outfitAnimator = null;
|
||||||
|
outfitRoot = outfitRoot.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outfitHumanoidBones = SetupOutfit.GetOutfitHumanoidBones(outfitRoot, outfitAnimator);
|
||||||
|
HeuristicBoneMapper.RenameBonesByHeuristic(target, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,14 +155,17 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
MessageType.Info
|
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(
|
posReset_adjustRotation = EditorGUILayout.ToggleLeft(
|
||||||
G("merge_armature.reset_pos.adjust_rotation"),
|
G("merge_armature.reset_pos.adjust_rotation"),
|
||||||
posReset_adjustRotation);
|
posReset_adjustRotation);
|
||||||
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
|
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
|
||||||
posReset_adjustScale);
|
posReset_adjustScale);
|
||||||
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
|
|
||||||
G("merge_armature.reset_pos.heuristic_scale"),
|
|
||||||
posReset_heuristicRootScale);
|
|
||||||
|
|
||||||
if (GUILayout.Button(G("merge_armature.reset_pos.execute")))
|
if (GUILayout.Button(G("merge_armature.reset_pos.execute")))
|
||||||
{
|
{
|
||||||
@ -188,6 +212,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (posReset_convertATPose)
|
||||||
|
{
|
||||||
|
SetupOutfit.FixAPose(RuntimeUtil.FindAvatarTransformInParents(mergeTarget.transform).gameObject, mama.transform, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (posReset_heuristicRootScale && !suppressRootScale)
|
if (posReset_heuristicRootScale && !suppressRootScale)
|
||||||
{
|
{
|
||||||
AdjustRootScale();
|
AdjustRootScale();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEngine;
|
||||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
@ -15,7 +15,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
_blendTree = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.BlendTree));
|
_blendTree = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.BlendTree));
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.PathMode));
|
_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.PathMode));
|
||||||
_relativePathRoot = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.RelativePathRoot));
|
_relativePathRoot = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.RelativePathRoot));
|
||||||
}
|
}
|
||||||
@ -24,7 +26,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
serializedObject.Update();
|
serializedObject.Update();
|
||||||
|
|
||||||
EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), G("merge_blend_tree.blend_tree"));
|
EditorGUILayout.ObjectField(_blendTree, typeof(Motion), G("merge_blend_tree.motion"));
|
||||||
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
|
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
|
||||||
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.core.ArmatureAwase;
|
|
||||||
using nadena.dev.ndmf.preview;
|
using nadena.dev.ndmf.preview;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -61,7 +60,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var grouped = ctx.Observe(target,
|
var grouped = ctx.Observe(target,
|
||||||
t => (t.GroupedBones ?? Array.Empty<GameObject>())
|
t => (t.GroupedBones ?? Array.Empty<GameObject>())
|
||||||
.Select(obj => obj.transform)
|
.Select(obj => obj.transform)
|
||||||
.ToHashSet(new ObjectIdentityComparer<Transform>()),
|
.ToHashSet(),
|
||||||
(x, y) => x.SetEquals(y)
|
(x, y) => x.SetEquals(y)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -35,14 +35,12 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
ROSimulatorButton.BindRefObject(root, target);
|
ROSimulatorButton.BindRefObject(root, target);
|
||||||
|
|
||||||
var listView = root.Q<ListView>("Shapes");
|
var listView = root.Q<ListView>("Shapes");
|
||||||
_dragAndDropManipulator = new DragAndDropManipulator(listView)
|
|
||||||
{
|
|
||||||
TargetComponent = target as ModularAvatarObjectToggle
|
|
||||||
};
|
|
||||||
|
|
||||||
listView.showBoundCollectionSize = false;
|
listView.showBoundCollectionSize = false;
|
||||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||||
|
|
||||||
|
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,92 +50,26 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
|
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DragAndDropManipulator : PointerManipulator
|
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
|
||||||
{
|
{
|
||||||
public ModularAvatarObjectToggle TargetComponent;
|
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
|
||||||
private GameObject[] _nowDragging = Array.Empty<GameObject>();
|
: base(targetElement, targetComponent) { }
|
||||||
private Transform _avatarRoot;
|
|
||||||
|
|
||||||
private readonly VisualElement _parentElem;
|
protected override bool AllowKnownObjects => false;
|
||||||
|
|
||||||
public DragAndDropManipulator(VisualElement target)
|
protected override void AddObjectReferences(AvatarObjectReference[] references)
|
||||||
{
|
{
|
||||||
this.target = target;
|
|
||||||
_parentElem = target.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void RegisterCallbacksOnTarget()
|
|
||||||
{
|
|
||||||
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
|
|
||||||
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
|
|
||||||
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
|
|
||||||
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UnregisterCallbacksFromTarget()
|
|
||||||
{
|
|
||||||
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
|
|
||||||
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
|
|
||||||
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
|
|
||||||
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void OnDragEnter(DragEnterEvent evt)
|
|
||||||
{
|
|
||||||
if (TargetComponent == null) return;
|
|
||||||
|
|
||||||
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
|
|
||||||
if (_avatarRoot == null) return;
|
|
||||||
|
|
||||||
_nowDragging = DragAndDrop.objectReferences.OfType<GameObject>()
|
|
||||||
.Where(o => RuntimeUtil.FindAvatarTransformInParents(o.transform) == _avatarRoot)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (_nowDragging.Length > 0)
|
|
||||||
{
|
|
||||||
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
|
|
||||||
|
|
||||||
_parentElem.AddToClassList("drop-area--drag-active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragUpdate(DragUpdatedEvent _)
|
|
||||||
{
|
|
||||||
if (_nowDragging.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragLeave(DragLeaveEvent evt)
|
|
||||||
{
|
|
||||||
_nowDragging = Array.Empty<GameObject>();
|
|
||||||
_parentElem.RemoveFromClassList("drop-area--drag-active");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragPerform(DragPerformEvent evt)
|
|
||||||
{
|
|
||||||
if (_nowDragging.Length > 0 && TargetComponent != null && _avatarRoot != null)
|
|
||||||
{
|
|
||||||
var knownObjs = TargetComponent.Objects.Select(o => o.Object.Get(TargetComponent)).ToHashSet();
|
|
||||||
|
|
||||||
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
|
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
|
||||||
foreach (var obj in _nowDragging)
|
|
||||||
|
foreach (var reference in references)
|
||||||
{
|
{
|
||||||
if (knownObjs.Contains(obj)) continue;
|
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
|
||||||
|
|
||||||
var aor = new AvatarObjectReference();
|
|
||||||
aor.Set(obj);
|
|
||||||
|
|
||||||
var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf };
|
|
||||||
TargetComponent.Objects.Add(toggledObject);
|
TargetComponent.Objects.Add(toggledObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorUtility.SetDirty(TargetComponent);
|
EditorUtility.SetDirty(TargetComponent);
|
||||||
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
_nowDragging = Array.Empty<GameObject>();
|
|
||||||
_parentElem.RemoveFromClassList("drop-area--drag-active");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,14 +39,24 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#f-active {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#f-object {
|
#f-object {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-area--drag-active > ListView ScrollView {
|
#f-active {
|
||||||
background-color: rgba(0, 255, 255, 0.1);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
|
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
|
||||||
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
|
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)
|
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||||
{
|
{
|
||||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
|
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
|
||||||
@ -24,6 +27,21 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
uxml.styleSheets.Add(uss);
|
uxml.styleSheets.Add(uss);
|
||||||
uxml.BindProperty(property);
|
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;
|
return uxml;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||||
<ui:VisualElement class="horizontal">
|
<ui:VisualElement class="horizontal">
|
||||||
<ed:PropertyField name="f-active" binding-path="Active" label=""/>
|
|
||||||
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
|
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
|
||||||
|
<ui:Toggle name="f-active" binding-path="Active" label=""/>
|
||||||
|
<ui:DropdownField name="f-active-dropdown"/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</UXML>
|
</UXML>
|
||||||
|
@ -6,7 +6,10 @@ using UnityEditor;
|
|||||||
using UnityEditor.UIElements;
|
using UnityEditor.UIElements;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
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
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
@ -35,6 +38,37 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
listView.showBoundCollectionSize = false;
|
listView.showBoundCollectionSize = false;
|
||||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
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");
|
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
|
||||||
|
|
||||||
@ -129,9 +163,68 @@ 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;
|
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()
|
private void DetectParameters()
|
||||||
{
|
{
|
||||||
var known = new HashSet<string>();
|
var known = new HashSet<string>();
|
||||||
|
@ -20,6 +20,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private readonly DropdownField _boolField;
|
private readonly DropdownField _boolField;
|
||||||
|
|
||||||
private ParameterSyncType _syncType;
|
private ParameterSyncType _syncType;
|
||||||
|
private bool _hasInitialBinding;
|
||||||
|
|
||||||
public DefaultValueField()
|
public DefaultValueField()
|
||||||
{
|
{
|
||||||
@ -57,28 +58,39 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
_numberField.style.display = DisplayStyle.Flex;
|
_numberField.style.display = DisplayStyle.Flex;
|
||||||
_boolField.style.display = DisplayStyle.None;
|
_boolField.style.display = DisplayStyle.None;
|
||||||
OnUpdateNumberValue(_numberField.value);
|
OnUpdateNumberValue(_numberField.value, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_numberField.style.display = DisplayStyle.None;
|
_numberField.style.display = DisplayStyle.None;
|
||||||
_boolField.style.display = DisplayStyle.Flex;
|
_boolField.style.display = DisplayStyle.Flex;
|
||||||
OnUpdateBoolValue(_boolField.value);
|
OnUpdateBoolValue(_boolField.value, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdateNumberValue(string value)
|
private void OnUpdateNumberValue(string value, bool implicitUpdate = false)
|
||||||
{
|
{
|
||||||
|
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
|
||||||
|
// In this case, suppress the update to avoid losing data.
|
||||||
|
if (implicitUpdate && !_hasInitialBinding) return;
|
||||||
|
|
||||||
|
var theValue = _defaultValueField.value;
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
if (!implicitUpdate)
|
||||||
{
|
{
|
||||||
_defaultValueField.value = 0;
|
_defaultValueField.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
theValue = _defaultValueField.value;
|
||||||
|
|
||||||
_hasExplicitDefaultValueField.value = false;
|
_hasExplicitDefaultValueField.value = false;
|
||||||
}
|
}
|
||||||
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
||||||
&& !float.IsNaN(parsed)
|
&& !float.IsNaN(parsed)
|
||||||
&& !float.IsInfinity(parsed))
|
&& !float.IsInfinity(parsed))
|
||||||
{
|
{
|
||||||
_defaultValueField.value = _syncType switch
|
theValue = _defaultValueField.value = _syncType switch
|
||||||
{
|
{
|
||||||
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
|
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
|
||||||
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
|
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
|
||||||
@ -88,11 +100,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_hasExplicitDefaultValueField.value = true;
|
_hasExplicitDefaultValueField.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value);
|
UpdateVisibleField(theValue, _hasExplicitDefaultValueField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdateBoolValue(string value)
|
private void OnUpdateBoolValue(string value, bool implicitUpdate = false)
|
||||||
{
|
{
|
||||||
|
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
|
||||||
|
// In this case, suppress the update to avoid losing data.
|
||||||
|
if (implicitUpdate && !_hasInitialBinding) return;
|
||||||
|
|
||||||
_defaultValueField.value = value == V_True ? 1 : 0;
|
_defaultValueField.value = value == V_True ? 1 : 0;
|
||||||
_hasExplicitDefaultValueField.value = value != V_None;
|
_hasExplicitDefaultValueField.value = value != V_None;
|
||||||
|
|
||||||
@ -101,6 +117,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private void UpdateVisibleField(float value, bool hasExplicitValue)
|
private void UpdateVisibleField(float value, bool hasExplicitValue)
|
||||||
{
|
{
|
||||||
|
_hasInitialBinding = true;
|
||||||
|
|
||||||
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
|
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
|
||||||
{
|
{
|
||||||
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));
|
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
using Toggle = UnityEngine.UIElements.Toggle;
|
using Toggle = UnityEngine.UIElements.Toggle;
|
||||||
|
|
||||||
@ -81,6 +82,16 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
|
|||||||
|
|
||||||
updateRemapToPlaceholder();
|
updateRemapToPlaceholder();
|
||||||
|
|
||||||
|
foreach (var elem in root.Query<TextElement>().Build())
|
||||||
|
{
|
||||||
|
// Prevent delete keypresses from bubbling up if we're in a text field
|
||||||
|
elem.RegisterCallback<KeyDownEvent>(evt =>
|
||||||
|
{
|
||||||
|
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
|
||||||
|
evt.StopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#ListViewContainer {
|
#ListViewContainer {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal {
|
.horizontal {
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
show-border="true"
|
show-border="true"
|
||||||
show-foldout-header="false"
|
show-foldout-header="false"
|
||||||
name="Parameters"
|
name="Parameters"
|
||||||
item-height="100"
|
|
||||||
binding-path="parameters"
|
binding-path="parameters"
|
||||||
style="flex-grow: 1;"
|
style="flex-grow: 1;"
|
||||||
/>
|
/>
|
||||||
@ -33,5 +32,7 @@
|
|||||||
/>
|
/>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
|
|
||||||
|
<editor:ObjectField name="p_import" label="merge_parameter.ui.importFromAsset" class="ndmf-tr"/>
|
||||||
|
|
||||||
<ma:LanguageSwitcherElement/>
|
<ma:LanguageSwitcherElement/>
|
||||||
</UXML>
|
</UXML>
|
39
Editor/Inspector/RemoveVertexColorEditor.cs
Normal file
39
Editor/Inspector/RemoveVertexColorEditor.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using UnityEditor;
|
||||||
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomPropertyDrawer(typeof(ModularAvatarRemoveVertexColor.RemoveMode))]
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
|
internal class RVCModeDrawer : EnumDrawer<ModularAvatarRemoveVertexColor.RemoveMode>
|
||||||
|
{
|
||||||
|
protected override string localizationPrefix => "remove-vertex-color.mode";
|
||||||
|
}
|
||||||
|
|
||||||
|
[CustomEditor(typeof(ModularAvatarRemoveVertexColor))]
|
||||||
|
internal class RemoveVertexColorEditor : MAEditorBase
|
||||||
|
{
|
||||||
|
private SerializedProperty _p_mode;
|
||||||
|
|
||||||
|
protected void OnEnable()
|
||||||
|
{
|
||||||
|
_p_mode = serializedObject.FindProperty(nameof(ModularAvatarRemoveVertexColor.Mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInnerInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
EditorGUILayout.PropertyField(_p_mode, G("remove-vertex-color.mode"));
|
||||||
|
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
{
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowLanguageUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/RemoveVertexColorEditor.cs.meta
Normal file
3
Editor/Inspector/RemoveVertexColorEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bfcaf601e9f94ba2900e66d66f469037
|
||||||
|
timeCreated: 1733085477
|
@ -64,7 +64,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
.Select(x => mesh.GetBlendShapeName(x))
|
.Select(x => mesh.GetBlendShapeName(x))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
catch (MissingComponentException e)
|
catch (MissingComponentException)
|
||||||
{
|
{
|
||||||
shapeNames = null;
|
shapeNames = null;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
[SerializeField] private StyleSheet uss;
|
[SerializeField] private StyleSheet uss;
|
||||||
[SerializeField] private VisualTreeAsset uxml;
|
[SerializeField] private VisualTreeAsset uxml;
|
||||||
|
|
||||||
|
private DragAndDropManipulator _dragAndDropManipulator;
|
||||||
private BlendshapeSelectWindow _window;
|
private BlendshapeSelectWindow _window;
|
||||||
|
|
||||||
protected override void OnInnerInspectorGUI()
|
protected override void OnInnerInspectorGUI()
|
||||||
@ -41,6 +42,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
listView.showBoundCollectionSize = false;
|
listView.showBoundCollectionSize = false;
|
||||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||||
|
|
||||||
|
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarShapeChanger);
|
||||||
|
|
||||||
// The Add button callback isn't exposed publicly for some reason...
|
// The Add button callback isn't exposed publicly for some reason...
|
||||||
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
|
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
|
||||||
var addButton = (Button)field_addButton.GetValue(listView);
|
var addButton = (Button)field_addButton.GetValue(listView);
|
||||||
@ -50,6 +53,41 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (_dragAndDropManipulator != null)
|
||||||
|
_dragAndDropManipulator.TargetComponent = target as ModularAvatarShapeChanger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarShapeChanger>
|
||||||
|
{
|
||||||
|
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarShapeChanger targetComponent)
|
||||||
|
: base(targetElement, targetComponent) { }
|
||||||
|
|
||||||
|
protected override bool FilterGameObject(GameObject obj)
|
||||||
|
{
|
||||||
|
if (obj.TryGetComponent<SkinnedMeshRenderer>(out var smr))
|
||||||
|
{
|
||||||
|
return smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddObjectReferences(AvatarObjectReference[] references)
|
||||||
|
{
|
||||||
|
Undo.RecordObject(TargetComponent, "Add Changed Shapes");
|
||||||
|
|
||||||
|
foreach (var reference in references)
|
||||||
|
{
|
||||||
|
var changedShape = new ChangedShape { Object = reference, ShapeName = string.Empty };
|
||||||
|
TargetComponent.Shapes.Add(changedShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorUtility.SetDirty(TargetComponent);
|
||||||
|
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
if (_window != null) DestroyImmediate(_window);
|
if (_window != null) DestroyImmediate(_window);
|
||||||
|
@ -68,3 +68,13 @@
|
|||||||
.change-type-delete #f-value-delete {
|
.change-type-delete #f-value-delete {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop-area--drag-active {
|
||||||
|
background-color: rgba(0, 127, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-area--drag-active .unity-scroll-view,
|
||||||
|
.drop-area--drag-active .unity-list-view__footer,
|
||||||
|
.drop-area--drag-active .unity-list-view__reorderable-item {
|
||||||
|
background-color: rgba(0, 0, 0, 0.0);
|
||||||
|
}
|
||||||
|
101
Editor/Inspector/SyncParameterSequenceEditor.cs
Normal file
101
Editor/Inspector/SyncParameterSequenceEditor.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(ModularAvatarSyncParameterSequence))]
|
||||||
|
[CanEditMultipleObjects]
|
||||||
|
public class SyncParameterSequenceEditor : MAEditorBase
|
||||||
|
{
|
||||||
|
private SerializedProperty _p_platform;
|
||||||
|
private SerializedProperty _p_parameters;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_p_platform = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.PrimaryPlatform));
|
||||||
|
_p_parameters = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.Parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInnerInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
|
||||||
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
var disable = false;
|
||||||
|
#else
|
||||||
|
bool disable = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||||
|
if (disable)
|
||||||
|
// ReSharper disable HeuristicUnreachableCode
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(S("general.vrcsdk-required"), MessageType.Warning);
|
||||||
|
}
|
||||||
|
// ReSharper restore HeuristicUnreachableCode
|
||||||
|
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||||
|
using (new EditorGUI.DisabledGroupScope(disable))
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(_p_platform, G("sync-param-sequence.platform"));
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
|
||||||
|
var label = G("sync-param-sequence.parameters");
|
||||||
|
var sizeCalc = EditorStyles.objectField.CalcSize(label);
|
||||||
|
EditorGUILayout.PropertyField(_p_parameters, label);
|
||||||
|
|
||||||
|
if (GUILayout.Button(G("sync-param-sequence.create-asset"),
|
||||||
|
GUILayout.ExpandWidth(false),
|
||||||
|
GUILayout.Height(sizeCalc.y)
|
||||||
|
))
|
||||||
|
{
|
||||||
|
CreateParameterAsset();
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
{
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowLanguageUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateParameterAsset()
|
||||||
|
{
|
||||||
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
Transform avatarRoot = null;
|
||||||
|
if (targets.Length == 1)
|
||||||
|
{
|
||||||
|
avatarRoot =
|
||||||
|
RuntimeUtil.FindAvatarTransformInParents(((ModularAvatarSyncParameterSequence)target).transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
var assetName = "Avatar";
|
||||||
|
if (avatarRoot != null) assetName = avatarRoot.gameObject.name;
|
||||||
|
|
||||||
|
assetName += " SyncedParams";
|
||||||
|
|
||||||
|
var file = EditorUtility.SaveFilePanelInProject("Create new parameter asset", assetName, "asset",
|
||||||
|
"Create a new parameter asset");
|
||||||
|
|
||||||
|
var obj = CreateInstance<VRCExpressionParameters>();
|
||||||
|
obj.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
|
||||||
|
obj.isEmpty = true;
|
||||||
|
|
||||||
|
AssetDatabase.CreateAsset(obj, file);
|
||||||
|
Undo.RegisterCreatedObjectUndo(obj, "Create parameter asset");
|
||||||
|
|
||||||
|
_p_parameters.objectReferenceValue = obj;
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/SyncParameterSequenceEditor.cs.meta
Normal file
3
Editor/Inspector/SyncParameterSequenceEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bf6030b7fa704997885767897d1acba0
|
||||||
|
timeCreated: 1733090792
|
12
Editor/Inspector/WorldScaleObjectEditor.cs
Normal file
12
Editor/Inspector/WorldScaleObjectEditor.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(ModularAvatarWorldScaleObject))]
|
||||||
|
internal class WorldScaleObjectEditor : MAEditorBase
|
||||||
|
{
|
||||||
|
protected override void OnInnerInspectorGUI()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/WorldScaleObjectEditor.cs.meta
Normal file
3
Editor/Inspector/WorldScaleObjectEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e9b8b83586074bd7a6441b4cd7539dc9
|
||||||
|
timeCreated: 1741658287
|
3
Editor/Inspector/vrchat.meta
Normal file
3
Editor/Inspector/vrchat.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d59587969bdd48f4ba16883ee3b30d4d
|
||||||
|
timeCreated: 1742695977
|
27
Editor/Inspector/vrchat/VRChatSettingsEditor.cs
Normal file
27
Editor/Inspector/vrchat/VRChatSettingsEditor.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
[CustomEditor(typeof(ModularAvatarVRChatSettings))]
|
||||||
|
internal class VRChatSettingsEditor : MAEditorBase
|
||||||
|
{
|
||||||
|
protected override void OnInnerInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUI.BeginChangeCheck();
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(
|
||||||
|
serializedObject.FindProperty(nameof(ModularAvatarVRChatSettings.m_mmdWorldSupport)),
|
||||||
|
Localization.G("platform.vrchat.settings.mmd_world_support")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (EditorGUI.EndChangeCheck())
|
||||||
|
{
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
Localization.ShowLanguageUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/vrchat/VRChatSettingsEditor.cs.meta
Normal file
3
Editor/Inspector/vrchat/VRChatSettingsEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1ffd87033f4d441b849ad147b6e2a6ef
|
||||||
|
timeCreated: 1742695986
|
@ -51,6 +51,7 @@
|
|||||||
"merge_parameter.ui.add_button": "Add",
|
"merge_parameter.ui.add_button": "Add",
|
||||||
"merge_parameter.ui.details": "Parameter Configuration",
|
"merge_parameter.ui.details": "Parameter Configuration",
|
||||||
"merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults",
|
"merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults",
|
||||||
|
"merge_parameter.ui.importFromAsset": "Import from asset",
|
||||||
"merge_armature.merge_target": "Merge Target",
|
"merge_armature.merge_target": "Merge Target",
|
||||||
"merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into",
|
"merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into",
|
||||||
"merge_armature.prefix": "Prefix",
|
"merge_armature.prefix": "Prefix",
|
||||||
@ -77,6 +78,9 @@
|
|||||||
"merge_animator.relative_path_root.tooltip": "The root object to use when interpreting relative paths. If not specified, the object this component is attached to will be used.",
|
"merge_animator.relative_path_root.tooltip": "The root object to use when interpreting relative paths. If not specified, the object this component is attached to will be used.",
|
||||||
"merge_animator.layer_priority": "Layer Priority",
|
"merge_animator.layer_priority": "Layer Priority",
|
||||||
"merge_animator.layer_priority.tooltip": "Controls the order in which layers are merged into the animator - lower to higher. Negative values are merged before the original layer on the avatar descriptor, while zero and positive numbers are merged after.",
|
"merge_animator.layer_priority.tooltip": "Controls the order in which layers are merged into the animator - lower to higher. Negative values are merged before the original layer on the avatar descriptor, while zero and positive numbers are merged after.",
|
||||||
|
"merge_animator.merge_mode": "Merge Mode",
|
||||||
|
"merge_animator.merge_mode.Append": "Append to Animator",
|
||||||
|
"merge_animator.merge_mode.Replace": "Replace Existing Animator",
|
||||||
"merge_armature.lockmode": "Position sync mode",
|
"merge_armature.lockmode": "Position sync mode",
|
||||||
"merge_armature.lockmode.not_locked.title": "Not locked",
|
"merge_armature.lockmode.not_locked.title": "Not locked",
|
||||||
"merge_armature.lockmode.not_locked.body": "Merged armature does not sync its position with the base avatar.",
|
"merge_armature.lockmode.not_locked.body": "Merged armature does not sync its position with the base avatar.",
|
||||||
@ -86,12 +90,13 @@
|
|||||||
"merge_armature.lockmode.bidirectional.body": "The base armature and the merged armature will always have the same position. This is useful when creating animations that are meant to target the base armature. In order to activate this, your armatures must already be in the exact same position.",
|
"merge_armature.lockmode.bidirectional.body": "The base armature and the merged armature will always have the same position. This is useful when creating animations that are meant to target the base armature. In order to activate this, your armatures must already be in the exact same position.",
|
||||||
"merge_armature.reset_pos": "Reset position to base avatar",
|
"merge_armature.reset_pos": "Reset position to base avatar",
|
||||||
"merge_armature.reset_pos.info": "This command will force the position of all bones in the outfit to match that of the base avatar. This can be helpful as a starting point for installing outfits not set up for your current avatar.",
|
"merge_armature.reset_pos.info": "This command will force the position of all bones in the outfit to match that of the base avatar. This can be helpful as a starting point for installing outfits not set up for your current avatar.",
|
||||||
|
"merge_armature.reset_pos.convert_atpose": "Convert A-Pose/T-Pose to match base avatar",
|
||||||
"merge_armature.reset_pos.adjust_rotation": "Also set rotation to base avatar",
|
"merge_armature.reset_pos.adjust_rotation": "Also set rotation to base avatar",
|
||||||
"merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar",
|
"merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar",
|
||||||
"merge_armature.reset_pos.execute": "Do it!",
|
"merge_armature.reset_pos.execute": "Do it!",
|
||||||
"merge_armature.reset_pos.heuristic_scale": "Adjust outfit overall scale to match base avatar",
|
"merge_armature.reset_pos.heuristic_scale": "Adjust outfit overall scale to match base avatar",
|
||||||
"merge_armature.reset_pos.heuristic_scale.tooltip": "Will set the overall scale of the outfit as a whole based on armspan measurements. Recommended for setting up outfits.",
|
"merge_armature.reset_pos.heuristic_scale.tooltip": "Will set the overall scale of the outfit as a whole based on armspan measurements. Recommended for setting up outfits.",
|
||||||
"merge_blend_tree.blend_tree": "Blend Tree",
|
"merge_blend_tree.motion": "Motion (or Blend Tree) to merge",
|
||||||
"merge_blend_tree.path_mode": "Path Mode",
|
"merge_blend_tree.path_mode": "Path Mode",
|
||||||
"merge_blend_tree.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
|
"merge_blend_tree.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
|
||||||
"merge_blend_tree.relative_path_root": "Relative Path Root",
|
"merge_blend_tree.relative_path_root": "Relative Path Root",
|
||||||
@ -99,7 +104,7 @@
|
|||||||
"worldfixed.quest": "This component is not compatible with Android builds and will have no effect.",
|
"worldfixed.quest": "This component is not compatible with Android builds and will have no effect.",
|
||||||
"worldfixed.normal": "This object will be fixed to world unless you fixed to avatar with constraint.",
|
"worldfixed.normal": "This object will be fixed to world unless you fixed to avatar with constraint.",
|
||||||
"fpvisible.normal": "This object will be visible in your first person view.",
|
"fpvisible.normal": "This object will be visible in your first person view.",
|
||||||
"fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.",
|
"fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.\nIf this will be placed under the head bone through Bone Proxy etc., this warning can be ignored.",
|
||||||
"fpvisible.quest": "This component is not compatible with Android builds and will have no effect.",
|
"fpvisible.quest": "This component is not compatible with Android builds and will have no effect.",
|
||||||
"fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead.",
|
"fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead.",
|
||||||
"blendshape.mesh": "Mesh",
|
"blendshape.mesh": "Mesh",
|
||||||
@ -149,6 +154,11 @@
|
|||||||
"error.rename_params.default_value_conflict:hint": "To avoid unpredictable behavior, leave the default value field blank in all but on MA Parameters component. If multiple values are present, Modular Avatar will select the first default value specified in the hierarchy order.",
|
"error.rename_params.default_value_conflict:hint": "To avoid unpredictable behavior, leave the default value field blank in all but on MA Parameters component. If multiple values are present, Modular Avatar will select the first default value specified in the hierarchy order.",
|
||||||
"error.replace_object.null_target": "[MA-0008] No target specified",
|
"error.replace_object.null_target": "[MA-0008] No target specified",
|
||||||
"error.replace_object.null_target:hint": "Replace object needs a target object to replace. Try setting one.",
|
"error.replace_object.null_target:hint": "Replace object needs a target object to replace. Try setting one.",
|
||||||
|
"error.replace_object.replacing_replacement": "[MA-0009] The same target object cannot be specified in multiple Replace Object components",
|
||||||
|
"error.replace_object.parent_of_target": "[MA-0010] The target object cannot be a parent of this object",
|
||||||
|
"error.singleton": "[MA-0011] Only one instance of {0} is allowed in an avatar",
|
||||||
|
"error.merge_animator.multiple_replacements": "[MA-0012] Multiple Merge Animators are trying to replace the same layer",
|
||||||
|
"error.merge_animator.multiple_replacements:hint": "Because the 'Replace' mode of Merge Animator replaces the entire animator with a different one, it's not clear which of these you wanted to have win. Try either deleting all but one of these Merge Animators, or setting them to Append mode.",
|
||||||
"validation.blendshape_sync.no_local_renderer": "[MA-1000] No renderer found on this object",
|
"validation.blendshape_sync.no_local_renderer": "[MA-1000] No renderer found on this object",
|
||||||
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync acts on a Skinned Mesh Renderer on the same GameObject. Did you attach it to the right object?",
|
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync acts on a Skinned Mesh Renderer on the same GameObject. Did you attach it to the right object?",
|
||||||
"validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
|
"validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
|
||||||
@ -240,7 +250,7 @@
|
|||||||
"setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0}'s parents. Make sure your outfit is placed inside your avatar.",
|
"setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0}'s parents. Make sure your outfit is placed inside your avatar.",
|
||||||
"setup_outfit.err.no_animator": "Your avatar does not have an Animator component.",
|
"setup_outfit.err.no_animator": "Your avatar does not have an Animator component.",
|
||||||
"setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.",
|
"setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.",
|
||||||
"setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names:",
|
"setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names(case-insensitive):",
|
||||||
"move_independently.group-header": "Objects to move together",
|
"move_independently.group-header": "Objects to move together",
|
||||||
"scale_adjuster.scale": "Scale adjustment",
|
"scale_adjuster.scale": "Scale adjustment",
|
||||||
"scale_adjuster.adjust_children": "Adjust position of child objects",
|
"scale_adjuster.adjust_children": "Adjust position of child objects",
|
||||||
@ -280,5 +290,21 @@
|
|||||||
|
|
||||||
"ro_sim.effect_group.rule_inverted": "This rule is inverted",
|
"ro_sim.effect_group.rule_inverted": "This rule is inverted",
|
||||||
"ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met",
|
"ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met",
|
||||||
"ro_sim.effect_group.conditions": "Conditions"
|
"ro_sim.effect_group.conditions": "Conditions",
|
||||||
|
|
||||||
|
"menuitem.label.long_name.tooltip": "Use a long name which may contain rich text and line breaks.",
|
||||||
|
"menuitem.label.gameobject_name.tooltip": "Use the GameObject name.",
|
||||||
|
|
||||||
|
"remove-vertex-color.mode": "Mode",
|
||||||
|
"remove-vertex-color.mode.Remove": "Remove Vertex Colors",
|
||||||
|
"remove-vertex-color.mode.DontRemove": "Keep Vertex Colors",
|
||||||
|
"general.vrcsdk-required": "This component requires the VRCSDK to function.",
|
||||||
|
"sync-param-sequence.platform": "Primary Platform",
|
||||||
|
"sync-param-sequence.platform.tooltip": "When building for this platform, Modular Avatar will record all expression parameters for use on other platform builds",
|
||||||
|
"sync-param-sequence.parameters": "Common parameters asset",
|
||||||
|
"sync-param-sequence.parameters.tooltip": "The asset to store common parameters in. Do not use the same Expression Parameters that you have set in your avatar descriptor.",
|
||||||
|
"sync-param-sequence.create-asset": "New",
|
||||||
|
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset",
|
||||||
|
"platform.vrchat.settings.mmd_world_support": "MMD world support"
|
||||||
|
|
||||||
}
|
}
|
@ -9,7 +9,7 @@
|
|||||||
"menuinstall.showcontents": "メニュー内容を表示",
|
"menuinstall.showcontents": "メニュー内容を表示",
|
||||||
"menuinstall.showcontents.notselected": "メニューが選択されていません",
|
"menuinstall.showcontents.notselected": "メニューが選択されていません",
|
||||||
"menuinstall.devoptions": "プレハブ開発者向け設定",
|
"menuinstall.devoptions": "プレハブ開発者向け設定",
|
||||||
"menuinstall.menu_icon_too_large": "メニューに設定されているアイコンが256ピクセルより大きすぎます。",
|
"menuinstall.menu_icon_too_large": "メニューに設定されているアイコンが256x256より大きいです。",
|
||||||
"menuinstall.menu_icon_uncompressed": "メニューに設定されているアイコンが圧縮設定されていません。",
|
"menuinstall.menu_icon_uncompressed": "メニューに設定されているアイコンが圧縮設定されていません。",
|
||||||
"menuinstall.srcmenu": "インストールされるメニュー",
|
"menuinstall.srcmenu": "インストールされるメニュー",
|
||||||
"params.syncmode.NotSynced": "Animatorのみ",
|
"params.syncmode.NotSynced": "Animatorのみ",
|
||||||
@ -31,32 +31,33 @@
|
|||||||
"merge_parameter.ui.name": "パラメーター名",
|
"merge_parameter.ui.name": "パラメーター名",
|
||||||
"merge_parameter.ui.prefix": "PhysBone 接頭辞",
|
"merge_parameter.ui.prefix": "PhysBone 接頭辞",
|
||||||
"merge_parameter.ui.remapTo": "名前を変更",
|
"merge_parameter.ui.remapTo": "名前を変更",
|
||||||
"merge_parameter.ui.remapTo.tooltip": "ここに新しい名前を入れることで、このパラメーターの名前を変更できます。これで名前かぶりを回避したり、あるいはあえて複数のギミックを連動できます。",
|
"merge_parameter.ui.remapTo.tooltip": "ここに新しい名前を入れることで、このパラメーターの名前を変更できます。これで名前かぶりを回避したり、複数のギミックを連動させたりすることができます。",
|
||||||
"merge_parameter.ui.remapTo.automatic": "(自動的に設定)",
|
"merge_parameter.ui.remapTo.automatic": "(自動的に設定)",
|
||||||
"merge_parameter.ui.defaultValue": "初期値",
|
"merge_parameter.ui.defaultValue": "初期値",
|
||||||
"merge_parameter.ui.defaultValue.tooltip": "アバターがリセット、または最初に着た時にこの値が採用されます。",
|
"merge_parameter.ui.defaultValue.tooltip": "アバターをリセットした時、または最初に着た時にこの値が採用されます。",
|
||||||
"merge_parameter.ui.saved": "保存する",
|
"merge_parameter.ui.saved": "保存する",
|
||||||
"merge_parameter.ui.saved.tooltip": "保存されたパラメーターは、アバター変更やワールド移動で保持されます",
|
"merge_parameter.ui.saved.tooltip": "保存されたパラメーターは、アバター変更やワールド移動をしても保持されます",
|
||||||
"merge_parameter.ui.internalParameter": "自動リネーム",
|
"merge_parameter.ui.internalParameter": "自動リネーム",
|
||||||
"merge_parameter.ui.internalParameter.tooltip": "有効にすると、名前かぶりを回避するために自動的に名前を変更します",
|
"merge_parameter.ui.internalParameter.tooltip": "有効にすると、名前かぶりを回避するために自動的に名前を変更します",
|
||||||
"merge_parameter.ui.isPrefix": "PhysBone 接頭辞",
|
"merge_parameter.ui.isPrefix": "PhysBone 接頭辞",
|
||||||
"merge_parameter.ui.syncType": "パラメーター型",
|
"merge_parameter.ui.syncType": "パラメーター型",
|
||||||
"merge_parameter.ui.synced": "同期する",
|
"merge_parameter.ui.synced": "同期する",
|
||||||
"merge_parameter.ui.synced.tooltip": "有効にすると、ネットワーク上同期されます",
|
"merge_parameter.ui.synced.tooltip": "有効にすると、このパラメーターは同期されます",
|
||||||
"merge_parameter.ui.unregistered_foldout": "未登録パラメーター",
|
"merge_parameter.ui.unregistered_foldout": "未登録パラメーター",
|
||||||
"merge_parameter.ui.add_button": "追加",
|
"merge_parameter.ui.add_button": "追加",
|
||||||
"merge_parameter.ui.details": "パラメーターの詳細設定",
|
"merge_parameter.ui.details": "パラメーターの詳細設定",
|
||||||
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターの初期値を設定",
|
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定",
|
||||||
|
"merge_parameter.ui.importFromAsset": "アセットからインポートする",
|
||||||
"merge_armature.merge_target": "統合先",
|
"merge_armature.merge_target": "統合先",
|
||||||
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
|
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
|
||||||
"merge_armature.prefix": "接頭辞",
|
"merge_armature.prefix": "接頭辞",
|
||||||
"merge_armature.prefix.tooltip": "マージするボーンに付いている接頭辞",
|
"merge_armature.prefix.tooltip": "統合されるボーンに付いている接頭辞",
|
||||||
"merge_armature.suffix": "接尾辞",
|
"merge_armature.suffix": "接尾辞",
|
||||||
"merge_armature.suffix.tooltip": "マージするボーンに付いている接尾辞",
|
"merge_armature.suffix.tooltip": "統合されるボーンに付いている接尾辞",
|
||||||
"merge_armature.locked": "位置を固定",
|
"merge_armature.locked": "位置を固定",
|
||||||
"merge_armature.locked.tooltip": "このオブジェクトのボーンを統合先のボーンに常に相互的に位置を合わせる。アニメーション制作向け",
|
"merge_armature.locked.tooltip": "このオブジェクトのボーンと統合先のボーンの位置を常に合わせます。アニメーション作成時に便利です。",
|
||||||
"merge_armature.adjust_names": "ボーン名を統合先に合わせる",
|
"merge_armature.adjust_names": "ボーン名を統合先に合わせる",
|
||||||
"merge_armature.adjust_names.tooltip": "統合先のボーン名に合わせて、衣装のボーン名を合わせて変更します。統合先アバターに非対応の衣装導入向け機能です。",
|
"merge_armature.adjust_names.tooltip": "衣装のボーン名をアバターのボーン名に合わせて変更します。アバターに非対応の衣装を導入する時に便利です。",
|
||||||
"merge_armature.mangle_names": "名前かぶりを回避",
|
"merge_armature.mangle_names": "名前かぶりを回避",
|
||||||
"merge_armature.mangle_names.tooltip": "ほかのアセットとの名前かぶりを裂けるため、新規ボーンの名前を自動で変更する",
|
"merge_armature.mangle_names.tooltip": "ほかのアセットとの名前かぶりを裂けるため、新規ボーンの名前を自動で変更する",
|
||||||
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
|
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
|
||||||
@ -66,13 +67,16 @@
|
|||||||
"merge_animator.delete_attached_animator": "付属アニメーターを削除",
|
"merge_animator.delete_attached_animator": "付属アニメーターを削除",
|
||||||
"merge_animator.delete_attached_animator.tooltip": "統合後、このオブジェクトについているアニメーターを削除します",
|
"merge_animator.delete_attached_animator.tooltip": "統合後、このオブジェクトについているアニメーターを削除します",
|
||||||
"merge_animator.path_mode": "パスモード",
|
"merge_animator.path_mode": "パスモード",
|
||||||
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
|
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
|
||||||
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
|
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
|
||||||
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
|
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
|
||||||
"merge_animator.relative_path_root": "相対的パスのルート",
|
"merge_animator.relative_path_root": "相対的パスのルート",
|
||||||
"merge_animator.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
|
"merge_animator.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
|
||||||
"merge_animator.layer_priority": "レイヤー優先度",
|
"merge_animator.layer_priority": "レイヤー統合優先度",
|
||||||
"merge_animator.layer_priority.tooltip": "アニメーターにレイヤーが統合される順番を制御します。低い値から高い値の順に統合されます。マイナスの場合は元々のAvatar Descriptorについているコントローラーより前に統合され、ゼロ以上の場合はそのあとに統合されます。",
|
"merge_animator.layer_priority.tooltip": "アニメーターにレイヤーが統合される順番を制御します。低い値から高い値の順に統合されます。マイナスの場合は元々のAvatar Descriptorについているコントローラーより前に統合され、ゼロ以上の場合はそのあとに統合されます。",
|
||||||
|
"merge_animator.merge_mode": "統合モード",
|
||||||
|
"merge_animator.merge_mode.Append": "アニメーターに追加",
|
||||||
|
"merge_animator.merge_mode.Replace": "既存アニメーターを置き換える",
|
||||||
"merge_armature.lockmode": "位置追従モード",
|
"merge_armature.lockmode": "位置追従モード",
|
||||||
"merge_armature.lockmode.not_locked.title": "追従なし",
|
"merge_armature.lockmode.not_locked.title": "追従なし",
|
||||||
"merge_armature.lockmode.not_locked.body": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
|
"merge_armature.lockmode.not_locked.body": "統合されるアーマチュアは、統合先のアーマチュアに追従しません。",
|
||||||
@ -81,28 +85,29 @@
|
|||||||
"merge_armature.lockmode.bidirectional.title": "アバター <=====> オブジェクト (双方向)",
|
"merge_armature.lockmode.bidirectional.title": "アバター <=====> オブジェクト (双方向)",
|
||||||
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
|
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
|
||||||
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
|
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
|
||||||
"merge_armature.reset_pos.info": "このコマンドは、衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入するとき、アバウトに合わせるために便利です。",
|
"merge_armature.reset_pos.info": "衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入する際、アバウトに位置を合わせるのに便利です。",
|
||||||
|
"merge_armature.reset_pos.convert_atpose": "Aポーズ/Tポーズを合わせる",
|
||||||
"merge_armature.reset_pos.adjust_rotation": "回転も合わせる",
|
"merge_armature.reset_pos.adjust_rotation": "回転も合わせる",
|
||||||
"merge_armature.reset_pos.adjust_scale": "スケールも合わせる",
|
"merge_armature.reset_pos.adjust_scale": "スケールも合わせる",
|
||||||
"merge_armature.reset_pos.execute": "実行",
|
"merge_armature.reset_pos.execute": "実行",
|
||||||
"merge_armature.reset_pos.heuristic_scale": "衣装の全体的なスケールをアバターに合わせる",
|
"merge_armature.reset_pos.heuristic_scale": "衣装の全体的なスケールをアバターに合わせる",
|
||||||
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入するときは推奨です。",
|
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入する時にお勧めです。",
|
||||||
"merge_blend_tree.blend_tree": "ブレンドツリー",
|
"merge_blend_tree.motion": "結合するモーション(またはブレンドツリー)",
|
||||||
"merge_blend_tree.path_mode": "パスモード",
|
"merge_blend_tree.path_mode": "パスモード",
|
||||||
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
|
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトにつけたアニメーターでアニメーションを編集することができます。",
|
||||||
"merge_blend_tree.relative_path_root": "相対的パスのルート",
|
"merge_blend_tree.relative_path_root": "相対的パスのルート",
|
||||||
"merge_blend_tree.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
|
"merge_blend_tree.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
|
||||||
"worldfixed.quest": "このコンポーネントはアンドロイドビルド非対応のため無効となっています。",
|
"worldfixed.quest": "このコンポーネントはアンドロイドビルドに非対応であるため、無効となっています。",
|
||||||
"worldfixed.normal": "このオブジェクトはConstraint等でアバターに追従させない限りワールドに固定されます。",
|
"worldfixed.normal": "このオブジェクトはConstraint等でアバターに追従させない限りワールドに固定されます。",
|
||||||
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
|
"fpvisible.normal": "このオブジェクトは一人称視点で表示されます。",
|
||||||
"fpvisible.NotUnderHead": "このコンポーネントはヘッドボーン外では効果がありません。",
|
"fpvisible.NotUnderHead": "このコンポーネントは頭ボーンの配下でないと効果がありません。\n(Bone Proxyなどで頭ボーンの配下に配置される場合は効果あります)",
|
||||||
"fpvisible.quest": "このコンポーネントはアンドロイドビルド非対応のため無効となっています。",
|
"fpvisible.quest": "このコンポーネントはアンドロイドビルドに非対応であるため、無効となっています。",
|
||||||
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。",
|
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneで制御されているため、一人称視点でうまく表示させることができません。PhysBoneの始点を指定してください。",
|
||||||
"blendshape.mesh": "メッシュ",
|
"blendshape.mesh": "メッシュ",
|
||||||
"blendshape.source": "元メッシュのブレンドシェープ",
|
"blendshape.source": "元メッシュのブレンドシェイプ",
|
||||||
"blendshape.target": "このメッシュのブレンドシェープ",
|
"blendshape.target": "このメッシュのブレンドシェイプ",
|
||||||
"hint.not_in_avatar": "このコンポーネントが正しく動作するには、アバター内に配置する必要があります。",
|
"hint.not_in_avatar": "このコンポーネントを正しく動作させるには、アバター内に配置する必要があります。",
|
||||||
"boneproxy.err.MovingTarget": "他のモジュラーアバターコンポーネントで移動されるオブジェクトを指定できません。",
|
"boneproxy.err.MovingTarget": "他のモジュラーアバターコンポーネントで移動されるオブジェクトは指定できません。",
|
||||||
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。",
|
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。",
|
||||||
"boneproxy.attachment": "配置モード",
|
"boneproxy.attachment": "配置モード",
|
||||||
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
|
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
|
||||||
@ -111,28 +116,28 @@
|
|||||||
"boneproxy.attachment.AsChildKeepRotation": "子として・ワールド向きを維持",
|
"boneproxy.attachment.AsChildKeepRotation": "子として・ワールド向きを維持",
|
||||||
"mesh_settings.header_probe_anchor": "Anchor Override 設定",
|
"mesh_settings.header_probe_anchor": "Anchor Override 設定",
|
||||||
"mesh_settings.inherit_probe_anchor": "設定モード",
|
"mesh_settings.inherit_probe_anchor": "設定モード",
|
||||||
"mesh_settings.probe_anchor": "アンカーオーバーライド",
|
"mesh_settings.probe_anchor": "Anchor Override",
|
||||||
"mesh_settings.probe_anchor.tooltip": "このオブジェクトとその子のレンダラーのAnchor Overrideを設定します。",
|
"mesh_settings.probe_anchor.tooltip": "このオブジェクトとその子のレンダラーのAnchor Overrideを設定します。",
|
||||||
"mesh_settings.header_bounds": "Bounds 設定",
|
"mesh_settings.header_bounds": "Bounds 設定",
|
||||||
"mesh_settings.inherit_bounds": "設定モード",
|
"mesh_settings.inherit_bounds": "設定モード",
|
||||||
"mesh_settings.root_bone": "ルートボーン",
|
"mesh_settings.root_bone": "ルートボーン",
|
||||||
"mesh_settings.root_bone.tooltip": "このオブジェクトとその子のメッシュで設定されるルートボーン。メッシュのバウンズを計算するための参照点として使用されます。",
|
"mesh_settings.root_bone.tooltip": "このオブジェクトとその子のメッシュで設定されるルートボーン。メッシュのBoundsを計算する際の基準として使用されます。",
|
||||||
"mesh_settings.bounds": "バウンズ",
|
"mesh_settings.bounds": "Bounds",
|
||||||
"mesh_settings.bounds.tooltip": "このオブジェクトとその子のメッシュで設定されるバウンズ。画面外のメッシュのレンダリングを省略するかどうかを決定するために使用されます。",
|
"mesh_settings.bounds.tooltip": "このオブジェクトとその子のメッシュで設定されるバウンズ。画面外のメッシュのレンダリングを省略するかどうかを決定するために使用されます。",
|
||||||
"mesh_settings.inherit_mode.Inherit": "継承",
|
"mesh_settings.inherit_mode.Inherit": "継承",
|
||||||
"mesh_settings.inherit_mode.Set": "設定",
|
"mesh_settings.inherit_mode.Set": "設定",
|
||||||
"mesh_settings.inherit_mode.DontSet": "設定しない(メッシュ本体の設定のまま)",
|
"mesh_settings.inherit_mode.DontSet": "設定しない(メッシュ本体の設定のまま)",
|
||||||
"mesh_settings.inherit_mode.SetOrInherit": "親が指定されてる時は継承、または設定",
|
"mesh_settings.inherit_mode.SetOrInherit": "親で指定されている時は継承、それ以外では設定",
|
||||||
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
|
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
|
||||||
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。",
|
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでも駄目な場合、Modular Avatarにも最新版が出ていないか確認してみてください。",
|
||||||
"error.stack_trace": "スタックトレース(バグを報告する時は必ず添付してください!)",
|
"error.stack_trace": "スタックトレース(バグを報告する時は必ず添付してください!)",
|
||||||
"error.merge_armature.circular_dependency": "[MA-0001] Merge Armatureに循環参照があります",
|
"error.merge_armature.circular_dependency": "[MA-0001] Merge Armatureに循環参照があります",
|
||||||
"error.merge_armature.circular_dependency:description": "Merge Armature コンポーネントは、自分自身、または自分の子をマージターゲットとして参照しています。",
|
"error.merge_armature.circular_dependency:description": "Merge Armatureコンポーネントの統合先として、自分自身、または自分の子が参照されています。",
|
||||||
"error.merge_armature.circular_dependency:hint": "Merge Armature は通常、ターゲットフィールドにアバター本体の Armature オブジェクトを指定する必要があります。衣装自体を指定しないでください。",
|
"error.merge_armature.circular_dependency:hint": "通常、Merge Armatureは統合先としてアバター本体のArmatureオブジェクトを指定する必要があります。衣装自体を指定しないように注意してください。",
|
||||||
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] ヒューマノイドボーンにPhysBoneコンポーネントを検出",
|
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] HumanoidボーンにPhysBoneコンポーネントがついています。",
|
||||||
"error.merge_armature.physbone_on_humanoid_bone:hint": "一部のヒューマノイドボーンは、PhysBonesによって制御されています。 マージ対象の対応するヒューマノイドボーンとは位置が異なるため、適切にマージすることはできません。マージするにはヒューマノイドボーンのPhysBonesを取り除く必要があります。",
|
"error.merge_armature.physbone_on_humanoid_bone:hint": "一部のHumanoidボーンがPhysBoneによって制御されています。対応する統合先のHumanoidボーンと位置が異なるため、適切に統合することができません。統合するにはHumanoidボーンについているPhysBoneを取り除く必要があります。",
|
||||||
"error.merge_blend_tree.missing_tree": "[MA-0009] ブレンドツリーが指定されていません",
|
"error.merge_blend_tree.missing_tree": "[MA-0009] ブレンドツリーが指定されていません",
|
||||||
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Treeが動作するには、どのブレンドツリーを統合するかを指定する必要があります。「ブレンドツリー」欄を設定してみてください。",
|
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Treeが動作するには、どのブレンドツリーを統合するか指定する必要があります。「ブレンドツリー」欄を設定しているかご確認ください。",
|
||||||
"error.internal_error": "[MA-9999] 内部エラーが発生しました:{0}\n以下のオブジェクトの処理中に発生しました:",
|
"error.internal_error": "[MA-9999] 内部エラーが発生しました:{0}\n以下のオブジェクトの処理中に発生しました:",
|
||||||
"error.merge_animator.param_type_mismatch": "[MA-0003] パラメータの型が競合しています",
|
"error.merge_animator.param_type_mismatch": "[MA-0003] パラメータの型が競合しています",
|
||||||
"error.merge_animator.param_type_mismatch:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
|
"error.merge_animator.param_type_mismatch:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
|
||||||
@ -142,37 +147,42 @@
|
|||||||
"error.rename_params.type_conflict:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
|
"error.rename_params.type_conflict:description": "パラメータ {0} には複数の型が指定されています: {1} != {2}",
|
||||||
"error.rename_params.default_value_conflict": "[MA-0007] 初期値の競合",
|
"error.rename_params.default_value_conflict": "[MA-0007] 初期値の競合",
|
||||||
"error.rename_params.default_value_conflict:description": "パラメータ {0} には複数の初期値が指定されています: {1} != {2}",
|
"error.rename_params.default_value_conflict:description": "パラメータ {0} には複数の初期値が指定されています: {1} != {2}",
|
||||||
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるために、MAパラメータコンポーネントのデフォルト値フィールドはパラメーター名毎に一個のコンポーネント以外空白のままにしてください。 複数の値が存在する場合、Modular Avatarは階層順に指定された最初のデフォルト値を選択します。",
|
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるため、MA Parametersコンポーネントの初期値フィールドはパラメーター名毎に1つだけしか指定しないようにし、他のコンポーネントでは空白のままにしてください。複数の値が存在する場合、Modular Avatarは階層順で最初に指定された初期値を採用します。",
|
||||||
"error.replace_object.null_target": "[MA-0008] ターゲットが指定されていません",
|
"error.replace_object.null_target": "[MA-0008] 置き換え先が指定されていません",
|
||||||
"error.replace_object.null_target:hint": "Replace Object には置き換えるべきターゲットオブジェクトが必要です。設定してみてください。",
|
"error.replace_object.null_target:hint": "Replace Objectは置き換え先のオブジェクトを指定する必要があります。",
|
||||||
|
"error.replace_object.replacing_replacement": "[MA-0009] 複数のReplace Objectコンポーネントで、同じ置き換え先を指定できません",
|
||||||
|
"error.replace_object.parent_of_target": "[MA-0010] このオブジェクトの親を置き換え先に指定できません",
|
||||||
|
"error.singleton": "[MA-0011] {0} はアバターに一個しか存在できません",
|
||||||
|
"error.merge_animator.multiple_replacements": "[MA-0012] 複数のMerge Animatorが同じレイヤーを置き換えようとしています",
|
||||||
|
"error.merge_animator.multiple_replacements:hint": "Merge Animator の「既存アニメーターを置き換える」設定は丸ごと置き換えるので、複数があるとどれに置き換えたいかわかりません。一つを残してMerge Animatorを削除するか、一つを除いて「アニメーターに追加」にしてみましょう。",
|
||||||
"validation.blendshape_sync.no_local_renderer": "[MA-1000] このオブジェクトにはSkinned Mesh Rendererがありません。",
|
"validation.blendshape_sync.no_local_renderer": "[MA-1000] このオブジェクトにはSkinned Mesh Rendererがありません。",
|
||||||
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上のSkinned Mesh Rendererに作用します。正しいオブジェクトに追加しましたか?",
|
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上のSkinned Mesh Rendererに作用します。コンポーネントが正しいオブジェクトに追加されているか確認してください。",
|
||||||
"validation.blendshape_sync.no_local_mesh": "[MA-1001] このオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
|
"validation.blendshape_sync.no_local_mesh": "[MA-1001] このオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
|
||||||
"validation.blendshape_sync.no_local_mesh:hint": "このオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
|
"validation.blendshape_sync.no_local_mesh:hint": "このオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
|
||||||
"validation.blendshape_sync.no_bindings": "[MA-1002] このBlendshape Syncにはバインドが設定されていません。",
|
"validation.blendshape_sync.no_bindings": "[MA-1002] このBlendshape Syncにはバインドが設定されていません。",
|
||||||
"validation.blendshape_sync.no_bindings:hint": "Blendshape Syncは、どのブレンドシェイプを同期するかを知る必要があります。追加するには、「+」ボタンをクリックしてください。",
|
"validation.blendshape_sync.no_bindings:hint": "Blendshape Syncは、どのブレンドシェイプを同期するかを知る必要があります。追加するには、「+」ボタンをクリックしてください。",
|
||||||
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 同期先のメッシュに該当するブレンドシェープ「{0}」がありません。",
|
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 同期先のメッシュにブレンドシェイプ「{0}」がありません。",
|
||||||
"validation.blendshape_sync.missing_local_shape:description": "ローカルブレンドシェイプがありません: {0}",
|
"validation.blendshape_sync.missing_local_shape:description": "ブレンドシェイプ「{0}」がありません。",
|
||||||
"validation.blendshape_sync.missing_local_shape:hint": "ターゲットオブジェクトから値を受け取るように設定されたブレンドシェイプがありません。赤で示されているブレンドシェイプ名を変更してみてください。",
|
"validation.blendshape_sync.missing_local_shape:hint": "値を「受け取る」ように設定されたブレンドシェイプが同期先のメッシュに存在しません。赤で示されているブレンドシェイプ名を変更してみてください。",
|
||||||
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 同期先メッシュにブレンドシェープ「{0}」が見つかりません",
|
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 同期元のメッシュにブレンドシェイプ「{0}」がありません。",
|
||||||
"validation.blendshape_sync.missing_target_shape:description": "ターゲットブレンドシェイプがありません: {0}",
|
"validation.blendshape_sync.missing_target_shape:description": "ブレンドシェイプ「{0}」がありません。",
|
||||||
"validation.blendshape_sync.missing_target_shape:hint": "ローカルオブジェクトに値を「送る」ように設定されたブレンドシェイプがありません。赤で示されているブレンドシェイプ名を変更してみてください。",
|
"validation.blendshape_sync.missing_target_shape:hint": "値を「送る」ように設定されたブレンドシェイプが同期元のメッシュに存在しません。赤で示されているブレンドシェイプ名を変更してみてください。",
|
||||||
"validation.blendshape_sync.no_target": "[MA-1005] このBlendshape Syncには同期元が設定されていないバインドがあります。",
|
"validation.blendshape_sync.no_target": "[MA-1005] このBlendshape Syncには同期元が設定されていないバインドがあります。",
|
||||||
"validation.blendshape_sync.no_target:hint": "どのオブジェクトからBlendshapeを同期するかを教える必要があります。メッシュを設定してみてください。",
|
"validation.blendshape_sync.no_target:hint": "どのオブジェクトからBlendshapeを同期するか指定する必要があります。メッシュを設定してください。",
|
||||||
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 同期元のオブジェクトにはSkinnedMeshRendererがありません。",
|
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 同期元のオブジェクトにSkinned Mesh Rendererがありません。",
|
||||||
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Syncは、対象オブジェクトのSkinned Mesh Rendererからブレンド形状の値を受け取ります。正しいオブジェクトに追加しましたか?",
|
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Syncは、同期元のSkinned Mesh Rendererからブレンドシェイプの値を受け取ります。コンポーネントが正しいオブジェクトに追加されているか確認してください。",
|
||||||
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 同期元のオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
|
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 同期元のオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
|
||||||
"validation.blendshape_sync.missing_target_mesh:hint": "ターゲットオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
|
"validation.blendshape_sync.missing_target_mesh:hint": "同期元のオブジェクトの Skinned Mesh Renderer の設定が壊れている可能性があります。 元のプレハブまたはFBXからオブジェクトを再作成してみてください。",
|
||||||
"validation.bone_proxy.no_target": "[MA-1100] ターゲットオブジェクトが未設定、もしくは存在しません。",
|
"validation.bone_proxy.no_target": "[MA-1100] ターゲットオブジェクトが未設定であるか、存在しません。",
|
||||||
"validation.bone_proxy.no_target:hint": "ボーンプロキシがどのオブジェクトに追尾するかを知る必要があります。追尾すべきオブジェクトをターゲットフィールドに設定してみてください。",
|
"validation.bone_proxy.no_target:hint": "Bone Proxyはどのオブジェクトに追従させるかを指定する必要があります。追従させたい対象のオブジェクトをターゲット欄に設定してみてください。",
|
||||||
"validation.menu_installer.no_menu": "[MA-1200] インストールするメニューがありません。",
|
"validation.menu_installer.no_menu": "[MA-1200] インストールするメニューがありません。",
|
||||||
"validation.menu_installer.no_menu:hint": "Menu Installer は、どのメニューをインストールするべきか設定する必要があります。 「Prefab開発者向けオプション」内の「インストールするメニュー」フィールドを設定するか、MA Menu Itemコンポーネントを追加してみてください。",
|
"validation.menu_installer.no_menu:hint": "Menu Installer は、どのメニューをインストールするべきか設定する必要があります。 「Prefab開発者向けオプション」内の「インストールするメニュー」フィールドを設定するか、MA Menu Itemコンポーネントを追加してみてください。",
|
||||||
"validation.merge_animator.no_animator": "[MA-1300] Animator Controllerが設定されていません",
|
"validation.merge_animator.no_animator": "[MA-1300] Animator Controllerが未設定であるか、存在しません。",
|
||||||
"validation.merge_animator.no_animator:hint": "Animatorをマージするには、どのアニメーターを統合するかを設定する必要があります。「統合されるアニメーター」を設定してみてください。",
|
"validation.merge_animator.no_animator:hint": "どのアニメーターを統合するか設定する必要があります。「統合されるアニメーター」を設定してみてください。",
|
||||||
"validation.merge_armature.no_target": "[MA-1400] 統合先が未設定、もしくは存在しません。",
|
"validation.merge_armature.no_target": "[MA-1400] 統合先が未設定であるか、存在しません。",
|
||||||
"validation.merge_armature.no_target:hint": "Merge Armatureはどこに統合するか設定する必要があります。「統合先」を設定してみてください。",
|
"validation.merge_armature.no_target:hint": "Merge Armatureはどこに統合するか設定する必要があります。「統合先」を設定してみてください。",
|
||||||
"validation.merge_armature.target_is_child": "[MA-1500] 統合先をこのオブジェクトの子にすることができません",
|
"validation.merge_armature.target_is_child": "[MA-1500] 統合先として自身の子を指定することはできません。",
|
||||||
"validation.merge_armature.target_is_child:hint": "Merge Armature は自身の子に統合できません。「統合先」を別のオブジェクトに設定してみてください。",
|
"validation.merge_armature.target_is_child:hint": "Merge Armatureで自身の子に統合することはできません。「統合先」を別のオブジェクトに設定してみてください。",
|
||||||
"submenu_source.Children": "子オブジェクトから生成",
|
"submenu_source.Children": "子オブジェクトから生成",
|
||||||
"submenu_source.MenuAsset": "Expressions Menu アセットを指定",
|
"submenu_source.MenuAsset": "Expressions Menu アセットを指定",
|
||||||
"menuitem.showcontents": "メニュー内容を表示",
|
"menuitem.showcontents": "メニュー内容を表示",
|
||||||
@ -182,27 +192,27 @@
|
|||||||
"menuitem.prop.type": "タイプ",
|
"menuitem.prop.type": "タイプ",
|
||||||
"menuitem.prop.type.tooltip": "この項目の種別",
|
"menuitem.prop.type.tooltip": "この項目の種別",
|
||||||
"menuitem.prop.value": "パラメーター値",
|
"menuitem.prop.value": "パラメーター値",
|
||||||
"menuitem.prop.value.tooltip": "この項目が操作されたとき、パラメーターが設定される値",
|
"menuitem.prop.value.tooltip": "この項目が操作されたとき、パラメーターに設定される値",
|
||||||
|
"menuitem.prop.automatic_value": "自動",
|
||||||
|
"menuitem.prop.automatic_value.tooltip": "かぶらない値を自動的に割り振る",
|
||||||
"menuitem.prop.parameter": "パラメーター名",
|
"menuitem.prop.parameter": "パラメーター名",
|
||||||
"menuitem.prop.label": "表示名",
|
"menuitem.prop.label": "表示名",
|
||||||
"menuitem.prop.submenu_asset": "サブメニューアセット",
|
"menuitem.prop.submenu_asset": "サブメニューアセット",
|
||||||
"menuitem.prop.submenu_asset.tooltip": "サブメニューとして引用するアセット",
|
"menuitem.prop.submenu_asset.tooltip": "サブメニューとして引用するアセット",
|
||||||
"menuitem.prop.submenu_source": "サブメニュー引用元",
|
"menuitem.prop.submenu_source": "サブメニュー引用元",
|
||||||
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきかを指定",
|
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきか指定します。",
|
||||||
"menuitem.prop.source_override": "引用元オブジェクト",
|
"menuitem.prop.source_override": "引用元オブジェクト",
|
||||||
"menuitem.prop.source_override.tooltip": "指定した場合は、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
|
"menuitem.prop.source_override.tooltip": "指定した場合、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
|
||||||
"menuitem.prop.is_default": "初期設定にする",
|
"menuitem.prop.is_default": "初期設定にする",
|
||||||
"menuitem.prop.is_default.tooltip": "ONの場合、アバター初期化の際にこのメニューアイテムを選択します",
|
"menuitem.prop.is_default.tooltip": "有効になっていると、アバター初期化の際にこのメニューアイテムを選択した状態にします。",
|
||||||
"menuitem.prop.is_saved": "保存する",
|
"menuitem.prop.is_saved": "保存する",
|
||||||
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
|
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動をしてもこの設定が保持されます。",
|
||||||
"menuitem.prop.is_synced": "同期する",
|
"menuitem.prop.is_synced": "同期する",
|
||||||
"menuitem.prop.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
|
"menuitem.prop.is_synced.tooltip": "有効になっていると、メニューがほかのプレイヤーに同期されます。",
|
||||||
"menuitem.prop.automatic_value": "自動",
|
"menuitem.param.rotation": "ラジアルメニュー用パラメーター名",
|
||||||
"menuitem.prop.automatic_value.tooltip": "かぶらない値を自動的に割り振る",
|
"menuitem.param.rotation.tooltip": "このラジアルメニューの操作と連動するべきパラメーター",
|
||||||
"menuitem.param.rotation": "回転パラメーター名",
|
|
||||||
"menuitem.param.rotation.tooltip": "このメニューアイテムの回転に連動するべきパラメーター",
|
|
||||||
"menuitem.param.horizontal": "横パラメーター名",
|
"menuitem.param.horizontal": "横パラメーター名",
|
||||||
"menuitem.param.horizontal.tooltip": "横操作に連動するパラメーター名",
|
"menuitem.param.horizontal.tooltip": "左右操作に連動するパラメーター名",
|
||||||
"menuitem.param.vertical": "縦パラメーター名",
|
"menuitem.param.vertical": "縦パラメーター名",
|
||||||
"menuitem.param.vertical.tooltip": "上下操作に連動するパラメーター名",
|
"menuitem.param.vertical.tooltip": "上下操作に連動するパラメーター名",
|
||||||
"menuitem.label.control_labels_and_params": "表示名・パラメーター",
|
"menuitem.label.control_labels_and_params": "表示名・パラメーター",
|
||||||
@ -218,29 +228,29 @@
|
|||||||
"control_group.foldout.actions": "アクション",
|
"control_group.foldout.actions": "アクション",
|
||||||
"control_group.foldout.menu_items": "関連付けされたメニューアイテム",
|
"control_group.foldout.menu_items": "関連付けされたメニューアイテム",
|
||||||
"control_group.is_saved": "保存する",
|
"control_group.is_saved": "保存する",
|
||||||
"control_group.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
|
"control_group.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動をしてもこの設定が保持されます。",
|
||||||
"control_group.is_synced": "同期する",
|
"control_group.is_synced": "同期する",
|
||||||
"control_group.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
|
"control_group.is_synced.tooltip": "有効になっていると、メニューがほかのプレイヤーに同期されます。",
|
||||||
"control_group.default_value": "初期値",
|
"control_group.default_value": "初期値",
|
||||||
"control_group.default_value.unset": "(どれも選択されない)",
|
"control_group.default_value.unset": "(どれも選択されない)",
|
||||||
"animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}",
|
"animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}",
|
||||||
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定に指定されたメニューアイテムが複数あります。",
|
"animation_gen.multiple_defaults": "同じコントロールグループに初期設定にするとして指定されたメニューアイテムが複数あります。",
|
||||||
"menuitem.misc.add_item": "メニューアイテムを追加",
|
"menuitem.misc.add_item": "メニューアイテムを追加",
|
||||||
"replace_object.target_object": "上書き先",
|
"replace_object.target_object": "置き換え先",
|
||||||
"setup_outfit.err.header.notarget": "Setup outfit の処理が失敗しました",
|
"setup_outfit.err.header.notarget": "Setup Outfit の処理に失敗しました",
|
||||||
"setup_outfit.err.header": "Setup outfit が「{0}」を処理中に失敗しました。",
|
"setup_outfit.err.header": "Setup Outfit が「{0}」を処理中に失敗しました。",
|
||||||
"setup_outfit.err.unknown": "原因不明のエラーが発生しました。",
|
"setup_outfit.err.unknown": "原因不明のエラーが発生しました。",
|
||||||
"setup_outfit.err.no_selection": "オブジェクトが選択されていません。",
|
"setup_outfit.err.no_selection": "オブジェクトが選択されていません。",
|
||||||
"setup_outfit.err.run_on_avatar_itself": "Setup Outfitはアバター自体ではなく、衣装のほうで実行してください。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。",
|
"setup_outfit.err.run_on_avatar_itself": "Setup Outfitはアバター自体ではなく、衣装の方で実行してください。\n\nキメラアバターを作る場合は、中の方のAvatar Descriptorを消して、衣装として扱ってください。",
|
||||||
"setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親に、複数のavatar descriptorを発見しました。\n\nキメラアバターを作る場合は、中のほうのAvatar Descriptorを消して、衣装として扱ってください。",
|
"setup_outfit.err.multiple_avatar_descriptors": "「{0}」とその親で複数のAvatar Descriptorを発見しました。\n\nキメラアバターを作る場合は、中の方のAvatar Descriptorを消して、衣装として扱ってください。",
|
||||||
"setup_outfit.err.no_avatar_descriptor": "「{0}」の親に、avatar descriptorが見つかりませんでした。衣装のオブジェクトをアバターの中に配置してください。",
|
"setup_outfit.err.no_avatar_descriptor": "「{0}」の親にAvatar Descriptorが見つかりませんでした。衣装のオブジェクトをアバターの中に配置してください。",
|
||||||
"setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。",
|
"setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。",
|
||||||
"setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup Outfitはヒューマノイドアバター以外には対応していません。",
|
"setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup OutfitはHumanoidアバター以外には対応していません。",
|
||||||
"setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。以下の名前を含むボーンを探しました:",
|
"setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。アクセサリー等に対してSetup Outfitを試みた場合は、代わりにBone Proxyコンポーネントを使って配置してみてください。\n以下の名前を含むボーンを(大文字・小文字を区別つけずに)探しました:",
|
||||||
"move_independently.group-header": "一緒に動かすオブジェクト",
|
"move_independently.group-header": "同時に動かすオブジェクト",
|
||||||
"scale_adjuster.scale": "Scale調整値",
|
"scale_adjuster.scale": "Scale調整値",
|
||||||
"scale_adjuster.adjust_children": "子オブジェクトの位置を調整",
|
"scale_adjuster.adjust_children": "子オブジェクトの位置を調整",
|
||||||
"world_fixed_object.err.unsupported_platform": "World Fixed Objectがこのプラットフォームに対応していません",
|
"world_fixed_object.err.unsupported_platform": "World Fixed Objectはこのプラットフォームに対応していません。",
|
||||||
"world_fixed_object.err.unsupported_platform:description": "World Fixed ObjectはAndroid向けビルドには対応していないため、動作しません。",
|
"world_fixed_object.err.unsupported_platform:description": "World Fixed ObjectはAndroid向けビルドには対応していないため、動作しません。",
|
||||||
"ma_info.param_usage_ui.header": "Expressions Parameter 使用状況",
|
"ma_info.param_usage_ui.header": "Expressions Parameter 使用状況",
|
||||||
"ma_info.param_usage_ui.other_objects": "このアバター内の他のオブジェクト",
|
"ma_info.param_usage_ui.other_objects": "このアバター内の他のオブジェクト",
|
||||||
@ -250,8 +260,8 @@
|
|||||||
"reactive_object.inverse": "条件を反転",
|
"reactive_object.inverse": "条件を反転",
|
||||||
"reactive_object.material-setter.set-to": "変更先のマテリアル ",
|
"reactive_object.material-setter.set-to": "変更先のマテリアル ",
|
||||||
"menuitem.misc.add_toggle": "トグルを追加",
|
"menuitem.misc.add_toggle": "トグルを追加",
|
||||||
"ro_sim.open_debugger_button": "リアクションデバッグツールを開く",
|
"ro_sim.open_debugger_button": "Reaction デバッガーを開く",
|
||||||
"ro_sim.window.title": "MA リアクションデバッグツール",
|
"ro_sim.window.title": "MA Reaction デバッガー",
|
||||||
"ro_sim.header.inspecting": "表示中のオブジェクト",
|
"ro_sim.header.inspecting": "表示中のオブジェクト",
|
||||||
"ro_sim.header.clear_overrides": "すべてのオーバーライドを解除",
|
"ro_sim.header.clear_overrides": "すべてのオーバーライドを解除",
|
||||||
"ro_sim.header.object_state": "オブジェクトのアクティブ状態",
|
"ro_sim.header.object_state": "オブジェクトのアクティブ状態",
|
||||||
@ -260,16 +270,30 @@
|
|||||||
"ro_sim.header.override_gameobject_state": "GameObject のアクティブ状態をオーバーライド",
|
"ro_sim.header.override_gameobject_state": "GameObject のアクティブ状態をオーバーライド",
|
||||||
"ro_sim.header.override_menuitem_state": "MenuItem の選択状態をオーバーライト",
|
"ro_sim.header.override_menuitem_state": "MenuItem の選択状態をオーバーライト",
|
||||||
"ro_sim.affected_by.title": "以下のルールに影響されています",
|
"ro_sim.affected_by.title": "以下のルールに影響されています",
|
||||||
"ro_sim.effect_group.controls_obj_state": "オブジェクトのアクティブ状態を設定する➡",
|
"ro_sim.effect_group.component": "Reactive Component",
|
||||||
|
"ro_sim.effect_group.controls_obj_state": "オブジェクトのアクティブ状態を次に設定する➡",
|
||||||
"ro_sim.effect_group.target_component": "コンポーネント",
|
"ro_sim.effect_group.target_component": "コンポーネント",
|
||||||
"ro_sim.effect_group.target_component.tooltip": "Reactive Componentに影響されるコンポーネント",
|
"ro_sim.effect_group.target_component.tooltip": "Reactive Componentに影響されるコンポーネント",
|
||||||
"ro_sim.effect_group.property": "プロパティ",
|
"ro_sim.effect_group.property": "プロパティ",
|
||||||
"ro_sim.effect_group.property.tooltip": "設定されるプロパティ",
|
"ro_sim.effect_group.property.tooltip": "設定されるプロパティ",
|
||||||
"ro_sim.effect_group.value": "値",
|
"ro_sim.effect_group.value": "値",
|
||||||
"ro_sim.effect_group.value.tooltip": "上記 Reactive Component が活性状態の時に設定される値",
|
"ro_sim.effect_group.value.tooltip": "上記の Reactive Component がアクティブな時に設定される値",
|
||||||
"ro_sim.effect_group.material": "マテリアル",
|
"ro_sim.effect_group.material": "マテリアル",
|
||||||
"ro_sim.effect_group.material.tooltip": "上記 Reactive Component が活性状態の時に設定されるマテリアル",
|
"ro_sim.effect_group.material.tooltip": "上記の Reactive Component がアクティブな時に設定されるマテリアル",
|
||||||
"ro_sim.effect_group.rule_inverted": "このルールの条件が反転されています",
|
"ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています",
|
||||||
"ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます",
|
"ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます",
|
||||||
"ro_sim.effect_group.conditions": "条件"
|
"ro_sim.effect_group.conditions": "条件",
|
||||||
|
"menuitem.label.long_name.tooltip": "リッチテキスト、改行を含む長い名前にする。",
|
||||||
|
"menuitem.label.gameobject_name.tooltip": "ゲームオブジェクトの名前を採用する。",
|
||||||
|
"remove-vertex-color.mode": "モード",
|
||||||
|
"remove-vertex-color.mode.Remove": "頂点カラーを削除する",
|
||||||
|
"remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない",
|
||||||
|
"general.vrcsdk-required": "このコンポーネントにはVRCSDKが必要です。",
|
||||||
|
"sync-param-sequence.platform": "主要プラットホーム",
|
||||||
|
"sync-param-sequence.platform.tooltip": "このプラットホームでビルドすると、他のプラットホームを合わせるためにパラメーターを記録します。",
|
||||||
|
"sync-param-sequence.parameters": "共用パラメーターアセット",
|
||||||
|
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
|
||||||
|
"sync-param-sequence.create-asset": "新規作成",
|
||||||
|
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します",
|
||||||
|
"platform.vrchat.settings.mmd_world_support": "MMDワールド対応"
|
||||||
}
|
}
|
@ -88,7 +88,6 @@
|
|||||||
"merge_armature.reset_pos.execute": "실행",
|
"merge_armature.reset_pos.execute": "실행",
|
||||||
"merge_armature.reset_pos.heuristic_scale": "의상의 전체적인 스케일을 원본 아바타에 맞춤",
|
"merge_armature.reset_pos.heuristic_scale": "의상의 전체적인 스케일을 원본 아바타에 맞춤",
|
||||||
"merge_armature.reset_pos.heuristic_scale.tooltip": "원본 아바타의 팔 길이를 참조하여, 의상의 전체 스케일을 이에 맞춥니다. 전용 의상이 아닌 경우 도움이 됩니다.",
|
"merge_armature.reset_pos.heuristic_scale.tooltip": "원본 아바타의 팔 길이를 참조하여, 의상의 전체 스케일을 이에 맞춥니다. 전용 의상이 아닌 경우 도움이 됩니다.",
|
||||||
"merge_blend_tree.blend_tree": "블랜드 트리",
|
|
||||||
"merge_blend_tree.path_mode": "경로 모드",
|
"merge_blend_tree.path_mode": "경로 모드",
|
||||||
"merge_blend_tree.path_mode.tooltip": "애니메이션에서 경로를 해석하는 방법에 대해 설명합니다. 상대 모드를 사용하면 이 오브젝트의 애니메이터에서 애니메이션을 기록할 수 있습니다.",
|
"merge_blend_tree.path_mode.tooltip": "애니메이션에서 경로를 해석하는 방법에 대해 설명합니다. 상대 모드를 사용하면 이 오브젝트의 애니메이터에서 애니메이션을 기록할 수 있습니다.",
|
||||||
"merge_blend_tree.relative_path_root": "상대적 경로",
|
"merge_blend_tree.relative_path_root": "상대적 경로",
|
||||||
@ -96,7 +95,6 @@
|
|||||||
"worldfixed.quest": "이 컴포넌트는 안드로이드 빌드를 대응하지 않습니다.",
|
"worldfixed.quest": "이 컴포넌트는 안드로이드 빌드를 대응하지 않습니다.",
|
||||||
"worldfixed.normal": "이 오브젝트는 Constraint 을 사용하여 아바타에 고정하지 않는 이상 월드축에 고정됩니다.",
|
"worldfixed.normal": "이 오브젝트는 Constraint 을 사용하여 아바타에 고정하지 않는 이상 월드축에 고정됩니다.",
|
||||||
"fpvisible.normal": "이 오브젝트는 일인칭 시점에서 보일 것입니다.",
|
"fpvisible.normal": "이 오브젝트는 일인칭 시점에서 보일 것입니다.",
|
||||||
"fpvisible.NotUnderHead": "Head 본(Bone) 내의 오브젝트가 아닌 경우 작동하지 않습니다.",
|
|
||||||
"fpvisible.quest": "이 컴포넌트는 오큘러스 퀘스트 단독 버전과 호환되지 않으며 영향을 미치지 않습니다.",
|
"fpvisible.quest": "이 컴포넌트는 오큘러스 퀘스트 단독 버전과 호환되지 않으며 영향을 미치지 않습니다.",
|
||||||
"fpvisible.InPhysBoneChain": "이 객체는 Physics Bone 체인에 의해 컨트롤되므로 체인의 세부적인 선택이 불가능합니다. 대신 체인의 시작 부분을 선택하세요.",
|
"fpvisible.InPhysBoneChain": "이 객체는 Physics Bone 체인에 의해 컨트롤되므로 체인의 세부적인 선택이 불가능합니다. 대신 체인의 시작 부분을 선택하세요.",
|
||||||
"blendshape.mesh": "메시",
|
"blendshape.mesh": "메시",
|
||||||
|
@ -1,108 +1,114 @@
|
|||||||
{
|
{
|
||||||
|
"test0.test_a": "test_a",
|
||||||
|
"test0.test_b": "test_b",
|
||||||
"boneproxy.foldout.advanced": "高级设置",
|
"boneproxy.foldout.advanced": "高级设置",
|
||||||
"boneproxy.target": "目标",
|
"boneproxy.target": "目标",
|
||||||
"menuinstall.help.hint_set_menu": "此预制件的菜单默认会安装到 Avatar 的顶部菜单中。如果不需要,可以选择其他菜单或取消勾选此组件。",
|
"menuinstall.help.hint_set_menu": "此预制件的菜单默认会安装到 Avatar 的顶部菜单中。如果不需要,可以选择其他菜单或取消勾选此组件。",
|
||||||
"menuinstall.help.hint_bad_menu": "选择的菜单不属于此 Avatar。",
|
"menuinstall.help.hint_bad_menu": "选择的菜单不属于此 Avatar。",
|
||||||
"menuinstall.installto": "安装到",
|
"menuinstall.installto": "安装到",
|
||||||
"menuinstall.installto.tooltip": "当前预制件的菜单将会安装到此菜单",
|
"menuinstall.installto.tooltip": "当前预制件包含的菜单将会安装到此菜单中",
|
||||||
"menuinstall.selectmenu": "选择菜单",
|
"menuinstall.selectmenu": "选择菜单",
|
||||||
"menuinstall.showcontents": "显示菜单内容",
|
"menuinstall.showcontents": "显示菜单内容",
|
||||||
"menuinstall.showcontents.notselected": "未选择菜单",
|
"menuinstall.showcontents.notselected": "未选择菜单",
|
||||||
"menuinstall.devoptions": "预制件开发者选项",
|
"menuinstall.devoptions": "预制件开发者选项",
|
||||||
"menuinstall.menu_icon_too_large": "菜单图标过大,图标应小于 256 像素。",
|
"menuinstall.menu_icon_too_large": "菜单图标过大,图标应小于 256 像素。",
|
||||||
"menuinstall.menu_icon_uncompressed": "菜单图标未设置压缩。",
|
"menuinstall.menu_icon_uncompressed": "菜单图标未设置压缩。",
|
||||||
"menuinstall.srcmenu": "安装的菜单",
|
"menuinstall.srcmenu": "要安装的菜单",
|
||||||
"params.syncmode.NotSynced": "仅动画控制器(不同步)",
|
"params.syncmode.NotSynced": "仅 Animator 内部(不同步)",
|
||||||
"params.syncmode.Int": "Int",
|
"params.syncmode.Int": "Int",
|
||||||
"params.syncmode.Float": "Float",
|
"params.syncmode.Float": "Float",
|
||||||
"params.syncmode.Bool": "Bool",
|
"params.syncmode.Bool": "Bool",
|
||||||
|
"params.syncmode.PhysBonesPrefix": "PB 前缀",
|
||||||
"params.__comment__": "=== Unity 2019 only strings ===",
|
"params.__comment__": "=== Unity 2019 only strings ===",
|
||||||
"params.autodetect_header": " 自动检测参数 ",
|
"params.autodetect_header": " 自动检测参数 ",
|
||||||
"params.internal": "内部",
|
"params.internal": "内部",
|
||||||
"params.pb_prefix": "PhysBones 前缀",
|
"params.pb_prefix": "PhysBones 前缀",
|
||||||
"params.syncmode": "参数模式",
|
"params.syncmode": "参数类型",
|
||||||
"params.saved": "已保存",
|
"params.saved": "保存",
|
||||||
"params.synced": "已同步",
|
"params.synced": "同步",
|
||||||
"params.default": "默认值",
|
"params.default": "默认值",
|
||||||
"params.fieldname": "字段名",
|
"params.fieldname": "字段名",
|
||||||
"params.remapto": "字段名映射到",
|
"params.remapto": "字段名映射到",
|
||||||
"params.remapto.tooltip": "输入新的字段名以防止参数名称冲突",
|
"params.remapto.tooltip": "输入新的名称以防止参数名称冲突",
|
||||||
"params.devmode": "显示预制件开发者选项",
|
"params.devmode": "显示预制件开发者选项",
|
||||||
"params.__comment1__": "=== Unity 2022 only strings ===",
|
"params.__comment1__": "=== Unity 2022 only strings ===",
|
||||||
"merge_parameter.ui.name": "参数名称",
|
"merge_parameter.ui.name": "参数名称",
|
||||||
"merge_parameter.ui.prefix": "PhysBone 前缀名称",
|
"merge_parameter.ui.prefix": "PhysBone 前缀名称",
|
||||||
"merge_parameter.ui.remapTo": "将名称更改为",
|
"merge_parameter.ui.remapTo": "将名称更改为",
|
||||||
"merge_parameter.ui.remapTo.tooltip": "在这里输入一个新名称以重命名此参数或前缀。这可以用来解决名称冲突或用以连接各种功能模块。",
|
"merge_parameter.ui.remapTo.tooltip": "在这里输入一个新名称以重新命名此参数或前缀。这可以用来解决名称冲突或链接多种功能模块。",
|
||||||
"merge_parameter.ui.remapTo.automatic": "(自动锁定)",
|
"merge_parameter.ui.remapTo.automatic": "(自动锁定)",
|
||||||
"merge_parameter.ui.defaultValue": "默认值",
|
"merge_parameter.ui.defaultValue": "默认值",
|
||||||
"merge_parameter.ui.defaultValue.tooltip": "当重置 Avatar 或第一次使用 Avatar 时,此参数将会被设置为该值",
|
"merge_parameter.ui.defaultValue.tooltip": "首次使用或重置 Avatar 时,参数将会被设置为此值",
|
||||||
"merge_parameter.ui.saved": "保存",
|
"merge_parameter.ui.saved": "保存",
|
||||||
"merge_parameter.ui.saved.tooltip": "如果勾选,当您更换 Avatar 或房间时,此参数的值将会被保存。",
|
"merge_parameter.ui.saved.tooltip": "如果勾选,当你更换 Avatar 或房间时,参数的值将被保存。",
|
||||||
"merge_parameter.ui.internalParameter": "自动重命名",
|
"merge_parameter.ui.internalParameter": "自动重命名",
|
||||||
"merge_parameter.ui.internalParameter.tooltip": "如果勾选,此参数将会被自动重命名以防止参数名称冲突",
|
"merge_parameter.ui.internalParameter.tooltip": "如果勾选,此参数将会自动重命名以防止其名称和其他参数冲突",
|
||||||
"merge_parameter.ui.isPrefix": "是 PhysBone 的前缀",
|
"merge_parameter.ui.isPrefix": "是 PhysBone 前缀",
|
||||||
"merge_parameter.ui.syncType": "参数模式",
|
"merge_parameter.ui.syncType": "参数类型",
|
||||||
|
"merge_parameter.ui.synced": "同步",
|
||||||
|
"merge_parameter.ui.synced.tooltip": "如果勾选,参数将会在网络上同步",
|
||||||
"merge_parameter.ui.unregistered_foldout": "未注册的参数",
|
"merge_parameter.ui.unregistered_foldout": "未注册的参数",
|
||||||
"merge_parameter.ui.add_button": "添加",
|
"merge_parameter.ui.add_button": "添加",
|
||||||
"merge_parameter.ui.details": "参数设置",
|
"merge_parameter.ui.details": "参数设置",
|
||||||
"merge_parameter.ui.overrideAnimatorDefaults": "覆盖动画控制器的默认值",
|
"merge_parameter.ui.overrideAnimatorDefaults": "覆盖 Animator 默认值",
|
||||||
|
"merge_parameter.ui.importFromAsset": "从 Assets 导入",
|
||||||
"merge_armature.merge_target": "合并目标",
|
"merge_armature.merge_target": "合并目标",
|
||||||
"merge_armature.merge_target.tooltip": "将当前对象合并到合并目标中",
|
"merge_armature.merge_target.tooltip": "当前对象要合并到的骨架(或其子级)",
|
||||||
"merge_armature.prefix": "骨骼前缀",
|
"merge_armature.prefix": "骨骼前缀",
|
||||||
"merge_armature.prefix.tooltip": "合并目标的骨骼前缀",
|
"merge_armature.prefix.tooltip": "当前对象里,要合并的骨骼的前缀",
|
||||||
"merge_armature.suffix": "骨骼后缀",
|
"merge_armature.suffix": "骨骼后缀",
|
||||||
"merge_armature.suffix.tooltip": "合并目标的骨骼后缀",
|
"merge_armature.suffix.tooltip": "当前对象里,要合并的骨骼的后缀",
|
||||||
"merge_armature.locked": "锁定位置",
|
"merge_armature.locked": "锁定位置",
|
||||||
"merge_armature.locked.tooltip": "将当前对象的骨架与合并目标的骨架锁定,用于动画的创建。",
|
"merge_armature.locked.tooltip": "将当前对象的骨架与合并目标的骨架锁定(反之亦然),常用于创建动画。",
|
||||||
"merge_armature.adjust_names": "根据合并目标调整骨骼名称",
|
"merge_armature.adjust_names": "根据合并目标调整骨骼名称",
|
||||||
"merge_armature.adjust_names.tooltip": "根据合并目标调整骨骼名称,通常用于不适配当前 Avatar 的服装。",
|
"merge_armature.adjust_names.tooltip": "根据合并目标调整骨骼名称,通常用于不适配当前 Avatar 的服装。",
|
||||||
"merge_armature.mangle_names": "避免命名冲突",
|
"merge_armature.mangle_names": "避免名称冲突",
|
||||||
"merge_armature.mangle_names.tooltip": "通过重新命名新添加的骨骼来避免与其他资源发生命名冲突。",
|
"merge_armature.mangle_names.tooltip": "通过重命名新添加的骨骼来避免与其他资源发生名称冲突。",
|
||||||
"path_mode.Relative": "相对路径(基于当前对象)",
|
"path_mode.Relative": "相对路径(基于当前对象)",
|
||||||
"path_mode.Absolute": "绝对路径(基于 Avatar 的 Root)",
|
"path_mode.Absolute": "绝对路径(基于 Avatar 的根对象)",
|
||||||
"merge_animator.animator": "合并的目标控制器",
|
"merge_animator.animator": "要合并的 Animator",
|
||||||
"merge_animator.layer_type": "Layer 类型",
|
"merge_animator.layer_type": "Layer 类型",
|
||||||
"merge_animator.delete_attached_animator": "删除额外的动画控制器",
|
"merge_animator.delete_attached_animator": "删除额外的 Animator",
|
||||||
"merge_animator.delete_attached_animator.tooltip": "合并后删除当前对象上的动画控制器",
|
"merge_animator.delete_attached_animator.tooltip": "合并后删除当前对象上的 Animator 组件",
|
||||||
"merge_animator.path_mode": "路径模式",
|
"merge_animator.path_mode": "路径模式",
|
||||||
"merge_animator.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让您在当前对象上录制动画。",
|
"merge_animator.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让你在当前对象上录制动画。",
|
||||||
"merge_animator.match_avatar_write_defaults": "匹配 Avatar 的 Write Defaults 设置",
|
"merge_animator.match_avatar_write_defaults": "匹配 Avatar 的 Write Defaults 设置",
|
||||||
"merge_animator.match_avatar_write_defaults.tooltip": "使用与 Avatar 一样的 Write Defaults 设置。\n如果设置存在冲突,将保留目标控制器的设置。",
|
"merge_animator.match_avatar_write_defaults.tooltip": "使用与 Avatar 一样的 Write Defaults 设置。\n如果 Avatar 将 WD 混用,Animator 的 WD 设置将维持不变。",
|
||||||
"merge_animator.relative_path_root": "相对路径根对象",
|
"merge_animator.relative_path_root": "相对路径根对象",
|
||||||
"merge_animator.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定,则使用当前对象。",
|
"merge_animator.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定,则使用当前对象。",
|
||||||
"merge_animator.layer_priority": "动画层合并优先级",
|
"merge_animator.layer_priority": "Animator layer 的合并优先级",
|
||||||
"merge_animator.layer_priority.tooltip": "控制动画层合并到目标控制器中的顺序,\n由小到大。负值将合并于原有动画层之前,\n0 或正值将使其合并于原有动画层之后。",
|
"merge_animator.layer_priority.tooltip": "控制动画层合并后在 Animator 里的位置,\n由小到大。负值将合并于原有动画层之前,\n0 或正值将合并于之后。",
|
||||||
"merge_armature.lockmode": "位置同步模式",
|
"merge_armature.lockmode": "位置同步模式",
|
||||||
"merge_armature.lockmode.not_locked.title": "不同步",
|
"merge_armature.lockmode.not_locked.title": "不同步",
|
||||||
"merge_armature.lockmode.not_locked.body": "Avatar 骨骼和合并目标骨骼不进行位置同步",
|
"merge_armature.lockmode.not_locked.body": "当前对象的骨骼不会和 Avatar 的骨骼位置同步。",
|
||||||
"merge_armature.lockmode.base_to_merge.title": "Avatar =====> 合并目标(单向)",
|
"merge_armature.lockmode.base_to_merge.title": "Avatar =====> 目标(单向)",
|
||||||
"merge_armature.lockmode.base_to_merge.body": "Avatar 骨骼位置改变,合并目标骨骼也会改变。\n合并目标骨骼位置改变,Avatar 骨骼不会改变。\n建议一般服装使用此模式,因为此模式允许调整合并目标的骨骼。",
|
"merge_armature.lockmode.base_to_merge.body": "Avatar 骨骼位置改变,当前对象的骨骼也会改变。\n但当当前对象的骨骼位置改变时,Avatar 骨骼不会改变。\n建议一般服装使用此模式,因为此模式允许调整服装的骨骼。",
|
||||||
"merge_armature.lockmode.bidirectional.title": "Avatar <=====> 合并目标(双向)",
|
"merge_armature.lockmode.bidirectional.title": "Avatar <=====> 目标(双向)",
|
||||||
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和合并目标骨骼的位置始终相同。\n此模式对创建基于 Avatar 骨骼的动画时非常有用。\n启用此模式要求 Avatar 骨骼与合并目标骨骼位置完全相同。",
|
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和当前对象的骨骼的位置始终相同。\n此模式对创建基于 Avatar 骨骼的动画时非常有用。\n启用此模式要求 Avatar 骨骼和当前对象的骨骼位置完全相同。",
|
||||||
"merge_armature.reset_pos": "将位置与 Avatar 进行对齐",
|
"merge_armature.reset_pos": "将位置与 Avatar 进行对齐",
|
||||||
"merge_armature.reset_pos.info": "此命令将强制服装骨骼与 Avatar 骨骼进行对齐,在穿戴不适配当前 Avatar 的服装时可能有帮助。",
|
"merge_armature.reset_pos.info": "此命令将强制服装骨骼与 Avatar 骨骼进行对齐,在穿戴不适配当前 Avatar 的服装时可能有帮助。",
|
||||||
|
"merge_armature.reset_pos.convert_atpose": "转换 A-Pose/T-Pose 以匹配 Base Avatar",
|
||||||
"merge_armature.reset_pos.adjust_rotation": "也对齐旋转",
|
"merge_armature.reset_pos.adjust_rotation": "也对齐旋转",
|
||||||
"merge_armature.reset_pos.adjust_scale": "也对齐缩放",
|
"merge_armature.reset_pos.adjust_scale": "也对齐缩放",
|
||||||
"merge_armature.reset_pos.execute": "执行",
|
"merge_armature.reset_pos.execute": "执行!",
|
||||||
"merge_armature.reset_pos.heuristic_scale": "根据 Avatar 调整服装的整体比例",
|
"merge_armature.reset_pos.heuristic_scale": "根据 Avatar 调整服装的整体比例",
|
||||||
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作为参考,调整服装的整体比例。\n推荐用于不适配当前 Avatar 的服装。",
|
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作为参考,调整服装的整体比例。\n推荐用于不适配当前 Avatar 的服装。",
|
||||||
"merge_blend_tree.blend_tree": "BlendTree",
|
|
||||||
"merge_blend_tree.path_mode": "路径模式",
|
"merge_blend_tree.path_mode": "路径模式",
|
||||||
"merge_blend_tree.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让您在当前对象上录制动画。",
|
"merge_blend_tree.path_mode.tooltip": "在动画中路径的工作模式。\n使用相对路径可以让你在当前对象上录制动画。",
|
||||||
"merge_blend_tree.relative_path_root": "相对路径根对象",
|
"merge_blend_tree.relative_path_root": "相对路径根对象",
|
||||||
"merge_blend_tree.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定,则使用当前对象。",
|
"merge_blend_tree.relative_path_root.tooltip": "解析相对路径时使用的根对象。\n如果未指定,则使用当前对象。",
|
||||||
"worldfixed.quest": "此组件未生效,因为它与 Oculus Quest 不兼容。",
|
"worldfixed.quest": "此组件未生效,因为它与 Android 环境不兼容。",
|
||||||
"worldfixed.normal": "当前对象将会固定于世界,除非您使用约束将它绑定在 Avatar 内。",
|
"worldfixed.normal": "当前对象将会固定于世界,除非你使用约束将它绑定在 Avatar 内。",
|
||||||
"fpvisible.normal": "当前对象将在第一人称视角中可见。",
|
"fpvisible.normal": "当前对象将在第一人称视角中可见。",
|
||||||
"fpvisible.NotUnderHead": "此组件未生效,因为它需要放置在 Head 骨骼下。",
|
"fpvisible.NotUnderHead": "当这个组件不被放置在“Head”骨骼下方时不会生效。\n如果这个组件将通过 Bone Proxy 等方式放置在 “Head”骨骼下,那么这个警告也许是可以被忽略的。",
|
||||||
"fpvisible.quest": "此组件未生效,因为它与 Oculus Quest 不兼容。",
|
"fpvisible.quest": "此组件未生效,因为它与 Android 环境不兼容。",
|
||||||
"fpvisible.InPhysBoneChain": "当前对象由 PhysicsBone 控制,可能无法在第一人称视角中可见;请指定 PhysicsBone 链的起点。",
|
"fpvisible.InPhysBoneChain": "当前对象由 PhysBones 控制,可能无法在第一人称视角中可见;请指定 Physics Bone 链的起点。",
|
||||||
"blendshape.mesh": "网格",
|
"blendshape.mesh": "网格",
|
||||||
"blendshape.source": "源 blendshape",
|
"blendshape.source": "源 blendshape",
|
||||||
"blendshape.target": "目标 blendshape",
|
"blendshape.target": "目标 blendshape",
|
||||||
"hint.not_in_avatar": "此组件需要放置于您的 Avatar 内才能工作",
|
"hint.not_in_avatar": "此组件需要放置于你的 Avatar 内才能工作。",
|
||||||
"boneproxy.err.MovingTarget": "您不能指定将由其他 Modular Avatar 组件移动的目标对象",
|
"boneproxy.err.MovingTarget": "你不能指定将由其他 Modular Avatar 组件移动的对象",
|
||||||
"boneproxy.err.NotInAvatar": "您必须指定一个在 Avatar 内的对象",
|
"boneproxy.err.NotInAvatar": "你必须指定一个在 Avatar 内的对象",
|
||||||
"boneproxy.attachment": "附加模式",
|
"boneproxy.attachment": "附加模式",
|
||||||
"boneproxy.attachment.AsChildAtRoot": "作为子级,放置于 Root",
|
"boneproxy.attachment.AsChildAtRoot": "作为子级,放置于 Root",
|
||||||
"boneproxy.attachment.AsChildKeepWorldPose": "作为子级,保持原有位置和旋转",
|
"boneproxy.attachment.AsChildKeepWorldPose": "作为子级,保持原有位置和旋转",
|
||||||
@ -111,64 +117,70 @@
|
|||||||
"mesh_settings.header_probe_anchor": "锚点覆盖设置",
|
"mesh_settings.header_probe_anchor": "锚点覆盖设置",
|
||||||
"mesh_settings.inherit_probe_anchor": "锚点覆盖模式",
|
"mesh_settings.inherit_probe_anchor": "锚点覆盖模式",
|
||||||
"mesh_settings.probe_anchor": "锚点覆盖",
|
"mesh_settings.probe_anchor": "锚点覆盖",
|
||||||
"mesh_settings.probe_anchor.tooltip": "Anchor Override,\n设置用于当前对象和子对象内的渲染器的锚点覆盖。",
|
"mesh_settings.probe_anchor.tooltip": "设置用于当前对象和子对象内的渲染器的锚点覆盖",
|
||||||
"mesh_settings.header_bounds": "网格边界设置",
|
"mesh_settings.header_bounds": "网格边界覆盖设置",
|
||||||
"mesh_settings.inherit_bounds": "网格边界模式",
|
"mesh_settings.inherit_bounds": "网格边界覆盖模式",
|
||||||
"mesh_settings.root_bone": "根骨骼",
|
"mesh_settings.root_bone": "根骨骼",
|
||||||
"mesh_settings.root_bone.tooltip": "网格的根骨骼(Root bone),\n这被用作计算网格边界的参考点。",
|
"mesh_settings.root_bone.tooltip": "网格的根骨骼(Root bone),\n这被用作计算网格边界的参考点。",
|
||||||
"mesh_settings.bounds": "网格边界",
|
"mesh_settings.bounds": "网格边界",
|
||||||
"mesh_settings.bounds.tooltip": "网格的边界(Bounds),\n用于确定何时要略过屏幕外的网格渲染。",
|
"mesh_settings.bounds.tooltip": "网格的边界(Bounds),\n用于确定何时要略过屏幕外的网格渲染。",
|
||||||
"mesh_settings.inherit_mode.Inherit": "继承",
|
"mesh_settings.inherit_mode.Inherit": "继承",
|
||||||
"mesh_settings.inherit_mode.Set": "设置",
|
"mesh_settings.inherit_mode.Set": "指定",
|
||||||
"mesh_settings.inherit_mode.DontSet": "不设置(保持原有的设置)",
|
"mesh_settings.inherit_mode.DontSet": "不指定(保持原有的设置)",
|
||||||
"pb_blocker.help": "当前对象不会受到附加到父对象的 PhysBones 的影响。",
|
"mesh_settings.inherit_mode.SetOrInherit": "父级设置优先,否则遵从指定的设置",
|
||||||
"hint.bad_vrcsdk": "检测到不兼容的 VRCSDK 版本。\n\n请尝试升级 VRCSDK;如果这不起作用,请尝试更新 Modular Avatar。",
|
"pb_blocker.help": "当前对象不会受到附加在父对象的 PhysBones 影响。",
|
||||||
|
"hint.bad_vrcsdk": "检测到不兼容的 VRCSDK 版本。\n\n请尝试升级 VRCSDK;如果这不起作用,请尝试更新到新版本的 Modular Avatar。",
|
||||||
"error.stack_trace": "Stack trace(请在报告错误时提供此信息!)",
|
"error.stack_trace": "Stack trace(请在报告错误时提供此信息!)",
|
||||||
"error.merge_armature.circular_dependency": "[MA-0001] 在 Merge armature 组件中存在循环引用",
|
"error.merge_armature.circular_dependency": "[MA-0001] 在 Merge armature 组件中存在循环引用",
|
||||||
"error.merge_armature.circular_dependency:description": "您的 Merge Armature 组件正在将自身或其子级作为合并目标。",
|
"error.merge_armature.circular_dependency:description": "你的 Merge Armature 组件正在将自身或其子级作为合并目标。",
|
||||||
"error.merge_armature.circular_dependency:hint": "通常应在 Merge Armature 的合并目标字段指定 Avatar 本身的骨骼。不要指定服装本身!",
|
"error.merge_armature.circular_dependency:hint": "通常应在 Merge Armature 组件的「合并目标」字段上指定 Avatar 本身的 Armture。不要指定服装本身!",
|
||||||
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] 在 Humanoid 骨骼上有 PhysBones 组件",
|
"error.merge_armature.physbone_on_humanoid_bone": "[MA-0002] 在 Humanoid 骨骼上存在 PhysBones 组件",
|
||||||
"error.merge_armature.physbone_on_humanoid_bone:hint": "在要合并的骨架中,某些 Humanoid 骨骼受到 PhysBones 控制,其位置与合并目标中相应的 Humanoid 骨骼不同。您应该在要合并的骨架中,移除这些 Humanoid 骨骼上的 PhysBones。",
|
"error.merge_armature.physbone_on_humanoid_bone:hint": "在要合并的骨架中,某些 Humanoid 骨骼受到 PhysBones 控制,其位置与合并目标中相应的 Humanoid 骨骼不同。你应该在要合并的骨架中,移除这些 Humanoid 骨骼上的 PhysBones。",
|
||||||
|
"error.merge_blend_tree.missing_tree": "[MA-0009] 未指定 Blend Tree",
|
||||||
|
"error.merge_blend_tree.missing_tree:hint": "Merge Blend Tree 需要知道要合并到哪个 Blend Tree。请尝试在「Blend Tree」中设置一个对象。",
|
||||||
"error.internal_error": "[MA-9999] 发生内部错误:{0}\nwhen processing:",
|
"error.internal_error": "[MA-9999] 发生内部错误:{0}\nwhen processing:",
|
||||||
"error.merge_animator.param_type_mismatch": "[MA-0003] 参数类型不符",
|
"error.merge_animator.param_type_mismatch": "[MA-0003] 参数类型不符",
|
||||||
"error.merge_animator.param_type_mismatch:description": "参数 {0} 具有多种类型:{1} != {2}",
|
"error.merge_animator.param_type_mismatch:description": "参数 {0} 具有多种类型:{1} != {2}",
|
||||||
"error.rename_params.too_many_synced_params": "同步参数超出限制:Cost {0} > {1}",
|
"error.rename_params.too_many_synced_params": "同步参数超出限制:Cost {0} > {1}",
|
||||||
"error.rename_params.too_many_synced_params:description": "您在 Avatar 中使用了太多的同步参数。您已使用 {0} bits 的参数,但限制为 {1}。",
|
"error.rename_params.too_many_synced_params:description": "你在 Avatar 中使用了太多的同步参数。你已使用 {0} bits 的参数,但限制为 {1}。",
|
||||||
"error.rename_params.type_conflict": "[MA-0006] 参数类型冲突",
|
"error.rename_params.type_conflict": "[MA-0006] 参数类型冲突",
|
||||||
"error.rename_params.type_conflict:description": "参数 {0} 指定了多种类型:{1} != {2}",
|
"error.rename_params.type_conflict:description": "参数 {0} 指定了多种类型:{1} != {2}",
|
||||||
"error.rename_params.default_value_conflict": "[MA-0007] 默认值冲突",
|
"error.rename_params.default_value_conflict": "[MA-0007] 默认值冲突",
|
||||||
"error.rename_params.default_value_conflict:description": "参数 {0} 指定了多个默认值:{1} != {2}",
|
"error.rename_params.default_value_conflict:description": "参数 {0} 指定了多个默认值:{1} != {2}",
|
||||||
"error.rename_params.default_value_conflict:hint": "为了避免不可预测的行为,请将 MA Parameters 组件中所有默认值留空,只留一个。如果存在多个值,Modular Avatar 将选择第一个默认值。",
|
"error.rename_params.default_value_conflict:hint": "为了避免不可预知的行为,请将 MA Parameters 组件中所有重复的默认值置空,只留一个相关值即可。如果存在多个值,Modular Avatar 将选择第一个默认值。",
|
||||||
"error.replace_object.null_target": "[MA-0008] 未指定替代对象",
|
"error.replace_object.null_target": "[MA-0008] 未指定要替换的对象",
|
||||||
"error.replace_object.null_target:hint": "Replace object 需要一个对象来替代。尝试指定一个。",
|
"error.replace_object.null_target:hint": "Replace object 需要知道要替换掉哪个对象。尝试指定一个对象再试试。",
|
||||||
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此对象上找不到 Renderer",
|
"error.replace_object.replacing_replacement": "[MA-0009] 不能在多个 Replace Object 组件中指定相同的目标对象",
|
||||||
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用于相同对象上的 Skinned Mesh Renderer。您是否将它附加到正确的对象上?",
|
"error.replace_object.parent_of_target": "[MA-0010] 目标对象不能是此对象的父级",
|
||||||
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此对象的 Renderer 上找不到网格(Mesh)",
|
"error.singleton": "[MA-0011] 一个 Avatar 中只允许存在一个 {0} 组件",
|
||||||
|
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此部件上找不到 Renderer",
|
||||||
|
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用于所在对象上的 Skinned Mesh Renderer。你是否将它附加到了正确的对象上?",
|
||||||
|
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此部件的 Renderer 上找不到网格(Mesh)",
|
||||||
"validation.blendshape_sync.no_local_mesh:hint": "当前对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
|
"validation.blendshape_sync.no_local_mesh:hint": "当前对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
|
||||||
"validation.blendshape_sync.no_bindings": "[MA-1002] 在此对象上找不到 BlendShape",
|
"validation.blendshape_sync.no_bindings": "[MA-1002] 在此部件上找不到 BlendShape",
|
||||||
"validation.blendshape_sync.no_bindings:hint": "Blendshape Sync 必须知道要同步哪些 blendshapes。点击「+」新增。",
|
"validation.blendshape_sync.no_bindings:hint": "Blendshape Sync 必须知道要同步哪些 blendshape。点击「+」新增。",
|
||||||
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 找不到目标 BlendShape:「{0}」",
|
"validation.blendshape_sync.missing_local_shape": "[MA-1003] 找不到目标 BlendShape:「{0}」",
|
||||||
"validation.blendshape_sync.missing_local_shape:description": "找不到目标 BlendShape:{0}",
|
"validation.blendshape_sync.missing_local_shape:description": "找不到目标 BlendShape:{0}",
|
||||||
"validation.blendshape_sync.missing_local_shape:hint": "配置为从目标对象「接收」值的 BlendShape 缺失。请尝试更换被标红的 Blendshape。",
|
"validation.blendshape_sync.missing_local_shape:hint": "找不到「接收」数值的 BlendShape。请更换被标为红色的 Blendshape。",
|
||||||
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 找不到源 BlendShape:「{0}」",
|
"validation.blendshape_sync.missing_target_shape": "[MA-1004] 找不到源 BlendShape:「{0}」",
|
||||||
"validation.blendshape_sync.missing_target_shape:description": "找不到源 BlendShape:{0}",
|
"validation.blendshape_sync.missing_target_shape:description": "找不到源 BlendShape:{0}",
|
||||||
"validation.blendshape_sync.missing_target_shape:hint": "配置为向本地对象「发送」值的 BlendShape 缺失。请尝试更换被标红的 Blendshape。",
|
"validation.blendshape_sync.missing_target_shape:hint": "找不到向当前部件「发送」数值的 BlendShape。请更换被标为红色的 Blendshape。",
|
||||||
"validation.blendshape_sync.no_target": "[MA-1005] 未指定目标对象(网格)",
|
"validation.blendshape_sync.no_target": "[MA-1005] 未指定目标对象(网格)",
|
||||||
"validation.blendshape_sync.no_target:hint": "Blendshape Sync 需要知道要从哪个对象同步 Blendshape。请尝试在「网格」中设置一个对象。",
|
"validation.blendshape_sync.no_target:hint": "Blendshape Sync 必须知道要从哪个对象同步 Blendshape。请尝试在「网格」中指定一个对象。",
|
||||||
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 在目标对象上找不到 Renderer",
|
"validation.blendshape_sync.missing_target_renderer": "[MA-1006] 在目标对象上找不到 Renderer",
|
||||||
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Sync 会从目标对象上的 Skinned Mesh Renderer 接收 blendshape 的值。您将它附加到正确的对象上了吗?",
|
"validation.blendshape_sync.missing_target_renderer:hint": "Blendshape Sync 会从目标对象上的 Skinned Mesh Renderer 接收 blendshape 的值。你有没有将它附加到正确的对象上呢?",
|
||||||
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 在目标对象的渲染器上找不到网格(Mesh)",
|
"validation.blendshape_sync.missing_target_mesh": "[MA-1007] 在目标对象的 Renderer 上找不到网格(Mesh)",
|
||||||
"validation.blendshape_sync.missing_target_mesh:hint": "目标对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
|
"validation.blendshape_sync.missing_target_mesh:hint": "目标对象上的 Skinned Mesh Renderer 配置可能有问题。请尝试从原预制件或 FBX 重新设置对象。",
|
||||||
"validation.bone_proxy.no_target": "[MA-1100] 未指定目标对象,或未找到目标对象",
|
"validation.bone_proxy.no_target": "[MA-1100] 未指定目标对象,或目标对象未找到",
|
||||||
"validation.bone_proxy.no_target:hint": "Bone Proxy 需要知道要将当前对象绑定到哪个目标对象。请尝试在「目标」中设置一个当前对象应该绑定到的目标对象。",
|
"validation.bone_proxy.no_target:hint": "Bone Proxy 必须知道要将当前对象绑定到哪个对象。请在「目标」中指定。",
|
||||||
"validation.menu_installer.no_menu": "[MA-1200] 未指定安装的菜单",
|
"validation.menu_installer.no_menu": "[MA-1200] 未指定要安装的菜单",
|
||||||
"validation.menu_installer.no_menu:hint": "Menu Installer 需要知道要将此预制件安装到哪个菜单。请先配置「预制件开发者选项」里的「安装的菜单」,或是新增一个「MA Menu Item」组件。",
|
"validation.menu_installer.no_menu:hint": "Menu Installer 必须知道要安装哪个菜单。请在「预制件开发者选项」里的「要安装的菜单」指定,或是新增一个「MA Menu Item」组件。",
|
||||||
"validation.merge_animator.no_animator": "[MA-1300] 未指定要合并的目标动画器",
|
"validation.merge_animator.no_animator": "[MA-1300] 未指定要合并的 Animator",
|
||||||
"validation.merge_animator.no_animator:hint": "Merge Animator 需要知道要合并到哪个动画控制器。请尝试在「合并的目标控制器」中设置一个对象。",
|
"validation.merge_animator.no_animator:hint": "Merge Animator 必须知道要合并哪个 Animator。请在「要合并的 Animator」中指定一个。",
|
||||||
"validation.merge_armature.no_target": "[MA-1400] 未指定合并目标",
|
"validation.merge_armature.no_target": "[MA-1400] 未指定合并目标",
|
||||||
"validation.merge_armature.no_target:hint": "Merge Armature 需要知道要合并到哪个骨架。请尝试在「合并目标」中设置一个对象。",
|
"validation.merge_armature.no_target:hint": "Merge Armature 必须知道相应骨骼要合并到哪个骨架里。请在「合并目标」中指定一个。",
|
||||||
"validation.merge_armature.target_is_child": "[MA-1500] 合并目标不能是此对象的子级",
|
"validation.merge_armature.target_is_child": "[MA-1500] 合并目标不能是此对象的子级",
|
||||||
"validation.merge_armature.target_is_child:hint": "Merge Armature 不能合并一个骨架到其自身。请尝试将「合并目标」设置为其他对象。",
|
"validation.merge_armature.target_is_child:hint": "Merge Armature 不能合并骨架到其自身。请将「合并目标」设置为其他对象。",
|
||||||
"submenu_source.Children": "子对象",
|
"submenu_source.Children": "子对象",
|
||||||
"submenu_source.MenuAsset": "菜单资源文件 (Expressions Menu)",
|
"submenu_source.MenuAsset": "菜单资源文件 (Expressions Menu)",
|
||||||
"menuitem.showcontents": "显示菜单内容",
|
"menuitem.showcontents": "显示菜单内容",
|
||||||
@ -178,63 +190,107 @@
|
|||||||
"menuitem.prop.type": "类型",
|
"menuitem.prop.type": "类型",
|
||||||
"menuitem.prop.type.tooltip": "此菜单项的类型",
|
"menuitem.prop.type.tooltip": "此菜单项的类型",
|
||||||
"menuitem.prop.value": "参数值",
|
"menuitem.prop.value": "参数值",
|
||||||
"menuitem.prop.value.tooltip": "菜单项触发时设置的参数值",
|
"menuitem.prop.value.tooltip": "设置菜单项触发时的参数值",
|
||||||
|
"menuitem.prop.automatic_value": "自动",
|
||||||
|
"menuitem.prop.automatic_value.tooltip": "自动将此控制项设置为唯一值",
|
||||||
"menuitem.prop.parameter": "参数",
|
"menuitem.prop.parameter": "参数",
|
||||||
"menuitem.prop.label": "名称",
|
"menuitem.prop.label": "标签",
|
||||||
"menuitem.prop.submenu_asset": "子菜单资源",
|
"menuitem.prop.submenu_asset": "子菜单资源",
|
||||||
"menuitem.prop.submenu_asset.tooltip": "用作子菜单的资源文件",
|
"menuitem.prop.submenu_asset.tooltip": "用作子菜单的资源文件 (Expressions Menu)",
|
||||||
"menuitem.prop.submenu_source": "子菜单来源",
|
"menuitem.prop.submenu_source": "子菜单来源",
|
||||||
"menuitem.prop.submenu_source.tooltip": "寻找子菜单的菜单项的方式",
|
"menuitem.prop.submenu_source.tooltip": "寻找子菜单内菜单项的方式",
|
||||||
"menuitem.prop.source_override": "源对象",
|
"menuitem.prop.source_override": "源部件覆盖",
|
||||||
"menuitem.prop.source_override.tooltip": "如果指定,此对象将被用作子菜单内容的来源。\n否则,将使用此菜单项目的子级。",
|
"menuitem.prop.source_override.tooltip": "如果指定,这个对象将被用作子菜单内容的来源。\n否则,将使用此菜单项的子级菜单作为来源。",
|
||||||
|
"menuitem.prop.is_default": "默认启用",
|
||||||
|
"menuitem.prop.is_default.tooltip": "如果勾选,初次使用或重置 Avatar 时会启用此菜单项",
|
||||||
"menuitem.prop.is_saved": "保存",
|
"menuitem.prop.is_saved": "保存",
|
||||||
"menuitem.prop.is_saved.tooltip": "如果勾选,当您更换 Avatar 或房间时,此菜单项目的值将被保存。",
|
"menuitem.prop.is_saved.tooltip": "如果勾选,当你更换 Avatar 或房间时,此菜单项的值将被保存。",
|
||||||
"menuitem.prop.is_synced": "同步",
|
"menuitem.prop.is_synced": "同步",
|
||||||
"menuitem.prop.is_synced.tooltip": "如果勾选,此菜单项目的值将和网络上的其他玩家同步。",
|
"menuitem.prop.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
|
||||||
"menuitem.param.rotation": "参数: 旋转 (Rotation)",
|
"menuitem.param.rotation": "参数: 旋转 (Rotation)",
|
||||||
"menuitem.param.rotation.tooltip": "基于此菜单项目的旋转设置的参数。",
|
"menuitem.param.rotation.tooltip": "基于此菜单项的旋转设置的参数。",
|
||||||
"menuitem.param.horizontal": "参数: 水平 (Horizontal)",
|
"menuitem.param.horizontal": "参数: 水平 (Horizontal)",
|
||||||
"menuitem.param.horizontal.tooltip": "基于摇杆的水平位置设置的参数。",
|
"menuitem.param.horizontal.tooltip": "基于摇杆的水平位置设置的参数。",
|
||||||
"menuitem.param.vertical": "参数: 垂直 (Vertical)",
|
"menuitem.param.vertical": "参数: 垂直 (Vertical)",
|
||||||
"menuitem.param.vertical.tooltip": "基于摇杆的垂直位置设置的参数。",
|
"menuitem.param.vertical.tooltip": "基于摇杆的垂直位置设置的参数。",
|
||||||
"menuitem.label.control_labels_and_params": "设置名称和参数",
|
"menuitem.label.control_labels_and_params": "设置名称和参数",
|
||||||
"menuitem.label.control_labels": "设置名称",
|
"menuitem.label.control_labels": "设置名称",
|
||||||
"menuitem.misc.multiple": "(复数设置)",
|
"menuitem.misc.multiple": "(多重设置)",
|
||||||
"menuitem.misc.no_icon": "(无图标)",
|
"menuitem.misc.no_icon": "(无图标)",
|
||||||
"menuitem.misc.extract": "提取到对象",
|
"menuitem.misc.extract": "提取为部件",
|
||||||
"menuitem.label.parameters": "参数",
|
"menuitem.label.parameters": "参数",
|
||||||
"action.toggle_object.header.object": "要显示 / 隐藏的对象",
|
"action.toggle_object.header.object": "要显示 / 隐藏的对象",
|
||||||
"action.toggle_object.header.show": "显示",
|
"action.toggle_object.header.show": "显示",
|
||||||
"menu_tree.title": "选择菜单",
|
"menu_tree.title": "选择菜单",
|
||||||
|
"menuitem.param.controlled_by_action": "<controlled by action>",
|
||||||
"control_group.foldout.actions": "Actions",
|
"control_group.foldout.actions": "Actions",
|
||||||
"control_group.foldout.menu_items": "相关菜单项",
|
"control_group.foldout.menu_items": "相关菜单项",
|
||||||
"control_group.is_saved": "保存",
|
"control_group.is_saved": "保存",
|
||||||
"control_group.is_saved.tooltip": "如果勾选,当您更换 Avatar 或世界时,此菜单项的值将被保存。",
|
"control_group.is_saved.tooltip": "如果勾选,当你更换 Avatar 或世界时,此菜单项的值将被保存。",
|
||||||
"control_group.is_synced": "同步",
|
"control_group.is_synced": "同步",
|
||||||
"control_group.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
|
"control_group.is_synced.tooltip": "如果勾选,此菜单项的值将和网络上的其他玩家同步。",
|
||||||
"control_group.default_value": "初始设置",
|
"control_group.default_value": "初始设置",
|
||||||
"control_group.default_value.unset": "(未选择)",
|
"control_group.default_value.unset": "(未选择)",
|
||||||
"animation_gen.duplicate_binding": "来自不同控制组的控制项尝试修改动画相同的参数。参数:{0}",
|
"animation_gen.duplicate_binding": "来自不同部件组的部件尝试动画化相同的参数。参数:{0}",
|
||||||
"animation_gen.multiple_defaults": "在同一个控制组中找到多个默认的菜单项。",
|
"animation_gen.multiple_defaults": "在同一个部件组中找到多个默认的菜单项。",
|
||||||
"menuitem.misc.add_item": "添加菜单项",
|
"menuitem.misc.add_item": "添加菜单项",
|
||||||
"replace_object.target_object": "要替换的对象",
|
"replace_object.target_object": "要替换的对象",
|
||||||
"setup_outfit.err.header.notarget": "Setup Outfit 失败",
|
"setup_outfit.err.header.notarget": "Setup Outfit 失败",
|
||||||
"setup_outfit.err.header": "对 {0} 进行 Setup Outfit 操作失败",
|
"setup_outfit.err.header": "对 {0} 进行 Setup Outfit 操作失败",
|
||||||
"setup_outfit.err.unknown": "未知错误",
|
"setup_outfit.err.unknown": "未知错误",
|
||||||
"setup_outfit.err.no_selection": "没有选择对象。",
|
"setup_outfit.err.no_selection": "没有选择对象。",
|
||||||
"setup_outfit.err.run_on_avatar_itself": "Setup outfit 必须在服装对象上进行,而不是在 Avatar 本身。\n\n您要制作「混合 Avatar」吗?如果是,请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
|
"setup_outfit.err.run_on_avatar_itself": "Setup outfit 必须在服装对象上运行,而不是在 Avatar 本身。\n\n你是要制作「混合 Avatar」吗?如果是,请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
|
||||||
"setup_outfit.err.multiple_avatar_descriptors": "在 {0} 和其父级中有多个 Avatar descriptor。\n\n您要制作「混合 Avatar」吗?如果是,请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
|
"setup_outfit.err.multiple_avatar_descriptors": "在 {0} 和其父级中有多个 Avatar descriptor。\n\n你是要制作「混合 Avatar」吗?如果是,请从「内层 Avatar」中移除 Avatar descriptor 组件,然后对其执行 Setup outfit。",
|
||||||
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父级中找不到 VRC Avatar Descriptor。请确保您的服装放置在 Avatar 内。",
|
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父级中找不到 VRC Avatar Descriptor。请确认你的服装是否被正确放置在 Avatar 里。",
|
||||||
"setup_outfit.err.no_animator": "您的 Avatar 没有动画控制器 (Animator) 组件。",
|
"setup_outfit.err.no_animator": "你的 Avatar 没有 Animator 组件。",
|
||||||
"setup_outfit.err.no_hips": "您的 Avatar 没有 Hips 骨骼。Setup Outfit 只能用于 humanoid Avatars。",
|
"setup_outfit.err.no_hips": "你的 Avatar 没有 Hips 骨骼。Setup Outfit 只能用于人形 (humanoid) 的 Avatar。",
|
||||||
"setup_outfit.err.no_outfit_hips": "无法识别服装的 Hips,已搜索包含以下名称的对象:",
|
"setup_outfit.err.no_outfit_hips": "无法识别服装的 Hips 骨骼,已尝试搜索包含以下名称的对象:",
|
||||||
"move_independently.group-header": "要一起移动的对象",
|
"move_independently.group-header": "要一起移动的对象",
|
||||||
"scale_adjuster.scale": "调整比例",
|
"scale_adjuster.scale": "调整比例",
|
||||||
"scale_adjuster.adjust_children": "调整子级的位置",
|
"scale_adjuster.adjust_children": "调整子级的位置",
|
||||||
"world_fixed_object.err.unsupported_platform": "此平台不支持 World Fixed Object 组件。",
|
"world_fixed_object.err.unsupported_platform": "此平台不支持 World Fixed Object 组件。",
|
||||||
"world_fixed_object.err.unsupported_platform:description": "此组件未生效。因为在 Android 平台下不支持 World Fixed Object。",
|
"world_fixed_object.err.unsupported_platform:description": "此组件未生效。因为在 Android 环境下不支持 World Fixed Object。",
|
||||||
"ma_info.param_usage_ui.header": "Expressions 参数使用情况",
|
"ma_info.param_usage_ui.header": "Expressions 参数使用情况",
|
||||||
"ma_info.param_usage_ui.other_objects": "此 Avatar 上的其他对象",
|
"ma_info.param_usage_ui.other_objects": "此 Avatar 中的其他东西",
|
||||||
"ma_info.param_usage_ui.free_space": "未使用的参数空间 ({0} bits)",
|
"ma_info.param_usage_ui.free_space": "未使用的参数 ({0} bits)",
|
||||||
"ma_info.param_usage_ui.bits_template": "{0} ({1} bits)"
|
"ma_info.param_usage_ui.bits_template": "{0} ({1} bits)",
|
||||||
|
"ma_info.param_usage_ui.no_data": "【无信息】",
|
||||||
|
"reactive_object.inverse": "反转条件",
|
||||||
|
"reactive_object.material-setter.set-to": "将材质设置为:",
|
||||||
|
"menuitem.misc.add_toggle": "新增开关",
|
||||||
|
"ro_sim.open_debugger_button": "开启 Reaction 调试工具",
|
||||||
|
"ro_sim.window.title": "MA 响应调试工具",
|
||||||
|
"ro_sim.header.inspecting": "检查部件",
|
||||||
|
"ro_sim.header.clear_overrides": "清除所有覆盖",
|
||||||
|
"ro_sim.header.object_state": "对象状态",
|
||||||
|
"ro_sim.state.active": "启用",
|
||||||
|
"ro_sim.state.inactive": "停用",
|
||||||
|
"ro_sim.header.override_gameobject_state": "覆写对象状态",
|
||||||
|
"ro_sim.header.override_menuitem_state": "覆写菜单项状态",
|
||||||
|
"ro_sim.affected_by.title": "受到以下影响:",
|
||||||
|
"ro_sim.effect_group.component": "接收组件",
|
||||||
|
"ro_sim.effect_group.controls_obj_state": "控制对象状态为:",
|
||||||
|
"ro_sim.effect_group.target_component": "目标组件",
|
||||||
|
"ro_sim.effect_group.target_component.tooltip": "受到 Reactive Component 影响的组件",
|
||||||
|
"ro_sim.effect_group.property": "属性",
|
||||||
|
"ro_sim.effect_group.property.tooltip": "受到 Reactive Component 影响的目标组件属性",
|
||||||
|
"ro_sim.effect_group.value": "值",
|
||||||
|
"ro_sim.effect_group.value.tooltip": "Reactive Component 启用时,属性将被设为此值",
|
||||||
|
"ro_sim.effect_group.material": "材质",
|
||||||
|
"ro_sim.effect_group.material.tooltip": "Reactive Component 启用时,将被设在目标组件上的材质",
|
||||||
|
"ro_sim.effect_group.rule_inverted": "规则的条件已反转",
|
||||||
|
"ro_sim.effect_group.rule_inverted.tooltip": "这条规则将在未达成其任一条件时应用。",
|
||||||
|
"ro_sim.effect_group.conditions": "条件",
|
||||||
|
"menuitem.label.long_name.tooltip": "使用可能包含富文本和间隔线的长名称。",
|
||||||
|
"menuitem.label.gameobject_name.tooltip": "使用对象的名称。",
|
||||||
|
"remove-vertex-color.mode": "模式",
|
||||||
|
"remove-vertex-color.mode.Remove": "删除顶点颜色",
|
||||||
|
"remove-vertex-color.mode.DontRemove": "保留顶点颜色",
|
||||||
|
"general.vrcsdk-required": "此组件需要 VRCSDK 才能工作。",
|
||||||
|
"sync-param-sequence.platform": "主平台",
|
||||||
|
"sync-param-sequence.platform.tooltip": "此平台构建时,Modular Avatar将记录所有参数,供其他平台使用",
|
||||||
|
"sync-param-sequence.parameters": "共用参数的 Asset",
|
||||||
|
"sync-param-sequence.parameters.tooltip": "用于存储共用参数的 Assets。请勿使用已经在 Avatar descriptor 中设置的相同参数 。",
|
||||||
|
"sync-param-sequence.create-asset": "新增",
|
||||||
|
"sync-param-sequence.create-asset.tooltip": "创建一个新的 expression parameters"
|
||||||
}
|
}
|
@ -51,6 +51,7 @@
|
|||||||
"merge_parameter.ui.add_button": "添加",
|
"merge_parameter.ui.add_button": "添加",
|
||||||
"merge_parameter.ui.details": "參數設定",
|
"merge_parameter.ui.details": "參數設定",
|
||||||
"merge_parameter.ui.overrideAnimatorDefaults": "覆蓋 Animator 預設值",
|
"merge_parameter.ui.overrideAnimatorDefaults": "覆蓋 Animator 預設值",
|
||||||
|
"merge_parameter.ui.importFromAsset": "從 Assets 匯入",
|
||||||
"merge_armature.merge_target": "合併目標",
|
"merge_armature.merge_target": "合併目標",
|
||||||
"merge_armature.merge_target.tooltip": "當前物件要合併到的骨架(或其子級)",
|
"merge_armature.merge_target.tooltip": "當前物件要合併到的骨架(或其子級)",
|
||||||
"merge_armature.prefix": "骨骼前綴",
|
"merge_armature.prefix": "骨骼前綴",
|
||||||
@ -86,12 +87,12 @@
|
|||||||
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和當前物件的骨骼的位置始終相同。\n此模式對創建基於 Avatar 骨骼的動畫時非常有用。\n啟用此模式要求 Avatar 骨骼和當前物件的骨骼位置完全相同。",
|
"merge_armature.lockmode.bidirectional.body": "Avatar 骨骼和當前物件的骨骼的位置始終相同。\n此模式對創建基於 Avatar 骨骼的動畫時非常有用。\n啟用此模式要求 Avatar 骨骼和當前物件的骨骼位置完全相同。",
|
||||||
"merge_armature.reset_pos": "將位置與 Avatar 進行對齊",
|
"merge_armature.reset_pos": "將位置與 Avatar 進行對齊",
|
||||||
"merge_armature.reset_pos.info": "此命令將強制服裝骨骼與 Avatar 骨骼進行對齊,在使用非 Avatar 對應的服裝時可能有幫助。",
|
"merge_armature.reset_pos.info": "此命令將強制服裝骨骼與 Avatar 骨骼進行對齊,在使用非 Avatar 對應的服裝時可能有幫助。",
|
||||||
|
"merge_armature.reset_pos.convert_atpose": "轉換 A-Pose/T-Pose 以符合角色",
|
||||||
"merge_armature.reset_pos.adjust_rotation": "也對齊旋轉",
|
"merge_armature.reset_pos.adjust_rotation": "也對齊旋轉",
|
||||||
"merge_armature.reset_pos.adjust_scale": "也對齊縮放",
|
"merge_armature.reset_pos.adjust_scale": "也對齊縮放",
|
||||||
"merge_armature.reset_pos.execute": "執行",
|
"merge_armature.reset_pos.execute": "執行",
|
||||||
"merge_armature.reset_pos.heuristic_scale": "根據 Avatar 調整服裝的整體比例",
|
"merge_armature.reset_pos.heuristic_scale": "根據 Avatar 調整服裝的整體比例",
|
||||||
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作為參考,調整服裝的整體比例。\n推薦用於非 Avatar 對應的服裝。",
|
"merge_armature.reset_pos.heuristic_scale.tooltip": "以臂展作為參考,調整服裝的整體比例。\n推薦用於非 Avatar 對應的服裝。",
|
||||||
"merge_blend_tree.blend_tree": "Blend Tree",
|
|
||||||
"merge_blend_tree.path_mode": "路徑模式",
|
"merge_blend_tree.path_mode": "路徑模式",
|
||||||
"merge_blend_tree.path_mode.tooltip": "在動畫中路徑的工作模式。\n使用相對路徑可以讓你在當前物件上錄制動畫。",
|
"merge_blend_tree.path_mode.tooltip": "在動畫中路徑的工作模式。\n使用相對路徑可以讓你在當前物件上錄制動畫。",
|
||||||
"merge_blend_tree.relative_path_root": "相對路徑根物件",
|
"merge_blend_tree.relative_path_root": "相對路徑根物件",
|
||||||
@ -99,7 +100,7 @@
|
|||||||
"worldfixed.quest": "此元件未生效,因為它與 Android 環境不相容。",
|
"worldfixed.quest": "此元件未生效,因為它與 Android 環境不相容。",
|
||||||
"worldfixed.normal": "當前物件將會固定於世界,除非你使用約束將它綁在 Avatar 內。",
|
"worldfixed.normal": "當前物件將會固定於世界,除非你使用約束將它綁在 Avatar 內。",
|
||||||
"fpvisible.normal": "當前物件將在第一人稱視角中可見。",
|
"fpvisible.normal": "當前物件將在第一人稱視角中可見。",
|
||||||
"fpvisible.NotUnderHead": "此元件未生效,因為它需要放置在 Head 骨骼下。",
|
"fpvisible.NotUnderHead": "此元件在未放置於 Head 骨骼下時不會生效。\n如果是透過 Bone Proxy 等方式將其放於 Head 骨骼下,則可忽略此警告。",
|
||||||
"fpvisible.quest": "此元件未生效,因為它與 Android 環境不相容。",
|
"fpvisible.quest": "此元件未生效,因為它與 Android 環境不相容。",
|
||||||
"fpvisible.InPhysBoneChain": "當前物件由 Physics Bone 控制,可能無法在第一人稱視角中可見;請指定 Physics Bone 鏈的起點。",
|
"fpvisible.InPhysBoneChain": "當前物件由 Physics Bone 控制,可能無法在第一人稱視角中可見;請指定 Physics Bone 鏈的起點。",
|
||||||
"blendshape.mesh": "網格",
|
"blendshape.mesh": "網格",
|
||||||
@ -149,6 +150,9 @@
|
|||||||
"error.rename_params.default_value_conflict:hint": "為了避免不可預測的行為,請將 MA Parameters 元件中所有預設值留空,只留一個。如果存在多個值,Modular Avatar 將選擇第一個預設值。",
|
"error.rename_params.default_value_conflict:hint": "為了避免不可預測的行為,請將 MA Parameters 元件中所有預設值留空,只留一個。如果存在多個值,Modular Avatar 將選擇第一個預設值。",
|
||||||
"error.replace_object.null_target": "[MA-0008] 未指定要替換的物件",
|
"error.replace_object.null_target": "[MA-0008] 未指定要替換的物件",
|
||||||
"error.replace_object.null_target:hint": "Replace object 需要知道要替換掉哪個物件。嘗試指定一個。",
|
"error.replace_object.null_target:hint": "Replace object 需要知道要替換掉哪個物件。嘗試指定一個。",
|
||||||
|
"error.replace_object.replacing_replacement": "[MA-0009] 不能在多個 Replace Object 元件中指定相同的目標物件",
|
||||||
|
"error.replace_object.parent_of_target": "[MA-0010] 目標物件不能是此物件的父級",
|
||||||
|
"error.singleton": "[MA-0011] 在一個 Avatar 中只允許存在單個 {0} 元件",
|
||||||
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此物件上找不到 Renderer",
|
"validation.blendshape_sync.no_local_renderer": "[MA-1000] 在此物件上找不到 Renderer",
|
||||||
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用於所在物件上的 Skinned Mesh Renderer。你是否將它附加到正確的物件上?",
|
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync 作用於所在物件上的 Skinned Mesh Renderer。你是否將它附加到正確的物件上?",
|
||||||
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此物件的 Renderer 上找不到網格(Mesh)",
|
"validation.blendshape_sync.no_local_mesh": "[MA-1001] 在此物件的 Renderer 上找不到網格(Mesh)",
|
||||||
@ -188,6 +192,7 @@
|
|||||||
"menuitem.prop.value": "參數值",
|
"menuitem.prop.value": "參數值",
|
||||||
"menuitem.prop.value.tooltip": "設定選單項觸發時的參數值",
|
"menuitem.prop.value.tooltip": "設定選單項觸發時的參數值",
|
||||||
"menuitem.prop.automatic_value": "自動",
|
"menuitem.prop.automatic_value": "自動",
|
||||||
|
"menuitem.prop.automatic_value.tooltip": "自動將此控制設定為唯一值",
|
||||||
"menuitem.prop.parameter": "參數",
|
"menuitem.prop.parameter": "參數",
|
||||||
"menuitem.prop.label": "名稱",
|
"menuitem.prop.label": "名稱",
|
||||||
"menuitem.prop.submenu_asset": "子選單資源",
|
"menuitem.prop.submenu_asset": "子選單資源",
|
||||||
@ -239,7 +244,7 @@
|
|||||||
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父級中找不到 VRC Avatar Descriptor。請確保你的服裝放置在 Avatar 裡。",
|
"setup_outfit.err.no_avatar_descriptor": "在 {0} 的父級中找不到 VRC Avatar Descriptor。請確保你的服裝放置在 Avatar 裡。",
|
||||||
"setup_outfit.err.no_animator": "你的 Avatar 沒有 Animator 元件。",
|
"setup_outfit.err.no_animator": "你的 Avatar 沒有 Animator 元件。",
|
||||||
"setup_outfit.err.no_hips": "你的 Avatar 沒有 Hips 骨骼。Setup Outfit 只能用於 humanoid Avatars。",
|
"setup_outfit.err.no_hips": "你的 Avatar 沒有 Hips 骨骼。Setup Outfit 只能用於 humanoid Avatars。",
|
||||||
"setup_outfit.err.no_outfit_hips": "識別不到服裝的 Hips 骨骼,已搜尋含有以下名稱的骨骼物件:",
|
"setup_outfit.err.no_outfit_hips": "識別不到服裝的 Hips 骨骼,已嘗試搜尋含有以下名稱的骨骼物件(不區分大小寫):",
|
||||||
"move_independently.group-header": "要一起移動的物件",
|
"move_independently.group-header": "要一起移動的物件",
|
||||||
"scale_adjuster.scale": "調整比例",
|
"scale_adjuster.scale": "調整比例",
|
||||||
"scale_adjuster.adjust_children": "調整子級的位置",
|
"scale_adjuster.adjust_children": "調整子級的位置",
|
||||||
@ -253,7 +258,7 @@
|
|||||||
"reactive_object.inverse": "反轉條件",
|
"reactive_object.inverse": "反轉條件",
|
||||||
"reactive_object.material-setter.set-to": "將材質設定為:",
|
"reactive_object.material-setter.set-to": "將材質設定為:",
|
||||||
"menuitem.misc.add_toggle": "新增開關",
|
"menuitem.misc.add_toggle": "新增開關",
|
||||||
"ro_sim.open_debugger_button": "開啟響應除錯工具",
|
"ro_sim.open_debugger_button": "開啟 Reaction 除錯工具",
|
||||||
"ro_sim.window.title": "MA 響應除錯工具",
|
"ro_sim.window.title": "MA 響應除錯工具",
|
||||||
"ro_sim.header.inspecting": "檢視物件",
|
"ro_sim.header.inspecting": "檢視物件",
|
||||||
"ro_sim.header.clear_overrides": "清除所有覆寫",
|
"ro_sim.header.clear_overrides": "清除所有覆寫",
|
||||||
@ -274,5 +279,17 @@
|
|||||||
"ro_sim.effect_group.material.tooltip": "Reactive Component 啟用時,將被設在目標元件上的材質",
|
"ro_sim.effect_group.material.tooltip": "Reactive Component 啟用時,將被設在目標元件上的材質",
|
||||||
"ro_sim.effect_group.rule_inverted": "規則的條件已反轉",
|
"ro_sim.effect_group.rule_inverted": "規則的條件已反轉",
|
||||||
"ro_sim.effect_group.rule_inverted.tooltip": "這條規則將在未達成其任一條件時套用。",
|
"ro_sim.effect_group.rule_inverted.tooltip": "這條規則將在未達成其任一條件時套用。",
|
||||||
"ro_sim.effect_group.conditions": "條件"
|
"ro_sim.effect_group.conditions": "條件",
|
||||||
|
"menuitem.label.long_name.tooltip": "使用可能含有富文本和換行符號的長名稱。",
|
||||||
|
"menuitem.label.gameobject_name.tooltip": "使用物件的名稱。",
|
||||||
|
"remove-vertex-color.mode": "模式",
|
||||||
|
"remove-vertex-color.mode.Remove": "移除頂點顏色",
|
||||||
|
"remove-vertex-color.mode.DontRemove": "保留頂點顏色",
|
||||||
|
"general.vrcsdk-required": "此元件需要 VRCSDK 才能運作。",
|
||||||
|
"sync-param-sequence.platform": "主平台",
|
||||||
|
"sync-param-sequence.platform.tooltip": "此平台進行建置時,Modular Avatar 將記錄所有參數,供其他平台使用。",
|
||||||
|
"sync-param-sequence.parameters": "共用的參數資源",
|
||||||
|
"sync-param-sequence.parameters.tooltip": "用來儲存共用參數的資源檔。請勿使用已在 Avatar Descriptor 中設定的相同參數。",
|
||||||
|
"sync-param-sequence.create-asset": "新增",
|
||||||
|
"sync-param-sequence.create-asset.tooltip": "創建一個新的 Expression Parameters"
|
||||||
}
|
}
|
@ -119,9 +119,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
|
internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
|
||||||
{
|
{
|
||||||
|
var type = c.type != 0 ? c.type : VRCExpressionsMenu.Control.ControlType.Button;
|
||||||
|
|
||||||
return new VRCExpressionsMenu.Control()
|
return new VRCExpressionsMenu.Control()
|
||||||
{
|
{
|
||||||
type = c.type,
|
type = type,
|
||||||
name = c.name,
|
name = c.name,
|
||||||
icon = c.icon,
|
icon = c.icon,
|
||||||
parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name },
|
parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name },
|
||||||
|
@ -7,9 +7,11 @@ using System.Linq;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using nadena.dev.modular_avatar.core.menu;
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor.menu
|
namespace nadena.dev.modular_avatar.core.editor.menu
|
||||||
{
|
{
|
||||||
@ -40,9 +42,9 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
private readonly ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>>
|
private readonly ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>>
|
||||||
_menuToInstallerMap;
|
_menuToInstallerMap;
|
||||||
|
|
||||||
private readonly ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>
|
private readonly ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>
|
||||||
_postProcessControls
|
_postProcessControls
|
||||||
= ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>.Empty;
|
= ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>.Empty;
|
||||||
|
|
||||||
private readonly VirtualMenuNode _node;
|
private readonly VirtualMenuNode _node;
|
||||||
private readonly NodeForDelegate _nodeFor;
|
private readonly NodeForDelegate _nodeFor;
|
||||||
@ -75,7 +77,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
VirtualMenuNode node,
|
VirtualMenuNode node,
|
||||||
NodeForDelegate nodeFor,
|
NodeForDelegate nodeFor,
|
||||||
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerMap,
|
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerMap,
|
||||||
ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> postProcessControls,
|
ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>> postProcessControls,
|
||||||
Action<VRCExpressionsMenu> visitedMenu,
|
Action<VRCExpressionsMenu> visitedMenu,
|
||||||
Action<VRCExpressionsMenu.Control> postprocessor
|
Action<VRCExpressionsMenu.Control> postprocessor
|
||||||
)
|
)
|
||||||
@ -102,7 +104,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
PushControl(control);
|
PushControl(control);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_menuToInstallerMap.TryGetValue(expMenu, out var installers))
|
if (_menuToInstallerMap.TryGetValue(ObjectRegistry.GetReference(expMenu), out var installers))
|
||||||
{
|
{
|
||||||
foreach (var installer in installers)
|
foreach (var installer in installers)
|
||||||
{
|
{
|
||||||
@ -127,7 +129,13 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
if (_visited.Contains(source)) return;
|
if (_visited.Contains(source)) return;
|
||||||
_visited.Add(source);
|
_visited.Add(source);
|
||||||
|
|
||||||
BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this));
|
var sourceObj = source as Object;
|
||||||
|
var postProcessor = sourceObj != null ? _postProcessControls.GetValueOrDefault(sourceObj) : default;
|
||||||
|
|
||||||
|
using (new PostprocessorContext(this, postProcessor))
|
||||||
|
{
|
||||||
|
BuildReport.ReportingObject(source as Object, () => source.Visit(this));
|
||||||
|
}
|
||||||
|
|
||||||
_visited.Remove(source);
|
_visited.Remove(source);
|
||||||
}
|
}
|
||||||
@ -228,9 +236,9 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
private Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
|
private Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
|
||||||
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
||||||
|
|
||||||
private ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>
|
private ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>
|
||||||
_postprocessControlsHooks =
|
_postprocessControlsHooks =
|
||||||
ImmutableDictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>.Empty;
|
ImmutableDictionary<Object, Action<VRCExpressionsMenu.Control>>.Empty;
|
||||||
|
|
||||||
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
|
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
|
||||||
|
|
||||||
@ -311,7 +319,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
// initial validation
|
// initial validation
|
||||||
if (installer.menuToAppend == null && installer.GetComponent<MenuSource>() == null) return;
|
if (installer.menuToAppend == null && installer.GetComponent<MenuSource>() == null) return;
|
||||||
|
|
||||||
var target = installer.installTargetMenu ? (object) installer.installTargetMenu : RootMenuKey;
|
var target = installer.installTargetMenu ? (object) ObjectRegistry.GetReference(installer.installTargetMenu) : RootMenuKey;
|
||||||
if (!_targetMenuToInstaller.TryGetValue(target, out var targets))
|
if (!_targetMenuToInstaller.TryGetValue(target, out var targets))
|
||||||
{
|
{
|
||||||
targets = new List<ModularAvatarMenuInstaller>();
|
targets = new List<ModularAvatarMenuInstaller>();
|
||||||
@ -366,7 +374,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Some menu installers may be bound to the root menu _asset_ directly.
|
// Some menu installers may be bound to the root menu _asset_ directly.
|
||||||
if (menuToInstallerFiltered.TryGetValue(menu, out var installers))
|
if (menuToInstallerFiltered.TryGetValue(ObjectRegistry.GetReference(menu), out var installers))
|
||||||
{
|
{
|
||||||
foreach (var installer in installers)
|
foreach (var installer in installers)
|
||||||
{
|
{
|
||||||
@ -405,7 +413,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
|
|
||||||
_pendingGeneration.Enqueue(() =>
|
_pendingGeneration.Enqueue(() =>
|
||||||
{
|
{
|
||||||
BuildReport.ReportingObject(key as UnityEngine.Object, () =>
|
BuildReport.ReportingObject(key as Object, () =>
|
||||||
{
|
{
|
||||||
var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered,
|
var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered,
|
||||||
_postprocessControlsHooks,
|
_postprocessControlsHooks,
|
||||||
@ -430,7 +438,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal VRCExpressionsMenu SerializeMenu(Action<UnityEngine.Object> SaveAsset)
|
internal VRCExpressionsMenu SerializeMenu(Action<Object> SaveAsset)
|
||||||
{
|
{
|
||||||
Dictionary<object, VRCExpressionsMenu> serializedMenus = new Dictionary<object, VRCExpressionsMenu>();
|
Dictionary<object, VRCExpressionsMenu> serializedMenus = new Dictionary<object, VRCExpressionsMenu>();
|
||||||
|
|
||||||
|
@ -27,47 +27,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.animation;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
using VRC.SDKBase;
|
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
internal class MergeAnimatorProcessor
|
internal class MergeAnimatorProcessor
|
||||||
{
|
{
|
||||||
private const string SAMPLE_PATH_PACKAGE =
|
private AnimatorServicesContext _asc;
|
||||||
"Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers";
|
|
||||||
|
|
||||||
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
|
[InitializeOnLoadMethod]
|
||||||
|
private static void Init()
|
||||||
|
{
|
||||||
|
ModularAvatarMergeAnimator.GetMotionBasePathCallback = (merge, objectBuildContext) =>
|
||||||
|
{
|
||||||
|
if (merge.pathMode == MergeAnimatorPathMode.Absolute) return "";
|
||||||
|
|
||||||
private const string GUID_GESTURE_HANDSONLY_MASK = "b2b8bad9583e56a46a3e21795e96ad92";
|
var context = (ndmf.BuildContext)objectBuildContext;
|
||||||
|
|
||||||
private BuildContext _context;
|
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
|
||||||
|
if (targetObject == null) targetObject = merge.gameObject;
|
||||||
|
|
||||||
private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
|
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
|
||||||
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
|
return relativePath != "" ? relativePath : "";
|
||||||
|
};
|
||||||
private Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?> writeDefaults_ =
|
}
|
||||||
new Dictionary<VRCAvatarDescriptor.AnimLayerType, bool?>();
|
|
||||||
|
|
||||||
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
|
|
||||||
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
|
|
||||||
|
|
||||||
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
|
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
|
||||||
|
|
||||||
defaultControllers_.Clear();
|
|
||||||
mergeSessions.Clear();
|
|
||||||
|
|
||||||
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
|
|
||||||
|
|
||||||
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
|
|
||||||
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
|
|
||||||
|
|
||||||
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
|
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
|
||||||
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
|
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
|
||||||
@ -88,10 +81,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
ProcessLayerType(context, entry.Key, entry.Value);
|
ProcessLayerType(context, entry.Key, entry.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers);
|
|
||||||
descriptor.specialAnimationLayers = FinishSessions(descriptor.specialAnimationLayers);
|
|
||||||
descriptor.customizeAnimationLayers = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessLayerType(
|
private void ProcessLayerType(
|
||||||
@ -100,229 +89,124 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
List<ModularAvatarMergeAnimator> toMerge
|
List<ModularAvatarMergeAnimator> toMerge
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Stable sort
|
// Layer priority sorting is handled by NDMF, so we just need to worry about replace mode going first
|
||||||
var sorted = toMerge.OrderBy(x => x.layerPriority)
|
var sorted = toMerge.OrderBy(x => x.mergeAnimatorMode == MergeAnimatorMode.Append)
|
||||||
.ToList();
|
|
||||||
var beforeOriginal = sorted.Where(x => x.layerPriority < 0)
|
|
||||||
.ToList();
|
|
||||||
var afterOriginal = sorted.Where(x => x.layerPriority >= 0)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var session = new AnimatorCombiner(context.PluginBuildContext, layerType.ToString() + " (merged)");
|
var controller = _asc.ControllerContext.Controllers[layerType];
|
||||||
mergeSessions[layerType] = session;
|
|
||||||
mergeSessions[layerType].BlendableLayer = BlendableLayerFor(layerType);
|
|
||||||
|
|
||||||
foreach (var component in beforeOriginal)
|
var replacements = sorted.Count(x => x.mergeAnimatorMode == MergeAnimatorMode.Replace);
|
||||||
|
if (replacements > 1)
|
||||||
{
|
{
|
||||||
MergeSingle(context, session, component);
|
BuildReport.LogFatal("error.merge_animator.multiple_replacements",
|
||||||
|
sorted.Where(x => x.mergeAnimatorMode == MergeAnimatorMode.Replace).ToArray<object>());
|
||||||
}
|
}
|
||||||
|
else if (replacements == 1)
|
||||||
if (defaultControllers_.TryGetValue(layerType, out var defaultController) &&
|
|
||||||
defaultController.layers.Length > 0)
|
|
||||||
{
|
{
|
||||||
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
|
// Delete all pre-existing layers.
|
||||||
|
// Retain the blend tree layer, since that will generally be placed as the first layer in the animator
|
||||||
|
controller.RemoveLayers(l => l.Name != MergeBlendTreePass.BlendTreeLayerName);
|
||||||
|
|
||||||
|
// Merge just the first controller (the one that replaces)
|
||||||
|
MergeSingle(context, controller, sorted.First(), null);
|
||||||
|
sorted.RemoveAt(0);
|
||||||
|
|
||||||
|
// We'll now continue processing the rest as normal.
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var component in afterOriginal)
|
bool? writeDefaults = null;
|
||||||
|
|
||||||
|
var wdStateCounter = controller.Layers.SelectMany(l => l.StateMachine.AllStates())
|
||||||
|
.Select(s => s.WriteDefaultValues)
|
||||||
|
.GroupBy(b => b)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
if (wdStateCounter.Count == 1) writeDefaults = wdStateCounter.First().Key;
|
||||||
|
|
||||||
|
foreach (var component in sorted)
|
||||||
{
|
{
|
||||||
MergeSingle(context, session, component);
|
MergeSingle(context, controller, component, writeDefaults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MergeSingle(BuildContext context, AnimatorCombiner session, ModularAvatarMergeAnimator merge)
|
private void MergeSingle(BuildContext context, VirtualAnimatorController targetController,
|
||||||
|
ModularAvatarMergeAnimator merge, bool? initialWriteDefaults)
|
||||||
{
|
{
|
||||||
if (merge.animator == null)
|
if (merge.animator == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string basePath;
|
if (!merge.matchAvatarWriteDefaults)
|
||||||
if (merge.pathMode == MergeAnimatorPathMode.Relative)
|
|
||||||
{
|
{
|
||||||
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
|
initialWriteDefaults = null;
|
||||||
if (targetObject == null) targetObject = merge.gameObject;
|
|
||||||
|
|
||||||
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
|
|
||||||
basePath = relativePath != "" ? relativePath + "/" : "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
basePath = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var writeDefaults = merge.matchAvatarWriteDefaults
|
var vac = context.PluginBuildContext.Extension<VirtualControllerContext>();
|
||||||
? writeDefaults_.GetValueOrDefault(merge.layerType)
|
|
||||||
: null;
|
if (!vac.Controllers.TryGetValue(merge, out var clonedController)) return;
|
||||||
var controller = _context.ConvertAnimatorController(merge.animator);
|
|
||||||
session.AddController(basePath, controller, writeDefaults);
|
var firstLayer = clonedController.Layers.FirstOrDefault();
|
||||||
|
// the first layer in an animator controller always has weight 1.0f (regardless of what is serialized)
|
||||||
|
if (firstLayer != null) firstLayer.DefaultWeight = 1.0f;
|
||||||
|
|
||||||
|
foreach (var l in clonedController.Layers)
|
||||||
|
{
|
||||||
|
if (initialWriteDefaults != null && !IsWriteDefaultsSafeLayer(l))
|
||||||
|
{
|
||||||
|
foreach (var s in l.StateMachine?.AllStates() ?? Array.Empty<VirtualState>())
|
||||||
|
{
|
||||||
|
s.WriteDefaultValues = initialWriteDefaults.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetController.AddLayer(new LayerPriority(merge.layerPriority), l);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (name, parameter) in clonedController.Parameters)
|
||||||
|
{
|
||||||
|
if (targetController.Parameters.TryGetValue(name, out var existingParam))
|
||||||
|
{
|
||||||
|
if (existingParam.type != parameter.type)
|
||||||
|
{
|
||||||
|
// Force to float
|
||||||
|
switch (parameter.type)
|
||||||
|
{
|
||||||
|
case AnimatorControllerParameterType.Bool:
|
||||||
|
existingParam.defaultFloat = existingParam.defaultBool ? 1.0f : 0.0f;
|
||||||
|
break;
|
||||||
|
case AnimatorControllerParameterType.Int:
|
||||||
|
existingParam.defaultFloat = existingParam.defaultInt;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingParam.type = AnimatorControllerParameterType.Float;
|
||||||
|
|
||||||
|
targetController.Parameters = targetController.Parameters.SetItem(name, existingParam);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetController.Parameters = targetController.Parameters.Add(name, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
if (merge.deleteAttachedAnimator)
|
if (merge.deleteAttachedAnimator)
|
||||||
{
|
{
|
||||||
var animator = merge.GetComponent<Animator>();
|
var animator = merge.GetComponent<Animator>();
|
||||||
if (animator != null) Object.DestroyImmediate(animator);
|
if (animator != null) Object.DestroyImmediate(animator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.DestroyImmediate(merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions(
|
private bool IsWriteDefaultsSafeLayer(VirtualLayer virtualLayer)
|
||||||
VRCAvatarDescriptor.CustomAnimLayer[] layers
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
layers = (VRCAvatarDescriptor.CustomAnimLayer[])layers.Clone();
|
if (virtualLayer.BlendingMode == AnimatorLayerBlendingMode.Additive) return true;
|
||||||
|
var sm = virtualLayer.StateMachine;
|
||||||
|
|
||||||
// Ensure types are consistent across layers
|
if (sm.StateMachines.Count != 0) return false;
|
||||||
Dictionary<string, AnimatorControllerParameterType> types =
|
return sm.States.Count == 1 && sm.AnyStateTransitions.Count == 0 &&
|
||||||
new Dictionary<string, AnimatorControllerParameterType>();
|
sm.DefaultState.Transitions.Count == 0;
|
||||||
// Learn types...
|
|
||||||
foreach (var session in mergeSessions.Values)
|
|
||||||
{
|
|
||||||
session.MergeTypes(types);
|
|
||||||
}
|
|
||||||
// And propagate them
|
|
||||||
foreach (var session in mergeSessions.Values)
|
|
||||||
{
|
|
||||||
session.MergeTypes(types);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < layers.Length; i++)
|
|
||||||
{
|
|
||||||
if (mergeSessions.TryGetValue(layers[i].type, out var session))
|
|
||||||
{
|
|
||||||
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.Gesture && layers[i].isDefault)
|
|
||||||
{
|
|
||||||
// We need to set the mask field for the gesture layer on initial configuration
|
|
||||||
layers[i].mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(
|
|
||||||
AssetDatabase.GUIDToAssetPath(GUID_GESTURE_HANDSONLY_MASK)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
layers[i].isDefault = false;
|
|
||||||
layers[i].animatorController = session.Finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return layers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitSessions(VRCAvatarDescriptor.CustomAnimLayer[] layers)
|
|
||||||
{
|
|
||||||
foreach (var layer in layers)
|
|
||||||
{
|
|
||||||
var controller = ResolveLayerController(layer);
|
|
||||||
if (controller == null) controller = new AnimatorController();
|
|
||||||
|
|
||||||
defaultControllers_[layer.type] = controller;
|
|
||||||
writeDefaults_[layer.type] = ProbeWriteDefaults(controller);
|
|
||||||
if (!layer.isDefault)
|
|
||||||
{
|
|
||||||
// For non-default layers, ensure we always clone the controller for the benefit of subsequent
|
|
||||||
// processing phases
|
|
||||||
mergeSessions[layer.type] =
|
|
||||||
new AnimatorCombiner(_context.PluginBuildContext, layer.type.ToString());
|
|
||||||
mergeSessions[layer.type].BlendableLayer = BlendableLayerFor(layer.type);
|
|
||||||
mergeSessions[layer.type].AddController("", controller, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private VRC_AnimatorLayerControl.BlendableLayer? BlendableLayerFor(VRCAvatarDescriptor.AnimLayerType layerType)
|
|
||||||
{
|
|
||||||
if (Enum.TryParse(layerType.ToString(), out VRC_AnimatorLayerControl.BlendableLayer result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool? ProbeWriteDefaults(AnimatorController controller)
|
|
||||||
{
|
|
||||||
bool hasWDOn = false;
|
|
||||||
bool hasWDOff = false;
|
|
||||||
|
|
||||||
var stateMachineQueue = new Queue<AnimatorStateMachine>();
|
|
||||||
foreach (var layer in controller.layers)
|
|
||||||
{
|
|
||||||
stateMachineQueue.Enqueue(layer.stateMachine);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (stateMachineQueue.Count > 0)
|
|
||||||
{
|
|
||||||
var stateMachine = stateMachineQueue.Dequeue();
|
|
||||||
foreach (var state in stateMachine.states)
|
|
||||||
{
|
|
||||||
if (state.state.writeDefaultValues) hasWDOn = true;
|
|
||||||
else hasWDOff = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in stateMachine.stateMachines)
|
|
||||||
{
|
|
||||||
stateMachineQueue.Enqueue(child.stateMachine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasWDOn == hasWDOff) return null;
|
|
||||||
return hasWDOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer)
|
|
||||||
{
|
|
||||||
AnimatorController controller = null;
|
|
||||||
|
|
||||||
if (!layer.isDefault && layer.animatorController != null &&
|
|
||||||
layer.animatorController is AnimatorController c)
|
|
||||||
{
|
|
||||||
controller = c;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string name;
|
|
||||||
switch (layer.type)
|
|
||||||
{
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.Action:
|
|
||||||
name = "Action";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.Additive:
|
|
||||||
name = "Idle";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.Base:
|
|
||||||
name = "Locomotion";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.Gesture:
|
|
||||||
name = "Hands";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.Sitting:
|
|
||||||
name = "Sitting";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.FX:
|
|
||||||
name = "Face";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.TPose:
|
|
||||||
name = "UtilityTPose";
|
|
||||||
break;
|
|
||||||
case VRCAvatarDescriptor.AnimLayerType.IKPose:
|
|
||||||
name = "UtilityIKPose";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
name = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name != null)
|
|
||||||
{
|
|
||||||
name = "/vrc_AvatarV3" + name + "Layer.controller";
|
|
||||||
|
|
||||||
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
|
|
||||||
if (controller == null)
|
|
||||||
{
|
|
||||||
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return controller;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
* Copyright (c) 2022 bd_
|
* Copyright (c) 2022 bd_
|
||||||
@ -31,8 +31,8 @@ using VRC.SDK3.Dynamics.PhysBone.Components;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.animation;
|
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Animations;
|
using UnityEngine.Animations;
|
||||||
@ -54,13 +54,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
#endif
|
#endif
|
||||||
private BoneDatabase BoneDatabase = new BoneDatabase();
|
private BoneDatabase BoneDatabase = new BoneDatabase();
|
||||||
|
|
||||||
private PathMappings PathMappings => frameworkContext.Extension<AnimationServicesContext>()
|
private AnimatorServicesContext AnimatorServices => frameworkContext.Extension<AnimatorServicesContext>();
|
||||||
.PathMappings;
|
|
||||||
|
|
||||||
private HashSet<Transform> humanoidBones = new HashSet<Transform>();
|
private HashSet<Transform> humanoidBones = new HashSet<Transform>();
|
||||||
private HashSet<Transform> mergedObjects = new HashSet<Transform>();
|
private readonly HashSet<Transform> prunePBsObjects = new();
|
||||||
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
|
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
|
||||||
|
|
||||||
|
private HashSet<Transform> transformLookthrough = new HashSet<Transform>();
|
||||||
|
|
||||||
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
|
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
|
||||||
{
|
{
|
||||||
this.frameworkContext = context;
|
this.frameworkContext = context;
|
||||||
@ -117,7 +118,86 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
RetainBoneReferences(c as Component);
|
RetainBoneReferences(c as Component);
|
||||||
}
|
}
|
||||||
|
|
||||||
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
|
foreach (var smr in avatarGameObject.transform.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||||
|
{
|
||||||
|
// If the root bone has been offset, or has a different sign for its scale, we need to retain it.
|
||||||
|
// see https://github.com/bdunderscore/modular-avatar/pull/1355
|
||||||
|
// (we avoid retaining otherwise to avoid excess bone transforms)
|
||||||
|
|
||||||
|
if (smr.rootBone == null || smr.rootBone.parent == null) continue;
|
||||||
|
|
||||||
|
var root = smr.rootBone;
|
||||||
|
var parent = root.parent;
|
||||||
|
|
||||||
|
if ((parent.position - root.position).sqrMagnitude > 0.000001f
|
||||||
|
|| Vector3.Dot(parent.localScale.normalized, root.localScale.normalized) < 0.9999f)
|
||||||
|
{
|
||||||
|
BoneDatabase.RetainMergedBone(smr.rootBone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, AnimatorServices);
|
||||||
|
|
||||||
|
ProcessTransformLookthrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessTransformLookthrough()
|
||||||
|
{
|
||||||
|
var asc = frameworkContext.Extension<AnimatorServicesContext>();
|
||||||
|
|
||||||
|
transformLookthrough.RemoveWhere(t => !t);
|
||||||
|
|
||||||
|
var clipsToEdit = transformLookthrough.SelectMany(
|
||||||
|
xform =>
|
||||||
|
{
|
||||||
|
var path = asc.ObjectPathRemapper.GetVirtualPathForObject(xform);
|
||||||
|
return asc.AnimationIndex.GetClipsForObjectPath(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
Dictionary<string, string> parentCache = new();
|
||||||
|
|
||||||
|
foreach (var clip in clipsToEdit)
|
||||||
|
{
|
||||||
|
foreach (var binding in clip.GetFloatCurveBindings())
|
||||||
|
{
|
||||||
|
if (binding.type == typeof(Transform))
|
||||||
|
{
|
||||||
|
var newPath = GetReplacementPath(binding.path);
|
||||||
|
|
||||||
|
var newBinding = EditorCurveBinding.FloatCurve(newPath, binding.type, binding.propertyName);
|
||||||
|
clip.SetFloatCurve(newBinding, clip.GetFloatCurve(binding));
|
||||||
|
clip.SetFloatCurve(binding, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetReplacementPath(string bindingPath)
|
||||||
|
{
|
||||||
|
if (parentCache.TryGetValue(bindingPath, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = asc.ObjectPathRemapper.GetObjectForPath(bindingPath)?.transform;
|
||||||
|
while (obj != null && transformLookthrough.Contains(obj))
|
||||||
|
{
|
||||||
|
obj = obj.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
string path;
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
path = bindingPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
path = asc.ObjectPathRemapper.GetVirtualPathForObject(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
parentCache[bindingPath] = path;
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures)
|
private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures)
|
||||||
@ -199,7 +279,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
BuildReport.ReportingObject(config, () =>
|
BuildReport.ReportingObject(config, () =>
|
||||||
{
|
{
|
||||||
mergedObjects.Clear();
|
prunePBsObjects.Clear();
|
||||||
thisPassAdded.Clear();
|
thisPassAdded.Clear();
|
||||||
MergeArmature(config, target);
|
MergeArmature(config, target);
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
@ -276,6 +356,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_activeRetargeter.FixupAnimations();
|
_activeRetargeter.FixupAnimations();
|
||||||
|
|
||||||
thisPassAdded.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
|
thisPassAdded.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
|
||||||
|
transformLookthrough.UnionWith(_activeRetargeter.AddedGameObjects.Select(x => x.transform));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,7 +366,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private void RecursiveMerge(ModularAvatarMergeArmature config,
|
private void RecursiveMerge(ModularAvatarMergeArmature config,
|
||||||
GameObject src,
|
GameObject src,
|
||||||
GameObject newParent,
|
GameObject newParent,
|
||||||
bool zipMerge)
|
bool zipMerge
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (src == newParent)
|
if (src == newParent)
|
||||||
{
|
{
|
||||||
@ -295,7 +377,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
if (zipMerge)
|
if (zipMerge)
|
||||||
{
|
{
|
||||||
mergedObjects.Add(src.transform);
|
|
||||||
thisPassAdded.Add(src.transform);
|
thisPassAdded.Add(src.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,7 +420,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
|
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
|
||||||
BoneDatabase.RetainMergedBone(mergedSrcBone.transform);
|
BoneDatabase.RetainMergedBone(mergedSrcBone.transform);
|
||||||
PathMappings.MarkTransformLookthrough(mergedSrcBone);
|
transformLookthrough.Add(mergedSrcBone.transform);
|
||||||
thisPassAdded.Add(mergedSrcBone.transform);
|
thisPassAdded.Add(mergedSrcBone.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,12 +430,28 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
src.name = src.name + "$" + Guid.NewGuid();
|
src.name = src.name + "$" + Guid.NewGuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
src.GetOrAddComponent<ModularAvatarPBBlocker>();
|
|
||||||
mergedSrcBone = src;
|
mergedSrcBone = src;
|
||||||
|
|
||||||
if (zipMerge)
|
HashSet<Transform> childPhysBonesBlockedSet = null;
|
||||||
|
|
||||||
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
src.GetOrAddComponent<ModularAvatarPBBlocker>();
|
||||||
|
|
||||||
|
if (physBoneByRootBone.TryGetValue(src.transform, out var pb)
|
||||||
|
&& !NotAffectedByPhysBoneOrSimilarChainsAsTarget(src.transform, newParent.transform))
|
||||||
{
|
{
|
||||||
PathMappings.MarkTransformLookthrough(src);
|
childPhysBonesBlockedSet = new HashSet<Transform>(pb.ignoreTransforms);
|
||||||
|
}
|
||||||
|
else if (zipMerge)
|
||||||
|
{
|
||||||
|
prunePBsObjects.Add(src.transform);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// If we're zipping, and the current object is not being used for PBs, we can remove it later.
|
||||||
|
if (zipMerge && childPhysBonesBlockedSet == null)
|
||||||
|
{
|
||||||
|
transformLookthrough.Add(src.transform);
|
||||||
BoneDatabase.AddMergedBone(src.transform);
|
BoneDatabase.AddMergedBone(src.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,6 +463,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
if (zipMerge)
|
if (zipMerge)
|
||||||
{
|
{
|
||||||
|
var reportedHumanoidBoneError = false;
|
||||||
|
|
||||||
foreach (Transform child in children)
|
foreach (Transform child in children)
|
||||||
{
|
{
|
||||||
if (child.GetComponent <ModularAvatarMergeArmature>() != null)
|
if (child.GetComponent <ModularAvatarMergeArmature>() != null)
|
||||||
@ -385,21 +484,32 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var targetObjectName = childName.Substring(config.prefix.Length,
|
var targetObjectName = childName.Substring(config.prefix.Length,
|
||||||
childName.Length - config.prefix.Length - config.suffix.Length);
|
childName.Length - config.prefix.Length - config.suffix.Length);
|
||||||
var targetObject = newParent.transform.Find(targetObjectName);
|
var targetObject = newParent.transform.Find(targetObjectName);
|
||||||
|
|
||||||
|
if (childPhysBonesBlockedSet != null
|
||||||
|
&& !childPhysBonesBlockedSet.Contains(child)
|
||||||
|
&& !child.TryGetComponent<ModularAvatarPBBlocker>(out _))
|
||||||
|
{
|
||||||
|
// This object is potentially impacted by the parent's physbones; is it humanoid?
|
||||||
|
if (!reportedHumanoidBoneError && targetObject != null &&
|
||||||
|
humanoidBones.Contains(targetObject.transform))
|
||||||
|
{
|
||||||
|
// If so, fail the build, as we won't properly apply this to humanoid children.
|
||||||
|
BuildReport.LogFatal(
|
||||||
|
"error.merge_armature.physbone_on_humanoid_bone", new string[0], config);
|
||||||
|
reportedHumanoidBoneError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't move this child object
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Zip merge bones if the names match and the outfit side is not affected by its own PhysBone.
|
// Zip merge bones if the names match and the outfit side is not affected by its own PhysBone.
|
||||||
// Also zip merge when it seems to have been copied from avatar side by checking the dinstance.
|
// Also zip merge when it seems to have been copied from avatar side by checking the dinstance.
|
||||||
if (targetObject != null)
|
if (targetObject != null)
|
||||||
{
|
|
||||||
if (NotAffectedByPhysBoneOrSimilarChainsAsTarget(child, targetObject))
|
|
||||||
{
|
{
|
||||||
childNewParent = targetObject.gameObject;
|
childNewParent = targetObject.gameObject;
|
||||||
shouldZip = true;
|
shouldZip = true;
|
||||||
}
|
}
|
||||||
else if (humanoidBones.Contains(targetObject))
|
|
||||||
{
|
|
||||||
BuildReport.LogFatal(
|
|
||||||
"error.merge_armature.physbone_on_humanoid_bone", new string[0], config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RecursiveMerge(config, childGameObject, childNewParent, shouldZip);
|
RecursiveMerge(config, childGameObject, childNewParent, shouldZip);
|
||||||
@ -450,7 +560,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
*/
|
*/
|
||||||
private void PruneDuplicatePhysBones()
|
private void PruneDuplicatePhysBones()
|
||||||
{
|
{
|
||||||
foreach (var obj in mergedObjects)
|
foreach (var obj in prunePBsObjects)
|
||||||
{
|
{
|
||||||
if (obj.GetComponent<VRCPhysBone>() == null) continue;
|
if (obj.GetComponent<VRCPhysBone>() == null) continue;
|
||||||
var baseObj = FindOriginalParent(obj);
|
var baseObj = FindOriginalParent(obj);
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using nadena.dev.modular_avatar.animation;
|
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
using nadena.dev.ndmf.util;
|
using nadena.dev.ndmf.animator;
|
||||||
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.Components;
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -20,16 +21,32 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal const string ALWAYS_ONE = "__ModularAvatarInternal/One";
|
internal const string ALWAYS_ONE = "__ModularAvatarInternal/One";
|
||||||
internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree";
|
internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree";
|
||||||
|
|
||||||
private AnimatorController _controller;
|
private AnimatorServicesContext _asc;
|
||||||
private BlendTree _rootBlendTree;
|
private VirtualBlendTree _rootBlendTree;
|
||||||
private GameObject _mergeHost;
|
|
||||||
private HashSet<string> _parameterNames;
|
private HashSet<string> _parameterNames;
|
||||||
|
|
||||||
|
[InitializeOnLoadMethod]
|
||||||
|
private static void Init()
|
||||||
|
{
|
||||||
|
ModularAvatarMergeBlendTree.GetMotionBasePathCallback = (mbt, objectBuildContext) =>
|
||||||
|
{
|
||||||
|
if (mbt.PathMode == MergeAnimatorPathMode.Absolute) return "";
|
||||||
|
|
||||||
|
var buildContext = (ndmf.BuildContext)objectBuildContext;
|
||||||
|
var root = mbt.RelativePathRoot.Get(buildContext.AvatarRootTransform);
|
||||||
|
if (root == null) root = mbt.gameObject;
|
||||||
|
|
||||||
|
return RuntimeUtil.AvatarRootPath(root);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Execute(ndmf.BuildContext context)
|
protected override void Execute(ndmf.BuildContext context)
|
||||||
{
|
{
|
||||||
|
_asc = context.Extension<AnimatorServicesContext>();
|
||||||
_rootBlendTree = null;
|
_rootBlendTree = null;
|
||||||
_parameterNames = new HashSet<string>();
|
_parameterNames = new HashSet<string>();
|
||||||
_controller = new AnimatorController();
|
|
||||||
|
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
|
||||||
|
|
||||||
foreach (var component in
|
foreach (var component in
|
||||||
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
|
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
|
||||||
@ -37,12 +54,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
|
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_parameterNames.Count + 1);
|
// always add the ALWAYS_ONE parameter
|
||||||
if (_mergeHost != null)
|
fx.Parameters = fx.Parameters.SetItem(ALWAYS_ONE, new AnimatorControllerParameter()
|
||||||
{
|
|
||||||
_parameterNames.Remove(ALWAYS_ONE);
|
|
||||||
|
|
||||||
parameters.Add(new AnimatorControllerParameter()
|
|
||||||
{
|
{
|
||||||
name = ALWAYS_ONE,
|
name = ALWAYS_ONE,
|
||||||
type = AnimatorControllerParameterType.Float,
|
type = AnimatorControllerParameterType.Float,
|
||||||
@ -51,133 +64,90 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
foreach (var name in _parameterNames)
|
foreach (var name in _parameterNames)
|
||||||
{
|
{
|
||||||
parameters.Add(new AnimatorControllerParameter()
|
if (fx.Parameters.ContainsKey(name)) continue;
|
||||||
|
|
||||||
|
fx.Parameters = fx.Parameters.SetItem(name, new AnimatorControllerParameter()
|
||||||
{
|
{
|
||||||
name = name,
|
name = name,
|
||||||
type = AnimatorControllerParameterType.Float,
|
type = AnimatorControllerParameterType.Float,
|
||||||
defaultFloat = 0
|
defaultFloat = 0.0f
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var paramsAnimator = new AnimatorController();
|
|
||||||
paramsAnimator.parameters = parameters.ToArray();
|
|
||||||
|
|
||||||
var paramsComponent = _mergeHost.AddComponent<ModularAvatarMergeAnimator>();
|
|
||||||
paramsComponent.animator = paramsAnimator;
|
|
||||||
paramsComponent.layerPriority = Int32.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
|
private void ProcessComponent(BuildContext context, ModularAvatarMergeBlendTree component)
|
||||||
{
|
{
|
||||||
BlendTree componentBlendTree = component.BlendTree as BlendTree;
|
var virtualBlendTree = _asc.ControllerContext.GetVirtualizedMotion(component);
|
||||||
|
|
||||||
if (componentBlendTree == null)
|
if (virtualBlendTree == null)
|
||||||
{
|
{
|
||||||
ErrorReport.ReportError(Localization.L, ErrorSeverity.NonFatal, "error.merge_blend_tree.missing_tree");
|
ErrorReport.ReportError(Localization.L, ErrorSeverity.NonFatal, "error.merge_blend_tree.missing_tree");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string basePath = null;
|
var rootBlend = GetRootBlendTree();
|
||||||
if (component.PathMode == MergeAnimatorPathMode.Relative)
|
|
||||||
{
|
|
||||||
var root = component.RelativePathRoot.Get(context.AvatarRootTransform);
|
|
||||||
if (root == null) root = component.gameObject;
|
|
||||||
|
|
||||||
basePath = RuntimeUtil.AvatarRootPath(root) + "/";
|
rootBlend.Children = rootBlend.Children.Add(new()
|
||||||
|
{
|
||||||
|
Motion = virtualBlendTree,
|
||||||
|
DirectBlendParameter = ALWAYS_ONE,
|
||||||
|
Threshold = 1,
|
||||||
|
CycleOffset = 1,
|
||||||
|
TimeScale = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var asset in virtualBlendTree.AllReachableNodes())
|
||||||
|
{
|
||||||
|
if (asset is VirtualBlendTree bt2)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(bt2.BlendParameter) && bt2.BlendType != BlendTreeType.Direct)
|
||||||
|
{
|
||||||
|
_parameterNames.Add(bt2.BlendParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var bt = new DeepClone(context).DoClone(componentBlendTree, basePath);
|
if (bt2.BlendType != BlendTreeType.Direct && bt2.BlendType != BlendTreeType.Simple1D)
|
||||||
var rootBlend = GetRootBlendTree(context);
|
|
||||||
|
|
||||||
rootBlend.AddChild(bt);
|
|
||||||
var children = rootBlend.children;
|
|
||||||
children[children.Length - 1].directBlendParameter = ALWAYS_ONE;
|
|
||||||
rootBlend.children = children;
|
|
||||||
|
|
||||||
foreach (var asset in bt.ReferencedAssets(includeScene: false))
|
|
||||||
{
|
{
|
||||||
if (asset is BlendTree bt2)
|
if (!string.IsNullOrEmpty(bt2.BlendParameterY))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(bt2.blendParameter) && bt2.blendType != BlendTreeType.Direct)
|
_parameterNames.Add(bt2.BlendParameterY);
|
||||||
{
|
|
||||||
_parameterNames.Add(bt2.blendParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bt2.blendType != BlendTreeType.Direct && bt2.blendType != BlendTreeType.Simple1D)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(bt2.blendParameterY))
|
|
||||||
{
|
|
||||||
_parameterNames.Add(bt2.blendParameterY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bt2.blendType == BlendTreeType.Direct)
|
if (bt2.BlendType == BlendTreeType.Direct)
|
||||||
{
|
{
|
||||||
foreach (var childMotion in bt2.children)
|
foreach (var childMotion in bt2.Children)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(childMotion.directBlendParameter))
|
if (!string.IsNullOrEmpty(childMotion.DirectBlendParameter))
|
||||||
{
|
{
|
||||||
_parameterNames.Add(childMotion.directBlendParameter);
|
_parameterNames.Add(childMotion.DirectBlendParameter);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BlendTree GetRootBlendTree(ndmf.BuildContext context)
|
Object.DestroyImmediate(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VirtualBlendTree GetRootBlendTree()
|
||||||
{
|
{
|
||||||
if (_rootBlendTree != null) return _rootBlendTree;
|
if (_rootBlendTree != null) return _rootBlendTree;
|
||||||
|
|
||||||
var newController = new AnimatorController();
|
var fx = _asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
|
||||||
var newStateMachine = new AnimatorStateMachine();
|
var controller = fx.AddLayer(new LayerPriority(int.MinValue), BlendTreeLayerName);
|
||||||
var newState = new AnimatorState();
|
var stateMachine = controller.StateMachine;
|
||||||
|
if (fx == null)
|
||||||
_rootBlendTree = new BlendTree();
|
|
||||||
_controller = newController;
|
|
||||||
|
|
||||||
newController.layers = new[]
|
|
||||||
{
|
{
|
||||||
new AnimatorControllerLayer
|
throw new Exception("FX layer not found");
|
||||||
{
|
|
||||||
blendingMode = AnimatorLayerBlendingMode.Override,
|
|
||||||
defaultWeight = 1,
|
|
||||||
name = BlendTreeLayerName,
|
|
||||||
stateMachine = newStateMachine
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
newStateMachine.name = "ModularAvatarMergeBlendTree";
|
_rootBlendTree = VirtualBlendTree.Create("Root");
|
||||||
newStateMachine.states = new[]
|
var state = stateMachine.AddState("State", _rootBlendTree);
|
||||||
{
|
stateMachine.DefaultState = state;
|
||||||
new ChildAnimatorState
|
state.WriteDefaultValues = true;
|
||||||
{
|
|
||||||
state = newState,
|
|
||||||
position = Vector3.zero
|
|
||||||
}
|
|
||||||
};
|
|
||||||
newStateMachine.defaultState = newState;
|
|
||||||
|
|
||||||
newState.writeDefaultValues = true;
|
_rootBlendTree.BlendType = BlendTreeType.Direct;
|
||||||
newState.motion = _rootBlendTree;
|
_rootBlendTree.BlendParameter = ALWAYS_ONE;
|
||||||
|
|
||||||
_rootBlendTree.blendType = BlendTreeType.Direct;
|
|
||||||
_rootBlendTree.blendParameter = ALWAYS_ONE;
|
|
||||||
|
|
||||||
var mergeObject = new GameObject("ModularAvatarMergeBlendTree");
|
|
||||||
var merger = mergeObject.AddComponent<ModularAvatarMergeAnimator>();
|
|
||||||
merger.animator = newController;
|
|
||||||
merger.pathMode = MergeAnimatorPathMode.Absolute;
|
|
||||||
merger.matchAvatarWriteDefaults = false;
|
|
||||||
merger.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
|
|
||||||
merger.deleteAttachedAnimator = false;
|
|
||||||
merger.layerPriority = Int32.MinValue;
|
|
||||||
|
|
||||||
mergeObject.transform.SetParent(context.AvatarRootTransform, false);
|
|
||||||
mergeObject.transform.SetSiblingIndex(0);
|
|
||||||
|
|
||||||
_mergeHost = mergeObject;
|
|
||||||
|
|
||||||
return _rootBlendTree;
|
return _rootBlendTree;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ using System.Linq;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using nadena.dev.modular_avatar.animation;
|
using nadena.dev.modular_avatar.animation;
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
|
using nadena.dev.ndmf.animator;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
@ -84,13 +85,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal class RetargetMeshes
|
internal class RetargetMeshes
|
||||||
{
|
{
|
||||||
private BoneDatabase _boneDatabase;
|
private BoneDatabase _boneDatabase;
|
||||||
private PathMappings _pathTracker;
|
private AnimationIndex _animationIndex;
|
||||||
|
private ObjectPathRemapper _pathRemapper;
|
||||||
|
|
||||||
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
|
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
|
||||||
PathMappings pathMappings)
|
AnimatorServicesContext pathMappings)
|
||||||
{
|
{
|
||||||
this._boneDatabase = boneDatabase;
|
this._boneDatabase = boneDatabase;
|
||||||
this._pathTracker = pathMappings;
|
this._animationIndex = pathMappings.AnimationIndex;
|
||||||
|
this._pathRemapper = pathMappings.ObjectPathRemapper;
|
||||||
|
|
||||||
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||||
{
|
{
|
||||||
@ -153,7 +156,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
child.SetParent(destBone, true);
|
child.SetParent(destBone, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pathTracker.MarkRemoved(sourceBone.gameObject);
|
// Remap any animation clips that reference this bone into its parent
|
||||||
|
_pathRemapper.ReplaceObject(sourceBone.gameObject, sourceBone.transform.parent.gameObject);
|
||||||
UnityEngine.Object.DestroyImmediate(sourceBone.gameObject);
|
UnityEngine.Object.DestroyImmediate(sourceBone.gameObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
@ -50,12 +54,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
or ModularAvatarMeshSettings.InheritMode.Inherit
|
or ModularAvatarMeshSettings.InheritMode.Inherit
|
||||||
or ModularAvatarMeshSettings.InheritMode.DontSet
|
or ModularAvatarMeshSettings.InheritMode.DontSet
|
||||||
or ModularAvatarMeshSettings.InheritMode.SetOrInherit), _):
|
or ModularAvatarMeshSettings.InheritMode.SetOrInherit), _):
|
||||||
throw new System.InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
|
throw new InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
|
||||||
case (_, not (ModularAvatarMeshSettings.InheritMode.Set
|
case (_, not (ModularAvatarMeshSettings.InheritMode.Set
|
||||||
or ModularAvatarMeshSettings.InheritMode.Inherit
|
or ModularAvatarMeshSettings.InheritMode.Inherit
|
||||||
or ModularAvatarMeshSettings.InheritMode.DontSet
|
or ModularAvatarMeshSettings.InheritMode.DontSet
|
||||||
or ModularAvatarMeshSettings.InheritMode.SetOrInherit)):
|
or ModularAvatarMeshSettings.InheritMode.SetOrInherit)):
|
||||||
throw new System.ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
|
throw new ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
|
||||||
|
|
||||||
// If current value is came from Set or DontSet, it should not be changed
|
// If current value is came from Set or DontSet, it should not be changed
|
||||||
case (ModularAvatarMeshSettings.InheritMode.Set, _):
|
case (ModularAvatarMeshSettings.InheritMode.Set, _):
|
||||||
@ -144,9 +148,57 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
if (newMesh) context.SaveAsset(newMesh);
|
if (newMesh) context.SaveAsset(newMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var settingsRootBone = settings.RootBone;
|
||||||
|
settingsRootBone = settingsRootBone == null ? smr.transform : settingsRootBone;
|
||||||
|
var smrRootBone = smr.rootBone;
|
||||||
|
smrRootBone = smrRootBone == null ? smr.transform : smrRootBone;
|
||||||
|
|
||||||
|
if (IsInverted(smrRootBone) != IsInverted(settingsRootBone))
|
||||||
|
{
|
||||||
|
smr.rootBone = GetInvertedRootBone(settingsRootBone);
|
||||||
|
|
||||||
|
var bounds = settings.Bounds;
|
||||||
|
var center = bounds.center;
|
||||||
|
center.x *= -1;
|
||||||
|
bounds.center = center;
|
||||||
|
smr.localBounds = bounds;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
smr.rootBone = settings.RootBone;
|
smr.rootBone = settings.RootBone;
|
||||||
smr.localBounds = settings.Bounds;
|
smr.localBounds = settings.Bounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsInverted(Transform bone)
|
||||||
|
{
|
||||||
|
var inverseCount = 0;
|
||||||
|
|
||||||
|
var scale = bone.lossyScale;
|
||||||
|
if (scale.x < 0) inverseCount += 1;
|
||||||
|
if (scale.y < 0) inverseCount += 1;
|
||||||
|
if (scale.z < 0) inverseCount += 1;
|
||||||
|
|
||||||
|
return (inverseCount % 2) != 0;
|
||||||
|
}
|
||||||
|
private Dictionary<Transform, Transform> invertedRootBoneCache = new();
|
||||||
|
private Transform GetInvertedRootBone(Transform rootBone)
|
||||||
|
{
|
||||||
|
if (invertedRootBoneCache.TryGetValue(rootBone, out var cache)) { return cache; }
|
||||||
|
|
||||||
|
var invertedRootBone = new GameObject($"{rootBone.gameObject.name}-InvertedRootBone");
|
||||||
|
EditorUtility.CopySerialized(rootBone, invertedRootBone.transform);
|
||||||
|
invertedRootBone.transform.parent = rootBone;
|
||||||
|
|
||||||
|
var transform = invertedRootBone.transform;
|
||||||
|
var scale = transform.localScale;
|
||||||
|
scale.x *= -1;
|
||||||
|
transform.localScale = scale;
|
||||||
|
|
||||||
|
invertedRootBoneCache[rootBone] = transform;
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
3
Editor/MiscPreview.meta
Normal file
3
Editor/MiscPreview.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea61a438a5d54a289c6abbb1e05c56da
|
||||||
|
timeCreated: 1733085642
|
122
Editor/MiscPreview/RemoveVertexColorPreview.cs
Normal file
122
Editor/MiscPreview/RemoveVertexColorPreview.cs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using nadena.dev.ndmf.preview;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
internal class RemoveVertexColorPreview : IRenderFilter
|
||||||
|
{
|
||||||
|
private static string ToPathString(ComputeContext ctx, Transform t)
|
||||||
|
{
|
||||||
|
return string.Join("/", ctx.ObservePath(t).Select(t2 => t2.gameObject.name).Reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||||
|
{
|
||||||
|
var roots = context.GetAvatarRoots()
|
||||||
|
.Where(r => context.ActiveInHierarchy(r) is true);
|
||||||
|
var removers = roots
|
||||||
|
.SelectMany(r => context.GetComponentsInChildren<ModularAvatarRemoveVertexColor>(r, true))
|
||||||
|
.Select(rvc => (ToPathString(context, rvc.transform),
|
||||||
|
context.Observe(rvc, r => r.Mode) == ModularAvatarRemoveVertexColor.RemoveMode.Remove))
|
||||||
|
.OrderBy(pair => pair.Item1)
|
||||||
|
.ToList();
|
||||||
|
var targets = roots.SelectMany(
|
||||||
|
r => context.GetComponentsInChildren<SkinnedMeshRenderer>(r, true)
|
||||||
|
.Concat(
|
||||||
|
context.GetComponentsInChildren<MeshFilter>(r, true)
|
||||||
|
.SelectMany(mf => context.GetComponents<Renderer>(mf.gameObject))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
targets = targets.Where(target =>
|
||||||
|
{
|
||||||
|
var stringPath = ToPathString(context, target.transform);
|
||||||
|
var index = removers.BinarySearch((stringPath, true));
|
||||||
|
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
// There is a component on this mesh
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var priorIndex = ~index - 1;
|
||||||
|
if (priorIndex < 0) return false; // no match
|
||||||
|
|
||||||
|
var (maybeParent, mode) = removers[priorIndex];
|
||||||
|
if (!stringPath.StartsWith(maybeParent)) return false; // no parent matched
|
||||||
|
return mode;
|
||||||
|
});
|
||||||
|
|
||||||
|
return targets.Select(RenderGroup.For).ToImmutableList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
|
||||||
|
ComputeContext context)
|
||||||
|
{
|
||||||
|
Dictionary<Mesh, Mesh> conversionMap = new();
|
||||||
|
|
||||||
|
foreach (var (_, proxy) in proxyPairs)
|
||||||
|
{
|
||||||
|
Component c = proxy;
|
||||||
|
if (!(c is SkinnedMeshRenderer))
|
||||||
|
{
|
||||||
|
c = context.GetComponent<MeshFilter>(proxy.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == null) continue;
|
||||||
|
|
||||||
|
RemoveVertexColorPass.ForceRemove(_ => false, c, conversionMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IRenderFilterNode>(new Node(conversionMap.Values.FirstOrDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Node : IRenderFilterNode
|
||||||
|
{
|
||||||
|
private readonly Mesh? _theMesh;
|
||||||
|
|
||||||
|
public Node(Mesh? theMesh)
|
||||||
|
{
|
||||||
|
_theMesh = theMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context,
|
||||||
|
RenderAspects updatedAspects)
|
||||||
|
{
|
||||||
|
if (updatedAspects.HasFlag(RenderAspects.Mesh)) return Task.FromResult<IRenderFilterNode>(null!);
|
||||||
|
if (_theMesh == null) return Task.FromResult<IRenderFilterNode>(null!);
|
||||||
|
|
||||||
|
return Task.FromResult<IRenderFilterNode>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderAspects WhatChanged => RenderAspects.Mesh;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_theMesh != null) Object.DestroyImmediate(_theMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnFrame(Renderer original, Renderer proxy)
|
||||||
|
{
|
||||||
|
if (_theMesh == null) return;
|
||||||
|
|
||||||
|
switch (proxy)
|
||||||
|
{
|
||||||
|
case SkinnedMeshRenderer smr: smr.sharedMesh = _theMesh; break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var mf = proxy.GetComponent<MeshFilter>();
|
||||||
|
if (mf != null) mf.sharedMesh = _theMesh;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user