Merge pull request #421 from ousttrue/exporter_blendshape_optimize

Add blendshape options to export dialog
This commit is contained in:
hiroj
2020-06-04 12:25:13 +09:00
committed by GitHub
19 changed files with 760 additions and 721 deletions

View File

@@ -0,0 +1,26 @@
using System;
using UnityEditor;
namespace VRM
{
/// <summary>
/// UndoをGroupを開始して、DisposeでUndoする。
/// using で使うのを想定。
/// using ブロック内で Undo されるべき操作をする。
/// </summary>
public struct RecordDisposer : IDisposable
{
int _group;
public RecordDisposer(UnityEngine.Object[] objects, string msg)
{
Undo.IncrementCurrentGroup();
_group = Undo.GetCurrentGroup();
Undo.RecordObjects(objects, msg);
}
public void Dispose()
{
Undo.RevertAllDownToGroup(_group);
}
}
}

View File

@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: f8ed5cb82dd13bf43a1556b24a6d13a0
timeCreated: 1532063757
licenseType: Free
guid: b304ed2aeece5a54191a5a8b69d9d113
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UniGLTF;
using UnityEditor;
using UnityEngine;
namespace VRM
{
public static class VRMEditorExporter
{
/// <summary>
/// Editor向けのエクスポート処理
/// </summary>
/// <param name="path">出力先</param>
/// <param name="settings">エクスポート設定</param>
public static void Export(string path, VRMExportSettings settings)
{
List<GameObject> destroy = new List<GameObject>();
try
{
Export(path, settings, destroy);
}
finally
{
foreach (var x in destroy)
{
Debug.LogFormat("destroy: {0}", x.name);
GameObject.DestroyImmediate(x);
}
}
}
static bool IsPrefab(GameObject go)
{
return !go.scene.IsValid();
}
/// <summary>
/// DeepCopy
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
static BlendShapeAvatar CopyBlendShapeAvatar(BlendShapeAvatar src, bool removeUnknown)
{
var avatar = GameObject.Instantiate(src);
avatar.Clips = new List<BlendShapeClip>();
foreach (var clip in src.Clips)
{
if (removeUnknown && clip.Preset == BlendShapePreset.Unknown)
{
continue;
}
avatar.Clips.Add(GameObject.Instantiate(clip));
}
return avatar;
}
/// <summary>
/// 使用されない BlendShape を間引いた Mesh を作成して置き換える
/// </summary>
/// <param name="mesh"></param>
/// <returns></returns>
static void ReplaceMesh(GameObject target, SkinnedMeshRenderer smr, BlendShapeAvatar copyBlendShapeAvatar)
{
Mesh mesh = smr.sharedMesh;
if (mesh == null) return;
if (mesh.blendShapeCount == 0) return;
// Mesh から BlendShapeClip からの参照がある blendShape の index を集める
var usedBlendshapeIndexArray = copyBlendShapeAvatar.Clips
.SelectMany(clip => clip.Values)
.Where(val => target.transform.Find(val.RelativePath) == smr.transform)
.Select(val => val.Index)
.Distinct()
.ToArray();
var copyMesh = mesh.Copy(copyBlendShape: false);
// 使われている BlendShape だけをコピーする
foreach (var i in usedBlendshapeIndexArray)
{
var name = mesh.GetBlendShapeName(i);
var vCount = mesh.vertexCount;
var vertices = new Vector3[vCount];
var normals = new Vector3[vCount];
var tangents = new Vector3[vCount];
mesh.GetBlendShapeFrameVertices(i, 0, vertices, normals, tangents);
copyMesh.AddBlendShapeFrame(name, 100f, vertices, normals, tangents);
}
// BlendShapeClip の BlendShapeIndex を更新する(前に詰める)
var indexMapper = usedBlendshapeIndexArray
.Select((x, i) => new { x, i })
.ToDictionary(pair => pair.x, pair => pair.i);
foreach (var clip in copyBlendShapeAvatar.Clips)
{
for (var i = 0; i < clip.Values.Length; ++i)
{
var value = clip.Values[i];
if (target.transform.Find(value.RelativePath) != smr.transform) continue;
value.Index = indexMapper[value.Index];
clip.Values[i] = value;
}
}
// mesh を置き換える
smr.sharedMesh = copyMesh;
}
/// <summary>
///
/// </summary>
/// <param name="path"></param>
/// <param name="settings"></param>
/// <param name="destroy">作業が終わったらDestoryするべき一時オブジェクト</param>
static void Export(string path, VRMExportSettings settings, List<GameObject> destroy)
{
var target = settings.Source;
// 常にコピーする。シーンを変化させない
target = GameObject.Instantiate(target);
destroy.Add(target);
// 正規化
if (settings.PoseFreeze)
{
// BoneNormalizer.Execute は Copy を作って正規化する。UNDO無用
target = BoneNormalizer.Execute(target, settings.ForceTPose, false);
destroy.Add(target);
}
// 元のBlendShapeClipに変更を加えないように複製
var proxy = target.GetComponent<VRMBlendShapeProxy>();
var copyBlendShapeAvatar = CopyBlendShapeAvatar(proxy.BlendShapeAvatar, settings.ReduceBlendshapeClip);
proxy.BlendShapeAvatar = copyBlendShapeAvatar;
// BlendShape削減
if (settings.ReduceBlendshape)
{
foreach (SkinnedMeshRenderer smr in target.GetComponentsInChildren<SkinnedMeshRenderer>())
{
// 未使用のBlendShapeを間引く
ReplaceMesh(target, smr, copyBlendShapeAvatar);
}
}
// 出力
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var vrm = VRMExporter.Export(target, new VRMExporterConfiguration
{
UseSparseAccessorForBlendShape = settings.UseSparseAccessor,
ExportOnlyBlendShapePosition = settings.OnlyBlendshapePosition
});
vrm.extensions.VRM.meta.title = settings.Title;
vrm.extensions.VRM.meta.version = settings.Version;
vrm.extensions.VRM.meta.author = settings.Author;
vrm.extensions.VRM.meta.contactInformation = settings.ContactInformation;
vrm.extensions.VRM.meta.reference = settings.Reference;
var bytes = vrm.ToGlbBytes(settings.UseExperimentalExporter ? SerializerTypes.Generated : SerializerTypes.UniJSON);
File.WriteAllBytes(path, bytes);
Debug.LogFormat("Export elapsed {0}", sw.Elapsed);
}
if (path.StartsWithUnityAssetPath())
{
// 出力ファイルのインポートを発動
AssetDatabase.ImportAsset(path.ToUnityRelativePath());
}
}
}
}

