mirror of
https://github.com/vrm-c/UniVRM.git
synced 2026-05-14 14:29:52 -05:00
commit
40e2f3c01b
2
MToon
2
MToon
|
|
@ -1 +1 @@
|
|||
Subproject commit 15dd8816dc1775871e5ca02802a561e6448e5792
|
||||
Subproject commit bf08610b38e27ae915b0613ee4dfb574bc8e381b
|
||||
|
|
@ -3,6 +3,10 @@ using System.Linq;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UniGLTF;
|
||||
using System.IO;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
|
|
@ -12,6 +16,24 @@ namespace VRM
|
|||
[SerializeField]
|
||||
public List<BlendShapeClip> Clips = new List<BlendShapeClip>();
|
||||
|
||||
/// <summary>
|
||||
/// NullのClipを削除して詰める
|
||||
/// </summary>
|
||||
public void RemoveNullClip()
|
||||
{
|
||||
if (Clips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int i = Clips.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (Clips[i] == null)
|
||||
{
|
||||
Clips.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Restore")]
|
||||
void Restore()
|
||||
|
|
@ -37,6 +59,19 @@ namespace VRM
|
|||
}
|
||||
Clips = Clips.OrderBy(x => BlendShapeKey.CreateFrom(x)).ToList();
|
||||
}
|
||||
|
||||
static public BlendShapeClip CreateBlendShapeClip(string path)
|
||||
{
|
||||
//Debug.LogFormat("{0}", path);
|
||||
var clip = ScriptableObject.CreateInstance<BlendShapeClip>();
|
||||
clip.BlendShapeName = Path.GetFileNameWithoutExtension(path);
|
||||
AssetDatabase.CreateAsset(clip, path);
|
||||
AssetDatabase.ImportAsset(path);
|
||||
return clip;
|
||||
//Clips.Add(clip);
|
||||
//EditorUtility.SetDirty(this);
|
||||
//AssetDatabase.SaveAssets();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
105
Scripts/BlendShape/BlendShapeBindingMerger.cs
Normal file
105
Scripts/BlendShape/BlendShapeBindingMerger.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
///
|
||||
/// A.Value * A.Weight + B.Value * B.Weight ...
|
||||
///
|
||||
class BlendShapeBindingMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// BlendShapeの適用値を蓄積する
|
||||
/// </summary>
|
||||
/// <typeparam name="BlendShapeBinding"></typeparam>
|
||||
/// <typeparam name="float"></typeparam>
|
||||
/// <returns></returns>
|
||||
Dictionary<BlendShapeBinding, float> m_blendShapeValueMap = new Dictionary<BlendShapeBinding, float>();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Dictionary<BlendShapeBinding, Action<float>> m_blendShapeSetterMap = new Dictionary<BlendShapeBinding, Action<float>>();
|
||||
|
||||
public BlendShapeBindingMerger(Dictionary<BlendShapeKey, BlendShapeClip> clipMap, Transform root)
|
||||
{
|
||||
foreach (var kv in clipMap)
|
||||
{
|
||||
foreach (var binding in kv.Value.Values)
|
||||
{
|
||||
if (!m_blendShapeSetterMap.ContainsKey(binding))
|
||||
{
|
||||
var _target = root.Find(binding.RelativePath);
|
||||
SkinnedMeshRenderer target = null;
|
||||
if (_target != null)
|
||||
{
|
||||
target = _target.GetComponent<SkinnedMeshRenderer>();
|
||||
}
|
||||
if (target != null)
|
||||
{
|
||||
if (binding.Index >= 0 && binding.Index < target.sharedMesh.blendShapeCount)
|
||||
{
|
||||
m_blendShapeSetterMap.Add(binding, x =>
|
||||
{
|
||||
target.SetBlendShapeWeight(binding.Index, x);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("Invalid blendshape binding: {0}: {1}", target.name, binding);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("SkinnedMeshRenderer: {0} not found", binding.RelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ImmediatelySetValue(BlendShapeClip clip, float value)
|
||||
{
|
||||
foreach (var binding in clip.Values)
|
||||
{
|
||||
Action<float> setter;
|
||||
if (m_blendShapeSetterMap.TryGetValue(binding, out setter))
|
||||
{
|
||||
setter(binding.Weight * value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AccumulateValue(BlendShapeClip clip, float value)
|
||||
{
|
||||
foreach (var binding in clip.Values)
|
||||
{
|
||||
float acc;
|
||||
if (m_blendShapeValueMap.TryGetValue(binding, out acc))
|
||||
{
|
||||
m_blendShapeValueMap[binding] = acc + binding.Weight * value;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_blendShapeValueMap[binding] = binding.Weight * value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
foreach (var kv in m_blendShapeValueMap)
|
||||
{
|
||||
Action<float> setter;
|
||||
if (m_blendShapeSetterMap.TryGetValue(kv.Key, out setter))
|
||||
{
|
||||
setter(kv.Value);
|
||||
}
|
||||
}
|
||||
m_blendShapeValueMap.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Scripts/BlendShape/BlendShapeBindingMerger.cs.meta
Normal file
12
Scripts/BlendShape/BlendShapeBindingMerger.cs.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ca4c8446451eeed46b1598db9e08bb73
|
||||
timeCreated: 1541229189
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -73,7 +73,7 @@ namespace VRM
|
|||
public BlendShapePreset Preset;
|
||||
|
||||
/// <summary>
|
||||
/// BlendShapeに対する参照(indexベース)
|
||||
/// BlendShapeに対する参照(index ベース)
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
[SerializeField]
|
||||
|
|
@ -91,5 +91,8 @@ namespace VRM
|
|||
/// </summary>
|
||||
[SerializeField]
|
||||
public bool IsBinary;
|
||||
|
||||
// [SerializeField]
|
||||
// public Texture2D Thumbnail;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,133 +7,38 @@ using UnityEngine;
|
|||
|
||||
namespace VRM
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// ブレンドシェイプを蓄えてまとめて適用するクラス
|
||||
/// </summary>
|
||||
class BlendShapeMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// Key からBlendShapeClipを得る
|
||||
/// </summary>
|
||||
Dictionary<BlendShapeKey, BlendShapeClip> m_clipMap;
|
||||
|
||||
/// <summary>
|
||||
/// BlendShape のWeightを記録する
|
||||
/// </summary>
|
||||
Dictionary<BlendShapeKey, float> m_valueMap;
|
||||
|
||||
Dictionary<string, Material> m_materialMap;
|
||||
BlendShapeBindingMerger m_blendShapeBindingMerger;
|
||||
|
||||
Dictionary<BlendShapeBinding, float> m_blendShapeValueMap = new Dictionary<BlendShapeBinding, float>();
|
||||
Dictionary<BlendShapeBinding, Action<float>> m_blendShapeSetterMap = new Dictionary<BlendShapeBinding, Action<float>>();
|
||||
MaterialValueBindingMerger m_materialValueBindingMerger;
|
||||
|
||||
Dictionary<MaterialValueBinding, float> m_materialValueMap = new Dictionary<MaterialValueBinding, float>();
|
||||
Dictionary<MaterialValueBinding, Action<float>> m_materialSetterMap = new Dictionary<MaterialValueBinding, Action<float>>();
|
||||
|
||||
public BlendShapeMerger(IEnumerable<BlendShapeClip> clips, Transform root)
|
||||
{
|
||||
m_materialMap = new Dictionary<string, Material>();
|
||||
foreach (var x in root.Traverse())
|
||||
{
|
||||
var renderer = x.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
foreach (var y in renderer.sharedMaterials.Where(y => y != null))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(y.name))
|
||||
{
|
||||
if (!m_materialMap.ContainsKey(y.name))
|
||||
{
|
||||
m_materialMap.Add(y.name, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_clipMap = clips.ToDictionary(x => BlendShapeKey.CreateFrom(x), x => x);
|
||||
|
||||
m_valueMap = new Dictionary<BlendShapeKey, float>();
|
||||
|
||||
foreach (var kv in m_clipMap)
|
||||
{
|
||||
foreach (var binding in kv.Value.Values)
|
||||
{
|
||||
if (!m_blendShapeSetterMap.ContainsKey(binding))
|
||||
{
|
||||
var _target = root.Find(binding.RelativePath);
|
||||
SkinnedMeshRenderer target = null;
|
||||
if (_target != null)
|
||||
{
|
||||
target = _target.GetComponent<SkinnedMeshRenderer>();
|
||||
}
|
||||
if (target != null)
|
||||
{
|
||||
if (binding.Index >= 0 && binding.Index < target.sharedMesh.blendShapeCount)
|
||||
{
|
||||
m_blendShapeSetterMap.Add(binding, x =>
|
||||
{
|
||||
target.SetBlendShapeWeight(binding.Index, x);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("Invalid blendshape binding: {0}: {1}", target.name, binding);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("SkinnedMeshRenderer: {0} not found", binding.RelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var binding in kv.Value.MaterialValues)
|
||||
{
|
||||
if (!m_materialSetterMap.ContainsKey(binding))
|
||||
{
|
||||
Material target;
|
||||
if (m_materialMap.TryGetValue(binding.MaterialName, out target))
|
||||
{
|
||||
if (binding.ValueName.EndsWith("_ST_S"))
|
||||
{
|
||||
var valueName = binding.ValueName.Substring(0, binding.ValueName.Length - 2);
|
||||
Action<float> setter = value =>
|
||||
{
|
||||
var propValue = binding.BaseValue + (binding.TargetValue - binding.BaseValue) * value;
|
||||
var src = target.GetVector(valueName);
|
||||
src.x = propValue.x; // horizontal only
|
||||
src.z = propValue.z; // horizontal only
|
||||
target.SetVector(valueName, src);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, setter);
|
||||
}
|
||||
else if(binding.ValueName.EndsWith("_ST_T"))
|
||||
{
|
||||
var valueName = binding.ValueName.Substring(0, binding.ValueName.Length - 2);
|
||||
Action<float> setter = value =>
|
||||
{
|
||||
var propValue = binding.BaseValue + (binding.TargetValue - binding.BaseValue) * value;
|
||||
var src = target.GetVector(valueName);
|
||||
src.y = propValue.y; // vertical only
|
||||
src.w = propValue.w; // vertical only
|
||||
target.SetVector(valueName, src);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, setter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Action<float> vec4Setter = x =>
|
||||
{
|
||||
var propValue = binding.BaseValue + (binding.TargetValue - binding.BaseValue) * x;
|
||||
target.SetColor(binding.ValueName, propValue);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, vec4Setter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("material: {0} not found", binding.MaterialName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m_blendShapeBindingMerger = new BlendShapeBindingMerger(m_clipMap, root);
|
||||
m_materialValueBindingMerger = new MaterialValueBindingMerger(m_clipMap, root);
|
||||
}
|
||||
|
||||
/*
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var kv in m_valueMap.ToArray())
|
||||
|
|
@ -142,40 +47,36 @@ namespace VRM
|
|||
}
|
||||
Apply();
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// 蓄積した値を適用する
|
||||
/// </summary>
|
||||
public void Apply()
|
||||
{
|
||||
foreach (var kv in m_blendShapeValueMap)
|
||||
{
|
||||
Action<float> setter;
|
||||
if (m_blendShapeSetterMap.TryGetValue(kv.Key, out setter))
|
||||
{
|
||||
setter(kv.Value);
|
||||
}
|
||||
}
|
||||
m_blendShapeValueMap.Clear();
|
||||
|
||||
foreach (var kv in m_materialValueMap)
|
||||
{
|
||||
Action<float> setter;
|
||||
if (m_materialSetterMap.TryGetValue(kv.Key, out setter))
|
||||
{
|
||||
setter(kv.Value);
|
||||
}
|
||||
}
|
||||
m_materialValueMap.Clear();
|
||||
m_blendShapeBindingMerger.Apply();
|
||||
m_materialValueBindingMerger.Apply();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// まとめて反映する。1フレームに1回呼び出されることを想定
|
||||
/// </summary>
|
||||
/// <param name="values"></param>
|
||||
public void SetValues(IEnumerable<KeyValuePair<BlendShapeKey, float>> values)
|
||||
{
|
||||
foreach (var kv in values)
|
||||
{
|
||||
SetValue(kv.Key, kv.Value, false);
|
||||
AccumulateValue(kv.Key, kv.Value);
|
||||
}
|
||||
Apply();
|
||||
}
|
||||
|
||||
public void SetValue(BlendShapeKey key, float value, bool replace)
|
||||
/// <summary>
|
||||
/// 即時に反映しない。後にApplyによって反映する
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AccumulateValue(BlendShapeKey key, float value)
|
||||
{
|
||||
m_valueMap[key] = value;
|
||||
|
||||
|
|
@ -185,57 +86,48 @@ namespace VRM
|
|||
return;
|
||||
}
|
||||
|
||||
foreach (var binding in clip.Values)
|
||||
if (clip.IsBinary)
|
||||
{
|
||||
if (replace)
|
||||
{
|
||||
// 値置き換え
|
||||
Action<float> setter;
|
||||
if (m_blendShapeSetterMap.TryGetValue(binding, out setter))
|
||||
{
|
||||
setter(binding.Weight * value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 積算
|
||||
float acc;
|
||||
if (m_blendShapeValueMap.TryGetValue(binding, out acc))
|
||||
{
|
||||
m_blendShapeValueMap[binding] = acc + binding.Weight * value;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_blendShapeValueMap[binding] = binding.Weight * value;
|
||||
}
|
||||
}
|
||||
value = Mathf.Round(value);
|
||||
}
|
||||
|
||||
// materialの更新
|
||||
foreach (var binding in clip.MaterialValues)
|
||||
m_blendShapeBindingMerger.AccumulateValue(clip, value);
|
||||
m_materialValueBindingMerger.AccumulateValue(clip, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 即時に反映する
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void ImmediatelySetValue(BlendShapeKey key, float value)
|
||||
{
|
||||
m_valueMap[key] = value;
|
||||
|
||||
BlendShapeClip clip;
|
||||
if (!m_clipMap.TryGetValue(key, out clip))
|
||||
{
|
||||
if (replace)
|
||||
{
|
||||
// 値置き換え
|
||||
Action<float> setter;
|
||||
if (m_materialSetterMap.TryGetValue(binding, out setter))
|
||||
{
|
||||
setter(value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 積算
|
||||
float acc;
|
||||
if (m_materialValueMap.TryGetValue(binding, out acc))
|
||||
{
|
||||
m_materialValueMap[binding] = acc + value;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_materialValueMap[binding] = value;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (clip.IsBinary)
|
||||
{
|
||||
value = Mathf.Round(value);
|
||||
}
|
||||
|
||||
m_blendShapeBindingMerger.ImmediatelySetValue(clip, value);
|
||||
m_materialValueBindingMerger.ImmediatelySetValue(clip, value);
|
||||
}
|
||||
|
||||
public void SetValue(BlendShapeKey key, float value, bool immediately)
|
||||
{
|
||||
if (immediately)
|
||||
{
|
||||
ImmediatelySetValue(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
AccumulateValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,25 +143,7 @@ namespace VRM
|
|||
|
||||
public void RestoreMaterialInitialValues(IEnumerable<BlendShapeClip> clips)
|
||||
{
|
||||
if (m_materialMap != null)
|
||||
{
|
||||
foreach (var x in clips)
|
||||
{
|
||||
foreach (var y in x.MaterialValues)
|
||||
{
|
||||
// restore values
|
||||
Material material;
|
||||
if(m_materialMap.TryGetValue(y.MaterialName, out material))
|
||||
{
|
||||
material.SetColor(y.ValueName, y.BaseValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("{0} not found", y.MaterialName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m_materialValueBindingMerger.RestoreMaterialInitialValues(clips);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using UniGLTF;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
|
|
@ -12,260 +13,147 @@ namespace VRM
|
|||
[CustomEditor(typeof(BlendShapeAvatar))]
|
||||
public class BlendShapeAvatarEditor : PreviewEditor
|
||||
{
|
||||
static String[] Presets = ((BlendShapePreset[])Enum.GetValues(typeof(BlendShapePreset)))
|
||||
.Select(x => x.ToString()).ToArray();
|
||||
ReorderableList m_clipList;
|
||||
|
||||
BlendShapeAvatar m_target;
|
||||
void AddBlendShapeClip()
|
||||
BlendShapeClipSelector m_selector;
|
||||
|
||||
SerializedBlendShapeEditor m_clipEditor;
|
||||
|
||||
protected override PreviewSceneManager.BakeValue GetBakeValue()
|
||||
{
|
||||
var dir = Path.GetDirectoryName(AssetDatabase.GetAssetPath(m_target));
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"Create BlendShapeClip",
|
||||
dir,
|
||||
string.Format("BlendShapeClip#{0}.asset", m_target.Clips.Count),
|
||||
"asset");
|
||||
if (string.IsNullOrEmpty(path))
|
||||
var clip = m_selector.Selected;
|
||||
var value = new PreviewSceneManager.BakeValue();
|
||||
if (clip != null)
|
||||
{
|
||||
return;
|
||||
value.BlendShapeBindings = clip.Values;
|
||||
value.MaterialValueBindings = clip.MaterialValues;
|
||||
value.Weight = 1.0f;
|
||||
}
|
||||
path = path.ToUnityRelativePath();
|
||||
//Debug.LogFormat("{0}", path);
|
||||
var clip = ScriptableObject.CreateInstance<BlendShapeClip>();
|
||||
clip.BlendShapeName = Path.GetFileNameWithoutExtension(path);
|
||||
clip.Prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GetAssetPath(m_target));
|
||||
AssetDatabase.CreateAsset(clip, path);
|
||||
AssetDatabase.ImportAsset(path);
|
||||
|
||||
m_target.Clips.Add(clip);
|
||||
EditorUtility.SetDirty(m_target);
|
||||
AssetDatabase.SaveAssets();
|
||||
return value;
|
||||
}
|
||||
|
||||
BlendShapeClip m_currentClip;
|
||||
BlendShapeClip CurrentClip
|
||||
void OnSelected(BlendShapeClip clip)
|
||||
{
|
||||
get { return m_currentClip; }
|
||||
set
|
||||
if (PreviewSceneManager == null)
|
||||
{
|
||||
if (m_currentClip == value) return;
|
||||
|
||||
m_currentClip = value;
|
||||
//ClearBlendShape();
|
||||
if (m_currentClip != null)
|
||||
m_clipEditor = null;
|
||||
}
|
||||
else if (clip != null)
|
||||
{
|
||||
m_clipEditor = new SerializedBlendShapeEditor(clip, PreviewSceneManager);
|
||||
PreviewSceneManager.Bake(new PreviewSceneManager.BakeValue
|
||||
{
|
||||
Bake(m_currentClip.Values, m_currentClip.MaterialValues, 1.0f);
|
||||
}
|
||||
BlendShapeBindings = clip.Values,
|
||||
MaterialValueBindings = clip.MaterialValues,
|
||||
Weight = 1.0f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void OnPrefabChanged()
|
||||
{
|
||||
if (m_currentClip != null)
|
||||
else
|
||||
{
|
||||
Bake(m_currentClip.Values, m_currentClip.MaterialValues, 1.0f);
|
||||
m_clipEditor = null;
|
||||
PreviewSceneManager.Bake(new PreviewSceneManager.BakeValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
PrefabChanged += OnPrefabChanged;
|
||||
m_selector = new BlendShapeClipSelector((BlendShapeAvatar)target, OnSelected);
|
||||
|
||||
var prop = serializedObject.FindProperty("Clips");
|
||||
m_clipList = new ReorderableList(serializedObject, prop);
|
||||
|
||||
m_clipList.drawHeaderCallback = (rect) =>
|
||||
EditorGUI.LabelField(rect, "BlendShapeClips");
|
||||
|
||||
m_clipList.elementHeight = BlendShapeClipDrawer.Height;
|
||||
m_clipList.drawElementCallback = (rect, index, isActive, isFocused) =>
|
||||
{
|
||||
var element = prop.GetArrayElementAtIndex(index);
|
||||
rect.height -= 4;
|
||||
rect.y += 2;
|
||||
EditorGUI.PropertyField(rect, element);
|
||||
};
|
||||
|
||||
m_clipList.onAddCallback += (list) =>
|
||||
{
|
||||
// Add slot
|
||||
prop.arraySize++;
|
||||
// select last item
|
||||
list.index = prop.arraySize - 1;
|
||||
// get last item
|
||||
var element = prop.GetArrayElementAtIndex(list.index);
|
||||
element.objectReferenceValue = null;
|
||||
|
||||
var dir = Path.GetDirectoryName(AssetDatabase.GetAssetPath(target));
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"Create BlendShapeClip",
|
||||
dir,
|
||||
string.Format("BlendShapeClip#{0}.asset", list.count),
|
||||
"asset");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var clip = BlendShapeAvatar.CreateBlendShapeClip(path.ToUnityRelativePath());
|
||||
//clip.Prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GetAssetPath(target));
|
||||
|
||||
element.objectReferenceValue = clip;
|
||||
}
|
||||
};
|
||||
|
||||
m_clipList.onSelectCallback += (list) =>
|
||||
{
|
||||
var a = list.serializedProperty;
|
||||
var selected = a.GetArrayElementAtIndex(list.index);
|
||||
OnSelected((BlendShapeClip)selected.objectReferenceValue);
|
||||
};
|
||||
|
||||
//m_clipList.onCanRemoveCallback += list => true;
|
||||
base.OnEnable();
|
||||
m_target = (BlendShapeAvatar)target;
|
||||
|
||||
// remove missing values
|
||||
foreach(var x in m_target.Clips.Select((x, i) => new { i, x }).Where(x => x.x == null).Reverse())
|
||||
{
|
||||
m_target.Clips.RemoveAt(x.i);
|
||||
}
|
||||
|
||||
|
||||
if(m_target.Clips.Count > 0)
|
||||
{
|
||||
CurrentClip = m_target.Clips[0];
|
||||
}
|
||||
OnSelected(m_selector.Selected);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
PrefabChanged -= OnPrefabChanged;
|
||||
}
|
||||
int m_mode;
|
||||
static readonly string[] MODES = new string[]{
|
||||
"Editor",
|
||||
"List"
|
||||
};
|
||||
|
||||
List<bool> m_meshFolds = new List<bool>();
|
||||
int m_preset;
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
base.OnInspectorGUI();
|
||||
|
||||
// buttons
|
||||
if (m_target.Clips != null)
|
||||
m_mode = GUILayout.Toolbar(m_mode, MODES);
|
||||
switch (m_mode)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Select BlendShapeClip", EditorStyles.boldLabel);
|
||||
var array = m_target.Clips
|
||||
.Select(x => x != null
|
||||
? BlendShapeKey.CreateFrom(x).ToString()
|
||||
: "null"
|
||||
).ToArray();
|
||||
var preset = GUILayout.SelectionGrid(m_preset, array, 4);
|
||||
if (preset != m_preset)
|
||||
{
|
||||
CurrentClip = m_target.Clips[preset];
|
||||
m_preset = preset;
|
||||
}
|
||||
}
|
||||
|
||||
// Add
|
||||
if (GUILayout.Button("Add BlendShapeClip"))
|
||||
{
|
||||
AddBlendShapeClip();
|
||||
}
|
||||
|
||||
if (CurrentClip != null)
|
||||
{
|
||||
// clip
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("CurrentClip", EditorStyles.boldLabel);
|
||||
|
||||
/*var loadClip = (BlendShapeClip)*/
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.ObjectField("Current clip",
|
||||
CurrentClip, typeof(BlendShapeClip), false);
|
||||
GUI.enabled = true;
|
||||
|
||||
CurrentClip.Preset = (BlendShapePreset)EditorGUILayout.Popup("Preset", (int)CurrentClip.Preset, Presets);
|
||||
|
||||
GUI.enabled = false;
|
||||
CurrentClip.BlendShapeName = EditorGUILayout.TextField("BlendShapeName", CurrentClip.BlendShapeName);
|
||||
GUI.enabled = true;
|
||||
|
||||
var key = BlendShapeKey.CreateFrom(CurrentClip);
|
||||
if (m_target.Clips.Where(x => key.Match(x)).Count() > 1)
|
||||
{
|
||||
EditorGUILayout.HelpBox("duplicate clip", MessageType.Error);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("BlendShapeValues", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
ClearBlendShape();
|
||||
}
|
||||
|
||||
if (CurrentClip != null && GUILayout.Button("Apply"))
|
||||
{
|
||||
string maxWeightString;
|
||||
CurrentClip.Values = GetBindings(out maxWeightString);
|
||||
EditorUtility.SetDirty(CurrentClip);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// sliders
|
||||
bool changed = false;
|
||||
int foldIndex = 0;
|
||||
if (PreviewSceneManager != null)
|
||||
{
|
||||
foreach (var item in PreviewSceneManager.EnumRenderItems.Where(x => x.SkinnedMeshRenderer != null))
|
||||
case 0:
|
||||
m_selector.SelectGUI();
|
||||
if (m_clipEditor != null)
|
||||
{
|
||||
var mesh = item.SkinnedMeshRenderer.sharedMesh;
|
||||
if (mesh != null && mesh.blendShapeCount > 0)
|
||||
Separator();
|
||||
var result = m_clipEditor.Draw();
|
||||
if (result.Changed)
|
||||
{
|
||||
//var relativePath = UniGLTF.UnityExtensions.RelativePathFrom(renderer.transform, m_target.transform);
|
||||
//EditorGUILayout.LabelField(m_target.name + "/" + item.Path);
|
||||
|
||||
if (foldIndex >= m_meshFolds.Count)
|
||||
PreviewSceneManager.Bake(new PreviewSceneManager.BakeValue
|
||||
{
|
||||
m_meshFolds.Add(false);
|
||||
}
|
||||
m_meshFolds[foldIndex] = EditorGUILayout.Foldout(m_meshFolds[foldIndex], item.SkinnedMeshRenderer.name);
|
||||
if (m_meshFolds[foldIndex])
|
||||
{
|
||||
//EditorGUI.indentLevel += 1;
|
||||
for (int i = 0; i < mesh.blendShapeCount; ++i)
|
||||
{
|
||||
var src = item.SkinnedMeshRenderer.GetBlendShapeWeight(i);
|
||||
var dst = EditorGUILayout.Slider(mesh.GetBlendShapeName(i), src, 0, 100.0f);
|
||||
if (dst != src)
|
||||
{
|
||||
item.SkinnedMeshRenderer.SetBlendShapeWeight(i, dst);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
//EditorGUI.indentLevel -= 1;
|
||||
}
|
||||
++foldIndex;
|
||||
BlendShapeBindings = result.BlendShapeBindings,
|
||||
MaterialValueBindings = result.MaterialValueBindings,
|
||||
Weight = 1.0f,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
if (changed)
|
||||
{
|
||||
PreviewSceneManager.Bake();
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
m_clipList.DoLayoutList();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
BlendShapeBinding[] GetBindings(out string _maxWeightName)
|
||||
{
|
||||
var maxWeight = 0.0f;
|
||||
var maxWeightName = "";
|
||||
// weightのついたblendShapeを集める
|
||||
var values = PreviewSceneManager.EnumRenderItems
|
||||
.Where(x => x.SkinnedMeshRenderer!=null)
|
||||
.SelectMany(x =>
|
||||
{
|
||||
var mesh = x.SkinnedMeshRenderer.sharedMesh;
|
||||
|
||||
var relativePath = x.Path;
|
||||
|
||||
var list = new List<BlendShapeBinding>();
|
||||
if (mesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.blendShapeCount; ++i)
|
||||
{
|
||||
var weight = x.SkinnedMeshRenderer.GetBlendShapeWeight(i);
|
||||
if (weight == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var name = mesh.GetBlendShapeName(i);
|
||||
if (weight > maxWeight)
|
||||
{
|
||||
maxWeightName = name;
|
||||
maxWeight = weight;
|
||||
}
|
||||
list.Add(new BlendShapeBinding
|
||||
{
|
||||
Index = i,
|
||||
RelativePath = relativePath,
|
||||
Weight = weight
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}).ToArray()
|
||||
;
|
||||
_maxWeightName = maxWeightName;
|
||||
return values;
|
||||
}
|
||||
|
||||
private void ClearBlendShape()
|
||||
{
|
||||
foreach (var item in PreviewSceneManager.EnumRenderItems.Where(x => x.SkinnedMeshRenderer!=null))
|
||||
{
|
||||
var renderer = item.SkinnedMeshRenderer;
|
||||
var mesh = renderer.sharedMesh;
|
||||
if (mesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.blendShapeCount; ++i)
|
||||
{
|
||||
renderer.SetBlendShapeWeight(i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
Scripts/BlendShape/Editor/BlendShapeClipDrawer.cs
Normal file
63
Scripts/BlendShape/Editor/BlendShapeClipDrawer.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
|
||||
[CustomPropertyDrawer(typeof(BlendShapeClip))]
|
||||
public class BlendShapeClipDrawer : PropertyDrawer
|
||||
{
|
||||
//public const int Height = 132;
|
||||
|
||||
//public const int ThumbnailSize = 128;
|
||||
|
||||
public const int Height = 80;
|
||||
public const int ThumbnailSize = 0;
|
||||
|
||||
public override void OnGUI(Rect position,
|
||||
SerializedProperty property, GUIContent label)
|
||||
{
|
||||
using (new EditorGUI.PropertyScope(position, label, property))
|
||||
{
|
||||
//EditorGUIUtility.labelWidth = 80;
|
||||
|
||||
position.height = EditorGUIUtility.singleLineHeight;
|
||||
|
||||
var halfWidth = position.width * 0.5f;
|
||||
|
||||
var rect = new Rect(position.x + ThumbnailSize, position.y, position.width - ThumbnailSize, position.height);
|
||||
EditorGUI.PropertyField(rect, property);
|
||||
|
||||
var clip = property.objectReferenceValue as BlendShapeClip;
|
||||
if (clip != null)
|
||||
{
|
||||
var clipObj = new SerializedObject(clip);
|
||||
//var thumbnail = clipObj.FindProperty("Thumbnail");
|
||||
var blendShapeName = clipObj.FindProperty("BlendShapeName");
|
||||
var preset = clipObj.FindProperty("Preset");
|
||||
var isBinary = clipObj.FindProperty("IsBinary");
|
||||
|
||||
/*
|
||||
EditorGUI.ObjectField(new Rect(position)
|
||||
{
|
||||
width = ThumbnailSize,
|
||||
height = ThumbnailSize
|
||||
}, thumbnail.objectReferenceValue, typeof(Texture), false);
|
||||
*/
|
||||
|
||||
rect.y += (EditorGUIUtility.singleLineHeight + 2);
|
||||
EditorGUI.PropertyField(rect, blendShapeName);
|
||||
rect.y += (EditorGUIUtility.singleLineHeight + 2);
|
||||
EditorGUI.PropertyField(rect, preset);
|
||||
rect.y += (EditorGUIUtility.singleLineHeight + 2);
|
||||
EditorGUI.PropertyField(rect, isBinary);
|
||||
|
||||
clipObj.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Scripts/BlendShape/Editor/BlendShapeClipDrawer.cs.meta
Normal file
12
Scripts/BlendShape/Editor/BlendShapeClipDrawer.cs.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6a31667cfcf461c47b3b384211e2d9ff
|
||||
timeCreated: 1541179390
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using UniGLTF;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
|
@ -9,113 +11,154 @@ namespace VRM
|
|||
[CustomEditor(typeof(BlendShapeClip))]
|
||||
public class BlendShapeClipEditor : PreviewEditor
|
||||
{
|
||||
float m_previewSlider;
|
||||
|
||||
#region for Editor
|
||||
SerializedProperty m_BlendShapeNameProp;
|
||||
SerializedProperty m_PresetProp;
|
||||
SerializedProperty m_ValuesProp;
|
||||
ReorderableList m_ValuesList;
|
||||
SerializedProperty m_MaterialValuesProp;
|
||||
ReorderableList m_MaterialValuesList;
|
||||
#endregion
|
||||
SerializedBlendShapeEditor m_serializedEditor;
|
||||
|
||||
BlendShapeClip m_target;
|
||||
bool m_changed;
|
||||
protected override PreviewSceneManager.BakeValue GetBakeValue()
|
||||
{
|
||||
return new PreviewSceneManager.BakeValue
|
||||
{
|
||||
BlendShapeBindings = m_target.Values,
|
||||
MaterialValueBindings = m_target.MaterialValues,
|
||||
Weight = 1.0f,
|
||||
};
|
||||
}
|
||||
|
||||
//SerializedProperty m_thumbnailProp;
|
||||
SerializedProperty m_isBinaryProp;
|
||||
|
||||
protected override GameObject GetPrefab()
|
||||
{
|
||||
return m_target.Prefab;
|
||||
}
|
||||
|
||||
void OnPrefabChanged()
|
||||
{
|
||||
m_target.Prefab = Prefab;
|
||||
Bake(m_target.Values, m_target.MaterialValues, 1.0f);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
m_target = (BlendShapeClip)target;
|
||||
PrefabChanged += OnPrefabChanged;
|
||||
|
||||
base.OnEnable();
|
||||
|
||||
m_previewSlider = 1.0f;
|
||||
|
||||
Bake(m_target.Values, m_target.MaterialValues, m_previewSlider);
|
||||
|
||||
m_BlendShapeNameProp = serializedObject.FindProperty("BlendShapeName");
|
||||
m_PresetProp = serializedObject.FindProperty("Preset");
|
||||
m_ValuesProp = serializedObject.FindProperty("Values");
|
||||
|
||||
m_ValuesList = new ReorderableList(serializedObject, m_ValuesProp);
|
||||
m_ValuesList.elementHeight = BlendShapeBindingHeight;
|
||||
m_ValuesList.drawElementCallback =
|
||||
(rect, index, isActive, isFocused) =>
|
||||
{
|
||||
var element = m_ValuesProp.GetArrayElementAtIndex(index);
|
||||
rect.height -= 4;
|
||||
rect.y += 2;
|
||||
if (DrawBlendShapeBinding(rect, element, PreviewSceneManager))
|
||||
{
|
||||
m_changed = true;
|
||||
}
|
||||
};
|
||||
|
||||
m_MaterialValuesProp = serializedObject.FindProperty("MaterialValues");
|
||||
m_MaterialValuesList = new ReorderableList(serializedObject, m_MaterialValuesProp);
|
||||
m_MaterialValuesList.elementHeight = MaterialValueBindingHeight;
|
||||
m_MaterialValuesList.drawElementCallback =
|
||||
(rect, index, isActive, isFocused) =>
|
||||
{
|
||||
var element = m_MaterialValuesProp.GetArrayElementAtIndex(index);
|
||||
rect.height -= 4;
|
||||
rect.y += 2;
|
||||
if (DrawMaterialValueBinding(rect, element, PreviewSceneManager))
|
||||
{
|
||||
m_changed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
float m_previewSlider = 1.0f;
|
||||
|
||||
static Texture2D SaveResizedImage(RenderTexture rt, UnityPath path, int size)
|
||||
{
|
||||
base.OnDisable();
|
||||
PrefabChanged -= OnPrefabChanged;
|
||||
var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
|
||||
RenderTexture.active = rt;
|
||||
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
||||
tex.Apply();
|
||||
|
||||
//TextureScale.Scale(tex, size, size);
|
||||
tex = TextureScale.GetResized(tex, size, size);
|
||||
|
||||
byte[] bytes;
|
||||
switch (path.Extension.ToLower())
|
||||
{
|
||||
case ".png":
|
||||
bytes = tex.EncodeToPNG();
|
||||
break;
|
||||
|
||||
case ".jpg":
|
||||
bytes = tex.EncodeToJPG();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(tex);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
}
|
||||
File.WriteAllBytes(path.FullPath, bytes);
|
||||
|
||||
path.ImportAsset();
|
||||
return path.LoadAsset<Texture2D>();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
m_changed = false;
|
||||
if (PreviewSceneManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
var previewSlider = EditorGUILayout.Slider("Preview Weight", m_previewSlider, 0, 1.0f);
|
||||
if (m_serializedEditor == null)
|
||||
{
|
||||
m_serializedEditor = new SerializedBlendShapeEditor(serializedObject, PreviewSceneManager);
|
||||
//m_thumbnailProp = serializedObject.FindProperty("Thumbnail");
|
||||
m_isBinaryProp = serializedObject.FindProperty("IsBinary");
|
||||
}
|
||||
|
||||
int thumbnailSize = 96;
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
/*
|
||||
var objectReferenceValue = EditorGUILayout.ObjectField(m_thumbnailProp.objectReferenceValue, typeof(Texture), false,
|
||||
GUILayout.Width(thumbnailSize), GUILayout.Height(thumbnailSize));
|
||||
if (m_thumbnailProp.objectReferenceValue != objectReferenceValue)
|
||||
{
|
||||
m_thumbnailProp.objectReferenceValue = objectReferenceValue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
*/
|
||||
|
||||
var changed = false;
|
||||
EditorGUILayout.BeginVertical();
|
||||
base.OnInspectorGUI();
|
||||
EditorGUILayout.LabelField("Preview Weight");
|
||||
var previewSlider = EditorGUILayout.Slider(m_previewSlider, 0, 1.0f);
|
||||
GUI.enabled = PreviewTexture != null;
|
||||
/*
|
||||
if (GUILayout.Button("save thumbnail"))
|
||||
{
|
||||
//var ext = "jpg";
|
||||
var ext = "png";
|
||||
var asset = UnityPath.FromAsset(target);
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"save thumbnail",
|
||||
asset.Parent.FullPath,
|
||||
string.Format("{0}.{1}", asset.FileNameWithoutExtension, ext),
|
||||
ext);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var thumbnail = SaveResizedImage(PreviewTexture, UnityPath.FromFullpath(path),
|
||||
BlendShapeClipDrawer.ThumbnailSize);
|
||||
m_thumbnailProp.objectReferenceValue = thumbnail;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
*/
|
||||
GUI.enabled = true;
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (m_isBinaryProp.boolValue)
|
||||
{
|
||||
previewSlider = Mathf.Round(previewSlider);
|
||||
}
|
||||
if (previewSlider != m_previewSlider)
|
||||
{
|
||||
m_previewSlider = previewSlider;
|
||||
m_changed = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
Separator();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.PropertyField(m_BlendShapeNameProp, true);
|
||||
EditorGUILayout.PropertyField(m_PresetProp, true);
|
||||
|
||||
EditorGUILayout.LabelField("BlendShapeBindings", EditorStyles.boldLabel);
|
||||
m_ValuesList.DoLayoutList();
|
||||
|
||||
EditorGUILayout.LabelField("MaterialValueBindings", EditorStyles.boldLabel);
|
||||
m_MaterialValuesList.DoLayoutList();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
if (m_changed && PreviewSceneManager != null)
|
||||
var result = m_serializedEditor.Draw();
|
||||
if ((changed || result.Changed) && PreviewSceneManager != null)
|
||||
{
|
||||
PreviewSceneManager.Bake(m_target.Values, m_target.MaterialValues, m_previewSlider);
|
||||
PreviewSceneManager.Bake(new PreviewSceneManager.BakeValue
|
||||
{
|
||||
BlendShapeBindings = result.BlendShapeBindings,
|
||||
MaterialValueBindings = result.MaterialValueBindings,
|
||||
Weight = m_previewSlider
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,202 +166,5 @@ namespace VRM
|
|||
{
|
||||
return BlendShapeKey.CreateFrom((BlendShapeClip)target).ToString();
|
||||
}
|
||||
|
||||
public static int BlendShapeBindingHeight = 60;
|
||||
public static bool DrawBlendShapeBinding(Rect position, SerializedProperty property,
|
||||
PreviewSceneManager scene)
|
||||
{
|
||||
bool changed = false;
|
||||
if (scene != null)
|
||||
{
|
||||
var height = 16;
|
||||
|
||||
var y = position.y;
|
||||
var rect = new Rect(position.x, y, position.width, height);
|
||||
int pathIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("RelativePath"), scene.SkinnedMeshRendererPathList, out pathIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
int blendShapeIndex;
|
||||
if (IntPopup(rect, property.FindPropertyRelative("Index"), scene.GetBlendShapeNames(pathIndex), out blendShapeIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (FloatSlider(rect, property.FindPropertyRelative("Weight"), 100))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public static int MaterialValueBindingHeight = 90;
|
||||
public static bool DrawMaterialValueBinding(Rect position, SerializedProperty property,
|
||||
PreviewSceneManager scene)
|
||||
{
|
||||
bool changed = false;
|
||||
if (scene != null)
|
||||
{
|
||||
var height = 16;
|
||||
|
||||
var y = position.y;
|
||||
var rect = new Rect(position.x, y, position.width, height);
|
||||
int materialIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("MaterialName"), scene.MaterialNames, out materialIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (materialIndex >= 0)
|
||||
{
|
||||
var materialItem = scene.GetMaterialItem(scene.MaterialNames[materialIndex]);
|
||||
if (materialItem != null)
|
||||
{
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
|
||||
// プロパティ名のポップアップ
|
||||
int propIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("ValueName"), materialItem.PropNames, out propIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (propIndex >= 0)
|
||||
{
|
||||
// 有効なプロパティ名が選択された
|
||||
var propItem = materialItem.PropMap[materialItem.PropNames[propIndex]];
|
||||
{
|
||||
switch (propItem.PropertyType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
{
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = propItem.DefaultValues;
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (ColorProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||
{
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = propItem.DefaultValues;
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (OffsetProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
static bool StringPopup(Rect rect, SerializedProperty prop, string[] options, out int newIndex)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
newIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldIndex = Array.IndexOf(options, prop.stringValue);
|
||||
newIndex = EditorGUI.Popup(rect, oldIndex, options);
|
||||
if (newIndex != oldIndex && newIndex >= 0 && newIndex < options.Length)
|
||||
{
|
||||
prop.stringValue = options[newIndex];
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IntPopup(Rect rect, SerializedProperty prop, string[] options, out int newIndex)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
newIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldIndex = prop.intValue;
|
||||
newIndex = EditorGUI.Popup(rect, oldIndex, options);
|
||||
if (newIndex != oldIndex && newIndex >= 0 && newIndex < options.Length)
|
||||
{
|
||||
prop.intValue = newIndex;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool FloatSlider(Rect rect, SerializedProperty prop, float maxValue)
|
||||
{
|
||||
var oldValue = prop.floatValue;
|
||||
var newValue = EditorGUI.Slider(rect, prop.floatValue, 0, 100f);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.floatValue = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ColorProp(Rect rect, SerializedProperty prop)
|
||||
{
|
||||
var oldValue = (Color)prop.vector4Value;
|
||||
var newValue = EditorGUI.ColorField(rect, prop.displayName, oldValue);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.vector4Value = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool OffsetProp(Rect rect, SerializedProperty prop)
|
||||
{
|
||||
var oldValue = prop.vector4Value;
|
||||
var newValue = EditorGUI.Vector4Field(rect, prop.displayName, oldValue);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.vector4Value = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
341
Scripts/BlendShape/Editor/BlendShapeClipEditorHelper.cs
Normal file
341
Scripts/BlendShape/Editor/BlendShapeClipEditorHelper.cs
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
public static class BlendShapeClipEditorHelper
|
||||
{
|
||||
///
|
||||
/// BlendShape List のElement描画
|
||||
///
|
||||
public static bool DrawBlendShapeBinding(Rect position, SerializedProperty property,
|
||||
PreviewSceneManager scene)
|
||||
{
|
||||
bool changed = false;
|
||||
if (scene != null)
|
||||
{
|
||||
var height = 16;
|
||||
|
||||
var y = position.y;
|
||||
var rect = new Rect(position.x, y, position.width, height);
|
||||
int pathIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("RelativePath"), scene.SkinnedMeshRendererPathList, out pathIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
int blendShapeIndex;
|
||||
if (IntPopup(rect, property.FindPropertyRelative("Index"), scene.GetBlendShapeNames(pathIndex), out blendShapeIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (FloatSlider(rect, property.FindPropertyRelative("Weight"), 100))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
///
|
||||
/// Material List のElement描画
|
||||
///
|
||||
public static bool DrawMaterialValueBinding(Rect position, SerializedProperty property,
|
||||
PreviewSceneManager scene)
|
||||
{
|
||||
bool changed = false;
|
||||
if (scene != null)
|
||||
{
|
||||
var height = 16;
|
||||
|
||||
var y = position.y;
|
||||
var rect = new Rect(position.x, y, position.width, height);
|
||||
int materialIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("MaterialName"), scene.MaterialNames, out materialIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (materialIndex >= 0)
|
||||
{
|
||||
var materialItem = scene.GetMaterialItem(scene.MaterialNames[materialIndex]);
|
||||
if (materialItem != null)
|
||||
{
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
|
||||
// プロパティ名のポップアップ
|
||||
int propIndex;
|
||||
if (StringPopup(rect, property.FindPropertyRelative("ValueName"), materialItem.PropNames, out propIndex))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (propIndex >= 0)
|
||||
{
|
||||
// 有効なプロパティ名が選択された
|
||||
var propItem = materialItem.PropMap[materialItem.PropNames[propIndex]];
|
||||
{
|
||||
switch (propItem.PropertyType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
{
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = propItem.DefaultValues;
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (ColorProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||
{
|
||||
property.FindPropertyRelative("BaseValue").vector4Value = propItem.DefaultValues;
|
||||
|
||||
// max
|
||||
y += height;
|
||||
rect = new Rect(position.x, y, position.width, height);
|
||||
if (OffsetProp(rect, property.FindPropertyRelative("TargetValue")))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
#region Private
|
||||
static bool StringPopup(Rect rect, SerializedProperty prop, string[] options, out int newIndex)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
newIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldIndex = Array.IndexOf(options, prop.stringValue);
|
||||
newIndex = EditorGUI.Popup(rect, oldIndex, options);
|
||||
if (newIndex != oldIndex && newIndex >= 0 && newIndex < options.Length)
|
||||
{
|
||||
prop.stringValue = options[newIndex];
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IntPopup(Rect rect, SerializedProperty prop, string[] options, out int newIndex)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
newIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldIndex = prop.intValue;
|
||||
newIndex = EditorGUI.Popup(rect, oldIndex, options);
|
||||
if (newIndex != oldIndex && newIndex >= 0 && newIndex < options.Length)
|
||||
{
|
||||
prop.intValue = newIndex;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool FloatSlider(Rect rect, SerializedProperty prop, float maxValue)
|
||||
{
|
||||
var oldValue = prop.floatValue;
|
||||
var newValue = EditorGUI.Slider(rect, prop.floatValue, 0, 100f);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.floatValue = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ColorProp(Rect rect, SerializedProperty prop)
|
||||
{
|
||||
var oldValue = (Color)prop.vector4Value;
|
||||
var newValue = EditorGUI.ColorField(rect, prop.displayName, oldValue);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.vector4Value = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Rect AdvanceRect(ref float x, float y, float w, float h)
|
||||
{
|
||||
var rect = new Rect(x, y, w, h);
|
||||
x += w;
|
||||
return rect;
|
||||
}
|
||||
|
||||
static float[] v2 = new float[2];
|
||||
static GUIContent[] l2 = new GUIContent[]{
|
||||
new GUIContent("x"),
|
||||
new GUIContent("y")
|
||||
};
|
||||
static Vector4 TilingOffset(Rect rect, string label, Vector4 src)
|
||||
{
|
||||
/*
|
||||
var style = new GUIStyle()
|
||||
{
|
||||
alignment = TextAnchor.MiddleRight,
|
||||
};
|
||||
*/
|
||||
|
||||
var quad = (rect.width - 56);
|
||||
var x = rect.x;
|
||||
//EditorGUIUtility.labelWidth = 18;
|
||||
|
||||
EditorGUI.LabelField(AdvanceRect(ref x, rect.y, 40, rect.height), "Tiling");
|
||||
v2[0] = src.x;
|
||||
v2[1] = src.y;
|
||||
EditorGUI.MultiFloatField(AdvanceRect(ref x, rect.y, quad, rect.height), l2, v2);
|
||||
src.x = v2[0];
|
||||
src.y = v2[1];
|
||||
|
||||
//EditorGUI.LabelField(AdvanceRect(ref x, rect.y, quad, rect.height), "Y", style);
|
||||
//src.y = EditorGUI.FloatField(AdvanceRect(ref x, rect.y, quad, rect.height), "Y", src.y);
|
||||
|
||||
rect.y += EditorGUIUtility.singleLineHeight;
|
||||
x = rect.x;
|
||||
EditorGUI.LabelField(AdvanceRect(ref x, rect.y, 40, rect.height), "Offset");
|
||||
v2[0] = src.z;
|
||||
v2[1] = src.w;
|
||||
EditorGUI.MultiFloatField(AdvanceRect(ref x, rect.y, quad, rect.height), l2, v2);
|
||||
src.z = v2[0];
|
||||
src.w = v2[1];
|
||||
|
||||
//EditorGUI.LabelField(AdvanceRect(ref x, rect.y, quad * 2, rect.height), "Offset X", style);
|
||||
//src.z = EditorGUI.FloatField(AdvanceRect(ref x, rect.y, quad, rect.height), "X", src.z);
|
||||
|
||||
//EditorGUI.LabelField(AdvanceRect(ref x, rect.y, quad, rect.height), "Y", style);
|
||||
//src.w = EditorGUI.FloatField(AdvanceRect(ref x, rect.y, quad, rect.height), "Y", src.w);
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
static bool OffsetProp(Rect rect, SerializedProperty prop)
|
||||
{
|
||||
var oldValue = prop.vector4Value;
|
||||
//var newValue = EditorGUI.Vector4Field(rect, prop.displayName, oldValue);
|
||||
var newValue = TilingOffset(rect, prop.displayName, oldValue);
|
||||
if (newValue != oldValue)
|
||||
{
|
||||
prop.vector4Value = newValue;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// https://gist.github.com/gszauer/7799899
|
||||
public class TextureScale
|
||||
{
|
||||
private static Color[] texColors;
|
||||
private static Color[] newColors;
|
||||
private static int w;
|
||||
private static float ratioX;
|
||||
private static float ratioY;
|
||||
private static int w2;
|
||||
|
||||
public static void Scale(Texture2D tex, int newWidth, int newHeight)
|
||||
{
|
||||
texColors = tex.GetPixels();
|
||||
newColors = new Color[newWidth * newHeight];
|
||||
ratioX = 1.0f / ((float)newWidth / (tex.width - 1));
|
||||
ratioY = 1.0f / ((float)newHeight / (tex.height - 1));
|
||||
w = tex.width;
|
||||
w2 = newWidth;
|
||||
|
||||
BilinearScale(0, newHeight);
|
||||
|
||||
tex.Resize(newWidth, newHeight);
|
||||
tex.SetPixels(newColors);
|
||||
tex.Apply();
|
||||
}
|
||||
|
||||
private static void BilinearScale(int start, int end)
|
||||
{
|
||||
for (var y = start; y < end; y++)
|
||||
{
|
||||
int yFloor = (int)Mathf.Floor(y * ratioY);
|
||||
var y1 = yFloor * w;
|
||||
var y2 = (yFloor + 1) * w;
|
||||
var yw = y * w2;
|
||||
|
||||
for (var x = 0; x < w2; x++)
|
||||
{
|
||||
int xFloor = (int)Mathf.Floor(x * ratioX);
|
||||
var xLerp = x * ratioX - xFloor;
|
||||
newColors[yw + x] = ColorLerpUnclamped(ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp),
|
||||
ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp),
|
||||
y * ratioY - yFloor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Color ColorLerpUnclamped(Color c1, Color c2, float value)
|
||||
{
|
||||
return new Color(c1.r + (c2.r - c1.r) * value,
|
||||
c1.g + (c2.g - c1.g) * value,
|
||||
c1.b + (c2.b - c1.b) * value,
|
||||
c1.a + (c2.a - c1.a) * value);
|
||||
}
|
||||
|
||||
/// http://light11.hatenadiary.com/entry/2018/04/19/194015
|
||||
public static Texture2D GetResized(Texture2D texture, int width, int height)
|
||||
{
|
||||
// リサイズ後のサイズを持つRenderTextureを作成して書き込む
|
||||
var rt = RenderTexture.GetTemporary(width, height);
|
||||
Graphics.Blit(texture, rt);
|
||||
|
||||
// リサイズ後のサイズを持つTexture2Dを作成してRenderTextureから書き込む
|
||||
var preRT = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
var ret = new Texture2D(width, height);
|
||||
ret.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
ret.Apply();
|
||||
RenderTexture.active = preRT;
|
||||
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Scripts/BlendShape/Editor/BlendShapeClipEditorHelper.cs.meta
Normal file
12
Scripts/BlendShape/Editor/BlendShapeClipEditorHelper.cs.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fcb56a7eb1db73c4cb9ea1689635b246
|
||||
timeCreated: 1541144400
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
99
Scripts/BlendShape/Editor/BlendShapeClipSelector.cs
Normal file
99
Scripts/BlendShape/Editor/BlendShapeClipSelector.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
using UniGLTF;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
class BlendShapeClipSelector
|
||||
{
|
||||
BlendShapeAvatar m_avatar;
|
||||
|
||||
public BlendShapeClip Selected
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_avatar == null || m_avatar.Clips == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (m_selectedIndex < 0 || m_selectedIndex >= m_avatar.Clips.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return m_avatar.Clips[m_selectedIndex];
|
||||
}
|
||||
}
|
||||
|
||||
int m_selectedIndex;
|
||||
int SelectedIndex
|
||||
{
|
||||
get { return m_selectedIndex; }
|
||||
set
|
||||
{
|
||||
if (m_selectedIndex == value) return;
|
||||
m_selectedIndex = value;
|
||||
if (m_onSelected != null)
|
||||
{
|
||||
m_onSelected(Selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Action<BlendShapeClip> m_onSelected;
|
||||
|
||||
public BlendShapeClipSelector(BlendShapeAvatar avatar, Action<BlendShapeClip> onSelected)
|
||||
{
|
||||
avatar.RemoveNullClip();
|
||||
|
||||
m_avatar = avatar;
|
||||
m_onSelected = onSelected;
|
||||
|
||||
onSelected(Selected);
|
||||
}
|
||||
|
||||
public void SelectGUI()
|
||||
{
|
||||
if (m_avatar != null && m_avatar.Clips != null)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Select BlendShapeClip", EditorStyles.boldLabel);
|
||||
var array = m_avatar.Clips
|
||||
.Select(x => x != null
|
||||
? BlendShapeKey.CreateFrom(x).ToString()
|
||||
: "null"
|
||||
).ToArray();
|
||||
SelectedIndex = GUILayout.SelectionGrid(SelectedIndex, array, 4);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Add BlendShapeClip"))
|
||||
{
|
||||
var dir = Path.GetDirectoryName(AssetDatabase.GetAssetPath(m_avatar));
|
||||
var path = EditorUtility.SaveFilePanel(
|
||||
"Create BlendShapeClip",
|
||||
dir,
|
||||
string.Format("BlendShapeClip#{0}.asset", m_avatar.Clips.Count),
|
||||
"asset");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var clip = BlendShapeAvatar.CreateBlendShapeClip(path.ToUnityRelativePath());
|
||||
//clip.Prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GetAssetPath(target));
|
||||
|
||||
m_avatar.Clips.Add(clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DuplicateWarn()
|
||||
{
|
||||
var key = BlendShapeKey.CreateFrom(Selected);
|
||||
if (m_avatar.Clips.Where(x => key.Match(x)).Count() > 1)
|
||||
{
|
||||
EditorGUILayout.HelpBox("duplicate clip: " + key, MessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
12
Scripts/BlendShape/Editor/BlendShapeClipSelector.cs.meta
Normal file
12
Scripts/BlendShape/Editor/BlendShapeClipSelector.cs.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7842ed7a65c676740aa02678316dd625
|
||||
timeCreated: 1541138009
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
using UnityEngine;
|
||||
using UnityEditorInternal;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
|
|
@ -11,7 +13,7 @@ namespace VRM
|
|||
/// * https://github.com/Unity-Technologies/UnityCsReference/blob/11bcfd801fccd2a52b09bb6fd636c1ddcc9f1705/Editor/Mono/Inspector/ModelInspector.cs
|
||||
///
|
||||
/// </summary>
|
||||
public class PreviewEditor : Editor
|
||||
public abstract class PreviewEditor : Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// PreviewRenderUtilityを管理する。
|
||||
|
|
@ -46,6 +48,8 @@ namespace VRM
|
|||
private set
|
||||
{
|
||||
if (m_prefab == value) return;
|
||||
|
||||
//Debug.LogFormat("Prefab = {0}", value);
|
||||
m_prefab = value;
|
||||
|
||||
if (m_scene != null)
|
||||
|
|
@ -62,30 +66,23 @@ namespace VRM
|
|||
{
|
||||
m_scene.gameObject.SetActive(false);
|
||||
}
|
||||
RaisePrefabChanged();
|
||||
|
||||
Bake();
|
||||
}
|
||||
}
|
||||
}
|
||||
protected event Action PrefabChanged;
|
||||
void RaisePrefabChanged()
|
||||
{
|
||||
var handler = PrefabChanged;
|
||||
if (handler == null) return;
|
||||
handler();
|
||||
}
|
||||
|
||||
protected abstract PreviewSceneManager.BakeValue GetBakeValue();
|
||||
|
||||
/// <summary>
|
||||
/// シーンにBlendShapeとMaterialMorphを適用する
|
||||
/// Preview シーンに BlendShape と MaterialValue を適用する
|
||||
/// </summary>
|
||||
/// <param name="values"></param>
|
||||
/// <param name="materialValues"></param>
|
||||
/// <param name="weight"></param>
|
||||
protected void Bake(BlendShapeBinding[] values, MaterialValueBinding[] materialValues, float weight)
|
||||
protected void Bake()
|
||||
{
|
||||
if (m_scene != null)
|
||||
{
|
||||
//Debug.Log("Bake");
|
||||
m_scene.Bake(values, materialValues, weight);
|
||||
m_scene.Bake(GetBakeValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +107,7 @@ namespace VRM
|
|||
protected virtual void OnEnable()
|
||||
{
|
||||
m_renderer = new PreviewFaceRenderer();
|
||||
|
||||
Prefab = GetPrefab();
|
||||
}
|
||||
|
||||
|
|
@ -133,20 +131,35 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
protected static void Separator()
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
//GUILayout.Space();
|
||||
GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
//base.OnInspectorGUI();
|
||||
|
||||
Prefab = (GameObject)EditorGUILayout.ObjectField("prefab", Prefab, typeof(GameObject), false);
|
||||
Prefab = (GameObject)EditorGUILayout.ObjectField("Preview Prefab", Prefab, typeof(GameObject), false);
|
||||
|
||||
//Separator();
|
||||
}
|
||||
|
||||
private static int sliderHash = "Slider".GetHashCode();
|
||||
Vector2 m_previewDir;
|
||||
float m_distance = 1.0f;
|
||||
float m_yaw = 180.0f;
|
||||
float m_pitch;
|
||||
Vector3 m_position = new Vector3(0, 0, -0.8f);
|
||||
|
||||
// very important to override this, it tells Unity to render an ObjectPreview at the bottom of the inspector
|
||||
public override bool HasPreviewGUI() { return true; }
|
||||
|
||||
public RenderTexture PreviewTexture;
|
||||
|
||||
// the main ObjectPreview function... it's called constantly, like other IMGUI On*GUI() functions
|
||||
public override void OnPreviewGUI(Rect r, GUIStyle background)
|
||||
{
|
||||
|
|
@ -155,23 +168,31 @@ namespace VRM
|
|||
{
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
{
|
||||
EditorGUI.DropShadowLabel(new Rect(r.x, r.y, r.width, 40f),
|
||||
EditorGUI.DropShadowLabel(new Rect(r.x, r.y, r.width, 40f),
|
||||
"Mesh preview requires\nrender texture support");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var src = r;
|
||||
|
||||
var min = Mathf.Min(r.width, r.height);
|
||||
r.width = min;
|
||||
r.height = min;
|
||||
r.x = src.x + (src.width - min) / 2;
|
||||
r.y = src.y + (src.height - min) / 2;
|
||||
|
||||
//previewDir = Drag2D(previewDir, r);
|
||||
{
|
||||
int controlId = GUIUtility.GetControlID(sliderHash, FocusType.Passive);
|
||||
Event current = Event.current;
|
||||
switch (current.GetTypeForControl(controlId))
|
||||
Event e = Event.current;
|
||||
switch (e.GetTypeForControl(controlId))
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
if (r.Contains(current.mousePosition) && (double)r.width > 50.0)
|
||||
if (r.Contains(e.mousePosition) && (double)r.width > 50.0)
|
||||
{
|
||||
GUIUtility.hotControl = controlId;
|
||||
current.Use();
|
||||
e.Use();
|
||||
EditorGUIUtility.SetWantsMouseJumping(1);
|
||||
break;
|
||||
}
|
||||
|
|
@ -186,25 +207,41 @@ namespace VRM
|
|||
case EventType.MouseDrag:
|
||||
if (GUIUtility.hotControl == controlId)
|
||||
{
|
||||
m_previewDir -= current.delta * (!current.shift ? 1f : 3f) / Mathf.Min(r.width, r.height) * 140f;
|
||||
m_previewDir.y = Mathf.Clamp(m_previewDir.y, -90f, 90f);
|
||||
current.Use();
|
||||
GUI.changed = true;
|
||||
if (e.button == 2)
|
||||
{
|
||||
var shift = e.delta * (!e.shift ? 1f : 3f) / Mathf.Min(r.width, r.height);
|
||||
m_position.x -= shift.x;
|
||||
m_position.y += shift.y;
|
||||
e.Use();
|
||||
GUI.changed = true;
|
||||
}
|
||||
else if (
|
||||
e.button == 0 ||
|
||||
e.button == 1)
|
||||
{
|
||||
var shift = e.delta * (!e.shift ? 1f : 3f) / Mathf.Min(r.width, r.height) * 140f;
|
||||
m_yaw += shift.x;
|
||||
m_pitch += shift.y;
|
||||
m_pitch = Mathf.Clamp(m_pitch, -90f, 90f);
|
||||
e.Use();
|
||||
GUI.changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.ScrollWheel:
|
||||
//Debug.LogFormat("wheel: {0}", current.delta);
|
||||
if (r.Contains(current.mousePosition)){
|
||||
if (current.delta.y > 0)
|
||||
if (r.Contains(e.mousePosition))
|
||||
{
|
||||
if (e.delta.y > 0)
|
||||
{
|
||||
m_distance *= 1.1f;
|
||||
m_position.z *= 1.1f;
|
||||
Repaint();
|
||||
}
|
||||
else if (current.delta.y < 0)
|
||||
else if (e.delta.y < 0)
|
||||
{
|
||||
m_distance *= 0.9f;
|
||||
m_position.z *= 0.9f;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
|
@ -222,11 +259,11 @@ namespace VRM
|
|||
|
||||
if (m_renderer != null && m_scene != null)
|
||||
{
|
||||
var texture = m_renderer.Render(r, background, m_scene, m_previewDir, m_distance);
|
||||
if (texture != null)
|
||||
PreviewTexture = m_renderer.Render(r, background, m_scene, m_yaw, m_pitch, m_position) as RenderTexture;
|
||||
if (PreviewTexture != null)
|
||||
{
|
||||
// draw the RenderTexture in the ObjectPreview pane
|
||||
GUI.DrawTexture(r, texture, ScaleMode.StretchToFill, false);
|
||||
GUI.DrawTexture(r, PreviewTexture, ScaleMode.StretchToFill, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ namespace VRM
|
|||
|
||||
//const float FACTOR = 0.1f;
|
||||
|
||||
public Texture Render(Rect r, GUIStyle background, PreviewSceneManager scene, Vector2 drag, float distance)
|
||||
public Texture Render(Rect r, GUIStyle background, PreviewSceneManager scene,
|
||||
float yaw, float pitch, Vector3 position)
|
||||
{
|
||||
if (scene == null) return null;
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ namespace VRM
|
|||
m_previewUtility.BeginPreview(r, background); // set up the PreviewRenderUtility's mini internal scene
|
||||
|
||||
// setup the ObjectPreview's camera
|
||||
scene.SetupCamera(PreviewCamera, scene.TargetPosition, -drag.x, drag.y, distance);
|
||||
scene.SetupCamera(PreviewCamera, scene.TargetPosition, yaw, pitch, position);
|
||||
|
||||
foreach (var item in scene.EnumRenderItems)
|
||||
{
|
||||
|
|
@ -115,7 +116,7 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false; // 重複する呼び出しを検出するには
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
|
|
@ -153,6 +154,6 @@ namespace VRM
|
|||
// TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。
|
||||
// GC.SuppressFinalize(this);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
309
Scripts/BlendShape/Editor/SerializedBlendShapeClipEditor.cs
Normal file
309
Scripts/BlendShape/Editor/SerializedBlendShapeClipEditor.cs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
public class SerializedBlendShapeEditor
|
||||
{
|
||||
BlendShapeClip m_targetObject;
|
||||
|
||||
SerializedObject m_serializedObject;
|
||||
|
||||
#region Properties
|
||||
SerializedProperty m_thumbnail;
|
||||
SerializedProperty m_blendShapeNameProp;
|
||||
SerializedProperty m_presetProp;
|
||||
|
||||
SerializedProperty m_isBinaryProp;
|
||||
#endregion
|
||||
|
||||
#region BlendShapeBind
|
||||
public static int BlendShapeBindingHeight = 60;
|
||||
ReorderableList m_ValuesList;
|
||||
|
||||
SerializedProperty m_valuesProp;
|
||||
#endregion
|
||||
|
||||
#region MaterialValueBind
|
||||
const int MaterialValueBindingHeight = 90;
|
||||
ReorderableList m_MaterialValuesList;
|
||||
|
||||
SerializedProperty m_materialsProp;
|
||||
#endregion
|
||||
|
||||
#region Editor values
|
||||
float m_previewSlider = 1.0f;
|
||||
|
||||
bool m_changed;
|
||||
|
||||
int m_mode;
|
||||
static string[] MODES = new[]{
|
||||
"BlendShape",
|
||||
"BlendShape List",
|
||||
"Material List"
|
||||
};
|
||||
|
||||
MeshPreviewItem[] m_items;
|
||||
#endregion
|
||||
|
||||
public SerializedBlendShapeEditor(SerializedObject serializedObject,
|
||||
PreviewSceneManager previewSceneManager) : this(
|
||||
serializedObject, (BlendShapeClip)serializedObject.targetObject, previewSceneManager)
|
||||
{ }
|
||||
|
||||
public SerializedBlendShapeEditor(BlendShapeClip blendShapeClip,
|
||||
PreviewSceneManager previewSceneManager) : this(
|
||||
new SerializedObject(blendShapeClip), blendShapeClip, previewSceneManager)
|
||||
{ }
|
||||
|
||||
public SerializedBlendShapeEditor(SerializedObject serializedObject, BlendShapeClip targetObject,
|
||||
PreviewSceneManager previewSceneManager)
|
||||
{
|
||||
this.m_serializedObject = serializedObject;
|
||||
this.m_targetObject = targetObject;
|
||||
|
||||
//m_thumbnail = serializedObject.FindProperty("Thumbnail");
|
||||
m_blendShapeNameProp = serializedObject.FindProperty("BlendShapeName");
|
||||
m_presetProp = serializedObject.FindProperty("Preset");
|
||||
m_isBinaryProp = serializedObject.FindProperty("IsBinary");
|
||||
|
||||
m_valuesProp = serializedObject.FindProperty("Values");
|
||||
|
||||
m_ValuesList = new ReorderableList(serializedObject, m_valuesProp);
|
||||
m_ValuesList.elementHeight = BlendShapeBindingHeight;
|
||||
m_ValuesList.drawElementCallback =
|
||||
(rect, index, isActive, isFocused) =>
|
||||
{
|
||||
var element = m_valuesProp.GetArrayElementAtIndex(index);
|
||||
rect.height -= 4;
|
||||
rect.y += 2;
|
||||
if (BlendShapeClipEditorHelper.DrawBlendShapeBinding(rect, element, previewSceneManager))
|
||||
{
|
||||
m_changed = true;
|
||||
}
|
||||
};
|
||||
|
||||
m_materialsProp = serializedObject.FindProperty("MaterialValues");
|
||||
m_MaterialValuesList = new ReorderableList(serializedObject, m_materialsProp);
|
||||
m_MaterialValuesList.elementHeight = MaterialValueBindingHeight;
|
||||
m_MaterialValuesList.drawElementCallback =
|
||||
(rect, index, isActive, isFocused) =>
|
||||
{
|
||||
var element = m_materialsProp.GetArrayElementAtIndex(index);
|
||||
rect.height -= 4;
|
||||
rect.y += 2;
|
||||
if (BlendShapeClipEditorHelper.DrawMaterialValueBinding(rect, element, previewSceneManager))
|
||||
{
|
||||
m_changed = true;
|
||||
}
|
||||
};
|
||||
|
||||
m_items = previewSceneManager.EnumRenderItems
|
||||
.Where(x => x.SkinnedMeshRenderer != null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public struct DrawResult
|
||||
{
|
||||
public bool Changed;
|
||||
|
||||
public BlendShapeBinding[] BlendShapeBindings;
|
||||
|
||||
public MaterialValueBinding[] MaterialValueBindings;
|
||||
}
|
||||
|
||||
public DrawResult Draw()
|
||||
{
|
||||
m_changed = false;
|
||||
|
||||
m_serializedObject.Update();
|
||||
|
||||
// Readonly のBlendShapeClip参照
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.ObjectField("Current clip",
|
||||
m_targetObject, typeof(BlendShapeClip), false);
|
||||
GUI.enabled = true;
|
||||
|
||||
EditorGUILayout.PropertyField(m_blendShapeNameProp, true);
|
||||
EditorGUILayout.PropertyField(m_presetProp, true);
|
||||
|
||||
// v0.45 Added. Binary flag
|
||||
EditorGUILayout.PropertyField(m_isBinaryProp, true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
//m_mode = EditorGUILayout.Popup("SourceType", m_mode, MODES);
|
||||
m_mode = GUILayout.Toolbar(m_mode, MODES);
|
||||
switch (m_mode)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
ClipGUI();
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
{
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
m_changed = true;
|
||||
m_valuesProp.arraySize = 0;
|
||||
}
|
||||
m_ValuesList.DoLayoutList();
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
m_changed = true;
|
||||
m_materialsProp.arraySize = 0;
|
||||
}
|
||||
m_MaterialValuesList.DoLayoutList();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
m_serializedObject.ApplyModifiedProperties();
|
||||
|
||||
return new DrawResult
|
||||
{
|
||||
Changed = m_changed,
|
||||
BlendShapeBindings = m_targetObject.Values,
|
||||
MaterialValueBindings = m_targetObject.MaterialValues
|
||||
};
|
||||
}
|
||||
|
||||
void ClipGUI()
|
||||
{
|
||||
var changed = BlendShapeBindsGUI();
|
||||
if (changed)
|
||||
{
|
||||
string maxWeightName;
|
||||
var bindings = GetBindings(out maxWeightName);
|
||||
m_valuesProp.ClearArray();
|
||||
m_valuesProp.arraySize = bindings.Length;
|
||||
for (int i = 0; i < bindings.Length; ++i)
|
||||
{
|
||||
var item = m_valuesProp.GetArrayElementAtIndex(i);
|
||||
|
||||
var endProperty = item.GetEndProperty();
|
||||
while (item.NextVisible(true))
|
||||
{
|
||||
if (SerializedProperty.EqualContents(item, endProperty))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
switch (item.name)
|
||||
{
|
||||
case "RelativePath":
|
||||
item.stringValue = bindings[i].RelativePath;
|
||||
break;
|
||||
|
||||
case "Index":
|
||||
item.intValue = bindings[i].Index;
|
||||
break;
|
||||
|
||||
case "Weight":
|
||||
item.floatValue = bindings[i].Weight;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
List<bool> m_meshFolds = new List<bool>();
|
||||
bool BlendShapeBindsGUI()
|
||||
{
|
||||
bool changed = false;
|
||||
int foldIndex = 0;
|
||||
// すべてのSkinnedMeshRendererを列挙する
|
||||
foreach (var renderer in m_items.Select(x => x.SkinnedMeshRenderer))
|
||||
{
|
||||
var mesh = renderer.sharedMesh;
|
||||
if (mesh != null && mesh.blendShapeCount > 0)
|
||||
{
|
||||
//var relativePath = UniGLTF.UnityExtensions.RelativePathFrom(renderer.transform, m_target.transform);
|
||||
//EditorGUILayout.LabelField(m_target.name + "/" + item.Path);
|
||||
|
||||
if (foldIndex >= m_meshFolds.Count)
|
||||
{
|
||||
m_meshFolds.Add(false);
|
||||
}
|
||||
m_meshFolds[foldIndex] = EditorGUILayout.Foldout(m_meshFolds[foldIndex], renderer.name);
|
||||
if (m_meshFolds[foldIndex])
|
||||
{
|
||||
//EditorGUI.indentLevel += 1;
|
||||
for (int i = 0; i < mesh.blendShapeCount; ++i)
|
||||
{
|
||||
var src = renderer.GetBlendShapeWeight(i);
|
||||
var dst = EditorGUILayout.Slider(mesh.GetBlendShapeName(i), src, 0, 100.0f);
|
||||
if (dst != src)
|
||||
{
|
||||
renderer.SetBlendShapeWeight(i, dst);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
//EditorGUI.indentLevel -= 1;
|
||||
}
|
||||
++foldIndex;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
BlendShapeBinding[] GetBindings(out string _maxWeightName)
|
||||
{
|
||||
var maxWeight = 0.0f;
|
||||
var maxWeightName = "";
|
||||
// weightのついたblendShapeを集める
|
||||
var values = m_items
|
||||
.SelectMany(x =>
|
||||
{
|
||||
var mesh = x.SkinnedMeshRenderer.sharedMesh;
|
||||
|
||||
var relativePath = x.Path;
|
||||
|
||||
var list = new List<BlendShapeBinding>();
|
||||
if (mesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.blendShapeCount; ++i)
|
||||
{
|
||||
var weight = x.SkinnedMeshRenderer.GetBlendShapeWeight(i);
|
||||
if (weight == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var name = mesh.GetBlendShapeName(i);
|
||||
if (weight > maxWeight)
|
||||
{
|
||||
maxWeightName = name;
|
||||
maxWeight = weight;
|
||||
}
|
||||
list.Add(new BlendShapeBinding
|
||||
{
|
||||
Index = i,
|
||||
RelativePath = relativePath,
|
||||
Weight = weight
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}).ToArray()
|
||||
;
|
||||
_maxWeightName = maxWeightName;
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: eaafa3ff7bf991642b922e6af7ecbbc0
|
||||
timeCreated: 1541081003
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -23,22 +23,14 @@ namespace VRM
|
|||
m_key = key;
|
||||
}
|
||||
|
||||
public void Slider()
|
||||
public KeyValuePair<BlendShapeKey, float> Slider()
|
||||
{
|
||||
if (m_target.BlendShapeAvatar == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldValue = m_target.GetValue(m_key);
|
||||
var enable = GUI.enabled;
|
||||
GUI.enabled = Application.isPlaying;
|
||||
var newValue = EditorGUILayout.Slider(m_key.ToString(), oldValue, 0, 1.0f);
|
||||
GUI.enabled = enable;
|
||||
if (Application.isPlaying && oldValue != newValue)
|
||||
{
|
||||
m_target.SetValue(m_key, newValue);
|
||||
}
|
||||
return new KeyValuePair<BlendShapeKey, float>(m_key, newValue);
|
||||
}
|
||||
}
|
||||
List<BlendShapeSlider> m_sliders;
|
||||
|
|
@ -65,12 +57,14 @@ namespace VRM
|
|||
EditorGUILayout.HelpBox("Enable when playing", MessageType.Info);
|
||||
}
|
||||
|
||||
if (m_target.BlendShapeAvatar == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_sliders != null)
|
||||
{
|
||||
foreach (var slider in m_sliders)
|
||||
{
|
||||
slider.Slider();
|
||||
}
|
||||
m_target.SetValues(m_sliders.Select(x => x.Slider()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
194
Scripts/BlendShape/MaterialValueBindingMerger.cs
Normal file
194
Scripts/BlendShape/MaterialValueBindingMerger.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UniGLTF;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
///
|
||||
/// Base + (A.Target - Base) * A.Weight + (B.Target - Base) * B.Weight ...
|
||||
///
|
||||
class MaterialValueBindingMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// 名前とmaterialのマッピング
|
||||
/// </summary>
|
||||
Dictionary<string, Material> m_materialMap = new Dictionary<string, Material>();
|
||||
|
||||
delegate void Setter(float value, bool firstValue);
|
||||
|
||||
/// <summary>
|
||||
/// MaterialValueの適用値を蓄積する
|
||||
/// </summary>
|
||||
/// <typeparam name="MaterialValueBinding"></typeparam>
|
||||
/// <typeparam name="float"></typeparam>
|
||||
/// <returns></returns>
|
||||
Dictionary<MaterialValueBinding, float> m_materialValueMap = new Dictionary<MaterialValueBinding, float>();
|
||||
|
||||
Dictionary<MaterialValueBinding, Setter> m_materialSetterMap = new Dictionary<MaterialValueBinding, Setter>();
|
||||
|
||||
public MaterialValueBindingMerger(Dictionary<BlendShapeKey, BlendShapeClip> clipMap, Transform root)
|
||||
{
|
||||
foreach (var x in root.Traverse())
|
||||
{
|
||||
var renderer = x.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
foreach (var y in renderer.sharedMaterials.Where(y => y != null))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(y.name))
|
||||
{
|
||||
if (!m_materialMap.ContainsKey(y.name))
|
||||
{
|
||||
m_materialMap.Add(y.name, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in clipMap)
|
||||
{
|
||||
foreach (var binding in kv.Value.MaterialValues)
|
||||
{
|
||||
if (!m_materialSetterMap.ContainsKey(binding))
|
||||
{
|
||||
Material target;
|
||||
if (m_materialMap.TryGetValue(binding.MaterialName, out target))
|
||||
{
|
||||
if (binding.ValueName.EndsWith("_ST_S"))
|
||||
{
|
||||
var valueName = binding.ValueName.Substring(0, binding.ValueName.Length - 2);
|
||||
Setter setter = (value, firstValue) =>
|
||||
{
|
||||
var propValue = firstValue
|
||||
? (binding.BaseValue + (binding.TargetValue - binding.BaseValue) * value)
|
||||
: (target.GetVector(binding.ValueName) + (binding.TargetValue - binding.BaseValue) * value)
|
||||
;
|
||||
var src = target.GetVector(valueName);
|
||||
src.x = propValue.x; // horizontal only
|
||||
src.z = propValue.z; // horizontal only
|
||||
target.SetVector(valueName, src);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, setter);
|
||||
}
|
||||
else if (binding.ValueName.EndsWith("_ST_T"))
|
||||
{
|
||||
var valueName = binding.ValueName.Substring(0, binding.ValueName.Length - 2);
|
||||
Setter setter = (value, firstValue) =>
|
||||
{
|
||||
var propValue = firstValue
|
||||
? (binding.BaseValue + (binding.TargetValue - binding.BaseValue) * value)
|
||||
: (target.GetVector(binding.ValueName) + (binding.TargetValue - binding.BaseValue) * value)
|
||||
;
|
||||
var src = target.GetVector(valueName);
|
||||
src.y = propValue.y; // vertical only
|
||||
src.w = propValue.w; // vertical only
|
||||
target.SetVector(valueName, src);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, setter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Setter vec4Setter = (value, firstValue) =>
|
||||
{
|
||||
var propValue = firstValue
|
||||
? (binding.BaseValue + (binding.TargetValue - binding.BaseValue) * value)
|
||||
: (target.GetVector(binding.ValueName) + (binding.TargetValue - binding.BaseValue) * value)
|
||||
;
|
||||
target.SetColor(binding.ValueName, propValue);
|
||||
};
|
||||
m_materialSetterMap.Add(binding, vec4Setter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("material: {0} not found", binding.MaterialName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RestoreMaterialInitialValues(IEnumerable<BlendShapeClip> clips)
|
||||
{
|
||||
if (m_materialMap != null)
|
||||
{
|
||||
foreach (var x in clips)
|
||||
{
|
||||
foreach (var y in x.MaterialValues)
|
||||
{
|
||||
// restore values
|
||||
Material material;
|
||||
if (m_materialMap.TryGetValue(y.MaterialName, out material))
|
||||
{
|
||||
material.SetColor(y.ValueName, y.BaseValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("{0} not found", y.MaterialName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ImmediatelySetValue(BlendShapeClip clip, float value)
|
||||
{
|
||||
foreach (var binding in clip.MaterialValues)
|
||||
{
|
||||
Setter setter;
|
||||
if (m_materialSetterMap.TryGetValue(binding, out setter))
|
||||
{
|
||||
setter(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AccumulateValue(BlendShapeClip clip, float value)
|
||||
{
|
||||
foreach (var binding in clip.MaterialValues)
|
||||
{
|
||||
// 積算
|
||||
float acc;
|
||||
if (m_materialValueMap.TryGetValue(binding, out acc))
|
||||
{
|
||||
m_materialValueMap[binding] = acc + value;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_materialValueMap[binding] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, int> m_used = new Dictionary<string, int>();
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
m_used.Clear();
|
||||
|
||||
// (binding.Value-Base) * weight を足す
|
||||
foreach (var kv in m_materialValueMap)
|
||||
{
|
||||
Setter setter;
|
||||
if (m_materialSetterMap.TryGetValue(kv.Key, out setter))
|
||||
{
|
||||
int count;
|
||||
if (m_used.TryGetValue(kv.Key.MaterialName, out count))
|
||||
{
|
||||
m_used[kv.Key.MaterialName] += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_used.Add(kv.Key.MaterialName, 1);
|
||||
}
|
||||
|
||||
setter(kv.Value, count == 0);
|
||||
}
|
||||
}
|
||||
m_materialValueMap.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Scripts/BlendShape/MaterialValueBindingMerger.cs.meta
Normal file
12
Scripts/BlendShape/MaterialValueBindingMerger.cs.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ccc88d77d5d1e74499d053083ade08d
|
||||
timeCreated: 1541229189
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -50,6 +50,7 @@ namespace VRM
|
|||
switch (propType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
// 色
|
||||
item.PropMap.Add(name, new PropItem
|
||||
{
|
||||
PropertyType = propType,
|
||||
|
|
@ -59,13 +60,35 @@ namespace VRM
|
|||
break;
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||
name += "_ST";
|
||||
item.PropMap.Add(name, new PropItem
|
||||
// テクスチャ
|
||||
{
|
||||
PropertyType = propType,
|
||||
DefaultValues = material.GetVector(name),
|
||||
});
|
||||
propNames.Add(name);
|
||||
name += "_ST";
|
||||
item.PropMap.Add(name, new PropItem
|
||||
{
|
||||
PropertyType = propType,
|
||||
DefaultValues = material.GetVector(name),
|
||||
});
|
||||
propNames.Add(name);
|
||||
}
|
||||
// 縦横分離用
|
||||
{
|
||||
var st_name = name + "_S";
|
||||
item.PropMap.Add(st_name, new PropItem
|
||||
{
|
||||
PropertyType = propType,
|
||||
DefaultValues = material.GetVector(name),
|
||||
});
|
||||
propNames.Add(st_name);
|
||||
}
|
||||
{
|
||||
var st_name = name + "_T";
|
||||
item.PropMap.Add(st_name, new PropItem
|
||||
{
|
||||
PropertyType = propType,
|
||||
DefaultValues = material.GetVector(name),
|
||||
});
|
||||
propNames.Add(st_name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +153,7 @@ namespace VRM
|
|||
Materials = materials;
|
||||
}
|
||||
|
||||
public void Bake(BlendShapeBinding[] values, float weight)
|
||||
public void Bake(IEnumerable<BlendShapeBinding> values, float weight)
|
||||
{
|
||||
if (SkinnedMeshRenderer == null) return;
|
||||
|
||||
|
|
@ -147,15 +170,15 @@ namespace VRM
|
|||
{
|
||||
if (x.RelativePath == Path)
|
||||
{
|
||||
if(x.Index>=0 && x.Index < SkinnedMeshRenderer.sharedMesh.blendShapeCount)
|
||||
if (x.Index >= 0 && x.Index < SkinnedMeshRenderer.sharedMesh.blendShapeCount)
|
||||
{
|
||||
SkinnedMeshRenderer.SetBlendShapeWeight(x.Index, x.Weight * weight);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarningFormat("Out of range {0}: 0 <= {1} < {2}",
|
||||
SkinnedMeshRenderer.name,
|
||||
x.Index,
|
||||
Debug.LogWarningFormat("Out of range {0}: 0 <= {1} < {2}",
|
||||
SkinnedMeshRenderer.name,
|
||||
x.Index,
|
||||
SkinnedMeshRenderer.sharedMesh.blendShapeCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ namespace VRM
|
|||
|
||||
private void Initialize(GameObject prefab)
|
||||
{
|
||||
//Debug.LogFormat("[PreviewSceneManager.Initialize] {0}", prefab);
|
||||
Prefab = prefab;
|
||||
|
||||
var materialNames = new List<string>();
|
||||
|
|
@ -90,7 +91,7 @@ namespace VRM
|
|||
if (string.IsNullOrEmpty(src.name)) return null; // !
|
||||
|
||||
Material dst;
|
||||
if(!map.TryGetValue(src, out dst))
|
||||
if (!map.TryGetValue(src, out dst))
|
||||
{
|
||||
dst = new Material(src);
|
||||
map.Add(src, dst);
|
||||
|
|
@ -111,7 +112,7 @@ namespace VRM
|
|||
|
||||
m_blendShapeMeshes = m_meshes
|
||||
.Where(x => x.SkinnedMeshRenderer != null
|
||||
&& x.SkinnedMeshRenderer.sharedMesh.blendShapeCount>0)
|
||||
&& x.SkinnedMeshRenderer.sharedMesh.blendShapeCount > 0)
|
||||
.ToArray();
|
||||
|
||||
//Bake(values, materialValues);
|
||||
|
|
@ -183,7 +184,8 @@ namespace VRM
|
|||
public MaterialItem GetMaterialItem(string materialName)
|
||||
{
|
||||
MaterialItem item;
|
||||
if(!m_materialMap.TryGetValue(materialName, out item)){
|
||||
if (!m_materialMap.TryGetValue(materialName, out item))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -204,22 +206,34 @@ namespace VRM
|
|||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Bounds m_bounds;
|
||||
public void Bake(BlendShapeBinding[] values=null, MaterialValueBinding[] materialValues=null, float weight=1.0f)
|
||||
|
||||
public struct BakeValue
|
||||
{
|
||||
//Debug.LogFormat("Bake");
|
||||
public IEnumerable<BlendShapeBinding> BlendShapeBindings;
|
||||
public IEnumerable<MaterialValueBinding> MaterialValueBindings;
|
||||
public float Weight;
|
||||
}
|
||||
|
||||
Bounds m_bounds;
|
||||
public void Bake(BakeValue bake)
|
||||
{
|
||||
//
|
||||
// Bake BlendShape
|
||||
//
|
||||
m_bounds = default(Bounds);
|
||||
if (m_meshes != null)
|
||||
{
|
||||
foreach (var x in m_meshes)
|
||||
{
|
||||
x.Bake(values, weight);
|
||||
x.Bake(bake.BlendShapeBindings, bake.Weight);
|
||||
m_bounds.Expand(x.Mesh.bounds.size);
|
||||
}
|
||||
}
|
||||
|
||||
// Udpate Material
|
||||
if (materialValues != null && m_materialMap != null)
|
||||
//
|
||||
// Update Material
|
||||
//
|
||||
if (bake.MaterialValueBindings != null && m_materialMap != null)
|
||||
{
|
||||
// clear
|
||||
//Debug.LogFormat("clear material");
|
||||
|
|
@ -231,7 +245,7 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var x in materialValues)
|
||||
foreach (var x in bake.MaterialValueBindings)
|
||||
{
|
||||
MaterialItem item;
|
||||
if (m_materialMap.TryGetValue(x.MaterialName, out item))
|
||||
|
|
@ -240,28 +254,29 @@ namespace VRM
|
|||
PropItem prop;
|
||||
if (item.PropMap.TryGetValue(x.ValueName, out prop))
|
||||
{
|
||||
var value = x.BaseValue + (x.TargetValue - x.BaseValue) * weight;
|
||||
item.Material.SetColor(x.ValueName, value);
|
||||
var valueName = x.ValueName;
|
||||
if (valueName.EndsWith("_ST_S")
|
||||
|| valueName.EndsWith("_ST_T"))
|
||||
{
|
||||
valueName = valueName.Substring(0, valueName.Length - 2);
|
||||
}
|
||||
|
||||
var value = item.Material.GetVector(valueName);
|
||||
Debug.LogFormat("{0} => {1}", valueName, x.TargetValue);
|
||||
value += ((x.TargetValue - x.BaseValue) * bake.Weight);
|
||||
item.Material.SetColor(valueName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
int PreviewLayer
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// カメラパラメーターを決める
|
||||
/// </summary>
|
||||
/// <param name="camera"></param>
|
||||
public void SetupCamera(Camera camera, Vector3 target, float yaw, float pitch, float distance)
|
||||
public void SetupCamera(Camera camera, Vector3 target, float yaw, float pitch, Vector3 position)
|
||||
{
|
||||
camera.backgroundColor = Color.gray;
|
||||
camera.clearFlags = CameraClearFlags.Color;
|
||||
|
|
@ -273,16 +288,16 @@ namespace VRM
|
|||
|
||||
camera.fieldOfView = 27f;
|
||||
camera.nearClipPlane = 0.3f;
|
||||
camera.farClipPlane = distance /*+ magnitude*/ * 2.1f;
|
||||
camera.farClipPlane = -position.z /*+ magnitude*/ * 2.1f;
|
||||
|
||||
#if false
|
||||
// this used to be "-Vector3.forward * num" but I hardcoded my camera position instead
|
||||
camera.transform.position = new Vector3(0f, 1.4f, distance);
|
||||
camera.transform.rotation = Quaternion.Euler(0, 180f, 0);
|
||||
#else
|
||||
camera.transform.position = target + Quaternion.Euler(pitch, yaw, 0) * Vector3.forward * distance;
|
||||
camera.transform.LookAt(target);
|
||||
#endif
|
||||
var t = Matrix4x4.Translate(position);
|
||||
var r = Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(pitch, yaw, 0), Vector3.one);
|
||||
// 回転してから移動
|
||||
var m = r * t;
|
||||
|
||||
camera.transform.position = target + m.ExtractPosition();
|
||||
camera.transform.rotation = m.ExtractRotation();
|
||||
//camera.transform.LookAt(target);
|
||||
|
||||
//previewLayer のみ表示する
|
||||
//camera.cullingMask = 1 << PreviewLayer;
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Clear all blendShape values
|
||||
/// </summary>
|
||||
|
|
@ -169,6 +170,7 @@ namespace VRM
|
|||
m_merger.Clear();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Apply blendShape values that use SetValue apply=false
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ namespace VRM
|
|||
{
|
||||
asset.BlendShapeName = groupName;
|
||||
asset.Preset = EnumUtil.TryParseOrDefault<BlendShapePreset>(group.presetName);
|
||||
asset.IsBinary = group.isBinary;
|
||||
if (asset.Preset == BlendShapePreset.Unknown)
|
||||
{
|
||||
// fallback
|
||||
|
|
@ -250,7 +251,7 @@ namespace VRM
|
|||
AvatarDescription = GLTF.extensions.VRM.humanoid.ToDescription(Nodes);
|
||||
AvatarDescription.name = "AvatarDescription";
|
||||
HumanoidAvatar = AvatarDescription.CreateAvatar(Root.transform);
|
||||
if(!HumanoidAvatar.isValid || !HumanoidAvatar.isHuman)
|
||||
if (!HumanoidAvatar.isValid || !HumanoidAvatar.isHuman)
|
||||
{
|
||||
throw new Exception("fail to create avatar");
|
||||
}
|
||||
|
|
@ -289,7 +290,7 @@ namespace VRM
|
|||
meta.Title = gltfMeta.title;
|
||||
|
||||
var thumbnail = GetTexture(gltfMeta.texture);
|
||||
if (thumbnail!=null)
|
||||
if (thumbnail != null)
|
||||
{
|
||||
// ロード済み
|
||||
meta.Thumbnail = thumbnail.Texture;
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ namespace VRM
|
|||
[JsonSchema(Description = "Expression name")]
|
||||
public string name;
|
||||
|
||||
[JsonSchema(Description = "Predefined Expression name", EnumValues =new object[] {
|
||||
[JsonSchema(Description = "Predefined Expression name", EnumValues = new object[] {
|
||||
"unknown",
|
||||
"neutral",
|
||||
"a",
|
||||
|
|
@ -119,10 +119,14 @@ namespace VRM
|
|||
[JsonSchema(Description = "Material animation references.")]
|
||||
public List<glTF_VRM_MaterialValueBind> materialValues = new List<glTF_VRM_MaterialValueBind>();
|
||||
|
||||
[JsonSchema(Description = "0 or 1. Do not allow an intermediate value. Value should rounded")]
|
||||
public bool isBinary;
|
||||
|
||||
protected override void SerializeMembers(GLTFJsonFormatter f)
|
||||
{
|
||||
f.KeyValue(() => name);
|
||||
f.KeyValue(() => presetName);
|
||||
f.KeyValue(() => isBinary);
|
||||
f.KeyValue(() => binds);
|
||||
f.KeyValue(() => materialValues);
|
||||
}
|
||||
|
|
@ -157,6 +161,7 @@ namespace VRM
|
|||
{
|
||||
name = clip.BlendShapeName,
|
||||
presetName = clip.Preset.ToString().ToLower(),
|
||||
isBinary = clip.IsBinary,
|
||||
binds = list,
|
||||
materialValues = materialList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ namespace VRM
|
|||
}
|
||||
|
||||
bool m_foldoutInfo = true;
|
||||
bool m_foldoutPersmission=true;
|
||||
bool m_foldoutDistribution=true;
|
||||
bool m_foldoutPersmission = true;
|
||||
bool m_foldoutDistribution = true;
|
||||
void VRMMetaObjectGUI(SerializedObject so)
|
||||
{
|
||||
InitMap(so);
|
||||
if (m_propMap == null || m_propMap.Count==0) return;
|
||||
if (m_propMap == null || m_propMap.Count == 0) return;
|
||||
|
||||
so.Update();
|
||||
|
||||
|
|
@ -95,8 +95,8 @@ namespace VRM
|
|||
EditorGUILayout.PropertyField(m_propMap["Author"]);
|
||||
EditorGUILayout.PropertyField(m_propMap["ContactInformation"]);
|
||||
EditorGUILayout.PropertyField(m_propMap["Reference"]);
|
||||
var thumbnail = m_propMap["Thumbnail"];
|
||||
thumbnail.objectReferenceValue = TextureField("", (Texture2D)thumbnail.objectReferenceValue, 100);
|
||||
//var thumbnail = m_propMap["Thumbnail"];
|
||||
//thumbnail.objectReferenceValue = TextureField("", (Texture2D)thumbnail.objectReferenceValue, 100);
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("License ", EditorStyles.boldLabel);
|
||||
|
|
@ -121,7 +121,7 @@ namespace VRM
|
|||
EditorGUILayout.PropertyField(m_propMap["OtherLicenseUrl"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user