feat: change shape changer to support multiple target renderers (#1011)

* feat: add target renderer to ChangedShape

* chore: add test for ShapeChanger target renderer

* feat: add override target to MaterialSetter

* fix: resolve added AvatarObjectReference

* fix: record prefab instance property modifications

* refactor: remove unused setter for AvatarObjectReference

* refactor: change ChangedShape and MaterialSwitchObject from struct to class

* feat: remove override target from ShapeChanger and MaterialSetter

* refactor: align flow and code style of ShapeChanger and MaterialSetter

* feat: ShapeChanger target migration

* fix: add null check

* chore: added some comments and nullchecks

---------

Co-authored-by: bd_ <bd_@nadena.dev>
This commit is contained in:
nekobako 2024-08-22 12:27:10 +09:00 committed by GitHub
parent 3b9f0d1838
commit 8418f8e047
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 334 additions and 389 deletions

View File

@ -28,44 +28,14 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
var f_material_index = uxml.Q<DropdownField>("f-material-index"); var f_material_index = uxml.Q<DropdownField>("f-material-index");
var f_object = uxml.Q<ObjectField>("f-object"); var f_object = uxml.Q<PropertyField>("f-object");
f_object.objectType = typeof(Renderer);
f_object.allowSceneObjects = true;
var f_target_object = uxml.Q<ObjectField>("f-obj-target-object"); f_object.RegisterValueChangeCallback(evt =>
var f_reference_path = uxml.Q<TextField>("f-obj-ref-path");
f_object.RegisterValueChangedCallback(evt =>
{ {
var gameObj = (evt.newValue as Renderer)?.gameObject;
if (gameObj == null)
{
f_target_object.value = null;
f_reference_path.value = "";
}
else
{
var path = RuntimeUtil.AvatarRootPath(gameObj);
f_reference_path.value = path;
if (path == "")
{
f_target_object.value = null;
}
else
{
f_target_object.value = gameObj;
}
}
EditorApplication.delayCall += UpdateMaterialDropdown; EditorApplication.delayCall += UpdateMaterialDropdown;
}); });
UpdateMaterialDropdown(); UpdateMaterialDropdown();
f_target_object.RegisterValueChangedCallback(_ => UpdateVisualTarget());
f_reference_path.RegisterValueChangedCallback(_ => UpdateVisualTarget());
// Link dropdown to material index field // Link dropdown to material index field
var f_material_index_int = uxml.Q<IntegerField>("f-material-index-int"); var f_material_index_int = uxml.Q<IntegerField>("f-material-index-int");
f_material_index_int.RegisterValueChangedCallback(evt => f_material_index_int.RegisterValueChangedCallback(evt =>
@ -83,30 +53,13 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
return uxml; return uxml;
void UpdateVisualTarget()
{
var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
Renderer targetRenderer;
try
{
targetRenderer = targetObject?.GetComponent<Renderer>();
}
catch (MissingComponentException e)
{
targetRenderer = null;
}
f_object.SetValueWithoutNotify(targetRenderer);
}
void UpdateMaterialDropdown() void UpdateMaterialDropdown()
{ {
var toggledObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object")); var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
Material[] sharedMaterials; Material[] sharedMaterials;
try try
{ {
sharedMaterials = toggledObject?.GetComponent<Renderer>()?.sharedMaterials; sharedMaterials = targetObject?.GetComponent<Renderer>()?.sharedMaterials;
} }
catch (MissingComponentException e) catch (MissingComponentException e)
{ {

View File

@ -1,13 +1,9 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"> <UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="toggled-object-editor"> <ui:VisualElement class="toggled-object-editor">
<ui:VisualElement class="horizontal"> <ui:VisualElement class="horizontal">
<!--<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>--> <ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ed:ObjectField label="" name="f-object" class="f-object"/>
<ui:DropdownField name="f-material-index" binding-path="MaterialIndex"/> <ui:DropdownField name="f-material-index" binding-path="MaterialIndex"/>
<ui:VisualElement style="display:none"> <ui:VisualElement style="display:none">
<ui:TextField binding-path="Object.referencePath" label="" name="f-obj-ref-path"/>
<ed:ObjectField name="f-obj-target-object" binding-path="Object.targetObject"/>
<ed:IntegerField binding-path="MaterialIndex" name="f-material-index-int"/> <ed:IntegerField binding-path="MaterialIndex" name="f-material-index-int"/>
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>

View File

@ -1,99 +0,0 @@
#region
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
public class AddShapePopup : PopupWindowContent
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/";
const string UxmlPath = Root + "AddShapePopup.uxml";
const string UssPath = Root + "ShapeChangerStyles.uss";
private VisualElement _elem;
private ScrollView _scrollView;
public AddShapePopup(ModularAvatarShapeChanger changer)
{
if (changer == null) return;
var target = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
if (target == null || target.sharedMesh == null) return;
var alreadyRegistered = changer.Shapes.Select(c => c.ShapeName).ToHashSet();
var keys = new List<string>();
for (int i = 0; i < target.sharedMesh.blendShapeCount; i++)
{
var name = target.sharedMesh.GetBlendShapeName(i);
if (alreadyRegistered.Contains(name)) continue;
keys.Add(name);
}
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
_elem = uxml.CloneTree();
_elem.styleSheets.Add(uss);
Localization.UI.Localize(_elem);
_scrollView = _elem.Q<ScrollView>("scroll-view");
if (keys.Count > 0)
{
_scrollView.contentContainer.Clear();
foreach (var key in keys)
{
var container = new VisualElement();
container.AddToClassList("add-shape-row");
Button btn = default;
btn = new Button(() =>
{
AddShape(changer, key);
container.RemoveFromHierarchy();
});
btn.text = "+";
container.Add(btn);
var label = new Label(key);
container.Add(label);
_scrollView.contentContainer.Add(container);
}
}
}
private void AddShape(ModularAvatarShapeChanger changer, string key)
{
Undo.RecordObject(changer, "Add Shape");
changer.Shapes.Add(new ChangedShape()
{
ShapeName = key,
ChangeType = ShapeChangeType.Delete,
Value = 100
});
}
public override void OnGUI(Rect rect)
{
}
public override void OnOpen()
{
editorWindow.rootVisualElement.Clear();
editorWindow.rootVisualElement.Add(_elem);
//editorWindow.rootVisualElement.Clear();
//editorWindow.rootVisualElement.Add(new Label("Hello, World!"));
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1a8351fafb3740918363f60365adfeda
timeCreated: 1717205112

View File

@ -1,11 +0,0 @@
<UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement class="add-shape-popup">
<ui:VisualElement class="add-shape-popup">
<ui:Label text="Select Blendshape"/>
<ui:VisualElement class="vline"/>
<ui:ScrollView show-horizontal-scroller="false" name="scroll-view">
<ui:Label text="&lt;none remaining>" class="placeholder"/>
</ui:ScrollView>
</ui:VisualElement>
</ui:VisualElement>
</UXML>

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6753a7b3eae1416cb04786cf53778c33
timeCreated: 1717205258

View File

@ -1,7 +1,10 @@
#region #region
using System.Collections.Generic;
using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
#endregion #endregion
@ -24,6 +27,16 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
uxml.styleSheets.Add(uss); uxml.styleSheets.Add(uss);
uxml.BindProperty(property); uxml.BindProperty(property);
var f_shape_name = uxml.Q<DropdownField>("f-shape-name");
var f_object = uxml.Q<PropertyField>("f-object");
f_object.RegisterValueChangeCallback(evt =>
{
EditorApplication.delayCall += UpdateShapeDropdown;
});
UpdateShapeDropdown();
uxml.Q<PropertyField>("f-change-type").RegisterCallback<ChangeEvent<string>>( uxml.Q<PropertyField>("f-change-type").RegisterCallback<ChangeEvent<string>>(
e => e =>
{ {
@ -39,6 +52,29 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
); );
return uxml; return uxml;
void UpdateShapeDropdown()
{
var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
List<string> shapeNames;
try
{
var mesh = targetObject?.GetComponent<SkinnedMeshRenderer>()?.sharedMesh;
shapeNames = mesh == null ? null : Enumerable.Range(0, mesh.blendShapeCount)
.Select(x => mesh.GetBlendShapeName(x))
.ToList();
}
catch (MissingComponentException e)
{
shapeNames = null;
}
f_shape_name.SetEnabled(shapeNames != null);
f_shape_name.choices = shapeNames ?? new();
f_shape_name.formatListItemCallback = name => shapeNames != null ? name : "<Missing SkinnedMeshRenderer>";
f_shape_name.formatSelectedValueCallback = f_shape_name.formatListItemCallback;
}
} }
} }
} }

View File

@ -1,8 +1,13 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"> <UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="changed-shape-editor"> <ui:VisualElement class="changed-shape-editor">
<ui:Label text="&lt;shape name>" binding-path="ShapeName" name="f-name"/> <ui:VisualElement class="horizontal">
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ed:PropertyField binding-path="ChangeType" label="" name="f-change-type"/> <ed:PropertyField binding-path="ChangeType" label="" name="f-change-type"/>
</ui:VisualElement>
<ui:VisualElement class="horizontal">
<ui:DropdownField name="f-shape-name" binding-path="ShapeName"/>
<ed:PropertyField binding-path="Value" label="" name="f-value" class="f-value"/> <ed:PropertyField binding-path="Value" label="" name="f-value" class="f-value"/>
<ui:VisualElement name="f-value-delete" class="f-value"/> <ui:VisualElement name="f-value-delete" class="f-value"/>
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement>
</UXML> </UXML>

View File

@ -2,7 +2,6 @@
xmlns:ma="nadena.dev.modular_avatar.core.editor"> xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement name="root-box"> <ui:VisualElement name="root-box">
<ui:VisualElement name="group-box"> <ui:VisualElement name="group-box">
<ed:PropertyField binding-path="m_targetRenderer" label="reactive_object.shape-changer.target-renderer" class="ndmf-tr"/>
<ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/> <ed:PropertyField binding-path="m_inverted" label="reactive_object.inverse" class="ndmf-tr"/>
<ui:VisualElement name="ListViewContainer"> <ui:VisualElement name="ListViewContainer">

View File

@ -1,6 +1,7 @@
#region #region
using System; using System;
using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEngine; using UnityEngine;
@ -18,6 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss; [SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml; [SerializeField] private VisualTreeAsset uxml;
private BlendshapeSelectWindow _window;
protected override void OnInnerInspectorGUI() protected override void OnInnerInspectorGUI()
{ {
@ -41,12 +43,50 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance); var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
var addButton = (Button)field_addButton.GetValue(listView); var addButton = (Button)field_addButton.GetValue(listView);
addButton.clickable = new Clickable(() => addButton.clickable = new Clickable(OpenAddWindow);
{
PopupWindow.Show(addButton.worldBound, new AddShapePopup(target as ModularAvatarShapeChanger));
});
return root; return root;
} }
private void OnDisable()
{
if (_window != null) DestroyImmediate(_window);
}
protected override void OnDestroy()
{
base.OnDestroy();
if (_window != null) DestroyImmediate(_window);
}
private void OpenAddWindow()
{
if (_window != null) DestroyImmediate(_window);
_window = CreateInstance<BlendshapeSelectWindow>();
_window.AvatarRoot = RuntimeUtil.FindAvatarTransformInParents((target as ModularAvatarShapeChanger).transform).gameObject;
_window.OfferBinding += OfferBinding;
_window.Show();
}
private void OfferBinding(BlendshapeBinding binding)
{
var changer = target as ModularAvatarShapeChanger;
if (changer.Shapes.Any(x => x.Object.Equals(binding.ReferenceMesh) && x.ShapeName == binding.Blendshape))
{
return;
}
Undo.RecordObject(changer, "Add Shape");
changer.Shapes.Add(new ChangedShape()
{
Object = binding.ReferenceMesh,
ShapeName = binding.Blendshape,
ChangeType = ShapeChangeType.Delete,
Value = 100
});
PrefabUtility.RecordPrefabInstancePropertyModifications(changer);
}
} }
} }

