mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-04-05 03:59:01 +08:00
Compare commits
403 Commits
1.10.0-alp
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
2ea9fb50b6 | ||
|
4e3001ad65 | ||
|
faa8d210f2 | ||
|
f4d80b857d | ||
|
fed6a22d72 | ||
|
a9c2815106 | ||
|
c0582a9961 | ||
|
3eaf8bee6d | ||
|
73755e7664 | ||
|
4f77723906 | ||
|
3be3cfb74a | ||
|
38384a3c70 | ||
|
29177f2c5a | ||
|
106ba8c5ff | ||
|
2735adf55c | ||
|
566a030730 | ||
|
1163fac2e7 | ||
|
4fa0621655 | ||
|
acd6c50543 | ||
|
89b4c8f921 | ||
|
389ae4f2cc | ||
|
422ed5cfb1 | ||
|
0ee291076f | ||
|
c63128095e | ||
|
d403f1b178 | ||
|
e07b18d87e | ||
|
668ab35b46 | ||
|
f9a9f1f1ef | ||
|
22cff4ba3f | ||
|
466017c102 | ||
|
1d58548013 | ||
|
ae950ad938 | ||
|
371809f430 | ||
|
1aa6c03202 | ||
|
db06a6a492 | ||
|
7330cda42a | ||
|
ece8a6837a | ||
|
c309d93bdd | ||
|
d33787a6b0 | ||
|
0a6270bb43 | ||
|
682a0de0e0 | ||
|
b0032a09c0 | ||
|
28ed2e0ed1 | ||
|
c6e863d409 | ||
|
3bc090dc7d | ||
|
2148ab0bfc | ||
|
818f16f839 | ||
|
7f3b0fec3e | ||
|
231feba3a2 | ||
|
13b9bf72e2 | ||
|
802fea09d9 | ||
|
f085ce07b6 | ||
|
6cb249be44 | ||
|
580cb2bfe9 | ||
|
9d48ae4f65 | ||
|
0243b8cc8e | ||
|
369cc010c3 | ||
|
f514a5e904 | ||
|
f9abb5c4fc | ||
|
ea857406ee | ||
|
b853514fea | ||
|
f19a4946bf | ||
|
ca681d6033 | ||
|
ae318b29d8 | ||
|
87a385a43e | ||
|
07660164ba | ||
|
14fd8b81aa | ||
|
46cf066e04 | ||
|
a2b9b817ce | ||
|
f96b2627aa | ||
|
b2ada9fe05 | ||
|
037c450760 | ||
|
f44e070c46 | ||
|
8418f8e047 | ||
|
3b9f0d1838 | ||
|
dd66cd2f7c | ||
|
3b44a0b44f | ||
|
8be802bee5 | ||
|
8ed649f9a4 | ||
|
a42295e0e6 | ||
|
159865e6cd | ||
|
c7df409d70 | ||
|
e7e030f0db | ||
|
9642c845cf | ||
|
436a7dc4dd | ||
|
a75a422cf4 | ||
|
eb5a04511d | ||
|
c2b381c721 | ||
|
3f02a1127f | ||
|
6f95800e41 | ||
|
b732c5268e | ||
|
b70727076c | ||
|
e5af635bd9 | ||
|
7a20eaf57e | ||
|
7384715059 | ||
|
d83c3351d7 | ||
|
1b3b9194c0 | ||
|
467b0d4431 | ||
|
3eea882019 | ||
|
81bcd14bcd | ||
|
c0ee54e8c2 | ||
|
3644ffdb12 | ||
|
622d846b9f | ||
|
3838014517 | ||
|
f4ab86fedc | ||
|
e2e0b88dfa | ||
|
4617575123 | ||
|
13822f33e1 | ||
|
3117275277 | ||
|
ed4f5dfd31 | ||
|
d49f87e754 | ||
|
5a7e02d591 | ||
|
915ddc0d5b | ||
|
a739598d13 | ||
|
a3ce9b2505 | ||
|
4d246fa238 | ||
|
cd366cab2d | ||
|
6b99b763a7 | ||
|
d998763fbe | ||
|
3d1b4f1c76 | ||
|
d4683f99e3 | ||
|
ead026a918 | ||
|
586fb90dca | ||
|
053a0d464b | ||
|
6d89db6a8a | ||
|
7d5860654a | ||
|
87ff50b161 | ||
|
dee5241436 | ||
|
1c766e9fe8 | ||
|
c2f37bb3a1 | ||
|
489d3a7374 | ||
|
3d3aefd4f9 | ||
|
9a974f5f09 | ||
|
098a85af50 | ||
|
2cd996db55 | ||
|
0806c621c5 | ||
|
7a3b782fae | ||
|
8e7526e711 | ||
|
bf9266f054 | ||
|
d999f799fb | ||
|
c50b3526f6 | ||
|
32dc864d8d | ||
|
366ff0832f | ||
|
08b3880d23 | ||
|
7beb5bf09b | ||
|
22555632bf | ||
|
e6af038379 | ||
|
17a85325f8 | ||
|
d8e01234f0 |
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": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.5.0"
|
||||
"version": "3.7.4"
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.4.0"
|
||||
"version": "1.7.2-rc.0"
|
||||
}
|
||||
},
|
||||
"locked": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.5.0",
|
||||
"version": "3.7.4",
|
||||
"dependencies": {
|
||||
"com.vrchat.base": "3.5.0"
|
||||
"com.vrchat.base": "3.7.4"
|
||||
}
|
||||
},
|
||||
"com.vrchat.base": {
|
||||
"version": "3.5.0",
|
||||
"version": "3.7.4",
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.5.0-beta.0"
|
||||
"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'
|
||||
type: boolean
|
||||
required: false
|
||||
prerelease:
|
||||
description: 'use prerelease changelog'
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
@ -67,6 +72,18 @@ jobs:
|
||||
BASEURL="/${{ inputs.path }}/" perl -i -p -e "s{baseUrl: '/'}{baseUrl: '\$ENV{BASEURL}'}" docs~/docusaurus.config.js
|
||||
cat docs~/docusaurus.config.js
|
||||
|
||||
- name: Format changelogs
|
||||
run: |
|
||||
SUFFIX=""
|
||||
export PRERELEASE=${{ inputs.prerelease && 'true' || 'false' }}
|
||||
|
||||
if [ ${{ inputs.prerelease }} == true ]; then
|
||||
SUFFIX="-PRERELEASE"
|
||||
fi
|
||||
|
||||
perl -n .github/gen-docs-changelog.pl < CHANGELOG$SUFFIX.md >> docs~/docs/changelog.md
|
||||
perl -n .github/gen-docs-changelog.pl < CHANGELOG$SUFFIX''-jp.md >> docs~/i18n/ja/docusaurus-plugin-content-docs/current/changelog.md
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
cd docs~
|
||||
|
58
.github/workflows/changelog-check.yml
vendored
Normal file
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:
|
||||
name: Build documentation (latest release)
|
||||
# TODO - update to build-docs.yml
|
||||
uses: bdunderscore/modular-avatar/.github/workflows/build-test-docs.yml@main
|
||||
needs:
|
||||
- snapshot-docs
|
||||
with:
|
||||
ref: docs-snapshot
|
||||
prerelease: false
|
||||
|
||||
build-docs-dev:
|
||||
name: Build documentation (main branch)
|
||||
@ -53,6 +55,7 @@ jobs:
|
||||
ref: main
|
||||
path: dev
|
||||
artifact: docs-dev
|
||||
prerelease: true
|
||||
|
||||
deploy-docs:
|
||||
name: Deploy documentation
|
||||
@ -122,7 +125,7 @@ jobs:
|
||||
workingDirectory: docs-site~
|
||||
|
||||
- name: Purge cache
|
||||
uses: nathanvaughn/actions-cloudflare-purge@367672c723960cd03bb7d8c2c4d89062a3fc1fac
|
||||
uses: nathanvaughn/actions-cloudflare-purge@784d555fc0fc48946a1e34873a43fc8cf634bcfa
|
||||
continue-on-error: true
|
||||
with:
|
||||
cf_zone: ${{ secrets.CF_ZONE_ID }}
|
||||
|
15
.github/workflows/gameci.yml
vendored
15
.github/workflows/gameci.yml
vendored
@ -110,12 +110,15 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- uses: anatawa12/sh-actions/resolve-vpm-packages@master
|
||||
name: Resolve VPM packages
|
||||
if: ${{ matrix.sdk == 'vrcsdk' && steps.setup.outputs.should_test == 'true' }}
|
||||
with:
|
||||
repos: |
|
||||
https://vpm.nadena.dev/vpm-prerelease.json
|
||||
- uses: anatawa12/sh-actions/setup-vrc-get@master
|
||||
|
||||
- name: Resolve packages
|
||||
if: ${{ steps.setup.outputs.should_test == 'true' }}
|
||||
run: |
|
||||
vrc-get repo add -- "https://vpm.nadena.dev/vpm-prerelease.json" || true
|
||||
vrc-get repo add -- "https://vrchat.github.io/packages/index.json?download" || true
|
||||
vrc-get resolve --project .
|
||||
vrc-get info project --project .
|
||||
|
||||
- if: ${{ steps.setup.outputs.should_test == 'true' }}
|
||||
name: "Debug: List project contents"
|
||||
|
231
.github/workflows/perform-release.yml
vendored
Normal file
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.Linq;
|
||||
using nadena.dev.modular_avatar.animation;
|
||||
using nadena.dev.ndmf.animator;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||
@ -16,7 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
private readonly BuildContext _context;
|
||||
private readonly BoneDatabase _boneDatabase;
|
||||
private readonly PathMappings _pathMappings;
|
||||
private readonly AnimatorServicesContext _asc;
|
||||
private readonly List<IntermediateObj> _intermediateObjs = new List<IntermediateObj>();
|
||||
|
||||
/// <summary>
|
||||
@ -55,15 +56,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
_context = context;
|
||||
_boneDatabase = boneDatabase;
|
||||
_pathMappings = context.PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
|
||||
_asc = context.PluginBuildContext.Extension<AnimatorServicesContext>();
|
||||
|
||||
while (root != null && !RuntimeUtil.IsAvatarRoot(root))
|
||||
{
|
||||
var originalPath = RuntimeUtil.AvatarRootPath(root.gameObject);
|
||||
System.Diagnostics.Debug.Assert(originalPath != null);
|
||||
|
||||
if (context.AnimationDatabase.ClipsForPath(originalPath).Any(clip =>
|
||||
GetActiveBinding(clip.CurrentClip as AnimationClip, originalPath) != null
|
||||
if (_asc.AnimationIndex.GetClipsForObjectPath(originalPath).Any(clip =>
|
||||
GetActiveBinding(clip, originalPath) != null
|
||||
))
|
||||
{
|
||||
_intermediateObjs.Add(new IntermediateObj
|
||||
@ -118,7 +119,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
// Ensure mesh retargeting looks through this
|
||||
_boneDatabase.AddMergedBone(sourceBone.transform);
|
||||
_boneDatabase.RetainMergedBone(sourceBone.transform);
|
||||
_pathMappings.MarkTransformLookthrough(sourceBone);
|
||||
}
|
||||
|
||||
return sourceBone;
|
||||
@ -130,22 +130,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
var path = intermediate.OriginalPath;
|
||||
|
||||
foreach (var holder in _context.AnimationDatabase.ClipsForPath(path))
|
||||
foreach (var clip in _asc.AnimationIndex.GetClipsForObjectPath(path))
|
||||
{
|
||||
if (!_context.PluginBuildContext.IsTemporaryAsset(holder.CurrentClip))
|
||||
{
|
||||
holder.CurrentClip = Object.Instantiate(holder.CurrentClip);
|
||||
}
|
||||
|
||||
var clip = holder.CurrentClip as AnimationClip;
|
||||
if (clip == null) continue;
|
||||
|
||||
var curve = GetActiveBinding(clip, path);
|
||||
if (curve != null)
|
||||
{
|
||||
foreach (var mapping in intermediate.Created)
|
||||
{
|
||||
clip.SetCurve(_pathMappings.GetObjectIdentifier(mapping), typeof(GameObject), "m_IsActive",
|
||||
clip.SetFloatCurve(_asc.ObjectPathRemapper.GetVirtualPathForObject(mapping), typeof(GameObject), "m_IsActive",
|
||||
curve);
|
||||
}
|
||||
}
|
||||
@ -153,10 +145,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
private AnimationCurve GetActiveBinding(AnimationClip clip, string path)
|
||||
private AnimationCurve GetActiveBinding(VirtualClip clip, string path)
|
||||
{
|
||||
return AnimationUtility.GetEditorCurve(clip,
|
||||
EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
|
||||
return clip.GetFloatCurve(EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive"));
|
||||
}
|
||||
}
|
||||
}
|
@ -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,121 +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;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: This is a temporary crutch until we rework the entire animator services system
|
||||
public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
|
||||
{
|
||||
var fx = (AnimatorController)
|
||||
_context.AvatarDescriptor.baseAnimationLayers
|
||||
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
|
||||
.animatorController;
|
||||
|
||||
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a parameter which proxies the "activeSelf" state of the specified GameObject.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="paramName"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public bool TryGetActiveSelfProxy(GameObject obj, out string paramName)
|
||||
{
|
||||
if (_selfProxies.TryGetValue(obj, out paramName)) return !string.IsNullOrEmpty(paramName);
|
||||
|
||||
var path = PathMappings.GetObjectIdentifier(obj);
|
||||
var clips = AnimationDatabase.ClipsForPath(path);
|
||||
if (clips == null || clips.IsEmpty)
|
||||
{
|
||||
_selfProxies[obj] = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
paramName = _readableProperty.ForActiveSelf(_pathMappings.GetObjectIdentifier(obj));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,615 +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()
|
||||
{
|
||||
var originalLayers = _layers;
|
||||
int[] layerIndexMappings = new int[originalLayers.Count];
|
||||
|
||||
List<AnimatorControllerLayer> newLayers = new List<AnimatorControllerLayer>();
|
||||
|
||||
for (int i = 0; i < originalLayers.Count; i++)
|
||||
{
|
||||
if (i > 0 && IsEmptyLayer(originalLayers[i]))
|
||||
{
|
||||
layerIndexMappings[i] = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
layerIndexMappings[i] = newLayers.Count;
|
||||
newLayers.Add(originalLayers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var layer in newLayers)
|
||||
{
|
||||
if (layer.stateMachine == null) continue;
|
||||
|
||||
foreach (var asset in layer.stateMachine.ReferencedAssets(includeScene: false))
|
||||
{
|
||||
if (asset is AnimatorState alc)
|
||||
{
|
||||
alc.behaviours = AdjustStateBehaviors(alc.behaviours);
|
||||
}
|
||||
else if (asset is AnimatorStateMachine asm)
|
||||
{
|
||||
asm.behaviours = AdjustStateBehaviors(asm.behaviours);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_layers = newLayers;
|
||||
|
||||
StateMachineBehaviour[] AdjustStateBehaviors(StateMachineBehaviour[] behaviours)
|
||||
{
|
||||
if (behaviours.Length == 0) return behaviours;
|
||||
|
||||
var newBehaviors = new List<StateMachineBehaviour>();
|
||||
foreach (var b in behaviours)
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
case VRCAnimatorLayerControl alc when alc.playable == BlendableLayer:
|
||||
int newLayer = -1;
|
||||
if (alc.layer >= 0 && alc.layer < layerIndexMappings.Length)
|
||||
{
|
||||
newLayer = layerIndexMappings[alc.layer];
|
||||
}
|
||||
|
||||
if (newLayer != -1)
|
||||
{
|
||||
alc.layer = newLayer;
|
||||
newBehaviors.Add(alc);
|
||||
}
|
||||
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
newBehaviors.Add(b);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return newBehaviors.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEmptyLayer(AnimatorControllerLayer layer)
|
||||
{
|
||||
if (layer.syncedLayerIndex >= 0) return false;
|
||||
if (layer.avatarMask != null) return false;
|
||||
|
||||
return layer.stateMachine == null
|
||||
|| (layer.stateMachine.states.Length == 0 && layer.stateMachine.stateMachines.Length == 0);
|
||||
}
|
||||
|
||||
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults,
|
||||
bool forceFirstLayerWeight = false)
|
||||
{
|
||||
_controllerBaseLayer = _layers.Count;
|
||||
_cloneMap = new Dictionary<Object, Object>();
|
||||
|
||||
foreach (var param in controller.parameters)
|
||||
{
|
||||
if (_parameters.TryGetValue(param.name, out var acp))
|
||||
{
|
||||
if (acp.type == param.type) continue;
|
||||
|
||||
if (acp.type != param.type &&
|
||||
(acp.type == AnimatorControllerParameterType.Trigger ||
|
||||
param.type == AnimatorControllerParameterType.Trigger))
|
||||
{
|
||||
BuildReport.LogFatal("error.merge_animator.param_type_mismatch",
|
||||
param.name,
|
||||
acp.type.ToString(),
|
||||
param.type.ToString(),
|
||||
controller,
|
||||
_parameterSource[param.name]
|
||||
);
|
||||
}
|
||||
|
||||
acp.type = AnimatorControllerParameterType.Float;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var clonedParameter = new AnimatorControllerParameter()
|
||||
{
|
||||
name = param.name,
|
||||
type = param.type,
|
||||
defaultBool = param.defaultBool,
|
||||
defaultFloat = param.defaultFloat,
|
||||
defaultInt = param.defaultInt
|
||||
};
|
||||
|
||||
_parameters.Add(param.name, clonedParameter);
|
||||
_parameterSource.Add(param.name, controller);
|
||||
}
|
||||
|
||||
bool first = true;
|
||||
var layers = controller.layers;
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
insertLayer(basePath, layer, first, writeDefaults, layers);
|
||||
if (first && forceFirstLayerWeight)
|
||||
{
|
||||
_layers[_layers.Count - 1].defaultWeight = 1;
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOverrideController(string basePath, AnimatorOverrideController overrideController,
|
||||
bool? writeDefaults)
|
||||
{
|
||||
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
|
||||
if (controller == null) return;
|
||||
_deepClone.OverrideController = overrideController;
|
||||
try
|
||||
{
|
||||
this.AddController(basePath, controller, writeDefaults);
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void insertLayer(
|
||||
string basePath,
|
||||
AnimatorControllerLayer layer,
|
||||
bool first,
|
||||
bool? writeDefaults,
|
||||
AnimatorControllerLayer[] layers
|
||||
)
|
||||
{
|
||||
var newLayer = new AnimatorControllerLayer()
|
||||
{
|
||||
name = layer.name,
|
||||
avatarMask = layer.avatarMask, // TODO map transforms
|
||||
blendingMode = layer.blendingMode,
|
||||
defaultWeight = first ? 1 : layer.defaultWeight,
|
||||
syncedLayerIndex = layer.syncedLayerIndex,
|
||||
syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming,
|
||||
iKPass = layer.iKPass,
|
||||
stateMachine = mapStateMachine(basePath, layer.stateMachine),
|
||||
};
|
||||
|
||||
UpdateWriteDefaults(newLayer.stateMachine, writeDefaults);
|
||||
|
||||
if (newLayer.syncedLayerIndex != -1 && newLayer.syncedLayerIndex >= 0 &&
|
||||
newLayer.syncedLayerIndex < layers.Length)
|
||||
{
|
||||
// Transfer any motion overrides onto the new synced layer
|
||||
var baseLayer = layers[newLayer.syncedLayerIndex];
|
||||
foreach (var state in WalkAllStates(baseLayer.stateMachine))
|
||||
{
|
||||
var overrideMotion = layer.GetOverrideMotion(state);
|
||||
if (overrideMotion != null)
|
||||
{
|
||||
newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], overrideMotion);
|
||||
}
|
||||
|
||||
var overrideBehaviors = (StateMachineBehaviour[])layer.GetOverrideBehaviours(state)?.Clone();
|
||||
if (overrideBehaviors != null)
|
||||
{
|
||||
for (int i = 0; i < overrideBehaviors.Length; i++)
|
||||
{
|
||||
overrideBehaviors[i] = _deepClone.DoClone(overrideBehaviors[i]);
|
||||
AdjustBehavior(overrideBehaviors[i], basePath);
|
||||
}
|
||||
|
||||
newLayer.SetOverrideBehaviours((AnimatorState)_cloneMap[state], overrideBehaviors);
|
||||
}
|
||||
}
|
||||
|
||||
newLayer.syncedLayerIndex += _controllerBaseLayer;
|
||||
}
|
||||
|
||||
_layers.Add(newLayer);
|
||||
}
|
||||
|
||||
IEnumerable<AnimatorState> WalkAllStates(AnimatorStateMachine animatorStateMachine)
|
||||
{
|
||||
HashSet<Object> visited = new HashSet<Object>();
|
||||
|
||||
foreach (var state in VisitStateMachine(animatorStateMachine))
|
||||
{
|
||||
yield return state;
|
||||
}
|
||||
|
||||
IEnumerable<AnimatorState> VisitStateMachine(AnimatorStateMachine layerStateMachine)
|
||||
{
|
||||
if (!visited.Add(layerStateMachine)) yield break;
|
||||
|
||||
foreach (var state in layerStateMachine.states)
|
||||
{
|
||||
if (state.state == null) continue;
|
||||
|
||||
yield return state.state;
|
||||
}
|
||||
|
||||
foreach (var child in layerStateMachine.stateMachines)
|
||||
{
|
||||
if (child.stateMachine == null) continue;
|
||||
|
||||
if (visited.Contains(child.stateMachine)) continue;
|
||||
foreach (var state in VisitStateMachine(child.stateMachine))
|
||||
{
|
||||
yield return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWriteDefaults(AnimatorStateMachine stateMachine, bool? writeDefaults)
|
||||
{
|
||||
if (!writeDefaults.HasValue) return;
|
||||
|
||||
var queue = new Queue<AnimatorStateMachine>();
|
||||
queue.Enqueue(stateMachine);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var sm = queue.Dequeue();
|
||||
foreach (var state in sm.states)
|
||||
{
|
||||
state.state.writeDefaultValues = writeDefaults.Value;
|
||||
}
|
||||
|
||||
foreach (var child in sm.stateMachines)
|
||||
{
|
||||
queue.Enqueue(child.stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine)
|
||||
{
|
||||
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
|
||||
|
||||
if (_stateMachines.TryGetValue(cacheKey, out var asm))
|
||||
{
|
||||
return asm;
|
||||
}
|
||||
|
||||
asm = _deepClone.DoClone(layerStateMachine, basePath, _cloneMap);
|
||||
|
||||
foreach (var state in WalkAllStates(asm))
|
||||
{
|
||||
foreach (var behavior in state.behaviours)
|
||||
{
|
||||
AdjustBehavior(behavior, basePath);
|
||||
}
|
||||
}
|
||||
|
||||
_stateMachines[cacheKey] = asm;
|
||||
return asm;
|
||||
}
|
||||
|
||||
private void AdjustBehavior(StateMachineBehaviour behavior, string basePath)
|
||||
{
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
switch (behavior)
|
||||
{
|
||||
case VRCAnimatorLayerControl layerControl:
|
||||
{
|
||||
// TODO - need to figure out how to handle cross-layer references. For now this will handle
|
||||
// intra-animator cases.
|
||||
layerControl.layer += _controllerBaseLayer;
|
||||
break;
|
||||
}
|
||||
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
|
||||
case VRCAnimatorPlayAudio playAudio:
|
||||
{
|
||||
if (!string.IsNullOrEmpty(playAudio.SourcePath) && !string.IsNullOrEmpty(basePath) && !playAudio.SourcePath.StartsWith(basePath))
|
||||
{
|
||||
playAudio.SourcePath = $"{basePath}/{playAudio.SourcePath}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 614457d82b1a4b109788029754c9fc1a
|
||||
timeCreated: 1703674134
|
@ -1,212 +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;
|
||||
|
||||
namespace nadena.dev.modular_avatar.animation
|
||||
{
|
||||
using UnityObject = UnityEngine.Object;
|
||||
|
||||
internal class DeepClone
|
||||
{
|
||||
private bool _isSaved;
|
||||
private UnityObject _combined;
|
||||
|
||||
public AnimatorOverrideController OverrideController { get; set; }
|
||||
|
||||
public DeepClone(BuildContext context)
|
||||
{
|
||||
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
|
||||
_combined = context.AssetContainer;
|
||||
}
|
||||
|
||||
public T DoClone<T>(T original,
|
||||
string basePath = null,
|
||||
Dictionary<UnityObject, UnityObject> cloneMap = null
|
||||
) where T : UnityObject
|
||||
{
|
||||
if (original == null) return null;
|
||||
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
|
||||
|
||||
Func<UnityObject, UnityObject> visitor = null;
|
||||
if (basePath != null)
|
||||
{
|
||||
visitor = o => CloneWithPathMapping(o, basePath);
|
||||
}
|
||||
|
||||
// We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes,
|
||||
// MonoScripts...), so check for the types we care about here
|
||||
switch (original)
|
||||
{
|
||||
// Any object referenced by an animator that we intend to mutate needs to be listed here.
|
||||
case Motion _:
|
||||
case AnimatorController _:
|
||||
case AnimatorState _:
|
||||
case AnimatorStateMachine _:
|
||||
case AnimatorTransitionBase _:
|
||||
case StateMachineBehaviour _:
|
||||
break; // We want to clone these types
|
||||
|
||||
case AudioClip _: //Used in VRC Animator Play Audio State Behavior
|
||||
// Leave textures, materials, and script definitions alone
|
||||
case Texture2D _:
|
||||
case MonoScript _:
|
||||
case Material _:
|
||||
return original;
|
||||
|
||||
// Also avoid copying unknown scriptable objects.
|
||||
// This ensures compatibility with e.g. avatar remote, which stores state information in a state
|
||||
// behaviour referencing a custom ScriptableObject
|
||||
case ScriptableObject _:
|
||||
return original;
|
||||
|
||||
default:
|
||||
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
|
||||
}
|
||||
|
||||
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
|
||||
if (OverrideController != null && original is AnimationClip srcClip)
|
||||
{
|
||||
T overrideClip = OverrideController[srcClip] as T;
|
||||
if (overrideClip != null)
|
||||
{
|
||||
original = overrideClip;
|
||||
}
|
||||
}
|
||||
|
||||
if (cloneMap.ContainsKey(original))
|
||||
{
|
||||
return (T)cloneMap[original];
|
||||
}
|
||||
|
||||
var obj = visitor?.Invoke(original);
|
||||
if (obj != null)
|
||||
{
|
||||
cloneMap[original] = obj;
|
||||
if (obj != original)
|
||||
{
|
||||
ObjectRegistry.RegisterReplacedObject(original, obj);
|
||||
}
|
||||
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
|
||||
if (ctor == null || original is ScriptableObject)
|
||||
{
|
||||
obj = UnityObject.Instantiate(original);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = (T)ctor.Invoke(Array.Empty<object>());
|
||||
EditorUtility.CopySerialized(original, obj);
|
||||
}
|
||||
|
||||
cloneMap[original] = obj;
|
||||
ObjectRegistry.RegisterReplacedObject(original, obj);
|
||||
|
||||
if (_isSaved)
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(obj, _combined);
|
||||
}
|
||||
|
||||
SerializedObject so = new SerializedObject(obj);
|
||||
SerializedProperty prop = so.GetIterator();
|
||||
|
||||
bool enterChildren = true;
|
||||
while (prop.Next(enterChildren))
|
||||
{
|
||||
enterChildren = true;
|
||||
switch (prop.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
{
|
||||
var newObj = DoClone(prop.objectReferenceValue, basePath, cloneMap);
|
||||
prop.objectReferenceValue = newObj;
|
||||
break;
|
||||
}
|
||||
// Iterating strings can get super slow...
|
||||
case SerializedPropertyType.String:
|
||||
enterChildren = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
|
||||
{
|
||||
if (o is AnimationClip clip)
|
||||
{
|
||||
// We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset
|
||||
// from a persistent asset. If the asset is persistent, skip cases where path editing isn't required,
|
||||
// or where this is one of the special VRC proxy animations.
|
||||
if (EditorUtility.IsPersistent(o) && (basePath == "" || Util.IsProxyAnimation(clip))) return clip;
|
||||
|
||||
AnimationClip newClip = new AnimationClip();
|
||||
newClip.name = "rebased " + clip.name;
|
||||
if (_isSaved)
|
||||
{
|
||||
AssetDatabase.AddObjectToAsset(newClip, _combined);
|
||||
}
|
||||
|
||||
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
|
||||
{
|
||||
var newBinding = binding;
|
||||
newBinding.path = MapPath(binding, basePath);
|
||||
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
|
||||
AnimationUtility.GetEditorCurve(clip, binding));
|
||||
}
|
||||
|
||||
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
|
||||
{
|
||||
var newBinding = objBinding;
|
||||
newBinding.path = MapPath(objBinding, basePath);
|
||||
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
|
||||
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
|
||||
}
|
||||
|
||||
newClip.wrapMode = clip.wrapMode;
|
||||
newClip.legacy = clip.legacy;
|
||||
newClip.frameRate = clip.frameRate;
|
||||
newClip.localBounds = clip.localBounds;
|
||||
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
|
||||
|
||||
return newClip;
|
||||
}
|
||||
else if (o is Texture)
|
||||
{
|
||||
return o;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapPath(UnityEditor.EditorCurveBinding binding, string basePath)
|
||||
{
|
||||
if (binding.type == typeof(Animator) && binding.path == "")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else
|
||||
{
|
||||
var newPath = binding.path == "" ? basePath : basePath + binding.path;
|
||||
if (newPath.EndsWith("/"))
|
||||
{
|
||||
newPath = newPath.Substring(0, newPath.Length - 1);
|
||||
}
|
||||
|
||||
return newPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
128
Editor/Animation/GameObjectDisableDelayPass.cs
Normal file
128
Editor/Animation/GameObjectDisableDelayPass.cs
Normal file
@ -0,0 +1,128 @@
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.editor;
|
||||
using nadena.dev.ndmf;
|
||||
using nadena.dev.ndmf.animator;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using BuildContext = nadena.dev.ndmf.BuildContext;
|
||||
|
||||
namespace nadena.dev.modular_avatar.animation
|
||||
{
|
||||
/// <summary>
|
||||
/// This pass delays turning GameObjects OFF by one frame when those objects control a ReadableProperty. This
|
||||
/// ensures that we don't expose hidden meshes when removing articles of clothing, for example.
|
||||
/// </summary>
|
||||
internal class GameObjectDelayDisablePass : Pass<GameObjectDelayDisablePass>
|
||||
{
|
||||
protected override void Execute(BuildContext context)
|
||||
{
|
||||
var asc = context.Extension<AnimatorServicesContext>();
|
||||
var activeProxies = context.GetState<ReadablePropertyExtension.Retained>().proxyProps
|
||||
.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
if (activeProxies.Count == 0) return;
|
||||
|
||||
// Filter any proxies not used in animator transitions
|
||||
var usedProxies = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX]
|
||||
.AllReachableNodes().OfType<VirtualTransitionBase>()
|
||||
.SelectMany(t => t.Conditions)
|
||||
.Select(c => c.parameter)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var proxyBinding in activeProxies.ToList())
|
||||
{
|
||||
if (!usedProxies.Contains(proxyBinding.Value))
|
||||
{
|
||||
activeProxies.Remove(proxyBinding.Key);
|
||||
}
|
||||
}
|
||||
|
||||
var fx = asc.ControllerContext.Controllers[VRCAvatarDescriptor.AnimLayerType.FX];
|
||||
if (fx == null) return;
|
||||
|
||||
var nullMotion = new AnimationClip();
|
||||
nullMotion.name = "NullMotion";
|
||||
|
||||
var blendTree = new BlendTree();
|
||||
blendTree.blendType = BlendTreeType.Direct;
|
||||
blendTree.useAutomaticThresholds = false;
|
||||
|
||||
blendTree.children = activeProxies
|
||||
.Select(prop => GenerateDelayChild(nullMotion, (prop.Key, prop.Value)))
|
||||
.ToArray();
|
||||
|
||||
var layer = fx.AddLayer(LayerPriority.Default, "DelayDisable");
|
||||
var state = layer.StateMachine.AddState("DelayDisable");
|
||||
layer.StateMachine.DefaultState = state;
|
||||
|
||||
state.WriteDefaultValues = true;
|
||||
state.Motion = asc.ControllerContext.Clone(blendTree);
|
||||
|
||||
// Ensure the initial state of readable props matches the actual state of the gameobject
|
||||
foreach (var controller in asc.ControllerContext.GetAllControllers())
|
||||
{
|
||||
foreach (var (binding, prop) in activeProxies)
|
||||
{
|
||||
var obj = asc.ObjectPathRemapper.GetObjectForPath(binding.path);
|
||||
|
||||
if (obj != null && controller.Parameters.TryGetValue(prop, out var p))
|
||||
{
|
||||
p.defaultFloat = obj.activeSelf ? 1 : 0;
|
||||
controller.Parameters = controller.Parameters.SetItem(prop, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
|
||||
{
|
||||
var ecb = binding.Item1;
|
||||
var prop = binding.Item2;
|
||||
|
||||
var motion = new AnimationClip();
|
||||
var curve = new AnimationCurve();
|
||||
curve.AddKey(0, 1);
|
||||
AnimationUtility.SetEditorCurve(motion, ecb, curve);
|
||||
|
||||
// Occasionally, we'll have a very small value pop up, probably due to FP errors.
|
||||
// To correct for this, instead of directly using the property in the direct blend tree,
|
||||
// we'll use a 1D blend tree to give ourselves a buffer.
|
||||
|
||||
var bufferBlendTree = new BlendTree();
|
||||
bufferBlendTree.blendType = BlendTreeType.Simple1D;
|
||||
bufferBlendTree.useAutomaticThresholds = false;
|
||||
bufferBlendTree.blendParameter = prop;
|
||||
bufferBlendTree.children = new[]
|
||||
{
|
||||
new ChildMotion
|
||||
{
|
||||
motion = nullMotion,
|
||||
timeScale = 1,
|
||||
threshold = 0
|
||||
},
|
||||
new ChildMotion
|
||||
{
|
||||
motion = nullMotion,
|
||||
timeScale = 1,
|
||||
threshold = 0.01f
|
||||
},
|
||||
new ChildMotion
|
||||
{
|
||||
motion = motion,
|
||||
timeScale = 1,
|
||||
threshold = 1
|
||||
}
|
||||
};
|
||||
|
||||
return new ChildMotion
|
||||
{
|
||||
motion = bufferBlendTree,
|
||||
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
||||
timeScale = 1
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
3
Editor/Animation/GameObjectDisableDelayPass.cs.meta
Normal file
3
Editor/Animation/GameObjectDisableDelayPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b3eb561f76b459fbfbcf29fc4484261
|
||||
timeCreated: 1722222066
|
@ -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,322 +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 UnityEngine;
|
||||
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.animation
|
||||
{
|
||||
#region
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// This extension context tracks when objects are renamed, and updates animations accordingly.
|
||||
/// Users of this context need to be aware that, when creating new curves (or otherwise introducing new motions,
|
||||
/// use context.ObjectPath to obtain a suitable path for the target objects).
|
||||
/// </summary>
|
||||
internal sealed class PathMappings
|
||||
{
|
||||
private AnimationDatabase _animationDatabase;
|
||||
|
||||
private Dictionary<GameObject, List<string>>
|
||||
_objectToOriginalPaths = new Dictionary<GameObject, List<string>>();
|
||||
|
||||
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
|
||||
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
|
||||
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
|
||||
private ImmutableDictionary<string, GameObject> _pathToObject = null;
|
||||
|
||||
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
|
||||
{
|
||||
_animationDatabase = animationDatabase;
|
||||
_objectToOriginalPaths.Clear();
|
||||
_transformLookthroughObjects.Clear();
|
||||
ClearCache();
|
||||
|
||||
foreach (var xform in context.AvatarRootTransform.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {xform.gameObject.AvatarRootPath()});
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_originalPathToMappedPath = null;
|
||||
_transformOriginalPathToMappedPath = null;
|
||||
_pathToObject = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the "transform lookthrough" flag for an object. Any transform animations on this object will be
|
||||
/// redirected to its parent. This is used in Modular Avatar as part of bone merging logic.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void MarkTransformLookthrough(GameObject obj)
|
||||
{
|
||||
_transformLookthroughObjects.Add(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path for use in dynamically generated animations for a given object. This can include objects not
|
||||
/// present at the time of context activation; in this case, they will be assigned a randomly-generated internal
|
||||
/// path and replaced during path remapping with the true path.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public string GetObjectIdentifier(GameObject obj)
|
||||
{
|
||||
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
|
||||
{
|
||||
return paths[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
var internalPath = "_NewlyCreatedObject/" + GUID.Generate() + "/" + obj.AvatarRootPath();
|
||||
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
|
||||
return internalPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an object as having been removed. Its paths will be remapped to its parent.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void MarkRemoved(GameObject obj)
|
||||
{
|
||||
ClearCache();
|
||||
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
|
||||
{
|
||||
var parent = obj.transform.parent.gameObject;
|
||||
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
|
||||
{
|
||||
parentPaths.AddRange(paths);
|
||||
}
|
||||
|
||||
_objectToOriginalPaths.Remove(obj);
|
||||
_transformLookthroughObjects.Remove(obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
|
||||
/// by the new object. References originally to the new object will continue to point to the new object.
|
||||
/// </summary>
|
||||
/// <param name="old"></param>
|
||||
/// <param name="newObject"></param>
|
||||
public void ReplaceObject(GameObject old, GameObject newObject)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
|
||||
{
|
||||
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
|
||||
{
|
||||
newObjectPaths = new List<string>();
|
||||
_objectToOriginalPaths.Add(newObject, newObjectPaths);
|
||||
}
|
||||
|
||||
newObjectPaths.AddRange(paths);
|
||||
|
||||
_objectToOriginalPaths.Remove(old);
|
||||
}
|
||||
|
||||
|
||||
if (_transformLookthroughObjects.Contains(old))
|
||||
{
|
||||
_transformLookthroughObjects.Remove(old);
|
||||
_transformLookthroughObjects.Add(newObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
|
||||
bool transformLookup)
|
||||
{
|
||||
if (cache != null) return cache;
|
||||
|
||||
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
|
||||
|
||||
foreach (var kvp in _objectToOriginalPaths)
|
||||
{
|
||||
var obj = kvp.Key;
|
||||
var paths = kvp.Value;
|
||||
|
||||
if (transformLookup)
|
||||
{
|
||||
while (_transformLookthroughObjects.Contains(obj))
|
||||
{
|
||||
obj = obj.transform.parent.gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
var newPath = obj.AvatarRootPath();
|
||||
foreach (var origPath in paths)
|
||||
{
|
||||
if (!dict.ContainsKey(origPath))
|
||||
{
|
||||
dict = dict.Add(origPath, newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache = dict;
|
||||
return cache;
|
||||
}
|
||||
|
||||
public string MapPath(string path, bool isTransformMapping = false)
|
||||
{
|
||||
ImmutableDictionary<string, string> mappings;
|
||||
|
||||
if (isTransformMapping)
|
||||
{
|
||||
mappings = BuildMapping(ref _originalPathToMappedPath, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
|
||||
}
|
||||
|
||||
if (mappings.TryGetValue(path, out var mappedPath))
|
||||
{
|
||||
return mappedPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
private string MapPath(UnityEditor.EditorCurveBinding binding)
|
||||
{
|
||||
if (binding.type == typeof(Animator) && binding.path == "")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else
|
||||
{
|
||||
return MapPath(binding.path, binding.type == typeof(Transform));
|
||||
}
|
||||
}
|
||||
|
||||
private AnimationClip ApplyMappingsToClip(AnimationClip originalClip,
|
||||
Dictionary<AnimationClip, AnimationClip> clipCache)
|
||||
{
|
||||
if (originalClip == null) return null;
|
||||
if (clipCache != null && clipCache.TryGetValue(originalClip, out var cachedClip)) return cachedClip;
|
||||
|
||||
if (originalClip.IsProxyAnimation()) return originalClip;
|
||||
|
||||
var curveBindings = AnimationUtility.GetCurveBindings(originalClip);
|
||||
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(originalClip);
|
||||
|
||||
bool hasMapping = false;
|
||||
foreach (var binding in curveBindings.Concat(objectBindings))
|
||||
{
|
||||
if (MapPath(binding) != binding.path)
|
||||
{
|
||||
hasMapping = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMapping) return originalClip;
|
||||
|
||||
|
||||
var newClip = new AnimationClip();
|
||||
newClip.name = originalClip.name;
|
||||
|
||||
SerializedObject before = new SerializedObject(originalClip);
|
||||
SerializedObject after = new SerializedObject(newClip);
|
||||
|
||||
var before_hqCurve = before.FindProperty("m_UseHighQualityCurve");
|
||||
var after_hqCurve = after.FindProperty("m_UseHighQualityCurve");
|
||||
|
||||
after_hqCurve.boolValue = before_hqCurve.boolValue;
|
||||
after.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
// TODO - should we use direct SerializedObject manipulation to avoid missing script issues?
|
||||
foreach (var binding in curveBindings)
|
||||
{
|
||||
var newBinding = binding;
|
||||
newBinding.path = MapPath(binding);
|
||||
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
|
||||
AnimationUtility.GetEditorCurve(originalClip, binding));
|
||||
}
|
||||
|
||||
foreach (var objBinding in objectBindings)
|
||||
{
|
||||
var newBinding = objBinding;
|
||||
newBinding.path = MapPath(objBinding);
|
||||
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
|
||||
AnimationUtility.GetObjectReferenceCurve(originalClip, objBinding));
|
||||
}
|
||||
|
||||
newClip.wrapMode = newClip.wrapMode;
|
||||
newClip.legacy = newClip.legacy;
|
||||
newClip.frameRate = newClip.frameRate;
|
||||
newClip.localBounds = newClip.localBounds;
|
||||
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(originalClip));
|
||||
|
||||
if (clipCache != null)
|
||||
{
|
||||
clipCache.Add(originalClip, newClip);
|
||||
}
|
||||
|
||||
return newClip;
|
||||
}
|
||||
|
||||
internal void OnDeactivate(BuildContext context)
|
||||
{
|
||||
Dictionary<AnimationClip, AnimationClip> clipCache = new Dictionary<AnimationClip, AnimationClip>();
|
||||
|
||||
_animationDatabase.ForeachClip(holder =>
|
||||
{
|
||||
if (holder.CurrentClip is AnimationClip clip)
|
||||
{
|
||||
holder.CurrentClip = ApplyMappingsToClip(clip, clipCache);
|
||||
}
|
||||
});
|
||||
|
||||
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
|
||||
_animationDatabase.ForeachPlayAudio(playAudio =>
|
||||
{
|
||||
if (playAudio == null) return;
|
||||
playAudio.SourcePath = MapPath(playAudio.SourcePath, true);
|
||||
});
|
||||
#endif
|
||||
|
||||
foreach (var listener in context.AvatarRootObject.GetComponentsInChildren<IOnCommitObjectRenames>())
|
||||
{
|
||||
listener.OnCommitObjectRenames(context, this);
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject PathToObject(string path)
|
||||
{
|
||||
if (_pathToObject == null)
|
||||
{
|
||||
_pathToObject = _objectToOriginalPaths.SelectMany(kvp => kvp.Value.Select(p => (p, kvp.Key)))
|
||||
.ToImmutableDictionary(t => t.p, t => t.Key);
|
||||
}
|
||||
|
||||
if (_pathToObject.TryGetValue(path, out var obj))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f60ee78d127fda546a84d5396edfc8b2
|
||||
timeCreated: 1691237971
|
@ -1,143 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.ndmf;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.animation
|
||||
{
|
||||
internal class ReadableProperty
|
||||
{
|
||||
private readonly BuildContext _context;
|
||||
private readonly AnimationDatabase _animDB;
|
||||
private readonly AnimationServicesContext _asc;
|
||||
private readonly Dictionary<EditorCurveBinding, string> _alreadyBound = new();
|
||||
private long _nextIndex;
|
||||
|
||||
public ReadableProperty(BuildContext context, AnimationDatabase animDB, AnimationServicesContext asc)
|
||||
{
|
||||
_context = context;
|
||||
_animDB = animDB;
|
||||
_asc = asc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an animator parameter which tracks the effective value of a property on a component. This only
|
||||
/// tracks FX layer properties.
|
||||
/// </summary>
|
||||
/// <param name="ecb"></param>
|
||||
/// <returns></returns>
|
||||
public string ForBinding(string path, Type componentType, string property)
|
||||
{
|
||||
var ecb = new EditorCurveBinding
|
||||
{
|
||||
path = path,
|
||||
type = componentType,
|
||||
propertyName = property
|
||||
};
|
||||
|
||||
if (_alreadyBound.TryGetValue(ecb, out var reader))
|
||||
{
|
||||
return reader;
|
||||
}
|
||||
|
||||
var lastComponent = path.Split("/")[^1];
|
||||
var emuPropName = $"__MA/ReadableProp/{lastComponent}/{componentType}/{property}#{_nextIndex++}";
|
||||
|
||||
float initialValue = 0;
|
||||
var gameObject = _asc.PathMappings.PathToObject(path);
|
||||
Object component = componentType == typeof(GameObject)
|
||||
? gameObject
|
||||
: gameObject?.GetComponent(componentType);
|
||||
if (component != null)
|
||||
{
|
||||
var so = new SerializedObject(component);
|
||||
var prop = so.FindProperty(property);
|
||||
if (prop != null)
|
||||
switch (prop.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Boolean:
|
||||
initialValue = prop.boolValue ? 1 : 0;
|
||||
break;
|
||||
case SerializedPropertyType.Float:
|
||||
initialValue = prop.floatValue;
|
||||
break;
|
||||
case SerializedPropertyType.Integer:
|
||||
initialValue = prop.intValue;
|
||||
break;
|
||||
default: throw new NotImplementedException($"Property type {prop.type} not supported");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_asc.AddPropertyDefinition(new AnimatorControllerParameter
|
||||
{
|
||||
defaultFloat = initialValue,
|
||||
name = emuPropName,
|
||||
type = AnimatorControllerParameterType.Float
|
||||
});
|
||||
|
||||
BindProperty(ecb, emuPropName);
|
||||
|
||||
_alreadyBound[ecb] = emuPropName;
|
||||
|
||||
return emuPropName;
|
||||
}
|
||||
|
||||
private void BindProperty(EditorCurveBinding ecb, string propertyName)
|
||||
{
|
||||
var boundProp = new EditorCurveBinding
|
||||
{
|
||||
path = "",
|
||||
type = typeof(Animator),
|
||||
propertyName = propertyName
|
||||
};
|
||||
|
||||
foreach (var clip in _animDB.ClipsForPath(ecb.path)) ProcessAnyClip(clip);
|
||||
|
||||
void ProcessBlendTree(BlendTree blendTree)
|
||||
{
|
||||
foreach (var child in blendTree.children)
|
||||
switch (child.motion)
|
||||
{
|
||||
case AnimationClip animationClip:
|
||||
ProcessAnimationClip(animationClip);
|
||||
break;
|
||||
|
||||
case BlendTree subBlendTree:
|
||||
ProcessBlendTree(subBlendTree);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessAnimationClip(AnimationClip animationClip)
|
||||
{
|
||||
var curve = AnimationUtility.GetEditorCurve(animationClip, ecb);
|
||||
if (curve == null) return;
|
||||
|
||||
AnimationUtility.SetEditorCurve(animationClip, boundProp, curve);
|
||||
}
|
||||
|
||||
void ProcessAnyClip(AnimationDatabase.ClipHolder clip)
|
||||
{
|
||||
switch (clip.CurrentClip)
|
||||
{
|
||||
case AnimationClip animationClip:
|
||||
ProcessAnimationClip(animationClip);
|
||||
break;
|
||||
|
||||
case BlendTree blendTree:
|
||||
ProcessBlendTree(blendTree);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ForActiveSelf(string path)
|
||||
{
|
||||
return ForBinding(path, typeof(GameObject), "m_IsActive");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using nadena.dev.ndmf;
|
||||
using nadena.dev.ndmf.animator;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
@ -20,41 +22,36 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var values = context.GetState<DefaultValues>()?.InitialValueOverrides
|
||||
?? ImmutableDictionary<string, float>.Empty;
|
||||
|
||||
foreach (var layer in context.AvatarDescriptor.baseAnimationLayers
|
||||
.Concat(context.AvatarDescriptor.specialAnimationLayers))
|
||||
var asc = context.Extension<AnimatorServicesContext>();
|
||||
|
||||
foreach (var controller in asc.ControllerContext.GetAllControllers())
|
||||
{
|
||||
if (layer.isDefault || layer.animatorController == null) continue;
|
||||
|
||||
// We should have converted anything that's not an AnimationController by now
|
||||
var controller = layer.animatorController as AnimatorController;
|
||||
if (controller == null || !context.IsTemporaryAsset(controller))
|
||||
var parameters = controller.Parameters;
|
||||
foreach (var (name, parameter) in parameters)
|
||||
{
|
||||
throw new Exception("Leaked unexpected controller: " + layer.animatorController + " (type " + layer.animatorController?.GetType() + ")");
|
||||
}
|
||||
if (!values.TryGetValue(name, out var defaultValue)) continue;
|
||||
|
||||
var parameters = controller.parameters;
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
if (!values.TryGetValue(parameters[i].name, out var defaultValue)) continue;
|
||||
|
||||
switch (parameters[i].type)
|
||||
switch (parameter.type)
|
||||
{
|
||||
case AnimatorControllerParameterType.Bool:
|
||||
parameters[i].defaultBool = defaultValue > 0.5f;
|
||||
parameter.defaultBool = defaultValue != 0.0f;
|
||||
break;
|
||||
case AnimatorControllerParameterType.Int:
|
||||
parameters[i].defaultInt = Mathf.RoundToInt(defaultValue);
|
||||
parameter.defaultInt = Mathf.RoundToInt(defaultValue);
|
||||
break;
|
||||
case AnimatorControllerParameterType.Float:
|
||||
parameters[i].defaultFloat = defaultValue;
|
||||
parameter.defaultFloat = defaultValue;
|
||||
break;
|
||||
default:
|
||||
continue; // unhandled type, e.g. trigger
|
||||
}
|
||||
|
||||
parameters = parameters.SetItem(name, parameter);
|
||||
}
|
||||
|
||||
controller.parameters = parameters;
|
||||
controller.Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -1,12 +1,14 @@
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||
using nadena.dev.ndmf.animator;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
@ -17,11 +19,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
*/
|
||||
internal class BlendshapeSyncAnimationProcessor
|
||||
{
|
||||
private BuildContext _context;
|
||||
private Dictionary<Motion, Motion> _motionCache;
|
||||
private readonly ndmf.BuildContext _context;
|
||||
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
|
||||
|
||||
private struct SummaryBinding
|
||||
internal BlendshapeSyncAnimationProcessor(ndmf.BuildContext context)
|
||||
{
|
||||
_context = context;
|
||||
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
||||
}
|
||||
|
||||
private struct SummaryBinding : IEquatable<SummaryBinding>
|
||||
{
|
||||
private const string PREFIX = "blendShape.";
|
||||
public string path;
|
||||
@ -33,71 +40,76 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
this.propertyName = PREFIX + blendShape;
|
||||
}
|
||||
|
||||
public static SummaryBinding FromEditorBinding(EditorCurveBinding binding)
|
||||
public static SummaryBinding? FromEditorBinding(EditorCurveBinding binding)
|
||||
{
|
||||
if (binding.type != typeof(SkinnedMeshRenderer) || !binding.propertyName.StartsWith(PREFIX))
|
||||
{
|
||||
return new SummaryBinding();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SummaryBinding(binding.path, binding.propertyName.Substring(PREFIX.Length));
|
||||
}
|
||||
|
||||
public EditorCurveBinding ToEditorCurveBinding()
|
||||
{
|
||||
return EditorCurveBinding.FloatCurve(
|
||||
path,
|
||||
typeof(SkinnedMeshRenderer),
|
||||
propertyName
|
||||
);
|
||||
}
|
||||
|
||||
public bool Equals(SummaryBinding other)
|
||||
{
|
||||
return path == other.path && propertyName == other.propertyName;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is SummaryBinding other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(path, propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPreprocessAvatar(BuildContext context)
|
||||
public void OnPreprocessAvatar()
|
||||
{
|
||||
_context = context;
|
||||
var avatarGameObject = context.AvatarRootObject;
|
||||
var animDb = _context.AnimationDatabase;
|
||||
|
||||
var avatarDescriptor = context.AvatarDescriptor;
|
||||
var avatarGameObject = _context.AvatarRootObject;
|
||||
var animDb = _context.Extension<AnimatorServicesContext>().AnimationIndex;
|
||||
|
||||
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
|
||||
_motionCache = new Dictionary<Motion, Motion>();
|
||||
|
||||
var components = avatarGameObject.GetComponentsInChildren<ModularAvatarBlendshapeSync>(true);
|
||||
if (components.Length == 0) return;
|
||||
|
||||
var layers = avatarDescriptor.baseAnimationLayers;
|
||||
var fxIndex = -1;
|
||||
AnimatorController controller = null;
|
||||
for (int i = 0; i < layers.Length; i++)
|
||||
{
|
||||
if (layers[i].type == VRCAvatarDescriptor.AnimLayerType.FX && !layers[i].isDefault)
|
||||
{
|
||||
if (layers[i].animatorController is AnimatorController c && c != null)
|
||||
{
|
||||
fxIndex = i;
|
||||
controller = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
// Nothing to do, return
|
||||
}
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
BuildReport.ReportingObject(component, () => ProcessComponent(avatarGameObject, component));
|
||||
}
|
||||
|
||||
// Walk and transform all clips
|
||||
animDb.ForeachClip(clip =>
|
||||
var clips = new HashSet<VirtualClip>();
|
||||
foreach (var key in _bindingMappings.Keys)
|
||||
{
|
||||
if (clip.CurrentClip is AnimationClip anim)
|
||||
{
|
||||
BuildReport.ReportingObject(clip.CurrentClip,
|
||||
() => { clip.CurrentClip = TransformMotion(anim); });
|
||||
}
|
||||
});
|
||||
var ecb = key.ToEditorCurveBinding();
|
||||
clips.UnionWith(animDb.GetClipsForBinding(ecb));
|
||||
}
|
||||
|
||||
// Walk and transform all clips
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
ProcessClip(clip);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessComponent(GameObject avatarGameObject, ModularAvatarBlendshapeSync component)
|
||||
{
|
||||
var targetObj = RuntimeUtil.RelativePath(avatarGameObject, component.gameObject);
|
||||
|
||||
if (targetObj == null) return;
|
||||
|
||||
foreach (var binding in component.Bindings)
|
||||
{
|
||||
var refObj = binding.ReferenceMesh.Get(component);
|
||||
@ -106,6 +118,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (refSmr == null) continue;
|
||||
|
||||
var refPath = RuntimeUtil.RelativePath(avatarGameObject, refObj);
|
||||
if (refPath == null) continue;
|
||||
|
||||
var srcBinding = new SummaryBinding(refPath, binding.Blendshape);
|
||||
|
||||
@ -123,108 +136,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
Motion TransformMotion(Motion motion)
|
||||
private void ProcessClip(VirtualClip clip)
|
||||
{
|
||||
if (motion == null) return null;
|
||||
if (_motionCache.TryGetValue(motion, out var cached)) return cached;
|
||||
|
||||
switch (motion)
|
||||
foreach (var binding in clip.GetFloatCurveBindings().ToList())
|
||||
{
|
||||
case AnimationClip clip:
|
||||
{
|
||||
motion = ProcessClip(clip);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case BlendTree tree:
|
||||
{
|
||||
bool anyChanged = false;
|
||||
var children = tree.children;
|
||||
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
{
|
||||
var newM = TransformMotion(children[i].motion);
|
||||
if (newM != children[i].motion)
|
||||
{
|
||||
anyChanged = true;
|
||||
children[i].motion = newM;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanged)
|
||||
{
|
||||
var newTree = new BlendTree();
|
||||
EditorUtility.CopySerialized(tree, newTree);
|
||||
_context.SaveAsset(newTree);
|
||||
|
||||
newTree.children = children;
|
||||
motion = newTree;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.LogWarning($"Ignoring unsupported motion type {motion.GetType()}");
|
||||
break;
|
||||
}
|
||||
|
||||
_motionCache[motion] = motion;
|
||||
return motion;
|
||||
}
|
||||
|
||||
AnimationClip ProcessClip(AnimationClip origClip)
|
||||
{
|
||||
var clip = origClip;
|
||||
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip);
|
||||
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (!_bindingMappings.TryGetValue(SummaryBinding.FromEditorBinding(binding), out var dstBindings))
|
||||
var srcBinding = SummaryBinding.FromEditorBinding(binding);
|
||||
if (srcBinding == null || !_bindingMappings.TryGetValue(srcBinding.Value, out var dstBindings))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (clip == origClip)
|
||||
{
|
||||
clip = Object.Instantiate(clip);
|
||||
}
|
||||
|
||||
var curve = clip.GetFloatCurve(binding);
|
||||
foreach (var dst in dstBindings)
|
||||
{
|
||||
clip.SetCurve(dst.path, typeof(SkinnedMeshRenderer), dst.propertyName,
|
||||
AnimationUtility.GetEditorCurve(origClip, binding));
|
||||
}
|
||||
}
|
||||
|
||||
return clip;
|
||||
}
|
||||
|
||||
IEnumerable<AnimatorState> AllStates(AnimatorController controller)
|
||||
{
|
||||
HashSet<AnimatorStateMachine> visitedStateMachines = new HashSet<AnimatorStateMachine>();
|
||||
Queue<AnimatorStateMachine> stateMachines = new Queue<AnimatorStateMachine>();
|
||||
|
||||
foreach (var layer in controller.layers)
|
||||
{
|
||||
if (layer.stateMachine != null)
|
||||
stateMachines.Enqueue(layer.stateMachine);
|
||||
}
|
||||
|
||||
while (stateMachines.Count > 0)
|
||||
{
|
||||
var next = stateMachines.Dequeue();
|
||||
if (visitedStateMachines.Contains(next)) continue;
|
||||
visitedStateMachines.Add(next);
|
||||
|
||||
foreach (var state in next.states)
|
||||
{
|
||||
yield return state.state;
|
||||
}
|
||||
|
||||
foreach (var sm in next.stateMachines)
|
||||
{
|
||||
stateMachines.Enqueue(sm.stateMachine);
|
||||
clip.SetFloatCurve(dst.ToEditorCurveBinding(), curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.animation;
|
||||
using nadena.dev.ndmf;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class BuildContext
|
||||
{
|
||||
internal readonly nadena.dev.ndmf.BuildContext PluginBuildContext;
|
||||
internal readonly ndmf.BuildContext PluginBuildContext;
|
||||
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
|
||||
@ -25,14 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
internal GameObject AvatarRootObject => PluginBuildContext.AvatarRootObject;
|
||||
internal Transform AvatarRootTransform => PluginBuildContext.AvatarRootTransform;
|
||||
|
||||
internal AnimationDatabase AnimationDatabase =>
|
||||
PluginBuildContext.Extension<AnimationServicesContext>().AnimationDatabase;
|
||||
|
||||
internal PathMappings PathMappings =>
|
||||
PluginBuildContext.Extension<AnimationServicesContext>().PathMappings;
|
||||
|
||||
internal UnityEngine.Object AssetContainer => PluginBuildContext.AssetContainer;
|
||||
|
||||
private bool SaveImmediate = false;
|
||||
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
@ -44,13 +31,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
/// replace the source menu for the purposes of identifying any other MAMIs that might install to the same
|
||||
/// menu asset.
|
||||
/// </summary>
|
||||
internal readonly Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> PostProcessControls
|
||||
= new Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>();
|
||||
internal readonly Dictionary<Object, Action<VRCExpressionsMenu.Control>> PostProcessControls = new();
|
||||
#endif
|
||||
public static implicit operator BuildContext(ndmf.BuildContext ctx) =>
|
||||
ctx.Extension<ModularAvatarContext>().BuildContext;
|
||||
|
||||
public BuildContext(nadena.dev.ndmf.BuildContext PluginBuildContext)
|
||||
public BuildContext(ndmf.BuildContext PluginBuildContext)
|
||||
{
|
||||
this.PluginBuildContext = PluginBuildContext;
|
||||
}
|
||||
@ -71,64 +57,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
|
||||
|
||||
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
|
||||
PluginBuildContext.AssetSaver.SaveAsset(obj);
|
||||
}
|
||||
|
||||
public AnimatorController CreateAnimator(AnimatorController toClone = null)
|
||||
{
|
||||
AnimatorController controller;
|
||||
if (toClone != null)
|
||||
{
|
||||
controller = Object.Instantiate(toClone);
|
||||
}
|
||||
else
|
||||
{
|
||||
controller = new AnimatorController();
|
||||
}
|
||||
|
||||
SaveAsset(controller);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
|
||||
{
|
||||
if (controller == null) return null;
|
||||
|
||||
var merger = new AnimatorCombiner(PluginBuildContext, controller.name + " (clone)");
|
||||
switch (controller)
|
||||
{
|
||||
case AnimatorController ac:
|
||||
merger.AddController("", ac, null);
|
||||
break;
|
||||
case AnimatorOverrideController oac:
|
||||
merger.AddOverrideController("", oac, null);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
|
||||
}
|
||||
|
||||
var result = merger.Finish();
|
||||
|
||||
ObjectRegistry.RegisterReplacedObject(controller, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public AnimatorController ConvertAnimatorController(RuntimeAnimatorController anyController)
|
||||
{
|
||||
switch (anyController)
|
||||
{
|
||||
case AnimatorController ac:
|
||||
return ac;
|
||||
case AnimatorOverrideController aoc:
|
||||
var merger = new AnimatorCombiner(PluginBuildContext, anyController.name + " (clone)");
|
||||
merger.AddOverrideController("", aoc, null);
|
||||
return merger.Finish();
|
||||
default:
|
||||
throw new Exception("Unknown RuntimeAnimatorContoller type " + anyController.GetType());
|
||||
}
|
||||
}
|
||||
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Experimental.Rendering;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
@ -19,6 +18,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
internal static void FixupExpressionsMenu(BuildContext context)
|
||||
{
|
||||
if (!context.AvatarDescriptor) return;
|
||||
|
||||
context.AvatarDescriptor.customExpressions = true;
|
||||
|
||||
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;
|
||||
@ -42,7 +43,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
|
||||
var parameters = context.AvatarDescriptor.expressionParameters.parameters
|
||||
?? new VRCExpressionParameters.Parameter[0];
|
||||
?? Array.Empty<VRCExpressionParameters.Parameter>();
|
||||
var parameterNames = parameters.Select(p => p.name).ToImmutableHashSet();
|
||||
|
||||
if (!context.PluginBuildContext.IsTemporaryAsset(expressionsMenu))
|
||||
@ -91,6 +92,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
control.icon = newIcon;
|
||||
}
|
||||
|
||||
if (control.subMenu != null)
|
||||
{
|
||||
VisitMenu(control.subMenu);
|
||||
}
|
||||
|
||||
if (control.labels != null)
|
||||
{
|
||||
for (int i = 0; i < control.labels.Length; i++)
|
||||
@ -113,11 +119,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_ANDROID
|
||||
private const TextureFormat TargetFormat = TextureFormat.ASTC_4x4;
|
||||
#else
|
||||
private const TextureFormat TargetFormat = TextureFormat.DXT5;
|
||||
#endif
|
||||
internal static TextureFormat TargetFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (EditorUserBuildSettings.activeBuildTarget)
|
||||
{
|
||||
case BuildTarget.StandaloneWindows64:
|
||||
return TextureFormat.DXT5;
|
||||
default:
|
||||
return TextureFormat.ASTC_4x4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Texture2D MaybeScaleIcon(BuildContext context, Texture2D original)
|
||||
{
|
||||
@ -126,10 +141,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
return original;
|
||||
}
|
||||
|
||||
var newRatio = Math.Min(256f / original.width, 256f / original.height);
|
||||
var newRatio = Math.Min(1, Math.Min(256f / original.width, 256f / original.height));
|
||||
var newWidth = Math.Min(256, Mathf.RoundToInt(original.width * newRatio));
|
||||
var newHeight = Math.Min(256, Mathf.RoundToInt(original.height * newRatio));
|
||||
|
||||
// Round up to a multiple of four
|
||||
newWidth = (newWidth + 3) & ~3;
|
||||
newHeight = (newHeight + 3) & ~3;
|
||||
|
||||
var newTex = new Texture2D(newWidth, newHeight, TextureFormat.RGBA32, true);
|
||||
context.SaveAsset(newTex);
|
||||
|
||||
@ -161,4 +180,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@ -11,6 +11,8 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
||||
{
|
||||
internal class PatchLoader
|
||||
{
|
||||
private const string HarmonyId = "nadena.dev.modular_avatar";
|
||||
|
||||
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
|
||||
{
|
||||
//HierarchyViewPatches.Patch,
|
||||
@ -19,7 +21,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
|
||||
[InitializeOnLoadMethod]
|
||||
static void ApplyPatches()
|
||||
{
|
||||
var harmony = new Harmony("nadena.dev.modular_avatar");
|
||||
var harmony = new Harmony(HarmonyId);
|
||||
|
||||
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); };
|
||||
}
|
||||
}
|
||||
}
|
@ -23,30 +23,30 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
// Licensed under the MIT License
|
||||
private static string[][] boneNamePatterns = new[]
|
||||
{
|
||||
new[] {"Hips", "Hip"},
|
||||
new[] {"Hips", "Hip", "pelvis"},
|
||||
new[]
|
||||
{
|
||||
"LeftUpperLeg", "UpperLeg_Left", "UpperLeg_L", "Leg_Left", "Leg_L", "ULeg_L", "Left leg", "LeftUpLeg",
|
||||
"UpLeg.L"
|
||||
"UpLeg.L", "Thigh_L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightUpperLeg", "UpperLeg_Right", "UpperLeg_R", "Leg_Right", "Leg_R", "ULeg_R", "Right leg",
|
||||
"RightUpLeg", "UpLeg.R"
|
||||
"RightUpLeg", "UpLeg.R", "Thigh_R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg"
|
||||
"LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L", "shin.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R", "LLeg_R", "Right knee",
|
||||
"RightLeg"
|
||||
"RightLeg", "leg_R", "shin.R"
|
||||
},
|
||||
new[] {"LeftFoot", "Foot_Left", "Foot_L", "Ankle_L", "Foot.L.001", "Left ankle", "heel.L", "heel"},
|
||||
new[] {"RightFoot", "Foot_Right", "Foot_R", "Ankle_R", "Foot.R.001", "Right ankle", "heel.R", "heel"},
|
||||
new[] {"Spine"},
|
||||
new[] {"Chest", "Bust"},
|
||||
new[] {"Spine", "spine01"},
|
||||
new[] {"Chest", "Bust", "spine02", "upper_chest"},
|
||||
new[] {"Neck"},
|
||||
new[] {"Head"},
|
||||
new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"},
|
||||
@ -60,8 +60,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
"RightUpperArm", "UpperArm_Right", "UpperArm_R", "Arm_Right", "Arm_R", "UArm_R", "Right arm",
|
||||
"UpperRightArm"
|
||||
},
|
||||
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L", "LArm_L", "Left elbow", "LeftForeArm", "Elbow_L"},
|
||||
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R", "LArm_R", "Right elbow", "RightForeArm", "Elbow_R"},
|
||||
new[] {"LeftLowerArm", "LowerArm_Left", "LowerArm_L", "LArm_L", "Left elbow", "LeftForeArm", "Elbow_L", "forearm_L", "ForArm_L"},
|
||||
new[] {"RightLowerArm", "LowerArm_Right", "LowerArm_R", "LArm_R", "Right elbow", "RightForeArm", "Elbow_R", "forearm_R", "ForArm_R"},
|
||||
new[] {"LeftHand", "Hand_Left", "Hand_L", "Left wrist", "Wrist_L"},
|
||||
new[] {"RightHand", "Hand_Right", "Hand_R", "Right wrist", "Wrist_R"},
|
||||
new[]
|
||||
@ -80,162 +80,165 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
new[]
|
||||
{
|
||||
"LeftThumbProximal", "ProximalThumb_Left", "ProximalThumb_L", "Thumb1_L", "ThumbFinger1_L",
|
||||
"LeftHandThumb1", "Thumb Proximal.L", "Thunb1_L"
|
||||
"LeftHandThumb1", "Thumb Proximal.L", "Thunb1_L", "finger01_01_L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftThumbIntermediate", "IntermediateThumb_Left", "IntermediateThumb_L", "Thumb2_L", "ThumbFinger2_L",
|
||||
"LeftHandThumb2", "Thumb Intermediate.L", "Thunb2_L"
|
||||
"LeftHandThumb2", "Thumb Intermediate.L", "Thunb2_L", "finger01_02_L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftThumbDistal", "DistalThumb_Left", "DistalThumb_L", "Thumb3_L", "ThumbFinger3_L", "LeftHandThumb3",
|
||||
"Thumb Distal.L", "Thunb3_L"
|
||||
"Thumb Distal.L", "Thunb3_L", "finger01_03_L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L", "Index1_L", "IndexFinger1_L",
|
||||
"LeftHandIndex1", "Index Proximal.L"
|
||||
"LeftHandIndex1", "Index Proximal.L", "finger02_01_L", "f_index.01.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L", "Index2_L", "IndexFinger2_L",
|
||||
"LeftHandIndex2", "Index Intermediate.L"
|
||||
"LeftHandIndex2", "Index Intermediate.L", "finger02_02_L", "f_index.02.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L", "Index3_L", "IndexFinger3_L", "LeftHandIndex3",
|
||||
"Index Distal.L"
|
||||
"Index Distal.L", "finger02_03_L", "f_index.03.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L", "Middle1_L", "MiddleFinger1_L",
|
||||
"LeftHandMiddle1", "Middle Proximal.L"
|
||||
"LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L", "f_middle.01.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L", "Middle2_L",
|
||||
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L"
|
||||
"MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L", "f_middle.02.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L", "Middle3_L", "MiddleFinger3_L",
|
||||
"LeftHandMiddle3", "Middle Distal.L"
|
||||
"LeftHandMiddle3", "Middle Distal.L", "finger03_03_L", "f_middle.03.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftRingProximal", "ProximalRing_Left", "ProximalRing_L", "Ring1_L", "RingFinger1_L", "LeftHandRing1",
|
||||
"Ring Proximal.L"
|
||||
"Ring Proximal.L", "finger04_01_L", "f_ring.01.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L", "Ring2_L", "RingFinger2_L",
|
||||
"LeftHandRing2", "Ring Intermediate.L"
|
||||
"LeftHandRing2", "Ring Intermediate.L", "finger04_02_L", "f_ring.02.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftRingDistal", "DistalRing_Left", "DistalRing_L", "Ring3_L", "RingFinger3_L", "LeftHandRing3",
|
||||
"Ring Distal.L"
|
||||
"Ring Distal.L", "finger04_03_L", "f_ring.03.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L", "Little1_L", "LittleFinger1_L",
|
||||
"LeftHandPinky1", "Little Proximal.L"
|
||||
"LeftHandPinky1", "Little Proximal.L", "finger05_01_L", "f_pinky.01.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L", "Little2_L",
|
||||
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L"
|
||||
"LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L", "f_pinky.02.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L", "Little3_L", "LittleFinger3_L",
|
||||
"LeftHandPinky3", "Little Distal.L"
|
||||
"LeftHandPinky3", "Little Distal.L", "finger05_03_L", "f_pinky.03.L"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightThumbProximal", "ProximalThumb_Right", "ProximalThumb_R", "Thumb1_R", "ThumbFinger1_R",
|
||||
"RightHandThumb1", "Thumb Proximal.R", "Thunb1_R"
|
||||
"RightHandThumb1", "Thumb Proximal.R", "Thunb1_R", "finger01_01_R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightThumbIntermediate", "IntermediateThumb_Right", "IntermediateThumb_R", "Thumb2_R",
|
||||
"ThumbFinger2_R", "RightHandThumb2", "Thumb Intermediate.R", "Thunb2_R"
|
||||
"ThumbFinger2_R", "RightHandThumb2", "Thumb Intermediate.R", "Thunb2_R", "finger01_02_R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightThumbDistal", "DistalThumb_Right", "DistalThumb_R", "Thumb3_R", "ThumbFinger3_R",
|
||||
"RightHandThumb3", "Thumb Distal.R", "Thunb3_R"
|
||||
"RightHandThumb3", "Thumb Distal.R", "Thunb3_R", "finger01_03_R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R", "Index1_R", "IndexFinger1_R",
|
||||
"RightHandIndex1", "Index Proximal.R"
|
||||
"RightHandIndex1", "Index Proximal.R", "finger02_01_R", "f_index.01.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R", "Index2_R",
|
||||
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R"
|
||||
"IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R", "f_index.02.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightIndexDistal", "DistalIndex_Right", "DistalIndex_R", "Index3_R", "IndexFinger3_R",
|
||||
"RightHandIndex3", "Index Distal.R"
|
||||
"RightHandIndex3", "Index Distal.R", "finger02_03_R", "f_index.03.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R", "Middle1_R", "MiddleFinger1_R",
|
||||
"RightHandMiddle1", "Middle Proximal.R"
|
||||
"RightHandMiddle1", "Middle Proximal.R", "finger03_01_R", "f_middle.01.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R", "Middle2_R",
|
||||
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R"
|
||||
"MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R", "f_middle.02.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R", "Middle3_R", "MiddleFinger3_R",
|
||||
"RightHandMiddle3", "Middle Distal.R"
|
||||
"RightHandMiddle3", "Middle Distal.R", "finger03_03_R", "f_middle.03.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightRingProximal", "ProximalRing_Right", "ProximalRing_R", "Ring1_R", "RingFinger1_R",
|
||||
"RightHandRing1", "Ring Proximal.R"
|
||||
"RightHandRing1", "Ring Proximal.R", "finger04_01_R", "f_ring.01.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R", "Ring2_R", "RingFinger2_R",
|
||||
"RightHandRing2", "Ring Intermediate.R"
|
||||
"RightHandRing2", "Ring Intermediate.R", "finger04_02_R", "f_ring.02.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightRingDistal", "DistalRing_Right", "DistalRing_R", "Ring3_R", "RingFinger3_R", "RightHandRing3",
|
||||
"Ring Distal.R"
|
||||
"Ring Distal.R", "finger04_03_R", "f_ring.03.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R", "Little1_R", "LittleFinger1_R",
|
||||
"RightHandPinky1", "Little Proximal.R"
|
||||
"RightHandPinky1", "Little Proximal.R", "finger05_01_R", "f_pinky.01.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R", "Little2_R",
|
||||
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R"
|
||||
"LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R", "f_pinky.02.R"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"RightLittleDistal", "DistalLittle_Right", "DistalLittle_R", "Little3_R", "LittleFinger3_R",
|
||||
"RightHandPinky3", "Little Distal.R"
|
||||
"RightHandPinky3", "Little Distal.R", "finger05_03_R", "f_pinky.03.R"
|
||||
},
|
||||
new[] {"UpperChest", "UChest"},
|
||||
};
|
||||
|
||||
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
|
||||
|
||||
internal static ImmutableHashSet<string> AllBoneNames =
|
||||
boneNamePatterns.SelectMany(x => x).Select(NormalizeName).ToImmutableHashSet();
|
||||
|
||||
internal static string NormalizeName(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
name = Regex.Replace(name, "[0-9 ._]", "");
|
||||
name = Regex.Replace(name, "^bone_|[0-9 ._]", "");
|
||||
|
||||
return name;
|
||||
}
|
||||
@ -243,6 +246,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
|
||||
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void InsertboneNamePatternsToRuntime()
|
||||
{
|
||||
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
|
||||
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
|
||||
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
|
||||
}
|
||||
|
||||
static HeuristicBoneMapper()
|
||||
{
|
||||
var pat_end_side = new Regex(@"[_\.]([LR])$");
|
||||
@ -306,7 +317,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
GameObject src,
|
||||
GameObject newParent,
|
||||
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>();
|
||||
@ -355,21 +368,65 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var childName = child.gameObject.name;
|
||||
var targetObjectName = childName.Substring(config.prefix.Length,
|
||||
childName.Length - config.prefix.Length - config.suffix.Length);
|
||||
|
||||
if (!NameToBoneMap.TryGetValue(
|
||||
NormalizeName(targetObjectName), out var bodyBones))
|
||||
List<HumanBodyBones> bodyBones = null;
|
||||
var isMapped = false;
|
||||
|
||||
if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
|
||||
{
|
||||
if (avatarAnimator != null)
|
||||
{
|
||||
var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone);
|
||||
if (avatarBone != null && unassigned.Contains(avatarBone))
|
||||
{
|
||||
mappings[child] = avatarBone;
|
||||
unassigned.Remove(avatarBone);
|
||||
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
|
||||
isMapped = true;
|
||||
} else {
|
||||
bodyBones = new List<HumanBodyBones> { outfitHumanoidBone };
|
||||
}
|
||||
} else {
|
||||
bodyBones = new List<HumanBodyBones>() { outfitHumanoidBone };
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue(
|
||||
NormalizeName(targetObjectName), out bodyBones))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
|
||||
if (!isMapped)
|
||||
{
|
||||
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
||||
foreach (var bodyBone in bodyBones)
|
||||
{
|
||||
mappings[child] = targetObject;
|
||||
unassigned.Remove(targetObject);
|
||||
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
||||
break;
|
||||
if (avatarAnimator != null)
|
||||
{
|
||||
var avatarBone = avatarAnimator.GetBoneTransform(bodyBone);
|
||||
if (avatarBone != null && unassigned.Contains(avatarBone))
|
||||
{
|
||||
mappings[child] = avatarBone;
|
||||
unassigned.Remove(avatarBone);
|
||||
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
|
||||
isMapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMapped)
|
||||
{
|
||||
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
|
||||
{
|
||||
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
||||
{
|
||||
mappings[child] = targetObject;
|
||||
unassigned.Remove(targetObject);
|
||||
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
||||
isMapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
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));
|
||||
if (target == null) return;
|
||||
@ -399,7 +456,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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)
|
||||
{
|
||||
@ -419,4 +476,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,20 +8,28 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (CustomGUI(position, property, label)) return;
|
||||
|
||||
var xButtonSize = EditorStyles.miniButtonRight.CalcSize(new GUIContent("x"));
|
||||
var xButtonRect = new Rect(position.xMax - xButtonSize.x, position.y, xButtonSize.x, position.height);
|
||||
position = new Rect(position.x, position.y, position.width - xButtonSize.x, position.height);
|
||||
|
||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||
|
||||
position = EditorGUI.PrefixLabel(position, label);
|
||||
|
||||
using (var scope = new ZeroIndentScope())
|
||||
{
|
||||
EditorGUI.LabelField(position,
|
||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||
label = EditorGUI.BeginProperty(position, label, property);
|
||||
try
|
||||
{
|
||||
if (CustomGUI(position, property, label)) return;
|
||||
|
||||
var xButtonSize = EditorStyles.miniButtonRight.CalcSize(new GUIContent("x"));
|
||||
var xButtonRect = new Rect(position.xMax - xButtonSize.x, position.y, xButtonSize.x, position.height);
|
||||
position = new Rect(position.x, position.y, position.width - xButtonSize.x, position.height);
|
||||
|
||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||
|
||||
position = EditorGUI.PrefixLabel(position, label);
|
||||
|
||||
using (var scope = new ZeroIndentScope())
|
||||
{
|
||||
EditorGUI.LabelField(position,
|
||||
string.IsNullOrEmpty(property.stringValue) ? "(null)" : property.stringValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,13 +37,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
var color = GUI.contentColor;
|
||||
|
||||
var targetObjectProp = property.FindPropertyRelative(nameof(AvatarObjectReference.targetObject));
|
||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||
|
||||
try
|
||||
{
|
||||
var avatarTransform = findContainingAvatarTransform(property);
|
||||
if (avatarTransform == null) return false;
|
||||
|
||||
|
||||
bool isRoot = property.stringValue == AvatarObjectReference.AVATAR_ROOT;
|
||||
bool isNull = string.IsNullOrEmpty(property.stringValue);
|
||||
Transform target;
|
||||
@ -43,6 +52,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
else if (isRoot) target = avatarTransform;
|
||||
else target = avatarTransform.Find(property.stringValue);
|
||||
|
||||
if (targetObjectProp.objectReferenceValue is GameObject go &&
|
||||
(go.transform == avatarTransform || go.transform.IsChildOf(avatarTransform)))
|
||||
{
|
||||
target = go.transform;
|
||||
isNull = false;
|
||||
isRoot = target == avatarTransform;
|
||||
}
|
||||
|
||||
var labelRect = position;
|
||||
position = EditorGUI.PrefixLabel(position, label);
|
||||
labelRect.width = position.x - labelRect.x;
|
||||
@ -73,6 +90,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
property.stringValue = relPath;
|
||||
}
|
||||
|
||||
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -104,6 +123,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
property.stringValue = relPath;
|
||||
}
|
||||
|
||||
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -95,7 +95,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var t = (ModularAvatarBoneProxy) targets[i];
|
||||
Undo.RecordObjects(targets, "Set targets");
|
||||
var xform = ((TempObjRef) objRefs[i]).target;
|
||||
if (RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
|
||||
if (xform != null && RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue;
|
||||
t.target = xform;
|
||||
}
|
||||
}
|
||||
@ -159,4 +159,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private VisualElement _inner;
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<LogoElement, UxmlTraits>
|
||||
{
|
||||
}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
}
|
||||
|
||||
private static void RegisterNode(LogoElement target)
|
||||
{
|
||||
if (_logoDisplayNode == null)
|
||||
|
@ -1,5 +1,6 @@
|
||||
#region
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
@ -28,11 +29,26 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
var image = new Image();
|
||||
image.image = LogoDisplay.LOGO_ASSET;
|
||||
image.style.width = new Length(LogoDisplay.ImageWidth(LogoDisplay.TARGET_HEIGHT), LengthUnit.Pixel);
|
||||
image.style.height = new Length(LogoDisplay.TARGET_HEIGHT, LengthUnit.Pixel);
|
||||
|
||||
SetImageSize(image);
|
||||
|
||||
_inner.Add(image);
|
||||
Add(_inner);
|
||||
}
|
||||
|
||||
private static void SetImageSize(Image image, int maxTries = 10)
|
||||
{
|
||||
var targetHeight = LogoDisplay.TARGET_HEIGHT;
|
||||
|
||||
if (targetHeight == 0)
|
||||
{
|
||||
if (maxTries <= 0) return;
|
||||
EditorApplication.delayCall += () => SetImageSize(image, maxTries - 1);
|
||||
targetHeight = 45;
|
||||
}
|
||||
|
||||
image.style.width = new Length(LogoDisplay.ImageWidth(targetHeight), LengthUnit.Pixel);
|
||||
image.style.height = new Length(targetHeight, LengthUnit.Pixel);
|
||||
}
|
||||
}
|
||||
}
|
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
|
||||
guid: d86c7d257d78fff4d8fdf56e2954a5c9
|
||||
guid: 528c660b56905844ea2f88bc73837e9f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
@ -1,4 +1,5 @@
|
||||
using UnityEditor;
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
using UnityEditor;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
@ -19,12 +20,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
var target = (ModularAvatarVisibleHeadAccessory) this.target;
|
||||
|
||||
|
||||
#if UNITY_ANDROID
|
||||
EditorGUILayout.HelpBox(Localization.S("fpvisible.quest"), MessageType.Warning);
|
||||
|
||||
#else
|
||||
|
||||
if (_validation != null)
|
||||
{
|
||||
var status = _validation.Validate(target);
|
||||
@ -35,6 +30,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
case VisibleHeadAccessoryValidation.ReadyStatus.ParentMarked:
|
||||
EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info);
|
||||
break;
|
||||
case VisibleHeadAccessoryValidation.ReadyStatus.NotUnderHead:
|
||||
EditorGUILayout.HelpBox(Localization.S("fpvisible.NotUnderHead"), MessageType.Warning);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var label = "fpvisible." + status;
|
||||
@ -44,9 +42,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Localization.ShowLanguageUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -11,7 +11,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
internal static class LogoDisplay
|
||||
{
|
||||
internal static readonly Texture2D LOGO_ASSET;
|
||||
internal static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3;
|
||||
internal static float TARGET_HEIGHT
|
||||
{
|
||||
get {
|
||||
try
|
||||
{
|
||||
return (EditorStyles.label?.lineHeight ?? 0) * 3;
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
// This can happen in early initialization...
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static float ImageWidth(float height)
|
||||
{
|
||||
|
14
Editor/Inspector/MAConvertConstraintsEditor.cs
Normal file
14
Editor/Inspector/MAConvertConstraintsEditor.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarConvertConstraints))]
|
||||
[CanEditMultipleObjects]
|
||||
internal class MAConvertConstraintsEditor : MAEditorBase
|
||||
{
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
// no UI
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/Inspector/MAConvertConstraintsEditor.cs.meta
Normal file
3
Editor/Inspector/MAConvertConstraintsEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 484ea04548b945ce9cf5fd6d49b50244
|
||||
timeCreated: 1723778102
|
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
|
3
Editor/Inspector/MaterialSetter.meta
Normal file
3
Editor/Inspector/MaterialSetter.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 131d9706ddc04331bd09cf13b863c537
|
||||
timeCreated: 1723334567
|
25
Editor/Inspector/MaterialSetter/MaterialSetter.uxml
Normal file
25
Editor/Inspector/MaterialSetter/MaterialSetter.uxml
Normal file
@ -0,0 +1,25 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement name="root-box">
|
||||
<ui:VisualElement name="group-box">
|
||||
<ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/>
|
||||
|
||||
<ui:VisualElement name="ListViewContainer">
|
||||
<ui:ListView virtualization-method="DynamicHeight"
|
||||
reorder-mode="Animated"
|
||||
reorderable="true"
|
||||
show-add-remove-footer="true"
|
||||
show-border="true"
|
||||
show-foldout-header="false"
|
||||
name="Shapes"
|
||||
item-height="100"
|
||||
binding-path="m_objects"
|
||||
style="flex-grow: 1;"
|
||||
/>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ma:ROSimulatorButton/>
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
3
Editor/Inspector/MaterialSetter/MaterialSetter.uxml.meta
Normal file
3
Editor/Inspector/MaterialSetter/MaterialSetter.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd5c518316b2435d8a666911d4131903
|
||||
timeCreated: 1723334567
|
81
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs
Normal file
81
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs
Normal file
@ -0,0 +1,81 @@
|
||||
#region
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMaterialSetter))]
|
||||
public class MaterialSetterEditor : MAEditorBase
|
||||
{
|
||||
[SerializeField] private StyleSheet uss;
|
||||
[SerializeField] private VisualTreeAsset uxml;
|
||||
|
||||
private DragAndDropManipulator _dragAndDropManipulator;
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
|
||||
}
|
||||
|
||||
protected override VisualElement CreateInnerInspectorGUI()
|
||||
{
|
||||
var root = uxml.CloneTree();
|
||||
Localization.UI.Localize(root);
|
||||
root.styleSheets.Add(uss);
|
||||
|
||||
root.Bind(serializedObject);
|
||||
|
||||
ROSimulatorButton.BindRefObject(root, target);
|
||||
|
||||
var listView = root.Q<ListView>("Shapes");
|
||||
|
||||
listView.showBoundCollectionSize = false;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||
|
||||
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_dragAndDropManipulator != null)
|
||||
_dragAndDropManipulator.TargetComponent = target as ModularAvatarMaterialSetter;
|
||||
}
|
||||
|
||||
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarMaterialSetter>
|
||||
{
|
||||
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarMaterialSetter targetComponent)
|
||||
: base(targetElement, targetComponent) { }
|
||||
|
||||
protected override bool FilterGameObject(GameObject obj)
|
||||
{
|
||||
if (obj.TryGetComponent<Renderer>(out var renderer))
|
||||
{
|
||||
return renderer.sharedMaterials.Length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void AddObjectReferences(AvatarObjectReference[] references)
|
||||
{
|
||||
Undo.RecordObject(TargetComponent, "Add Material Switch Objects");
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var materialSwitchObject = new MaterialSwitchObject { Object = reference, MaterialIndex = 0 };
|
||||
TargetComponent.Objects.Add(materialSwitchObject);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(TargetComponent);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs.meta
Normal file
13
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs.meta
Normal file
@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 339dd3848a2044b1aa04f543226de0e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- uss: {fileID: 7433441132597879392, guid: fce9f3fe74434b718abac5ea66775acb, type: 3}
|
||||
- uxml: {fileID: 9197481963319205126, guid: cd5c518316b2435d8a666911d4131903, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
74
Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss
Normal file
74
Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss
Normal file
@ -0,0 +1,74 @@
|
||||
#group-box {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
border-width: 3px;
|
||||
border-left-color: rgba(0, 1, 0, 0.2);
|
||||
border-top-color: rgba(0, 1, 0, 0.2);
|
||||
border-right-color: rgba(0, 1, 0, 0.2);
|
||||
border-bottom-color: rgba(0, 1, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
#group-box > Label {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
#ListViewContainer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.horizontal > * {
|
||||
height: 18px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.horizontal > Label {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.horizontal > PropertyField > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#f-object {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#f-material-index {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#f-material-index-dropdown {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#f-material-index-original {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.horizontal > Label {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#f-material {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.drop-area--drag-active {
|
||||
background-color: rgba(0, 127, 255, 0.2);
|
||||
}
|
||||
|
||||
.drop-area--drag-active .unity-scroll-view,
|
||||
.drop-area--drag-active .unity-list-view__footer,
|
||||
.drop-area--drag-active .unity-list-view__reorderable-item {
|
||||
background-color: rgba(0, 0, 0, 0.0);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fce9f3fe74434b718abac5ea66775acb
|
||||
timeCreated: 1723334567
|
157
Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs
Normal file
157
Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs
Normal file
@ -0,0 +1,157 @@
|
||||
#region
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(MaterialSwitchObject))]
|
||||
public class MaterialSwitchObjectEditor : PropertyDrawer
|
||||
{
|
||||
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/MaterialSetter/";
|
||||
private const string UxmlPath = Root + "MaterialSwitchObjectEditor.uxml";
|
||||
private const string UssPath = Root + "MaterialSetterStyles.uss";
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
|
||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
|
||||
Localization.UI.Localize(uxml);
|
||||
uxml.styleSheets.Add(uss);
|
||||
uxml.BindProperty(property);
|
||||
|
||||
var f_material_index = uxml.Q<IntegerField>("f-material-index");
|
||||
var f_material_index_dropdown = uxml.Q<DropdownField>("f-material-index-dropdown");
|
||||
var f_material_index_original = uxml.Q<ObjectField>("f-material-index-original");
|
||||
|
||||
var f_object = uxml.Q<PropertyField>("f-object");
|
||||
|
||||
f_object.RegisterValueChangeCallback(evt =>
|
||||
{
|
||||
EditorApplication.delayCall += UpdateMaterialDropdown;
|
||||
});
|
||||
UpdateMaterialDropdown();
|
||||
|
||||
// Link dropdown and original field to material index field
|
||||
f_material_index.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
f_material_index_dropdown.SetValueWithoutNotify(evt.newValue.ToString());
|
||||
UpdateOriginalMaterial();
|
||||
});
|
||||
f_material_index_dropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue != null && int.TryParse(evt.newValue, out var i))
|
||||
{
|
||||
f_material_index.value = i;
|
||||
}
|
||||
});
|
||||
f_material_index_original.SetEnabled(false);
|
||||
|
||||
return uxml;
|
||||
|
||||
void UpdateMaterialDropdown()
|
||||
{
|
||||
var sharedMaterials = GetSharedMaterials();
|
||||
|
||||
if (sharedMaterials != null)
|
||||
{
|
||||
var matCount = sharedMaterials.Length;
|
||||
|
||||
f_material_index_dropdown.SetEnabled(true);
|
||||
|
||||
f_material_index_dropdown.choices.Clear();
|
||||
for (int i = 0; i < matCount; i++)
|
||||
{
|
||||
f_material_index_dropdown.choices.Add(i.ToString());
|
||||
}
|
||||
|
||||
f_material_index_dropdown.formatListItemCallback = idx_s =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(idx_s)) return "";
|
||||
|
||||
var idx = int.Parse(idx_s);
|
||||
if (idx < 0 || idx >= sharedMaterials.Length)
|
||||
{
|
||||
return $"<color=\"red\">Element {idx_s}: <???></color>";
|
||||
}
|
||||
else if (sharedMaterials[idx] == null)
|
||||
{
|
||||
return $"Element {idx_s}: <None>";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Element {idx_s}: {sharedMaterials[idx].name}";
|
||||
}
|
||||
};
|
||||
f_material_index_dropdown.formatSelectedValueCallback = idx_s =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(idx_s)) return "";
|
||||
|
||||
var idx = int.Parse(idx_s);
|
||||
if (idx < 0 || idx >= sharedMaterials.Length)
|
||||
{
|
||||
return $"<color=\"red\">Element {idx_s}</color>";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Element {idx_s}";
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
f_material_index_dropdown.SetEnabled(false);
|
||||
if (f_material_index_dropdown.choices.Count == 0)
|
||||
{
|
||||
f_material_index_dropdown.choices.Add("0");
|
||||
}
|
||||
|
||||
f_material_index_dropdown.formatListItemCallback = idx_s => "<Missing Renderer>";
|
||||
f_material_index_dropdown.formatSelectedValueCallback = f_material_index_dropdown.formatListItemCallback;
|
||||
}
|
||||
|
||||
UpdateOriginalMaterial();
|
||||
}
|
||||
|
||||
void UpdateOriginalMaterial()
|
||||
{
|
||||
var sharedMaterials = GetSharedMaterials();
|
||||
|
||||
if (sharedMaterials != null)
|
||||
{
|
||||
var idx = f_material_index.value;
|
||||
if (idx < 0 || idx >= sharedMaterials.Length)
|
||||
{
|
||||
f_material_index_original.SetValueWithoutNotify(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
f_material_index_original.SetValueWithoutNotify(sharedMaterials[idx]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f_material_index_original.SetValueWithoutNotify(null);
|
||||
}
|
||||
}
|
||||
|
||||
Material[] GetSharedMaterials()
|
||||
{
|
||||
var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
|
||||
try
|
||||
{
|
||||
return targetObject?.GetComponent<Renderer>()?.sharedMaterials;
|
||||
}
|
||||
catch (MissingComponentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6361a17f884644988ef3ece7fbe73ab7
|
||||
timeCreated: 1723334567
|
@ -0,0 +1,16 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ed:IntegerField name="f-material-index" binding-path="MaterialIndex"/>
|
||||
<ui:DropdownField name="f-material-index-dropdown"/>
|
||||
<ed:ObjectField name="f-material-index-original"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ui:Label text="reactive_object.material-setter.set-to" class="ndmf-tr"/>
|
||||
<ed:PropertyField name="f-material" binding-path="Material" label=""/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55b5e53f6c364089a1871b68e0de17c6
|
||||
timeCreated: 1723334567
|
@ -33,6 +33,29 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
||||
|
||||
private static Editor _cachedEditor;
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void Init()
|
||||
{
|
||||
ModularAvatarMenuInstaller._openSelectMenu = OpenSelectInstallTargetMenu;
|
||||
}
|
||||
|
||||
private static void OpenSelectInstallTargetMenu(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
CreateCachedEditor(installer, typeof(MenuInstallerEditor), ref _cachedEditor);
|
||||
|
||||
var editor = (MenuInstallerEditor)_cachedEditor;
|
||||
editor.OnEnable();
|
||||
|
||||
var serializedObject = editor.serializedObject;
|
||||
var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu));
|
||||
|
||||
var root = editor.FindCommonAvatar();
|
||||
|
||||
editor.OpenSelectMenu(root, installTo);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_installer = (ModularAvatarMenuInstaller) target;
|
||||
@ -215,74 +238,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var avatar = commonAvatar;
|
||||
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||
{
|
||||
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||
{
|
||||
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
|
||||
|
||||
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
|
||||
{
|
||||
DestroyInstallTargets();
|
||||
}
|
||||
|
||||
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
|
||||
{
|
||||
// Menu, ContextCallback
|
||||
menu = vt.Item1;
|
||||
}
|
||||
|
||||
if (menu is ModularAvatarMenuItem item)
|
||||
{
|
||||
if (item.MenuSource == SubmenuSource.MenuAsset)
|
||||
{
|
||||
menu = item.Control.subMenu;
|
||||
}
|
||||
else
|
||||
{
|
||||
var menuParent = item.menuSource_otherObjectChildren != null
|
||||
? item.menuSource_otherObjectChildren
|
||||
: item.gameObject;
|
||||
|
||||
menu = new MenuNodesUnder(menuParent);
|
||||
}
|
||||
}
|
||||
else if (menu is ModularAvatarMenuGroup group)
|
||||
{
|
||||
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
|
||||
else menu = new MenuNodesUnder(group.gameObject);
|
||||
}
|
||||
|
||||
if (menu is VRCExpressionsMenu expMenu)
|
||||
{
|
||||
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
|
||||
else installTo.objectReferenceValue = expMenu;
|
||||
}
|
||||
else if (menu is RootMenu)
|
||||
{
|
||||
installTo.objectReferenceValue = null;
|
||||
}
|
||||
else if (menu is MenuNodesUnder nodesUnder)
|
||||
{
|
||||
installTo.objectReferenceValue = null;
|
||||
|
||||
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) target;
|
||||
var child = new GameObject();
|
||||
Undo.RegisterCreatedObjectUndo(child, "Set install target");
|
||||
child.transform.SetParent(nodesUnder.root.transform, false);
|
||||
child.name = installer.gameObject.name;
|
||||
|
||||
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
targetComponent.installer = installer;
|
||||
|
||||
EditorGUIUtility.PingObject(child);
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
VirtualMenu.InvalidateCaches();
|
||||
Repaint();
|
||||
});
|
||||
OpenSelectMenu(avatar, installTo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +324,79 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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)
|
||||
@ -415,6 +443,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
|
||||
var menuRoot = new GameObject();
|
||||
menuRoot.name = "Menu";
|
||||
|
||||
group.targetObject = menuRoot;
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
|
||||
menuRoot.transform.SetParent(group.transform, false);
|
||||
foreach (var control in menu.controls)
|
||||
|
@ -1,13 +1,19 @@
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using nadena.dev.ndmf;
|
||||
using nadena.dev.ndmf.preview;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
@ -17,8 +23,44 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
protected override string localizationPrefix => "submenu_source";
|
||||
}
|
||||
|
||||
internal static class ParameterIntrospectionCache
|
||||
{
|
||||
internal static PropCache<GameObject, ImmutableList<ProvidedParameter>> ProvidedParameterCache =
|
||||
new("GetParametersForObject", GetParametersForObject_miss);
|
||||
|
||||
internal static PropCache<GameObject, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>>
|
||||
ParameterRemappingCache = new("GetParameterRemappingsAt", GetParameterRemappingsAt_miss);
|
||||
|
||||
private static ImmutableList<ProvidedParameter> GetParametersForObject_miss(ComputeContext ctx, GameObject obj)
|
||||
{
|
||||
if (obj == null) return ImmutableList<ProvidedParameter>.Empty;
|
||||
|
||||
return ParameterInfo.ForPreview(ctx).GetParametersForObject(obj).ToImmutableList();
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>
|
||||
GetParameterRemappingsAt_miss(ComputeContext ctx, GameObject obj)
|
||||
{
|
||||
if (obj == null) return ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>.Empty;
|
||||
|
||||
return ParameterInfo.ForPreview(ctx).GetParameterRemappingsAt(obj);
|
||||
}
|
||||
|
||||
internal static ImmutableList<ProvidedParameter> GetParametersForObject(GameObject avatar)
|
||||
{
|
||||
return ProvidedParameterCache.Get(ComputeContext.NullContext, avatar);
|
||||
}
|
||||
|
||||
internal static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> GetParameterRemappingsAt(GameObject avatar)
|
||||
{
|
||||
return ParameterRemappingCache.Get(ComputeContext.NullContext, avatar);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MenuItemCoreGUI
|
||||
{
|
||||
private const string ImpliesRichText = "<";
|
||||
|
||||
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
|
||||
private readonly GameObject _parameterReference;
|
||||
private readonly Action _redraw;
|
||||
@ -32,6 +74,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
private readonly SerializedProperty _submenu;
|
||||
|
||||
private readonly ParameterGUI _parameterGUI;
|
||||
private readonly SerializedProperty _parameterName;
|
||||
|
||||
private readonly SerializedProperty _subParamsRoot;
|
||||
private readonly SerializedProperty _labelsRoot;
|
||||
@ -46,9 +89,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
private readonly SerializedProperty _prop_submenuSource;
|
||||
private readonly SerializedProperty _prop_otherObjSource;
|
||||
|
||||
private readonly SerializedProperty _prop_isSynced;
|
||||
private readonly SerializedProperty _prop_isSaved;
|
||||
private readonly SerializedProperty _prop_isDefault;
|
||||
private readonly SerializedProperty _prop_automaticValue;
|
||||
|
||||
private readonly SerializedProperty _prop_label;
|
||||
|
||||
public bool AlwaysExpandContents = false;
|
||||
public bool ExpandContents = false;
|
||||
|
||||
private readonly Dictionary<string, ProvidedParameter> _knownParameters = new();
|
||||
private bool _parameterSourceNotDetermined;
|
||||
private bool _useLabel;
|
||||
|
||||
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
||||
{
|
||||
_obj = obj;
|
||||
@ -62,9 +116,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_parameterReference = parameterReference;
|
||||
_redraw = redraw;
|
||||
|
||||
InitKnownParameters();
|
||||
|
||||
var gameObjects = new SerializedObject(
|
||||
obj.targetObjects.Select(o =>
|
||||
(UnityEngine.Object) ((ModularAvatarMenuItem) o).gameObject
|
||||
(Object) ((ModularAvatarMenuItem) o).gameObject
|
||||
).ToArray()
|
||||
);
|
||||
|
||||
@ -74,21 +130,76 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||
_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
||||
var parameter = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
_parameterName = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||
|
||||
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||
_submenu = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
|
||||
|
||||
_subParamsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
||||
|
||||
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
||||
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
|
||||
|
||||
_prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced));
|
||||
_prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved));
|
||||
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
|
||||
_prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue));
|
||||
|
||||
_prop_label = obj.FindProperty(nameof(ModularAvatarMenuItem.label));
|
||||
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
private void InitKnownParameters()
|
||||
{
|
||||
var paramRef = _parameterReference;
|
||||
if (_parameterReference == null)
|
||||
// TODO: This could give incorrect results in some cases when we have multiple objects selected with
|
||||
// different rename contexts.
|
||||
paramRef = (_obj.targetObjects[0] as Component)?.gameObject;
|
||||
|
||||
if (paramRef == null)
|
||||
{
|
||||
_parameterSourceNotDetermined = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var parentAvatar = RuntimeUtil.FindAvatarInParents(paramRef.transform);
|
||||
if (parentAvatar == null)
|
||||
{
|
||||
_parameterSourceNotDetermined = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, ProvidedParameter> rootParameters = new();
|
||||
|
||||
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
|
||||
.Where(p => p.Namespace == ParameterNamespace.Animator)
|
||||
)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
|
||||
{
|
||||
rootParameters[param.EffectiveName] = param;
|
||||
}
|
||||
}
|
||||
|
||||
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
|
||||
foreach (var remap in remaps)
|
||||
{
|
||||
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
|
||||
if (rootParameters.ContainsKey(remap.Value.ParameterName))
|
||||
_knownParameters[remap.Key.Item2] = rootParameters[remap.Value.ParameterName];
|
||||
}
|
||||
|
||||
foreach (var rootParam in rootParameters)
|
||||
if (!remaps.ContainsKey((ParameterNamespace.Animator, rootParam.Key)))
|
||||
_knownParameters[rootParam.Key] = rootParam.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
|
||||
/// </summary>
|
||||
@ -99,43 +210,136 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
_obj = _control.serializedObject;
|
||||
_parameterReference = parameterReference;
|
||||
InitKnownParameters();
|
||||
|
||||
_redraw = redraw;
|
||||
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
|
||||
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||
_type = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
||||
var parameter = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
_parameterName = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||
|
||||
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
|
||||
|
||||
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||
_labelsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
||||
|
||||
_prop_isSynced = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSynced));
|
||||
_prop_isSaved = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSaved));
|
||||
_prop_isDefault = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isDefault));
|
||||
_prop_automaticValue = null;
|
||||
|
||||
_prop_submenuSource = null;
|
||||
_prop_otherObjSource = null;
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
private void DrawHorizontalToggleProp(
|
||||
SerializedProperty prop,
|
||||
GUIContent label,
|
||||
bool? forceMixedValues = null,
|
||||
bool? forceValue = null
|
||||
)
|
||||
{
|
||||
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
|
||||
var labelSize = EditorStyles.label.CalcSize(label);
|
||||
var width = toggleSize.x + labelSize.x + 4;
|
||||
|
||||
var rect = EditorGUILayout.GetControlRect(GUILayout.Width(width));
|
||||
EditorGUI.BeginProperty(rect, label, prop);
|
||||
|
||||
if (forceMixedValues != null) EditorGUI.showMixedValue = forceMixedValues.Value;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var value = EditorGUI.ToggleLeft(rect, label, forceValue ?? prop.boolValue);
|
||||
if (EditorGUI.EndChangeCheck()) prop.boolValue = value;
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public void DoGUI()
|
||||
{
|
||||
if (_obj != null) _obj.UpdateIfRequiredOrScript();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
if (_prop_label == null)
|
||||
{
|
||||
_name.serializedObject.ApplyModifiedProperties();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
if (_obj != null && _obj.isEditingMultipleObjects)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||
}
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
_name.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_useLabel |= !string.IsNullOrEmpty(_prop_label.stringValue);
|
||||
|
||||
if (!_useLabel)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var previousName = _name.stringValue;
|
||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (!previousName.Contains(ImpliesRichText) && _name.stringValue.Contains(ImpliesRichText))
|
||||
{
|
||||
_prop_label.stringValue = _name.stringValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_name.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_label, G("menuitem.prop.name"));
|
||||
}
|
||||
|
||||
var linkIcon = EditorGUIUtility.IconContent(_useLabel ? "UnLinked" : "Linked").image;
|
||||
var guiIcon = new GUIContent(linkIcon, S(_useLabel ? "menuitem.label.gameobject_name.tooltip" : "menuitem.label.long_name.tooltip"));
|
||||
if (GUILayout.Button(guiIcon, GUILayout.Height(EditorGUIUtility.singleLineHeight), GUILayout.Width(25)))
|
||||
{
|
||||
_prop_label.stringValue = !_useLabel ? _name.stringValue : "";
|
||||
_useLabel = !_useLabel;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (_useLabel && _prop_label.stringValue.Contains(ImpliesRichText))
|
||||
{
|
||||
var style = new GUIStyle(EditorStyles.textField);
|
||||
style.richText = true;
|
||||
style.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
EditorGUILayout.LabelField(" ", _prop_label.stringValue, style, GUILayout.Height(EditorGUIUtility.singleLineHeight * 3));
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
|
||||
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
|
||||
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
|
||||
DoValueField();
|
||||
|
||||
_parameterGUI.DoGUI(true);
|
||||
|
||||
ShowInnateParameterGUI();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (_texture != null)
|
||||
@ -167,10 +371,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
if (_type.hasMultipleDifferentValues) return;
|
||||
VRCExpressionsMenu.Control.ControlType type =
|
||||
(VRCExpressionsMenu.Control.ControlType) Enum
|
||||
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
|
||||
.GetValue(_type.enumValueIndex);
|
||||
var controlTypeArray = Enum.GetValues(typeof(VRCExpressionsMenu.Control.ControlType));
|
||||
var index = Math.Clamp(_type.enumValueIndex, 0, controlTypeArray.Length - 1);
|
||||
var type = (VRCExpressionsMenu.Control.ControlType)controlTypeArray.GetValue(index);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
@ -329,6 +532,243 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowInnateParameterGUI()
|
||||
{
|
||||
if (_prop_isDefault == null)
|
||||
// This is probably coming from a VRC Expressions menu asset.
|
||||
// For now, don't show the UI in this case.
|
||||
return;
|
||||
|
||||
var multipleSelections = _obj.targetObjects.Length > 1;
|
||||
|
||||
var paramName = _parameterName.stringValue;
|
||||
var siblings = FindSiblingMenuItems(_obj);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
var forceMixedValues = _parameterName.hasMultipleDifferentValues;
|
||||
|
||||
var syncedIsMixed = forceMixedValues || _prop_isSynced.hasMultipleDifferentValues ||
|
||||
siblings != null && siblings.Any(s => s.isSynced != _prop_isSynced.boolValue);
|
||||
var savedIsMixed = forceMixedValues || _prop_isSaved.hasMultipleDifferentValues ||
|
||||
siblings != null && siblings.Any(s => s.isSaved != _prop_isSaved.boolValue);
|
||||
|
||||
var knownParameter = _parameterName.hasMultipleDifferentValues
|
||||
? null
|
||||
: _knownParameters.GetValueOrDefault(paramName);
|
||||
|
||||
var knownSource = knownParameter?.Source;
|
||||
var externalSource = knownSource != null && knownSource is not ModularAvatarMenuItem;
|
||||
|
||||
if (externalSource) savedIsMixed = true; // NDMF doesn't yet support querying for the saved state
|
||||
var forceSyncedValue = externalSource ? knownParameter?.WantSynced : null;
|
||||
|
||||
var knownParamDefault = knownParameter?.DefaultValue;
|
||||
var isDefaultByKnownParam =
|
||||
knownParamDefault != null ? _value.floatValue == knownParamDefault : (bool?)null;
|
||||
|
||||
if (knownParameter != null && knownParameter.Source is ModularAvatarMenuItem)
|
||||
isDefaultByKnownParam = null;
|
||||
|
||||
if (_prop_automaticValue?.boolValue == true) isDefaultByKnownParam = null;
|
||||
|
||||
Object controller = knownParameter?.Source;
|
||||
|
||||
// If we can't figure out what to reference the parameter names to, or if they're controlled by something
|
||||
// other than the Menu Item component itself, disable the UI
|
||||
var controllerIsElsewhere = externalSource || _parameterSourceNotDetermined;
|
||||
|
||||
using (new EditorGUI.DisabledScope(
|
||||
_parameterName.hasMultipleDifferentValues || controllerIsElsewhere)
|
||||
)
|
||||
{
|
||||
// If we have multiple menu items selected, it probably doesn't make sense to make them all default.
|
||||
// But, we do want to see if _any_ are default.
|
||||
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
|
||||
var mixedIsDefault = multipleSelections && anyIsDefault;
|
||||
|
||||
var allAreAutoParams = !_parameterName.hasMultipleDifferentValues &&
|
||||
string.IsNullOrWhiteSpace(_parameterName.stringValue);
|
||||
|
||||
using (new EditorGUI.DisabledScope((!allAreAutoParams && multipleSelections) ||
|
||||
isDefaultByKnownParam != null))
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
|
||||
isDefaultByKnownParam);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
_obj.ApplyModifiedProperties();
|
||||
ClearConflictingDefaults(siblings);
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), savedIsMixed);
|
||||
if (EditorGUI.EndChangeCheck() && siblings != null)
|
||||
foreach (var sibling in siblings)
|
||||
{
|
||||
sibling.isSaved = _prop_isSaved.boolValue;
|
||||
EditorUtility.SetDirty(sibling);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), syncedIsMixed,
|
||||
forceSyncedValue);
|
||||
if (EditorGUI.EndChangeCheck() && siblings != null)
|
||||
foreach (var sibling in siblings)
|
||||
{
|
||||
sibling.isSynced = _prop_isSynced.boolValue;
|
||||
EditorUtility.SetDirty(sibling);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
if (controllerIsElsewhere)
|
||||
{
|
||||
var refStyle = EditorStyles.toggle;
|
||||
var refContent = new GUIContent("test");
|
||||
var refRect = refStyle.CalcSize(refContent);
|
||||
var height = refRect.y + EditorStyles.toggle.margin.top + EditorStyles.toggle.margin.bottom;
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
var style = new GUIStyle(EditorStyles.miniButton);
|
||||
style.fixedWidth = 0;
|
||||
style.fixedHeight = 0;
|
||||
style.stretchHeight = true;
|
||||
style.stretchWidth = true;
|
||||
style.imagePosition = ImagePosition.ImageOnly;
|
||||
var icon = EditorGUIUtility.FindTexture("d_Search Icon");
|
||||
|
||||
var rect = GUILayoutUtility.GetRect(new GUIContent(), style, GUILayout.ExpandWidth(false),
|
||||
GUILayout.Width(height), GUILayout.Height(height));
|
||||
|
||||
if (GUI.Button(rect, new GUIContent(), style))
|
||||
{
|
||||
if (controller is VRCAvatarDescriptor desc) controller = desc.expressionParameters;
|
||||
Selection.activeObject = controller;
|
||||
EditorGUIUtility.PingObject(controller);
|
||||
}
|
||||
|
||||
rect.xMin += 2;
|
||||
rect.yMin += 2;
|
||||
rect.xMax -= 2;
|
||||
rect.yMax -= 2;
|
||||
GUI.DrawTexture(rect, icon);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DoValueField()
|
||||
{
|
||||
var value_label = G("menuitem.prop.value");
|
||||
var auto_label = G("menuitem.prop.automatic_value");
|
||||
|
||||
if (_prop_automaticValue == null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_value, value_label);
|
||||
return;
|
||||
}
|
||||
|
||||
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
|
||||
var autoLabelSize = EditorStyles.label.CalcSize(auto_label);
|
||||
|
||||
var style = EditorStyles.numberField;
|
||||
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, style);
|
||||
|
||||
var valueRect = rect;
|
||||
valueRect.xMax -= toggleSize.x + autoLabelSize.x + 4;
|
||||
|
||||
var autoRect = rect;
|
||||
autoRect.xMin = valueRect.xMax + 4;
|
||||
|
||||
var suppressValue = _prop_automaticValue.boolValue || _prop_automaticValue.hasMultipleDifferentValues;
|
||||
|
||||
using (new EditorGUI.DisabledScope(suppressValue))
|
||||
{
|
||||
if (suppressValue)
|
||||
{
|
||||
EditorGUI.TextField(valueRect, value_label, "", style);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.PropertyField(valueRect, _value, value_label);
|
||||
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.BeginProperty(autoRect, auto_label, _prop_automaticValue);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
EditorGUI.showMixedValue = _prop_automaticValue.hasMultipleDifferentValues;
|
||||
var autoValue = EditorGUI.ToggleLeft(autoRect, auto_label, _prop_automaticValue.boolValue);
|
||||
|
||||
if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = autoValue;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
private List<ModularAvatarMenuItem> FindSiblingMenuItems(SerializedObject serializedObject)
|
||||
{
|
||||
if (serializedObject == null || serializedObject.isEditingMultipleObjects) return null;
|
||||
|
||||
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
|
||||
if (myMenuItem == null) return null;
|
||||
|
||||
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
|
||||
if (avatarRoot == null) return null;
|
||||
|
||||
var myParameterName = myMenuItem.Control.parameter.name;
|
||||
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
|
||||
|
||||
var myMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(myMenuItem.gameObject);
|
||||
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
|
||||
myParameterName = myReplacement.ParameterName;
|
||||
|
||||
var siblings = new List<ModularAvatarMenuItem>();
|
||||
|
||||
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))
|
||||
{
|
||||
if (otherMenuItem == myMenuItem) continue;
|
||||
|
||||
var otherParameterName = otherMenuItem.Control.parameter.name;
|
||||
if (string.IsNullOrEmpty(otherParameterName)) continue;
|
||||
|
||||
var otherMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(otherMenuItem.gameObject);
|
||||
if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName),
|
||||
out var otherReplacement))
|
||||
otherParameterName = otherReplacement.ParameterName;
|
||||
|
||||
if (otherParameterName != myParameterName) continue;
|
||||
|
||||
siblings.Add(otherMenuItem);
|
||||
}
|
||||
|
||||
return siblings;
|
||||
}
|
||||
|
||||
private void ClearConflictingDefaults(List<ModularAvatarMenuItem> siblingItems)
|
||||
{
|
||||
var siblings = siblingItems;
|
||||
if (siblings == null) return;
|
||||
|
||||
foreach (var otherMenuItem in siblings)
|
||||
{
|
||||
if (otherMenuItem.isDefault)
|
||||
{
|
||||
Undo.RecordObject(otherMenuItem, "");
|
||||
otherMenuItem.isDefault = false;
|
||||
EditorUtility.SetDirty(otherMenuItem);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(otherMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLabelCount(int i)
|
||||
{
|
||||
if (_labels == null || _labelsRoot.arraySize < i || _labels.Length < i)
|
||||
|
@ -206,14 +206,40 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
if (source is MenuNodesUnder nodesUnder)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button(G("menuitem.misc.add_item")))
|
||||
{
|
||||
var newChild = new GameObject();
|
||||
newChild.name = "New item";
|
||||
newChild.transform.SetParent(nodesUnder.root.transform, false);
|
||||
newChild.AddComponent<ModularAvatarMenuItem>();
|
||||
|
||||
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
|
||||
mami.InitSettings();
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(newChild, "Added menu item");
|
||||
}
|
||||
|
||||
if (GUILayout.Button(G("menuitem.misc.add_toggle")))
|
||||
{
|
||||
var newChild = new GameObject();
|
||||
newChild.name = "New toggle";
|
||||
newChild.transform.SetParent(nodesUnder.root.transform, false);
|
||||
|
||||
var mami = newChild.AddComponent<ModularAvatarMenuItem>();
|
||||
mami.InitSettings();
|
||||
mami.Control = new VRCExpressionsMenu.Control()
|
||||
{
|
||||
type = VRCExpressionsMenu.Control.ControlType.Toggle,
|
||||
value = 1,
|
||||
};
|
||||
|
||||
newChild.AddComponent<ModularAvatarObjectToggle>();
|
||||
|
||||
Selection.activeObject = newChild;
|
||||
Undo.RegisterCreatedObjectUndo(newChild, "Added menu toggle");
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,10 +275,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
|
||||
var desc = node?.GetComponent<VRCAvatarDescriptor>();
|
||||
if (desc != null)
|
||||
if (desc?.expressionParameters?.parameters != null)
|
||||
{
|
||||
foreach (var param in desc.expressionParameters.parameters)
|
||||
{
|
||||
if (param == null) continue;
|
||||
if (emitted.Add(param.name)) yield return (node, param.name);
|
||||
}
|
||||
}
|
||||
|
218
Editor/Inspector/Menu/ToggleCreatorShortcut.cs
Normal file
218
Editor/Inspector/Menu/ToggleCreatorShortcut.cs
Normal file
@ -0,0 +1,218 @@
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.ui;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal static class ToggleCreatorShortcut
|
||||
{
|
||||
[MenuItem(UnityMenuItems.GameObject_CreateToggleForSelection, false, UnityMenuItems.GameObject_CreateToggleForSelectionOrder)]
|
||||
private static void CreateToggleForSelection()
|
||||
{
|
||||
var forSelection = true;
|
||||
|
||||
var selections = Selection.objects.OfType<GameObject>();
|
||||
// Ignore GameObjects with submenu in the context of CreateToggleForSelection.
|
||||
selections = selections.Where(s => !TryGetChildrenSourceSubmenu(s, out var _));
|
||||
if (selections.Count() == 0) return;
|
||||
|
||||
// Grouping according to parent
|
||||
var groups = new Dictionary<GameObject, HashSet<GameObject>>();
|
||||
foreach (var selected in selections)
|
||||
{
|
||||
var parent = selected.transform.parent?.gameObject;
|
||||
if (parent == null) continue;
|
||||
|
||||
if (!groups.ContainsKey(parent))
|
||||
{
|
||||
groups[parent] = new();
|
||||
}
|
||||
groups[parent].Add(selected);
|
||||
}
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var parent = group.Key;
|
||||
var targets = group.Value;
|
||||
|
||||
if (parent == null) continue;
|
||||
if (targets == null || targets.Count() == 0) continue;
|
||||
|
||||
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(parent.transform);
|
||||
if (avatarRoot == null) continue;
|
||||
|
||||
var subMenuName = parent.name + " Toggles";
|
||||
|
||||
// Try to find target submenu that should be the parent of toggles
|
||||
ModularAvatarMenuItem targetSubMenu = null;
|
||||
if (TryGetChildrenSourceSubmenu(parent, out var subMenu))
|
||||
{
|
||||
// If parent has subMenu, use it as target submenu.
|
||||
targetSubMenu = subMenu;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If parent hasn't subMenu, get submenus at the same level
|
||||
var subMenus = new List<ModularAvatarMenuItem>();
|
||||
foreach (Transform sibling in parent.transform)
|
||||
{
|
||||
if (TryGetChildrenSourceSubmenu(sibling.gameObject, out var m)) { subMenus.Add(m); }
|
||||
}
|
||||
// Filter to submenus with the same name
|
||||
subMenus = subMenus.Where(m => m.gameObject.name == subMenuName).ToList();
|
||||
// If only one submenu as target is found, use it as target submenu.
|
||||
if (subMenus.Count() == 1) targetSubMenu = subMenus.First();
|
||||
}
|
||||
|
||||
if (targetSubMenu != null) // If target SubMenu is found, add the toggles as children of it.
|
||||
{
|
||||
parent = targetSubMenu.gameObject;
|
||||
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targets.Count() > 1) // Create a submenu and add the toggles as children of it.
|
||||
{
|
||||
parent = CreateSubMenu(parent, subMenuName).gameObject;
|
||||
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
|
||||
}
|
||||
else // Create a single toggle with installer.
|
||||
{
|
||||
var target = targets.First();
|
||||
CreateToggleImpl(target, parent, forSelection, createInstaller:true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Selection.objects = null;
|
||||
}
|
||||
|
||||
[MenuItem(UnityMenuItems.GameObject_CreateToggle, false, UnityMenuItems.GameObject_CreateToggleOrder)]
|
||||
private static void CreateToggle()
|
||||
{
|
||||
var selections = Selection.objects.OfType<GameObject>();
|
||||
if (selections.Count() == 0) return;
|
||||
|
||||
foreach (var selected in selections)
|
||||
{
|
||||
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
|
||||
if (avatarRoot == null) return;
|
||||
|
||||
var parent = avatarRoot.gameObject;
|
||||
var createInstaller = true;
|
||||
|
||||
if (TryGetChildrenSourceSubmenu(selected, out var _))
|
||||
{
|
||||
parent = selected;
|
||||
createInstaller = false;
|
||||
}
|
||||
|
||||
CreateToggleImpl(selected, parent, createInstaller:createInstaller);
|
||||
}
|
||||
|
||||
Selection.objects = null;
|
||||
}
|
||||
|
||||
private static bool TryGetChildrenSourceSubmenu(GameObject target, out ModularAvatarMenuItem subMenu)
|
||||
{
|
||||
subMenu = null;
|
||||
try
|
||||
{
|
||||
var mami = target.GetComponent<ModularAvatarMenuItem>();
|
||||
if (mami?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
|
||||
&& mami.MenuSource == SubmenuSource.Children
|
||||
)
|
||||
{
|
||||
subMenu = mami;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (MissingComponentException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ModularAvatarMenuItem CreateSubMenu(GameObject parent, string submenuname)
|
||||
{
|
||||
var submenu = new GameObject(submenuname);
|
||||
submenu.transform.SetParent(parent.transform);
|
||||
|
||||
var mami = submenu.AddComponent<ModularAvatarMenuItem>();
|
||||
mami.InitSettings();
|
||||
mami.Control = new VRCExpressionsMenu.Control
|
||||
{
|
||||
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||
name = submenuname,
|
||||
};
|
||||
submenu.AddComponent<ModularAvatarMenuInstaller>();
|
||||
|
||||
Selection.activeGameObject = submenu;
|
||||
EditorGUIUtility.PingObject(submenu);
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(submenu, "Create SubMenu");
|
||||
|
||||
return mami;
|
||||
}
|
||||
|
||||
private static void CreateToggleImpl(IEnumerable<GameObject> selections, GameObject parent, bool forSelection = false, bool createInstaller = true)
|
||||
{
|
||||
foreach (var selected in selections)
|
||||
{
|
||||
CreateToggleImpl(selected, parent, forSelection, createInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateToggleImpl(GameObject selected, GameObject parent, bool forSelection = false, bool createInstaller = true)
|
||||
{
|
||||
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
|
||||
if (avatarRoot == null) return;
|
||||
|
||||
var suffix = selected.activeSelf ? "OFF" : "ON";
|
||||
var name = forSelection ? $"{selected.name} {suffix}" : "New Toggle";
|
||||
|
||||
var toggle = new GameObject(name);
|
||||
|
||||
var objToggle = toggle.AddComponent<ModularAvatarObjectToggle>();
|
||||
if (forSelection)
|
||||
{
|
||||
var path = RuntimeUtil.RelativePath(avatarRoot.gameObject, selected);
|
||||
objToggle.Objects.Add(new ToggledObject
|
||||
{
|
||||
Object = new AvatarObjectReference(){ referencePath = path },
|
||||
Active = !selected.activeSelf
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
toggle.transform.SetParent(parent.transform, false);
|
||||
|
||||
var mami = toggle.AddComponent<ModularAvatarMenuItem>();
|
||||
mami.InitSettings();
|
||||
mami.Control = new VRCExpressionsMenu.Control
|
||||
{
|
||||
type = VRCExpressionsMenu.Control.ControlType.Toggle,
|
||||
name = name,
|
||||
value = 1,
|
||||
};
|
||||
|
||||
if (createInstaller)
|
||||
{
|
||||
toggle.AddComponent<ModularAvatarMenuInstaller>();
|
||||
}
|
||||
|
||||
Selection.activeGameObject = toggle;
|
||||
EditorGUIUtility.PingObject(objToggle);
|
||||
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(toggle, "Create Toggle");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
3
Editor/Inspector/Menu/ToggleCreatorShortcut.cs.meta
Normal file
3
Editor/Inspector/Menu/ToggleCreatorShortcut.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e15fef260544783af5ff1fd5f13acd3
|
||||
timeCreated: 1723341065
|
@ -11,6 +11,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
protected override string localizationPrefix => "path_mode";
|
||||
}
|
||||
|
||||
[CustomPropertyDrawer(typeof(MergeAnimatorMode))]
|
||||
internal class MergeModeDrawer : EnumDrawer<MergeAnimatorMode>
|
||||
{
|
||||
protected override string localizationPrefix => "merge_animator.merge_mode";
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(ModularAvatarMergeAnimator))]
|
||||
class MergeAnimationEditor : MAEditorBase
|
||||
{
|
||||
@ -20,7 +26,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
prop_pathMode,
|
||||
prop_matchAvatarWriteDefaults,
|
||||
prop_relativePathRoot,
|
||||
prop_layerPriority;
|
||||
prop_layerPriority,
|
||||
prop_mergeMode;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@ -34,6 +41,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
prop_relativePathRoot =
|
||||
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.relativePathRoot));
|
||||
prop_layerPriority = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.layerPriority));
|
||||
prop_mergeMode = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.mergeAnimatorMode));
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
@ -47,8 +55,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
||||
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
|
||||
EditorGUILayout.PropertyField(prop_layerPriority, G("merge_animator.layer_priority"));
|
||||
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
|
||||
G("merge_animator.match_avatar_write_defaults"));
|
||||
EditorGUILayout.PropertyField(prop_mergeMode, G("merge_animator.merge_mode"));
|
||||
using (new EditorGUI.DisabledScope(prop_mergeMode.enumValueIndex == (int)MergeAnimatorMode.Replace))
|
||||
{
|
||||
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
|
||||
G("merge_animator.match_avatar_write_defaults"));
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
|
@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
|
||||
private bool posResetOptionFoldout = false;
|
||||
private bool posReset_convertATPose = true;
|
||||
private bool posReset_adjustRotation = false;
|
||||
private bool posReset_adjustScale = false;
|
||||
private bool posReset_heuristicRootScale = true;
|
||||
@ -99,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
if (target.mergeTargetObject != null && priorMergeTarget == null
|
||||
if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject
|
||||
&& string.IsNullOrEmpty(target.prefix)
|
||||
&& string.IsNullOrEmpty(target.suffix))
|
||||
{
|
||||
@ -114,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
|
||||
G("merge_armature.reset_pos.heuristic_scale"),
|
||||
posReset_heuristicRootScale);
|
||||
posReset_convertATPose = EditorGUILayout.ToggleLeft(
|
||||
G("merge_armature.reset_pos.convert_atpose"),
|
||||
posReset_convertATPose);
|
||||
posReset_adjustRotation = EditorGUILayout.ToggleLeft(
|
||||
G("merge_armature.reset_pos.adjust_rotation"),
|
||||
posReset_adjustRotation);
|
||||
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
|
||||
posReset_adjustScale);
|
||||
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
|
||||
G("merge_armature.reset_pos.heuristic_scale"),
|
||||
posReset_heuristicRootScale);
|
||||
|
||||
if (GUILayout.Button(G("merge_armature.reset_pos.execute")))
|
||||
{
|
||||
@ -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)
|
||||
{
|
||||
AdjustRootScale();
|
||||
@ -279,4 +308,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
@ -15,7 +15,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
_blendTree = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.BlendTree));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.PathMode));
|
||||
_relativePathRoot = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.RelativePathRoot));
|
||||
}
|
||||
@ -24,7 +26,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), G("merge_blend_tree.blend_tree"));
|
||||
EditorGUILayout.ObjectField(_blendTree, typeof(Motion), G("merge_blend_tree.motion"));
|
||||
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
|
||||
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
|
||||
{
|
||||
|
@ -54,7 +54,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
EditorGUILayout.LabelField(G("mesh_settings.header_probe_anchor"), EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(_prop_inherit_probe_anchor, G("mesh_settings.inherit_probe_anchor"));
|
||||
if (_prop_inherit_probe_anchor.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
|
||||
if (_prop_inherit_probe_anchor.enumValueIndex is (int) ModularAvatarMeshSettings.InheritMode.Set or (int) ModularAvatarMeshSettings.InheritMode.SetOrInherit)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_probe_anchor, G("mesh_settings.probe_anchor"));
|
||||
}
|
||||
@ -72,7 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
EditorGUILayout.LabelField(G("mesh_settings.header_bounds"), EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(_prop_inherit_bounds, G("mesh_settings.inherit_bounds"));
|
||||
if (_prop_inherit_bounds.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
|
||||
if (_prop_inherit_bounds.enumValueIndex is (int) ModularAvatarMeshSettings.InheritMode.Set or (int) ModularAvatarMeshSettings.InheritMode.SetOrInherit)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_root_bone, G("mesh_settings.root_bone"));
|
||||
EditorGUILayout.PropertyField(_prop_bounds, G("mesh_settings.bounds"));
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.ArmatureAwase;
|
||||
using nadena.dev.ndmf.preview;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
@ -15,15 +15,38 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
[SerializeField] private StyleSheet uss;
|
||||
[SerializeField] private VisualTreeAsset uxml;
|
||||
|
||||
private ComputeContext _ctx;
|
||||
private VisualElement _root;
|
||||
|
||||
private TransformChildrenNode _groupedNodesElem;
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
|
||||
}
|
||||
|
||||
protected override VisualElement CreateInnerInspectorGUI()
|
||||
{
|
||||
_root = new VisualElement();
|
||||
|
||||
RebuildInnerGUI();
|
||||
|
||||
return _root;
|
||||
}
|
||||
|
||||
private void RebuildInnerGUI()
|
||||
{
|
||||
_root.Clear();
|
||||
_ctx = new ComputeContext("MoveIndependentlyEditor");
|
||||
_root.Add(BuildInnerGUI(_ctx));
|
||||
}
|
||||
|
||||
private VisualElement BuildInnerGUI(ComputeContext ctx)
|
||||
{
|
||||
if (this.target == null) return new VisualElement();
|
||||
|
||||
_ctx.InvokeOnInvalidate(this, editor => editor.RebuildInnerGUI());
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var root = uxml.Localize();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
@ -32,9 +55,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var container = root.Q<VisualElement>("group-container");
|
||||
|
||||
MAMoveIndependently target = (MAMoveIndependently) this.target;
|
||||
var grouped = (target.GroupedBones ?? Array.Empty<GameObject>())
|
||||
.Select(obj => obj.transform)
|
||||
.ToImmutableHashSet();
|
||||
// Note: We specifically _don't_ use an ImmutableHashSet here as we want to update the previously-returned
|
||||
// set in place to avoid rebuilding GUI elements after the user changes the grouping.
|
||||
var grouped = ctx.Observe(target,
|
||||
t => (t.GroupedBones ?? Array.Empty<GameObject>())
|
||||
.Select(obj => obj.transform)
|
||||
.ToHashSet(),
|
||||
(x, y) => x.SetEquals(y)
|
||||
);
|
||||
|
||||
_groupedNodesElem = new TransformChildrenNode(target.transform, grouped);
|
||||
_groupedNodesElem.AddToClassList("group-root");
|
||||
@ -43,6 +71,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
Undo.RecordObject(target, "Toggle grouped nodes");
|
||||
target.GroupedBones = _groupedNodesElem.Active().Select(t => t.gameObject).ToArray();
|
||||
grouped.Clear();
|
||||
grouped.UnionWith(target.GroupedBones.Select(obj => obj.transform));
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(target);
|
||||
};
|
||||
|
||||
|
3
Editor/Inspector/ObjectToggle.meta
Normal file
3
Editor/Inspector/ObjectToggle.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61045dcdc7f24658a5b47fb0b67ab9fe
|
||||
timeCreated: 1722736548
|
25
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
Normal file
25
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
Normal file
@ -0,0 +1,25 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement name="root-box">
|
||||
<ui:VisualElement name="group-box">
|
||||
<ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/>
|
||||
|
||||
<ui:VisualElement name="ListViewContainer">
|
||||
<ui:ListView virtualization-method="DynamicHeight"
|
||||
reorder-mode="Animated"
|
||||
reorderable="true"
|
||||
show-add-remove-footer="true"
|
||||
show-border="true"
|
||||
show-foldout-header="false"
|
||||
name="Shapes"
|
||||
item-height="100"
|
||||
binding-path="m_objects"
|
||||
style="flex-grow: 1;"
|
||||
/>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ma:ROSimulatorButton/>
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
3
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
Normal file
3
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02f9cb4b3be34457870f111d73e2fd2f
|
||||
timeCreated: 1722736548
|
75
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
Normal file
75
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
Normal file
@ -0,0 +1,75 @@
|
||||
#region
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarObjectToggle))]
|
||||
public class ObjectSwitcherEditor : MAEditorBase
|
||||
{
|
||||
[SerializeField] private StyleSheet uss;
|
||||
[SerializeField] private VisualTreeAsset uxml;
|
||||
|
||||
private DragAndDropManipulator _dragAndDropManipulator;
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info);
|
||||
}
|
||||
|
||||
protected override VisualElement CreateInnerInspectorGUI()
|
||||
{
|
||||
var root = uxml.CloneTree();
|
||||
Localization.UI.Localize(root);
|
||||
root.styleSheets.Add(uss);
|
||||
|
||||
root.Bind(serializedObject);
|
||||
|
||||
ROSimulatorButton.BindRefObject(root, target);
|
||||
|
||||
var listView = root.Q<ListView>("Shapes");
|
||||
|
||||
listView.showBoundCollectionSize = false;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||
|
||||
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_dragAndDropManipulator != null)
|
||||
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
|
||||
}
|
||||
|
||||
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
|
||||
{
|
||||
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
|
||||
: base(targetElement, targetComponent) { }
|
||||
|
||||
protected override bool AllowKnownObjects => false;
|
||||
|
||||
protected override void AddObjectReferences(AvatarObjectReference[] references)
|
||||
{
|
||||
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
|
||||
TargetComponent.Objects.Add(toggledObject);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(TargetComponent);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
Normal file
13
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
Normal file
@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a77f3b5f35d04831a7896261cabd3370
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- uss: {fileID: 7433441132597879392, guid: b7559b81cea245b68c66602ea0cbbbcf, type: 3}
|
||||
- uxml: {fileID: 9197481963319205126, guid: 02f9cb4b3be34457870f111d73e2fd2f, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
62
Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
Normal file
62
Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
Normal file
@ -0,0 +1,62 @@
|
||||
#group-box {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
border-width: 3px;
|
||||
border-left-color: rgba(0, 1, 0, 0.2);
|
||||
border-top-color: rgba(0, 1, 0, 0.2);
|
||||
border-right-color: rgba(0, 1, 0, 0.2);
|
||||
border-bottom-color: rgba(0, 1, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
#group-box > Label {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
#ListViewContainer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.horizontal > * {
|
||||
height: 18px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.horizontal > Label {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.horizontal > PropertyField > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#f-object {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#f-active {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#f-active-dropdown {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.drop-area--drag-active {
|
||||
background-color: rgba(0, 127, 255, 0.2);
|
||||
}
|
||||
|
||||
.drop-area--drag-active .unity-scroll-view,
|
||||
.drop-area--drag-active .unity-list-view__footer,
|
||||
.drop-area--drag-active .unity-list-view__reorderable-item {
|
||||
background-color: rgba(0, 0, 0, 0.0);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7559b81cea245b68c66602ea0cbbbcf
|
||||
timeCreated: 1722736548
|
48
Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
Normal file
48
Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
Normal file
@ -0,0 +1,48 @@
|
||||
#region
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(ToggledObject))]
|
||||
public class ToggledObjectEditor : PropertyDrawer
|
||||
{
|
||||
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ObjectToggle/";
|
||||
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
|
||||
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
|
||||
|
||||
private const string V_On = "ON";
|
||||
private const string V_Off = "OFF";
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
|
||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
|
||||
Localization.UI.Localize(uxml);
|
||||
uxml.styleSheets.Add(uss);
|
||||
uxml.BindProperty(property);
|
||||
|
||||
var f_active = uxml.Q<Toggle>("f-active");
|
||||
var f_active_dropdown = uxml.Q<DropdownField>("f-active-dropdown");
|
||||
|
||||
f_active_dropdown.choices.Add(V_On);
|
||||
f_active_dropdown.choices.Add(V_Off);
|
||||
|
||||
f_active.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
f_active_dropdown.SetValueWithoutNotify(evt.newValue ? V_On : V_Off);
|
||||
});
|
||||
f_active_dropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
f_active.value = evt.newValue == V_On;
|
||||
});
|
||||
|
||||
return uxml;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d18528c5f704d3daf1160d9672bd09e
|
||||
timeCreated: 1722736548
|
7
Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
Normal file
7
Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
Normal file
@ -0,0 +1,7 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ed:PropertyField name="f-object" binding-path="Object" label=""/>
|
||||
<ui:Toggle name="f-active" binding-path="Active" label=""/>
|
||||
<ui:DropdownField name="f-active-dropdown"/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 565803cb95a04d1f98f7050c18234cdd
|
||||
timeCreated: 1722736548
|
@ -6,7 +6,10 @@ using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
using Button = UnityEngine.UIElements.Button;
|
||||
using Image = UnityEngine.UIElements.Image;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
@ -35,6 +38,37 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
listView.showBoundCollectionSize = false;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||
listView.selectionType = SelectionType.Multiple;
|
||||
listView.RegisterCallback<KeyDownEvent>(evt =>
|
||||
{
|
||||
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
var prop = serializedObject.FindProperty("parameters");
|
||||
|
||||
var indices = listView.selectedIndices.ToList();
|
||||
|
||||
foreach (var index in indices.OrderByDescending(i => i))
|
||||
{
|
||||
prop.DeleteArrayElementAtIndex(index);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
if (indices.Count == 0)
|
||||
{
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
// Works around an issue where the inner text boxes are auto-selected, preventing you from
|
||||
// just hitting delete over and over
|
||||
listView.SetSelectionWithoutNotify(indices);
|
||||
};
|
||||
}
|
||||
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}, TrickleDown.NoTrickleDown);
|
||||
|
||||
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
|
||||
|
||||
@ -128,10 +162,69 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
EditorApplication.delayCall += DetectParameters;
|
||||
}
|
||||
};
|
||||
|
||||
var importProp = root.Q<ObjectField>("p_import");
|
||||
importProp.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
ImportValues(importProp);
|
||||
importProp.SetValueWithoutNotify(null);
|
||||
});
|
||||
importProp.objectType = typeof(VRCExpressionParameters);
|
||||
importProp.allowSceneObjects = false;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void ImportValues(ObjectField importProp)
|
||||
{
|
||||
var known = new HashSet<string>();
|
||||
|
||||
var target = (ModularAvatarParameters)this.target;
|
||||
foreach (var parameter in target.parameters)
|
||||
{
|
||||
if (!parameter.isPrefix)
|
||||
{
|
||||
known.Add(parameter.nameOrPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
Undo.RecordObject(target, "Import parameters");
|
||||
|
||||
var source = (VRCExpressionParameters)importProp.value;
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameter in source.parameters)
|
||||
{
|
||||
if (!known.Contains(parameter.name))
|
||||
{
|
||||
ParameterSyncType pst;
|
||||
|
||||
switch (parameter.valueType)
|
||||
{
|
||||
case VRCExpressionParameters.ValueType.Bool: pst = ParameterSyncType.Bool; break;
|
||||
case VRCExpressionParameters.ValueType.Float: pst = ParameterSyncType.Float; break;
|
||||
case VRCExpressionParameters.ValueType.Int: pst = ParameterSyncType.Int; break;
|
||||
default: pst = ParameterSyncType.Float; break;
|
||||
}
|
||||
|
||||
target.parameters.Add(new ParameterConfig()
|
||||
{
|
||||
internalParameter = false,
|
||||
nameOrPrefix = parameter.name,
|
||||
isPrefix = false,
|
||||
remapTo = "",
|
||||
syncType = pst,
|
||||
localOnly = !parameter.networkSynced,
|
||||
defaultValue = parameter.defaultValue,
|
||||
saved = parameter.saved,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DetectParameters()
|
||||
{
|
||||
var known = new HashSet<string>();
|
||||
@ -162,4 +255,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
@ -1,7 +1,4 @@
|
||||
using System.Globalization;
|
||||
using nadena.dev.modular_avatar.core;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
@ -13,66 +10,125 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
}
|
||||
|
||||
private readonly TextField _visibleField;
|
||||
private readonly FloatField _defaultValueField;
|
||||
private readonly Toggle _hasExplicitDefaultSetField;
|
||||
private const string V_None = " ";
|
||||
private const string V_True = "ON";
|
||||
private const string V_False = "OFF";
|
||||
|
||||
private readonly FloatField _defaultValueField;
|
||||
private readonly Toggle _hasExplicitDefaultValueField;
|
||||
private readonly TextField _numberField;
|
||||
private readonly DropdownField _boolField;
|
||||
|
||||
private ParameterSyncType _syncType;
|
||||
private bool _hasInitialBinding;
|
||||
|
||||
public DefaultValueField()
|
||||
{
|
||||
// Hidden binding elements
|
||||
_defaultValueField = new FloatField();
|
||||
_hasExplicitDefaultSetField = new Toggle();
|
||||
|
||||
_defaultValueField.RegisterValueChangedCallback(
|
||||
evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultSetField.value));
|
||||
_defaultValueField.style.display = DisplayStyle.None;
|
||||
_defaultValueField.bindingPath = nameof(ParameterConfig.defaultValue);
|
||||
|
||||
_hasExplicitDefaultSetField.RegisterValueChangedCallback(
|
||||
evt => UpdateVisibleField(_defaultValueField.value, evt.newValue));
|
||||
_hasExplicitDefaultSetField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue);
|
||||
_defaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultValueField.value));
|
||||
_hasExplicitDefaultValueField = new Toggle();
|
||||
_hasExplicitDefaultValueField.style.display = DisplayStyle.None;
|
||||
_hasExplicitDefaultValueField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue);
|
||||
_hasExplicitDefaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(_defaultValueField.value, evt.newValue));
|
||||
|
||||
_visibleField = new TextField();
|
||||
_visibleField.RegisterValueChangedCallback(evt =>
|
||||
// Visible elements for input
|
||||
_numberField = new TextField();
|
||||
_numberField.isDelayed = true;
|
||||
_numberField.RegisterValueChangedCallback(evt => OnUpdateNumberValue(evt.newValue));
|
||||
_boolField = new DropdownField();
|
||||
_boolField.choices.Add(V_None);
|
||||
_boolField.choices.Add(V_True);
|
||||
_boolField.choices.Add(V_False);
|
||||
_boolField.RegisterValueChangedCallback(evt => OnUpdateBoolValue(evt.newValue));
|
||||
|
||||
Add(_defaultValueField);
|
||||
Add(_hasExplicitDefaultValueField);
|
||||
Add(_numberField);
|
||||
Add(_boolField);
|
||||
}
|
||||
|
||||
public void OnUpdateSyncType(ParameterSyncType syncType)
|
||||
{
|
||||
_syncType = syncType;
|
||||
|
||||
if (syncType != ParameterSyncType.Bool)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(evt.newValue))
|
||||
_numberField.style.display = DisplayStyle.Flex;
|
||||
_boolField.style.display = DisplayStyle.None;
|
||||
OnUpdateNumberValue(_numberField.value, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_numberField.style.display = DisplayStyle.None;
|
||||
_boolField.style.display = DisplayStyle.Flex;
|
||||
OnUpdateBoolValue(_boolField.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
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 (!implicitUpdate)
|
||||
{
|
||||
_hasExplicitDefaultSetField.value = false;
|
||||
_defaultValueField.value = 0;
|
||||
}
|
||||
else
|
||||
|
||||
theValue = _defaultValueField.value;
|
||||
|
||||
_hasExplicitDefaultValueField.value = false;
|
||||
}
|
||||
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
||||
&& !float.IsNaN(parsed)
|
||||
&& !float.IsInfinity(parsed))
|
||||
{
|
||||
theValue = _defaultValueField.value = _syncType switch
|
||||
{
|
||||
_hasExplicitDefaultSetField.value = true;
|
||||
_defaultValueField.value = float.Parse(evt.newValue, CultureInfo.InvariantCulture);
|
||||
}
|
||||
});
|
||||
|
||||
_defaultValueField.style.width = 0;
|
||||
_defaultValueField.SetEnabled(false);
|
||||
_hasExplicitDefaultSetField.style.width = 0;
|
||||
_hasExplicitDefaultSetField.SetEnabled(false);
|
||||
|
||||
style.flexDirection = FlexDirection.Row;
|
||||
|
||||
Add(_visibleField);
|
||||
Add(_defaultValueField);
|
||||
Add(_hasExplicitDefaultSetField);
|
||||
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
|
||||
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
|
||||
ParameterSyncType.Bool => parsed != 0 ? 1 : 0,
|
||||
_ => parsed,
|
||||
};
|
||||
_hasExplicitDefaultValueField.value = true;
|
||||
}
|
||||
|
||||
UpdateVisibleField(theValue, _hasExplicitDefaultValueField.value);
|
||||
}
|
||||
|
||||
public void ManualBindProperty(SerializedProperty property)
|
||||
private void OnUpdateBoolValue(string value, bool implicitUpdate = false)
|
||||
{
|
||||
_defaultValueField.BindProperty(property);
|
||||
_hasExplicitDefaultSetField.BindProperty(property);
|
||||
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
|
||||
// In this case, suppress the update to avoid losing data.
|
||||
if (implicitUpdate && !_hasInitialBinding) return;
|
||||
|
||||
_defaultValueField.value = value == V_True ? 1 : 0;
|
||||
_hasExplicitDefaultValueField.value = value != V_None;
|
||||
|
||||
UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value);
|
||||
}
|
||||
|
||||
|
||||
private void UpdateVisibleField(float value, bool hasExplicitValue)
|
||||
{
|
||||
if (Mathf.Abs(value) > 0.0000001)
|
||||
{
|
||||
hasExplicitValue = true;
|
||||
}
|
||||
_hasInitialBinding = true;
|
||||
|
||||
var str = hasExplicitValue ? value.ToString(CultureInfo.InvariantCulture) : "";
|
||||
_visibleField.SetValueWithoutNotify(str);
|
||||
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
|
||||
{
|
||||
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));
|
||||
_boolField.SetValueWithoutNotify(value != 0 ? V_True : V_False);
|
||||
}
|
||||
else
|
||||
{
|
||||
_numberField.SetValueWithoutNotify(string.Empty);
|
||||
_boolField.SetValueWithoutNotify(V_None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Toggle = UnityEngine.UIElements.Toggle;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.Parameters
|
||||
{
|
||||
@ -20,105 +21,251 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
|
||||
Localization.UI.Localize(root);
|
||||
root.styleSheets.Add(uss);
|
||||
|
||||
var foldout = root.Q<Foldout>();
|
||||
var foldoutLabel = foldout?.Q<Label>();
|
||||
if (foldoutLabel != null)
|
||||
{
|
||||
foldoutLabel.bindingPath = "nameOrPrefix";
|
||||
}
|
||||
|
||||
var miniDisplay = root.Q<VisualElement>("MiniDisplay");
|
||||
miniDisplay.RemoveFromHierarchy();
|
||||
foldoutLabel.parent.Add(miniDisplay);
|
||||
miniDisplay.styleSheets.Add(uss);
|
||||
var f_type = root.Q<DropdownField>("f-type");
|
||||
var f_sync_type = root.Q<DropdownField>("f-sync-type");
|
||||
var f_is_prefix = root.Q<VisualElement>("f-is-prefix");
|
||||
SetupPairedDropdownField(
|
||||
root,
|
||||
f_type,
|
||||
f_sync_type,
|
||||
f_is_prefix,
|
||||
("Bool", "False", "params.syncmode.Bool"),
|
||||
("Float", "False", "params.syncmode.Float"),
|
||||
("Int", "False", "params.syncmode.Int"),
|
||||
("Not Synced", "False", "params.syncmode.NotSynced"),
|
||||
(null, "True", "params.syncmode.PhysBonesPrefix")
|
||||
);
|
||||
|
||||
var isPrefixProp = root.Q<PropertyField>("isPrefix");
|
||||
bool isPrefix = false;
|
||||
Action evaluateMiniDisplay = () =>
|
||||
var f_default = root.Q<DefaultValueField>();
|
||||
f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index);
|
||||
f_sync_type.RegisterValueChangedCallback(evt => f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index));
|
||||
|
||||
var f_synced = root.Q<Toggle>("f-synced");
|
||||
var f_local_only = root.Q<Toggle>("f-local-only");
|
||||
|
||||
// Invert f_local_only and f_synced
|
||||
f_local_only.RegisterValueChangedCallback(evt => { f_synced.SetValueWithoutNotify(!evt.newValue); });
|
||||
|
||||
f_synced.RegisterValueChangedCallback(evt => { f_local_only.value = !evt.newValue; });
|
||||
|
||||
var internalParamAccessor = root.Q<Toggle>("f-internal-parameter");
|
||||
internalParamAccessor.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
miniDisplay.style.display = (isPrefix || foldout.value) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
if (evt.newValue)
|
||||
root.AddToClassList("st-internal-parameter");
|
||||
else
|
||||
root.RemoveFromClassList("st-internal-parameter");
|
||||
});
|
||||
|
||||
root.Q<VisualElement>("remap-to-group-disabled").SetEnabled(false);
|
||||
|
||||
var name = root.Q<TextField>("f-name");
|
||||
var remapTo = root.Q<TextField>("f-remap-to");
|
||||
var remapToInner = remapTo.Q<TextElement>();
|
||||
var remapToPlaceholder = root.Q<Label>("f-remap-to-placeholder");
|
||||
remapToPlaceholder.pickingMode = PickingMode.Ignore;
|
||||
|
||||
Action updateRemapToPlaceholder = () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(remapTo.value))
|
||||
remapToPlaceholder.text = name.value;
|
||||
else
|
||||
remapToPlaceholder.text = "";
|
||||
};
|
||||
|
||||
|
||||
foldout.RegisterValueChangedCallback(evt => evaluateMiniDisplay());
|
||||
|
||||
isPrefixProp.RegisterValueChangeCallback(evt =>
|
||||
{
|
||||
var value = evt.changedProperty.boolValue;
|
||||
if (value)
|
||||
{
|
||||
root.AddToClassList("ParameterConfig__isPrefix_true");
|
||||
root.RemoveFromClassList("ParameterConfig__isPrefix_false");
|
||||
}
|
||||
else
|
||||
{
|
||||
root.AddToClassList("ParameterConfig__isPrefix_false");
|
||||
root.RemoveFromClassList("ParameterConfig__isPrefix_true");
|
||||
}
|
||||
|
||||
isPrefix = value;
|
||||
evaluateMiniDisplay();
|
||||
});
|
||||
|
||||
var syncTypeProp = root.Q<PropertyField>("syncType");
|
||||
// TODO: This callback is not actually invoked on initial bind...
|
||||
syncTypeProp.RegisterValueChangeCallback(evt =>
|
||||
{
|
||||
var value = (ParameterSyncType) evt.changedProperty.enumValueIndex;
|
||||
if (value == ParameterSyncType.NotSynced)
|
||||
{
|
||||
root.AddToClassList("ParameterConfig__animatorOnly_true");
|
||||
root.RemoveFromClassList("ParameterConfig__animatorOnly_false");
|
||||
}
|
||||
else
|
||||
{
|
||||
root.AddToClassList("ParameterConfig__animatorOnly_false");
|
||||
name.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
|
||||
|
||||
root.RemoveFromClassList("ParameterConfig__animatorOnly_true");
|
||||
}
|
||||
});
|
||||
remapTo.RegisterValueChangedCallback(evt => { updateRemapToPlaceholder(); });
|
||||
|
||||
/*
|
||||
var overridePlaceholder = root.Q<Toggle>("overridePlaceholder");
|
||||
overridePlaceholder.labelElement.AddToClassList("ndmf-tr");
|
||||
overridePlaceholder.SetEnabled(false);
|
||||
*/
|
||||
|
||||
var remapTo = root.Q<PropertyField>("remapTo");
|
||||
var remapToPlaceholder = root.Q<TextField>("remapToPlaceholder");
|
||||
remapToPlaceholder.labelElement.AddToClassList("ndmf-tr");
|
||||
remapToPlaceholder.SetEnabled(false);
|
||||
|
||||
Localization.UI.Localize(remapToPlaceholder.labelElement);
|
||||
|
||||
root.Q<PropertyField>("internalParameter").RegisterValueChangeCallback(evt =>
|
||||
{
|
||||
remapTo.style.display = evt.changedProperty.boolValue ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
remapToPlaceholder.style.display = evt.changedProperty.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
});
|
||||
|
||||
|
||||
// This is a bit of a hack, but I'm not sure of another way to properly align property labels with a custom
|
||||
// field, when we only want to manipulate a subset of fields on an object...
|
||||
var defaultValueField = root.Q<VisualElement>("innerDefaultValueField"); // create ahead of time so it's bound...
|
||||
|
||||
// Then move it into the property field once the property field has created its inner controls
|
||||
var defaultValueProp = root.Q<PropertyField>("defaultValueProp");
|
||||
defaultValueProp.RegisterCallback<GeometryChangedEvent>(evt =>
|
||||
{
|
||||
var floatField = defaultValueProp.Q<FloatField>();
|
||||
var innerField = floatField?.Q<DefaultValueField>();
|
||||
remapToPlaceholder.RemoveFromHierarchy();
|
||||
remapToInner.Add(remapToPlaceholder);
|
||||
|
||||
if (floatField != null && innerField == null)
|
||||
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 =>
|
||||
{
|
||||
defaultValueField.RemoveFromHierarchy();
|
||||
floatField.contentContainer.Add(defaultValueField);
|
||||
}
|
||||
});
|
||||
if (evt.keyCode == KeyCode.Delete && evt.modifiers == EventModifiers.FunctionKey)
|
||||
evt.StopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private interface Accessor
|
||||
{
|
||||
Action<string> OnValueChanged { get; set; }
|
||||
string Value { get; set; }
|
||||
}
|
||||
|
||||
private class ToggleAccessor : Accessor
|
||||
{
|
||||
private readonly Toggle _toggle;
|
||||
|
||||
public ToggleAccessor(Toggle toggle)
|
||||
{
|
||||
_toggle = toggle;
|
||||
_toggle.RegisterValueChangedCallback(evt => OnValueChanged?.Invoke(evt.newValue.ToString()));
|
||||
}
|
||||
|
||||
public Action<string> OnValueChanged { get; set; }
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _toggle.value.ToString();
|
||||
set => _toggle.value = value == "True";
|
||||
}
|
||||
}
|
||||
|
||||
private class DropdownAccessor : Accessor
|
||||
{
|
||||
private readonly DropdownField _dropdown;
|
||||
|
||||
public DropdownAccessor(DropdownField dropdown)
|
||||
{
|
||||
_dropdown = dropdown;
|
||||
_dropdown.RegisterValueChangedCallback(evt => OnValueChanged?.Invoke(evt.newValue));
|
||||
}
|
||||
|
||||
public Action<string> OnValueChanged { get; set; }
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _dropdown.value;
|
||||
set => _dropdown.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Accessor GetAccessor(VisualElement elem)
|
||||
{
|
||||
var toggle = elem.Q<Toggle>();
|
||||
if (toggle != null) return new ToggleAccessor(toggle);
|
||||
|
||||
var dropdown = elem.Q<DropdownField>();
|
||||
if (dropdown != null)
|
||||
{
|
||||
return new DropdownAccessor(dropdown);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported element type");
|
||||
}
|
||||
|
||||
private void SetupPairedDropdownField(
|
||||
VisualElement root,
|
||||
DropdownField target,
|
||||
VisualElement v_type,
|
||||
VisualElement v_pbPrefix,
|
||||
// p1, p2, localization key
|
||||
params (string, string, string)[] choices
|
||||
)
|
||||
{
|
||||
var p_type = GetAccessor(v_type);
|
||||
var p_prefix = GetAccessor(v_pbPrefix);
|
||||
|
||||
for (var i = 0; i < choices.Length; i++) target.choices.Add("" + i);
|
||||
|
||||
target.formatListItemCallback = s_n =>
|
||||
{
|
||||
if (int.TryParse(s_n, out var n) && n >= 0 && n < choices.Length)
|
||||
{
|
||||
return Localization.S(choices[n].Item3);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
};
|
||||
target.formatSelectedValueCallback = target.formatListItemCallback;
|
||||
|
||||
var inLoop = false;
|
||||
string current_type_class = null;
|
||||
|
||||
target.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (inLoop) return;
|
||||
|
||||
if (int.TryParse(evt.newValue, out var n) && n >= 0 && n < choices.Length)
|
||||
{
|
||||
p_type.Value = choices[n].Item1;
|
||||
p_prefix.Value = choices[n].Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
p_type.Value = "";
|
||||
p_prefix.Value = "";
|
||||
}
|
||||
});
|
||||
|
||||
p_type.OnValueChanged = s =>
|
||||
{
|
||||
inLoop = true;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
var new_class = "st-ty-" + s.Replace(" ", "-");
|
||||
|
||||
root.RemoveFromClassList(current_type_class);
|
||||
current_type_class = null;
|
||||
|
||||
root.AddToClassList(new_class);
|
||||
current_type_class = new_class;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(s)) return;
|
||||
|
||||
for (var i = 0; i < choices.Length; i++)
|
||||
if (choices[i].Item1 == s && (choices[i].Item2 == null || choices[i].Item2 == p_prefix.Value))
|
||||
{
|
||||
target.SetValueWithoutNotify("" + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
inLoop = false;
|
||||
}
|
||||
};
|
||||
|
||||
p_prefix.OnValueChanged = s =>
|
||||
{
|
||||
inLoop = true;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return;
|
||||
|
||||
if (bool.TryParse(s, out var b))
|
||||
{
|
||||
if (b) root.AddToClassList("st-pb-prefix");
|
||||
else root.RemoveFromClassList("st-pb-prefix");
|
||||
}
|
||||
|
||||
for (var i = 0; i < choices.Length; i++)
|
||||
if ((choices[i].Item1 == null || choices[i].Item1 == p_type.Value) && choices[i].Item2 == s)
|
||||
{
|
||||
target.SetValueWithoutNotify("" + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
inLoop = false;
|
||||
}
|
||||
};
|
||||
|
||||
inLoop = true;
|
||||
for (var i = 0; i < choices.Length; i++)
|
||||
if (choices[i].Item1 == p_type.Value && choices[i].Item2 == p_prefix.Value)
|
||||
{
|
||||
target.SetValueWithoutNotify("" + i);
|
||||
break;
|
||||
}
|
||||
|
||||
inLoop = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -1,47 +1,38 @@
|
||||
<ui:UXML
|
||||
xmlns:ui="UnityEngine.UIElements"
|
||||
xmlns:engine="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor"
|
||||
editor-extension-mode="False"
|
||||
>
|
||||
<ui:VisualElement name="MiniDisplay">
|
||||
<ui:Label text="merge_parameter.ui.defaultValue" class="ndmf-tr"/>
|
||||
<ma:DefaultValueField/>
|
||||
<ui:Label text="merge_parameter.ui.saved" class="ndmf-tr"/>
|
||||
<ui:Toggle binding-path="saved"/>
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ui:TextField name="f-name" binding-path="nameOrPrefix" label=""/>
|
||||
<ui:DropdownField name="f-type"/>
|
||||
<ui:DropdownField name="f-sync-type" binding-path="syncType"/>
|
||||
<ui:Toggle name="f-is-prefix" binding-path="isPrefix"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:Foldout name="ParameterConfigRoot" text="(placeholder)" value="false">
|
||||
<engine:PropertyField binding-path="nameOrPrefix" label="merge_parameter.ui.name" name="f-name" class="ndmf-tr ParameterConfig__isPrefix_falseOnly" />
|
||||
<engine:PropertyField binding-path="nameOrPrefix" label="merge_parameter.ui.prefix" name="f-prefix" class="ndmf-tr ParameterConfig__isPrefix_trueOnly" />
|
||||
<engine:PropertyField binding-path="remapTo" label="merge_parameter.ui.remapTo" name="remapTo" class="ndmf-tr" />
|
||||
<ui:TextField label="merge_parameter.ui.remapTo" text="merge_parameter.ui.remapTo.automatic"
|
||||
name="remapToPlaceholder" enabled="false"
|
||||
class="ndmf-tr unity-base-field__aligned disabledPlaceholder"/>
|
||||
|
||||
<!-- this field is not visible until it's moved into the PropertyField below -->
|
||||
<ma:DefaultValueField
|
||||
name="innerDefaultValueField"
|
||||
class="unity-base-field__input unity-property-field__input"
|
||||
/>
|
||||
|
||||
<engine:PropertyField binding-path="defaultValue" name="defaultValueProp" label="merge_parameter.ui.defaultValue" class="ndmf-tr ParameterConfig__isPrefix_falseOnly">
|
||||
|
||||
</engine:PropertyField>
|
||||
|
||||
<engine:PropertyField binding-path="saved" label="merge_parameter.ui.saved" class="ndmf-tr ParameterConfig__isPrefix_falseOnly" />"
|
||||
|
||||
<engine:PropertyField binding-path="internalParameter" label="merge_parameter.ui.internalParameter" name="internalParameter" class="ndmf-tr" />
|
||||
<engine:PropertyField binding-path="isPrefix" label="merge_parameter.ui.isPrefix" name="isPrefix" class="ndmf-tr" />
|
||||
|
||||
<engine:PropertyField binding-path="syncType" label="merge_parameter.ui.syncType"
|
||||
class="ParameterConfig__isPrefix_falseOnly ndmf-tr" name="syncType"/>
|
||||
<engine:PropertyField binding-path="m_overrideAnimatorDefaults" name="overrideDefaults"
|
||||
label="merge_parameter.ui.overrideAnimatorDefaults"
|
||||
class="ParameterConfig__isPrefix_falseOnly ndmf-tr"/>
|
||||
<!-- <ui:Toggle label="merge_parameter.ui.overrideAnimatorDefaults" value="true" enabled="false" name="overridePlaceholder" class="ParameterConfig__isPrefix_falseOnly ParameterConfig__animatorOnly_trueOnly ndmf-tr" /> -->
|
||||
|
||||
<engine:PropertyField binding-path="localOnly" label="merge_parameter.ui.localOnly" class="ParameterConfig__isPrefix_falseOnly ParameterConfig__animatorOnly_falseOnly ndmf-tr" />
|
||||
</ui:Foldout>
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ui:VisualElement name="remap-to-group-disabled" class="horizontal">
|
||||
<ui:Label text="merge_parameter.ui.remapTo" class="ndmf-tr"/>
|
||||
<ui:TextField name="f-remap-to-disabled"/>
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="remap-to-group" class="horizontal">
|
||||
<ui:Label text="merge_parameter.ui.remapTo" class="ndmf-tr"/>
|
||||
<ui:TextField name="f-remap-to" binding-path="remapTo"/>
|
||||
<ui:Label name="f-remap-to-placeholder"/>
|
||||
</ui:VisualElement>
|
||||
<ui:Toggle name="f-internal-parameter" binding-path="internalParameter"
|
||||
text="merge_parameter.ui.internalParameter" class="ndmf-tr"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="horizontal st-pb-prefix__hide">
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ui:Label text="merge_parameter.ui.defaultValue" class="ndmf-tr"/>
|
||||
<ma:DefaultValueField/>
|
||||
</ui:VisualElement>
|
||||
<ui:Toggle binding-path="saved"
|
||||
text="merge_parameter.ui.saved" class="ndmf-tr st-anim-only__hide"/>
|
||||
<ui:Toggle name="f-local-only" binding-path="localOnly"
|
||||
text="merge_parameter.ui.localOnly" class="ndmf-tr st-anim-only__hide"/>
|
||||
<ui:Toggle name="f-synced"
|
||||
text="merge_parameter.ui.synced" class="ndmf-tr st-anim-only__hide"/>
|
||||
<ui:Toggle binding-path="m_overrideAnimatorDefaults"
|
||||
text="merge_parameter.ui.overrideAnimatorDefaults" class="ndmf-tr st-anim-only__hide"/>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
|
@ -1,116 +1,106 @@
|
||||
VisualElement {}
|
||||
#ListViewContainer {
|
||||
margin-top: 4px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.ParameterConfig__isPrefix_true .ParameterConfig__isPrefix_falseOnly {
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.horizontal > * {
|
||||
height: 18px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.horizontal > Label {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.horizontal > PropertyField > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#f-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#f-sync-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ParameterConfig__isPrefix_false .ParameterConfig__isPrefix_trueOnly {
|
||||
.st-ty-Not-Synced .st-anim-only__hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ParameterConfig__animatorOnly_true .ParameterConfig__animatorOnly_falseOnly {
|
||||
#f-is-prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ParameterConfig__animatorOnly_false .ParameterConfig__animatorOnly_trueOnly {
|
||||
.st-pb-prefix .st-pb-prefix__hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#defaultValueGroup {
|
||||
#f-remap-to, #f-remap-to-disabled {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#f-remap-to-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
#f-internal-parameter {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
#remap-to-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#defaultValueGroup > .unity-base-field__input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#defaultValueGroup > .unity-base-field__input > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#defaultValueProp > FloatField > FloatInput {
|
||||
#remap-to-group-disabled {
|
||||
display: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.st-internal-parameter #remap-to-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ParameterConfigRoot > DefaultValueField {
|
||||
.st-internal-parameter #remap-to-group-disabled {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.horizontal > .horizontal {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
DefaultValueField > * {
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#f-local-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#innerDefaultValueField {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
DefaultValueField > TextField {
|
||||
flex-grow: 1;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#MiniDisplay {
|
||||
flex-direction: row;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
#MiniDisplay > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#MiniDisplay > DefaultValueField {
|
||||
max-width: 60px;
|
||||
flex-grow: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
#MiniDisplay > DefaultValueField TextElement {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
|
||||
#MiniDisplay > Toggle {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#ParameterConfigRoot > Toggle .unity-toggle__text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#UnregisteredParameters #unity-list-view__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#UnregisteredParameters Label {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.DetectedParameter {
|
||||
flex-direction: row;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.DetectedParameter > Label {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.SourceButton {
|
||||
flex-grow:0;
|
||||
align-self: flex-end;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
/* Vertically align the reorder handle with the foldout chevron */
|
||||
#Parameters #unity-list-view__reorderable-handle {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#ParameterConfigRoot .unity-foldout__input > #unity-checkmark {
|
||||
margin-top: 4px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
#ParameterConfigRoot .unity-foldout__input > Label {
|
||||
margin-top: 4px;
|
||||
align-self: flex-start;
|
||||
}
|
@ -12,7 +12,6 @@
|
||||
show-border="true"
|
||||
show-foldout-header="false"
|
||||
name="Parameters"
|
||||
item-height="100"
|
||||
binding-path="parameters"
|
||||
style="flex-grow: 1;"
|
||||
/>
|
||||
@ -33,5 +32,7 @@
|
||||
/>
|
||||
</ui:Foldout>
|
||||
|
||||
<editor:ObjectField name="p_import" label="merge_parameter.ui.importFromAsset" class="ndmf-tr"/>
|
||||
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
}
|
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