View File

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

View File

@@ -1,104 +0,0 @@
using System;
using UnityEditor;
using UnityEngine;
namespace VRM
{
[CustomEditor(typeof(VRMExportObject))]
public class VRMExportObjectEditor : Editor
{
SerializedProperty m_settings;
VRMExportObject m_target;
void OnEnable()
{
m_target = target as VRMExportObject;
m_settings = serializedObject.FindProperty("Settings");
}
public override void OnInspectorGUI()
{
//
// Editor
//
serializedObject.Update();
var before = m_target.Settings.Source;
EditorGUILayout.PropertyField(m_settings, true);
serializedObject.ApplyModifiedProperties();
//
//
//
var after = m_target.Settings.Source;
if (before != after)
{
m_target.Settings.InitializeFrom(after as GameObject);
}
bool canExport = m_target.Settings.Source != null;
foreach (var validation in m_target.Settings.CanExport())
{
if (!validation.CanExport)
{
canExport = false;
}
EditorGUILayout.HelpBox(validation.Message, validation.CanExport ? MessageType.Warning : MessageType.Error);
}
if (canExport)
{
if (GUILayout.Button("Export"))
{
var path = EditorUtility.SaveFilePanel(
"Save vrm",
null,//Dir,
m_target.Settings.Source.name + ".vrm",
"vrm");
if (!string.IsNullOrEmpty(path))
{
var target = m_target;
EditorApplication.delayCall += () =>
{
target.Settings.Export(path);
};
}
}
}
}
class DisposableInstance : IDisposable
{
GameObject m_go;
public GameObject GameObject
{
get
{
return m_go;
}
}
public DisposableInstance(GameObject prefab)
{
m_go = GameObject.Instantiate(prefab);
}
public void Dispose()
{
if (m_go != null)
{
if (Application.isPlaying)
{
GameObject.Destroy(m_go);
}
else
{
GameObject.DestroyImmediate(m_go);
}
}
}
}
}
}

View File

