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:
ousttrue 2023-12-07 13:43:05 +09:00 committed by GitHub
commit 07311ece5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1713 additions and 1511 deletions

View File

@ -54,6 +54,7 @@
},
"cSpell.words": [
"GLTF",
"Scriptable",
"UNIVRM"
]
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 07d031dea01a55c43b1ec68cd10bf461
guid: 27b74253f485b4b45a8d2847ec3c2b34
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -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;
}
}
}

View File

@ -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,
}
}

View 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;
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: fb47e24fc1463584fa0b6b685d75f25e
guid: 948458ced6bcc904781eed04ebe7cd01
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -2,7 +2,7 @@ using UnityEngine;
using UnityEditor;
using System;
namespace UniVRM10
namespace UniGLTF.MeshUtility
{
[Serializable]
public abstract class Splitter

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 385274edf555ed84a9f3706ca2a99023
guid: dbb5f5bceb86592499a56fc18011553e
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -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;
}
}
}

View 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: あれば一時オブジェクトの破棄
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2021859c0d7255643bd93c95ab3fcf3d
guid: 5019966789c0e094a8f58fc2734c9a35
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -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
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}
}
}

View 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;
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 65a227dcf3cb5f34085bd6829894fb64
guid: e2425cf6ac1f2434986968ff0d4a4755
MonoImporter:
externalObjects: {}
serializedVersion: 2

View 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);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6eff3bee96e61849ac190387ec28542
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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)
{

View File

@ -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);
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a982d9d30c0145038245b0214dc2f2e4
timeCreated: 1560190306

View File

@ -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;

View File

@ -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;
}
}
}

View 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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 488693244cf1ec548a9eb4d81164706f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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 (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,
}
}
}

View File

@ -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();

View File

@ -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)

View File

@ -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>

View 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);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb346da8b7688c74eb627bebe1b21060
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d74b29fa7bb59ae4a9f641bf16468848
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec2a4df10a08bab4a980b7203d341bda
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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 (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,
}
}
}

View File

@ -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)]

View File

@ -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");
}
}
}
}
}