add project files
This commit is contained in:
@@ -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
|
||||||
@@ -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<TM2Format.Texture> textures = new();
|
||||||
|
public List<MDSConverter> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IncludedFile> FromBytes(byte[] chrFile)
|
||||||
|
{
|
||||||
|
var result = new List<IncludedFile>();
|
||||||
|
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<IncludedFile>();
|
||||||
|
|
||||||
|
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<IncludedFile> UnpackImgContainer(string root, byte[] imgData)
|
||||||
|
{
|
||||||
|
var rootFolderName = Path.GetFileNameWithoutExtension(root) + "_img";
|
||||||
|
|
||||||
|
var fileCount = BitConverter.ToUInt32(imgData, 8);
|
||||||
|
var currentOffset = 16L;
|
||||||
|
|
||||||
|
var files = new List<IncludedFile>();
|
||||||
|
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<ImgFile>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace dq8chr2glb.Container;
|
||||||
|
|
||||||
|
public enum FileExtension
|
||||||
|
{
|
||||||
|
CHR,
|
||||||
|
CFG,
|
||||||
|
TEXT,
|
||||||
|
IMG,
|
||||||
|
TM2,
|
||||||
|
MDS,
|
||||||
|
MOT,
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace dq8chr2glb.Container;
|
||||||
|
|
||||||
|
public class IncludedFile
|
||||||
|
{
|
||||||
|
public string name;
|
||||||
|
public FileExtension extension;
|
||||||
|
public byte[] data = Array.Empty<byte>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace dq8chr2glb.Container;
|
||||||
|
|
||||||
|
public static class Utils
|
||||||
|
{
|
||||||
|
public static List<IncludedFile> SortFiles(List<IncludedFile> files)
|
||||||
|
{
|
||||||
|
var other = new List<IncludedFile>();
|
||||||
|
var configs = new List<IncludedFile>();
|
||||||
|
var textures = new List<IncludedFile>();
|
||||||
|
var models = new List<IncludedFile>();
|
||||||
|
var motion = new List<IncludedFile>();
|
||||||
|
|
||||||
|
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<IncludedFile>();
|
||||||
|
output.AddRange(other);
|
||||||
|
output.AddRange(configs);
|
||||||
|
output.AddRange(textures);
|
||||||
|
output.AddRange(models);
|
||||||
|
output.AddRange(motion);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<int, Node> _nodeMap = new();
|
||||||
|
private readonly List<Node> _boneNodes = new();
|
||||||
|
private readonly Dictionary<string, MaterialBuilder> _materialCache = new();
|
||||||
|
private Dictionary<string, ImageBuilder> _textureBuilders = new Dictionary<string, ImageBuilder>();
|
||||||
|
|
||||||
|
private const float FRAMERATE = 16f;
|
||||||
|
|
||||||
|
public MDSConverter(string name)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_gltfModel = ModelRoot.CreateModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Convert(MDSScene scene, List<TM2Format.Texture> images, string name)
|
||||||
|
{
|
||||||
|
var gltfScene = _gltfModel.UseScene("DefaultScene");
|
||||||
|
|
||||||
|
var nodesWithParents = new HashSet<int>();
|
||||||
|
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<MDSMesh>().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<MotionCurve> 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<float, Quaternion>();
|
||||||
|
var translationKeyframes = new Dictionary<float, Vector3>();
|
||||||
|
|
||||||
|
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<TM2Format.Texture> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, MaterialBuilder> materialCache)
|
||||||
|
{
|
||||||
|
var (hasUvs, hasSkinning) = AnalyzeMeshAttributes(mdsMesh);
|
||||||
|
|
||||||
|
// Выбираем подходящий тип вершины на основе атрибутов
|
||||||
|
if (hasSkinning)
|
||||||
|
{
|
||||||
|
if (hasUvs)
|
||||||
|
{
|
||||||
|
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexJoints4>(
|
||||||
|
_root, mdsMesh, materials, materialCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexJoints4>(
|
||||||
|
_root, mdsMesh, materials, materialCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (hasUvs)
|
||||||
|
{
|
||||||
|
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexEmpty>(
|
||||||
|
_root, mdsMesh, materials, materialCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexEmpty>(
|
||||||
|
_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<TvG, TvM, TvS>(
|
||||||
|
ModelRoot root,
|
||||||
|
MDSMesh mdsMesh,
|
||||||
|
MDSMaterial[] materials,
|
||||||
|
Dictionary<string, MaterialBuilder> materialCache)
|
||||||
|
where TvG : struct, IVertexGeometry
|
||||||
|
where TvM : struct, IVertexMaterial
|
||||||
|
where TvS : struct, IVertexSkinning
|
||||||
|
{
|
||||||
|
var meshBuilder = new MeshBuilder<MaterialBuilder, TvG, TvM, TvS>(mdsMesh.name);
|
||||||
|
|
||||||
|
var positions = new List<Vector3>();
|
||||||
|
var normals = new List<Vector3>();
|
||||||
|
var texCoords = new List<Vector2>();
|
||||||
|
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<int>();
|
||||||
|
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<TvG, TvM, TvS>(
|
||||||
|
positions[idxA],
|
||||||
|
normals[idxA],
|
||||||
|
texCoords.Count > idxA ? texCoords[idxA] : Vector2.Zero,
|
||||||
|
skinningData.Count > idxA ? skinningData[idxA] : null);
|
||||||
|
|
||||||
|
var vertexB = CreateVertex<TvG, TvM, TvS>(
|
||||||
|
positions[idxB],
|
||||||
|
normals[idxB],
|
||||||
|
texCoords.Count > idxB ? texCoords[idxB] : Vector2.Zero,
|
||||||
|
skinningData.Count > idxB ? skinningData[idxB] : null);
|
||||||
|
|
||||||
|
var vertexC = CreateVertex<TvG, TvM, TvS>(
|
||||||
|
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<TvG, TvM, TvS> CreateVertex<TvG, TvM, TvS>(
|
||||||
|
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<TvG, TvM, TvS>(geometry, material, skinning);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ComputeNormals(List<Vector3> positions, int[] triangles, List<Vector3> 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace dq8chr2glb.Core.InfoCfg;
|
||||||
|
|
||||||
|
public class Clip
|
||||||
|
{
|
||||||
|
public string name;
|
||||||
|
public int startFrame;
|
||||||
|
public int endFrame;
|
||||||
|
public float speed;
|
||||||
|
}
|
||||||
@@ -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<Clip> GetClips()
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
var clips = new List<Clip>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Clip> clips;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<MDSHeader>(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<MDTHeader>(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<short[], float[]>(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<byte[], float[]>(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<byte[], float[]>(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<ushort[], float[]>(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<short[], int[]>(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<SubMesh>();
|
||||||
|
var triangleGroupOffset = offset + mdtHeader.vertexAttrSize;
|
||||||
|
var lastEndIndex = 0;
|
||||||
|
|
||||||
|
var verticesList = new List<float[]>();
|
||||||
|
var trianglesList = new List<int>();
|
||||||
|
var colorsList = new List<float[]>();
|
||||||
|
var uvList = new List<float[]>();
|
||||||
|
var weightsList = new List<float[]>();
|
||||||
|
var bonesList = new List<int[]>();
|
||||||
|
|
||||||
|
for (var submeshIndex = 0; submeshIndex < mdtHeader.triangleGroupCount; submeshIndex++)
|
||||||
|
{
|
||||||
|
var triangleGroupHeader = Utils.ReadStruct<TriangleGroupHeader>(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<Node>(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<Material>(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<int>();
|
||||||
|
if (header.ReadMode == TriangleReadMode.Triangle)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < header.idsCount; i += 3)
|
||||||
|
{
|
||||||
|
var triangle = Utils.ReadArray<short, int>(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<short, int>(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>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<MotionCurve> Import(byte[] data)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
var header = Utils.ReadStruct<MOTHeader>(data, ref offset);
|
||||||
|
offset += header.bonesOffset;
|
||||||
|
|
||||||
|
var boneCount = header.boneCount;
|
||||||
|
var animation = new List<MotionCurve>();
|
||||||
|
for (var i = 0; i < boneCount; i++)
|
||||||
|
{
|
||||||
|
var motionCurves = ReadBoneAnimation(data, ref offset);
|
||||||
|
animation.AddRange(motionCurves);
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TransformGroup> 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<TransformGroup>();
|
||||||
|
var transformGroupOffset = offset;
|
||||||
|
for (var i = 0; i < attribCount; i++)
|
||||||
|
{
|
||||||
|
var group = Utils.ReadStruct<TransformGroup>(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<KeyframesBlockHeader>(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<KeyFrame>();
|
||||||
|
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<KeyframesBlockHeader>(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<KeyFrame>();
|
||||||
|
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<MotionCurve> ReadBoneAnimation(byte[] data, ref int offset)
|
||||||
|
{
|
||||||
|
var headerPosition = offset;
|
||||||
|
var header = Utils.ReadStruct<BoneHeader>(data, ref offset, true);
|
||||||
|
|
||||||
|
var groupsBlockPosition = offset;
|
||||||
|
var transformGroups = GetTransformGroups(data, header, groupsBlockPosition, out var offsetAfter);
|
||||||
|
|
||||||
|
var boneCurves = new List<MotionCurve>();
|
||||||
|
foreach (var group in transformGroups)
|
||||||
|
{
|
||||||
|
var curve = new MotionCurve();
|
||||||
|
curve.boneIndex = header.boneIndex;
|
||||||
|
curve.keyframes = new List<KeyFrame>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<KeyFrame> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
namespace dq8chr2glb.Core;
|
||||||
|
|
||||||
@@ -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<T>(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<T, T2>(byte[] data, int start, int count, int size, Func<T, T2> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string>();
|
||||||
|
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: <input_dir>");
|
||||||
|
ShowHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (processedArgs.Count != 2)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error: Expected <input_file> and <output_dir>");
|
||||||
|
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 <input_file> <output_dir> [options]");
|
||||||
|
Console.WriteLine(" chrFormat.exe <input_dir> -b (batch mode: output files are saved in the <input_dir>)");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Rgba32>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<TM2Header>()];
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace dq8chr2glb.TM2Format;
|
||||||
|
|
||||||
|
public class Texture
|
||||||
|
{
|
||||||
|
public readonly string name;
|
||||||
|
public readonly Image<Rgba32> data;
|
||||||
|
|
||||||
|
public Texture(string name, Image<Rgba32> data)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<LangVersion>11</LangVersion>
|
||||||
|
<PackageId>dq8chr2glb</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SharpGLTF.Core" Version="1.0.6" />
|
||||||
|
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.6" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="11.0.0-preview.1.26104.118" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Reference\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Remove="Reference\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Reference\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user