@@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UniGLTF;
using UnityEngine;
namespace VRM
{
[Serializable]
public class VRMExportSettings
{
/// <summary>
/// エクスポート対象
/// </summary>
public GameObject Source;
#region Meta
/// <summary>
/// エクスポート名
/// </summary>
public string Title;
/// <summary>
/// エクスポートバージョン(エクスポートするModelのバージョン)
/// </summary>
public string Version;
/// <summary>
/// 作者
/// </summary>
public string Author;
/// <summary>
/// 作者連絡先
/// </summary>
public string ContactInformation;
/// <summary>
/// 作品引用
/// </summary>
public string Reference;
#endregion
#region Settings
/// <summary>
/// エクスポート時に強制的にT-Pose化する
/// </summary>
[Tooltip("Option")]
public bool ForceTPose = false;
/// <summary>
/// エクスポート時にヒエラルキーの正規化を実施する
/// </summary>
[Tooltip("Require only first time")]
public bool PoseFreeze = true;
/// <summary>
/// エクスポート時に新しいJsonSerializerを使う
/// </summary>
[Tooltip("Use new JSON serializer")]
public bool UseExperimentalExporter = false;
/// <summary>
/// BlendShapeのシリアライズにSparseAccessorを使う
/// </summary>
[Tooltip("Use sparse accessor for blendshape. This may reduce vrm size")]
public bool UseSparseAccessor = true;
/// <summary>
/// BlendShapeのPositionのみをエクスポートする
/// </summary>
[Tooltip("UniVRM-0.54 or later can load it. Otherwise fail to load")]
public bool OnlyBlendshapePosition = false;
/// <summary>
/// エクスポート時にBlendShapeClipから参照されないBlendShapeを削除する
/// </summary>
[Tooltip("Remove blendshape that is not used from BlendShapeClip")]
public bool ReduceBlendshape = false;
/// <summary>
/// skip if BlendShapeClip.Preset == Unknown
/// </summary>
[Tooltip("Remove blendShapeClip that preset is Unknown")]
public bool ReduceBlendshapeClip = false;
#endregion
public struct Validation
{
/// <summary>
/// エクスポート可能か否か。
/// true のメッセージは警告
/// false のメッセージはエラー
/// </summary>
public readonly bool CanExport;
public readonly String Message;
Validation(bool canExport, string message)
{
CanExport = canExport;
Message = message;
}
public static Validation Error(string msg)
{
return new Validation(false, msg);
}
public static Validation Warning(string msg)
{
return new Validation(true, msg);
}
}
/// <summary>
/// ボーン名の重複を確認
/// </summary>
/// <returns></returns>
bool DuplicateBoneNameExists()
{
var bones = Source.transform.Traverse().ToArray();
var duplicates = bones
.GroupBy(p => p.name)
.Where(g => g.Count() > 1)
.Select(g => g.Key);
return (duplicates.Any());
}
/// <summary>
/// エクスポート可能か検証する
/// </summary>
/// <returns></returns>
public IEnumerable<Validation> Validate()
{
if (Source == null)
{
yield return Validation.Error("Require source");
yield break;
}
var animator = Source.GetComponent<Animator>();
if (animator == null)
{
yield return Validation.Error("Require animator. ");
}
else if (animator.avatar == null)
{
yield return Validation.Error("Require animator.avatar. ");
}
else if (!animator.avatar.isValid)
{
yield return Validation.Error("Animator.avatar is not valid. ");
}
else if (!animator.avatar.isHuman)
{
yield return Validation.Error("Animator.avatar is not humanoid. Please change model's AnimationType to humanoid. ");
}
var jaw = animator.GetBoneTransform(HumanBodyBones.Jaw);
if (jaw != null)
{
yield return Validation.Warning("Jaw bone is included. It may not be what you intended. Please check the humanoid avatar setting screen");
}
if (DuplicateBoneNameExists())
{
yield return Validation.Error("Find duplicate Bone names. Please check model's bone names. ");
}
if (string.IsNullOrEmpty(Title))
{
yield return Validation.Error("Require Title. ");
}
if (string.IsNullOrEmpty(Version))
{
yield return Validation.Error("Require Version. ");
}
if (string.IsNullOrEmpty(Author))
{
yield return Validation.Error("Require Author. ");
}
if (ReduceBlendshape && Source.GetComponent<VRMBlendShapeProxy>() == null)
{
yield return Validation.Error("ReduceBlendshapeSize is need VRMBlendShapeProxy, you need to convert to VRM once.");
}
var renderers = Source.GetComponentsInChildren<Renderer>();
if (renderers.All(x => !x.gameObject.activeInHierarchy))
{
yield return Validation.Error("No active mesh");
}
var materials = renderers.SelectMany(x => x.sharedMaterials).Distinct();
foreach (var material in materials)
{
if (material.shader.name == "Standard")
{
// standard
continue;
}
if (MaterialExporter.UseUnlit(material.shader.name))
{
// unlit
continue;
}
if (VRMMaterialExporter.VRMExtensionShaders.Contains(material.shader.name))
{
// VRM supported
continue;
}
yield return Validation.Warning(string.Format("unknown material '{0}' is used. this will export as `Standard` fallback", material.shader.name));
}
}
/// <summary>
/// 対象のモデルからMeta情報を取得し、エクスポート設定を初期する
/// </summary>
/// <param name="go"></param>
public void InitializeFrom(GameObject go)
{
if (Source == go) return;
Source = go;
//
// initialize
//
var desc = Source == null ? null : go.GetComponent<VRMHumanoidDescription>();
if (desc == null)
{
// 初回のVRMエクスポートとみなす
ForceTPose = false; // option
PoseFreeze = true;
}
else
{
// すでに正規化済みとみなす
ForceTPose = false;
PoseFreeze = false;
}
//
// Meta
//
var meta = Source == null ? null : go.GetComponent<VRMMeta>();
if (meta != null && meta.Meta != null)
{
Title = meta.Meta.Title;
Version = string.IsNullOrEmpty(meta.Meta.Version) ? "0.0" : meta.Meta.Version;
Author = meta.Meta.Author;
ContactInformation = meta.Meta.ContactInformation;
Reference = meta.Meta.Reference;
}
else
{
Title = go.name;
Version = "0.0";
}
}
}
}

View File

