Files

939 lines
39 KiB
C#

using Gallop;
using LibMMD.Material;
using LibMMD.Model;
using LibMMD.Reader;
using LibMMD.Unity3D;
using LibMMD.Writer;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Rendering;
using Gallop.Live.Cutt; // For FacialMouthId
using static BillboardBuilder;
using static LibMMD.Model.Morph;
using static LibMMD.Model.SkinningOperator;
//using UnityGLTF;
public class ModelExporter
{
static string modelInfo = $"Generated by UmaViewer {Application.version}, Copyright Cygames \nLink : https://github.com/katboi01/UmaViewer";
// 仅过滤浮点烘焙产生的近零噪声;平方阈值 1e-12 对应实际位移 1e-6。
private const float MorphVertexNoiseSqrThreshold = 0.000000000001f;
private const int VmdMorphNameMaxShiftJisBytes = 15;
// 标准 MMD 表情按部位收集,最终烘焙为 Blender、MMD 与 MMDSkin 均可见的 Vertex Morph。
private enum MorphPart
{
Eyebrow,
Eye,
Mouth
}
public static void ExportModel(UmaContainerCharacter container, string path)
{
PMXExportStateSnapshot stateSnapshot = new PMXExportStateSnapshot(container);
try
{
stateSnapshot.FreezeForExport();
// PMX 与 VMD 共用同一套 MMD 默认 A-pose,避免导出当前动画帧或近似 T-pose。
stateSnapshot.ApplyMmdReferencePose();
container.FaceDrivenKeyTarget?.FacialResetAll();
AddBlendShape(container);
BuildBillboard(container);
var textures = TextureExporter.ExportAllTexture(Path.GetDirectoryName(path), container.gameObject);
var model = ReadPMXModel(container, textures, container.CharaEntry);
using (FileStream fileStream = new FileStream(path, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
var config = new ModelConfig() { GlobalToonPath = "Toon" };
PMXWriter.Write(writer, model, config);
}
}
finally
{
try
{
RemoveBillboard(container);
ClearBlendShape(container);
}
finally
{
// 临时导出资源清理失败时,也必须恢复角色的姿势和运行时组件。
stateSnapshot.Dispose();
}
}
UmaViewerUI.Instance.ShowMessage($"PMX Save at {path}", UIMessageType.Success);
}
public static void ExportModel(UmaContainer container, string path)
{
var textures = TextureExporter.ExportAllTexture(Path.GetDirectoryName(path), container.gameObject);
var model = ReadPMXModel(container, textures);
FileStream fileStream = new FileStream(path, FileMode.Create);
BinaryWriter writer = new BinaryWriter(fileStream);
var config = new ModelConfig() { GlobalToonPath = "Toon" };
PMXWriter.Write(writer, model, config);
writer.Close();
fileStream.Close();
UmaViewerUI.Instance.ShowMessage($"PMX Save at {path}", UIMessageType.Success);
}
public static RawMMDModel ReadPMXModel(UmaContainer container, string[] textures, CharaEntry entry = null)
{
RawMMDModel model = new RawMMDModel();
if (entry != null)
{
model.Name = entry.Name;
model.NameEn = entry.GetName();
model.Description = model.DescriptionEn = $"{entry.GetName()}\n{modelInfo}";
}
else
{
model.Name = container.gameObject.name;
model.NameEn = container.gameObject.name;
model.Description = model.DescriptionEn = $"{container.gameObject.name}\n{modelInfo}";
}
// 仅从蒙皮引用收集骨骼,避免把 Handle、Pole、Target 等运行时控制节点导出为 PMX 骨骼。
var rootBone = container.transform.Find("Position");
if (rootBone == null)
{
rootBone = container.transform;
}
List<Renderer> renderers = new List<Renderer>(container.GetComponentsInChildren<Renderer>());
// Geometry, materials and morphs must consume the same mesh-only renderer sequence.
renderers.RemoveAll(renderer => !(renderer is MeshRenderer) && !(renderer is SkinnedMeshRenderer));
PMXPhysicsExporter.Context physicsContext = null;
if (container is UmaContainerCharacter physicsCharacter)
{
physicsContext = PMXPhysicsExporter.Prepare(physicsCharacter, rootBone);
}
PMXBoneExporter.Result boneResult = PMXBoneExporter.Build(
rootBone,
container.transform,
renderers,
physicsContext != null ? physicsContext.DynamicBones : null);
//Read vertices And triangles
List<int> triangles = new List<int>();
model.Vertices = ReadVerticesAndTriangles(renderers, boneResult.BoneIndexes, ref triangles, container.transform);
model.TriangleIndexes = triangles.ToArray();
//Read Texture reference
foreach (var texture in textures)
{
model.TextureList.Add(new MMDTexture(texture));
}
model.Parts = ReadPartMaterials(renderers, model);
model.Bones = boneResult.Bones;
if(container is UmaContainerCharacter chara)
{
ClearBlendShape(chara);
AddBlendShape(chara);
}
model.Morphs = ReadMorph(renderers);
if (physicsContext != null)
{
PMXPhysicsExporter.Build(physicsContext, container.transform, boneResult, model);
}
else
{
model.Rigidbodies = new MMDRigidBody[0];
model.Joints = new MMDJoint[0];
}
return model;
}
public static Vector3[] Vec4ToVec3(Vector4[] vector4s)
{
Vector3[] tmp = new Vector3[vector4s.Length];
for (int i = 0; i < vector4s.Length; i++)
{
tmp[i] = new Vector3(vector4s[i].x, vector4s[i].y, vector4s[i].z);
}
return tmp;
}
public static Vector3[] CalDelta(Vector3[] ori, Vector3[] end)
{
Vector3[] tmp = new Vector3[ori.Length];
for (int i = 0; i < ori.Length; i++)
{
tmp[i] = end[i] - ori[i];
}
return tmp;
}
private static void AddBlendShape(UmaContainerCharacter container)
{
if (!container.FaceDrivenKeyTarget) return;
SkinnedMeshRenderer faceMesh = null;
SkinnedMeshRenderer eyebrowMesh = null;
SkinnedMeshRenderer earMesh = null;
SkinnedMeshRenderer mouthMesh = null; // New
SkinnedMeshRenderer eyeMesh = null; // New
foreach (SkinnedMeshRenderer s in container.GetComponentsInChildren<SkinnedMeshRenderer>())
{
if (s.name.Equals("M_Face")) faceMesh = s;
else if (s.name.Equals("M_Mayu")) eyebrowMesh = s;
else if (s.name.Equals("M_Hair")) earMesh = s;
else if (s.name.Equals("M_Mouth")) mouthMesh = s; // New
else if (s.name.Equals("M_Eye")) eyeMesh = s; // New
}
var facial = container.FaceDrivenKeyTarget;
Action<SkinnedMeshRenderer, List<FacialMorph>, Mesh> addBlendShapePart = delegate (SkinnedMeshRenderer skin, List<FacialMorph> morphs, Mesh baseMesh)
{
container.FaceOverrideData = null;
HashSet<string> usedNames = new HashSet<string>(StringComparer.Ordinal);
for (int blendShapeIndex = 0; blendShapeIndex < skin.sharedMesh.blendShapeCount; blendShapeIndex++)
{
usedNames.Add(skin.sharedMesh.GetBlendShapeName(blendShapeIndex));
}
foreach (var morph in morphs)
{
facial.ClearAllWeights();
// [Fix] Reset weight to 1 (User request: 10x was too strong)
morph.weight = 1;
facial.ChangeMorph();
Mesh deltaMesh = new Mesh();
skin.BakeMesh(deltaMesh);
// VMD 名称限制为 Shift-JIS 15 字节;冲突时追加稳定数字后缀,不能静默丢表情。
string validName = MakeUniqueShiftJisName(morph.name, usedNames, VmdMorphNameMaxShiftJisBytes);
usedNames.Add(validName);
// [Fix] Use weight 100.0f (Standard Unity Full Weight)
// Previous value 1.0f meant "This delta is for 1% weight", causing MMD (100%) to explode the mesh 100x.
skin.sharedMesh.AddBlendShapeFrame(validName, 100.0f,
CalDelta(baseMesh.vertices, deltaMesh.vertices),
CalDelta(baseMesh.normals, deltaMesh.normals),
CalDelta(Vec4ToVec3(baseMesh.tangents), Vec4ToVec3(deltaMesh.tangents)));
}
};
if (faceMesh)
{
Mesh basefaceMesh = new Mesh();
faceMesh.BakeMesh(basefaceMesh);
addBlendShapePart(faceMesh, facial.EyeBrowMorphs, basefaceMesh);
addBlendShapePart(faceMesh, facial.EyeMorphs, basefaceMesh);
addBlendShapePart(faceMesh, facial.MouthMorphs, basefaceMesh);
}
if (eyebrowMesh)
{
Mesh baseEyeBrowMesh = new Mesh();
eyebrowMesh.BakeMesh(baseEyeBrowMesh);
addBlendShapePart(eyebrowMesh, facial.EyeBrowMorphs, baseEyeBrowMesh);
}
if (earMesh)
{
Mesh baseEarMesh = new Mesh();
earMesh.BakeMesh(baseEarMesh);
addBlendShapePart(earMesh, facial.EarMorphs, baseEarMesh);
}
// [New] Process Mouth Mesh
if (mouthMesh)
{
Mesh baseMouthMesh = new Mesh();
mouthMesh.BakeMesh(baseMouthMesh);
addBlendShapePart(mouthMesh, facial.MouthMorphs, baseMouthMesh);
}
// [New] Process Eye Mesh
if (eyeMesh)
{
Mesh baseEyeMesh = new Mesh();
eyeMesh.BakeMesh(baseEyeMesh);
addBlendShapePart(eyeMesh, facial.EyeMorphs, baseEyeMesh);
}
facial.ClearAllWeights();
facial.ChangeMorph();
}
private static void ClearBlendShape(UmaContainerCharacter container)
{
if (!container.FaceDrivenKeyTarget) return;
//assuming base meshes do not have any blend shapes
foreach (SkinnedMeshRenderer s in container.GetComponentsInChildren<SkinnedMeshRenderer>())
{
if (s.name.Equals("M_Face") || s.name.Equals("M_Mayu") || s.name.Equals("M_Hair") ||
s.name.Equals("M_Mouth") || s.name.Equals("M_Eye"))
{
s.sharedMesh.ClearBlendShapes();
}
}
}
private static string TruncateToShiftJIS(string s, int maxBytes)
{
var encoding = System.Text.Encoding.GetEncoding("shift_jis");
if (encoding.GetByteCount(s) <= maxBytes) return s;
// 按字符逐个累计字节,避免直接截断 Shift-JIS 双字节字符产生乱码。
int charCount = 0;
int byteCount = 0;
while (charCount < s.Length)
{
int currentBytes = encoding.GetByteCount(s.Substring(charCount, 1));
if (byteCount + currentBytes > maxBytes) break;
byteCount += currentBytes;
charCount++;
}
return s.Substring(0, charCount);
}
private static string MakeUniqueShiftJisName(string sourceName, HashSet<string> usedNames, int maxBytes)
{
string candidate = TruncateToShiftJIS(sourceName, maxBytes);
if (!usedNames.Contains(candidate)) return candidate;
// 从 2 开始表示同一截断名的第 2 项;后缀始终计入 15 字节限制。
for (int duplicateIndex = 2; ; duplicateIndex++)
{
string suffix = "_" + duplicateIndex;
int prefixBytes = maxBytes - System.Text.Encoding.GetEncoding("shift_jis").GetByteCount(suffix);
candidate = TruncateToShiftJIS(sourceName, System.Math.Max(0, prefixBytes)) + suffix;
if (!usedNames.Contains(candidate)) return candidate;
}
}
private static Morph[] ReadMorph(List<Renderer> renderers)
{
Dictionary<string, List<VertexMorphData>> mergedMorphData = new Dictionary<string, List<VertexMorphData>>();
List<string> rawMorphOrder = new List<string>();
int vertexOffset = 0;
foreach (Renderer renderer in renderers)
{
Mesh mesh;
if (renderer is MeshRenderer mr)
{
var meshfilter = mr.GetComponent<MeshFilter>();
mesh = meshfilter.mesh;
}
else
{
mesh = ((SkinnedMeshRenderer)renderer).sharedMesh;
}
var vertexCount = mesh.vertexCount;
for (int i = 0; i < mesh.blendShapeCount; i++)
{
string morphName = mesh.GetBlendShapeName(i);
int frameCount = mesh.GetBlendShapeFrameCount(i);
if (frameCount <= 0)
{
// 没有任何有效帧时无法取得顶点偏移,直接跳过该 BlendShape。
continue;
}
if (!mergedMorphData.ContainsKey(morphName))
{
mergedMorphData[morphName] = new List<VertexMorphData>();
rawMorphOrder.Add(morphName);
}
var deltaVertices = new Vector3[vertexCount];
var deltaNormals = new Vector3[vertexCount];
var deltaTangents = new Vector3[vertexCount];
// Unity BlendShape 可能含多个权重帧,导出最终有效帧而不是固定读取第 0 帧。
mesh.GetBlendShapeFrameVertices(i, frameCount - 1, deltaVertices, deltaNormals, deltaTangents);
for (int j = 0; j < vertexCount; j++)
{
// 只剔除真正零位移/数值噪声,保留脸部的细微顶点变化。
if (deltaVertices[j].sqrMagnitude > MorphVertexNoiseSqrThreshold)
{
var data = new VertexMorphData();
data.VertexIndex = vertexOffset + j;
data.Offset = deltaVertices[j];
mergedMorphData[morphName].Add(data);
}
}
}
vertexOffset += vertexCount;
}
List<Morph> finalMorphs = new List<Morph>();
HashSet<string> usedMorphNames = new HashSet<string>(StringComparer.Ordinal);
Dictionary<string, Dictionary<MorphPart, List<VertexMorphData>>> mappedMorphData =
new Dictionary<string, Dictionary<MorphPart, List<VertexMorphData>>>(StringComparer.Ordinal);
List<string> mappedMorphOrder = new List<string>();
// 标准入口名必须保持精确,先预留名称;原始 Morph 若同名则使用稳定数字后缀。
HashSet<string> reservedStandardNames = new HashSet<string>(StringComparer.Ordinal);
foreach (string rawName in rawMorphOrder)
{
if (mergedMorphData[rawName].Count == 0) continue;
string mappedName = GetMappedMMDName(rawName);
if (!string.IsNullOrEmpty(mappedName)) reservedStandardNames.Add(mappedName);
}
foreach (string rawName in rawMorphOrder)
{
List<VertexMorphData> rawData = mergedMorphData[rawName];
// 空 Morph 不包含任何有效顶点,既不导出原始项,也不参与标准表情组合。
if (rawData.Count == 0) continue;
string uniqueRawName = MakeUniqueMorphName(rawName, usedMorphNames, reservedStandardNames);
MorphPart rawPart = GetMorphPart(rawName);
finalMorphs.Add(CreateVertexMorph(uniqueRawName, rawName, rawData, GetOriginalMorphCategory(rawName)));
usedMorphNames.Add(uniqueRawName);
// 将左右同部位顶点合并,但不再跨眉、眼、口直接拼成同一个 Vertex Morph。
string mmdName = GetMappedMMDName(rawName);
if (!string.IsNullOrEmpty(mmdName))
{
if (!mappedMorphData.ContainsKey(mmdName))
{
mappedMorphData[mmdName] = new Dictionary<MorphPart, List<VertexMorphData>>();
mappedMorphOrder.Add(mmdName);
}
if (!mappedMorphData[mmdName].ContainsKey(rawPart))
{
mappedMorphData[mmdName][rawPart] = new List<VertexMorphData>();
}
mappedMorphData[mmdName][rawPart].AddRange(rawData);
}
}
foreach (string mmdName in mappedMorphOrder)
{
Dictionary<MorphPart, List<VertexMorphData>> partData = mappedMorphData[mmdName];
List<VertexMorphData> bakedData = new List<VertexMorphData>();
foreach (MorphPart part in new MorphPart[] { MorphPart.Eyebrow, MorphPart.Eye, MorphPart.Mouth })
{
if (!partData.ContainsKey(part)) continue;
bakedData.AddRange(partData[part]);
// 多部位组合仍保留可单独调整的部位副本,但标准入口不再使用 Group Morph。
if (partData.Count > 1)
{
string partName = MakeUniqueMorphName(mmdName + GetMorphPartSuffix(part), usedMorphNames, reservedStandardNames);
finalMorphs.Add(CreateVertexMorph(partName, partName, MergeVertexMorphData(partData[part]), GetMorphCategory(part)));
usedMorphNames.Add(partName);
}
}
// 同一顶点可能同时受多个源表情影响,按顶点索引求和后烘焙为一个标准 Vertex Morph。
MorphPart standardPart = GetStandardMorphPart(partData);
finalMorphs.Add(CreateVertexMorph(mmdName, mmdName, MergeVertexMorphData(bakedData), GetMorphCategory(standardPart)));
usedMorphNames.Add(mmdName);
}
return finalMorphs.ToArray();
}
private static Morph CreateVertexMorph(string name, string nameEn, List<VertexMorphData> datas, MorphCategory category)
{
Morph morph = new Morph();
morph.Name = name;
morph.NameEn = nameEn;
morph.Type = MorphType.MorphTypeVertex;
morph.MorphDatas = datas.ToArray();
morph.Category = category;
return morph;
}
private static List<VertexMorphData> MergeVertexMorphData(IEnumerable<VertexMorphData> source)
{
Dictionary<int, Vector3> offsetsByVertex = new Dictionary<int, Vector3>();
foreach (VertexMorphData data in source)
{
if (offsetsByVertex.TryGetValue(data.VertexIndex, out Vector3 currentOffset))
{
offsetsByVertex[data.VertexIndex] = currentOffset + data.Offset;
}
else
{
offsetsByVertex[data.VertexIndex] = data.Offset;
}
}
List<int> sortedVertexIndices = new List<int>(offsetsByVertex.Keys);
sortedVertexIndices.Sort();
List<VertexMorphData> merged = new List<VertexMorphData>(offsetsByVertex.Count);
foreach (int vertexIndex in sortedVertexIndices)
{
Vector3 offset = offsetsByVertex[vertexIndex];
// 多源相加后若恰好抵消为零,则无需写入 PMX。
if (offset.sqrMagnitude <= MorphVertexNoiseSqrThreshold) continue;
merged.Add(new VertexMorphData { VertexIndex = vertexIndex, Offset = offset });
}
return merged;
}
private static MorphPart GetStandardMorphPart(Dictionary<MorphPart, List<VertexMorphData>> partData)
{
// 标准名正常只属于一个 MMD 分类;兼容旧组合时按眉、眼、口优先级选择面板分类。
if (partData.ContainsKey(MorphPart.Eyebrow)) return MorphPart.Eyebrow;
if (partData.ContainsKey(MorphPart.Eye)) return MorphPart.Eye;
return MorphPart.Mouth;
}
private static MorphPart GetMorphPart(string rawName)
{
if (rawName.StartsWith("EyeBrow_", StringComparison.Ordinal) || rawName.Contains("眉"))
{
return MorphPart.Eyebrow;
}
if (rawName.StartsWith("Mouth_", StringComparison.Ordinal) || rawName.Contains("口") || rawName.Contains("Lip"))
{
return MorphPart.Mouth;
}
return MorphPart.Eye;
}
private static MorphCategory GetOriginalMorphCategory(string rawName)
{
// 原始 Morph 保持原有的按名称分类;耳部等非眉眼口项目归入 Other。
if (rawName.Contains("Mouth") || rawName.Contains("口") || rawName.Contains("Lip"))
{
return MorphCategory.MorphCatMouth;
}
if (rawName.Contains("EyeBrow") || rawName.Contains("眉"))
{
return MorphCategory.MorphCatEyebrow;
}
if (rawName.Contains("Eye") || rawName.Contains("目") || rawName.Contains("まばたき"))
{
return MorphCategory.MorphCatEye;
}
return MorphCategory.MorphCatOther;
}
private static MorphCategory GetMorphCategory(MorphPart part)
{
if (part == MorphPart.Eyebrow) return MorphCategory.MorphCatEyebrow;
if (part == MorphPart.Mouth) return MorphCategory.MorphCatMouth;
return MorphCategory.MorphCatEye;
}
private static string GetMorphPartSuffix(MorphPart part)
{
if (part == MorphPart.Eyebrow) return "_眉";
if (part == MorphPart.Mouth) return "_口";
return "_目";
}
private static string MakeUniqueMorphName(string sourceName, HashSet<string> usedNames, HashSet<string> reservedNames)
{
if (!usedNames.Contains(sourceName) && !reservedNames.Contains(sourceName)) return sourceName;
// PMX 本身不受 VMD 15 字节限制;数字后缀保证结果稳定、可预测且全局唯一。
for (int duplicateIndex = 2; ; duplicateIndex++)
{
string candidate = sourceName + "_" + duplicateIndex;
if (!usedNames.Contains(candidate) && !reservedNames.Contains(candidate)) return candidate;
}
}
private static string GetMappedMMDName(string rawName)
{
// 1. Direct Mapping
if (MorphNameMapping.TryGetValue(rawName, out string mapped)) return mapped;
// 2. Parse Unity Patterns (e.g. Eye_2_L -> Eye_CloseA -> まばたき)
// Eyes: Eye_{id}_{L/R}
if (rawName.StartsWith("Eye_"))
{
var parts = rawName.Split('_');
if (parts.Length >= 2 && int.TryParse(parts[1], out int id))
{
if (Enum.IsDefined(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialEyeId), id))
{
string tag = Enum.GetName(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialEyeId), id);
string key = "Eye_" + tag; // e.g. "Eye_CloseA"
// 左右 WaraiD 在正确参考模型中可精确对应传统 MMD 的左右 Wink。
if (parts.Length >= 3 && tag == "WaraiD")
{
if (parts[2].StartsWith("L", StringComparison.Ordinal)) return "ウィンク";
if (parts[2].StartsWith("R", StringComparison.Ordinal)) return "ウィンク右";
}
if (MorphNameMapping.TryGetValue(key, out string mmdName)) return mmdName;
}
}
}
// Eyebrows: EyeBrow_{id}_{L/R}
else if (rawName.StartsWith("EyeBrow_"))
{
var parts = rawName.Split('_');
if (parts.Length >= 2 && int.TryParse(parts[1], out int id))
{
if (Enum.IsDefined(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialEyebrowId), id))
{
string tag = Enum.GetName(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialEyebrowId), id);
string key = "EyeBrow_" + tag;
if (MorphNameMapping.TryGetValue(key, out string mmdName)) return mmdName;
}
}
}
// Mouth: Mouth_{id}_{index}
else if (rawName.StartsWith("Mouth_"))
{
var parts = rawName.Split('_');
if (parts.Length >= 2 && int.TryParse(parts[1], out int id))
{
if (Enum.IsDefined(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialMouthId), id))
{
string tag = Enum.GetName(typeof(Gallop.Live.Cutt.LiveTimelineDefine.FacialMouthId), id);
string key = "Mouth_" + tag; // e.g. "Mouth_WaraiA"
if (MorphNameMapping.TryGetValue(key, out string mmdName)) return mmdName;
// Fallback for Mouth which sometimes had "Mouth" prefix in mapping keys or direct names?
// Existing mapping has keys like "Mouth_WaraiA". So "Mouth_" + tag is correct.
// Check for specialized suffix matches if needed, but for now tag matching is safer
}
}
}
return null;
}
// UmaViewer Morph Name -> MMD 生态事实标准名称。
// 仅映射有参考模型或通行惯例支撑的形态,避免生成“名称正确、形状错误”的空壳标准项。
private static readonly Dictionary<string, string> MorphNameMapping = new Dictionary<string, string>()
{
// 眉
{ "EyeBrow_SeriousA", "真面目" },
{ "EyeBrow_WaraiD", "にこり" },
{ "EyeBrow_IkariA", "怒り" },
{ "EyeBrow_KanasiA", "悲しむ" },
{ "EyeBrow_KomariA", "困る" },
{ "EyeBrow_Offset_U", "上" },
{ "EyeBrow_Offset_D", "下" },
// 眼
{ "Eye_CloseA", "まばたき" }, // Blink
{ "Eye_WaraiA", "笑い" }, // Smile
{ "Eye_HalfA", "じと目" }, // Half-closed
{ "Eye_OdorokiA", "びっくり" }, // Surprised
// 参考模型顶点差异显示 KanasiA 比字面上的 IkariA 更接近传统“怒り目”。
{ "Eye_KanasiA", "怒り目" },
{ "Eye_PupilA", "瞳小" },
// 口 - 元音;大口型占用标准 VMD 名,小口型保留传统数字后缀。
{ "Mouth_TalkA_A_L", "あ" },
{ "Mouth_TalkA_I_L", "い" },
{ "Mouth_TalkA_U_L", "う" },
{ "Mouth_TalkA_E_L", "え" },
{ "Mouth_TalkA_O_L", "お" },
{ "Mouth_TalkA_A_S", "あ2" }, // Smaller versions?
{ "Mouth_TalkA_I_S", "い2" },
{ "Mouth_TalkA_U_S", "う2" },
{ "Mouth_TalkA_E_S", "え2" },
{ "Mouth_TalkA_O_S", "お2" },
// 口 - 表情。不要再与眉/眼的标准名称跨部位合并。
{ "Mouth_WaraiA", "にやり" },
{ "Mouth_WaraiB", "にっこり" },
{ "Mouth_IkariA", "怒り口" },
{ "Mouth_KomariA", "困る口" },
{ "Mouth_UreiA", "▲" }, // Triangle mouth?
{ "Mouth_OdorokiA", "□" }, // Box mouth?
};
private static Part[] ReadPartMaterials(List<Renderer> renderers, RawMMDModel model)
{
List<Part> parts = new List<Part>();
int baseShift = 0;
foreach (Renderer renderer in renderers)
{
Mesh mesh;
if (renderer is MeshRenderer mr)
{
var meshfilter = mr.GetComponent<MeshFilter>();
mesh = meshfilter.mesh;
}
else
{
mesh = ((SkinnedMeshRenderer)renderer).sharedMesh;
}
var materials = new List<Material>(renderer.sharedMaterials);
int rendererIndexCount = 0;
for (int i = 0; i < mesh.subMeshCount; i++)
{
var material = (i < materials.Count ? materials[i] : materials[materials.Count - 1]);
var part = new Part();
var mat = new MMDMaterial();
part.Material = mat;
mat.Name = mat.NameEn = material.name.Replace(" (Instance)","");
mat.DiffuseColor = Color.white;
mat.SpecularColor = Color.clear;
mat.AmbientColor = Color.white * 0.5f;
mat.Shiness = 5;
mat.CastSelfShadow = true;
mat.DrawGroundShadow = true;
mat.DrawSelfShadow = true;
mat.EdgeColor = Color.black;
mat.EdgeSize = 0.4f;
if (material.HasProperty("_MainTex") && material.mainTexture != null)
{
var tex = model.TextureList.Find(t => t.TexturePath.Contains($"/{material.mainTexture.name}.png"));
mat.Texture = (tex ?? model.TextureList[0]);
}
else
{
mat.Texture = model.TextureList[0];
}
mat.MetaInfo = "";
var subMesh = mesh.GetSubMesh(i);
part.BaseShift = baseShift + subMesh.indexStart;
part.TriangleIndexNum = subMesh.indexCount;
rendererIndexCount += subMesh.indexCount;
parts.Add(part);
}
baseShift += rendererIndexCount;
}
return parts.ToArray();
}
private static Vertex[] ReadVerticesAndTriangles(List<Renderer> renderers, Dictionary<Transform, int> boneIndexes, ref List<int> triangleList, Transform root)
{
List<Vertex> verticesList = new List<Vertex>();
int vertexOffset = 0;
foreach (Renderer renderer in renderers)
{
if (renderer is MeshRenderer mr)
{
var meshfilter = mr.GetComponent<MeshFilter>();
//Cache this data to avoid copying arrays.
var mesh = meshfilter.mesh;
if (!mesh.isReadable)
{
UmaViewerUI.Instance.ShowMessage($"Mesh {mesh.name} was not readable. Attempting to export", UIMessageType.Warning);
try
{
mesh = MakeReadableMeshCopy(mesh);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Cannot export unreadable mesh {mesh.name} on {renderer.name}", ex);
}
}
var vertices = mesh.vertices;
var normals = mesh.normals;
var uv = mesh.uv;
var uv1 = mesh.uv2;
var uv2 = mesh.uv3;
var colors = mesh.colors;
var triangles = mesh.triangles;
for (int i = 0; i < vertices.Length; i++)
{
Vertex vertex = new Vertex();
vertex.Coordinate = renderer.transform.TransformPoint(vertices[i]);
vertex.Normal = normals[i];
vertex.UvCoordinate = new Vector2(uv[i].x, 1 - uv[i].y);
vertex.ExtraUvCoordinate = new Vector4[3]
{
uv1.Length > 0 ? new Vector2(uv1[i].x, 1 - uv1[i].y) : Vector2.zero,
uv2.Length > 0 ? new Vector2(uv2[i].x, 1 - uv2[i].y) : Vector2.zero,
colors.Length > 0 ? colors[i] : Color.clear
};
vertex.SkinningOperator = new SkinningOperator() { Type = SkinningType.SkinningBdef1 };
vertex.SkinningOperator.Param = new Bdef1() { BoneId = GetBoneIndex(boneIndexes, renderer.transform) };
vertex.EdgeScale = 1;
verticesList.Add(vertex);
}
foreach (var triangle in mesh.triangles)
{
triangleList.Add(triangle + vertexOffset);
}
vertexOffset += vertices.Length;
}
else if (renderer is SkinnedMeshRenderer smr)
{
var skinMesh = smr.sharedMesh;
var mesh = skinMesh;
if (!mesh.isReadable)
{
UmaViewerUI.Instance.ShowMessage($"Mesh {mesh.name} was not readable. Attempting to export", UIMessageType.Warning);
try
{
mesh = MakeReadableMeshCopy(mesh);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Cannot export unreadable skinned mesh {mesh.name} on {renderer.name}", ex);
}
}
var vertices = mesh.vertices;
var normals = mesh.normals;
var uv = mesh.uv;
var uv1 = mesh.uv2;
var uv2 = mesh.uv3;
var colors = mesh.colors;
// Geometry may need a GPU-readable copy, but that copy has no valid skinning data.
var boneCounts = skinMesh.GetBonesPerVertex();
var skinbone = smr.bones;
var skinbones = smr.bones;
var weights = skinMesh.boneWeights;
var bakemesh = new Mesh();
smr.BakeMesh(bakemesh,true);
for (int i = 0; i < vertices.Length; i++)
{
Vertex vertex = new Vertex();
vertex.Coordinate = root.InverseTransformPoint(smr.transform.TransformPoint(bakemesh.vertices[i]));
vertex.Normal = normals[i];
vertex.UvCoordinate = new Vector2(uv[i].x, 1 - uv[i].y);
vertex.ExtraUvCoordinate = new Vector4[3]
{
uv1.Length > 0 ? new Vector2(uv1[i].x, 1 - uv1[i].y) : Vector2.zero,
uv2.Length > 0 ? new Vector2(uv2[i].x, 1 - uv2[i].y) : Vector2.zero,
colors.Length > 0 ? colors[i] : Color.clear
};
var boneWeight = weights[i];
var boneCount = boneCounts[i];
switch (boneCount)
{
case 0:
vertex.SkinningOperator = new SkinningOperator() { Type = SkinningType.SkinningBdef1 };
vertex.SkinningOperator.Param = new Bdef1() { BoneId = GetBoneIndex(boneIndexes, renderer.transform) };
break;
default:
case 1:
vertex.SkinningOperator = new SkinningOperator() { Type = SkinningType.SkinningBdef1 };
vertex.SkinningOperator.Param = new Bdef1() { BoneId = GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex0]) };
break;
case 2:
vertex.SkinningOperator = new SkinningOperator() { Type = SkinningType.SkinningBdef2 };
vertex.SkinningOperator.Param = new Bdef2()
{
BoneId = new int[]{
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex0]),
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex1]),
},
BoneWeight = boneWeight.weight0
};
break;
case 3:
case 4:
vertex.SkinningOperator = new SkinningOperator() { Type = SkinningType.SkinningBdef4 };
vertex.SkinningOperator.Param = new Bdef4()
{
BoneId = new int[]{
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex0]),
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex1]),
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex2]),
GetBoneIndex(boneIndexes, skinbone[boneWeight.boneIndex3]),
},
BoneWeight = new float[]
{
boneWeight.weight0,
boneWeight.weight1,
boneWeight.weight2,
boneWeight.weight3
}
};
break;
}
vertex.EdgeScale = 1;
verticesList.Add(vertex);
}
foreach (var triangle in mesh.triangles)
{
triangleList.Add(triangle + vertexOffset);
}
vertexOffset += vertices.Length;
}
}
return verticesList.ToArray();
}
private static int GetBoneIndex(Dictionary<Transform, int> boneIndexes, Transform bone)
{
// 被过滤骨的权重转交给最近的有效祖先,保证索引始终指向实际 PMX 骨骼。
Transform current = bone;
while (current != null)
{
if (boneIndexes.TryGetValue(current, out int index))
{
return index;
}
current = current.parent;
}
// 兜底到「全ての親」。
return 0;
}
/// <summary> Credit to pohype: https://discussions.unity.com/t/reading-meshes-at-runtime-that-are-not-enabled-for-read-write/804189/7 </summary>
public static Mesh MakeReadableMeshCopy(Mesh nonReadableMesh)
{
Mesh meshCopy = new Mesh();
meshCopy.indexFormat = nonReadableMesh.indexFormat;
// Handle vertices
GraphicsBuffer verticesBuffer = nonReadableMesh.GetVertexBuffer(0);
int totalSize = verticesBuffer.stride * verticesBuffer.count;
byte[] data = new byte[totalSize];
verticesBuffer.GetData(data);
meshCopy.SetVertexBufferParams(nonReadableMesh.vertexCount, nonReadableMesh.GetVertexAttributes());
meshCopy.SetVertexBufferData(data, 0, 0, totalSize);
verticesBuffer.Release();
// Handle triangles
meshCopy.subMeshCount = nonReadableMesh.subMeshCount;
GraphicsBuffer indexesBuffer = nonReadableMesh.GetIndexBuffer();
int tot = indexesBuffer.stride * indexesBuffer.count;
byte[] indexesData = new byte[tot];
indexesBuffer.GetData(indexesData);
meshCopy.SetIndexBufferParams(indexesBuffer.count, nonReadableMesh.indexFormat);
meshCopy.SetIndexBufferData(indexesData, 0, 0, tot);
indexesBuffer.Release();
// Restore submesh structure
uint currentIndexOffset = 0;
for (int i = 0; i < meshCopy.subMeshCount; i++)
{
uint subMeshIndexCount = nonReadableMesh.GetIndexCount(i);
meshCopy.SetSubMesh(i, new SubMeshDescriptor((int)currentIndexOffset, (int)subMeshIndexCount));
currentIndexOffset += subMeshIndexCount;
}
// Recalculate normals and bounds
meshCopy.RecalculateNormals();
meshCopy.RecalculateBounds();
return meshCopy;
}
}