View File

@ -39,11 +39,15 @@
flex-direction: row; flex-direction: row;
} }
.changed-shape-editor { .changed-shape-editor .horizontal {
flex-direction: row; flex-direction: row;
} }
.changed-shape-editor #f-name { .changed-shape-editor #f-object {
flex-grow: 1;
}
.changed-shape-editor #f-shape-name {
flex-grow: 1; flex-grow: 1;
} }
@ -64,7 +68,7 @@
} }
.f-value { .f-value {
width: 40px; width: 75px;
} }
#f-value-delete { #f-value-delete {
@ -77,6 +81,7 @@
.change-type-delete #f-value-delete { .change-type-delete #f-value-delete {
display: flex; display: flex;
height: 20px;
} }
/* Add shape window */ /* Add shape window */

View File

@ -251,6 +251,5 @@
"ma_info.param_usage_ui.no_data": "[ NO DATA ]", "ma_info.param_usage_ui.no_data": "[ NO DATA ]",
"reactive_object.inverse": "Inverse Condition", "reactive_object.inverse": "Inverse Condition",
"reactive_object.material-setter.set-to": "Set material to: ", "reactive_object.material-setter.set-to": "Set material to: ",
"reactive_object.shape-changer.target-renderer": "Target Renderer",
"menuitem.misc.add_toggle": "Add toggle" "menuitem.misc.add_toggle": "Add toggle"
} }