@@ -1,93 +1,9 @@
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor;
using UnityEngine;
namespace VRM
{
public class VRMExporterWizard : ScriptableWizard
{
const string EXTENSION = ".vrm";
VRMMeta m_meta;
private static string m_lastExportDir;
public VRMExportSettings m_settings = new VRMExportSettings();
public static void CreateWizard()
{
var wiz = ScriptableWizard.DisplayWizard<VRMExporterWizard>(
"VRM Exporter", "Export");
var go = Selection.activeObject as GameObject;
// update checkbox
wiz.m_settings.InitializeFrom(go);
wiz.OnWizardUpdate();
}
void OnEnable()
{
// Debug.Log("OnEnable");
Undo.willFlushUndoRecord += OnWizardUpdate;
}
void OnDisable()
{
// Debug.Log("OnDisable");
Undo.willFlushUndoRecord -= OnWizardUpdate;
}
void OnWizardCreate()
{
string directory;
if (string.IsNullOrEmpty(m_lastExportDir))
directory = Directory.GetParent(Application.dataPath).ToString();
else
directory = m_lastExportDir;
// save dialog
var path = EditorUtility.SaveFilePanel(
"Save vrm",
directory,
m_settings.Source.name + EXTENSION,
EXTENSION.Substring(1));
if (string.IsNullOrEmpty(path))
{
return;
}
m_lastExportDir = Path.GetDirectoryName(path).Replace("\\", "/");
// export
m_settings.Export(path);
}
void OnWizardUpdate()
{
isValid = true;
var helpBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
foreach (var validation in m_settings.CanExport())
{
if (!validation.CanExport)
{
isValid = false;
errorBuilder.Append(validation.Message);
}
else
{
helpBuilder.AppendLine(validation.Message);
}
}
helpString = helpBuilder.ToString();
errorString = errorBuilder.ToString();
}
}
public static class VRMExporterMenu
{
const string CONVERT_HUMANOID_KEY = VRMVersion.MENU + "/Export humanoid";

View File

@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: a8e41aa30fcc76e43ad8588aef8572ea
timeCreated: 1520491195
licenseType: Free
guid: a1429b9028f33544e94aa367c2acb7fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0

View File

@@ -0,0 +1,90 @@
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace VRM
{
public class VRMExporterWizard : ScriptableWizard
{
const string EXTENSION = ".vrm";
VRMMeta m_meta;
private static string m_lastExportDir;
public VRMExportSettings m_settings = new VRMExportSettings();
public static void CreateWizard()
{
var wiz = ScriptableWizard.DisplayWizard<VRMExporterWizard>(
"VRM Exporter", "Export");
var go = Selection.activeObject as GameObject;
// update checkbox
wiz.m_settings.InitializeFrom(go);
wiz.OnWizardUpdate();
}
void OnEnable()
{
// Debug.Log("OnEnable");
Undo.willFlushUndoRecord += OnWizardUpdate;
}
void OnDisable()
{
// Debug.Log("OnDisable");
Undo.willFlushUndoRecord -= OnWizardUpdate;
}
void OnWizardCreate()
{
string directory;
if (string.IsNullOrEmpty(m_lastExportDir))
directory = Directory.GetParent(Application.dataPath).ToString();
else
directory = m_lastExportDir;
// save dialog
var path = EditorUtility.SaveFilePanel(
"Save vrm",
directory,
m_settings.Source.name + EXTENSION,
EXTENSION.Substring(1));
if (string.IsNullOrEmpty(path))
{
return;
}
m_lastExportDir = Path.GetDirectoryName(path).Replace("\\", "/");
// export
VRMEditorExporter.Export(path, m_settings);
}
void OnWizardUpdate()
{
isValid = true;
var helpBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
foreach (var validation in m_settings.Validate())
{
if (!validation.CanExport)
{
isValid = false;
errorBuilder.Append(validation.Message);
}
else
{
helpBuilder.AppendLine(validation.Message);
}
}
helpString = helpBuilder.ToString();
errorString = errorBuilder.ToString();
}
}
}

View File

@@ -1,6 +1,6 @@
fileFormatVersion: 2
guid: 01708ecf1aa756948be6996d987684a7
timeCreated: 1532063961
guid: a8e41aa30fcc76e43ad8588aef8572ea
timeCreated: 1520491195
licenseType: Free
MonoImporter:
serializedVersion: 2

View File

@@ -48,14 +48,8 @@ namespace VRM
{
var go = Selection.activeObject as GameObject;
GameObject normalizedRoot = null;
using (new VRMExportSettings.RecordDisposer(go.transform.Traverse().ToArray(), "before normalize"))
{
var normalized = BoneNormalizer.Execute(go, true, false);
VRMExportSettings.CopyVRMComponents(go, normalized.Root, normalized.BoneMap);
normalizedRoot = normalized.Root;
}
Selection.activeGameObject = normalizedRoot;
// BoneNormalizer.Execute はコピーを正規化する。UNDO無用
Selection.activeGameObject = BoneNormalizer.Execute(go, true, false);
}
}
}

View File

@@ -2,12 +2,17 @@
"name": "UniVRM.Editor",
"references": [
"VRM",
"UniJSON"
"UniJSON",
"UniHumanoid"
],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -1,12 +0,0 @@
using UnityEngine;
namespace VRM
{
[CreateAssetMenu(menuName = "VRM/ExportObject")]
public class VRMExportObject : ScriptableObject
{
[SerializeField]
public VRMExportSettings Settings = new VRMExportSettings();
}
}

