diff --git a/dq8chr2glb.sln b/dq8chr2glb.sln new file mode 100644 index 0000000..f04a90a --- /dev/null +++ b/dq8chr2glb.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dq8chr2glb", "dq8chr2glb\dq8chr2glb.csproj", "{AB578EF6-C431-4578-A9AB-AD8706CB412F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB578EF6-C431-4578-A9AB-AD8706CB412F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB578EF6-C431-4578-A9AB-AD8706CB412F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB578EF6-C431-4578-A9AB-AD8706CB412F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB578EF6-C431-4578-A9AB-AD8706CB412F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/dq8chr2glb/ChrFile.cs b/dq8chr2glb/ChrFile.cs new file mode 100644 index 0000000..4c543c0 --- /dev/null +++ b/dq8chr2glb/ChrFile.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using dq8chr2glb.Container; +using dq8chr2glb.Converter; +using dq8chr2glb.Core.InfoCfg; +using dq8chr2glb.Core.MDSFormat; +using dq8chr2glb.Core.MOTFormat; +using SixLabors.ImageSharp; +using Texture = dq8chr2glb.TM2Format.Texture; + +namespace dq8chr2glb; + +public class ChrFile +{ + public ModelConfig infoCfg; + public List textures = new(); + public List mdsConverters = new(); + + public bool extract; + public bool convert; + public bool textFormat; + + public void Process(string inputPath, string outputPath, bool isBatch = false) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + var chrData = File.ReadAllBytes(inputPath); + var container = ChrContainer.FromBytes(chrData); + + var outputName = Path.GetFileNameWithoutExtension(inputPath); + var outputDir = Path.Combine(outputPath, outputName); + EnsurePath(outputDir); + + foreach (var file in container) + { + if (!isBatch) + { + PrintTask(file); + } + + switch (file.extension) + { + case FileExtension.CFG: + ProcessConfig(file, outputDir); + break; + case FileExtension.TEXT: + ProcessTextFile(file, outputDir); + break; + case FileExtension.TM2: + ProcessTextures(file, outputDir); + break; + case FileExtension.MDS: + ProcessMDSFile(file, outputDir); + break; + case FileExtension.MOT: + ProcessMOTFile(file, outputDir); + break; + default: + ProcessRawFile(file, outputDir); + break; + } + } + + if (convert) + { + foreach (var converter in mdsConverters) + { + converter.Save(outputDir, textFormat); + } + } + } + + private void PrintTask(IncludedFile file) + { + var spaces = 30 - file.name.Length; + if (spaces <= 0) + { + spaces = 1; + } + + var spacer = new string('.', spaces); + Console.WriteLine($" Process: {file.name + spacer} {file.data.Length} bytes"); + } + + public void Clean() + { + infoCfg = null; + textures = new(); + mdsConverters = new(); + } + + private void EnsurePath(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + private void ProcessMOTFile(IncludedFile file, string outputDir) + { + if (extract) + { + ProcessRawFile(file, outputDir); + } + + if (convert) + { + foreach (var converter in mdsConverters) + { + var motImporter = new Importer(); + var animation = motImporter.Import(file.data); + converter.CreateAnimation(animation, infoCfg); + } + } + } + + private void ProcessMDSFile(IncludedFile file, string outputDir) + { + var mdsScene = Reader.Read(file.data, file.name); + + if (convert) + { + var converter = new MDSConverter(file.name); + var rootName = Path.GetFileNameWithoutExtension(file.name); + converter.Convert(mdsScene, textures, rootName); + mdsConverters.Add(converter); + } + + if (extract) + { + ProcessRawFile(file, outputDir); + } + } + + private void ProcessConfig(IncludedFile file, string outputDir) + { + if (extract) + { + var isSecond = infoCfg != null; + if (isSecond) + { + file.name = file.name.Replace(".cfg", "_2.cfg"); + } + + ProcessTextFile(file, outputDir); + } + + var data = Encoding.GetEncoding("shift_jis").GetString(file.data).TrimEnd('\0'); + var configFile = new ConfigFile(data); + + var info = configFile.ReadConfig(); + if (infoCfg == null) + { + infoCfg = info; + } + } + + private void ProcessTextFile(IncludedFile file, string outputDir) + { + var root = Path.GetDirectoryName(file.name); + var fileName = Path.GetFileName(file.name); + var outputPath = Path.Combine(outputDir, root); + var text = Encoding.GetEncoding("shift_jis").GetString(file.data).TrimEnd('\0'); + if (extract) + { + EnsurePath(outputPath); + File.WriteAllText(Path.Combine(outputPath, fileName), text); + } + } + + private void ProcessRawFile(IncludedFile file, string outputDir) + { + var root = Path.GetDirectoryName(file.name); + var fileName = Path.GetFileName(file.name); + var outputPath = Path.Combine(outputDir, root); + + if (extract) + { + EnsurePath(outputPath); + File.WriteAllBytes(Path.Combine(outputPath, fileName), file.data); + } + } + + private void ProcessTextures(IncludedFile file, string outputDir) + { + var root = Path.GetDirectoryName(file.name); + var fileName = Path.GetFileNameWithoutExtension(file.name); + var outputPath = Path.Combine(outputDir, root); + + var image = TM2Format.TM2Format.GetImage(file.data, fileName); + if (extract) + { + EnsurePath(outputPath); + image.data.SaveAsPng(Path.Combine(outputPath, fileName + ".png")); + } + + textures.Add(image); + } +} diff --git a/dq8chr2glb/Container/ChrContainer.cs b/dq8chr2glb/Container/ChrContainer.cs new file mode 100644 index 0000000..1b087f6 --- /dev/null +++ b/dq8chr2glb/Container/ChrContainer.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace dq8chr2glb.Container; + +public class ChrContainer +{ + public static List FromBytes(byte[] chrFile) + { + var result = new List(); + var pos = 0; + + while (pos < chrFile.Length) + { + if (pos + 80 > chrFile.Length) + { + break; + } + + var buffer = chrFile.AsSpan(pos, 80); + var dataSize = BitConverter.ToUInt32(buffer.Slice(68, 4)); + var nextOffset = BitConverter.ToUInt32(buffer.Slice(72, 4)); + var name = ExtractName(buffer.Slice(0, 64).ToArray()); + + if (pos + 80 + dataSize > chrFile.Length) + { + break; + } + + var data = chrFile.AsSpan(pos + 80, (int)dataSize).ToArray(); + + if (dataSize < 3) + { + break; + } + + var subContainers = new List(); + + if (Encoding.ASCII.GetString(data, 0, 3) == "IM3") + { + var files = UnpackImgContainer(name, data); + subContainers.AddRange(files); + } + else if (name.EndsWith(".chr")) + { + var files = FromBytes(data); + subContainers.AddRange(files); + } + else + { + var file = new IncludedFile(); + file.name = name; + file.data = data; + subContainers.Add(file); + } + + foreach (var file in subContainers) + { + var extension = file.name.ToLower().Split(".")[^1]; + file.extension = extension switch + { + "mds" => FileExtension.MDS, + "mot" => FileExtension.MOT, + "tm2" => FileExtension.TM2, + "cfg" => FileExtension.TEXT, + "img" => FileExtension.IMG, + _ => FileExtension.TEXT + }; + + if (file.name == "info.cfg") + { + file.extension = FileExtension.CFG; + } + } + + result.AddRange(subContainers); + pos += (int)nextOffset; + } + + result = Utils.SortFiles(result); + return result; + } + + private static List UnpackImgContainer(string root, byte[] imgData) + { + var rootFolderName = Path.GetFileNameWithoutExtension(root) + "_img"; + + var fileCount = BitConverter.ToUInt32(imgData, 8); + var currentOffset = 16L; + + var files = new List(); + for (var i = 0; i < fileCount; i++) + { + var file = new IncludedFile(); + + var headerBytes = new byte[64]; + Array.Copy(imgData, currentOffset, headerBytes, 0, 64); + + var handle = GCHandle.Alloc(headerBytes, GCHandleType.Pinned); + var imgFile = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + handle.Free(); + + var name = ExtractName(imgFile.Name); + var isTexture = !name.StartsWith("#"); + name = name.Replace("#", ""); + + name = Path.Combine(rootFolderName, name); + + file.extension = isTexture ? FileExtension.TM2 : FileExtension.TEXT; + file.name = name + (isTexture ? ".tm2" : ".cfg"); + + var fileData = new byte[imgFile.FileSize]; + Array.Copy(imgData, imgFile.DataOffset, fileData, 0, imgFile.FileSize); + + file.data = fileData; + + files.Add(file); + + currentOffset += 64; + } + + return files; + } + + private static string ExtractName(byte[] nameBytes) + { + var len = Array.IndexOf(nameBytes, (byte)0); + if (len < 0) + { + len = 16; + } + + return Encoding.ASCII.GetString(nameBytes, 0, len).Trim(); + } +} \ No newline at end of file diff --git a/dq8chr2glb/Container/FileExtension.cs b/dq8chr2glb/Container/FileExtension.cs new file mode 100644 index 0000000..8381ad4 --- /dev/null +++ b/dq8chr2glb/Container/FileExtension.cs @@ -0,0 +1,12 @@ +namespace dq8chr2glb.Container; + +public enum FileExtension +{ + CHR, + CFG, + TEXT, + IMG, + TM2, + MDS, + MOT, +} diff --git a/dq8chr2glb/Container/ImgFile.cs b/dq8chr2glb/Container/ImgFile.cs new file mode 100644 index 0000000..67193d1 --- /dev/null +++ b/dq8chr2glb/Container/ImgFile.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace dq8chr2glb.Container; + +[StructLayout(LayoutKind.Explicit, Size = 64)] +public struct ImgFile +{ + [FieldOffset(0x00)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] Name; + + [FieldOffset(0x14)] public uint FileTypeFlag; + [FieldOffset(0x20)] public uint HeaderSize; + [FieldOffset(0x24)] public uint DataOffset; + [FieldOffset(0x34)] public uint FileSize; + [FieldOffset(0x3C)] public uint Padding; +} \ No newline at end of file diff --git a/dq8chr2glb/Container/IncludedFile.cs b/dq8chr2glb/Container/IncludedFile.cs new file mode 100644 index 0000000..062dc50 --- /dev/null +++ b/dq8chr2glb/Container/IncludedFile.cs @@ -0,0 +1,10 @@ +using System; + +namespace dq8chr2glb.Container; + +public class IncludedFile +{ + public string name; + public FileExtension extension; + public byte[] data = Array.Empty(); +} diff --git a/dq8chr2glb/Container/Utils.cs b/dq8chr2glb/Container/Utils.cs new file mode 100644 index 0000000..5637e33 --- /dev/null +++ b/dq8chr2glb/Container/Utils.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace dq8chr2glb.Container; + +public static class Utils +{ + public static List SortFiles(List files) + { + var other = new List(); + var configs = new List(); + var textures = new List(); + var models = new List(); + var motion = new List(); + + foreach (var file in files) + { + switch (file.extension) + { + case FileExtension.TEXT: + configs.Add(file); + break; + case FileExtension.TM2: + textures.Add(file); + break; + case FileExtension.MDS: + models.Add(file); + break; + case FileExtension.MOT: + motion.Add(file); + break; + case FileExtension.CHR: + default: + other.Add(file); + break; + } + } + + var output = new List(); + output.AddRange(other); + output.AddRange(configs); + output.AddRange(textures); + output.AddRange(models); + output.AddRange(motion); + + return output; + } +} \ No newline at end of file diff --git a/dq8chr2glb/Converter/MDSConverter.cs b/dq8chr2glb/Converter/MDSConverter.cs new file mode 100644 index 0000000..4d4f62f --- /dev/null +++ b/dq8chr2glb/Converter/MDSConverter.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using dq8chr2glb.Core.InfoCfg; +using dq8chr2glb.Core.MDSFormat; +using dq8chr2glb.Core.MOTFormat; +using SharpGLTF.Materials; +using SharpGLTF.Memory; +using SharpGLTF.Schema2; +using SixLabors.ImageSharp; +using Node = SharpGLTF.Schema2.Node; +using Texture = dq8chr2glb.TM2Format.Texture; + +namespace dq8chr2glb.Converter; + +public class MDSConverter +{ + private readonly string _name; + private readonly ModelRoot _gltfModel; + private readonly Dictionary _nodeMap = new(); + private readonly List _boneNodes = new(); + private readonly Dictionary _materialCache = new(); + private Dictionary _textureBuilders = new Dictionary(); + + private const float FRAMERATE = 16f; + + public MDSConverter(string name) + { + _name = name; + _gltfModel = ModelRoot.CreateModel(); + } + + public void Convert(MDSScene scene, List images, string name) + { + var gltfScene = _gltfModel.UseScene("DefaultScene"); + + var nodesWithParents = new HashSet(); + foreach (var node in scene.nodes) + { + if (node.parentIndex >= 0) + { + nodesWithParents.Add(node.index); + } + } + + foreach (var node in scene.nodes) + { + if (node.parentIndex < 0) + { + var gltfNode = gltfScene.CreateNode(node.name); + _nodeMap[node.index] = gltfNode; + + if (node is MDSBone) + { + _boneNodes.Add(gltfNode); + } + + gltfNode.WithLocalTransform(ToNumericsMatrix4x4(node.transform)); + } + } + + foreach (var node in scene.nodes) + { + if (node.parentIndex >= 0 && _nodeMap.ContainsKey(node.parentIndex)) + { + var parent = _nodeMap[node.parentIndex]; + var gltfNode = parent.CreateNode(node.name); + _nodeMap[node.index] = gltfNode; + + if (node is MDSBone) + { + _boneNodes.Add(gltfNode); + } + + gltfNode.WithLocalTransform(ToNumericsMatrix4x4(node.transform)); + } + } + + CreateMaterials(scene.materials, images); + + var meshes = scene.nodes.Where(n => n is MDSMesh).Cast().ToArray(); + foreach (var mdsMesh in meshes) + { + if (mdsMesh?.vertices == null || mdsMesh.triangles == null) continue; + + var mesh = CreateGltfMesh(mdsMesh, scene.materials); + if (_nodeMap.TryGetValue(mdsMesh.index, out var meshNode)) + { + meshNode.WithMesh(mesh); + } + } + + if (_boneNodes.Count > 0) + { + SetupSkeleton(scene.nodes, name); + } + } + + public void CreateAnimation(List motionCurves, ModelConfig config) + { + foreach (var clip in config.clips) + { + var animation = _gltfModel.CreateAnimation(clip.name); + + foreach (var curve in motionCurves) + { + if (_nodeMap.TryGetValue(curve.boneIndex, out var node)) + { + var rotationKeyframes = new Dictionary(); + var translationKeyframes = new Dictionary(); + + for (var i = clip.startFrame; i < clip.endFrame; i++) + { + var keyframe = curve.keyframes[i]; + if (keyframe == null) + { + continue; + } + + if (curve.curveType == KeyframeType.Quaternion) + { + var time = keyframe.frame / FRAMERATE / clip.speed; + rotationKeyframes[time] = keyframe.rotation; + } + else if (curve.curveType == KeyframeType.Translation) + { + var time = keyframe.frame / FRAMERATE / clip.speed; + translationKeyframes[time] = keyframe.translation; + } + } + + if (rotationKeyframes.Count > 0) + { + animation.CreateRotationChannel(node, rotationKeyframes); + } + + if (translationKeyframes.Count > 0) + { + animation.CreateTranslationChannel(node, translationKeyframes); + } + } + } + } + } + + private void CreateMaterials(MDSMaterial[] materials, List textures) + { + foreach (var tex in textures) + { + byte[] imageBytes; + using (var ms = new MemoryStream()) + { + tex.data.SaveAsPng(ms); + imageBytes = ms.ToArray(); + } + + var imageBuilder = ImageBuilder.From(new MemoryImage(imageBytes), tex.name); + _textureBuilders[tex.name] = imageBuilder; + } + + var defaultMat = new MaterialBuilder(); + _materialCache["default"] = defaultMat; + + foreach (var mat in materials) + { + var materialBuilder = new MaterialBuilder(); + materialBuilder.Name = mat.name; + + if (!string.IsNullOrEmpty(mat.textureName) && _textureBuilders.ContainsKey(mat.textureName)) + { + var textureImage = _textureBuilders[mat.textureName]; + materialBuilder.WithBaseColor(textureImage); + } + + _materialCache[mat.name] = materialBuilder; + } + } + + private Mesh CreateGltfMesh(MDSMesh mdsMesh, MDSMaterial[] materials) + { + return UniversalMeshBuilder.CreateMesh(_gltfModel, mdsMesh, materials, _materialCache); + } + + private void SetupSkeleton(MDSNode[] nodes, string name) + { + Node rootBone = null; + foreach (var boneNode in _boneNodes) + { + var mdsNode = Array.Find(nodes, n => n.name == boneNode.Name); + if (mdsNode is MDSBone bone && bone.parentIndex < 0) + { + rootBone = boneNode; + break; + } + } + + if (rootBone == null && _boneNodes.Count > 0) + { + rootBone = _boneNodes[0]; + } + + if (_boneNodes.Count == 0) + { + Console.WriteLine("Skip mesh with 0 bones"); + return; + } + + var skin = _gltfModel.CreateSkin(name); + skin.Skeleton = rootBone; + + var jointBindings = new (Node Joint, Matrix4x4 InverseBindMatrix)[_boneNodes.Count]; + for (var i = 0; i < _boneNodes.Count; i++) + { + var boneNode = _boneNodes[i]; + var mdsNode = Array.Find(nodes, n => n.name == boneNode.Name); + + Matrix4x4 bindMatrix; + if (mdsNode is MDSBone b) + { + var bindposeMatrix = ToNumericsMatrix4x4(b.bindpose); + if (Matrix4x4.Invert(bindposeMatrix, out var bindposeInverse)) + { + var multiplied = Matrix4x4.Multiply(bindposeInverse, Matrix4x4.Identity); + bindMatrix = SanitizeBindMatrix(multiplied); + } + else + { + bindMatrix = Matrix4x4.Identity; + } + + jointBindings[i] = (boneNode, bindMatrix); + } + } + + skin.BindJoints(jointBindings); + + foreach (var node in _gltfModel.LogicalNodes) + { + if (node.Mesh != null) + { + if (node.Mesh.Primitives.All(i => i.VertexAccessors.ContainsKey("WEIGHTS_0"))) + { + if (Quaternion.Dot(node.LocalTransform.GetDecomposed().Rotation, Quaternion.Identity) >= 0.999f) + { + node.LocalTransform = Matrix4x4.Identity; + } + + node.Skin = skin; + } + } + } + } + + private Matrix4x4 SanitizeBindMatrix(Matrix4x4 matrix) + { + return matrix; + } + + private Matrix4x4 ToNumericsMatrix4x4(MDSMatrix m) + { + return new Matrix4x4( + m.m00, m.m01, m.m02, m.m03, + m.m10, m.m11, m.m12, m.m13, + m.m20, m.m21, m.m22, m.m23, + m.m30, m.m31, m.m32, m.m33 + ); + } + + public void Save(string filePath, bool textFormat = true) + { + var path = Path.Combine(filePath, _name + (textFormat ? ".gltf" : ".glb")); + + if (textFormat) + { + _gltfModel.SaveGLTF(path); + } + else + { + _gltfModel.SaveGLB(path); + } + } +} \ No newline at end of file diff --git a/dq8chr2glb/Converter/UniversalMeshBuilder.cs b/dq8chr2glb/Converter/UniversalMeshBuilder.cs new file mode 100644 index 0000000..00ea047 --- /dev/null +++ b/dq8chr2glb/Converter/UniversalMeshBuilder.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using dq8chr2glb.Core.MDSFormat; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; +using SharpGLTF.Schema2; + +namespace dq8chr2glb.Converter +{ + public static class UniversalMeshBuilder + { + public static Mesh CreateMesh(ModelRoot _root, MDSMesh mdsMesh, MDSMaterial[] materials, + Dictionary materialCache) + { + var (hasUvs, hasSkinning) = AnalyzeMeshAttributes(mdsMesh); + + // Выбираем подходящий тип вершины на основе атрибутов + if (hasSkinning) + { + if (hasUvs) + { + return CreateMeshWithAllAttributes( + _root, mdsMesh, materials, materialCache); + } + else + { + return CreateMeshWithAllAttributes( + _root, mdsMesh, materials, materialCache); + } + } + else + { + if (hasUvs) + { + return CreateMeshWithAllAttributes( + _root, mdsMesh, materials, materialCache); + } + else + { + return CreateMeshWithAllAttributes( + _root, mdsMesh, materials, materialCache); + } + } + } + + private static (bool hasUvs, bool hasSkinning) AnalyzeMeshAttributes(MDSMesh mdsMesh) + { + var hasUvs = mdsMesh.uv != null && mdsMesh.uv.Length > 0 && + Array.Exists(mdsMesh.uv, uv => uv != null && uv.Length >= 2); + var hasSkinning = mdsMesh.weights != null && mdsMesh.bones != null && + mdsMesh.bones.Length > 0 && + Array.Exists(mdsMesh.bones, b => b != null); + + return (hasUvs, hasSkinning); + } + + private static Mesh CreateMeshWithAllAttributes( + ModelRoot root, + MDSMesh mdsMesh, + MDSMaterial[] materials, + Dictionary materialCache) + where TvG : struct, IVertexGeometry + where TvM : struct, IVertexMaterial + where TvS : struct, IVertexSkinning + { + var meshBuilder = new MeshBuilder(mdsMesh.name); + + var positions = new List(); + var normals = new List(); + var texCoords = new List(); + var skinningData = new List<(int, float)[]>(); + var hasUvs = mdsMesh.uv != null && mdsMesh.uv.Length > 0; + var hasSkinning = mdsMesh.weights != null && mdsMesh.bones != null; + + for (int i = 0; i < mdsMesh.vertices.Length; i++) + { + positions.Add(new Vector3( + mdsMesh.vertices[i][0], + mdsMesh.vertices[i][1], + mdsMesh.vertices[i][2])); + + normals.Add(Vector3.Zero); + + if (hasUvs && i < mdsMesh.uv.Length && mdsMesh.uv[i] != null && mdsMesh.uv[i].Length >= 2) + { + texCoords.Add(new Vector2(mdsMesh.uv[i][0], mdsMesh.uv[i][1])); + } + else + { + texCoords.Add(Vector2.Zero); + } + + if (hasSkinning && i < mdsMesh.bones.Length && mdsMesh.bones[i] != null) + { + var boneIndices = mdsMesh.bones[i]; + var boneWeights = mdsMesh.weights[i]; + + var bindings = new (int JointIndex, float Weight)[Math.Min(8, boneIndices.Length)]; + for (var b = 0; b < bindings.Length; b++) + { + bindings[b] = (boneIndices[b], boneWeights[b]); + } + + skinningData.Add(bindings); + } + else + { + skinningData.Add(Array.Empty<(int, float)>()); + } + } + + ComputeNormals(positions, mdsMesh.triangles, normals); + + foreach (var submesh in mdsMesh.submeshes) + { + var materialBuilder = materialCache.TryGetValue( + submesh.materialIndex >= 0 && + submesh.materialIndex < materials.Length + ? materials[submesh.materialIndex].name + : "default", + out var mat) + ? mat + : materialCache["default"]; + + var primitive = meshBuilder.UsePrimitive(materialBuilder); + + var indices = new List(); + for (var i = submesh.startIndex; i < submesh.startIndex + submesh.indexCount; i += 3) + { + indices.Add(mdsMesh.triangles[i]); + indices.Add(mdsMesh.triangles[i + 1]); + indices.Add(mdsMesh.triangles[i + 2]); + } + + for (var i = 0; i < indices.Count; i += 3) + { + var idxA = indices[i]; + var idxB = indices[i + 1]; + var idxC = indices[i + 2]; + + var vertexA = CreateVertex( + positions[idxA], + normals[idxA], + texCoords.Count > idxA ? texCoords[idxA] : Vector2.Zero, + skinningData.Count > idxA ? skinningData[idxA] : null); + + var vertexB = CreateVertex( + positions[idxB], + normals[idxB], + texCoords.Count > idxB ? texCoords[idxB] : Vector2.Zero, + skinningData.Count > idxB ? skinningData[idxB] : null); + + var vertexC = CreateVertex( + positions[idxC], + normals[idxC], + texCoords.Count > idxC ? texCoords[idxC] : Vector2.Zero, + skinningData.Count > idxC ? skinningData[idxC] : null); + + primitive.AddTriangle(vertexA, vertexB, vertexC); + } + } + + return root.CreateMesh(meshBuilder); + } + + private static VertexBuilder CreateVertex( + Vector3 position, + Vector3 normal, + Vector2 texCoord, + (int, float)[] skinningData) + where TvG : struct, IVertexGeometry + where TvM : struct, IVertexMaterial + where TvS : struct, IVertexSkinning + { + var geometry = default(TvG); + geometry.SetPosition(position); + geometry.SetNormal(normal); + + var material = default(TvM); + if (typeof(TvM) != typeof(VertexEmpty)) + { + try + { + material.SetTexCoord(0, texCoord); + } + catch + { + } + } + + var skinning = default(TvS); + if (skinningData != null && typeof(TvS) != typeof(VertexEmpty)) + { + try + { + skinning.SetBindings(skinningData); + } + catch + { + } + } + + return new VertexBuilder(geometry, material, skinning); + } + + private static void ComputeNormals(List positions, int[] triangles, List normals) + { + for (var i = 0; i < normals.Count; i++) + { + normals[i] = Vector3.Zero; + } + + for (var i = 0; i < triangles.Length; i += 3) + { + var idx1 = triangles[i]; + var idx2 = triangles[i + 1]; + var idx3 = triangles[i + 2]; + + var p1 = positions[idx1]; + var p2 = positions[idx2]; + var p3 = positions[idx3]; + + var edge1 = p2 - p1; + var edge2 = p3 - p1; + + var normal = Vector3.Cross(edge1, edge2); + normal = Vector3.Normalize(normal); + + normals[idx1] += normal; + normals[idx2] += normal; + normals[idx3] += normal; + } + + for (var i = 0; i < normals.Count; i++) + { + normals[i] = Vector3.Normalize(normals[i]); + } + } + } +} \ No newline at end of file diff --git a/dq8chr2glb/Core/InfoCfg/Clip.cs b/dq8chr2glb/Core/InfoCfg/Clip.cs new file mode 100644 index 0000000..158c1f4 --- /dev/null +++ b/dq8chr2glb/Core/InfoCfg/Clip.cs @@ -0,0 +1,9 @@ +namespace dq8chr2glb.Core.InfoCfg; + +public class Clip +{ + public string name; + public int startFrame; + public int endFrame; + public float speed; +} diff --git a/dq8chr2glb/Core/InfoCfg/ConfigFile.cs b/dq8chr2glb/Core/InfoCfg/ConfigFile.cs new file mode 100644 index 0000000..5ad4857 --- /dev/null +++ b/dq8chr2glb/Core/InfoCfg/ConfigFile.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace dq8chr2glb.Core.InfoCfg; + +public class ConfigFile +{ + public string text; + + public ConfigFile(string text) + { + this.text = text; + } + + public ModelConfig ReadConfig() + { + var config = new ModelConfig(); + config.model = GetModelFileName(); + config.clips = GetClips(); + return config; + } + + public string GetModelFileName() + { + var lines = text.Split('\n'); + + foreach (var line in lines) + { + if (line.StartsWith("MODEL")) + { + var match = Regex.Match(line, @"MODEL\s+""([^""]+)"""); + return match.Groups[1].Value; + } + } + + return null; + } + + public List GetClips() + { + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + + var clips = new List(); + var lines = text.Split('\n'); + var insideKeyBlock = false; + + foreach (var line in lines) + { + var t = line.Trim(); + + if (t == "KEY_START;") + { + insideKeyBlock = true; + continue; + } + + if (t == "KEY_END;") + { + insideKeyBlock = false; + continue; + } + + if (!insideKeyBlock || !t.StartsWith("KEY ")) + { + continue; + } + + var parts = t[4..^1].Split(", "); + + clips.Add(new Clip + { + name = parts[0][1..^1], + startFrame = int.Parse(parts[1]), + endFrame = int.Parse(parts[2]), + speed = float.Parse(parts[3]) + }); + } + + return clips; + } +} \ No newline at end of file diff --git a/dq8chr2glb/Core/InfoCfg/ModelConfig.cs b/dq8chr2glb/Core/InfoCfg/ModelConfig.cs new file mode 100644 index 0000000..b59b223 --- /dev/null +++ b/dq8chr2glb/Core/InfoCfg/ModelConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace dq8chr2glb.Core.InfoCfg; + +public class ModelConfig +{ + public string name; + public string model; + public string shadow; + public List clips; +} diff --git a/dq8chr2glb/Core/MDSFormat/Enums.cs b/dq8chr2glb/Core/MDSFormat/Enums.cs new file mode 100644 index 0000000..5ab368a --- /dev/null +++ b/dq8chr2glb/Core/MDSFormat/Enums.cs @@ -0,0 +1,29 @@ +using System; + +namespace dq8chr2glb.Core.MDSFormat; + +public enum TriangleReadMode : short +{ + None = 0, + Triangle = 3, + TriangleStrip = 4, + Unknown1 = 17, + Unknown2 = 19, +} + +public enum NodeType +{ + None, + Mesh, + Bone +} + +[Flags] +public enum MeshFeatures +{ + None = 0, + Verts = 1 << 0, + Colors = 1 << 1, + UVs = 1 << 2, + Weights = 1 << 3, +} diff --git a/dq8chr2glb/Core/MDSFormat/Headers.cs b/dq8chr2glb/Core/MDSFormat/Headers.cs new file mode 100644 index 0000000..4b98840 --- /dev/null +++ b/dq8chr2glb/Core/MDSFormat/Headers.cs @@ -0,0 +1,135 @@ +using System.Runtime.InteropServices; + +namespace dq8chr2glb.Core.MDSFormat; + +[StructLayout(LayoutKind.Explicit, Size = 80)] +public struct MDSHeader +{ + [FieldOffset(00)] public int MagicCode; + [FieldOffset(04)] public int version; + [FieldOffset(08)] public int headerSize; + [FieldOffset(12)] public int nodesCount; + [FieldOffset(20)] public int nodeSize; + [FieldOffset(24)] public int offsetToNodes; + [FieldOffset(32)] public int meshCount; + [FieldOffset(36)] public int offsetToMDT; + [FieldOffset(44)] public int materialsCount; + [FieldOffset(48)] public int materialSize; + [FieldOffset(52)] public int offsetToMaterials; + [FieldOffset(56)] public int offsetToNames; + [FieldOffset(60)] public int namesBlockSize; +} + +[StructLayout(LayoutKind.Explicit, Size = 256)] +public struct MDTHeader +{ + [FieldOffset(00)] public int MagicCode; + [FieldOffset(08)] public int toVertexCount; + [FieldOffset(12)] public int totalSize; + + [FieldOffset(16)] public int value1; // always 2064? + [FieldOffset(20)] public int value2; // always 4104? + [FieldOffset(24)] public int value3; // always 4112? + + [FieldOffset(32)] public float scaleFactorX; + [FieldOffset(36)] public float scaleFactorY; + + [FieldOffset(40)] public float scaleFactorZ; + + // [FieldOffset(64)] public Float3 someScale; // always 1.079? + [FieldOffset(80)] public float uvScaleX; + [FieldOffset(84)] public float uvScaleY; + + [FieldOffset(96)] public int value4; // always 144? + + [FieldOffset(112)] public int triangleGroupCount; + [FieldOffset(116)] public int vertexAttrSize; + + [FieldOffset(128)] public int value5; // some offset + + [FieldOffset(144)] public int vertsCount; + [FieldOffset(148)] public int bytesToFirstVertex; + [FieldOffset(152)] public int colorsCount; + [FieldOffset(156)] public int bytesToFirstColor; + [FieldOffset(160)] public int uvCount; + [FieldOffset(164)] public int uvOffset; + [FieldOffset(176)] public int bonesPerVertex; + [FieldOffset(180)] public int toWeightsOffset; + [FieldOffset(184)] public int toBoneIndicesOffset; + + [FieldOffset(188)] public int value6; // always 1? + [FieldOffset(192)] public AABB bounds; +} + +[StructLayout(LayoutKind.Explicit, Size = 40)] +public struct AABB +{ + [FieldOffset(00)] public float minX; + [FieldOffset(04)] public float minY; + [FieldOffset(08)] public float minZ; + [FieldOffset(12)] public int value1; // corner flag? + [FieldOffset(16)] public float maxX; + [FieldOffset(20)] public float maxY; + [FieldOffset(24)] public float maxZ; + [FieldOffset(28)] public int value2; // corner flag? + [FieldOffset(32)] public float centerX; + [FieldOffset(36)] public float centerY; + [FieldOffset(40)] public float centerZ; + [FieldOffset(44)] public float floatValue1; // ? +} + +[StructLayout(LayoutKind.Explicit, Size = 160)] +public struct Node +{ + [FieldOffset(00)] public int nameOffset; + [FieldOffset(12)] public int meshIndex; + [FieldOffset(16)] public int parentIndex; + [FieldOffset(32)] public MDSMatrix matrix; + [FieldOffset(96)] public MDSMatrix bindpose; +} + +[StructLayout(LayoutKind.Explicit, Size = 96)] +public struct Material +{ + [FieldOffset(00)] public float value1; + [FieldOffset(04)] public float value2; + [FieldOffset(08)] public float value3; + [FieldOffset(12)] public float value4; + + [FieldOffset(16)] public float value5; + [FieldOffset(20)] public float value6; + [FieldOffset(24)] public float value7; + [FieldOffset(28)] public float value8; + + [FieldOffset(32)] public float value9; + [FieldOffset(36)] public float value10; + [FieldOffset(40)] public float value11; + [FieldOffset(44)] public float value12; + + [FieldOffset(48)] public int nameOffset; + [FieldOffset(52)] public int textureNameOffset; + + [FieldOffset(64)] public int value13; // 0 для обычных материалов, 1 для материалов без имён + [FieldOffset(68)] public int value14; + [FieldOffset(72)] public int value15; + [FieldOffset(76)] public int value16; + + [FieldOffset(80)] public int value17; + [FieldOffset(84)] public int value18; + [FieldOffset(88)] public int value19; + [FieldOffset(92)] public int value20; +} + +[StructLayout(LayoutKind.Explicit, Size = 32)] +public struct TriangleGroupHeader +{ + [FieldOffset(00)] public short toNext; + [FieldOffset(04)] public short readMode; + + [FieldOffset(06)] public short idsCount; + + // [FieldOffset(06)]public short dataSize; + [FieldOffset(16)] public short headerSize; + + public TriangleReadMode ReadMode => (TriangleReadMode)readMode; +} diff --git a/dq8chr2glb/Core/MDSFormat/Reader.cs b/dq8chr2glb/Core/MDSFormat/Reader.cs new file mode 100644 index 0000000..7c882be --- /dev/null +++ b/dq8chr2glb/Core/MDSFormat/Reader.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace dq8chr2glb.Core.MDSFormat; + +public class Reader +{ + public static MDSScene Read(byte[] data, string filename) + { + var offset = 0; + var header = Utils.ReadStruct(data, ref offset, true); + + if (header.version != 200) + { + Console.WriteLine($"Unknown version: {header.version}"); + return null; + } + + var scene = new MDSScene(); + + var names = ReadNames(header, data); + ReadNodes(scene, header, data, names); + ReadMaterials(scene, header, data, names); + + var mdtHeaders = new MDTHeader[header.meshCount]; + for (var meshIndex = 0; meshIndex < header.meshCount; meshIndex++) + { + offset = header.offsetToMDT + mdtHeaders.Sum(h => h.totalSize); + + var mdtHeader = Utils.ReadStruct(data, ref offset); + mdtHeaders[meshIndex] = mdtHeader; + + var mesh = Utils.GetMeshByIndex(scene, meshIndex); + + var hasVertices = mdtHeader.vertsCount != 0; + var hasColors = mdtHeader.colorsCount != 0; + var hasUVs = mdtHeader.uvCount != 0; + var isSkinned = mdtHeader.bonesPerVertex != 0; + + var features = Utils.GetFeaturesFlags(hasVertices, hasColors, hasUVs, isSkinned); + mesh.features = features; + mesh.bounds = mdtHeader.bounds; + + // Console.WriteLine($"Features {features}"); + + var gScale = 0.00003f; + var scale = new float[3] + { + mdtHeader.scaleFactorX * gScale, mdtHeader.scaleFactorY * gScale, mdtHeader.scaleFactorZ * gScale + }; + var uvScale = new float[2] { mdtHeader.uvScaleX, mdtHeader.uvScaleY }; + + // read vertices array + var vOffset = offset + mdtHeader.toVertexCount + mdtHeader.bytesToFirstVertex; + var vCount = mdtHeader.vertsCount; + var vertices = Utils.ReadArray(data, vOffset, vCount, 3, + s => + { + return new[] + { + s[0] * scale[0], s[1] * scale[1], + s[2] * scale[2] + }; + }); + + // read colors array + var colors = new float[mdtHeader.colorsCount][]; + if ((features & MeshFeatures.Colors) == MeshFeatures.Colors) + { + var cOffset = offset + mdtHeader.toVertexCount + mdtHeader.bytesToFirstColor; + var cCount = mdtHeader.colorsCount; + colors = Utils.ReadArray(data, cOffset, cCount, 3, + s => + { + return new[] + { s[0] / 256f, s[1] / 256f, s[2] / 256f }; + }); + } + + // read texcoords array + var tOffset = offset + mdtHeader.toVertexCount + mdtHeader.uvOffset; + var tCount = mdtHeader.uvCount; + var uvs = Utils.ReadArray(data, tOffset, tCount, 2, + s => + { + return new[] + { + s[0] / 127f * uvScale[0], s[1] / 127f * uvScale[1] + }; + }); + + // read weights array + var wOffset = offset + mdtHeader.toVertexCount + mdtHeader.toWeightsOffset; + var weights = Utils.ReadArray(data, wOffset, vCount, 4, s => + { + var output = new float[4]; + for (var i = 0; i < 4; i++) + { + output[i] = s[i] / 32768f; + } + + return output; + }); + + // read bones per vertex array + var bOffset = offset + mdtHeader.toVertexCount + mdtHeader.toBoneIndicesOffset; + var boneIndices = Utils.ReadArray(data, bOffset, vCount, 4, s => + { + var output = new int[4]; + for (var i = 0; i < 4; i++) + { + output[i] = s[i]; + } + + return output; + }); + + var submeshes = new List(); + var triangleGroupOffset = offset + mdtHeader.vertexAttrSize; + var lastEndIndex = 0; + + var verticesList = new List(); + var trianglesList = new List(); + var colorsList = new List(); + var uvList = new List(); + var weightsList = new List(); + var bonesList = new List(); + + for (var submeshIndex = 0; submeshIndex < mdtHeader.triangleGroupCount; submeshIndex++) + { + var triangleGroupHeader = Utils.ReadStruct(data, ref triangleGroupOffset); + if (triangleGroupHeader.headerSize == 32) + { + continue; + } + + var submesh = new SubMesh(); + + var triangleIndexOffset = triangleGroupOffset + triangleGroupHeader.headerSize; + var triangles = ReadTriangles(triangleGroupHeader, triangleIndexOffset, data); + var cornerVerts = GetCornerAttribute(vertices, triangles, triangleIndexOffset, "vertices"); + verticesList.AddRange(cornerVerts); + trianglesList.AddRange(Enumerable.Range(lastEndIndex, cornerVerts.Length)); + + if ((features & MeshFeatures.Weights) == MeshFeatures.Weights) + { + var cornerWeights = GetCornerAttribute(weights, triangles, 0, "weights"); + weightsList.AddRange(cornerWeights); + var cornerBoneIndices = GetCornerAttribute(boneIndices, triangles, 0, "vertex bones"); + bonesList.AddRange(cornerBoneIndices); + } + + submesh.materialIndex = BitConverter.ToInt32(data, triangleGroupOffset + 32); + submesh.startIndex = lastEndIndex; + submesh.indexCount = triangles.Length; + lastEndIndex += cornerVerts.Length; + submeshes.Add(submesh); + + if ((features & MeshFeatures.Colors) == MeshFeatures.Colors) + { + var colorsTriangleOffset = BitConverter.ToInt32(data, triangleGroupOffset + 40); + if (colorsTriangleOffset != 0) + { + colorsTriangleOffset += triangleGroupOffset; + var colorTriangles = ReadTriangles(triangleGroupHeader, colorsTriangleOffset, data); + var cornerColors = + GetCornerAttribute(colors, colorTriangles, colorsTriangleOffset, "color"); + colorsList.AddRange(cornerColors); + } + } + + if ((features & MeshFeatures.UVs) == MeshFeatures.UVs && + triangleGroupHeader.ReadMode != TriangleReadMode.None) + { + var uvOffset = BitConverter.ToInt32(data, triangleGroupOffset + 36); + if (uvOffset != 0) + { + uvOffset += triangleGroupOffset; + var uvTriangles = ReadTriangles(triangleGroupHeader, uvOffset, data); + var cornerUVs = GetCornerAttribute(uvs, uvTriangles, uvOffset, "texcoords"); + uvList.AddRange(cornerUVs); + } + } + + triangleGroupOffset += triangleGroupHeader.toNext; + } + + mesh.vertices = verticesList.ToArray(); + mesh.triangles = trianglesList.ToArray(); + mesh.color = colorsList.ToArray(); + mesh.uv = uvList.ToArray(); + mesh.weights = weightsList.ToArray(); + mesh.bones = bonesList.ToArray(); + mesh.submeshes = submeshes.ToArray(); + } + + return scene; + } + + private static string ReadNames(MDSHeader header, byte[] data) + { + var offset = header.offsetToNames; + var decodedString = Encoding.UTF8.GetString(data, offset, header.namesBlockSize); + return decodedString.Replace("\0", " "); + } + + private static void ReadNodes(MDSScene scene, MDSHeader header, byte[] data, string names) + { + scene.nodes = new MDSNode[header.nodesCount]; + + var offset = header.offsetToNodes; + for (var i = 0; i < header.nodesCount; i++) + { + var node = Utils.ReadStruct(data, ref offset, true); + + var type = node.meshIndex < 0 ? NodeType.Bone : NodeType.Mesh; + + MDSNode mdsNode; + if (type == NodeType.Bone) + { + var boneNode = new MDSBone(); + mdsNode = new MDSNode(); + mdsNode = boneNode; + } + else + { + mdsNode = new MDSMesh(); + } + + mdsNode.bindpose = node.bindpose; + mdsNode.name = Utils.GetName(names, node.nameOffset); + mdsNode.transform = node.matrix; + mdsNode.index = i; + mdsNode.parentIndex = node.parentIndex; + mdsNode.type = type; + mdsNode.meshIndex = node.meshIndex; + + scene.nodes[i] = mdsNode; + } + } + + private static void ReadMaterials(MDSScene scene, MDSHeader header, byte[] data, string names) + { + var offset = header.offsetToMaterials; + + scene.materials = new MDSMaterial[header.materialsCount]; + + for (var i = 0; i < header.materialsCount; i++) + { + var rawMaterial = Utils.ReadStruct(data, ref offset, true); + var mdsMaterial = new MDSMaterial(); + mdsMaterial.materialType = rawMaterial.value17; + + if (rawMaterial.value13 == 1) + { + mdsMaterial.name = "NONE"; + mdsMaterial.textureName = "NONE"; + } + else + { + mdsMaterial.name = Utils.GetName(names, rawMaterial.nameOffset); + mdsMaterial.textureName = Utils.GetName(names, rawMaterial.textureNameOffset); + } + + scene.materials[i] = mdsMaterial; + } + } + + private static int[] ReadTriangles(TriangleGroupHeader header, int offset, byte[] data) + { + var triangles = new List(); + if (header.ReadMode == TriangleReadMode.Triangle) + { + for (var i = 0; i < header.idsCount; i += 3) + { + var triangle = Utils.ReadArray(data, offset, 3, 0, i => i); + offset += 6; + + triangles.Add(triangle[0]); + triangles.Add(triangle[1]); + triangles.Add(triangle[2]); + } + } + + var readCount = 0; + if (header.ReadMode == TriangleReadMode.TriangleStrip) + { + var isDefaultOrder = true; + while (readCount < header.idsCount - 2) + { + var triangle = Utils.ReadArray(data, offset, 3, 0, i => i); + readCount++; + + triangles.Add(isDefaultOrder ? triangle[0] : triangle[1]); + triangles.Add(isDefaultOrder ? triangle[1] : triangle[0]); + triangles.Add(triangle[2]); + offset += 2; + + isDefaultOrder = !isDefaultOrder; + } + } + + return triangles.ToArray(); + } + + private static T[][] GetCornerAttribute(T[][] attribute, int[] triangles, int address, string content) + { + var faceAttribute = new T[triangles.Length][]; + for (var i = 0; i < triangles.Length; i++) + { + var triangleVertexIndex = triangles[i]; + try + { + faceAttribute[i] = attribute[triangleVertexIndex]; + } + catch (Exception e) + { + Console.WriteLine($"Bad index [{triangleVertexIndex}] in {content} array. Array start index: {address}, elements: {attribute.Length}"); + break; + } + } + + return faceAttribute; + } +} \ No newline at end of file diff --git a/dq8chr2glb/Core/MDSFormat/Types.cs b/dq8chr2glb/Core/MDSFormat/Types.cs new file mode 100644 index 0000000..13397df --- /dev/null +++ b/dq8chr2glb/Core/MDSFormat/Types.cs @@ -0,0 +1,81 @@ +using System.Runtime.InteropServices; + +namespace dq8chr2glb.Core.MDSFormat; + +public class MDSScene +{ + public MDSNode[] nodes; + public MDSMaterial[] materials; +} + +public class MDSNode +{ + public string name; + public int index; + public int parentIndex; + public MDSMatrix transform; + public MDSMatrix bindpose; + public NodeType type; + public int meshIndex; +} + +public class MDSBone : MDSNode +{ +} + +public class MDSMesh : MDSNode +{ + public SubMesh[] submeshes; + public MeshFeatures features; + public AABB bounds; + // merged data + public float[][] vertices; + public float[][] color; + public float[][] uv; + public float[][] weights; + public int[][] bones; + public int[] triangles; +} + +public class SubMesh +{ + public int materialIndex; + public int startIndex; + public int indexCount; +} + +public class MDSMaterial +{ + public string name; + public string textureName; + public int materialType; +} + +[StructLayout(LayoutKind.Explicit, Size=64)] +public struct MDSMatrix +{ + [FieldOffset(00)]public float m00; + [FieldOffset(04)]public float m01; + [FieldOffset(08)]public float m02; + [FieldOffset(12)]public float m03; + [FieldOffset(16)]public float m10; + [FieldOffset(20)]public float m11; + [FieldOffset(24)]public float m12; + [FieldOffset(28)]public float m13; + [FieldOffset(32)]public float m20; + [FieldOffset(36)]public float m21; + [FieldOffset(40)]public float m22; + [FieldOffset(44)]public float m23; + [FieldOffset(48)]public float m30; + [FieldOffset(52)]public float m31; + [FieldOffset(56)]public float m32; + [FieldOffset(60)]public float m33; + + public float[] elements => new[] + { + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33 + }; +} \ No newline at end of file diff --git a/dq8chr2glb/Core/MOTFormat/Importer.cs b/dq8chr2glb/Core/MOTFormat/Importer.cs new file mode 100644 index 0000000..8b11b79 --- /dev/null +++ b/dq8chr2glb/Core/MOTFormat/Importer.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace dq8chr2glb.Core.MOTFormat; + +public class Importer +{ + public List Import(byte[] data) + { + var offset = 0; + var header = Utils.ReadStruct(data, ref offset); + offset += header.bonesOffset; + + var boneCount = header.boneCount; + var animation = new List(); + for (var i = 0; i < boneCount; i++) + { + var motionCurves = ReadBoneAnimation(data, ref offset); + animation.AddRange(motionCurves); + } + + return animation; + } + + private List GetTransformGroups(byte[] data, BoneHeader header, int offset, out int offsetAfter) + { + var attribCount = TransformFlagsHelper.GetAttribCount(header.transformFlags); + var attribHeadersOffset = attribCount > 1 ? 32 : 16; + + var transformGroups = new List(); + var transformGroupOffset = offset; + for (var i = 0; i < attribCount; i++) + { + var group = Utils.ReadStruct(data, ref transformGroupOffset, true); + transformGroups.Add(group); + } + + offsetAfter = offset + attribHeadersOffset; + + return transformGroups; + } + + private void ReadRotationKeyframes(byte[] data, MotionCurve curve, int offset) + { + curve.curveType = KeyframeType.Quaternion; + var headerOffset = offset; + var header = Utils.ReadStruct(data, ref headerOffset); + var valuesOffset = headerOffset + header.valuesOffset; + + if (header.toFramesOffset == 0) + { + var count = header.framesCount; + for (var i = 0; i < count; i++) + { + var keyframe = new KeyFrame(); + keyframe.frame = i; + + var scale = new Vector4(header.scaleX, header.scaleY, + header.scaleZ, header.scaleW); + + keyframe.rotation = ReadQuaternion(data, valuesOffset, scale); + curve.keyframes.Add(keyframe); + valuesOffset += 8; + } + } + else + { + var framesCount = header.framesCount; + var toFramesOffset = headerOffset + header.toFramesOffset; + var frames = ReadFrames(data, toFramesOffset, framesCount); + if (frames.Length == 0) + { + return; + } + + var maxFrame = frames.Max(); + curve.keyframes = new List(); + for (var i = 0; i < maxFrame + 1; i++) + { + curve.keyframes.Add(null); + } + + var toValues = header.valuesOffset; + foreach (var frame in frames) + { + var scale = new Vector4(header.scaleX, header.scaleY, + header.scaleZ, header.scaleW); + + var keyframe = new KeyFrame(); + keyframe.frame = frame; + keyframe.type = KeyframeType.Quaternion; + keyframe.rotation = ReadQuaternion(data, valuesOffset, scale); + + curve.keyframes[frame] = keyframe; + valuesOffset += 8; + } + } + } + + private void ReadPositionKeyframes(byte[] data, MotionCurve curve, int offset) + { + curve.curveType = KeyframeType.Translation; + var headerOffset = offset; + var header = Utils.ReadStruct(data, ref offset, false); + var valuesOffset = headerOffset + header.valuesOffset; + + if (header.toFramesOffset == 0) + { + var count = header.framesCount; + for (var i = 0; i < count; i++) + { + var keyframe = new KeyFrame(); + keyframe.frame = i; + + var scale = new Vector3(header.scaleX, header.scaleY, + header.scaleZ); + + var translation = ReadVector3(data, valuesOffset, scale); + keyframe.translation = translation; + curve.keyframes.Add(keyframe); + + valuesOffset += 6; + } + } + else + { + var framesCount = header.framesCount; + var toFramesOffset = headerOffset + header.toFramesOffset; + var frames = ReadFrames(data, toFramesOffset, framesCount); + if (frames.Length == 0) + { + return; + } + + var maxFrame = frames.Max(); + curve.keyframes = new List(); + for (var i = 0; i < maxFrame + 1; i++) + { + curve.keyframes.Add(null); + } + + var toValues = header.valuesOffset; + foreach (var frame in frames) + { + var scale = new Vector3(header.scaleX, header.scaleY, + header.scaleZ); + + var keyframe = new KeyFrame(); + keyframe.frame = frame; + keyframe.type = KeyframeType.Translation; + keyframe.translation = ReadVector3(data, valuesOffset, scale); + + curve.keyframes[frame] = keyframe; + valuesOffset += 6; + } + } + } + + private List ReadBoneAnimation(byte[] data, ref int offset) + { + var headerPosition = offset; + var header = Utils.ReadStruct(data, ref offset, true); + + var groupsBlockPosition = offset; + var transformGroups = GetTransformGroups(data, header, groupsBlockPosition, out var offsetAfter); + + var boneCurves = new List(); + foreach (var group in transformGroups) + { + var curve = new MotionCurve(); + curve.boneIndex = header.boneIndex; + curve.keyframes = new List(); + + switch (group.keyframeType) + { + case KeyframeType.Quaternion: + ReadRotationKeyframes(data, curve, headerPosition + group.offset); + break; + case KeyframeType.Translation: + ReadPositionKeyframes(data, curve, headerPosition + group.offset); + break; + default: + // Debug.Log($"[UNK KF TYPE]: {group.keyframeType}"); + break; + } + + boneCurves.Add(curve); + } + + switch (header.transformFlags) + { + case TransformFlags.Rotation: + // Debug.Log($"Rotation: {(int)header.transformFlags}."); + break; + case TransformFlags.TR: + // Debug.Log($"Rot/Pos: {(int)header.transformFlags}"); + break; + default: + // Debug.Log($"Unknown flag: {(int)header.transformFlags}, {header.transformFlags}"); + break; + } + + offset = headerPosition + header.toNext; + return boneCurves; + } + + private static Quaternion ReadQuaternion(byte[] data, int offset, Vector4 scale) + { + var x = ShortToFloat(data, offset, scale.X); + offset += 2; + var y = ShortToFloat(data, offset, scale.Y); + offset += 2; + var z = ShortToFloat(data, offset, scale.Z); + offset += 2; + var w = ShortToFloat(data, offset, scale.W); + return new Quaternion(x, y, z, -w); + } + + private static Vector3 ReadVector3(byte[] data, int offset, Vector3 scale) + { + var x = ShortToFloat(data, offset, scale.X); + offset += 2; + var y = ShortToFloat(data, offset, scale.Y); + offset += 2; + var z = ShortToFloat(data, offset, scale.Z); + return new Vector3(x, y, z); + } + + private static float ShortToFloat(byte[] data, int offset, float scale) + { + var value = ReadShort(data, offset); + return (float)value / (float)short.MaxValue * scale; + } + + private static short ReadShort(byte[] data, int offset) + { + return BitConverter.ToInt16(new[] { data[offset], data[offset + 1] }); + } + + public static int[] ReadFrames(byte[] data, int offset, int count) + { + var result = new int[count]; + for (var i = 0; i < count; i++) + { + result[i] = ReadShort(data, offset + i * 2); + } + + return result; + } +} \ No newline at end of file diff --git a/dq8chr2glb/Core/MOTFormat/Types.cs b/dq8chr2glb/Core/MOTFormat/Types.cs new file mode 100644 index 0000000..fedb6bb --- /dev/null +++ b/dq8chr2glb/Core/MOTFormat/Types.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace dq8chr2glb.Core.MOTFormat +{ + [StructLayout(LayoutKind.Explicit, Size = 64)] + public struct MOTHeader + { + [FieldOffset(00)] public int MagicCode; + [FieldOffset(08)] public int headerSize; + [FieldOffset(28)] public int boneCount; + [FieldOffset(32)] public int bonesOffset; + [FieldOffset(48)] public int fileSizeClaim; + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + public struct BoneHeader + { + [FieldOffset(00)] public int toNext; + [FieldOffset(04)] public int boneIndex; + [FieldOffset(12)] public TransformFlags transformFlags; + } + + [StructLayout(LayoutKind.Explicit, Size = 8)] + public struct TransformGroup + { + [FieldOffset(00)] public KeyframeType keyframeType; + [FieldOffset(04)] public int offset; + } + + [StructLayout(LayoutKind.Explicit, Size = 32)] + public struct KeyframesBlockHeader + { + [FieldOffset(00)] public short flag0; + [FieldOffset(02)] public short flag1; + [FieldOffset(06)] public short framesCount; + [FieldOffset(08)] public short toFramesOffset; + [FieldOffset(12)] public short valuesOffset; + [FieldOffset(16)] public float scaleX; + [FieldOffset(20)] public float scaleY; + [FieldOffset(24)] public float scaleZ; + [FieldOffset(28)] public float scaleW; + } + + [Flags] + public enum TransformFlags : short + { + None = 0, + Translation = 1 << 0, + Rotation = 1 << 1, + Scale = 1 << 2, + + TR = Translation | Rotation, + TS = Translation | Scale, + RS = Rotation | Scale, + TRS = Translation | Rotation | Scale + } + + public enum KeyframeType : int + { + None = 0, + Quaternion = 1, + Translation = 3, + Scale = 6, + } + + public class MotionCurve + { + public int boneIndex; + public KeyframeType curveType; + public List keyframes; + } + + public class KeyFrame + { + public int frame; + public KeyframeType type; + public Quaternion rotation; + public Vector3 translation; + public Vector3 scale; + } + + public static class TransformFlagsHelper + { + public static int GetAttribCount(TransformFlags flag) + { + var value = (int)flag; + return (value & 1) + ((value >> 1) & 1) + ((value >> 2) & 1); + } + } +} \ No newline at end of file diff --git a/dq8chr2glb/Core/Types.cs b/dq8chr2glb/Core/Types.cs new file mode 100644 index 0000000..0bb9739 --- /dev/null +++ b/dq8chr2glb/Core/Types.cs @@ -0,0 +1,2 @@ +namespace dq8chr2glb.Core; + diff --git a/dq8chr2glb/Core/Utils.cs b/dq8chr2glb/Core/Utils.cs new file mode 100644 index 0000000..aed10cd --- /dev/null +++ b/dq8chr2glb/Core/Utils.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.InteropServices; +using dq8chr2glb.Core.MDSFormat; + +namespace dq8chr2glb.Core; + +public static class Utils +{ + public static T ReadStruct(byte[] data, ref int offset, bool addToOffset = false) + { + var size = Marshal.SizeOf(typeof(T)); + var buffer = new byte[size]; + Array.Copy(data, offset, buffer, 0, size); + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + var structure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); + handle.Free(); + if (addToOffset) + { + offset += size; + } + + return structure; + } + + public static T2[] ReadArray(byte[] data, int start, int count, int size, Func convert) + { + var isArray = typeof(T2).IsArray; + var result = new T2[count]; + var elementType = typeof(T).GetElementType(); + + for (var i = 0; i < count; i++) + { + var elementOffset = start + i * (isArray ? size * Marshal.SizeOf(elementType) : Marshal.SizeOf(typeof(T))); + + if (isArray) + { + var array = Array.CreateInstance(elementType, size); + Buffer.BlockCopy(data, elementOffset, array, 0, size * Marshal.SizeOf(elementType)); + result[i] = convert((T)(object)array); + } + else + { + var buffer = new byte[Marshal.SizeOf(typeof(T))]; + Array.Copy(data, elementOffset, buffer, 0, buffer.Length); + + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + var item = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); + handle.Free(); + + result[i] = convert(item); + } + } + + return result; + } + + public static string GetName(string raw, int startIndex) + { + var endIndex = raw.IndexOf(' ', startIndex); + + if (endIndex == -1) + { + return raw[startIndex..]; + } + + return raw.Substring(startIndex, endIndex - startIndex); + } + + public static MDSMesh GetMeshByIndex(MDSScene scene, int index) + { + foreach (var node in scene.nodes) + { + if (node.type == NodeType.Mesh) + { + var mesh = node as MDSMesh; + if (mesh.meshIndex == index) + { + return mesh; + } + } + } + + return null; + } + + public static MeshFeatures GetFeaturesFlags(bool vertices, bool colors, bool uvs, bool weights) + { + var result = MeshFeatures.None; + if (vertices) result |= MeshFeatures.Verts; + if (colors) result |= MeshFeatures.Colors; + if (uvs) result |= MeshFeatures.UVs; + if (weights) result |= MeshFeatures.Weights; + return result; + } +} diff --git a/dq8chr2glb/Program.cs b/dq8chr2glb/Program.cs new file mode 100644 index 0000000..4713946 --- /dev/null +++ b/dq8chr2glb/Program.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace dq8chr2glb; + +public class Program +{ + public static void Main(string[] args) + { + if (args.Length < 2) + { + ShowHelp(); + return; + } + + var extractOnly = false; + var textFormat = false; + var batchMode = false; + + var processedArgs = new List(); + for (int i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (arg == "-e") + extractOnly = true; + else if (arg == "-t") + textFormat = true; + else if (arg == "-b") + batchMode = true; + else + processedArgs.Add(arg); + } + + if (batchMode) + { + if (processedArgs.Count != 1) + { + Console.WriteLine("Error: Batch mode (-b) requires exactly one argument: "); + ShowHelp(); + return; + } + } + else + { + if (processedArgs.Count != 2) + { + Console.WriteLine("Error: Expected and "); + ShowHelp(); + return; + } + } + + var inputPath = processedArgs[0]; + var outputPath = batchMode ? inputPath : processedArgs[1]; + + var chrFile = new ChrFile(); + chrFile.convert = !extractOnly; + chrFile.extract = extractOnly; + chrFile.textFormat = textFormat; + + if (batchMode) + { + var files = Directory.GetFiles(inputPath, "*.chr", SearchOption.TopDirectoryOnly); + foreach (var file in files) + { + Console.WriteLine($"Processing: {Path.GetFileName(file)}"); + try + { + chrFile.Process(file, outputPath); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + chrFile.Clean(); + } + + Console.WriteLine(files.Length == 0 ? "No .chr files found in the input directory." : "Done!"); + } + else + { + if (!File.Exists(inputPath)) + { + Console.WriteLine($"Error: Input file not found: {inputPath}"); + return; + } + + chrFile.Process(inputPath, outputPath); + Console.WriteLine("Done!"); + } + } + + private static void ShowHelp() + { + Console.WriteLine("Converts .CHR files from Dragon Quest VIII (Playstation 2) to .glb/.glTF format."); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" chrFormat.exe [options]"); + Console.WriteLine(" chrFormat.exe -b (batch mode: output files are saved in the )"); + Console.WriteLine("Examples:"); + Console.WriteLine(" chrFormat.exe \"C:\\Users\\Boris\\Desktop\\ChrFormatTest\\ap002.chr\" \"C:\\Users\\Boris\\Desktop\\ChrFormatTest\" -e"); + Console.WriteLine(" chrFormat.exe \"C:\\Users\\Boris\\Desktop\\ChrFormatTest\" -b"); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" -e - Extract only - unpack .chr without conversion"); + Console.WriteLine(" -t - Output as .glTF (text) instead of .glb (binary)"); + Console.WriteLine(" -b - Batch mode - process all .chr files in the input directory"); + Console.WriteLine(); + var line = Console.ReadLine(); + } +} \ No newline at end of file diff --git a/dq8chr2glb/TM2Format/TM2Format.cs b/dq8chr2glb/TM2Format/TM2Format.cs new file mode 100644 index 0000000..8ce483c --- /dev/null +++ b/dq8chr2glb/TM2Format/TM2Format.cs @@ -0,0 +1,78 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace dq8chr2glb.TM2Format; + +// Взято из https://github.com/Souzooka/TM2Unswizzler +public static class TM2Format +{ + public static Texture GetImage(byte[] data, string name) + { + var header = TM2Header.GetHeader(data); + var image = GetImageBuffer(header, data); + + var img = new Image(header.Width, header.Height); + + for (var y = 0; y < header.Height; y++) + { + for (var x = 0; x < header.Width; x++) + { + var i = (y * header.Width + x) * 4; + var r = image[i + 2]; + var g = image[i + 1]; + var b = image[i + 0]; + var a = image[i + 3]; + + img[x, y] = new Rgba32(r, g, b, a); + } + } + + return new Texture(name, img); + } + + private static int GetAddress8BppSwizzle(int width, int x, int y) + { + var block = (y & (~0x0f)) * width + (x & (~0x0f)) * 2; + var swap = (((y + 2) >> 2) & 0x01) * 4; + var line = (((y & (~0x03)) >> 1) + (y & 0x01)) & 0x07; + var column = line * width * 2 + ((x + swap) & 0x07) * 4; + var offset = ((y >> 1) & 0x01) + ((x >> 2) & 0x02); + return block + column + offset; + } + + private static byte[][] GetPalette8bbp(byte[] file, TM2Header header) + { + var palette = new byte[256][]; + + for (var i = 0; i < 256; ++i) + { + var rgba = new byte[4]; + Array.Copy(file, 0x40 + header.ImageSize + i * 4, rgba, 0, 4); + rgba[3] = (byte)Math.Min(rgba[3] << 1, 0xFF); + + (rgba[2], rgba[0]) = (rgba[0], rgba[2]); + + palette[(i & 0xe7) | ((i & 0x10) >> 1) | ((i & 0x08) << 1)] = rgba; + } + + return palette; + } + + private static byte[] GetImageBuffer(TM2Header header, byte[] tm2file) + { + var palette = GetPalette8bbp(tm2file, header); + + var imageBuf = new byte[4 * header.Width * header.Height]; + for (var y = header.Height - 1; y >= 0; --y) + { + for (var x = 0; x < header.Width; ++x) + { + var c = tm2file[0x40 + GetAddress8BppSwizzle(header.Width, x, y)]; + Array.Copy(palette[c], 0, imageBuf, (x * 4) + (header.Width * y) * 4, 4); + } + } + + return imageBuf; + } +} \ No newline at end of file diff --git a/dq8chr2glb/TM2Format/TM2Types.cs b/dq8chr2glb/TM2Format/TM2Types.cs new file mode 100644 index 0000000..f36ca4e --- /dev/null +++ b/dq8chr2glb/TM2Format/TM2Types.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.InteropServices; + +namespace dq8chr2glb.TM2Format; + +public struct TM2Header +{ + public int Label; + public byte Version; + public byte Format; + public int padding0; + public int padding1; + public uint TotalSize; + public uint ClutSize; + public uint ImageSize; + public ushort HeaderSize; + public ushort ClutColors; + public TM2ImageFormat TM2ImageFormat; + public byte MipMapTextures; + public ClutType ClutType; + ImageType ImageType; + public ushort Width; + public ushort Height; + public ulong GsTex0; + public ulong GsText1; + public uint GsReg; + public uint GsTexClut; + + public static TM2Header GetHeader(byte[] file) + { + var headerBuf = new byte[Marshal.SizeOf()]; + Array.Copy(file, headerBuf, headerBuf.Length); + var handle = GCHandle.Alloc(headerBuf, GCHandleType.Pinned); + var header = (TM2Header)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(TM2Header)); + handle.Free(); + return header; + } +} + +public enum TM2ImageFormat : byte +{ + Default = 0, + Format16bpp = 1, + Format24bpp = 2, + Format32bpp = 3, + Format4bbp = 4, + Format8bbp = 5 +} + +public enum ImageType : byte +{ + unk0 = 0 +} + +public enum ClutType : byte +{ + unk0 = 0 +} diff --git a/dq8chr2glb/TM2Format/Texture.cs b/dq8chr2glb/TM2Format/Texture.cs new file mode 100644 index 0000000..a5574de --- /dev/null +++ b/dq8chr2glb/TM2Format/Texture.cs @@ -0,0 +1,16 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace dq8chr2glb.TM2Format; + +public class Texture +{ + public readonly string name; + public readonly Image data; + + public Texture(string name, Image data) + { + this.name = name; + this.data = data; + } +} \ No newline at end of file diff --git a/dq8chr2glb/dq8chr2glb.csproj b/dq8chr2glb/dq8chr2glb.csproj new file mode 100644 index 0000000..ba57cf5 --- /dev/null +++ b/dq8chr2glb/dq8chr2glb.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + disable + enable + Exe + 11 + dq8chr2glb + + + + + + + + + + + + + + + + + + + + + +