View File

@ -247,6 +247,5 @@
"ma_info.param_usage_ui.no_data": "[ NO DATA ]", "ma_info.param_usage_ui.no_data": "[ NO DATA ]",
"reactive_object.inverse": "条件を反転", "reactive_object.inverse": "条件を反転",
"reactive_object.material-setter.set-to": "変更先のマテリアル ", "reactive_object.material-setter.set-to": "変更先のマテリアル ",
"reactive_object.shape-changer.target-renderer": "操作するレンダラー",
"menuitem.misc.add_toggle": "トグルを追加" "menuitem.misc.add_toggle": "トグルを追加"
} }

View File

@ -250,6 +250,5 @@
"ma_info.param_usage_ui.no_data": "【無資訊】", "ma_info.param_usage_ui.no_data": "【無資訊】",
"reactive_object.inverse": "反轉條件", "reactive_object.inverse": "反轉條件",
"reactive_object.material-setter.set-to": "將材質設定為:", "reactive_object.material-setter.set-to": "將材質設定為:",
"reactive_object.shape-changer.target-renderer": "目標 Renderer",
"menuitem.misc.add_toggle": "新增開關" "menuitem.misc.add_toggle": "新增開關"
} }

View File

@ -15,15 +15,16 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var changer in changers) foreach (var changer in changers)
{ {
var renderer = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>(); if (changer.Shapes == null) continue;
if (renderer == null) continue;
var mesh = renderer.sharedMesh;
if (mesh == null) continue;
foreach (var shape in changer.Shapes) foreach (var shape in changer.Shapes)
{ {
var renderer = shape.Object.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
if (renderer == null) continue;
var mesh = renderer.sharedMesh;
if (mesh == null) continue;
var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName); var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName);
if (shapeId < 0) continue; if (shapeId < 0) continue;
@ -92,9 +93,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var obj in setter.Objects) foreach (var obj in setter.Objects)
{ {
var target = obj.Object.Get(setter); var renderer = obj.Object.Get(setter)?.GetComponent<Renderer>();
if (target == null) continue;
var renderer = target.GetComponent<Renderer>();
if (renderer == null || renderer.sharedMaterials.Length < obj.MaterialIndex) continue; if (renderer == null || renderer.sharedMaterials.Length < obj.MaterialIndex) continue;
var key = new TargetProp var key = new TargetProp

View File

@ -31,40 +31,41 @@ namespace nadena.dev.modular_avatar.core.editor
var menuItemPreview = new MenuItemPreviewCondition(context); var menuItemPreview = new MenuItemPreviewCondition(context);
var setters = context.GetComponentsByType<ModularAvatarMaterialSetter>(); var setters = context.GetComponentsByType<ModularAvatarMaterialSetter>();
var groups = new Dictionary<Renderer, ImmutableList<ModularAvatarMaterialSetter>>(); var builders =
new Dictionary<Renderer, ImmutableList<ModularAvatarMaterialSetter>.Builder>(
new ObjectIdentityComparer<Renderer>());
foreach (var setter in setters) foreach (var setter in setters)
{ {
if (setter == null) continue;
var mami = context.GetComponent<ModularAvatarMenuItem>(setter.gameObject); var mami = context.GetComponent<ModularAvatarMenuItem>(setter.gameObject);
bool active = context.ActiveAndEnabled(setter) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); bool active = context.ActiveAndEnabled(setter) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
if (active == context.Observe(setter, t => t.Inverted)) continue; if (active == context.Observe(setter, s => s.Inverted)) continue;
var objs = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), (x, y) => x.SequenceEqual(y)); var objects = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), Enumerable.SequenceEqual);
if (setter.Objects == null) continue; foreach (var (target, mat, index) in objects)
foreach (var (obj, mat, index) in objs)
{ {
if (obj == null) continue; var renderer = context.GetComponent<Renderer>(target);
var renderer = context.GetComponent<Renderer>(obj);
if (renderer == null) continue; if (renderer == null) continue;
var matCount = context.Observe(renderer, r => r.sharedMaterials.Length); var matCount = context.Observe(renderer, r => r.sharedMaterials.Length);
if (matCount <= index) continue; if (matCount <= index) continue;
if (!groups.TryGetValue(renderer, out var list)) if (!builders.TryGetValue(renderer, out var builder))
{ {
list = ImmutableList.Create<ModularAvatarMaterialSetter>(); builder = ImmutableList.CreateBuilder<ModularAvatarMaterialSetter>();
groups.Add(renderer, list); builders[renderer] = builder;
} }
groups[renderer] = list.Add(setter); builder.Add(setter);
} }
} }
var finalGroups = groups.Select(g => RenderGroup.For(g.Key).WithData(g.Value)).ToImmutableList(); return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable()))
return finalGroups; .ToImmutableList();
} }
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
@ -80,10 +81,57 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly ImmutableList<ModularAvatarMaterialSetter> _setters; private readonly ImmutableList<ModularAvatarMaterialSetter> _setters;
private ImmutableList<(int, Material)> _materials; private ImmutableList<(int, Material)> _materials;
public RenderAspects WhatChanged {get; private set; } public RenderAspects WhatChanged => RenderAspects.Material;
public Node(ImmutableList<ModularAvatarMaterialSetter> setters)
{
_setters = setters;
_materials = ImmutableList<(int, Material)>.Empty;
}
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects)
{
var (original, proxy) = proxyPairs.First();
if (original == null || proxy == null) return null;
var mats = new Material[proxy.sharedMaterials.Length];
foreach (var setter in _setters)
{
if (setter == null) continue;
var objects = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), Enumerable.SequenceEqual);
foreach (var (target, mat, index) in objects)
{
var renderer = context.GetComponent<Renderer>(target);
if (renderer != original) continue;
if (index <= mats.Length)
{
mats[index] = mat;
}
}
}
var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList();
if (!materials.SequenceEqual(_materials))
{
return Task.FromResult<IRenderFilterNode>(new Node(_setters)
{
_materials = materials,
});
}
return Task.FromResult<IRenderFilterNode>(this);
}
public void OnFrame(Renderer original, Renderer proxy) public void OnFrame(Renderer original, Renderer proxy)
{ {
if (original == null || proxy == null) return;
var mats = proxy.sharedMaterials; var mats = proxy.sharedMaterials;
foreach (var mat in _materials) foreach (var mat in _materials)
@ -96,50 +144,6 @@ namespace nadena.dev.modular_avatar.core.editor
proxy.sharedMaterials = mats; proxy.sharedMaterials = mats;
} }
public Node(ImmutableList<ModularAvatarMaterialSetter> setters)
{
_setters = setters;
_materials = ImmutableList<(int, Material)>.Empty;
WhatChanged = RenderAspects.Material;
}
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects)
{
var proxyPair = proxyPairs.First();
var original = proxyPair.Item1;
var proxy = proxyPair.Item2;
var mats = new Material[proxy.sharedMaterials.Length];
foreach (var setter in _setters)
{
var objects = context.Observe(setter, s => s.Objects
.Where(obj => obj.Object.Get(s) == original.gameObject)
.Select(obj => (obj.Material, obj.MaterialIndex)),
(x, y) => x.SequenceEqual(y)
);
foreach (var (mat, index) in objects)
{
if (index <= mats.Length)
{
mats[index] = mat;
}
}
}
var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList();
if (materials.SequenceEqual(_materials))
{
return Task.FromResult<IRenderFilterNode>(this);
}
else
{
return Task.FromResult<IRenderFilterNode>(new Node(_setters) { _materials = materials });
}
}
} }
} }
} }