View File

@@ -1,482 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UniGLTF;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace VRM
{
[Serializable]
public class VRMExportSettings
{
public GameObject Source;
public string Title;
public string Version;
public string Author;
public string ContactInformation;
public string Reference;
public bool ForceTPose = true;
public bool PoseFreeze = true;
public bool UseExperimentalExporter = false;
public bool ReduceBlendshapeSize = false;
public struct Validation
{
public readonly bool CanExport;
public readonly String Message;
Validation(bool canExport, string message)
{
CanExport = canExport;
Message = message;
}
public static Validation Error(string msg)
{
return new Validation(false, msg);
}
public static Validation Warning(string msg)
{
return new Validation(true, msg);
}
}
public IEnumerable<Validation> CanExport()
{
if (Source == null)
{
yield return Validation.Error("Require source");
yield break;
}
var animator = Source.GetComponent<Animator>();
if (animator == null)
{
yield return Validation.Error("Require animator. ");
}
else if (animator.avatar == null)
{
yield return Validation.Error("Require animator.avatar. ");
}
else if (!animator.avatar.isValid)
{
yield return Validation.Error("Animator.avatar is not valid. ");
}
else if (!animator.avatar.isHuman)
{
yield return Validation.Error("Animator.avatar is not humanoid. Please change model's AnimationType to humanoid. ");
}
var jaw = animator.GetBoneTransform(HumanBodyBones.Jaw);
if (jaw != null)
{
yield return Validation.Warning("Jaw bone is included. It may not be what you intended. Please check the humanoid avatar setting screen");
}
if (DuplicateBoneNameExists())
{
yield return Validation.Error("Find duplicate Bone names. Please check model's bone names. ");
}
if (string.IsNullOrEmpty(Title))
{
yield return Validation.Error("Require Title. ");
}
if (string.IsNullOrEmpty(Version))
{
yield return Validation.Error("Require Version. ");
}
if (string.IsNullOrEmpty(Author))
{
yield return Validation.Error("Require Author. ");
}
if (ReduceBlendshapeSize && Source.GetComponent<VRMBlendShapeProxy>() == null)
{
yield return Validation.Error("ReduceBlendshapeSize is need VRMBlendShapeProxy, you need to convert to VRM once.");
}
var renderers = Source.GetComponentsInChildren<Renderer>();
if (renderers.All(x => !x.gameObject.activeInHierarchy))
{
yield return Validation.Error("No active mesh");
}
var materials = renderers.SelectMany(x => x.sharedMaterials).Distinct();
foreach (var material in materials)
{
if (material.shader.name == "Standard")
{
// standard
continue;
}
if (MaterialExporter.UseUnlit(material.shader.name))
{
// unlit
continue;
}
if (VRMMaterialExporter.VRMExtensionShaders.Contains(material.shader.name))
{
// VRM supported
continue;
}
yield return Validation.Warning(string.Format("unknown material '{0}' is used. this will export as `Standard` fallback", material.shader.name));
}
}
public void InitializeFrom(GameObject go)
{
if (Source == go) return;
Source = go;
var desc = Source == null ? null : go.GetComponent<VRMHumanoidDescription>();
if (desc == null)
{
ForceTPose = true;
PoseFreeze = true;
}
else
{
ForceTPose = false;
PoseFreeze = false;
}
var meta = Source == null ? null : go.GetComponent<VRMMeta>();
if (meta != null && meta.Meta != null)
{
Title = meta.Meta.Title;
Version = string.IsNullOrEmpty(meta.Meta.Version) ? "0.0" : meta.Meta.Version;
Author = meta.Meta.Author;
ContactInformation = meta.Meta.ContactInformation;
Reference = meta.Meta.Reference;
}
else
{
Title = go.name;
Version = "0.0";
}
}
//
// トップレベルのMonoBehaviourを移植する
//
public static void CopyVRMComponents(GameObject go, GameObject root,
Dictionary<Transform, Transform> map)
{
{
// blendshape
var src = go.GetComponent<VRMBlendShapeProxy>();
if (src != null)
{
var dst = root.AddComponent<VRMBlendShapeProxy>();
dst.BlendShapeAvatar = src.BlendShapeAvatar;
}
}
{
var secondary = go.transform.Find("secondary");
if (secondary == null)
{
secondary = go.transform;
}
var dstSecondary = root.transform.Find("secondary");
if (dstSecondary == null)
{
dstSecondary = new GameObject("secondary").transform;
dstSecondary.SetParent(root.transform, false);
}
// 揺れモノ
foreach (var src in go.transform.Traverse().Select(x => x.GetComponent<VRMSpringBoneColliderGroup>()).Where(x => x != null))
{
var dst = map[src.transform];
var dstColliderGroup = dst.gameObject.AddComponent<VRMSpringBoneColliderGroup>();
dstColliderGroup.Colliders = src.Colliders.Select(y =>
{
var offset = dst.worldToLocalMatrix.MultiplyPoint(src.transform.localToWorldMatrix.MultiplyPoint(y.Offset));
return new VRMSpringBoneColliderGroup.SphereCollider
{
Offset = offset,
Radius = y.Radius
};
}).ToArray();
}
foreach (var src in go.transform.Traverse().SelectMany(x => x.GetComponents<VRMSpringBone>()))
{
// Copy VRMSpringBone
var dst = dstSecondary.gameObject.AddComponent<VRMSpringBone>();
dst.m_comment = src.m_comment;
dst.m_stiffnessForce = src.m_stiffnessForce;
dst.m_gravityPower = src.m_gravityPower;
dst.m_gravityDir = src.m_gravityDir;
dst.m_dragForce = src.m_dragForce;
if (src.m_center != null)
{
dst.m_center = map[src.m_center];
}
dst.RootBones = src.RootBones.Select(x => map[x]).ToList();
dst.m_hitRadius = src.m_hitRadius;
if (src.ColliderGroups != null)
{
dst.ColliderGroups = src.ColliderGroups
.Select(x => map[x.transform].GetComponent<VRMSpringBoneColliderGroup>()).ToArray();
}
}
}
#pragma warning disable 0618
{
// meta(obsolete)
var src = go.GetComponent<VRMMetaInformation>();
if (src != null)
{
src.CopyTo(root);
}
}
#pragma warning restore 0618
{
// meta
var src = go.GetComponent<VRMMeta>();
if (src != null)
{
var dst = root.AddComponent<VRMMeta>();
dst.Meta = src.Meta;
}
}
{
// firstPerson
var src = go.GetComponent<VRMFirstPerson>();
if (src != null)
{
src.CopyTo(root, map);
}
}
{
// humanoid
var dst = root.AddComponent<VRMHumanoidDescription>();
var src = go.GetComponent<VRMHumanoidDescription>();
if (src != null)
{
dst.Avatar = src.Avatar;
dst.Description = src.Description;
}
else
{
var animator = go.GetComponent<Animator>();
if (animator != null)
{
dst.Avatar = animator.avatar;
}
}
}
}
public static bool IsPrefab(GameObject go)
{
return !go.scene.IsValid();
}
#if UNITY_EDITOR
public struct RecordDisposer : IDisposable
{
int _group;
public RecordDisposer(UnityEngine.Object[] objects, string msg)
{
Undo.IncrementCurrentGroup();
_group = Undo.GetCurrentGroup();
Undo.RecordObjects(objects, msg);
}
public void Dispose()
{
Undo.RevertAllDownToGroup(_group);
}
}
public void Export(string path)
{
List<GameObject> destroy = new List<GameObject>();
try
{
Export(path, destroy);
}
finally
{
foreach (var x in destroy)
{
Debug.LogFormat("destroy: {0}", x.name);
GameObject.DestroyImmediate(x);
}
}
}
void Export(string path, List<GameObject> destroy)
{
var target = Source;
if (IsPrefab(target))
{
using (new RecordDisposer(Source.transform.Traverse().ToArray(), "before normalize"))
{
target = GameObject.Instantiate(target);
destroy.Add(target);
}
}
if (PoseFreeze)
{
using (new RecordDisposer(target.transform.Traverse().ToArray(), "before normalize"))
{
var normalized = BoneNormalizer.Execute(target, ForceTPose, false);
CopyVRMComponents(target, normalized.Root, normalized.BoneMap);
target = normalized.Root;
destroy.Add(target);
}
}
// remove unused blendShape
if (ReduceBlendshapeSize)
{
var proxy = target.GetComponent<VRMBlendShapeProxy>();
// 元のBlendShapeClipに変更を加えないように複製
var copyBlendShapeAvatar = GameObject.Instantiate(proxy.BlendShapeAvatar);
var copyBlendShapClips = new List<BlendShapeClip>();
foreach (var clip in proxy.BlendShapeAvatar.Clips)
{
copyBlendShapClips.Add(GameObject.Instantiate(clip));
}
var skinnedMeshRenderers = target.GetComponentsInChildren<SkinnedMeshRenderer>();
var names = new Dictionary<int, string>();
var vs = new Dictionary<int, Vector3[]>();
var ns = new Dictionary<int, Vector3[]>();
var ts = new Dictionary<int, Vector3[]>();
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers)
{
Mesh mesh = smr.sharedMesh;
if (mesh == null) continue;
if (mesh.blendShapeCount == 0) continue;
var copyMesh = mesh.Copy(true);
var vCount = copyMesh.vertexCount;
names.Clear();
vs.Clear();
ns.Clear();
ts.Clear();
var usedBlendshapeIndexArray = copyBlendShapClips
.SelectMany(clip => clip.Values)
.Where(val => target.transform.Find(val.RelativePath) == smr.transform)
.Select(val => val.Index)
.Distinct()
.ToArray();
foreach (var i in usedBlendshapeIndexArray)
{
var name = copyMesh.GetBlendShapeName(i);
var vertices = new Vector3[vCount];
var normals = new Vector3[vCount];
var tangents = new Vector3[vCount];
copyMesh.GetBlendShapeFrameVertices(i, 0, vertices, normals, tangents);
names.Add(i, name);
vs.Add(i, vertices);
ns.Add(i, normals);
ts.Add(i, tangents);
}
copyMesh.ClearBlendShapes();
foreach (var i in usedBlendshapeIndexArray)
{
copyMesh.AddBlendShapeFrame(names[i], 100f, vs[i], ns[i], ts[i]);
}
var indexMapper = usedBlendshapeIndexArray
.Select((x, i) => new { x, i })
.ToDictionary(pair => pair.x, pair => pair.i);
foreach (var clip in copyBlendShapClips)
{
for (var i = 0; i < clip.Values.Length; ++i)
{
var value = clip.Values[i];
if (target.transform.Find(value.RelativePath) != smr.transform) continue;
value.Index = indexMapper[value.Index];
clip.Values[i] = value;
}
}
copyBlendShapeAvatar.Clips = copyBlendShapClips;
proxy.BlendShapeAvatar = copyBlendShapeAvatar;
smr.sharedMesh = copyMesh;
}
}
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var vrm = VRMExporter.Export(target, ReduceBlendshapeSize);
vrm.extensions.VRM.meta.title = Title;
vrm.extensions.VRM.meta.version = Version;
vrm.extensions.VRM.meta.author = Author;
vrm.extensions.VRM.meta.contactInformation = ContactInformation;
vrm.extensions.VRM.meta.reference = Reference;
var bytes = vrm.ToGlbBytes(UseExperimentalExporter ? SerializerTypes.Generated : SerializerTypes.UniJSON);
File.WriteAllBytes(path, bytes);
Debug.LogFormat("Export elapsed {0}", sw.Elapsed);
}
if (path.StartsWithUnityAssetPath())
{
AssetDatabase.ImportAsset(path.ToUnityRelativePath());
}
}
#endif
//ここで重複ボーン名のチェックをする
bool DuplicateBoneNameExists()
{
var bones = Source.transform.Traverse().ToArray();
var duplicates = bones
.GroupBy(p => p.name)
.Where(g => g.Count() > 1)
.Select(g => g.Key);
return (duplicates.Any());
}
}
}

