mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 02:35:06 +08:00
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:
parent
3b9f0d1838
commit
8418f8e047
@ -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)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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!"));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a8351fafb3740918363f60365adfeda
|
||||
timeCreated: 1717205112
|
@ -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="<none remaining>" class="placeholder"/>
|
||||
</ui:ScrollView>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6753a7b3eae1416cb04786cf53778c33
|
||||
timeCreated: 1717205258
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||
<ui:VisualElement class="changed-shape-editor">
|
||||
<ui:Label text="<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>
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
@ -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"
|
||||
}
|
@ -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": "トグルを追加"
|
||||
}
|
@ -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": "新增開關"
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user