View File

@ -31,79 +31,107 @@ namespace nadena.dev.modular_avatar.core.editor
return context.Observe(EnableNode.IsEnabled); return context.Observe(EnableNode.IsEnabled);
} }
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx) public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
{ {
var menuItemPreview = new MenuItemPreviewCondition(ctx); var menuItemPreview = new MenuItemPreviewCondition(context);
var changers = context.GetComponentsByType<ModularAvatarShapeChanger>();
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>(); var builders =
var groups =
new Dictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>.Builder>( new Dictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>.Builder>(
new ObjectIdentityComparer<Renderer>()); new ObjectIdentityComparer<Renderer>());
foreach (var changer in allChangers) foreach (var changer in changers)
{ {
if (changer == null) continue; if (changer == null) continue;
var mami = ctx.GetComponent<ModularAvatarMenuItem>(changer.gameObject); var mami = context.GetComponent<ModularAvatarMenuItem>(changer.gameObject);
bool active = ctx.ActiveAndEnabled(changer) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); bool active = context.ActiveAndEnabled(changer) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
if (active == ctx.Observe(changer, t => t.Inverted)) continue; if (active == context.Observe(changer, c => c.Inverted)) continue;
var target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer)); var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual);
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
foreach (var (target, name, type, value) in shapes)
{
var renderer = context.GetComponent<SkinnedMeshRenderer>(target);
if (renderer == null) continue; if (renderer == null) continue;
if (!groups.TryGetValue(renderer, out var group)) if (!builders.TryGetValue(renderer, out var builder))
{ {
group = ImmutableList.CreateBuilder<ModularAvatarShapeChanger>(); builder = ImmutableList.CreateBuilder<ModularAvatarShapeChanger>();
groups[renderer] = group; builders[renderer] = builder;
} }
group.Add(changer); builder.Add(changer);
}
} }
return groups.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable()))
.ToImmutableList(); .ToImmutableList();
} }
public async Task<IRenderFilterNode> Instantiate( public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
RenderGroup group,
IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context)
{ {
var node = new Node(group); var changers = group.GetData<ImmutableList<ModularAvatarShapeChanger>>();
var node = new Node(changers);
try return node.Refresh(proxyPairs, context, 0);
{
await node.Init(proxyPairs, context);
}
catch (Exception e)
{
// dispose
throw;
}
return node;
} }
private class Node : IRenderFilterNode private class Node : IRenderFilterNode
{ {
private readonly RenderGroup _group;
private readonly ImmutableList<ModularAvatarShapeChanger> _changers; private readonly ImmutableList<ModularAvatarShapeChanger> _changers;
private ImmutableHashSet<(int, float)> _shapes;
private ImmutableHashSet<int> _toDelete;
private Mesh _generatedMesh = null; private Mesh _generatedMesh = null;
private HashSet<int> _toDelete;
internal Node(RenderGroup group) public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh;
internal Node(ImmutableList<ModularAvatarShapeChanger> changers)
{ {
_group = group; _changers = changers;
_changers = _group.GetData<ImmutableList<ModularAvatarShapeChanger>>(); _shapes = ImmutableHashSet<(int, float)>.Empty;
_toDelete = ImmutableHashSet<int>.Empty;
_generatedMesh = null;
} }
private HashSet<int> GetToDeleteSet(SkinnedMeshRenderer proxy, ComputeContext context) public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects)
{ {
var toDelete = new HashSet<int>(); var (original, proxy) = proxyPairs.First();
if (original == null || proxy == null) return null;
if (original is not SkinnedMeshRenderer originalSmr || proxy is not SkinnedMeshRenderer proxySmr) return null;
var shapes = GetShapesSet(originalSmr, proxySmr, context);
var toDelete = GetToDeleteSet(originalSmr, proxySmr, context);
if (!toDelete.SequenceEqual(_toDelete))
{
return Task.FromResult<IRenderFilterNode>(new Node(_changers)
{
_shapes = shapes,
_toDelete = toDelete,
_generatedMesh = GetGeneratedMesh(proxySmr, toDelete),
});
}
if (!shapes.SequenceEqual(_shapes))
{
var reusableMesh = _generatedMesh;
_generatedMesh = null;
return Task.FromResult<IRenderFilterNode>(new Node(_changers)
{
_shapes = shapes,
_toDelete = toDelete,
_generatedMesh = reusableMesh,
});
}
return Task.FromResult<IRenderFilterNode>(this);
}
private ImmutableHashSet<(int, float)> GetShapesSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context)
{
var builder = ImmutableHashSet.CreateBuilder<(int, float)>();
var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) => var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) =>
{ {
if (a != b) if (a != b)
@ -117,67 +145,60 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var changer in _changers) foreach (var changer in _changers)
{ {
var shapes = context.Observe(changer, if (changer == null) continue;
c => c.Shapes
.Where(s => s.ChangeType == ShapeChangeType.Delete)
.Select(s => s.ShapeName)
.ToImmutableList(),
Enumerable.SequenceEqual
);
foreach (var shape in shapes) var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual);
foreach (var (target, name, type, value) in shapes)
{ {
var index = mesh.GetBlendShapeIndex(shape); var renderer = context.GetComponent<SkinnedMeshRenderer>(target);
if (renderer != original) continue;
var index = mesh.GetBlendShapeIndex(name);
if (index < 0) continue; if (index < 0) continue;
toDelete.Add(index); builder.Add((index, type == ShapeChangeType.Delete ? 100 : value));
} }
} }
return toDelete; return builder.ToImmutable();
} }
public async Task Init( private ImmutableHashSet<int> GetToDeleteSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context)
IEnumerable<(Renderer, Renderer)> renderers,
ComputeContext context
)
{ {
var (original, proxy) = renderers.First(); var builder = ImmutableHashSet.CreateBuilder<int>();
var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) =>
if (original == null || proxy == null) return; {
if (!(proxy is SkinnedMeshRenderer smr)) return; if (a != b)
{
await Init(smr, context, GetToDeleteSet(smr, context)); Debug.Log($"mesh changed {a.GetInstanceID()} -> {b.GetInstanceID()}");
return false;
} }
public async Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, return true;
ComputeContext context, RenderAspects updatedAspects) });
foreach (var changer in _changers)
{ {
if ((updatedAspects & RenderAspects.Mesh) != 0) return null; var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual);
var (original, proxy) = proxyPairs.First(); foreach (var (target, name, type, value) in shapes)
if (original == null || proxy == null) return null;
if (!(proxy is SkinnedMeshRenderer smr)) return null;
var toDelete = GetToDeleteSet(smr, context);
if (toDelete.Count == _toDelete.Count && toDelete.All(_toDelete.Contains))
{ {
//System.Diagnostics.Debug.WriteLine("[ShapeChangerPreview] No changes detected. Retaining node."); if (type != ShapeChangeType.Delete) continue;
return this;
var renderer = context.GetComponent<SkinnedMeshRenderer>(target);
if (renderer != original) continue;
var index = mesh.GetBlendShapeIndex(name);
if (index < 0) continue;
builder.Add(index);
}
} }
var node = new Node(_group); return builder.ToImmutable();
await node.Init(smr, context, toDelete);
return node;
} }
public async Task Init( public Mesh GetGeneratedMesh(SkinnedMeshRenderer proxy, ImmutableHashSet<int> toDelete)
SkinnedMeshRenderer proxy,
ComputeContext context,
HashSet<int> toDelete
)
{ {
_toDelete = toDelete;
var mesh = proxy.sharedMesh; var mesh = proxy.sharedMesh;
if (toDelete.Count > 0) if (toDelete.Count > 0)
@ -224,60 +245,32 @@ namespace nadena.dev.modular_avatar.core.editor
mesh.SetTriangles(tris, subMesh, false, baseVertex: baseVertex); mesh.SetTriangles(tris, subMesh, false, baseVertex: baseVertex);
} }
_generatedMesh = mesh; return mesh;
}
} }
return null;
}
public RenderAspects Reads => RenderAspects.Shapes | RenderAspects.Mesh; public void OnFrame(Renderer original, Renderer proxy)
public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh; {
if (original == null || proxy == null) return;
if (original is not SkinnedMeshRenderer originalSmr || proxy is not SkinnedMeshRenderer proxySmr) return;
if (_generatedMesh != null)
{
proxySmr.sharedMesh = _generatedMesh;
}
foreach (var shape in _shapes)
{
proxySmr.SetBlendShapeWeight(shape.Item1, shape.Item2);
}
}
public void Dispose() public void Dispose()
{ {
if (_generatedMesh != null) Object.DestroyImmediate(_generatedMesh); if (_generatedMesh != null) Object.DestroyImmediate(_generatedMesh);
} }
public void OnFrame(Renderer original, Renderer proxy)
{
if (_changers == null) return; // can happen transiently as we disable the last component
if (!(proxy is SkinnedMeshRenderer smr)) return;
Mesh mesh;
if (_generatedMesh != null)
{
smr.sharedMesh = _generatedMesh;
mesh = _generatedMesh;
}
else
{
mesh = smr.sharedMesh;
}
if (mesh == null) return;
foreach (var changer in _changers)
{
foreach (var shape in changer.Shapes)
{
var index = mesh.GetBlendShapeIndex(shape.ShapeName);
if (index < 0) continue;
float setToValue = -1;
switch (shape.ChangeType)
{
case ShapeChangeType.Delete:
setToValue = 100;
break;
case ShapeChangeType.Set:
setToValue = shape.Value;
break;
}
smr.SetBlendShapeWeight(index, setToValue);
}
}
}
} }
} }
} }

