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_object = uxml.Q<ObjectField>("f-object");
f_object.objectType = typeof(Renderer);
f_object.allowSceneObjects = true;
var f_object = uxml.Q<PropertyField>("f-object");
var f_target_object = uxml.Q<ObjectField>("f-obj-target-object");
var f_reference_path = uxml.Q<TextField>("f-obj-ref-path");
f_object.RegisterValueChangedCallback(evt =>
f_object.RegisterValueChangeCallback(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;
});
UpdateMaterialDropdown();
f_target_object.RegisterValueChangedCallback(_ => UpdateVisualTarget());
f_reference_path.RegisterValueChangedCallback(_ => UpdateVisualTarget());
// Link dropdown to material index field
var f_material_index_int = uxml.Q<IntegerField>("f-material-index-int");
f_material_index_int.RegisterValueChangedCallback(evt =>
@ -83,30 +53,13 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
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()
{
var toggledObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
Material[] sharedMaterials;
try
{
sharedMaterials = toggledObject?.GetComponent<Renderer>()?.sharedMaterials;
sharedMaterials = targetObject?.GetComponent<Renderer>()?.sharedMaterials;
}
catch (MissingComponentException e)
{

View File

@ -1,13 +1,9 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="toggled-object-editor">
<ui:VisualElement class="horizontal">
<!--<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>-->
<ed:ObjectField label="" name="f-object" class="f-object"/>
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
<ui:DropdownField name="f-material-index" binding-path="MaterialIndex"/>
<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"/>
</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
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
@ -24,6 +27,16 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
uxml.styleSheets.Add(uss);
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>>(
e =>
{
@ -39,6 +52,29 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
);
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">
<ui:VisualElement class="changed-shape-editor">
<ui:Label text="&lt;shape name>" binding-path="ShapeName" name="f-name"/>
<ed:PropertyField binding-path="ChangeType" label="" name="f-change-type"/>
<ed:PropertyField binding-path="Value" label="" name="f-value" class="f-value"/>
<ui:VisualElement name="f-value-delete" class="f-value"/>
<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"/>
</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"/>
<ui:VisualElement name="f-value-delete" class="f-value"/>
</ui:VisualElement>
</ui:VisualElement>
</UXML>

View File

@ -2,7 +2,6 @@
xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement name="root-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"/>
<ui:VisualElement name="ListViewContainer">

View File

@ -1,6 +1,7 @@
#region
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
@ -18,6 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private BlendshapeSelectWindow _window;
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 addButton = (Button)field_addButton.GetValue(listView);
addButton.clickable = new Clickable(() =>
{
PopupWindow.Show(addButton.worldBound, new AddShapePopup(target as ModularAvatarShapeChanger));
});
addButton.clickable = new Clickable(OpenAddWindow);
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;
}
.changed-shape-editor {
.changed-shape-editor .horizontal {
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;
}
@ -64,7 +68,7 @@
}
.f-value {
width: 40px;
width: 75px;
}
#f-value-delete {
@ -77,6 +81,7 @@
.change-type-delete #f-value-delete {
display: flex;
height: 20px;
}
/* Add shape window */

View File

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

View File

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

View File

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

View File

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

View File

@ -31,40 +31,41 @@ namespace nadena.dev.modular_avatar.core.editor
var menuItemPreview = new MenuItemPreviewCondition(context);
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)
{
if (setter == null) continue;
var mami = context.GetComponent<ModularAvatarMenuItem>(setter.gameObject);
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 (obj, mat, index) in objs)
foreach (var (target, mat, index) in objects)
{
if (obj == null) continue;
var renderer = context.GetComponent<Renderer>(obj);
var renderer = context.GetComponent<Renderer>(target);
if (renderer == null) continue;
var matCount = context.Observe(renderer, r => r.sharedMaterials.Length);
if (matCount <= index) continue;
if (!groups.TryGetValue(renderer, out var list))
if (!builders.TryGetValue(renderer, out var builder))
{
list = ImmutableList.Create<ModularAvatarMaterialSetter>();
groups.Add(renderer, list);
builder = ImmutableList.CreateBuilder<ModularAvatarMaterialSetter>();
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 finalGroups;
return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable()))
.ToImmutableList();
}
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
@ -80,48 +81,33 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly ImmutableList<ModularAvatarMaterialSetter> _setters;
private ImmutableList<(int, Material)> _materials;
public RenderAspects WhatChanged {get; private set; }
public void OnFrame(Renderer original, Renderer proxy)
{
var mats = proxy.sharedMaterials;
foreach (var mat in _materials)
{
if (mat.Item1 <= mats.Length)
{
mats[mat.Item1] = mat.Item2;
}
}
proxy.sharedMaterials = mats;
}
public RenderAspects WhatChanged => RenderAspects.Material;
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 (original, proxy) = proxyPairs.First();
if (original == null || proxy == null) return null;
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)
);
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 (mat, index) in objects)
foreach (var (target, mat, index) in objects)
{
var renderer = context.GetComponent<Renderer>(target);
if (renderer != original) continue;
if (index <= mats.Length)
{
mats[index] = mat;
@ -131,14 +117,32 @@ namespace nadena.dev.modular_avatar.core.editor
var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList();
if (materials.SequenceEqual(_materials))
if (!materials.SequenceEqual(_materials))
{
return Task.FromResult<IRenderFilterNode>(this);
return Task.FromResult<IRenderFilterNode>(new Node(_setters)
{
_materials = materials,
});
}
else
return Task.FromResult<IRenderFilterNode>(this);
}
public void OnFrame(Renderer original, Renderer proxy)
{
if (original == null || proxy == null) return;
var mats = proxy.sharedMaterials;
foreach (var mat in _materials)
{
return Task.FromResult<IRenderFilterNode>(new Node(_setters) { _materials = materials });
if (mat.Item1 <= mats.Length)
{
mats[mat.Item1] = mat.Item2;
}
}
proxy.sharedMaterials = mats;
}
}
}

View File

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

View File

@ -17,15 +17,16 @@ namespace nadena.dev.modular_avatar.core
}
[Serializable]
public struct ChangedShape
public class ChangedShape
{
public AvatarObjectReference Object;
public string ShapeName;
public ShapeChangeType ChangeType;
public float Value;
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)
@ -35,12 +36,12 @@ namespace nadena.dev.modular_avatar.core
public override int GetHashCode()
{
return HashCode.Combine(ShapeName, (int)ChangeType, Value);
return HashCode.Combine(Object, ShapeName, (int)ChangeType, Value);
}
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")]
public class ModularAvatarShapeChanger : ReactiveComponent
{
[SerializeField] [FormerlySerializedAs("targetRenderer")]
private AvatarObjectReference m_targetRenderer;
public AvatarObjectReference targetRenderer
{
get => m_targetRenderer;
set => m_targetRenderer = value;
}
// Migration field to help with 1.10-beta series avatar data. Since this was never in a released version of MA,
// this migration support will be removed in 1.10.0.
[SerializeField] [FormerlySerializedAs("targetRenderer")] [HideInInspector]
private AvatarObjectReference m_targetRenderer = new();
[SerializeField] [FormerlySerializedAs("Shapes")]
private List<ChangedShape> m_shapes = new();
@ -68,7 +65,44 @@ namespace nadena.dev.modular_avatar.core
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;
}
}
}
}