mirror of
https://github.com/vrm-c/UniVRM.git
synced 2026-09-07 19:38:13 -05:00
Merge pull request #421 from ousttrue/exporter_blendshape_optimize
Add blendshape options to export dialog
This commit is contained in:
26
Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs
Normal file
26
Assets/VRM/UniVRM/Editor/Format/RecordDisposer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8ed5cb82dd13bf43a1556b24a6d13a0
|
||||
timeCreated: 1532063757
|
||||
licenseType: Free
|
||||
guid: b304ed2aeece5a54191a5a8b69d9d113
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
175
Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs
Normal file
175
Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs.meta
Normal file
11
Assets/VRM/UniVRM/Editor/Format/VRMEditorExporter.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cea266830a7f57843bb928d0ea37bcbc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs
Normal file
266
Assets/VRM/UniVRM/Editor/Format/VRMExportSettings.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8e41aa30fcc76e43ad8588aef8572ea
|
||||
timeCreated: 1520491195
|
||||
licenseType: Free
|
||||
guid: a1429b9028f33544e94aa367c2acb7fb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
|
||||
90
Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs
Normal file
90
Assets/VRM/UniVRM/Editor/Format/VRMExporterWizard.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01708ecf1aa756948be6996d987684a7
|
||||
timeCreated: 1532063961
|
||||
guid: a8e41aa30fcc76e43ad8588aef8572ea
|
||||
timeCreated: 1520491195
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace VRM
|
||||
{
|
||||
[CreateAssetMenu(menuName = "VRM/ExportObject")]
|
||||
public class VRMExportObject : ScriptableObject
|
||||
{
|
||||
[SerializeField]
|
||||
public VRMExportSettings Settings = new VRMExportSettings();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
14
Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs
Normal file
14
Assets/VRM/UniVRM/Scripts/Format/VRMExporterConfiguation.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 974aea88b487e5543b1c39e5a528f751
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user