View File

@@ -19,17 +19,21 @@ namespace VRM
gltf.extensions.VRM = new glTF_VRM_extensions();
}
public new static glTF Export(GameObject go, bool exportOnlyBlendShapePosition = false)
public static glTF Export(GameObject go, bool exportOnlyBlendShapePosition = false)
{
var config = VRMExporterConfiguration.Default;
config.ExportOnlyBlendShapePosition = exportOnlyBlendShapePosition;
return Export(go, config);
}
public static glTF Export(GameObject go, VRMExporterConfiguration configuration)
{
var gltf = new glTF();
using (var exporter = new VRMExporter(gltf)
{
#if VRM_EXPORTER_USE_SPARSE
// experimental
UseSparseAccessorForBlendShape = true
#endif
ExportOnlyBlendShapePosition = exportOnlyBlendShapePosition
UseSparseAccessorForBlendShape = configuration.UseSparseAccessorForBlendShape,
ExportOnlyBlendShapePosition = configuration.ExportOnlyBlendShapePosition
})
{
_Export(gltf, exporter, go);

View File

@@ -0,0 +1,14 @@
namespace VRM
{
public struct VRMExporterConfiguration
{
public bool UseSparseAccessorForBlendShape;
public bool ExportOnlyBlendShapePosition;
public static VRMExporterConfiguration Default => new VRMExporterConfiguration
{
UseSparseAccessorForBlendShape = true,
ExportOnlyBlendShapePosition = false,
};
}
}

View File

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

View File

@@ -181,9 +181,10 @@ namespace VRM
var indexMap =
srcBones
.Select((x, i) => new { i, x })
.Select(x => {
.Select(x =>
{
Transform dstBone;
if(boneMap.TryGetValue(x.x, out dstBone))
if (boneMap.TryGetValue(x.x, out dstBone))
{
return dstBones.IndexOf(dstBone);
}
@@ -191,7 +192,7 @@ namespace VRM
{
return -1;
}
})
})
.ToArray();
for (int i = 0; i < srcBones.Length; ++i)
@@ -322,14 +323,14 @@ namespace VRM
var mesh = srcMesh.Copy(false);
mesh.name = srcMesh.name + ".baked";
srcRenderer.BakeMesh(mesh);
var blendShapeValues = new Dictionary<int,float>();
var blendShapeValues = new Dictionary<int, float>();
for (int i = 0; i < srcMesh.blendShapeCount; i++)
{
var val = srcRenderer.GetBlendShapeWeight(i);
if (val > 0) blendShapeValues.Add(i, val);
}
mesh.boneWeights = MapBoneWeight(srcMesh.boneWeights, boneMap, srcRenderer.bones, dstBones); // restore weights. clear when BakeMesh
// recalc bindposes
@@ -385,7 +386,7 @@ namespace VRM
srcRenderer.SetBlendShapeWeight(i, value);
Vector3[] vertices = blendShapeMesh.vertices;
for (int j = 0; j < vertices.Length; ++j)
{
if (originalBlendShapePositions[j] == Vector3.zero)
@@ -404,7 +405,7 @@ namespace VRM
if (originalBlendShapeNormals[j] == Vector3.zero)
{
normals[j] = Vector3.zero;
}
else
{
@@ -428,7 +429,7 @@ namespace VRM
#endif
var frameCount = srcMesh.GetBlendShapeFrameCount(i);
for(int f=0; f<frameCount; f++)
for (int f = 0; f < frameCount; f++)
{
var weight = srcMesh.GetBlendShapeFrameWeight(i, f);
@@ -520,7 +521,7 @@ namespace VRM
/// <param name="go">対象モデルのルート</param>
/// <param name="forceTPose">強制的にT-Pose化するか</param>
/// <returns>正規化済みのモデル</returns>
public static NormalizedResult Execute(GameObject go, bool forceTPose, bool clearBlendShapeBeforeNormalize)
public static GameObject Execute(GameObject go, bool forceTPose, bool clearBlendShapeBeforeNormalize)
{
Dictionary<Transform, Transform> boneMap = new Dictionary<Transform, Transform>();
@@ -564,11 +565,137 @@ namespace VRM
NormalizeNoneSkinnedMesh(src, dst);
}
return new NormalizedResult
CopyVRMComponents(go, normalized, boneMap);
// return new NormalizedResult
// {
// Root = normalized,
// BoneMap = boneMap
// };
return normalized;
}
/// <summary>
/// VRMを構成するコンポーネントをコピーする。
/// </summary>
/// <param name="go">コピー元</param>
/// <param name="root">コピー先</param>
/// <param name="map">コピー元とコピー先の対応関係</param>
static void CopyVRMComponents(GameObject go, GameObject root,
Dictionary<Transform, Transform> map)
{
{
Root = normalized,
BoneMap = boneMap
};
// blendshape
var src = go.GetComponent<VRMBlendShapeProxy>();
if (src != null)
{
var dst = root.AddComponent<VRMBlendShapeProxy>();
dst.BlendShapeAvatar = src.BlendShapeAvatar;
}
}
{
var secondary = go.transform.Find("secondary");
if (secondary == null)
{
secondary = go.transform;
}
var dstSecondary = root.transform.Find("secondary");
if (dstSecondary == null)
{
dstSecondary = new GameObject("secondary").transform;
dstSecondary.SetParent(root.transform, false);
}
// 揺れモノ
foreach (var src in go.transform.GetComponentsInChildren<VRMSpringBoneColliderGroup>())
{
var dst = map[src.transform];
var dstColliderGroup = dst.gameObject.AddComponent<VRMSpringBoneColliderGroup>();
dstColliderGroup.Colliders = src.Colliders.Select(y =>
{
var offset = dst.worldToLocalMatrix.MultiplyPoint(src.transform.localToWorldMatrix.MultiplyPoint(y.Offset));
return new VRMSpringBoneColliderGroup.SphereCollider
{
Offset = offset,
Radius = y.Radius
};
}).ToArray();
}
foreach (var src in go.transform.GetComponentsInChildren<VRMSpringBone>())
{
// Copy VRMSpringBone
var dst = dstSecondary.gameObject.AddComponent<VRMSpringBone>();
dst.m_comment = src.m_comment;
dst.m_stiffnessForce = src.m_stiffnessForce;
dst.m_gravityPower = src.m_gravityPower;
dst.m_gravityDir = src.m_gravityDir;
dst.m_dragForce = src.m_dragForce;
if (src.m_center != null)
{
dst.m_center = map[src.m_center];
}
dst.RootBones = src.RootBones.Select(x => map[x]).ToList();
dst.m_hitRadius = src.m_hitRadius;
if (src.ColliderGroups != null)
{
dst.ColliderGroups = src.ColliderGroups
.Select(x => map[x.transform].GetComponent<VRMSpringBoneColliderGroup>()).ToArray();
}
}
}
#pragma warning disable 0618
{
// meta(obsolete)
var src = go.GetComponent<VRMMetaInformation>();
if (src != null)
{
src.CopyTo(root);
}
}
#pragma warning restore 0618
{
// meta
var src = go.GetComponent<VRMMeta>();
if (src != null)
{
var dst = root.AddComponent<VRMMeta>();
dst.Meta = src.Meta;
}
}
{
// firstPerson
var src = go.GetComponent<VRMFirstPerson>();
if (src != null)
{
src.CopyTo(root, map);
}
}
{
// humanoid
var dst = root.AddComponent<VRMHumanoidDescription>();
var src = go.GetComponent<VRMHumanoidDescription>();
if (src != null)
{
dst.Avatar = src.Avatar;
dst.Description = src.Description;
}
else
{
var animator = go.GetComponent<Animator>();
if (animator != null)
{
dst.Avatar = animator.avatar;
}
}
}
}
}
}