View File

@ -5,7 +5,7 @@ using UnityEngine;
namespace nadena.dev.modular_avatar.core namespace nadena.dev.modular_avatar.core
{ {
[Serializable] [Serializable]
public struct MaterialSwitchObject public class MaterialSwitchObject
{ {
public AvatarObjectReference Object; public AvatarObjectReference Object;
public Material Material; public Material Material;

View File

@ -17,15 +17,16 @@ namespace nadena.dev.modular_avatar.core
} }
[Serializable] [Serializable]
public struct ChangedShape public class ChangedShape
{ {
public AvatarObjectReference Object;
public string ShapeName; public string ShapeName;
public ShapeChangeType ChangeType; public ShapeChangeType ChangeType;
public float Value; public float Value;
public bool Equals(ChangedShape other) public bool Equals(ChangedShape other)
{ {
return ShapeName == other.ShapeName && ChangeType == other.ChangeType && Value.Equals(other.Value); return Equals(Object, other.Object) && ShapeName == other.ShapeName && ChangeType == other.ChangeType && Value.Equals(other.Value);
} }
public override bool Equals(object obj) public override bool Equals(object obj)
@ -35,12 +36,12 @@ namespace nadena.dev.modular_avatar.core
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine(ShapeName, (int)ChangeType, Value); return HashCode.Combine(Object, ShapeName, (int)ChangeType, Value);
} }
public override string ToString() public override string ToString()
{ {
return $"{ShapeName} {ChangeType} {Value}"; return $"{Object.referencePath} {ShapeName} {ChangeType} {Value}";
} }
} }
@ -48,14 +49,10 @@ namespace nadena.dev.modular_avatar.core
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")] [HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
public class ModularAvatarShapeChanger : ReactiveComponent public class ModularAvatarShapeChanger : ReactiveComponent
{ {
[SerializeField] [FormerlySerializedAs("targetRenderer")] // Migration field to help with 1.10-beta series avatar data. Since this was never in a released version of MA,
private AvatarObjectReference m_targetRenderer; // this migration support will be removed in 1.10.0.
[SerializeField] [FormerlySerializedAs("targetRenderer")] [HideInInspector]
public AvatarObjectReference targetRenderer private AvatarObjectReference m_targetRenderer = new();
{
get => m_targetRenderer;
set => m_targetRenderer = value;
}
[SerializeField] [FormerlySerializedAs("Shapes")] [SerializeField] [FormerlySerializedAs("Shapes")]
private List<ChangedShape> m_shapes = new(); private List<ChangedShape> m_shapes = new();
@ -68,7 +65,44 @@ namespace nadena.dev.modular_avatar.core
public override void ResolveReferences() public override void ResolveReferences()
{ {
m_targetRenderer?.Get(this); foreach (var shape in m_shapes)
{
shape.Object?.Get(this);
}
}
private void OnEnable()
{
MigrateTargetRenderer();
}
protected override void OnValidate()
{
base.OnValidate();
MigrateTargetRenderer();
}
// Migrate early versions of MASC (from Modular Avatar 1.10.0-beta.4 or earlier) to the new format, where the
// target renderer is stored separately for each shape.
// This logic will be removed in 1.10.0.
private void MigrateTargetRenderer()
{
// Note: This method runs in the context of OnValidate, and therefore cannot touch any other unity objects.
if (!string.IsNullOrEmpty(m_targetRenderer.referencePath) || m_targetRenderer.targetObject != null)
{
foreach (var shape in m_shapes)
{
if (shape.Object == null) shape.Object = new AvatarObjectReference();
if (string.IsNullOrEmpty(shape.Object.referencePath) && shape.Object.targetObject == null)
{
shape.Object.referencePath = m_targetRenderer.referencePath;
shape.Object.targetObject = m_targetRenderer.targetObject;
}
}
m_targetRenderer.referencePath = null;
m_targetRenderer.targetObject = null;
}
} }
} }
} }