mirror of
https://github.com/vrm-c/UniVRM.git
synced 2026-05-24 03:51:23 -05:00
Merge pull request #2187 from ousttrue/fix/integrate_glb_vrm0_vrm1_meshutility
[Mesh統合] glb/vrm-0/vrm-1 の Mesh統合 Utility を整理
This commit is contained in:
commit
07311ece5b
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -54,6 +54,7 @@
|
|||
},
|
||||
"cSpell.words": [
|
||||
"GLTF",
|
||||
"Scriptable",
|
||||
"UNIVRM"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
using UniGLTF.M17N;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// BoneMeshRemover 向けのエディタ。
|
||||
///
|
||||
/// SerializedProperty 経由で ユーザー定義 struct のフィールド
|
||||
/// public List<BoneMeshEraser.EraseBone> _eraseBones;
|
||||
/// を EditorGUILayout.PropertyField するための細工である。
|
||||
///
|
||||
/// SerializedObject は UnityEngine.Object から作成するので、
|
||||
/// UnityEngine.Object を継承したクラスのフィールドに ユーザー定義 struct を配置する。
|
||||
/// 持ち主の SerializedObject を経由して EditorGUILayout.PropertyField してる。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(MeshUtilityDialog), true)]
|
||||
class BoneMeshEraserEditor : Editor
|
||||
{
|
||||
MeshUtilityDialog _targetDialog;
|
||||
SerializedProperty _skinnedMesh;
|
||||
SerializedProperty _eraseBones;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_targetDialog = target as MeshUtilityDialog;
|
||||
if (_targetDialog)
|
||||
{
|
||||
_skinnedMesh = serializedObject.FindProperty(nameof(MeshUtilityDialog._skinnedMeshRenderer));
|
||||
_eraseBones = serializedObject.FindProperty(nameof(MeshUtilityDialog._eraseBones));
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
EditorGUILayout.PropertyField(_skinnedMesh);
|
||||
EditorGUILayout.PropertyField(_eraseBones);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniVRM10
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
class MeshIntegrationTab
|
||||
public class MeshIntegrationTab
|
||||
{
|
||||
bool _modified = false;
|
||||
Vrm10MeshUtility _meshUti;
|
||||
protected GltfMeshUtility _meshUtil;
|
||||
|
||||
Splitter _splitter;
|
||||
ReorderableList _groupList;
|
||||
ReorderableList _rendererList;
|
||||
public List<Renderer> _renderers = new List<Renderer>();
|
||||
int _selected = -1;
|
||||
int Selected
|
||||
protected int _selected = -1;
|
||||
protected int Selected
|
||||
{
|
||||
set
|
||||
{
|
||||
|
|
@ -24,29 +23,29 @@ namespace UniVRM10
|
|||
{
|
||||
return;
|
||||
}
|
||||
if (value < 0 || value >= _meshUti.MeshIntegrationGroups.Count)
|
||||
if (value < 0 || value >= _meshUtil.MeshIntegrationGroups.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_selected = value;
|
||||
_renderers.Clear();
|
||||
_renderers.AddRange(_meshUti.MeshIntegrationGroups[_selected].Renderers);
|
||||
_renderers.AddRange(_meshUtil.MeshIntegrationGroups[_selected].Renderers);
|
||||
}
|
||||
}
|
||||
|
||||
public MeshIntegrationTab(EditorWindow editor, Vrm10MeshUtility meshUtility)
|
||||
public MeshIntegrationTab(EditorWindow editor, GltfMeshUtility meshUtility)
|
||||
{
|
||||
_meshUti = meshUtility;
|
||||
_meshUtil = meshUtility;
|
||||
_splitter = new VerticalSplitter(editor, 200, 50);
|
||||
|
||||
_groupList = new ReorderableList(_meshUti.MeshIntegrationGroups, typeof(MeshIntegrationGroup));
|
||||
_groupList = new ReorderableList(_meshUtil.MeshIntegrationGroups, typeof(MeshIntegrationGroup));
|
||||
_groupList.drawHeaderCallback = (Rect rect) =>
|
||||
{
|
||||
GUI.Label(rect, "Integration group");
|
||||
};
|
||||
_groupList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
var group = _meshUti.MeshIntegrationGroups[index];
|
||||
var group = _meshUtil.MeshIntegrationGroups[index];
|
||||
EditorGUI.TextField(rect, group.Name);
|
||||
};
|
||||
_groupList.onSelectCallback = rl =>
|
||||
|
|
@ -69,8 +68,7 @@ namespace UniVRM10
|
|||
public void UpdateMeshIntegrationList(GameObject root)
|
||||
{
|
||||
_selected = -1;
|
||||
_meshUti.MeshIntegrationGroups.Clear();
|
||||
_meshUti.IntegrateFirstPerson(root);
|
||||
_meshUtil.UpdateMeshIntegrationGroups(root);
|
||||
Selected = 0;
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 07d031dea01a55c43b1ec68cd10bf461
|
||||
guid: 27b74253f485b4b45a8d2847ec3c2b34
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -2,34 +2,16 @@ using UnityEngine;
|
|||
using UnityEditor;
|
||||
using UniGLTF.M17N;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
public class MeshUtilityDialog : EditorWindow
|
||||
{
|
||||
public const string MENU_NAME = "glTF MeshUtility";
|
||||
enum MeshProcessDialogTabs
|
||||
{
|
||||
MeshSeparator,
|
||||
MeshIntegrator,
|
||||
BoneMeshEraser,
|
||||
}
|
||||
MeshProcessDialogTabs _tab;
|
||||
|
||||
private GameObject _exportTarget;
|
||||
|
||||
[SerializeField]
|
||||
public bool _separateByBlendShape = true;
|
||||
|
||||
[SerializeField]
|
||||
public SkinnedMeshRenderer _skinnedMeshRenderer = null;
|
||||
|
||||
[SerializeField]
|
||||
public List<BoneMeshEraser.EraseBone> _eraseBones;
|
||||
|
||||
private BoneMeshEraserEditor _boneMeshEraserEditor;
|
||||
private Vector2 _scrollPos = new Vector2(0, 0);
|
||||
|
||||
protected const string ASSET_SUFFIX = ".mesh.asset";
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var window =
|
||||
|
|
@ -38,82 +20,301 @@ namespace UniGLTF.MeshUtility
|
|||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
protected enum Tabs
|
||||
{
|
||||
if (!_boneMeshEraserEditor)
|
||||
Freeze,
|
||||
IntegrateSplit,
|
||||
}
|
||||
protected Tabs _tab;
|
||||
protected GameObject _exportTarget;
|
||||
|
||||
GltfMeshUtility _meshUtil;
|
||||
protected virtual GltfMeshUtility MeshUtility
|
||||
{
|
||||
get
|
||||
{
|
||||
_boneMeshEraserEditor = (BoneMeshEraserEditor)Editor.CreateEditor(this);
|
||||
if (_meshUtil == null)
|
||||
{
|
||||
_meshUtil = new GltfMeshUtility();
|
||||
}
|
||||
return _meshUtil;
|
||||
}
|
||||
}
|
||||
MeshIntegrationTab _integrationTab;
|
||||
protected virtual MeshIntegrationTab MeshIntegration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_integrationTab == null)
|
||||
{
|
||||
_integrationTab = new MeshIntegrationTab(this, MeshUtility);
|
||||
}
|
||||
return _integrationTab;
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Validation> _validations = new List<Validation>();
|
||||
protected virtual void Validate()
|
||||
{
|
||||
_validations.Clear();
|
||||
if (_exportTarget == null)
|
||||
{
|
||||
_validations.Add(Validation.Error("set target GameObject"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
bool IsValid => !_validations.Any(v => !v.CanExport);
|
||||
|
||||
MeshInfo[] integrationResults;
|
||||
|
||||
Vector2 _scrollPos;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void DialogMessage()
|
||||
{
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.MESH_UTILITY.Msg(), MessageType.Info);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
var modified = false;
|
||||
EditorGUIUtility.labelWidth = 200;
|
||||
LanguageGetter.OnGuiSelectLang();
|
||||
_exportTarget = (GameObject)EditorGUILayout.ObjectField(MeshUtilityMessages.TARGET_OBJECT.Msg(), _exportTarget, typeof(GameObject), true);
|
||||
|
||||
DialogMessage();
|
||||
|
||||
var exportTarget = (GameObject)EditorGUILayout.ObjectField(
|
||||
MeshUtilityMessages.TARGET_OBJECT.Msg(),
|
||||
_exportTarget, typeof(GameObject), true);
|
||||
if (exportTarget != _exportTarget)
|
||||
{
|
||||
_exportTarget = exportTarget;
|
||||
MeshIntegration.UpdateMeshIntegrationList(_exportTarget);
|
||||
modified = true;
|
||||
}
|
||||
if (_exportTarget == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
|
||||
// GameObject or Prefab ?
|
||||
switch (_exportTarget.GetPrefabType())
|
||||
{
|
||||
case UnityExtensions.PrefabType.PrefabAsset:
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.PREFAB_ASSET.Msg(), MessageType.Warning);
|
||||
break;
|
||||
|
||||
case UnityExtensions.PrefabType.PrefabInstance:
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.PREFAB_INSTANCE.Msg(), MessageType.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
// tab bar
|
||||
_tab = TabBar.OnGUI(_tab, "LargeButton", GUI.ToolbarButtonSize.Fixed);
|
||||
|
||||
var processed = false;
|
||||
foreach (var validation in _validations)
|
||||
{
|
||||
validation.DrawGUI();
|
||||
}
|
||||
|
||||
switch (_tab)
|
||||
{
|
||||
case MeshProcessDialogTabs.MeshSeparator:
|
||||
case Tabs.Freeze:
|
||||
{
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.MESH_SEPARATOR.Msg(), MessageType.Info);
|
||||
if (TabMeshSeparator.TryExecutable(_exportTarget, out string msg))
|
||||
if (MeshFreezeGui())
|
||||
{
|
||||
processed = TabMeshSeparator.OnGUI(_exportTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox(msg, MessageType.Error);
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MeshProcessDialogTabs.MeshIntegrator:
|
||||
case Tabs.IntegrateSplit:
|
||||
{
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.MESH_INTEGRATOR.Msg(), MessageType.Info);
|
||||
_separateByBlendShape = EditorGUILayout.Toggle(MeshUtilityMessages.MESH_SEPARATOR_BY_BLENDSHAPE.Msg(), _separateByBlendShape);
|
||||
if (TabMeshIntegrator.TryExecutable(_exportTarget, out string msg))
|
||||
if (MeshIntegrateGui())
|
||||
{
|
||||
if (GUILayout.Button("Process", GUILayout.MinWidth(100)))
|
||||
{
|
||||
processed = TabMeshIntegrator.Execute(_exportTarget, _separateByBlendShape);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox(msg, MessageType.Error);
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MeshProcessDialogTabs.BoneMeshEraser:
|
||||
{
|
||||
EditorGUILayout.HelpBox(MeshUtilityMessages.BONE_MESH_ERASER.Msg(), MessageType.Info);
|
||||
if (_boneMeshEraserEditor)
|
||||
{
|
||||
_boneMeshEraserEditor.OnInspectorGUI();
|
||||
}
|
||||
if (TabBoneMeshRemover.TryExecutable(_exportTarget, _skinnedMeshRenderer, out string msg))
|
||||
{
|
||||
processed = TabBoneMeshRemover.OnGUI(_exportTarget, _skinnedMeshRenderer, _eraseBones);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox(msg, MessageType.Error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO:
|
||||
// Mesh統合のオプション
|
||||
// case Tabs.BoneMeshEraser:
|
||||
// {
|
||||
// // TODO: FirstPerson 処理と統合する
|
||||
// EditorGUILayout.HelpBox(MeshUtilityMessages.BONE_MESH_ERASER.Msg(), MessageType.Info);
|
||||
// // if (_boneMeshEraserEditor)
|
||||
// // {
|
||||
// // _boneMeshEraserEditor.OnInspectorGUI();
|
||||
// // }
|
||||
// // if (TabBoneMeshRemover.TryExecutable(_exportTarget, _skinnedMeshRenderer, out string msg))
|
||||
// // {
|
||||
// // processed = TabBoneMeshRemover.OnGUI(_exportTarget, _skinnedMeshRenderer, _eraseBones);
|
||||
// // }
|
||||
// // else
|
||||
// // {
|
||||
// // EditorGUILayout.HelpBox(msg, MessageType.Error);
|
||||
// // }
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
if (processed)
|
||||
if (modified)
|
||||
{
|
||||
Close();
|
||||
GUIUtility.ExitGUI();
|
||||
Validate();
|
||||
}
|
||||
|
||||
GUI.enabled = IsValid;
|
||||
var pressed = GUILayout.Button("Process", GUILayout.MinWidth(100));
|
||||
GUI.enabled = true;
|
||||
if (pressed)
|
||||
{
|
||||
if (_exportTarget.GetPrefabType() == UnityExtensions.PrefabType.PrefabAsset)
|
||||
{
|
||||
/// [prefab]
|
||||
///
|
||||
/// * prefab から instance を作る
|
||||
/// * instance に対して 焼き付け, 統合, 分離 を実行する
|
||||
/// * instance のヒエラルキーが改変され、mesh 等のアセットは改変版が作成される(元は変わらない)
|
||||
/// * instance を asset に保存してから prefab を削除して終了する
|
||||
///
|
||||
UnityPath assetFolder = default;
|
||||
try
|
||||
{
|
||||
assetFolder = PrefabContext.GetOutFolder(_exportTarget);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
EditorUtility.DisplayDialog("asset folder", "Target folder must be in the Assets or writable Packages folder", "cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
using (var context = new PrefabContext(_exportTarget, assetFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
// prefab が instantiate されていた場合に
|
||||
// Mesh統合設定を instantiate に置き換える
|
||||
var groupCopy = MeshUtility.CopyInstantiate(_exportTarget, context.Instance);
|
||||
|
||||
var (results, created) = MeshUtility.Process(context.Instance, groupCopy);
|
||||
|
||||
// TODO: this should be replaced export and reimport ?
|
||||
WriteAssets(context.AssetFolder, context.Instance, results);
|
||||
WritePrefab(context.AssetFolder, context.Instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.LogException(ex, context.Instance);
|
||||
context.Keep = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var context = new UndoContext("MeshUtility", _exportTarget))
|
||||
{
|
||||
var (results, created) = MeshUtility.Process(_exportTarget, MeshUtility.MeshIntegrationGroups);
|
||||
MeshUtility.Clear(results);
|
||||
|
||||
foreach (var go in created)
|
||||
{
|
||||
// 処理後の mesh をアタッチした Renderer.gameobject
|
||||
Undo.RegisterCreatedObjectUndo(go, "MeshUtility");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Show Result ?
|
||||
_exportTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Mesh
|
||||
/// </summary>
|
||||
protected virtual void WriteAssets(string assetFolder, GameObject instance, List<MeshIntegrationResult> results)
|
||||
{
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Integrated != null)
|
||||
{
|
||||
var childAssetPath = $"{assetFolder}/{result.Integrated.IntegratedRenderer.gameObject.name}{ASSET_SUFFIX}";
|
||||
Debug.LogFormat("CreateAsset: {0}", childAssetPath);
|
||||
AssetDatabase.CreateAsset(result.Integrated.IntegratedRenderer.sharedMesh, childAssetPath);
|
||||
result.Integrated.Reload(childAssetPath);
|
||||
}
|
||||
if (result.IntegratedNoBlendShape != null)
|
||||
{
|
||||
var childAssetPath = $"{assetFolder}/{result.IntegratedNoBlendShape.IntegratedRenderer.gameObject.name}{ASSET_SUFFIX}";
|
||||
Debug.LogFormat("CreateAsset: {0}", childAssetPath);
|
||||
AssetDatabase.CreateAsset(result.Integrated.IntegratedRenderer.sharedMesh, childAssetPath);
|
||||
result.IntegratedNoBlendShape.Reload(childAssetPath);
|
||||
}
|
||||
}
|
||||
|
||||
MeshUtility.Clear(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Prefab
|
||||
/// </summary>
|
||||
protected virtual string WritePrefab(string assetFolder, GameObject instance)
|
||||
{
|
||||
var prefabPath = $"{assetFolder}/Integrated.prefab";
|
||||
Debug.Log(prefabPath);
|
||||
PrefabUtility.SaveAsPrefabAsset(instance, prefabPath, out bool success);
|
||||
if (!success)
|
||||
{
|
||||
throw new Exception($"PrefabUtility.SaveAsPrefabAsset: {prefabPath}");
|
||||
}
|
||||
return prefabPath;
|
||||
}
|
||||
|
||||
protected bool ToggleIsModified(string label, ref bool value)
|
||||
{
|
||||
var newValue = EditorGUILayout.Toggle(label, value);
|
||||
if (newValue == value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshFreezeGui()
|
||||
{
|
||||
var blendShape = ToggleIsModified("BlendShape", ref MeshUtility.FreezeBlendShape);
|
||||
var scale = ToggleIsModified("Scale", ref MeshUtility.FreezeScaling);
|
||||
var rotation = ToggleIsModified("Rotation", ref MeshUtility.FreezeRotation);
|
||||
return blendShape || scale || rotation;
|
||||
}
|
||||
|
||||
protected virtual bool MeshIntegrateGui()
|
||||
{
|
||||
var split = ToggleIsModified("Separate by BlendShape", ref MeshUtility.SplitByBlendShape);
|
||||
var p = position;
|
||||
var last = GUILayoutUtility.GetLastRect();
|
||||
var y = last.y + last.height;
|
||||
var rect = new Rect
|
||||
{
|
||||
x = last.x,
|
||||
y = y,
|
||||
width = p.width,
|
||||
height = p.height - y
|
||||
// process button の高さ
|
||||
- 30
|
||||
};
|
||||
var mod = MeshIntegration.OnGui(rect);
|
||||
return split || mod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,61 +8,80 @@ namespace UniGLTF.MeshUtility
|
|||
[LangMsg(Languages.en, "TargetObject")]
|
||||
TARGET_OBJECT,
|
||||
|
||||
[LangMsg(Languages.ja, @"ターゲットオブジェクト下の SkinnedMeshRenderer にアタッチされたメッシュを、 BlendShape の有無で分割します。
|
||||
[LangMsg(Languages.ja, @"凍結 > 統合 > 分割 という一連の処理を実行します。
|
||||
|
||||
* Asset: 新しい Mesh Asset が元と同じフォルダに作成されます。例: Original -> Original_WithBlendShape.mesh & Original_WithoutBlendShape.mesh
|
||||
* Scene: コピーされたヒエラルキーでは、分割された Mesh は BlendShape のある Mesh で置き換えられて、BlendShape の無い Mesh を使った SkinnedMeshRenderer が追加されます。
|
||||
[凍結]
|
||||
- ヒエラルキーの 回転・拡縮を Mesh に焼き付けます。
|
||||
- BlendShape の現状を Mesh に焼き付けます。
|
||||
|
||||
[統合]
|
||||
- ヒエラルキーに含まれる MeshRenderer と SkinnedMeshRenderer をひとつの SkinnedMeshRenderer に統合します。
|
||||
|
||||
[分割]
|
||||
- 統合結果を BlendShape の有無を基準に分割します。
|
||||
|
||||
[Scene と Prefab]
|
||||
Scene と Prefab で挙動が異なります。
|
||||
|
||||
(Scene/Runtime)
|
||||
- 対象のヒエラルキーを変更します。UNDO可能。
|
||||
- Asset の書き出しはしません。Unityを再起動すると、書き出していない Mesh などの Asset が消滅します。
|
||||
|
||||
(Prefab/Editor)
|
||||
- 対象の prefab をシーンにコピーして処理を実行し、生成する Asset を指定されたフォルダに書き出します。
|
||||
- Asset 書き出し後にコピーを削除します。
|
||||
- Undo はありません。
|
||||
")]
|
||||
[LangMsg(Languages.en, @"Separate the mesh attached to the SkinnedMeshRenderer under the target object with or without BlendShape.
|
||||
[LangMsg(Languages.en, @"
|
||||
Separate the mesh attached to the SkinnedMeshRenderer under the target object with or without BlendShape.
|
||||
|
||||
* Asset: A new Mesh Asset will be created in the same folder as the original. Example: Original-> Original_WithBlendShape.mesh & Original_WithoutBlendShape.mesh
|
||||
* Scene: In the copied hierarchy, the split Mesh is replaced with a Mesh that holds the BlendShape, and a SkinnedMeshRenderer with a Mesh without BlendShape is added.
|
||||
")]
|
||||
MESH_SEPARATOR,
|
||||
|
||||
MESH_UTILITY,
|
||||
|
||||
[LangMsg(Languages.ja, "ブレンドシェイプの有無で分割する")]
|
||||
[LangMsg(Languages.en, "Divide by the presence or absence of `blendshape`")]
|
||||
MESH_SEPARATOR_BY_BLENDSHAPE,
|
||||
|
||||
[LangMsg(Languages.ja, @"ターゲットオブジェクト下の SkinnedMeshRenderer または MeshFilter にアタッチされたメッシュを統合します。
|
||||
// [LangMsg(Languages.ja, @"ターゲットオブジェクト下の SkinnedMeshRenderer または MeshFilter にアタッチされたメッシュを統合します。
|
||||
|
||||
* Asset: Assets/MeshIntegrated.mesh が作成されます(上書きされるので注意してください)。
|
||||
* Scene: コピーされたヒエラルキーでは、統合された Mesh は除去されます。新しく MeshIntegrator ノードが追加されます。
|
||||
* VRMではBlendShapeClipの統合など追加の処理が必要です。`VRM0-MeshIntegratorWizard` を使ってください。
|
||||
")]
|
||||
[LangMsg(Languages.en, @"Integrates the attached mesh into the SkinnedMeshRenderer or MeshFilter under the target object.
|
||||
// * Asset: Assets/MeshIntegrated.mesh が作成されます(上書きされるので注意してください)。
|
||||
// * Scene: コピーされたヒエラルキーでは、統合された Mesh は除去されます。新しく MeshIntegrator ノードが追加されます。
|
||||
// * VRMではBlendShapeClipの統合など追加の処理が必要です。`VRM0-MeshIntegratorWizard` を使ってください。
|
||||
// ")]
|
||||
// [LangMsg(Languages.en, @"Integrates the attached mesh into the SkinnedMeshRenderer or MeshFilter under the target object.
|
||||
|
||||
* Asset: Assets/MeshIntegrated.mesh is created (note that it will be overwritten).
|
||||
* Scene: In the copied hierarchy, the integrated mesh is removed. A new MeshIntegrator node is added.
|
||||
* VRM requires additional processing such as BlendShapeClip integration. Use the `VRM0-MeshIntegratorWizard` integration feature.
|
||||
")]
|
||||
MESH_INTEGRATOR,
|
||||
// * Asset: Assets/MeshIntegrated.mesh is created (note that it will be overwritten).
|
||||
// * Scene: In the copied hierarchy, the integrated mesh is removed. A new MeshIntegrator node is added.
|
||||
// * VRM requires additional processing such as BlendShapeClip integration. Use the `VRM0-MeshIntegratorWizard` integration feature.
|
||||
// ")]
|
||||
// MESH_INTEGRATOR,
|
||||
|
||||
// [LangMsg(Languages.ja, "静的メッシュを一つに統合します")]
|
||||
// [LangMsg(Languages.en, "Integrate static meshes into one")]
|
||||
// STATIC_MESH_INTEGRATOR,
|
||||
// // [LangMsg(Languages.ja, "静的メッシュを一つに統合します")]
|
||||
// // [LangMsg(Languages.en, "Integrate static meshes into one")]
|
||||
// // STATIC_MESH_INTEGRATOR,
|
||||
|
||||
[LangMsg(Languages.ja, @"指定された SkinnedMeshRenderer から、指定されたボーンに対する Weight を保持する三角形を除去します。
|
||||
// [LangMsg(Languages.ja, @"指定された SkinnedMeshRenderer から、指定されたボーンに対する Weight を保持する三角形を除去します。
|
||||
|
||||
* Asset: 元の Mesh と同じフォルダに、三角形を除去した Mesh を保存します。
|
||||
* Scene: コピーされたヒエラルキーでは、三角形が除去された Mesh に差し替えられます。
|
||||
")]
|
||||
[LangMsg(Languages.en, @"Removes the triangle that holds the weight for the specified bone from the specified SkinnedMeshRenderer.
|
||||
// * Asset: 元の Mesh と同じフォルダに、三角形を除去した Mesh を保存します。
|
||||
// * Scene: コピーされたヒエラルキーでは、三角形が除去された Mesh に差し替えられます。
|
||||
// ")]
|
||||
// [LangMsg(Languages.en, @"Removes the triangle that holds the weight for the specified bone from the specified SkinnedMeshRenderer.
|
||||
|
||||
* Assets: Save the mesh with the triangles removed in the same folder as the original mesh.
|
||||
* Scene: In the copied hierarchy, it will be replaced with a Mesh with the triangles removed.
|
||||
")]
|
||||
// * Assets: Save the mesh with the triangles removed in the same folder as the original mesh.
|
||||
// * Scene: In the copied hierarchy, it will be replaced with a Mesh with the triangles removed.
|
||||
// ")]
|
||||
|
||||
BONE_MESH_ERASER,
|
||||
// BONE_MESH_ERASER,
|
||||
|
||||
[LangMsg(Languages.ja, "Skinned Meshを選んでください")]
|
||||
[LangMsg(Languages.en, "Select a skinned mesh")]
|
||||
SELECT_SKINNED_MESH,
|
||||
|
||||
[LangMsg(Languages.ja, "Erase Rootを選んでください")]
|
||||
[LangMsg(Languages.en, "Select a erase root")]
|
||||
SELECT_ERASE_ROOT,
|
||||
// [LangMsg(Languages.ja, "Erase Rootを選んでください")]
|
||||
// [LangMsg(Languages.en, "Select a erase root")]
|
||||
// SELECT_ERASE_ROOT,
|
||||
|
||||
[LangMsg(Languages.ja, "GameObjectを選んでください")]
|
||||
[LangMsg(Languages.en, "Select a GameObject first")]
|
||||
|
|
@ -72,9 +91,9 @@ namespace UniGLTF.MeshUtility
|
|||
[LangMsg(Languages.en, "No skinned mesh is contained")]
|
||||
NO_SKINNED_MESH,
|
||||
|
||||
[LangMsg(Languages.ja, "GameObjectに静的メッシュが含まれていません")]
|
||||
[LangMsg(Languages.en, "No static mesh is contained")]
|
||||
NO_STATIC_MESH,
|
||||
// [LangMsg(Languages.ja, "GameObjectに静的メッシュが含まれていません")]
|
||||
// [LangMsg(Languages.en, "No static mesh is contained")]
|
||||
// NO_STATIC_MESH,
|
||||
|
||||
[LangMsg(Languages.ja, "GameObjectにスキンメッシュ・静的メッシュが含まれていません")]
|
||||
[LangMsg(Languages.en, "Skinned/Static mesh is not contained")]
|
||||
|
|
@ -83,5 +102,13 @@ namespace UniGLTF.MeshUtility
|
|||
[LangMsg(Languages.ja, "BlendShapeClipが不整合を起こすので、`VRM0-> MeshIntegrator`を使ってください")]
|
||||
[LangMsg(Languages.en, "Because BlendShapeClip causes inconsistency , use `VRM0 -> MeshIntegrator` instead")]
|
||||
VRM_DETECTED,
|
||||
|
||||
[LangMsg(Languages.ja, "対象は, Prefab Asset です。実行時に書き出しファイルの指定があります。")]
|
||||
[LangMsg(Languages.en, "The target is prefab asset. A temporary file is specified during execution.")]
|
||||
PREFAB_ASSET,
|
||||
|
||||
[LangMsg(Languages.ja, "対象は, Prefab Instance です。Unpack されます。")]
|
||||
[LangMsg(Languages.en, "The target is prefab asset. A temporary file is specified during execution.")]
|
||||
PREFAB_INSTANCE,
|
||||
}
|
||||
}
|
||||
63
Assets/UniGLTF/Editor/MeshUtility/PrefabContext.cs
Normal file
63
Assets/UniGLTF/Editor/MeshUtility/PrefabContext.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
// Instantiate
|
||||
class PrefabContext : IDisposable
|
||||
{
|
||||
public readonly GameObject Instance;
|
||||
|
||||
readonly UnityPath _assetFolder;
|
||||
|
||||
public bool Keep = false;
|
||||
|
||||
public string AssetFolder => _assetFolder.Value;
|
||||
|
||||
public PrefabContext(GameObject prefab, UnityPath assetFolder)
|
||||
{
|
||||
this._assetFolder = assetFolder;
|
||||
this.Instance = GameObject.Instantiate(prefab);
|
||||
if (PrefabUtility.IsOutermostPrefabInstanceRoot(this.Instance))
|
||||
{
|
||||
// どういう条件でここに来るかはよくわからない
|
||||
PrefabUtility.UnpackPrefabInstance(this.Instance, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||
}
|
||||
}
|
||||
|
||||
// - Instance を Asset に書き出す
|
||||
// - Instance を削除する
|
||||
public void Dispose()
|
||||
{
|
||||
if (Keep)
|
||||
{
|
||||
// for debug
|
||||
return;
|
||||
}
|
||||
UnityEngine.Object.DestroyImmediate(Instance);
|
||||
}
|
||||
|
||||
public static UnityPath GetOutFolder(GameObject _exportTarget)
|
||||
{
|
||||
// 出力フォルダを決める
|
||||
var folder = "Assets";
|
||||
var prefab = _exportTarget.GetPrefab();
|
||||
if (prefab != null)
|
||||
{
|
||||
folder = AssetDatabase.GetAssetPath(prefab);
|
||||
// Debug.Log(folder);
|
||||
}
|
||||
|
||||
// 新規で作成されるアセットはすべてこのフォルダの中に作る。上書きチェックはしない
|
||||
var assetFolder = EditorUtility.SaveFolderPanel("select asset save folder", Path.GetDirectoryName(folder), "Integrated");
|
||||
var unityPath = UniGLTF.UnityPath.FromFullpath(assetFolder);
|
||||
if (!unityPath.IsUnderWritableFolder)
|
||||
{
|
||||
throw new Exception("not in asset folder");
|
||||
}
|
||||
return unityPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fb47e24fc1463584fa0b6b685d75f25e
|
||||
guid: 948458ced6bcc904781eed04ebe7cd01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -2,7 +2,7 @@ using UnityEngine;
|
|||
using UnityEditor;
|
||||
using System;
|
||||
|
||||
namespace UniVRM10
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
[Serializable]
|
||||
public abstract class Splitter
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 385274edf555ed84a9f3706ca2a99023
|
||||
guid: dbb5f5bceb86592499a56fc18011553e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UniGLTF.M17N;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
public static class TabMeshIntegrator
|
||||
{
|
||||
public static bool TryExecutable(GameObject root, out string msg)
|
||||
{
|
||||
// check
|
||||
if (root == null)
|
||||
{
|
||||
msg = MeshUtilityMessages.NO_GAMEOBJECT_SELECTED.Msg();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasVrm(root))
|
||||
{
|
||||
msg = MeshUtilityMessages.VRM_DETECTED.Msg();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.GetComponentsInChildren<SkinnedMeshRenderer>().Length == 0 && root.GetComponentsInChildren<MeshFilter>().Length == 0)
|
||||
{
|
||||
msg = MeshUtilityMessages.NO_MESH.Msg();
|
||||
return false;
|
||||
}
|
||||
|
||||
msg = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
const string VRM_META = "VRMMeta";
|
||||
static bool HasVrm(GameObject root)
|
||||
{
|
||||
var allComponents = root.GetComponents(typeof(Component));
|
||||
foreach (var component in allComponents)
|
||||
{
|
||||
if (component == null) continue;
|
||||
var sourceString = component.ToString();
|
||||
if (sourceString.Contains(VRM_META))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const string ASSET_SUFFIX = ".mesh.asset";
|
||||
|
||||
static string GetMeshWritePath(Mesh mesh)
|
||||
{
|
||||
if (!string.IsNullOrEmpty((AssetDatabase.GetAssetPath(mesh))))
|
||||
{
|
||||
var directory = Path.GetDirectoryName(AssetDatabase.GetAssetPath(mesh)).Replace("\\", "/");
|
||||
return $"{directory}/{Path.GetFileNameWithoutExtension(mesh.name)}{ASSET_SUFFIX}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Assets/{Path.GetFileNameWithoutExtension(mesh.name)}{ASSET_SUFFIX}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="src">GameObject instance in scene or prefab</param>
|
||||
public static bool Execute(GameObject src, bool onlyBlendShapeRenderers)
|
||||
{
|
||||
var results = new List<MeshIntegrationResult>();
|
||||
|
||||
// instance or prefab => copy
|
||||
var copy = GameObject.Instantiate(src);
|
||||
copy.name = copy.name + "_mesh_integration";
|
||||
|
||||
// integrate
|
||||
if (onlyBlendShapeRenderers)
|
||||
{
|
||||
results.Add(MeshIntegratorUtility.Integrate(copy, onlyBlendShapeRenderers: MeshEnumerateOption.OnlyWithBlendShape));
|
||||
results.Add(MeshIntegratorUtility.Integrate(copy, onlyBlendShapeRenderers: MeshEnumerateOption.OnlyWithoutBlendShape));
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(MeshIntegratorUtility.Integrate(copy, onlyBlendShapeRenderers: MeshEnumerateOption.All));
|
||||
}
|
||||
|
||||
// replace
|
||||
MeshIntegratorUtility.ReplaceMeshWithResults(copy, results);
|
||||
|
||||
// write mesh asset.
|
||||
foreach (var result in results)
|
||||
{
|
||||
var mesh = result.Integrated.Mesh;
|
||||
var assetPath = GetMeshWritePath(mesh);
|
||||
Debug.LogFormat("CreateAsset: {0}", assetPath);
|
||||
AssetDatabase.CreateAsset(mesh, assetPath);
|
||||
}
|
||||
|
||||
if (src.GetGameObjectType() == GameObjectType.AssetPrefab)
|
||||
{
|
||||
// write prefab.
|
||||
{
|
||||
var prefabPath = UnityPath.FromAsset(src);
|
||||
prefabPath = prefabPath.Parent.Child($"{prefabPath.FileNameWithoutExtension}_integrated.prefab");
|
||||
Debug.LogFormat("WritePrefab: {0}", prefabPath);
|
||||
PrefabUtility.SaveAsPrefabAsset(copy, prefabPath.Value);
|
||||
}
|
||||
|
||||
// destroy copy in scene.
|
||||
GameObject.DestroyImmediate(copy);
|
||||
}
|
||||
else
|
||||
{
|
||||
// do nothing. keep copy.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Assets/UniGLTF/Editor/MeshUtility/UndoContext.cs
Normal file
27
Assets/UniGLTF/Editor/MeshUtility/UndoContext.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
// Instantiate
|
||||
class UndoContext : IDisposable
|
||||
{
|
||||
public UndoContext(string undoName, GameObject go)
|
||||
{
|
||||
Undo.RegisterFullObjectHierarchyUndo(go, undoName);
|
||||
if (go.GetPrefabType() == UnityExtensions.PrefabType.PrefabInstance)
|
||||
{
|
||||
PrefabUtility.UnpackPrefabInstance(go, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 特に何もしない
|
||||
// Undo すると元に戻ってしまう
|
||||
|
||||
// TODO: あれば一時オブジェクトの破棄
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2021859c0d7255643bd93c95ab3fcf3d
|
||||
guid: 5019966789c0e094a8f58fc2734c9a35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace UniGLTF
|
||||
{
|
||||
|
|
@ -25,19 +27,33 @@ namespace UniGLTF
|
|||
private static void ImportGltfFile() => GltfImportMenu.ImportGltfFileToGameObject();
|
||||
|
||||
|
||||
[MenuItem(UserGltfMenuPrefix + "/" + MeshUtility.MeshUtilityDialog.MENU_NAME, priority = 3)]
|
||||
[MenuItem(UserGltfMenuPrefix + "/" + MeshUtility.MeshUtilityDialog.MENU_NAME, priority = 31)]
|
||||
private static void OpenMeshProcessingWindow() => MeshUtility.MeshUtilityDialog.OpenWindow();
|
||||
|
||||
#if VRM_DEVELOP
|
||||
[MenuItem(DevelopmentMenuPrefix + "/Generate Serialization Code", priority = 40)]
|
||||
[MenuItem(DevelopmentMenuPrefix + "/Generate Serialization Code", priority = 51)]
|
||||
private static void GenerateSerializationCode()
|
||||
{
|
||||
SerializerGenerator.GenerateSerializer();
|
||||
DeserializerGenerator.GenerateSerializer();
|
||||
}
|
||||
|
||||
[MenuItem(DevelopmentMenuPrefix + "/Generate UniJSON ConcreteCast", priority = 41)]
|
||||
[MenuItem(DevelopmentMenuPrefix + "/Generate UniJSON ConcreteCast", priority = 52)]
|
||||
private static void GenerateUniJsonConcreteCastCode() => UniJSON.ConcreteCast.GenerateGenericCast();
|
||||
|
||||
[MenuItem("GameObject/CheckPrefabType", false, 53)]
|
||||
[MenuItem("Assets/CheckPrefabType", false, 53)]
|
||||
private static void CheckPrefabType()
|
||||
{
|
||||
if (Selection.activeObject is GameObject go)
|
||||
{
|
||||
Debug.Log(go.GetPrefabType());
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(Selection.activeContext.GetType());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,5 +449,40 @@ namespace UniGLTF
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum PrefabType
|
||||
{
|
||||
PrefabAsset,
|
||||
PrefabInstance,
|
||||
NotPrefab,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene と Prefab で挙動をスイッチする。
|
||||
///
|
||||
/// - Scene: ヒエラルキーを操作する。Asset の 書き出しはしない。UNDO はする。TODO: 明示的な Asset の書き出し。
|
||||
/// - Prefab: 対象をコピーして処理する。Undo は実装しない。結果を Asset として書き出し、処理後にコピーは削除する。
|
||||
///
|
||||
/// </summary>
|
||||
public static PrefabType GetPrefabType(this GameObject go)
|
||||
{
|
||||
if (go == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (!go.scene.IsValid())
|
||||
{
|
||||
return PrefabType.PrefabAsset;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (PrefabUtility.GetOutermostPrefabInstanceRoot(go) != null)
|
||||
{
|
||||
return PrefabType.PrefabInstance;
|
||||
}
|
||||
#endif
|
||||
|
||||
return PrefabType.NotPrefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniGLTF.Utils;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
|
|
@ -8,170 +9,98 @@ namespace UniGLTF.MeshUtility
|
|||
{
|
||||
public static class BoneNormalizer
|
||||
{
|
||||
public static (GameObject, Dictionary<Transform, Transform>) CreateNormalizedHierarchy(GameObject go,
|
||||
bool removeScaling = true,
|
||||
bool removeRotation = true)
|
||||
private static MeshAttachInfo CreateMeshInfo(Transform src, bool freezeRotation)
|
||||
{
|
||||
var boneMap = new Dictionary<Transform, Transform>();
|
||||
var normalized = new GameObject(go.name + "(normalized)");
|
||||
normalized.transform.position = go.transform.position;
|
||||
|
||||
if (removeScaling && removeRotation)
|
||||
// SkinnedMeshRenderer
|
||||
var smr = src.GetComponent<SkinnedMeshRenderer>();
|
||||
var mesh = MeshFreezer.NormalizeSkinnedMesh(smr);
|
||||
if (mesh != null)
|
||||
{
|
||||
RemoveScaleAndRotationRecursive(go.transform, normalized.transform, boneMap);
|
||||
}
|
||||
else if (removeScaling)
|
||||
{
|
||||
RemoveScaleAndRotationRecursive(go.transform, normalized.transform, boneMap);
|
||||
}
|
||||
else if (removeRotation)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
return (normalized, boneMap);
|
||||
}
|
||||
|
||||
static void RemoveScaleRecursive(Transform src, Transform dst, Dictionary<Transform, Transform> boneMap)
|
||||
{
|
||||
boneMap[src] = dst;
|
||||
|
||||
foreach (Transform child in src)
|
||||
{
|
||||
if (child.gameObject.activeSelf)
|
||||
return new MeshAttachInfo
|
||||
{
|
||||
var dstChild = new GameObject(child.name);
|
||||
dstChild.transform.SetParent(dst);
|
||||
dstChild.transform.position = child.position; // copy world position
|
||||
dstChild.transform.rotation = child.localToWorldMatrix.rotation; // copy world rotation
|
||||
// scale is removed
|
||||
RemoveScaleRecursive(child, dstChild.transform, boneMap);
|
||||
Mesh = mesh,
|
||||
Materials = smr.sharedMaterials,
|
||||
Bones = smr.bones,
|
||||
RootBone = smr.rootBone,
|
||||
};
|
||||
}
|
||||
|
||||
// MeshRenderer
|
||||
var mr = src.GetComponent<MeshRenderer>();
|
||||
if (mr != null)
|
||||
{
|
||||
var dstMesh = MeshFreezer.NormalizeNoneSkinnedMesh(mr, freezeRotation);
|
||||
if (dstMesh != null)
|
||||
{
|
||||
return new MeshAttachInfo
|
||||
{
|
||||
Mesh = dstMesh,
|
||||
Materials = mr.sharedMaterials,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void RemoveScaleAndRotationRecursive(Transform src, Transform dst, Dictionary<Transform, Transform> boneMap)
|
||||
{
|
||||
boneMap[src] = dst;
|
||||
|
||||
foreach (Transform child in src)
|
||||
{
|
||||
if (child.gameObject.activeSelf)
|
||||
{
|
||||
var dstChild = new GameObject(child.name);
|
||||
dstChild.transform.SetParent(dst);
|
||||
dstChild.transform.position = child.position; // copy world position
|
||||
|
||||
RemoveScaleAndRotationRecursive(child, dstChild.transform, boneMap);
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 回転とスケールを除去したヒエラルキーのコピーを作成する(MeshをBakeする)
|
||||
/// 各レンダラー(SkinnedMeshRenderer と MeshRenderer)にアタッチされた sharedMesh に対して
|
||||
/// 回転とスケールを除去し、BlendShape の現状を焼き付けた版を作成する(まだ、アタッチしない)
|
||||
/// </summary>
|
||||
/// <param name="go">対象のヒエラルキーのルート</param>
|
||||
/// <param name="bakeCurrentBlendShape">BlendShapeを0クリアするか否か。false の場合 BlendShape の現状を Bake する</param>
|
||||
/// <param name="createAvatar">Avatarを作る関数</param>
|
||||
/// <returns></returns>
|
||||
public static (GameObject, Dictionary<Transform, Transform>) NormalizeHierarchyFreezeMesh(GameObject go,
|
||||
bool removeScaling = true,
|
||||
bool removeRotation = true,
|
||||
bool freezeBlendShape = true
|
||||
)
|
||||
public static Dictionary<Transform, MeshAttachInfo> NormalizeHierarchyFreezeMesh(
|
||||
GameObject go, bool freezeRotation)
|
||||
{
|
||||
//
|
||||
// 正規化されたヒエラルキーを作る
|
||||
//
|
||||
var (normalized, boneMap) = CreateNormalizedHierarchy(go, removeScaling, removeRotation);
|
||||
|
||||
//
|
||||
// 各メッシュから回転・スケールを取り除いてBinding行列を再計算する
|
||||
//
|
||||
var result = new Dictionary<Transform, MeshAttachInfo>();
|
||||
foreach (var src in go.transform.Traverse())
|
||||
{
|
||||
Transform dst;
|
||||
if (!boneMap.TryGetValue(src, out dst))
|
||||
var info = CreateMeshInfo(src, freezeRotation);
|
||||
if (info != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
// SkinnedMeshRenderer
|
||||
var srcRenderer = src.GetComponent<SkinnedMeshRenderer>();
|
||||
var (mesh, dstBones) = MeshFreezer.NormalizeSkinnedMesh(
|
||||
srcRenderer,
|
||||
boneMap,
|
||||
dst.localToWorldMatrix,
|
||||
freezeBlendShape);
|
||||
if (mesh != null)
|
||||
{
|
||||
var dstRenderer = dst.gameObject.AddComponent<SkinnedMeshRenderer>();
|
||||
dstRenderer.sharedMaterials = srcRenderer.sharedMaterials;
|
||||
if (srcRenderer.rootBone != null)
|
||||
{
|
||||
if (boneMap.TryGetValue(srcRenderer.rootBone, out Transform found))
|
||||
{
|
||||
dstRenderer.rootBone = found;
|
||||
}
|
||||
}
|
||||
dstRenderer.bones = dstBones;
|
||||
dstRenderer.sharedMesh = mesh;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// MeshRenderer
|
||||
var srcRenderer = src.GetComponent<MeshRenderer>();
|
||||
if (srcRenderer != null)
|
||||
{
|
||||
var dstMesh = MeshFreezer.NormalizeNoneSkinnedMesh(srcRenderer);
|
||||
if (dstMesh != null)
|
||||
{
|
||||
var dstFilter = dst.gameObject.AddComponent<MeshFilter>();
|
||||
dstFilter.sharedMesh = dstMesh;
|
||||
// Materialをコピー
|
||||
var dstRenderer = dst.gameObject.AddComponent<MeshRenderer>();
|
||||
dstRenderer.sharedMaterials = srcRenderer.sharedMaterials;
|
||||
}
|
||||
}
|
||||
result.Add(src, info);
|
||||
}
|
||||
}
|
||||
|
||||
return (normalized, boneMap);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void WriteBackResult(GameObject go, GameObject normalized, Dictionary<Transform, Transform> boneMap)
|
||||
public static void Replace(GameObject go, Dictionary<Transform, MeshAttachInfo> newMesh,
|
||||
bool FreezeRotation, bool FreezeScaling)
|
||||
{
|
||||
Func<Transform, Transform> getSrc = dst =>
|
||||
var boneMap = go.transform.Traverse().ToDictionary(x => x, x => new EuclideanTransform(x.rotation, x.position));
|
||||
|
||||
// first, update hierarchy
|
||||
foreach (var src in go.transform.Traverse())
|
||||
{
|
||||
foreach (var (k, v) in boneMap)
|
||||
var tr = boneMap[src];
|
||||
if (FreezeScaling)
|
||||
{
|
||||
if (v == dst)
|
||||
{
|
||||
return k;
|
||||
}
|
||||
src.localScale = Vector3.one;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
};
|
||||
foreach (var (src, dst) in boneMap)
|
||||
{
|
||||
src.localPosition = dst.localPosition;
|
||||
src.localRotation = dst.localRotation;
|
||||
src.localScale = dst.localScale;
|
||||
var srcR = src.GetComponent<SkinnedMeshRenderer>();
|
||||
var dstR = dst.GetComponent<SkinnedMeshRenderer>();
|
||||
if (srcR != null && dstR != null)
|
||||
else
|
||||
{
|
||||
srcR.sharedMesh = dstR.sharedMesh;
|
||||
srcR.bones = dstR.bones.Select(x => getSrc(x)).ToArray();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (FreezeRotation)
|
||||
{
|
||||
src.rotation = Quaternion.identity;
|
||||
}
|
||||
else
|
||||
{
|
||||
src.rotation = tr.Rotation;
|
||||
}
|
||||
|
||||
src.position = tr.Translation;
|
||||
}
|
||||
|
||||
// second, replace mesh
|
||||
foreach (var (src, tr) in boneMap)
|
||||
{
|
||||
if (newMesh.TryGetValue(src, out var info))
|
||||
{
|
||||
info.ReplaceMesh(src.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
219
Assets/UniGLTF/Runtime/MeshUtility/GltfMeshUtility.cs
Normal file
219
Assets/UniGLTF/Runtime/MeshUtility/GltfMeshUtility.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// - Freeze
|
||||
/// - Integration
|
||||
/// - Split
|
||||
///
|
||||
/// - Implement runtime logic => Process a hierarchy in scene. Do not process prefab.
|
||||
/// - Implement undo
|
||||
///
|
||||
/// </summary>
|
||||
public class GltfMeshUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeBlendShape = false;
|
||||
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeScaling = true;
|
||||
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeRotation = false;
|
||||
|
||||
public List<MeshIntegrationGroup> MeshIntegrationGroups = new List<MeshIntegrationGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a headless model and solve VRM.FirstPersonFlag.Auto
|
||||
/// </summary>
|
||||
public bool GenerateMeshForFirstPersonAuto = false;
|
||||
|
||||
/// <summary>
|
||||
/// Split into having and not having BlendShape
|
||||
/// </summary>
|
||||
public bool SplitByBlendShape = false;
|
||||
|
||||
protected UniGLTF.MeshUtility.MeshIntegrationGroup _GetOrCreateGroup(string name)
|
||||
{
|
||||
foreach (var g in MeshIntegrationGroups)
|
||||
{
|
||||
if (g.Name == name)
|
||||
{
|
||||
return g;
|
||||
}
|
||||
}
|
||||
MeshIntegrationGroups.Add(new UniGLTF.MeshUtility.MeshIntegrationGroup
|
||||
{
|
||||
Name = name,
|
||||
});
|
||||
return MeshIntegrationGroups.Last();
|
||||
}
|
||||
|
||||
public virtual void UpdateMeshIntegrationGroups(GameObject root)
|
||||
{
|
||||
MeshIntegrationGroups.Clear();
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var group = _GetOrCreateGroup("all mesh");
|
||||
group.Renderers.AddRange(root.GetComponentsInChildren<Renderer>());
|
||||
}
|
||||
|
||||
public void IntegrateAll(GameObject root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
MeshIntegrationGroups.Add(new MeshIntegrationGroup
|
||||
{
|
||||
Name = "ALL",
|
||||
Renderers = root.GetComponentsInChildren<Renderer>().ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
static GameObject GetOrCreateEmpty(GameObject go, string name)
|
||||
{
|
||||
foreach (var child in go.transform.GetChildren())
|
||||
{
|
||||
if (child.name == name
|
||||
&& child.localPosition == Vector3.zero
|
||||
&& child.localScale == Vector3.one
|
||||
&& child.localRotation == Quaternion.identity)
|
||||
{
|
||||
return child.gameObject;
|
||||
}
|
||||
}
|
||||
var empty = new GameObject(name);
|
||||
empty.transform.SetParent(go.transform, false);
|
||||
return empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="go">MeshIntegrationGroup を作ったとき root</param>
|
||||
/// <param name="instance">go が prefab だった場合に instance されたもの</param>
|
||||
/// <returns></returns>
|
||||
public virtual IEnumerable<MeshIntegrationGroup> CopyInstantiate(GameObject go, GameObject instance)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
foreach (var g in MeshIntegrationGroups)
|
||||
{
|
||||
yield return g;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var g in MeshIntegrationGroups)
|
||||
{
|
||||
yield return g.CopyInstantiate(go, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual (List<MeshIntegrationResult>, List<GameObject>) Process(
|
||||
GameObject target, IEnumerable<MeshIntegrationGroup> groupCopy)
|
||||
{
|
||||
if (FreezeBlendShape || FreezeRotation || FreezeScaling)
|
||||
{
|
||||
// MeshをBakeする
|
||||
var newMesh = BoneNormalizer.NormalizeHierarchyFreezeMesh(target, FreezeRotation);
|
||||
|
||||
// - ヒエラルキーから回転・拡縮を除去する
|
||||
// - BakeされたMeshで置き換える
|
||||
// - bindPoses を再計算する
|
||||
BoneNormalizer.Replace(target, newMesh, FreezeRotation, FreezeScaling);
|
||||
}
|
||||
|
||||
var newList = new List<GameObject>();
|
||||
|
||||
var empty = GetOrCreateEmpty(target, "mesh");
|
||||
|
||||
var results = new List<MeshIntegrationResult>();
|
||||
foreach (var group in groupCopy)
|
||||
{
|
||||
if (TryIntegrate(empty, group, out var resultAndAdded))
|
||||
{
|
||||
var (result, newGo) = resultAndAdded;
|
||||
results.Add(result);
|
||||
newList.AddRange(newGo);
|
||||
}
|
||||
}
|
||||
|
||||
return (results, newList);
|
||||
}
|
||||
|
||||
public void Clear(List<MeshIntegrationResult> results)
|
||||
{
|
||||
// 用が済んだ 統合前 の renderer を削除する
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var r in result.SourceMeshRenderers)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(r.gameObject.GetComponent<MeshFilter>());
|
||||
GameObject.Destroy(r);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(r.gameObject.GetComponent<MeshFilter>());
|
||||
GameObject.DestroyImmediate(r);
|
||||
}
|
||||
}
|
||||
foreach (var r in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(r);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(r, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeshIntegrationGroups.Clear();
|
||||
}
|
||||
|
||||
protected virtual bool TryIntegrate(GameObject empty,
|
||||
MeshIntegrationGroup group, out (MeshIntegrationResult, GameObject[]) resultAndAdded)
|
||||
{
|
||||
if (MeshIntegrator.TryIntegrate(group, SplitByBlendShape
|
||||
? MeshIntegrator.BlendShapeOperation.Split
|
||||
: MeshIntegrator.BlendShapeOperation.Use, out var result))
|
||||
{
|
||||
var newGo = result.AddIntegratedRendererTo(empty).ToArray();
|
||||
resultAndAdded = (result, newGo);
|
||||
return true;
|
||||
}
|
||||
|
||||
resultAndAdded = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 65a227dcf3cb5f34085bd6829894fb64
|
||||
guid: e2425cf6ac1f2434986968ff0d4a4755
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
58
Assets/UniGLTF/Runtime/MeshUtility/MeshAttachInfo.cs
Normal file
58
Assets/UniGLTF/Runtime/MeshUtility/MeshAttachInfo.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
public class MeshAttachInfo
|
||||
{
|
||||
public Mesh Mesh;
|
||||
public Material[] Materials;
|
||||
public Transform[] Bones;
|
||||
public Transform RootBone;
|
||||
public void ReplaceMesh(GameObject dst)
|
||||
{
|
||||
if (dst == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (Bones != null)
|
||||
{
|
||||
// recalc bindposes
|
||||
Mesh.bindposes = Bones.Select(x => x.worldToLocalMatrix * dst.transform.localToWorldMatrix).ToArray();
|
||||
|
||||
if (dst.GetComponent<SkinnedMeshRenderer>() is SkinnedMeshRenderer dstRenderer)
|
||||
{
|
||||
dstRenderer.sharedMesh = Mesh;
|
||||
dstRenderer.sharedMaterials = Materials;
|
||||
dstRenderer.bones = Bones;
|
||||
dstRenderer.rootBone = RootBone;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"SkinnedMeshRenderer not found", dst);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dst.GetComponent<MeshFilter>() is MeshFilter dstFilter)
|
||||
{
|
||||
dstFilter.sharedMesh = Mesh;
|
||||
if (dst.gameObject.GetComponent<MeshRenderer>() is MeshRenderer dstRenderer)
|
||||
{
|
||||
dstRenderer.sharedMaterials = Materials;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"MeshRenderer not found", dst);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"MeshFilter not found", dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/UniGLTF/Runtime/MeshUtility/MeshAttachInfo.cs.meta
Normal file
11
Assets/UniGLTF/Runtime/MeshUtility/MeshAttachInfo.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e6eff3bee96e61849ac190387ec28542
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using VRMShaders;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
|
|
@ -126,14 +127,9 @@ namespace UniGLTF.MeshUtility
|
|||
/// </summary>
|
||||
/// <param name="src"></param>
|
||||
/// <param name="boneMap">正規化前のボーンから正規化後のボーンを得る</param>
|
||||
/// <param name="dstLocalToWorldMatrix"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static (Mesh, Transform[]) NormalizeSkinnedMesh(
|
||||
SkinnedMeshRenderer src,
|
||||
Dictionary<Transform, Transform> boneMap,
|
||||
Matrix4x4 dstLocalToWorldMatrix,
|
||||
bool FreezeBlendShape = true)
|
||||
public static Mesh NormalizeSkinnedMesh(SkinnedMeshRenderer src)
|
||||
{
|
||||
if (src == null
|
||||
|| !src.enabled
|
||||
|
|
@ -147,12 +143,6 @@ namespace UniGLTF.MeshUtility
|
|||
var srcMesh = src.sharedMesh;
|
||||
var originalSrcMesh = srcMesh;
|
||||
|
||||
// 元の Transform[] bones から、無効なboneを取り除いて前に詰めた配列を作る
|
||||
var dstBones = src.bones
|
||||
.Where(x => x != null && boneMap.ContainsKey(x))
|
||||
.Select(x => boneMap[x])
|
||||
.ToArray();
|
||||
|
||||
var hasBoneWeight = src.bones != null && src.bones.Length > 0;
|
||||
if (!hasBoneWeight)
|
||||
{
|
||||
|
|
@ -172,34 +162,17 @@ namespace UniGLTF.MeshUtility
|
|||
weight3 = 0.0f,
|
||||
};
|
||||
srcMesh.boneWeights = Enumerable.Range(0, srcMesh.vertexCount).Select(x => bw).ToArray();
|
||||
srcMesh.bindposes = new Matrix4x4[] { Matrix4x4.identity };
|
||||
|
||||
src.rootBone = src.transform;
|
||||
dstBones = new[] { boneMap[src.transform] };
|
||||
src.bones = new[] { src.transform };
|
||||
src.sharedMesh = srcMesh;
|
||||
}
|
||||
|
||||
var blendShapeBackup = new List<float>();
|
||||
if (!FreezeBlendShape)
|
||||
{
|
||||
for (int i = 0; i < srcMesh.blendShapeCount; ++i)
|
||||
{
|
||||
blendShapeBackup.Add(src.GetBlendShapeWeight(i));
|
||||
src.SetBlendShapeWeight(i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// BakeMesh
|
||||
var mesh = srcMesh.Copy(false);
|
||||
mesh.name = srcMesh.name + ".baked";
|
||||
src.BakeMesh(mesh);
|
||||
|
||||
// 新しい骨格のボーンウェイトを作成する
|
||||
mesh.boneWeights = MapBoneWeight(srcMesh.boneWeights, boneMap, src.bones, dstBones);
|
||||
|
||||
// recalc bindposes
|
||||
mesh.bindposes = dstBones.Select(x => x.worldToLocalMatrix * dstLocalToWorldMatrix).ToArray();
|
||||
mesh.boneWeights = srcMesh.boneWeights;
|
||||
|
||||
//var m = src.localToWorldMatrix; // include scaling
|
||||
var m = default(Matrix4x4);
|
||||
|
|
@ -211,11 +184,6 @@ namespace UniGLTF.MeshUtility
|
|||
//
|
||||
CopyBlendShapes(src, srcMesh, mesh, m);
|
||||
|
||||
for (int i = 0; i < blendShapeBackup.Count; ++i)
|
||||
{
|
||||
src.SetBlendShapeWeight(i, blendShapeBackup[i]);
|
||||
}
|
||||
|
||||
if (!hasBoneWeight)
|
||||
{
|
||||
// restore bones
|
||||
|
|
@ -223,7 +191,7 @@ namespace UniGLTF.MeshUtility
|
|||
src.sharedMesh = originalSrcMesh;
|
||||
}
|
||||
|
||||
return (mesh, dstBones);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
private static void CopyBlendShapes(SkinnedMeshRenderer src, Mesh srcMesh, Mesh mesh, Matrix4x4 m)
|
||||
|
|
@ -363,7 +331,7 @@ namespace UniGLTF.MeshUtility
|
|||
}
|
||||
}
|
||||
|
||||
public static Mesh NormalizeNoneSkinnedMesh(MeshRenderer srcRenderer)
|
||||
public static Mesh NormalizeNoneSkinnedMesh(MeshRenderer srcRenderer, bool freezeRotation)
|
||||
{
|
||||
if (srcRenderer == null || !srcRenderer.enabled)
|
||||
{
|
||||
|
|
@ -380,7 +348,15 @@ namespace UniGLTF.MeshUtility
|
|||
|
||||
var dstMesh = srcFilter.sharedMesh.Copy(false);
|
||||
// Meshに乗っているボーンの姿勢を適用する
|
||||
dstMesh.ApplyRotationAndScale(srcRenderer.transform.localToWorldMatrix);
|
||||
if (freezeRotation)
|
||||
{
|
||||
dstMesh.ApplyRotationAndScale(srcRenderer.transform.localToWorldMatrix);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (t, r, s) = srcRenderer.transform.localToWorldMatrix.Decompose();
|
||||
dstMesh.ApplyRotationAndScale(Matrix4x4.TRS(t, Quaternion.identity, s));
|
||||
}
|
||||
return dstMesh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
|
|
@ -8,5 +7,26 @@ namespace UniGLTF.MeshUtility
|
|||
{
|
||||
public string Name;
|
||||
public List<Renderer> Renderers = new List<Renderer>();
|
||||
|
||||
public MeshIntegrationGroup CopyInstantiate(GameObject go, GameObject instance)
|
||||
{
|
||||
var copy = new MeshIntegrationGroup
|
||||
{
|
||||
Name = Name
|
||||
};
|
||||
foreach (var r in Renderers)
|
||||
{
|
||||
var relative = r.transform.RelativePathFrom(go.transform);
|
||||
if (r is SkinnedMeshRenderer smr)
|
||||
{
|
||||
copy.Renderers.Add(instance.transform.GetFromPath(relative).GetComponent<SkinnedMeshRenderer>());
|
||||
}
|
||||
else if (r is MeshRenderer mr)
|
||||
{
|
||||
copy.Renderers.Add(instance.transform.GetFromPath(relative).GetComponent<MeshRenderer>());
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
public struct DrawCount
|
||||
|
|
@ -29,6 +30,15 @@ namespace UniGLTF.MeshUtility
|
|||
smr.bones = bones;
|
||||
IntegratedRenderer = smr;
|
||||
}
|
||||
|
||||
public void Reload(string assetPath)
|
||||
{
|
||||
var unityPath = UnityPath.FromUnityPath(assetPath);
|
||||
unityPath.ImportAsset();
|
||||
var mesh = unityPath.LoadAsset<Mesh>();
|
||||
// replace reloaded
|
||||
IntegratedRenderer.sharedMesh = mesh;
|
||||
}
|
||||
}
|
||||
|
||||
public class MeshIntegrationResult
|
||||
|
|
@ -48,16 +58,24 @@ namespace UniGLTF.MeshUtility
|
|||
|
||||
public IEnumerable<GameObject> AddIntegratedRendererTo(GameObject parent)
|
||||
{
|
||||
int count = 0;
|
||||
if (Integrated != null)
|
||||
{
|
||||
Integrated.AddIntegratedRendererTo(parent, Bones);
|
||||
++count;
|
||||
yield return Integrated.IntegratedRenderer.gameObject;
|
||||
}
|
||||
if (IntegratedNoBlendShape != null)
|
||||
{
|
||||
IntegratedNoBlendShape.AddIntegratedRendererTo(parent, Bones);
|
||||
++count;
|
||||
yield return IntegratedNoBlendShape.IntegratedRenderer.gameObject;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,11 +128,10 @@ namespace UniGLTF.MeshUtility
|
|||
})
|
||||
);
|
||||
|
||||
var self = renderer.transform;
|
||||
var bone = self.parent;
|
||||
var bone = renderer.transform;
|
||||
if (bone == null)
|
||||
{
|
||||
Debug.LogWarningFormat("{0} is root gameobject.", self.name);
|
||||
Debug.LogWarningFormat("{0} is root gameobject.", bone.name);
|
||||
return;
|
||||
}
|
||||
var boneIndex = AddBoneIfUnique(bone);
|
||||
|
|
@ -275,7 +274,8 @@ namespace UniGLTF.MeshUtility
|
|||
return found;
|
||||
}
|
||||
|
||||
public static MeshIntegrationResult Integrate(MeshIntegrationGroup group, BlendShapeOperation op)
|
||||
public static bool TryIntegrate(MeshIntegrationGroup group, BlendShapeOperation op,
|
||||
out MeshIntegrationResult result)
|
||||
{
|
||||
var integrator = new MeshUtility.MeshIntegrator();
|
||||
foreach (var x in group.Renderers)
|
||||
|
|
@ -289,7 +289,15 @@ namespace UniGLTF.MeshUtility
|
|||
integrator.Push(mr);
|
||||
}
|
||||
}
|
||||
return integrator.Integrate(group.Name, op);
|
||||
result = integrator.Integrate(group.Name, op);
|
||||
if (result.Integrated != null || result.IntegratedNoBlendShape != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
delegate bool TriangleFilter(int i0, int i1, int i2);
|
||||
|
|
@ -351,7 +359,7 @@ namespace UniGLTF.MeshUtility
|
|||
return mesh;
|
||||
}
|
||||
|
||||
public MeshIntegrationResult Integrate(string name, BlendShapeOperation op)
|
||||
MeshIntegrationResult Integrate(string name, BlendShapeOperation op)
|
||||
{
|
||||
if (_Bones.Count != _BindPoses.Count)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniGLTF.MeshUtility
|
||||
{
|
||||
public static class MeshIntegratorUtility
|
||||
{
|
||||
public const string INTEGRATED_MESH_WITHOUT_BLENDSHAPE_NAME = "Integrated(WithoutBlendShape)";
|
||||
public const string INTEGRATED_MESH_WITH_BLENDSHAPE_NAME = "Integrated(WithBlendShape)";
|
||||
public const string INTEGRATED_MESH_ALL_NAME = "Integrated(All)";
|
||||
|
||||
/// <summary>
|
||||
/// go を root としたヒエラルキーから Renderer を集めて、統合された Mesh 作成する
|
||||
/// </summary>
|
||||
/// <param name="go"></param>
|
||||
/// <param name="onlyBlendShapeRenderers">
|
||||
/// true: BlendShapeを保持するSkinnedMeshRendererのみ
|
||||
/// false: BlendShapeを保持しないSkinnedMeshRenderer + MeshRenderer
|
||||
/// null: すべてのSkinnedMeshRenderer + MeshRenderer
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public static MeshIntegrationResult Integrate(GameObject go, MeshEnumerateOption onlyBlendShapeRenderers,
|
||||
IEnumerable<Mesh> excludes = null,
|
||||
bool destroyIntegratedRenderer = false)
|
||||
{
|
||||
var exclude = new MeshExclude(excludes);
|
||||
|
||||
var group = new MeshIntegrationGroup();
|
||||
bool useBlendShape = false;
|
||||
|
||||
switch (onlyBlendShapeRenderers)
|
||||
{
|
||||
case MeshEnumerateOption.OnlyWithBlendShape:
|
||||
{
|
||||
group.Name = INTEGRATED_MESH_WITH_BLENDSHAPE_NAME;
|
||||
useBlendShape = true;
|
||||
|
||||
foreach (var x in EnumerateSkinnedMeshRenderer(go.transform, onlyBlendShapeRenderers))
|
||||
{
|
||||
if (exclude.IsExcluded(x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
group.Renderers.Add(x);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MeshEnumerateOption.OnlyWithoutBlendShape:
|
||||
{
|
||||
group.Name = INTEGRATED_MESH_WITHOUT_BLENDSHAPE_NAME;
|
||||
useBlendShape = false;
|
||||
|
||||
foreach (var x in EnumerateSkinnedMeshRenderer(go.transform, onlyBlendShapeRenderers))
|
||||
{
|
||||
if (exclude.IsExcluded(x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
group.Renderers.Add(x);
|
||||
}
|
||||
|
||||
foreach (var x in EnumerateMeshRenderer(go.transform))
|
||||
{
|
||||
if (exclude.IsExcluded(x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
group.Renderers.Add(x);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MeshEnumerateOption.All:
|
||||
{
|
||||
group.Name = INTEGRATED_MESH_ALL_NAME;
|
||||
useBlendShape = true;
|
||||
|
||||
foreach (var x in EnumerateSkinnedMeshRenderer(go.transform, onlyBlendShapeRenderers))
|
||||
{
|
||||
if (exclude.IsExcluded(x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
group.Renderers.Add(x);
|
||||
}
|
||||
|
||||
foreach (var x in EnumerateMeshRenderer(go.transform))
|
||||
{
|
||||
if (exclude.IsExcluded(x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
group.Renderers.Add(x);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return MeshIntegrator.Integrate(group, useBlendShape
|
||||
? MeshIntegrator.BlendShapeOperation.Use
|
||||
: MeshIntegrator.BlendShapeOperation.None);
|
||||
}
|
||||
|
||||
public static IEnumerable<SkinnedMeshRenderer> EnumerateSkinnedMeshRenderer(Transform root, MeshEnumerateOption hasBlendShape)
|
||||
{
|
||||
foreach (var x in Traverse(root))
|
||||
{
|
||||
var renderer = x.GetComponent<SkinnedMeshRenderer>();
|
||||
if (renderer != null &&
|
||||
renderer.gameObject.activeInHierarchy &&
|
||||
renderer.sharedMesh != null &&
|
||||
renderer.enabled)
|
||||
{
|
||||
switch (hasBlendShape)
|
||||
{
|
||||
case MeshEnumerateOption.OnlyWithBlendShape:
|
||||
if (renderer.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
yield return renderer;
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshEnumerateOption.OnlyWithoutBlendShape:
|
||||
if (renderer.sharedMesh.blendShapeCount == 0)
|
||||
{
|
||||
yield return renderer;
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshEnumerateOption.All:
|
||||
{
|
||||
yield return renderer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<MeshRenderer> EnumerateMeshRenderer(Transform root)
|
||||
{
|
||||
foreach (var x in Traverse(root))
|
||||
{
|
||||
var renderer = x.GetComponent<MeshRenderer>();
|
||||
var filter = x.GetComponent<MeshFilter>();
|
||||
|
||||
if (renderer != null &&
|
||||
filter != null &&
|
||||
renderer.gameObject.activeInHierarchy &&
|
||||
filter.sharedMesh != null)
|
||||
{
|
||||
yield return renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Transform> Traverse(Transform parent)
|
||||
{
|
||||
if (parent.gameObject.activeSelf)
|
||||
{
|
||||
yield return parent;
|
||||
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
foreach (var x in Traverse(child))
|
||||
{
|
||||
yield return x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReplaceMeshWithResults(GameObject copy, List<MeshIntegrationResult> results)
|
||||
{
|
||||
// destroy original meshes
|
||||
foreach (var skinnedMesh in copy.GetComponentsInChildren<SkinnedMeshRenderer>())
|
||||
{
|
||||
GameObject.DestroyImmediate(skinnedMesh);
|
||||
}
|
||||
foreach (var normalMesh in copy.GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
if (normalMesh.gameObject.GetComponent<MeshRenderer>())
|
||||
{
|
||||
GameObject.DestroyImmediate(normalMesh.gameObject.GetComponent<MeshRenderer>());
|
||||
}
|
||||
GameObject.DestroyImmediate(normalMesh);
|
||||
}
|
||||
|
||||
// Add integrated
|
||||
foreach (var result in results)
|
||||
{
|
||||
result.AddIntegratedRendererTo(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a982d9d30c0145038245b0214dc2f2e4
|
||||
timeCreated: 1560190306
|
||||
|
|
@ -327,6 +327,7 @@ namespace UniHumanoid
|
|||
|
||||
var avatarDescription = UniHumanoid.AvatarDescription.Create();
|
||||
avatarDescription.SetHumanBones(map);
|
||||
|
||||
var avatar = avatarDescription.CreateAvatar(src.transform);
|
||||
avatar.name = "created";
|
||||
return avatar;
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UniGLTF;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
/// <summary>
|
||||
/// Meshを統合し、統合後のMeshのBlendShapeの変化をVRMのBlendShapeClipに反映する
|
||||
/// </summary>
|
||||
public static class VRMMeshIntegratorUtility
|
||||
{
|
||||
public static List<BlendShapeClip> FollowBlendshapeRendererChange(List<MeshIntegrationResult> results, GameObject root, string assetFolder)
|
||||
{
|
||||
var clips = new List<BlendShapeClip>();
|
||||
var proxy = root.GetComponent<VRMBlendShapeProxy>();
|
||||
if (proxy == null || proxy.BlendShapeAvatar == null)
|
||||
{
|
||||
return clips;
|
||||
}
|
||||
var result = results.FirstOrDefault(x => x.Integrated.Mesh.blendShapeCount > 0);
|
||||
if (result == null)
|
||||
{
|
||||
return clips;
|
||||
}
|
||||
|
||||
var rendererDict = new Dictionary<string, SkinnedMeshRenderer>();
|
||||
foreach (var x in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
rendererDict.Add(x.transform.RelativePathFrom(root.transform), x);
|
||||
}
|
||||
var dstPath = result.Integrated.IntegratedRenderer.transform.RelativePathFrom(root.transform);
|
||||
|
||||
// copy modify and write
|
||||
var clipAssetPathList = new List<string>();
|
||||
var sb = new StringBuilder();
|
||||
foreach (var src in proxy.BlendShapeAvatar.Clips)
|
||||
{
|
||||
if (src == null) continue;
|
||||
|
||||
// copy
|
||||
var copy = ScriptableObject.CreateInstance<BlendShapeClip>();
|
||||
copy.CopyFrom(src);
|
||||
copy.Prefab = null;
|
||||
|
||||
// modify
|
||||
for (var i = 0; i < copy.Values.Length; ++i)
|
||||
{
|
||||
var val = copy.Values[i];
|
||||
if (rendererDict.ContainsKey(val.RelativePath))
|
||||
{
|
||||
var srcRenderer = rendererDict[val.RelativePath];
|
||||
var name = srcRenderer.sharedMesh.GetBlendShapeName(val.Index);
|
||||
var newIndex = result.Integrated.IntegratedRenderer.sharedMesh.GetBlendShapeIndex(name);
|
||||
if (newIndex == -1)
|
||||
{
|
||||
throw new KeyNotFoundException($"blendshape:{name} not found");
|
||||
}
|
||||
|
||||
val.RelativePath = dstPath;
|
||||
val.Index = newIndex;
|
||||
}
|
||||
copy.Values[i] = val;
|
||||
}
|
||||
|
||||
// write
|
||||
var assetPath = $"{assetFolder}/{copy.name}.asset";
|
||||
sb.AppendLine($"write: {assetPath}");
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
|
||||
clipAssetPathList.Add(assetPath);
|
||||
clips.Add(copy);
|
||||
}
|
||||
Debug.Log(sb.ToString());
|
||||
|
||||
{
|
||||
// create blendshape avatar & replace
|
||||
var copy = ScriptableObject.CreateInstance<BlendShapeAvatar>();
|
||||
copy.Clips.AddRange(clips);
|
||||
var assetPath = $"{assetFolder}/blendshape.asset";
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
|
||||
// assign
|
||||
proxy.BlendShapeAvatar = copy;
|
||||
}
|
||||
|
||||
return clips;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
Assets/VRM/Editor/SkinnedMeshUtility/VrmBlendShapeUpdater.cs
Normal file
148
Assets/VRM/Editor/SkinnedMeshUtility/VrmBlendShapeUpdater.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniGLTF;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
/// <summary>
|
||||
/// Meshを統合し、統合後のMeshのBlendShapeの変化をVRMのBlendShapeClipに反映する
|
||||
/// </summary>
|
||||
public class VrmBlendShapeUpdater
|
||||
{
|
||||
// BlendShapeBinding.RelativePath からの逆引き
|
||||
Dictionary<string, List<MeshIntegrationResult>> _rendererPathMap = new();
|
||||
GameObject _root;
|
||||
|
||||
VrmBlendShapeUpdater(GameObject root, List<MeshIntegrationResult> results)
|
||||
{
|
||||
_root = root;
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var x in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
var srcPath = x.transform.RelativePathFrom(root.transform);
|
||||
if (_rendererPathMap.TryGetValue(srcPath, out var value))
|
||||
{
|
||||
value.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = new List<MeshIntegrationResult>();
|
||||
value.Add(result);
|
||||
_rendererPathMap.Add(srcPath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分割されて増える => 増えない BlendShape のある方にいく
|
||||
// 統合されて減る => 名前が同じものが統合される
|
||||
private IEnumerable<BlendShapeBinding> ReplaceBlendShapeBinding(IEnumerable<BlendShapeBinding> values)
|
||||
{
|
||||
var used = new HashSet<BlendShapeBinding>();
|
||||
foreach (var val in values)
|
||||
{
|
||||
if (_rendererPathMap.TryGetValue(val.RelativePath, out var results))
|
||||
{
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Integrated == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var name = result.Integrated.Mesh.GetBlendShapeName(val.Index);
|
||||
var newIndex = result.Integrated.Mesh.GetBlendShapeIndex(name);
|
||||
if (newIndex == -1)
|
||||
{
|
||||
throw new KeyNotFoundException($"blendshape:{name} not found");
|
||||
}
|
||||
|
||||
var dstPath = result.Integrated.IntegratedRenderer.transform.RelativePathFrom(_root.transform);
|
||||
var binding = new BlendShapeBinding
|
||||
{
|
||||
RelativePath = dstPath,
|
||||
Index = newIndex,
|
||||
Weight = val.Weight,
|
||||
};
|
||||
if (used.Contains(binding))
|
||||
{
|
||||
Debug.LogWarning($"duplicated: {binding}");
|
||||
}
|
||||
else
|
||||
{
|
||||
#if VRM_DEVELOP
|
||||
Debug.Log($"{val} >> {binding}");
|
||||
#endif
|
||||
used.Add(binding);
|
||||
yield return binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// skip
|
||||
Debug.LogWarning($"SkinnedMeshRenderer not found: {val.RelativePath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BlendShapeClip> FollowBlendshapeRendererChange(string assetFolder,
|
||||
GameObject root,
|
||||
List<MeshIntegrationResult> results)
|
||||
{
|
||||
var clips = new List<BlendShapeClip>();
|
||||
var proxy = root.GetComponent<VRMBlendShapeProxy>();
|
||||
if (proxy == null || proxy.BlendShapeAvatar == null)
|
||||
{
|
||||
return clips;
|
||||
}
|
||||
|
||||
var util = new VrmBlendShapeUpdater(root, results);
|
||||
|
||||
// create modified BlendShapeClip
|
||||
var clipAssetPathList = new List<string>();
|
||||
foreach (var src in proxy.BlendShapeAvatar.Clips.Where(x => x != null))
|
||||
{
|
||||
var copy = util.RecreateBlendShapeClip(src, assetFolder);
|
||||
var assetPath = $"{assetFolder}/{copy.name}.asset";
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
clipAssetPathList.Add(assetPath);
|
||||
clips.Add(copy);
|
||||
}
|
||||
|
||||
// create BlendShapeAvatar
|
||||
proxy.BlendShapeAvatar = RecreateBlendShapeAvatar(clips, assetFolder);
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
BlendShapeClip RecreateBlendShapeClip(BlendShapeClip src, string assetFolder)
|
||||
{
|
||||
if (src == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
// copy
|
||||
var copy = ScriptableObject.CreateInstance<BlendShapeClip>();
|
||||
copy.CopyFrom(src);
|
||||
copy.Prefab = null;
|
||||
copy.Values = ReplaceBlendShapeBinding(copy.Values).ToArray();
|
||||
return copy;
|
||||
}
|
||||
|
||||
static BlendShapeAvatar RecreateBlendShapeAvatar(IReadOnlyCollection<BlendShapeClip> clips, string assetFolder)
|
||||
{
|
||||
var copy = ScriptableObject.CreateInstance<BlendShapeAvatar>();
|
||||
copy.Clips.AddRange(clips);
|
||||
var assetPath = $"{assetFolder}/blendshape.asset";
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 488693244cf1ec548a9eb4d81164706f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,370 +1,135 @@
|
|||
#pragma warning disable 0414, 0649
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UniGLTF.MeshUtility;
|
||||
using System.IO;
|
||||
using UniGLTF.M17N;
|
||||
using UniGLTF;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
public class VrmMeshIntegratorWizard : ScriptableWizard
|
||||
public class VrmMeshIntegratorWizard : UniGLTF.MeshUtility.MeshUtilityDialog
|
||||
{
|
||||
public const string MENU_NAME = "VRM 0.x MeshUtility";
|
||||
const string ASSET_SUFFIX = ".mesh.asset";
|
||||
|
||||
enum HelpMessage
|
||||
public new const string MENU_NAME = "VRM 0.x MeshUtility";
|
||||
public new static void OpenWindow()
|
||||
{
|
||||
Ready,
|
||||
SetTarget,
|
||||
InvalidTarget,
|
||||
var window =
|
||||
(VrmMeshIntegratorWizard)EditorWindow.GetWindow(typeof(VrmMeshIntegratorWizard));
|
||||
window.titleContent = new GUIContent(MENU_NAME);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
enum ValidationError
|
||||
protected override void Validate()
|
||||
{
|
||||
None,
|
||||
NoTarget,
|
||||
HasParent,
|
||||
NotPrefab,
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
GameObject m_root;
|
||||
|
||||
[SerializeField]
|
||||
bool m_separateByBlendShape = true;
|
||||
|
||||
[Header("Validation")]
|
||||
[SerializeField]
|
||||
Material[] m_uniqueMaterials;
|
||||
|
||||
[Serializable]
|
||||
struct MaterialKey
|
||||
{
|
||||
public string Shader;
|
||||
public KeyValuePair<string, object>[] Properties;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
base.Validate();
|
||||
if (_exportTarget.GetComponent<VRMMeta>() == null)
|
||||
{
|
||||
if (!(obj is MaterialKey))
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
var key = (MaterialKey)obj;
|
||||
|
||||
return Shader == key.Shader
|
||||
&& Properties.SequenceEqual(key.Properties)
|
||||
;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
struct MaterialList
|
||||
{
|
||||
public Material[] Materials;
|
||||
|
||||
public MaterialList(Material[] list)
|
||||
{
|
||||
Materials = list;
|
||||
}
|
||||
}
|
||||
[SerializeField]
|
||||
MaterialList[] m_duplicateMaterials;
|
||||
|
||||
[Serializable]
|
||||
public class ExcludeItem
|
||||
{
|
||||
public Mesh Mesh;
|
||||
public bool Exclude;
|
||||
}
|
||||
|
||||
[Header("Options")]
|
||||
[SerializeField]
|
||||
List<ExcludeItem> m_excludes = new List<ExcludeItem>();
|
||||
|
||||
[Header("Result")]
|
||||
[SerializeField]
|
||||
MeshInfo[] integrationResults;
|
||||
|
||||
public static void CreateWizard()
|
||||
{
|
||||
ScriptableWizard.DisplayWizard<VrmMeshIntegratorWizard>(MENU_NAME, "Integrate and close window", "Integrate");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Clear(HelpMessage.Ready, ValidationError.None);
|
||||
OnValidate();
|
||||
}
|
||||
|
||||
protected override bool DrawWizardGUI()
|
||||
{
|
||||
var t = m_root.GetGameObjectType();
|
||||
EditorGUILayout.HelpBox($"{t}", MessageType.Info);
|
||||
return base.DrawWizardGUI();
|
||||
}
|
||||
|
||||
static object GetPropertyValue(Shader shader, int i, Material m)
|
||||
{
|
||||
var propType = ShaderUtil.GetPropertyType(shader, i);
|
||||
switch (propType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
return m.GetColor(ShaderUtil.GetPropertyName(shader, i));
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.Range:
|
||||
case ShaderUtil.ShaderPropertyType.Float:
|
||||
return m.GetFloat(ShaderUtil.GetPropertyName(shader, i));
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.Vector:
|
||||
return m.GetVector(ShaderUtil.GetPropertyName(shader, i));
|
||||
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||
return m.GetTexture(ShaderUtil.GetPropertyName(shader, i));
|
||||
|
||||
default:
|
||||
throw new NotImplementedException(propType.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
static MaterialKey GetMaterialKey(Material m)
|
||||
{
|
||||
var key = new MaterialKey
|
||||
{
|
||||
Shader = m.shader.name,
|
||||
};
|
||||
|
||||
key.Properties = Enumerable.Range(0, ShaderUtil.GetPropertyCount(m.shader))
|
||||
.Select(x => new KeyValuePair<string, object>(
|
||||
ShaderUtil.GetPropertyName(m.shader, x),
|
||||
GetPropertyValue(m.shader, x, m))
|
||||
)
|
||||
.OrderBy(x => x.Key)
|
||||
.ToArray()
|
||||
;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
void Clear(HelpMessage help, ValidationError error)
|
||||
{
|
||||
helpString = help.Msg();
|
||||
errorString = error != ValidationError.None ? error.Msg() : null;
|
||||
m_uniqueMaterials = new Material[] { };
|
||||
m_duplicateMaterials = new MaterialList[] { };
|
||||
m_excludes.Clear();
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
isValid = false;
|
||||
if (m_root == null)
|
||||
{
|
||||
Clear(HelpMessage.SetTarget, ValidationError.NoTarget);
|
||||
_validations.Add(Validation.Error("target is not vrm1"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_root.GetGameObjectType() != GameObjectType.AssetPrefab)
|
||||
{
|
||||
Clear(HelpMessage.SetTarget, ValidationError.NotPrefab);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_root.transform.parent != null)
|
||||
{
|
||||
Clear(HelpMessage.InvalidTarget, ValidationError.HasParent);
|
||||
return;
|
||||
}
|
||||
|
||||
var backup = m_excludes.ToArray();
|
||||
Clear(HelpMessage.Ready, ValidationError.None);
|
||||
isValid = true;
|
||||
m_uniqueMaterials = MeshIntegratorUtility.EnumerateSkinnedMeshRenderer(m_root.transform, MeshEnumerateOption.OnlyWithoutBlendShape)
|
||||
.SelectMany(x => x.sharedMaterials)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
m_duplicateMaterials = m_uniqueMaterials
|
||||
.GroupBy(x => GetMaterialKey(x), x => x)
|
||||
.Select(x => new MaterialList(x.ToArray()))
|
||||
.Where(x => x.Materials.Length > 1)
|
||||
.ToArray()
|
||||
;
|
||||
|
||||
UpdateExcludes(backup);
|
||||
}
|
||||
|
||||
void UpdateExcludes(ExcludeItem[] backup)
|
||||
VrmMeshUtility _meshUtil;
|
||||
VrmMeshUtility VrmMeshUtility
|
||||
{
|
||||
var exclude_map = new Dictionary<Mesh, ExcludeItem>();
|
||||
var excludes = new List<ExcludeItem>();
|
||||
foreach (var x in m_root.GetComponentsInChildren<Renderer>())
|
||||
get
|
||||
{
|
||||
var mesh = x.GetMesh();
|
||||
if (mesh == null)
|
||||
if (_meshUtil == null)
|
||||
{
|
||||
continue;
|
||||
_meshUtil = new VrmMeshUtility();
|
||||
}
|
||||
if (exclude_map.ContainsKey(mesh))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new ExcludeItem
|
||||
{
|
||||
Mesh = mesh,
|
||||
};
|
||||
var found = backup.FirstOrDefault(y => y.Mesh == mesh);
|
||||
if (found != null)
|
||||
{
|
||||
item.Exclude = found.Exclude;
|
||||
}
|
||||
excludes.Add(item);
|
||||
exclude_map[mesh] = item;
|
||||
return _meshUtil;
|
||||
}
|
||||
m_excludes.AddRange(excludes);
|
||||
}
|
||||
protected override UniGLTF.MeshUtility.GltfMeshUtility MeshUtility => VrmMeshUtility;
|
||||
|
||||
protected override bool MeshIntegrateGui()
|
||||
{
|
||||
var firstPerson = ToggleIsModified("FirstPerson == AUTO の生成", ref MeshUtility.GenerateMeshForFirstPersonAuto);
|
||||
var mod = base.MeshIntegrateGui();
|
||||
return firstPerson || mod;
|
||||
}
|
||||
|
||||
void OnWizardUpdate()
|
||||
List<BlendShapeClip> _clips;
|
||||
protected override void WriteAssets(string assetFolder,
|
||||
GameObject instance, List<UniGLTF.MeshUtility.MeshIntegrationResult> results)
|
||||
{
|
||||
}
|
||||
|
||||
/// 2022.05 仕様変更
|
||||
///
|
||||
/// * prefab 専用
|
||||
/// * backup するのではなく 変更した copy を作成する。元は変えない
|
||||
/// * copy 先の統合前の renderer を disable で残さず destroy する
|
||||
/// * 実行すると mesh, blendshape, blendShape を新規に作成する
|
||||
/// * 新しいヒエラルキーを prefab に保存してから削除して終了する
|
||||
///
|
||||
void Integrate()
|
||||
{
|
||||
if (m_root.GetGameObjectType() != GameObjectType.AssetPrefab)
|
||||
{
|
||||
throw new Exception("for prefab only");
|
||||
}
|
||||
|
||||
String folder = "Assets";
|
||||
var prefab = m_root.GetPrefab();
|
||||
if (prefab != null)
|
||||
{
|
||||
folder = AssetDatabase.GetAssetPath(prefab);
|
||||
Debug.Log(folder);
|
||||
}
|
||||
|
||||
// 新規で作成されるアセットはすべてこのフォルダの中に作る。上書きチェックはしない
|
||||
var assetFolder = EditorUtility.SaveFolderPanel("select asset save folder", Path.GetDirectoryName(folder), "VrmIntegrated");
|
||||
var unityPath = UniGLTF.UnityPath.FromFullpath(assetFolder);
|
||||
if (!unityPath.IsUnderWritableFolder)
|
||||
{
|
||||
EditorUtility.DisplayDialog("asset folder", "Target folder must be in the Assets or writable Packages folder", "cancel");
|
||||
return;
|
||||
}
|
||||
assetFolder = unityPath.Value;
|
||||
|
||||
var copy = GameObject.Instantiate(m_root);
|
||||
|
||||
// 統合
|
||||
var excludes = m_excludes.Where(x => x.Exclude).Select(x => x.Mesh);
|
||||
var results = Integrate(copy, excludes, m_separateByBlendShape);
|
||||
|
||||
// write mesh asset
|
||||
foreach (var result in results)
|
||||
{
|
||||
var childAssetPath = $"{assetFolder}/{result.Integrated.IntegratedRenderer.gameObject.name}{ASSET_SUFFIX}";
|
||||
Debug.LogFormat("CreateAsset: {0}", childAssetPath);
|
||||
AssetDatabase.CreateAsset(result.Integrated.IntegratedRenderer.sharedMesh, childAssetPath);
|
||||
}
|
||||
|
||||
// 統合した結果をヒエラルキーに追加する
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Integrated.IntegratedRenderer != null)
|
||||
{
|
||||
result.Integrated.IntegratedRenderer.transform.SetParent(copy.transform, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 統合した結果を反映した BlendShapeClip を作成して置き換える
|
||||
var clips = VRMMeshIntegratorUtility.FollowBlendshapeRendererChange(results, copy, assetFolder);
|
||||
_clips = VrmBlendShapeUpdater.FollowBlendshapeRendererChange(assetFolder, instance, results);
|
||||
|
||||
// 用が済んだ 統合前 の renderer を削除する
|
||||
foreach (var result in results)
|
||||
// write mesh
|
||||
base.WriteAssets(assetFolder, instance, results);
|
||||
|
||||
// reset firstPerson
|
||||
if (instance.GetComponent<VRMFirstPerson>() is VRMFirstPerson firstPerson)
|
||||
{
|
||||
foreach (var renderer in result.SourceMeshRenderers)
|
||||
{
|
||||
GameObject.DestroyImmediate(renderer);
|
||||
}
|
||||
foreach (var renderer in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
GameObject.DestroyImmediate(renderer);
|
||||
}
|
||||
// TODO:
|
||||
firstPerson.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
// reset firstperson
|
||||
var firstperson = copy.GetComponent<VRMFirstPerson>();
|
||||
if (firstperson != null)
|
||||
{
|
||||
firstperson.Reset();
|
||||
}
|
||||
|
||||
// prefab
|
||||
var prefabPath = $"{assetFolder}/VrmIntegrated.prefab";
|
||||
Debug.Log(prefabPath);
|
||||
PrefabUtility.SaveAsPrefabAsset(copy, prefabPath, out bool success);
|
||||
if (!success)
|
||||
{
|
||||
throw new System.Exception($"PrefabUtility.SaveAsPrefabAsset: {prefabPath}");
|
||||
}
|
||||
|
||||
// destroy scene
|
||||
UnityEngine.Object.DestroyImmediate(copy);
|
||||
protected override string WritePrefab(string assetFolder,
|
||||
GameObject instance)
|
||||
{
|
||||
var prefabPath = base.WritePrefab(assetFolder, instance);
|
||||
|
||||
// PostProcess
|
||||
// update prefab reference of BlendShapeClip
|
||||
var prefabReference = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
foreach (var clip in clips)
|
||||
foreach (var clip in _clips)
|
||||
{
|
||||
var so = new SerializedObject(clip);
|
||||
so.Update();
|
||||
// clip.Prefab = copy;
|
||||
var prop = so.FindProperty("m_prefab");
|
||||
prop.objectReferenceValue = prefabReference;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
return prefabPath;
|
||||
}
|
||||
|
||||
static List<UniGLTF.MeshUtility.MeshIntegrationResult> Integrate(GameObject root, IEnumerable<Mesh> excludes, bool separateByBlendShape)
|
||||
protected override void DialogMessage()
|
||||
{
|
||||
var results = new List<UniGLTF.MeshUtility.MeshIntegrationResult>();
|
||||
if (separateByBlendShape)
|
||||
{
|
||||
results.Add(MeshIntegratorUtility.Integrate(root, onlyBlendShapeRenderers: MeshEnumerateOption.OnlyWithoutBlendShape, excludes: excludes));
|
||||
results.Add(MeshIntegratorUtility.Integrate(root, onlyBlendShapeRenderers: MeshEnumerateOption.OnlyWithBlendShape, excludes: excludes));
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(MeshIntegratorUtility.Integrate(root, onlyBlendShapeRenderers: MeshEnumerateOption.All, excludes: excludes));
|
||||
}
|
||||
return results;
|
||||
EditorGUILayout.HelpBox(Message.MESH_UTILITY.Msg(), MessageType.Info);
|
||||
}
|
||||
|
||||
void OnWizardCreate()
|
||||
enum Message
|
||||
{
|
||||
Integrate();
|
||||
// close
|
||||
}
|
||||
[LangMsg(Languages.ja, @"(VRM-0.x専用) 凍結 > 統合 > 分割 という一連の処理を実行します。
|
||||
|
||||
void OnWizardOtherButton()
|
||||
{
|
||||
Integrate();
|
||||
[凍結]
|
||||
- ヒエラルキーの 回転・拡縮を Mesh に焼き付けます。
|
||||
- BlendShape の現状を Mesh に焼き付けます。
|
||||
|
||||
- VRM-0.x の正規化処理です。
|
||||
- HumanoidAvatar の再生成。
|
||||
- BlendShapeClip, SpringBone, Constraint なども影響を受けます。
|
||||
|
||||
[統合]
|
||||
- ヒエラルキーに含まれる MeshRenderer と SkinnedMeshRenderer をひとつの SkinnedMeshRenderer に統合します。
|
||||
|
||||
- VRM の FirstPerson 設定に応じて3種類(BOTH, FirstPerson, ThirdPerson) にグループ化して統合します。
|
||||
- FirstPerson=AUTO を前処理できます。
|
||||
- 元の Mesh は ThirdPerson として処理されます。頭なしのモデルを追加生成して FirstPersonOnly とします。
|
||||
|
||||
[分割]
|
||||
- 統合結果を BlendShape の有無を基準に分割します。
|
||||
- BOTH, FirstPerson, ThirdPerson x 2 で、最大で 6Mesh になります。空の部分ができることが多いので 3Mesh くらいが多くなります。
|
||||
|
||||
[Scene と Prefab]
|
||||
Scene と Prefab で挙動が異なります。
|
||||
|
||||
(Scene/Runtime)
|
||||
- 対象のヒエラルキーを変更します。UNDO可能。
|
||||
- Asset の書き出しはしません。Unityを再起動すると、書き出していない Mesh などの Asset が消滅します。
|
||||
|
||||
(Prefab/Editor)
|
||||
- 対象の prefab をシーンにコピーして処理を実行し、生成する Asset を指定されたフォルダに書き出します。
|
||||
- Asset 書き出し後にコピーを削除します。
|
||||
- Undo はありません。
|
||||
")]
|
||||
[LangMsg(Languages.en, @"TODO
|
||||
")]
|
||||
MESH_UTILITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ namespace VRM
|
|||
private static void ImportFromVrmFile() => VRMImporterMenu.OpenImportMenu();
|
||||
|
||||
|
||||
[MenuItem(UserMenuPrefix + "/" + VrmMeshIntegratorWizard.MENU_NAME, false, 3)]
|
||||
private static void OpenMeshIntegratorWizard() => VrmMeshIntegratorWizard.CreateWizard();
|
||||
[MenuItem(UserMenuPrefix + "/" + VrmMeshIntegratorWizard.MENU_NAME, false, 51)]
|
||||
private static void OpenMeshIntegratorWizard() => VrmMeshIntegratorWizard.OpenWindow();
|
||||
|
||||
|
||||
[MenuItem(UserMenuPrefix + "/" + VRMHumanoidNormalizerMenu.MENU_NAME, true, 51)]
|
||||
[MenuItem(UserMenuPrefix + "/" + VRMHumanoidNormalizerMenu.MENU_NAME, true, 52)]
|
||||
private static bool FreezeTPoseValidation() => VRMHumanoidNormalizerMenu.NormalizeValidation();
|
||||
[MenuItem(UserMenuPrefix + "/" + VRMHumanoidNormalizerMenu.MENU_NAME, false, 52)]
|
||||
private static void FreezeTPose() => VRMHumanoidNormalizerMenu.Normalize();
|
||||
|
|
|
|||
|
|
@ -367,6 +367,17 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// for MeshUtility interface
|
||||
/// </summary>
|
||||
public Mesh ProcessFirstPerson(Transform firstPersonBone, SkinnedMeshRenderer smr)
|
||||
{
|
||||
SetVisibilityFunc dummy = (Renderer renderer, bool firstPerson, bool thirdPerson) =>
|
||||
{
|
||||
};
|
||||
return CreateHeadlessModel(smr, FirstPersonBone, dummy);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
foreach (var mesh in m_headlessMeshes)
|
||||
|
|
|
|||
|
|
@ -66,32 +66,19 @@ namespace VRM
|
|||
}
|
||||
}
|
||||
|
||||
// 正規化されたヒエラルキーを作る
|
||||
var (normalized, bMap) = BoneNormalizer.NormalizeHierarchyFreezeMesh(go);
|
||||
// Meshの焼きこみ
|
||||
var newMesh = BoneNormalizer.NormalizeHierarchyFreezeMesh(go, true);
|
||||
// 焼いたMeshで置き換える
|
||||
BoneNormalizer.Replace(go, newMesh, true, true);
|
||||
|
||||
// 新しいヒエラルキーからAvatarを作る
|
||||
var newAvatar = UniHumanoid.AvatarDescription.CreateAvatarForCopyHierarchy(
|
||||
go.GetComponent<Animator>(), normalized, bMap, avatarDescription =>
|
||||
{
|
||||
var vrmHuman = go.GetComponent<VRMHumanoidDescription>();
|
||||
if (vrmHuman != null && vrmHuman.Description != null)
|
||||
{
|
||||
avatarDescription.armStretch = vrmHuman.Description.armStretch;
|
||||
avatarDescription.legStretch = vrmHuman.Description.legStretch;
|
||||
avatarDescription.upperArmTwist = vrmHuman.Description.upperArmTwist;
|
||||
avatarDescription.lowerArmTwist = vrmHuman.Description.lowerArmTwist;
|
||||
avatarDescription.upperLegTwist = vrmHuman.Description.upperLegTwist;
|
||||
avatarDescription.lowerLegTwist = vrmHuman.Description.lowerLegTwist;
|
||||
avatarDescription.feetSpacing = vrmHuman.Description.feetSpacing;
|
||||
avatarDescription.hasTranslationDoF = vrmHuman.Description.hasTranslationDoF;
|
||||
}
|
||||
});
|
||||
var newAnimator = normalized.GetOrAddComponent<Animator>();
|
||||
var newAnimator = go.GetComponent<Animator>();
|
||||
var newAvatar = UniHumanoid.AvatarDescription.RecreateAvatar(newAnimator);
|
||||
newAnimator.avatar = newAvatar;
|
||||
|
||||
CopyVRMComponents(go, normalized, bMap);
|
||||
// CopyVRMComponents(go, normalized, bMap);
|
||||
|
||||
return normalized;
|
||||
return go;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
149
Assets/VRM/Runtime/SkinnedMeshUtility/VrmMeshUtility.cs
Normal file
149
Assets/VRM/Runtime/SkinnedMeshUtility/VrmMeshUtility.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniHumanoid;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
public class VrmMeshUtility : UniGLTF.MeshUtility.GltfMeshUtility
|
||||
{
|
||||
bool _generateFirstPerson = false;
|
||||
public override IEnumerable<UniGLTF.MeshUtility.MeshIntegrationGroup> CopyInstantiate(GameObject go, GameObject instance)
|
||||
{
|
||||
_generateFirstPerson = false;
|
||||
|
||||
var copy = base.CopyInstantiate(go, instance);
|
||||
if (GenerateMeshForFirstPersonAuto)
|
||||
{
|
||||
foreach (var g in copy)
|
||||
{
|
||||
if (g.Name == "auto")
|
||||
{
|
||||
_generateFirstPerson = true;
|
||||
// 元のメッシュを三人称に変更
|
||||
yield return new UniGLTF.MeshUtility.MeshIntegrationGroup
|
||||
{
|
||||
Name = FirstPersonFlag.ThirdPersonOnly.ToString(),
|
||||
Renderers = g.Renderers.ToList(),
|
||||
};
|
||||
}
|
||||
yield return g;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var g in copy)
|
||||
{
|
||||
yield return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool
|
||||
TryIntegrate(
|
||||
GameObject empty,
|
||||
UniGLTF.MeshUtility.MeshIntegrationGroup group,
|
||||
out (UniGLTF.MeshUtility.MeshIntegrationResult, GameObject[]) resultAndAdded)
|
||||
{
|
||||
if (!base.TryIntegrate(empty, group, out resultAndAdded))
|
||||
{
|
||||
resultAndAdded = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (result, newGo) = resultAndAdded;
|
||||
if (_generateFirstPerson && group.Name == nameof(FirstPersonFlag.Auto))
|
||||
{
|
||||
// Mesh 統合の後処理
|
||||
// FirstPerson == "auto" の場合に
|
||||
// 頭部の無いモデルを追加で作成する
|
||||
Debug.Log("generateFirstPerson");
|
||||
if (result.Integrated.Mesh != null)
|
||||
{
|
||||
// BlendShape 有り
|
||||
_ProcessFirstPerson(_vrmInstance.FirstPersonBone, result.Integrated.IntegratedRenderer);
|
||||
}
|
||||
if (result.IntegratedNoBlendShape.Mesh != null)
|
||||
{
|
||||
// BlendShape 無しの方
|
||||
_ProcessFirstPerson(_vrmInstance.FirstPersonBone, result.IntegratedNoBlendShape.IntegratedRenderer);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _ProcessFirstPerson(Transform firstPersonBone, SkinnedMeshRenderer smr)
|
||||
{
|
||||
var mesh = _vrmInstance.ProcessFirstPerson(firstPersonBone, smr);
|
||||
if (mesh != null)
|
||||
{
|
||||
smr.sharedMesh = mesh;
|
||||
smr.name = "auto.headless";
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("no result");
|
||||
}
|
||||
}
|
||||
|
||||
VRMFirstPerson _vrmInstance = null;
|
||||
/// <summary>
|
||||
/// glTF に比べて Humanoid や FirstPerson の処理が追加される
|
||||
/// </summary>
|
||||
public override (List<UniGLTF.MeshUtility.MeshIntegrationResult>, List<GameObject>) Process(
|
||||
GameObject target, IEnumerable<UniGLTF.MeshUtility.MeshIntegrationGroup> copyGroup)
|
||||
{
|
||||
_vrmInstance = target.GetComponent<VRMFirstPerson>();
|
||||
if (_vrmInstance == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
// TODO: update: spring
|
||||
// TODO: update: constraint
|
||||
// TODO: update: firstPerson offset
|
||||
var (list, newList) = base.Process(target, copyGroup);
|
||||
|
||||
if (FreezeBlendShape || FreezeRotation || FreezeScaling)
|
||||
{
|
||||
var animator = target.GetComponent<Animator>();
|
||||
var newAvatar = AvatarDescription.RecreateAvatar(animator);
|
||||
|
||||
// ??? clear old avatar ???
|
||||
var t = animator.gameObject;
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(animator);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(animator);
|
||||
}
|
||||
|
||||
t.AddComponent<Animator>().avatar = newAvatar;
|
||||
}
|
||||
|
||||
return (list, newList);
|
||||
}
|
||||
|
||||
public override void UpdateMeshIntegrationGroups(GameObject root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var vrm0 = root.GetComponent<VRMFirstPerson>();
|
||||
if (vrm0 == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var a in vrm0.Renderers)
|
||||
{
|
||||
var g = _GetOrCreateGroup(a.FirstPersonFlag.ToString());
|
||||
g.Renderers.Add(a.Renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/VRM/Runtime/SkinnedMeshUtility/VrmMeshUtility.cs.meta
Normal file
11
Assets/VRM/Runtime/SkinnedMeshUtility/VrmMeshUtility.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fb346da8b7688c74eb627bebe1b21060
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
120
Assets/VRM10/Editor/MeshUtility/Vrm10ExpressionUpdater.cs
Normal file
120
Assets/VRM10/Editor/MeshUtility/Vrm10ExpressionUpdater.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace UniVRM10
|
||||
{
|
||||
public class Vrm10ExpressionUpdater
|
||||
{
|
||||
// BlendShapeBinding.RelativePath からの逆引き
|
||||
Dictionary<string, List<MeshIntegrationResult>> _rendererPathMap = new();
|
||||
GameObject _root;
|
||||
|
||||
Vrm10ExpressionUpdater(GameObject root, List<MeshIntegrationResult> results)
|
||||
{
|
||||
_root = root;
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var x in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
var srcPath = x.transform.RelativePathFrom(root.transform);
|
||||
if (_rendererPathMap.TryGetValue(srcPath, out var value))
|
||||
{
|
||||
value.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = new List<MeshIntegrationResult>();
|
||||
value.Add(result);
|
||||
_rendererPathMap.Add(srcPath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分割されて増える => 増えない BlendShape のある方にいく
|
||||
// 統合されて減る => 名前が同じものが統合される
|
||||
IEnumerable<MorphTargetBinding> ReplaceBlendShapeBinding(IEnumerable<MorphTargetBinding> values)
|
||||
{
|
||||
var used = new HashSet<MorphTargetBinding>();
|
||||
foreach (var val in values)
|
||||
{
|
||||
if (_rendererPathMap.TryGetValue(val.RelativePath, out var results))
|
||||
{
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Integrated == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var name = result.Integrated.Mesh.GetBlendShapeName(val.Index);
|
||||
var newIndex = result.Integrated.Mesh.GetBlendShapeIndex(name);
|
||||
if (newIndex == -1)
|
||||
{
|
||||
throw new KeyNotFoundException($"blendshape:{name} not found");
|
||||
}
|
||||
|
||||
var dstPath = result.Integrated.IntegratedRenderer.transform.RelativePathFrom(_root.transform);
|
||||
var binding = new MorphTargetBinding
|
||||
{
|
||||
RelativePath = dstPath,
|
||||
Index = newIndex,
|
||||
Weight = val.Weight,
|
||||
};
|
||||
if (used.Contains(binding))
|
||||
{
|
||||
Debug.LogWarning($"duplicated: {binding}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (VRMShaders.Symbols.VRM_DEVELOP)
|
||||
{
|
||||
Debug.Log($"{val} >> {binding}");
|
||||
}
|
||||
used.Add(binding);
|
||||
yield return binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// skip
|
||||
Debug.LogWarning($"SkinnedMeshRenderer not found: {val.RelativePath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<VRM10Expression, VRM10Expression> Update(string assetFolder, GameObject instance,
|
||||
List<MeshIntegrationResult> results)
|
||||
{
|
||||
var vrm = instance.GetComponent<Vrm10Instance>();
|
||||
var util = new Vrm10ExpressionUpdater(instance, results);
|
||||
|
||||
// write Vrm10Expressions
|
||||
var copyMap = new Dictionary<VRM10Expression, VRM10Expression>();
|
||||
foreach (var (preset, clip) in vrm.Vrm.Expression.Clips)
|
||||
{
|
||||
var copy = ScriptableObject.Instantiate(clip);
|
||||
copy.MorphTargetBindings = util.ReplaceBlendShapeBinding(clip.MorphTargetBindings).ToArray();
|
||||
var assetPath = $"{assetFolder}/{copy.name}.asset";
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
copyMap.Add(clip, copy);
|
||||
}
|
||||
|
||||
// write Vrm10Object
|
||||
{
|
||||
var copy = ScriptableObject.Instantiate<VRM10Object>(vrm.Vrm);
|
||||
var assetPath = $"{assetFolder}/{copy.name}.asset";
|
||||
copy.Expression.Replace(copyMap);
|
||||
AssetDatabase.CreateAsset(copy, assetPath);
|
||||
vrm.Vrm = copy;
|
||||
}
|
||||
|
||||
return copyMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d74b29fa7bb59ae4a9f641bf16468848
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/VRM10/Editor/MeshUtility/Vrm10MeshIntegrationTab.cs
Normal file
15
Assets/VRM10/Editor/MeshUtility/Vrm10MeshIntegrationTab.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UniVRM10
|
||||
{
|
||||
class Vrm10MeshIntegrationTab : UniGLTF.MeshUtility.MeshIntegrationTab
|
||||
{
|
||||
Vrm10MeshUtility _vrmMeshUtil;
|
||||
|
||||
public Vrm10MeshIntegrationTab(EditorWindow editor, Vrm10MeshUtility meshUtility) : base(editor, meshUtility)
|
||||
{
|
||||
_vrmMeshUtil = meshUtility;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ec2a4df10a08bab4a980b7203d341bda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,163 +1,140 @@
|
|||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UniGLTF;
|
||||
using UniGLTF.M17N;
|
||||
using System.Collections.Generic;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UniGLTF;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace UniVRM10
|
||||
{
|
||||
public class Vrm10MeshUtilityDialog : EditorWindow
|
||||
public class Vrm10MeshUtilityDialog : UniGLTF.MeshUtility.MeshUtilityDialog
|
||||
{
|
||||
public const string MENU_NAME = "VRM 1.0 MeshUtility";
|
||||
|
||||
enum Tabs
|
||||
{
|
||||
Freeze,
|
||||
IntegrateSplit,
|
||||
}
|
||||
Tabs _tab;
|
||||
|
||||
public static void OpenWindow()
|
||||
public new const string MENU_NAME = "VRM 1.0 MeshUtility";
|
||||
public new static void OpenWindow()
|
||||
{
|
||||
var window =
|
||||
(Vrm10MeshUtilityDialog)EditorWindow.GetWindow(typeof(Vrm10MeshUtilityDialog));
|
||||
window.titleContent = new GUIContent(MENU_NAME);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
Vrm10MeshUtility _meshUtility = new Vrm10MeshUtility();
|
||||
|
||||
List<Validation> _validations = new List<Validation>();
|
||||
private void Validate()
|
||||
protected override void Validate()
|
||||
{
|
||||
_validations.Clear();
|
||||
if (_exportTarget == null)
|
||||
{
|
||||
_validations.Add(Validation.Error("set vrm1"));
|
||||
return;
|
||||
}
|
||||
base.Validate();
|
||||
if (_exportTarget.GetComponent<Vrm10Instance>() == null)
|
||||
{
|
||||
_validations.Add(Validation.Error("target is not vrm1"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
bool IsValid => !_validations.Any(v => !v.CanExport);
|
||||
|
||||
Vector2 _scrollPos;
|
||||
GameObject _exportTarget;
|
||||
MeshIntegrationTab _meshIntegration;
|
||||
void OnEnable()
|
||||
Vrm10MeshUtility _meshUtil;
|
||||
Vrm10MeshUtility Vrm10MeshUtility
|
||||
{
|
||||
_meshIntegration = new MeshIntegrationTab(this, _meshUtility);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var modified = false;
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
EditorGUIUtility.labelWidth = 200;
|
||||
LanguageGetter.OnGuiSelectLang();
|
||||
var exportTarget = (GameObject)EditorGUILayout.ObjectField(
|
||||
MeshUtilityMessages.TARGET_OBJECT.Msg(),
|
||||
_exportTarget, typeof(GameObject), true);
|
||||
if (exportTarget != _exportTarget)
|
||||
get
|
||||
{
|
||||
_exportTarget = exportTarget;
|
||||
_meshIntegration.UpdateMeshIntegrationList(_exportTarget);
|
||||
modified = true;
|
||||
}
|
||||
_tab = TabBar.OnGUI(_tab, "LargeButton", GUI.ToolbarButtonSize.Fixed);
|
||||
|
||||
foreach (var validation in _validations)
|
||||
{
|
||||
validation.DrawGUI();
|
||||
}
|
||||
|
||||
switch (_tab)
|
||||
{
|
||||
case Tabs.Freeze:
|
||||
{
|
||||
if (MeshFreezeGui())
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Tabs.IntegrateSplit:
|
||||
{
|
||||
if (MeshIntegrateGui())
|
||||
{
|
||||
modified = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
if (modified)
|
||||
{
|
||||
Validate();
|
||||
}
|
||||
|
||||
GUI.enabled = IsValid;
|
||||
var pressed = GUILayout.Button("Process", GUILayout.MinWidth(100));
|
||||
GUI.enabled = true;
|
||||
if (pressed)
|
||||
{
|
||||
Undo.RegisterFullObjectHierarchyUndo(exportTarget, "MeshUtility");
|
||||
foreach (var go in _meshUtility.Process(exportTarget))
|
||||
if (_meshUtil == null)
|
||||
{
|
||||
Undo.RegisterCreatedObjectUndo(go, "MeshUtility");
|
||||
_meshUtil = new Vrm10MeshUtility();
|
||||
}
|
||||
_exportTarget = null;
|
||||
// Show Result ?
|
||||
// Close();
|
||||
// GUIUtility.ExitGUI();
|
||||
return _meshUtil;
|
||||
}
|
||||
}
|
||||
protected override UniGLTF.MeshUtility.GltfMeshUtility MeshUtility => Vrm10MeshUtility;
|
||||
|
||||
Vrm10MeshIntegrationTab _integrationTab;
|
||||
protected override UniGLTF.MeshUtility.MeshIntegrationTab MeshIntegration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_integrationTab == null)
|
||||
{
|
||||
_integrationTab = new Vrm10MeshIntegrationTab(this, Vrm10MeshUtility);
|
||||
}
|
||||
return _integrationTab;
|
||||
}
|
||||
}
|
||||
|
||||
bool ToggleIsModified(string label, ref bool value)
|
||||
protected override bool MeshIntegrateGui()
|
||||
{
|
||||
var newValue = EditorGUILayout.Toggle(label, value);
|
||||
if (newValue == value)
|
||||
var firstPerson = ToggleIsModified("FirstPerson == AUTO の生成", ref MeshUtility.GenerateMeshForFirstPersonAuto);
|
||||
var mod = base.MeshIntegrateGui();
|
||||
return firstPerson || mod;
|
||||
}
|
||||
|
||||
List<VRM10Expression> _clips;
|
||||
protected override void WriteAssets(string assetFolder, GameObject instance,
|
||||
List<UniGLTF.MeshUtility.MeshIntegrationResult> results)
|
||||
{
|
||||
_clips = Vrm10ExpressionUpdater.Update(assetFolder, instance, results).Values.ToList();
|
||||
|
||||
// write mesh
|
||||
base.WriteAssets(assetFolder, instance, results);
|
||||
}
|
||||
|
||||
protected override string WritePrefab(string assetFolder,
|
||||
GameObject instance)
|
||||
{
|
||||
var prefabPath = base.WritePrefab(assetFolder, instance);
|
||||
|
||||
// PostProcess
|
||||
// update prefab reference of BlendShapeClip
|
||||
var prefabReference = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
foreach (var clip in _clips)
|
||||
{
|
||||
return false;
|
||||
var so = new SerializedObject(clip);
|
||||
so.Update();
|
||||
var prop = so.FindProperty("m_prefab");
|
||||
prop.objectReferenceValue = prefabReference;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
value = newValue;
|
||||
return true;
|
||||
|
||||
return prefabPath;
|
||||
}
|
||||
|
||||
bool MeshFreezeGui()
|
||||
protected override void DialogMessage()
|
||||
{
|
||||
var forceUniqueName = ToggleIsModified("ForceUniqueName", ref _meshUtility.ForceUniqueName);
|
||||
var blendShape = ToggleIsModified("BlendShape", ref _meshUtility.FreezeBlendShape);
|
||||
var scale = ToggleIsModified("Scale", ref _meshUtility.FreezeScaling);
|
||||
var rotation = ToggleIsModified("Rotation", ref _meshUtility.FreezeRotation);
|
||||
return forceUniqueName || blendShape || scale || rotation;
|
||||
EditorGUILayout.HelpBox(Message.MESH_UTILITY.Msg(), MessageType.Info);
|
||||
}
|
||||
|
||||
bool MeshIntegrateGui()
|
||||
enum Message
|
||||
{
|
||||
var firstPerson = ToggleIsModified("FirstPerson == AUTO の生成", ref _meshUtility.GenerateMeshForFirstPersonAuto);
|
||||
var split = ToggleIsModified("Separate by BlendShape", ref _meshUtility.SplitByBlendShape);
|
||||
var p = position;
|
||||
var last = GUILayoutUtility.GetLastRect();
|
||||
var y = last.y + last.height;
|
||||
var rect = new Rect
|
||||
{
|
||||
x = last.x,
|
||||
y = y,
|
||||
width = p.width,
|
||||
height = p.height - y
|
||||
// process button の高さ
|
||||
- 30
|
||||
};
|
||||
var mod = _meshIntegration.OnGui(rect);
|
||||
return firstPerson || split || mod;
|
||||
[LangMsg(Languages.ja, @"(VRM-1.0専用) 凍結 > 統合 > 分割 という一連の処理を実行します。
|
||||
|
||||
[凍結]
|
||||
- ヒエラルキーの 回転・拡縮を Mesh に焼き付けます。
|
||||
- BlendShape の現状を Mesh に焼き付けます。
|
||||
|
||||
- VRM-1.0 では正規化は必須でなくなりました。任意のオプションです。
|
||||
- VRM-1.0 でも拡縮の凍結は推奨しています。
|
||||
- HumanoidAvatar の再生成。
|
||||
- Expression, SpringBone, Constraint なども影響を受けます。
|
||||
|
||||
[統合]
|
||||
- ヒエラルキーに含まれる MeshRenderer と SkinnedMeshRenderer をひとつの SkinnedMeshRenderer に統合します。
|
||||
|
||||
- VRM の FirstPerson 設定に応じて3種類(BOTH, FirstPerson, ThirdPerson) にグループ化して統合します。
|
||||
- FirstPerson=AUTO を前処理できます。
|
||||
- 元の Mesh は ThirdPerson として処理されます。頭なしのモデルを追加生成して FirstPersonOnly とします。
|
||||
|
||||
[分割]
|
||||
- 統合結果を BlendShape の有無を基準に分割します。
|
||||
- BOTH, FirstPerson, ThirdPerson x 2 で、最大で 6Mesh になります。空の部分ができることが多いので 3Mesh くらいが多くなります。
|
||||
|
||||
[Scene と Prefab]
|
||||
Scene と Prefab で挙動が異なります。
|
||||
|
||||
(Scene/Runtime)
|
||||
- 対象のヒエラルキーを変更します。UNDO可能。
|
||||
- Asset の書き出しはしません。Unityを再起動すると、書き出していない Mesh などの Asset が消滅します。
|
||||
|
||||
(Prefab/Editor)
|
||||
- 対象の prefab をシーンにコピーして処理を実行し、生成する Asset を指定されたフォルダに書き出します。
|
||||
- Asset 書き出し後にコピーを削除します。
|
||||
- Undo はありません。
|
||||
")]
|
||||
[LangMsg(Languages.en, @"TODO
|
||||
")]
|
||||
MESH_UTILITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,15 +13,15 @@ namespace UniVRM10
|
|||
[MenuItem(UserMenuPrefix + "/" + VRM10ExportDialog.MENU_NAME, priority = 1)]
|
||||
private static void OpenExportDialog() => VRM10ExportDialog.Open();
|
||||
|
||||
[MenuItem(UserMenuPrefix + "/" + Vrm10MeshUtilityDialog.MENU_NAME, priority = 2)]
|
||||
[MenuItem(UserMenuPrefix + "/" + Vrm10MeshUtilityDialog.MENU_NAME, priority = 21)]
|
||||
private static void OpenMeshUtility() => Vrm10MeshUtilityDialog.OpenWindow();
|
||||
|
||||
|
||||
[MenuItem(ExperimentalMenuPrefix + "/" + VrmAnimationMenu.MENU_NAME, priority = 21)]
|
||||
[MenuItem(ExperimentalMenuPrefix + "/" + VrmAnimationMenu.MENU_NAME, priority = 22)]
|
||||
private static void ConvertVrmAnimation() => VrmAnimationMenu.BvhToVrmAnimationMenu();
|
||||
|
||||
#if VRM_DEVELOP
|
||||
[MenuItem(ExperimentalMenuPrefix + "/" + VRM10Window.MENU_NAME, false, 22)]
|
||||
[MenuItem(ExperimentalMenuPrefix + "/" + VRM10Window.MENU_NAME, false, 23)]
|
||||
private static void OpenWindow() => VRM10Window.Open();
|
||||
|
||||
[MenuItem(DevelopmentMenuPrefix + "/Generate from JsonSchema", false, 100)]
|
||||
|
|
|
|||
|
|
@ -1,96 +1,125 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniGLTF.MeshUtility;
|
||||
using UniHumanoid;
|
||||
using UnityEngine;
|
||||
using VRMShaders;
|
||||
|
||||
|
||||
namespace UniVRM10
|
||||
{
|
||||
/// <summary>
|
||||
/// - Freeze
|
||||
/// - Integration
|
||||
/// - Split
|
||||
///
|
||||
/// - Implement runtime logic => Process a hierarchy in scene. Do not process prefab.
|
||||
/// - Implement undo
|
||||
///
|
||||
/// </summary>
|
||||
public class Vrm10MeshUtility
|
||||
public class Vrm10MeshUtility : UniGLTF.MeshUtility.GltfMeshUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// GameObject 名が重複している場合にリネームする。
|
||||
/// 最初に実行(Avatar生成時のエラーを回避?)
|
||||
/// </summary>
|
||||
public bool ForceUniqueName = false;
|
||||
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeBlendShape = false;
|
||||
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeScaling = true;
|
||||
|
||||
/// <summary>
|
||||
/// Same as VRM-0 normalization
|
||||
/// - Mesh
|
||||
/// - Node
|
||||
/// - InverseBindMatrices
|
||||
/// </summary>
|
||||
public bool FreezeRotation = false;
|
||||
|
||||
public List<MeshIntegrationGroup> MeshIntegrationGroups = new List<MeshIntegrationGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a headless model and solve VRM.FirstPersonFlag.Auto
|
||||
/// </summary>
|
||||
public bool GenerateMeshForFirstPersonAuto = false;
|
||||
|
||||
/// <summary>
|
||||
/// Split into having and not having BlendShape
|
||||
/// </summary>
|
||||
public bool SplitByBlendShape = false;
|
||||
|
||||
public void IntegrateAll(GameObject root)
|
||||
bool _generateFirstPerson = false;
|
||||
public override IEnumerable<UniGLTF.MeshUtility.MeshIntegrationGroup> CopyInstantiate(GameObject go, GameObject instance)
|
||||
{
|
||||
if (root == null)
|
||||
var copy = base.CopyInstantiate(go, instance);
|
||||
_generateFirstPerson = false;
|
||||
if (GenerateMeshForFirstPersonAuto)
|
||||
{
|
||||
return;
|
||||
}
|
||||
MeshIntegrationGroups.Add(new MeshIntegrationGroup
|
||||
{
|
||||
Name = "ALL",
|
||||
Renderers = root.GetComponentsInChildren<Renderer>().ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
MeshIntegrationGroup GetOrCreateGroup(string name)
|
||||
{
|
||||
foreach (var g in MeshIntegrationGroups)
|
||||
{
|
||||
if (g.Name == name)
|
||||
foreach (var g in copy)
|
||||
{
|
||||
return g;
|
||||
if (g.Name == "auto")
|
||||
{
|
||||
_generateFirstPerson = true;
|
||||
// 元のメッシュを三人称に変更
|
||||
yield return new UniGLTF.MeshUtility.MeshIntegrationGroup
|
||||
{
|
||||
Name = UniGLTF.Extensions.VRMC_vrm.FirstPersonType.thirdPersonOnly.ToString(),
|
||||
Renderers = g.Renderers.ToList(),
|
||||
};
|
||||
}
|
||||
yield return g;
|
||||
}
|
||||
}
|
||||
MeshIntegrationGroups.Add(new MeshIntegrationGroup
|
||||
else
|
||||
{
|
||||
Name = name,
|
||||
});
|
||||
return MeshIntegrationGroups.Last();
|
||||
foreach (var g in copy)
|
||||
{
|
||||
yield return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void IntegrateFirstPerson(GameObject root)
|
||||
protected override
|
||||
bool TryIntegrate(
|
||||
GameObject empty,
|
||||
UniGLTF.MeshUtility.MeshIntegrationGroup group,
|
||||
out (UniGLTF.MeshUtility.MeshIntegrationResult, GameObject[]) resultAndAdded)
|
||||
{
|
||||
if (!base.TryIntegrate(empty, group, out resultAndAdded))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var (result, newList) = resultAndAdded;
|
||||
|
||||
if (_generateFirstPerson && group.Name == nameof(UniGLTF.Extensions.VRMC_vrm.FirstPersonType.auto))
|
||||
{
|
||||
// Mesh 統合の後処理
|
||||
// FirstPerson == "auto" の場合に
|
||||
// 頭部の無いモデルを追加で作成する
|
||||
Debug.Log("generateFirstPerson");
|
||||
if (result.Integrated.Mesh != null)
|
||||
{
|
||||
// BlendShape 有り
|
||||
_ProcessFirstPerson(_vrmInstance.Humanoid.Head, result.Integrated.IntegratedRenderer);
|
||||
}
|
||||
if (result.IntegratedNoBlendShape.Mesh != null)
|
||||
{
|
||||
// BlendShape 無しの方
|
||||
_ProcessFirstPerson(_vrmInstance.Humanoid.Head, result.IntegratedNoBlendShape.IntegratedRenderer);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void _ProcessFirstPerson(Transform firstPersonBone, SkinnedMeshRenderer smr)
|
||||
{
|
||||
var task = VRM10ObjectFirstPerson.CreateErasedMeshAsync(
|
||||
smr,
|
||||
firstPersonBone,
|
||||
new VRMShaders.ImmediateCaller());
|
||||
task.Wait();
|
||||
var mesh = task.Result;
|
||||
if (mesh != null)
|
||||
{
|
||||
smr.sharedMesh = mesh;
|
||||
smr.name = "auto.headless";
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("no result");
|
||||
}
|
||||
}
|
||||
|
||||
Vrm10Instance _vrmInstance = null;
|
||||
/// <summary>
|
||||
/// glTF に比べて Humanoid や FirstPerson の処理が追加される
|
||||
/// </summary>
|
||||
public override (List<UniGLTF.MeshUtility.MeshIntegrationResult>, List<GameObject>) Process(
|
||||
GameObject target, IEnumerable<UniGLTF.MeshUtility.MeshIntegrationGroup> groupCopy)
|
||||
{
|
||||
_vrmInstance = target.GetComponent<Vrm10Instance>();
|
||||
if (_vrmInstance == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
// TODO: update: spring
|
||||
// TODO: update: constraint
|
||||
// TODO: update: firstPerson offset
|
||||
var (list, newList) = base.Process(target, groupCopy);
|
||||
|
||||
if (FreezeBlendShape || FreezeRotation || FreezeScaling)
|
||||
{
|
||||
var animator = target.GetComponent<Animator>();
|
||||
var newAvatar = AvatarDescription.RecreateAvatar(animator);
|
||||
animator.avatar = newAvatar;
|
||||
}
|
||||
|
||||
return (list, newList);
|
||||
}
|
||||
|
||||
public override void UpdateMeshIntegrationGroups(GameObject root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
|
|
@ -113,202 +142,9 @@ namespace UniVRM10
|
|||
}
|
||||
foreach (var a in fp.Renderers)
|
||||
{
|
||||
var g = GetOrCreateGroup(a.FirstPersonFlag.ToString());
|
||||
var g = _GetOrCreateGroup(a.FirstPersonFlag.ToString());
|
||||
g.Renderers.Add(a.GetRenderer(root.transform));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveComponent<T>(T c) where T : Component
|
||||
{
|
||||
if (c == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var t = c.transform;
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(c);
|
||||
}
|
||||
|
||||
if (t.childCount == 0)
|
||||
{
|
||||
var list = t.GetComponents<Component>();
|
||||
// Debug.Log($"{list[0].GetType().Name}");
|
||||
if (list.Length == 1 && list[0] == t)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(t.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(t.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GameObject GetOrCreateEmpty(GameObject go, string name)
|
||||
{
|
||||
foreach (var child in go.transform.GetChildren())
|
||||
{
|
||||
if (child.name == name
|
||||
&& child.localPosition == Vector3.zero
|
||||
&& child.localScale == Vector3.one
|
||||
&& child.localRotation == Quaternion.identity)
|
||||
{
|
||||
return child.gameObject;
|
||||
}
|
||||
}
|
||||
var empty = new GameObject(name);
|
||||
empty.transform.SetParent(go.transform, false);
|
||||
return empty;
|
||||
}
|
||||
|
||||
public List<GameObject> Process(GameObject go)
|
||||
{
|
||||
var vrmInstance = go.GetComponent<Vrm10Instance>();
|
||||
if (vrmInstance == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
// TODO unpack prefab
|
||||
|
||||
if (ForceUniqueName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
// 必用?
|
||||
var animator = go.GetComponent<Animator>();
|
||||
var newAvatar = AvatarDescription.RecreateAvatar(animator);
|
||||
animator.avatar = newAvatar;
|
||||
}
|
||||
|
||||
// 正規化されたヒエラルキーを作る
|
||||
if (FreezeBlendShape || FreezeRotation || FreezeScaling)
|
||||
{
|
||||
// TODO: UNDO
|
||||
var (normalized, boneMap) = BoneNormalizer.NormalizeHierarchyFreezeMesh(go,
|
||||
removeScaling: FreezeScaling,
|
||||
removeRotation: FreezeRotation,
|
||||
freezeBlendShape: FreezeBlendShape);
|
||||
|
||||
// TODO: update: spring
|
||||
// TODO: update: constraint
|
||||
// TODO: update: firstPerson offset
|
||||
|
||||
// write back normalized transform to boneMap
|
||||
BoneNormalizer.WriteBackResult(go, normalized, boneMap);
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GameObject.Destroy(normalized);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject.DestroyImmediate(normalized);
|
||||
}
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
var newAvatar = AvatarDescription.RecreateAvatar(animator);
|
||||
animator.avatar = newAvatar;
|
||||
}
|
||||
|
||||
var copy = new List<MeshIntegrationGroup>();
|
||||
var generateFirstPerson = false;
|
||||
if (GenerateMeshForFirstPersonAuto)
|
||||
{
|
||||
foreach (var g in MeshIntegrationGroups)
|
||||
{
|
||||
if (g.Name == "auto")
|
||||
{
|
||||
generateFirstPerson = true;
|
||||
// 元のメッシュを三人称に変更
|
||||
copy.Add(new MeshIntegrationGroup
|
||||
{
|
||||
Name = UniGLTF.Extensions.VRMC_vrm.FirstPersonType.thirdPersonOnly.ToString(),
|
||||
Renderers = g.Renderers.ToList(),
|
||||
});
|
||||
}
|
||||
copy.Add(g);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
copy.AddRange(MeshIntegrationGroups);
|
||||
}
|
||||
|
||||
var newGo = new List<GameObject>();
|
||||
{
|
||||
var empty = GetOrCreateEmpty(go, "mesh");
|
||||
|
||||
var results = new List<MeshIntegrationResult>();
|
||||
foreach (var group in copy)
|
||||
{
|
||||
var result = MeshIntegrator.Integrate(group, SplitByBlendShape
|
||||
? MeshIntegrator.BlendShapeOperation.Split
|
||||
: MeshIntegrator.BlendShapeOperation.Use);
|
||||
results.Add(result);
|
||||
|
||||
foreach (var created in result.AddIntegratedRendererTo(empty))
|
||||
{
|
||||
newGo.Add(created);
|
||||
}
|
||||
|
||||
if (generateFirstPerson && group.Name == "auto")
|
||||
{
|
||||
Debug.Log("generateFirstPerson");
|
||||
if (result.Integrated.Mesh != null)
|
||||
{
|
||||
ProcessFirstPerson(vrmInstance, result.Integrated);
|
||||
}
|
||||
if (result.IntegratedNoBlendShape.Mesh != null)
|
||||
{
|
||||
ProcessFirstPerson(vrmInstance, result.IntegratedNoBlendShape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var r in result.SourceMeshRenderers)
|
||||
{
|
||||
RemoveComponent(r);
|
||||
}
|
||||
foreach (var r in result.SourceSkinnedMeshRenderers)
|
||||
{
|
||||
RemoveComponent(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeshIntegrationGroups.Clear();
|
||||
|
||||
return newGo;
|
||||
}
|
||||
|
||||
void ProcessFirstPerson(Vrm10Instance vrmInstance, MeshInfo info)
|
||||
{
|
||||
var firstPersonBone = vrmInstance.Humanoid.Head;
|
||||
var task = VRM10ObjectFirstPerson.CreateErasedMeshAsync(
|
||||
info.IntegratedRenderer,
|
||||
firstPersonBone,
|
||||
new ImmediateCaller());
|
||||
task.Wait();
|
||||
|
||||
if (task.Result != null)
|
||||
{
|
||||
info.IntegratedRenderer.sharedMesh = task.Result;
|
||||
info.IntegratedRenderer.name = "auto.headless";
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("no result");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user