11 Commits

Author SHA1 Message Date
Boris Nikolaev 46ddb283b2 add: 3d preview 2026-06-07 10:50:22 +03:00
Boris Nikolaev 291babd1c0 fix codestyle 2026-05-10 17:05:25 +03:00
Boris Nikolaev 87b48441f8 fix error messages 2026-02-16 12:19:53 +03:00
Boris Nikolaev 60510b1d7d fix: error logging 2026-02-15 22:49:55 +03:00
Boris Nikolaev ae99c946c5 add: Bake transform for skinned mesh 2026-02-15 22:49:22 +03:00
Boris Nikolaev c9373df232 add: Context for task data 2026-02-15 22:49:04 +03:00
Boris Nikolaev 5e2df678ab add System.CommandLine argument parser 2026-02-15 22:45:57 +03:00
Boris Nikolaev 46d2169b1d fix conversion for models if animation has 0 frames 2026-02-14 23:13:47 +03:00
Boris Nikolaev 67b2837684 fix for conversion interruption when errors occur 2026-02-14 13:31:12 +03:00
Boris Nikolaev 92b615f077 fix node index mapping in skinning 2026-02-14 12:04:52 +03:00
Boris Nikolaev 7c669b6635 fix gitignore 2026-02-13 21:34:44 +03:00
29 changed files with 1335 additions and 920 deletions
+3
View File
@@ -2,3 +2,6 @@
dq8chr2glb/bin dq8chr2glb/bin
dq8chr2glb/obj dq8chr2glb/obj
*.DotSettings.user *.DotSettings.user
dq8chr2glb/Properties
dq8chr2glb/dq8chr2glb.csproj.user
.vs
+21 -10
View File
@@ -1,6 +1,15 @@
# DQ8 chr to glTF Converter # DQ8 chr to glTF Converter
![](preview.jpg) ![](preview.jpg)
<iframe
src="https://boris-code.ru/files/ap002-preview/"
width="100%"
height="500"
style="border: 1px solid #30363d; border-radius: 8px;"
allow="autoplay"
title="3D Model Preview">
</iframe>
Convert Dragon Quest VIII (PS2) character models to modern 3D format. Convert Dragon Quest VIII (PS2) character models to modern 3D format.
A command-line tool for converting .CHR character model files from Dragon Quest VIII: Journey of the Cursed King (PlayStation 2) into glTF/GLB 3D formats for use in modern 3D applications, game engines, and viewers. A command-line tool for converting .CHR character model files from Dragon Quest VIII: Journey of the Cursed King (PlayStation 2) into glTF/GLB 3D formats for use in modern 3D applications, game engines, and viewers.
@@ -19,25 +28,27 @@ A command-line tool for converting .CHR character model files from Dragon Quest
| Option | Description | | Option | Description |
|--------|-------------| |--------|-------------|
| `-e` | Extract only - unpack `.chr` without conversion | | `-i`, `--input` | Input path |
| `-t` | Output as `.glTF` (text) instead of `.glb` (binary) | | `-o`, `--output` | Output path |
| `-b` | Batch mode - process all `.chr` files in directory | | `-e`, `--extract` | Extract only - unpack `.chr` without conversion |
| `-f`, `--format` | Output format. `GLTF` for text format or `GLB` for binary (default: `GLB`) |
| `-b`, `--batch` | Batch mode - process all `.chr` files in directory |
| `-h`, `--help` | Show help |
## Usage ## Usage
### Basic Syntax ### Basic Syntax
``` ```
dq8chr2glb.exe <input_file> <output_dir> [options] dq8chr2glb.exe -i <input_file> -o <output_dir> [options]
dq8chr2glb.exe <input_dir> -b # Batch mode dq8chr2glb.exe -i <input_dir> -b # Batch mode
dq8chr2glb.exe # No args for get help
``` ```
### Examples ### Examples
``` ```
dq8chr2glb.exe "C:\Games\DQ8\@DATA.DAT\chara\ap002.chr" "C:\Exports" dq8chr2glb.exe -i "C:\Games\DQ8\@DATA.DAT\chara\ap002.chr" -o "C:\Exports"
dq8chr2glb.exe "C:\Games\DQ8\@DATA.DAT\chara" -b dq8chr2glb.exe -i "C:\Games\DQ8\@DATA.DAT\chara" -b
dq8chr2glb.exe "C:\Games\DQ8\@DATA.DAT\chara\ap002.chr" "C:\Exports" -e dq8chr2glb.exe -i "C:\Games\DQ8\@DATA.DAT\chara\ap002.chr" -o "C:\Exports" -e
dq8chr2glb.exe "C:\Games\DQ8\@DATA.DAT\chara" -b -e dq8chr2glb.exe -i "C:\Games\DQ8\@DATA.DAT\chara" -b -t GLTF
``` ```
+62
View File
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using dq8chr2glb.Converter;
using dq8chr2glb.Converter.GLTF;
using dq8chr2glb.Core.InfoCfg;
using dq8chr2glb.Core.MOTFormat;
using dq8chr2glb.Logger;
using SharpGLTF.Schema2;
namespace dq8chr2glb;
public class AppendAnimations
{
public void Append(string glbPath, string extractedPath)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var modelName = Path.GetFileNameWithoutExtension(glbPath);
var glbDir = Path.GetDirectoryName(glbPath);
var model = ModelRoot.Load(glbPath);
var mdsConverter = new MDSConverter(modelName, model);
var ctx = new Context();
Context.Current.MdsConverters = new List<MDSConverter>();
Context.Current.MdsConverters.Add(mdsConverter);
foreach (var dataFile in Directory.GetFiles(extractedPath))
{
if (dataFile.ToLower().EndsWith(".cfg"))
{
var directory = Path.GetDirectoryName(dataFile);
ReadConfig(dataFile);
var motionFile = Path.Combine(directory, Context.Current.InfoCfg.motion);
ProcessMOTFile(motionFile);
}
}
mdsConverter.Save(@"C:\Users\Boris\Desktop\yangus");
}
private void ReadConfig(string path)
{
Log.Line(path);
var bytes = File.ReadAllBytes(path);
var data = Encoding.GetEncoding("shift_jis").GetString(bytes).TrimEnd('\0');
var configFile = new ConfigFile(data);
var info = configFile.ReadConfig();
Context.Current.InfoCfg = info;
}
private void ProcessMOTFile(string path)
{
Log.Line(path);
var bytes = File.ReadAllBytes(path);
var motImporter = new Importer();
var animation = motImporter.Import(bytes);
var converter = Context.Current.MdsConverters[0];
converter.AddAnimation(animation, Context.Current.InfoCfg);
}
}
+119 -87
View File
@@ -4,9 +4,11 @@ using System.IO;
using System.Text; using System.Text;
using dq8chr2glb.Container; using dq8chr2glb.Container;
using dq8chr2glb.Converter; using dq8chr2glb.Converter;
using dq8chr2glb.Converter.GLTF;
using dq8chr2glb.Core.InfoCfg; using dq8chr2glb.Core.InfoCfg;
using dq8chr2glb.Core.MDSFormat; using dq8chr2glb.Core.MDSFormat;
using dq8chr2glb.Core.MOTFormat; using dq8chr2glb.Core.MOTFormat;
using dq8chr2glb.Logger;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using Texture = dq8chr2glb.TM2Format.Texture; using Texture = dq8chr2glb.TM2Format.Texture;
@@ -14,80 +16,84 @@ namespace dq8chr2glb;
public class ChrFile public class ChrFile
{ {
public ModelConfig infoCfg; public bool Extract;
public List<TM2Format.Texture> textures = new(); public bool Convert;
public List<MDSConverter> mdsConverters = new(); public bool TextFormat;
public bool extract; public void Process(string inputPath, string outputPath)
public bool convert;
public bool textFormat;
public void Process(string inputPath, string outputPath, bool isBatch = false)
{ {
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var chrData = File.ReadAllBytes(inputPath);
var container = ChrContainer.FromBytes(chrData);
var outputName = Path.GetFileNameWithoutExtension(inputPath); var ctx = new Context();
var outputDir = Path.Combine(outputPath, outputName); ctx.InputPath = inputPath;
EnsurePath(outputDir); ctx.ModelName = Path.GetFileNameWithoutExtension(ctx.InputPath);
ctx.OutputPath = Path.Combine(outputPath, ctx.ModelName);
var chrData = File.ReadAllBytes(ctx.InputPath);
var container = Container.ChrFile.FromBytes(chrData);
EnsurePath(ctx.OutputPath);
foreach (var file in container) foreach (var file in container)
{ {
if (!isBatch) PrintTask(file);
{
PrintTask(file);
}
switch (file.extension) switch (file.Extension)
{ {
case FileExtension.CFG: case FileExtension.CFG:
ProcessConfig(file, outputDir); ProcessConfig(file);
break; break;
case FileExtension.TEXT: case FileExtension.TEXT:
ProcessTextFile(file, outputDir); ProcessTextFile(file);
break; break;
case FileExtension.TM2: case FileExtension.TM2:
ProcessTextures(file, outputDir); ProcessTextures(file);
break; break;
case FileExtension.MDS: case FileExtension.MDS:
ProcessMDSFile(file, outputDir); ProcessMDSFile(file);
break; break;
case FileExtension.MOT: case FileExtension.MOT:
ProcessMOTFile(file, outputDir); ProcessMOTFile(file);
break; break;
default: default:
ProcessRawFile(file, outputDir); ProcessRawFile(file);
break; break;
} }
} }
if (convert) if (Convert)
{ {
foreach (var converter in mdsConverters) foreach (var converter in Context.Current.MdsConverters)
{ {
converter.Save(outputDir, textFormat); converter.Save(ctx.OutputPath, TextFormat);
} }
} }
foreach (var err in Context.Current.Errors)
{
Log.Line($"{err.Name}, {err.Text}", LogLevel.Error);
if (err.Exception != null)
{
Log.Error(err.Exception);
}
}
foreach (var err in Context.Current.Messages)
{
Log.Line($"{err.Name}, {err.Text}");
}
} }
private void PrintTask(IncludedFile file) private void PrintTask(IncludedFile file)
{ {
var spaces = 30 - file.name.Length; var spaces = 30 - file.Name.Length;
if (spaces <= 0) if (spaces <= 0)
{ {
spaces = 1; spaces = 1;
} }
var spacer = new string('.', spaces); var spacer = new string('.', spaces);
Console.WriteLine($" Process: {file.name + spacer} {file.data.Length} bytes"); Log.Line($" Process: {file.Name + spacer} {file.Data.Length} bytes");
}
public void Clean()
{
infoCfg = null;
textures = new();
mdsConverters = new();
} }
private void EnsurePath(string path) private void EnsurePath(string path)
@@ -98,104 +104,130 @@ public class ChrFile
} }
} }
private void ProcessMOTFile(IncludedFile file, string outputDir) private void ProcessMOTFile(IncludedFile file)
{ {
if (extract) if (Extract)
{ {
ProcessRawFile(file, outputDir); ProcessRawFile(file);
} }
if (convert) if (Convert)
{ {
foreach (var converter in mdsConverters) foreach (var converter in Context.Current.MdsConverters)
{ {
var motImporter = new Importer(); var motImporter = new Importer();
var animation = motImporter.Import(file.data); var animation = motImporter.Import(file.Data);
converter.CreateAnimation(animation, infoCfg); converter.AddAnimation(animation, Context.Current.InfoCfg);
} }
} }
} }
private void ProcessMDSFile(IncludedFile file, string outputDir) private void ProcessMDSFile(IncludedFile file)
{ {
var mdsScene = Reader.Read(file.data, file.name); if (Extract)
if (convert)
{ {
var converter = new MDSConverter(file.name); ProcessRawFile(file);
var rootName = Path.GetFileNameWithoutExtension(file.name);
converter.Convert(mdsScene, textures, rootName);
mdsConverters.Add(converter);
} }
if (extract) var mdsScene = Reader.Read(file.Data, file.Name);
if (mdsScene == null)
{ {
ProcessRawFile(file, outputDir); Context.Current.Errors.Add(new Error(file.Name, "mdsScene is empty!"));
return;
}
if (Convert)
{
var converter = new MDSConverter(file.Name);
var rootName = Path.GetFileNameWithoutExtension(file.Name);
converter.Convert(mdsScene, Context.Current.Textures, rootName);
Context.Current.MdsConverters.Add(converter);
} }
} }
private void ProcessConfig(IncludedFile file, string outputDir) private void ProcessConfig(IncludedFile file)
{ {
if (extract) if (Extract)
{ {
var isSecond = infoCfg != null; var isSecond = Context.Current.InfoCfg != null;
if (isSecond) if (isSecond)
{ {
file.name = file.name.Replace(".cfg", "_2.cfg"); file.Name = file.Name.Replace(".cfg", "_2.cfg");
} }
ProcessTextFile(file, outputDir); ProcessTextFile(file);
} }
var data = Encoding.GetEncoding("shift_jis").GetString(file.data).TrimEnd('\0'); var data = Encoding.GetEncoding("shift_jis").GetString(file.Data).TrimEnd('\0');
var configFile = new ConfigFile(data); var configFile = new ConfigFile(data);
var info = configFile.ReadConfig(); var info = configFile.ReadConfig();
if (infoCfg == null) if (Context.Current.InfoCfg == null)
{ {
infoCfg = info; Context.Current.InfoCfg = info;
} }
} }
private void ProcessTextFile(IncludedFile file, string outputDir) private void ProcessTextFile(IncludedFile file)
{ {
var root = Path.GetDirectoryName(file.name); try
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); var root = Path.GetDirectoryName(file.Name);
File.WriteAllText(Path.Combine(outputPath, fileName), text); var fileName = Path.GetFileName(file.Name);
var outputPath = Path.Combine(Context.Current.OutputPath, root);
var text = Encoding.GetEncoding("shift_jis").GetString(file.Data).TrimEnd('\0');
if (string.IsNullOrEmpty(text))
{
Context.Current.Errors.Add(new Error(file.Name, "Text data is empty!"));
return;
}
if (Extract)
{
EnsurePath(outputPath);
File.WriteAllText(Path.Combine(outputPath, fileName), text);
}
}
catch (Exception e)
{
Context.Current.Errors.Add(new Error(file.Name, "Error while process text file", e));
} }
} }
private void ProcessRawFile(IncludedFile file, string outputDir) private void ProcessRawFile(IncludedFile file)
{ {
var root = Path.GetDirectoryName(file.name); try
var fileName = Path.GetFileName(file.name);
var outputPath = Path.Combine(outputDir, root);
if (extract)
{ {
EnsurePath(outputPath); var root = Path.GetDirectoryName(file.Name);
File.WriteAllBytes(Path.Combine(outputPath, fileName), file.data); var fileName = Path.GetFileName(file.Name);
var outputPath = Path.Combine(Context.Current.OutputPath, root);
if (Extract)
{
EnsurePath(outputPath);
File.WriteAllBytes(Path.Combine(outputPath, fileName), file.Data);
}
}
catch (Exception e)
{
Context.Current.Errors.Add(new Error(file.Name, $"File extraction failed", e));
} }
} }
private void ProcessTextures(IncludedFile file, string outputDir) private void ProcessTextures(IncludedFile file)
{ {
var root = Path.GetDirectoryName(file.name); var root = Path.GetDirectoryName(file.Name);
var fileName = Path.GetFileNameWithoutExtension(file.name); var fileName = Path.GetFileNameWithoutExtension(file.Name);
var outputPath = Path.Combine(outputDir, root); var outputPath = Path.Combine(Context.Current.OutputPath, root);
var image = TM2Format.TM2Format.GetImage(file.data, fileName); var image = TM2Format.TM2Format.GetImage(file, fileName);
if (extract) if (Extract && image != null && image.Data != null)
{ {
EnsurePath(outputPath); EnsurePath(outputPath);
image.data.SaveAsPng(Path.Combine(outputPath, fileName + ".png")); image.Data.SaveAsPng(Path.Combine(outputPath, fileName + ".png"));
} }
textures.Add(image); Context.Current.Textures.Add(image);
} }
} }
@@ -6,7 +6,7 @@ using System.Text;
namespace dq8chr2glb.Container; namespace dq8chr2glb.Container;
public class ChrContainer public class ChrFile
{ {
public static List<IncludedFile> FromBytes(byte[] chrFile) public static List<IncludedFile> FromBytes(byte[] chrFile)
{ {
@@ -52,15 +52,15 @@ public class ChrContainer
else else
{ {
var file = new IncludedFile(); var file = new IncludedFile();
file.name = name; file.Name = name;
file.data = data; file.Data = data;
subContainers.Add(file); subContainers.Add(file);
} }
foreach (var file in subContainers) foreach (var file in subContainers)
{ {
var extension = file.name.ToLower().Split(".")[^1]; var extension = file.Name.ToLower().Split(".")[^1];
file.extension = extension switch file.Extension = extension switch
{ {
"mds" => FileExtension.MDS, "mds" => FileExtension.MDS,
"mot" => FileExtension.MOT, "mot" => FileExtension.MOT,
@@ -70,9 +70,9 @@ public class ChrContainer
_ => FileExtension.TEXT _ => FileExtension.TEXT
}; };
if (file.name == "info.cfg") if (file.Name == "info.cfg")
{ {
file.extension = FileExtension.CFG; file.Extension = FileExtension.CFG;
} }
} }
@@ -109,13 +109,13 @@ public class ChrContainer
name = Path.Combine(rootFolderName, name); name = Path.Combine(rootFolderName, name);
file.extension = isTexture ? FileExtension.TM2 : FileExtension.TEXT; file.Extension = isTexture ? FileExtension.TM2 : FileExtension.TEXT;
file.name = name + (isTexture ? ".tm2" : ".cfg"); file.Name = name + (isTexture ? ".tm2" : ".cfg");
var fileData = new byte[imgFile.FileSize]; var fileData = new byte[imgFile.FileSize];
Array.Copy(imgData, imgFile.DataOffset, fileData, 0, imgFile.FileSize); Array.Copy(imgData, imgFile.DataOffset, fileData, 0, imgFile.FileSize);
file.data = fileData; file.Data = fileData;
files.Add(file); files.Add(file);
+3 -3
View File
@@ -4,7 +4,7 @@ namespace dq8chr2glb.Container;
public class IncludedFile public class IncludedFile
{ {
public string name; public string Name;
public FileExtension extension; public FileExtension Extension;
public byte[] data = Array.Empty<byte>(); public byte[] Data = Array.Empty<byte>();
} }
+1 -1
View File
@@ -14,7 +14,7 @@ public static class Utils
foreach (var file in files) foreach (var file in files)
{ {
switch (file.extension) switch (file.Extension)
{ {
case FileExtension.TEXT: case FileExtension.TEXT:
configs.Add(file); configs.Add(file);
+51
View File
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using dq8chr2glb.Converter.GLTF;
using dq8chr2glb.Core.InfoCfg;
namespace dq8chr2glb.Converter;
public class Message
{
public readonly string Name;
public readonly string Text;
public Message(string name, string text)
{
Name = name;
Text = text;
}
}
public class Error
{
public readonly string Name;
public readonly string Text;
public readonly Exception? Exception;
public Error(string name, string text, Exception? exception = null)
{
Name = name;
Text = text;
Exception = exception;
}
}
public class Context
{
public static Context Current;
public string ModelName;
public string InputPath;
public string OutputPath;
public ModelConfig InfoCfg;
public List<TM2Format.Texture> Textures = new();
public List<MDSConverter> MdsConverters = new();
public List<Error> Errors = new();
public List<Message> Messages = new();
public Context()
{
Current = this;
}
}
+390
View File
@@ -0,0 +1,390 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.Json.Nodes;
using dq8chr2glb.Core.InfoCfg;
using dq8chr2glb.Core.MDSFormat;
using dq8chr2glb.Core.MOTFormat;
using dq8chr2glb.Logger;
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.GLTF;
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();
private const float FRAMERATE = 16f;
public MDSConverter(string name)
{
_name = name;
_gltfModel = ModelRoot.CreateModel();
}
public MDSConverter(string name, ModelRoot model)
{
_name = name;
_gltfModel = model;
foreach (var scene in _gltfModel.LogicalScenes)
{
foreach (var node in scene.VisualChildren)
{
ProcessIndicesRecursive(node);
}
}
foreach (var item in _nodeMap)
{
Log.Line(item.Value.Name);
}
}
private void ProcessIndicesRecursive(Node? node)
{
if (node == null)
{
return;
}
_nodeMap.Add(node.Extras["mds_index"]!.GetValue<int>(), node);
foreach (var child in node.VisualChildren)
{
ProcessIndicesRecursive(child);
}
}
public void Convert(MDSScene scene, List<Texture> images, string name)
{
var gltfScene = _gltfModel.UseScene("DefaultScene");
var nodesWithParents = new HashSet<int>();
if (scene.Nodes == null || scene.Nodes.Length == 0)
{
Log.Line($"There is nothing to convert in {name}", LogLevel.Info);
return;
}
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);
gltfNode.Extras = new JsonObject();
gltfNode.Extras["mds_index"] = JsonValue.Create(node.Index);
_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);
gltfNode.Extras = new JsonObject();
gltfNode.Extras["mds_index"] = JsonValue.Create(node.Index);
_nodeMap[node.Index] = gltfNode;
if (node is MDSBone)
{
_boneNodes.Add(gltfNode);
}
gltfNode.WithLocalTransform(ToNumericsMatrix4x4(node.Transform));
}
}
var nodesMapping = new Dictionary<int, int>();
for (int i = 0; i < _boneNodes.Count; i++)
{
var boneNode = _boneNodes[i];
var mdsNode = Array.Find(scene.Nodes, n => n.Name == boneNode.Name);
if (mdsNode != null)
{
nodesMapping[mdsNode.Index] = i;
}
}
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, nodesMapping);
if (_nodeMap.TryGetValue(mdsMesh.Index, out var meshNode))
{
meshNode.WithMesh(mesh);
}
}
if (_boneNodes.Count > 0)
{
SetupSkeleton(scene.Nodes, name);
}
}
public void AddAnimation(List<MotionCurve> motionCurves, ModelConfig config)
{
foreach (var clip in config.clips)
{
var rotationCurves = new List<(Node node, Dictionary<float, Quaternion> items)>();
var translationCurves = new List<(Node node, Dictionary<float, Vector3> items)>();
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++)
{
if (i >= curve.Keyframes.Count || curve.Keyframes[i] == null)
{
continue;
}
var keyframe = curve.Keyframes[i];
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)
{
rotationCurves.Add((node, rotationKeyframes));
}
if (translationKeyframes.Count > 0)
{
translationCurves.Add((node, translationKeyframes));
}
}
}
if (rotationCurves.Count > 0 || translationCurves.Count > 0)
{
var animation = _gltfModel.CreateAnimation(clip.Name);
foreach (var (node, items) in rotationCurves)
{
if (items.Count > 0 && node != null)
{
animation.CreateRotationChannel(node, items);
}
}
foreach (var (node, items) in translationCurves)
{
if (items.Count > 0 && node != null)
{
animation.CreateTranslationChannel(node, items);
}
}
}
}
}
private void CreateMaterials(MDSMaterial[] materials, List<TM2Format.Texture> textures)
{
foreach (var tex in textures)
{
if (tex == null)
{
continue;
}
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, Dictionary<int, int> nodesMapping)
{
return UniversalMeshBuilder.CreateMesh(_gltfModel, mdsMesh, materials, _materialCache, nodesMapping);
}
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 (_boneNodes.Count == 0)
{
Log.Line("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))
{
bindMatrix = Matrix4x4.Multiply(bindposeInverse, Matrix4x4.Identity);
}
else
{
bindMatrix = Matrix4x4.Identity;
}
jointBindings[i] = (boneNode, bindMatrix);
}
}
try
{
skin.BindJoints(jointBindings);
}
catch (Exception e)
{
Log.Error(e);
return;
}
foreach (var node in _gltfModel.LogicalNodes)
{
if (node.Mesh != null)
{
var hasWeights = node.Mesh.Primitives.All(i => i.VertexAccessors.ContainsKey("WEIGHTS_0"));
if (hasWeights)
{
MeshUtils.ApplyTransform(node);
try
{
node.Skin = skin;
}
catch (Exception e)
{
Context.Current.Errors.Add(new Error(name, "Can't set Skin", e));
}
}
}
}
}
private string DebugMatrix(Matrix4x4 mat)
{
return $"{mat.M11:F}, {mat.M12:F}, {mat.M13:F}, {mat.M14:F}\n" +
$"{mat.M21:F}, {mat.M22:F}, {mat.M23:F}, {mat.M24:F}\n" +
$"{mat.M31:F}, {mat.M32:F}, {mat.M33:F}, {mat.M34:F}\n" +
$"{mat.M41:F}, {mat.M42:F}, {mat.M43:F}, {mat.M44:F}\n";
}
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 = false)
{
var name = Path.GetFileNameWithoutExtension(_name);
var path = Path.Combine(filePath, name + (textFormat ? ".gltf" : ".glb"));
Log.Line(path);
try
{
if (textFormat)
{
_gltfModel.SaveGLTF(path);
}
else
{
_gltfModel.SaveGLB(path);
}
}
catch (Exception e)
{
Log.Error(e);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using SharpGLTF.Schema2;
using System.Runtime.InteropServices;
namespace dq8chr2glb.Converter.GLTF;
public static class MeshUtils
{
public 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]);
}
}
public static void ApplyTransform(Node node)
{
Matrix4x4.Invert(node.LocalMatrix, out var invTransform);
var normalMatrix = Matrix4x4.Transpose(invTransform);
foreach (var primitive in node.Mesh.Primitives)
{
var positionAccessor = primitive.GetVertexAccessor("POSITION");
var positions = positionAccessor.AsVector3Array();
var newPositions = new Vector3[positions.Count];
for (var i = 0; i < positions.Count; i++)
{
newPositions[i] = Vector3.Transform(positions[i], node.LocalMatrix);
}
var count = positionAccessor.Count;
var format = positionAccessor.Format;
var positionBytes = MemoryMarshal.AsBytes(newPositions.AsSpan()).ToArray();
var newPositionBufferView = primitive.LogicalParent.LogicalParent.UseBufferView(
positionBytes
);
var newAccessor = primitive.LogicalParent.LogicalParent.CreateAccessor();
newAccessor.SetVertexData(
newPositionBufferView,
0,
count,
format
);
primitive.SetVertexAccessor("POSITION", newAccessor);
var normalAccessor = primitive.GetVertexAccessor("NORMAL");
if (normalAccessor != null)
{
var normals = normalAccessor.AsVector3Array();
var newNormals = new Vector3[normals.Count];
for (var i = 0; i < normals.Count; i++)
{
var transformedNormal = Vector3.TransformNormal(normals[i], normalMatrix);
newNormals[i] = Vector3.Normalize(transformedNormal);
}
var normalBytes = MemoryMarshal.AsBytes(newNormals.AsSpan()).ToArray();
var newNormalBufferView = primitive.LogicalParent.LogicalParent.UseBufferView(
normalBytes
);
var newNormalAccessor = primitive.LogicalParent.LogicalParent.CreateAccessor();
newNormalAccessor.SetVertexData(
newNormalBufferView,
0,
normalAccessor.Count,
normalAccessor.Format
);
primitive.SetVertexAccessor("NORMAL", newNormalAccessor);
}
}
node.LocalMatrix = Matrix4x4.Identity;
}
}
@@ -2,32 +2,33 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using dq8chr2glb.Core.MDSFormat; using dq8chr2glb.Core.MDSFormat;
using dq8chr2glb.Logger;
using SharpGLTF.Geometry; using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Materials; using SharpGLTF.Materials;
using SharpGLTF.Schema2; using SharpGLTF.Schema2;
namespace dq8chr2glb.Converter namespace dq8chr2glb.Converter.GLTF
{ {
public static class UniversalMeshBuilder public static class UniversalMeshBuilder
{ {
public static Mesh CreateMesh(ModelRoot _root, MDSMesh mdsMesh, MDSMaterial[] materials, public static Mesh CreateMesh(ModelRoot _root, MDSMesh mdsMesh, MDSMaterial[] materials,
Dictionary<string, MaterialBuilder> materialCache) Dictionary<string, MaterialBuilder> materialCache, Dictionary<int, int> nodesMap)
{ {
var (hasUvs, hasSkinning) = AnalyzeMeshAttributes(mdsMesh); var hasUvs = (mdsMesh.Features & MeshFeatures.UVs) != 0;
var hasSkinning = (mdsMesh.Features & MeshFeatures.Weights) != 0;
// Выбираем подходящий тип вершины на основе атрибутов
if (hasSkinning) if (hasSkinning)
{ {
if (hasUvs) if (hasUvs)
{ {
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexJoints4>( return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexJoints4>(
_root, mdsMesh, materials, materialCache); _root, mdsMesh, materials, materialCache, nodesMap);
} }
else else
{ {
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexJoints4>( return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexJoints4>(
_root, mdsMesh, materials, materialCache); _root, mdsMesh, materials, materialCache, nodesMap);
} }
} }
else else
@@ -35,23 +36,22 @@ namespace dq8chr2glb.Converter
if (hasUvs) if (hasUvs)
{ {
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexEmpty>( return CreateMeshWithAllAttributes<VertexPositionNormal, VertexTexture1, VertexEmpty>(
_root, mdsMesh, materials, materialCache); _root, mdsMesh, materials, materialCache, nodesMap);
} }
else else
{ {
return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexEmpty>( return CreateMeshWithAllAttributes<VertexPositionNormal, VertexEmpty, VertexEmpty>(
_root, mdsMesh, materials, materialCache); _root, mdsMesh, materials, materialCache, nodesMap);
} }
} }
} }
private static (bool hasUvs, bool hasSkinning) AnalyzeMeshAttributes(MDSMesh mdsMesh) private static (bool hasUvs, bool hasSkinning) AnalyzeMeshAttributes(MDSMesh mdsMesh)
{ {
var hasUvs = mdsMesh.uv != null && mdsMesh.uv.Length > 0 && var hasUvs = mdsMesh.Uv != null && mdsMesh.Uv.Length > 0 &&
Array.Exists(mdsMesh.uv, uv => uv != null && uv.Length >= 2); Array.Exists(mdsMesh.Uv, uv => uv != null && uv.Length >= 2);
var hasSkinning = mdsMesh.weights != null && mdsMesh.bones != null && var hasSkinning = mdsMesh.Weights != null && mdsMesh.Bones != null &&
mdsMesh.bones.Length > 0 && mdsMesh.Bones.Length > 0;
Array.Exists(mdsMesh.bones, b => b != null);
return (hasUvs, hasSkinning); return (hasUvs, hasSkinning);
} }
@@ -60,47 +60,71 @@ namespace dq8chr2glb.Converter
ModelRoot root, ModelRoot root,
MDSMesh mdsMesh, MDSMesh mdsMesh,
MDSMaterial[] materials, MDSMaterial[] materials,
Dictionary<string, MaterialBuilder> materialCache) Dictionary<string, MaterialBuilder> materialCache,
Dictionary<int, int> nodesMap)
where TvG : struct, IVertexGeometry where TvG : struct, IVertexGeometry
where TvM : struct, IVertexMaterial where TvM : struct, IVertexMaterial
where TvS : struct, IVertexSkinning where TvS : struct, IVertexSkinning
{ {
var meshBuilder = new MeshBuilder<MaterialBuilder, TvG, TvM, TvS>(mdsMesh.name); var meshBuilder = new MeshBuilder<MaterialBuilder, TvG, TvM, TvS>(mdsMesh.Name);
var positions = new List<Vector3>(); var positions = new List<Vector3>();
var normals = new List<Vector3>(); var normals = new List<Vector3>();
var texCoords = new List<Vector2>(); var texCoords = new List<Vector2>();
var skinningData = new List<(int, float)[]>(); var skinningData = new List<(int, float)[]>();
var hasUvs = mdsMesh.uv != null && mdsMesh.uv.Length > 0; var hasUvs = (mdsMesh.Features & MeshFeatures.UVs) != 0;
var hasSkinning = mdsMesh.weights != null && mdsMesh.bones != null; var hasSkinning = (mdsMesh.Features & MeshFeatures.Weights) != 0;
for (int i = 0; i < mdsMesh.vertices.Length; i++) for (var i = 0; i < mdsMesh.Vertices.Length; i++)
{ {
positions.Add(new Vector3( positions.Add(new Vector3(
mdsMesh.vertices[i][0], mdsMesh.Vertices[i][0],
mdsMesh.vertices[i][1], mdsMesh.Vertices[i][1],
mdsMesh.vertices[i][2])); mdsMesh.Vertices[i][2]));
normals.Add(Vector3.Zero); normals.Add(Vector3.Zero);
if (hasUvs && i < mdsMesh.uv.Length && mdsMesh.uv[i] != null && mdsMesh.uv[i].Length >= 2) 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])); texCoords.Add(new Vector2(mdsMesh.Uv[i][0], mdsMesh.Uv[i][1]));
} }
else else
{ {
texCoords.Add(Vector2.Zero); texCoords.Add(Vector2.Zero);
} }
if (hasSkinning && i < mdsMesh.bones.Length && mdsMesh.bones[i] != null) if (hasSkinning && i < mdsMesh.Bones.Length)
{ {
var boneIndices = mdsMesh.bones[i]; var boneIndices = mdsMesh.Bones[i];
var boneWeights = mdsMesh.weights[i]; var boneWeights = mdsMesh.Weights[i];
var bindings = new (int JointIndex, float Weight)[Math.Min(8, boneIndices.Length)]; var bindings = new (int JointIndex, float Weight)[Math.Min(4, boneIndices.Length)];
var wSum = 0f;
for (var b = 0; b < bindings.Length; b++) for (var b = 0; b < bindings.Length; b++)
{ {
bindings[b] = (boneIndices[b], boneWeights[b]); var mdsBoneIndex = boneIndices[b];
if (nodesMap.ContainsKey(mdsBoneIndex))
{
var gltfBoneIndex = nodesMap[mdsBoneIndex];
bindings[b] = (Math.Max(gltfBoneIndex, 0), boneWeights[b]);
wSum += boneWeights[b];
}
else
{
bindings[b] = (0, boneWeights[b]);
}
}
if (wSum < 0.00001f)
{
skinningData.Add(Array.Empty<(int, float)>());
continue;
}
for (var index = 0; index < bindings.Length; index++)
{
bindings[index].Weight /= wSum;
} }
skinningData.Add(bindings); skinningData.Add(bindings);
@@ -111,14 +135,14 @@ namespace dq8chr2glb.Converter
} }
} }
ComputeNormals(positions, mdsMesh.triangles, normals); MeshUtils.ComputeNormals(positions, mdsMesh.Triangles, normals);
foreach (var submesh in mdsMesh.submeshes) foreach (var submesh in mdsMesh.Submeshes)
{ {
var materialBuilder = materialCache.TryGetValue( var materialBuilder = materialCache.TryGetValue(
submesh.materialIndex >= 0 && submesh.MaterialIndex >= 0 &&
submesh.materialIndex < materials.Length submesh.MaterialIndex < materials.Length
? materials[submesh.materialIndex].name ? materials[submesh.MaterialIndex].Name
: "default", : "default",
out var mat) out var mat)
? mat ? mat
@@ -127,11 +151,20 @@ namespace dq8chr2glb.Converter
var primitive = meshBuilder.UsePrimitive(materialBuilder); var primitive = meshBuilder.UsePrimitive(materialBuilder);
var indices = new List<int>(); var indices = new List<int>();
for (var i = submesh.startIndex; i < submesh.startIndex + submesh.indexCount; i += 3) for (var i = submesh.StartIndex; i < submesh.StartIndex + submesh.IndexCount; i += 3)
{ {
indices.Add(mdsMesh.triangles[i]); indices.Add(mdsMesh.Triangles[i]);
indices.Add(mdsMesh.triangles[i + 1]); indices.Add(mdsMesh.Triangles[i + 1]);
indices.Add(mdsMesh.triangles[i + 2]); indices.Add(mdsMesh.Triangles[i + 2]);
}
var maxBoneIndex = 0;
foreach (var bones in mdsMesh.Bones)
{
foreach (var boneID in bones)
{
maxBoneIndex = Math.Max(boneID, maxBoneIndex);
}
} }
for (var i = 0; i < indices.Count; i += 3) for (var i = 0; i < indices.Count; i += 3)
@@ -144,25 +177,34 @@ namespace dq8chr2glb.Converter
positions[idxA], positions[idxA],
normals[idxA], normals[idxA],
texCoords.Count > idxA ? texCoords[idxA] : Vector2.Zero, texCoords.Count > idxA ? texCoords[idxA] : Vector2.Zero,
skinningData.Count > idxA ? skinningData[idxA] : null); skinningData.Count != 0 ? skinningData[idxA] : null);
var vertexB = CreateVertex<TvG, TvM, TvS>( var vertexB = CreateVertex<TvG, TvM, TvS>(
positions[idxB], positions[idxB],
normals[idxB], normals[idxB],
texCoords.Count > idxB ? texCoords[idxB] : Vector2.Zero, texCoords.Count > idxB ? texCoords[idxB] : Vector2.Zero,
skinningData.Count > idxB ? skinningData[idxB] : null); skinningData.Count != 0 ? skinningData[idxB] : null);
var vertexC = CreateVertex<TvG, TvM, TvS>( var vertexC = CreateVertex<TvG, TvM, TvS>(
positions[idxC], positions[idxC],
normals[idxC], normals[idxC],
texCoords.Count > idxC ? texCoords[idxC] : Vector2.Zero, texCoords.Count > idxC ? texCoords[idxC] : Vector2.Zero,
skinningData.Count > idxC ? skinningData[idxC] : null); skinningData.Count != 0 ? skinningData[idxC] : null);
primitive.AddTriangle(vertexA, vertexB, vertexC); primitive.AddTriangle(vertexA, vertexB, vertexC);
} }
} }
return root.CreateMesh(meshBuilder); try
{
return root.CreateMesh(meshBuilder);
}
catch (Exception e)
{
Log.Line($"Create Mesh Error: {mdsMesh.Name}", LogLevel.Error);
Log.Error(e);
return null;
}
} }
private static VertexBuilder<TvG, TvM, TvS> CreateVertex<TvG, TvM, TvS>( private static VertexBuilder<TvG, TvM, TvS> CreateVertex<TvG, TvM, TvS>(
@@ -185,8 +227,9 @@ namespace dq8chr2glb.Converter
{ {
material.SetTexCoord(0, texCoord); material.SetTexCoord(0, texCoord);
} }
catch catch (Exception e)
{ {
Log.Error(e);
} }
} }
@@ -197,46 +240,13 @@ namespace dq8chr2glb.Converter
{ {
skinning.SetBindings(skinningData); skinning.SetBindings(skinningData);
} }
catch catch (Exception e)
{ {
Log.Error(e);
} }
} }
return new VertexBuilder<TvG, TvM, TvS>(geometry, material, skinning); 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]);
}
}
} }
} }
-284
View File
@@ -1,284 +0,0 @@
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);
}
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace dq8chr2glb.Converter;
public enum OutputFormat
{
GLB,
GLTF,
}
+4 -4
View File
@@ -2,8 +2,8 @@ namespace dq8chr2glb.Core.InfoCfg;
public class Clip public class Clip
{ {
public string name; public string Name;
public int startFrame; public int StartFrame;
public int endFrame; public int EndFrame;
public float speed; public float Speed;
} }
+30 -19
View File
@@ -6,43 +6,54 @@ namespace dq8chr2glb.Core.InfoCfg;
public class ConfigFile public class ConfigFile
{ {
public string text; public string Text;
public ModelConfig Config;
public ConfigFile(string text) public ConfigFile(string text)
{ {
this.text = text; Text = text;
} }
public ModelConfig ReadConfig() public ModelConfig ReadConfig()
{ {
var config = new ModelConfig(); Config = new ModelConfig();
config.model = GetModelFileName(); GetModelFileName();
config.clips = GetClips(); GetClips();
return config; return Config;
} }
public string GetModelFileName() public void GetModelFileName()
{ {
var lines = text.Split('\n'); var lines = Text.Split('\n');
foreach (var line in lines) foreach (var line in lines)
{ {
if (line.StartsWith("MODEL")) if (line.StartsWith("MODEL"))
{ {
var match = Regex.Match(line, @"MODEL\s+""([^""]+)"""); var match = Regex.Match(line, "\"([^\"]+)\"");
return match.Groups[1].Value; if (match.Success)
{
Config.model = match.Groups[1].Value;
}
}
if (line.StartsWith("MOTION"))
{
var match = Regex.Match(line, "\"([^\"]+)\"");
if (match.Success)
{
Config.motion = match.Groups[1].Value;
}
} }
} }
return null;
} }
public List<Clip> GetClips() public void GetClips()
{ {
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var clips = new List<Clip>(); var clips = new List<Clip>();
var lines = text.Split('\n'); var lines = Text.Split('\n');
var insideKeyBlock = false; var insideKeyBlock = false;
foreach (var line in lines) foreach (var line in lines)
@@ -70,13 +81,13 @@ public class ConfigFile
clips.Add(new Clip clips.Add(new Clip
{ {
name = parts[0][1..^1], Name = parts[0][1..^1],
startFrame = int.Parse(parts[1]), StartFrame = int.Parse(parts[1]),
endFrame = int.Parse(parts[2]), EndFrame = int.Parse(parts[2]),
speed = float.Parse(parts[3]) Speed = float.Parse(parts[3])
}); });
} }
return clips; Config.clips = clips;
} }
} }
+1
View File
@@ -6,6 +6,7 @@ public class ModelConfig
{ {
public string name; public string name;
public string model; public string model;
public string motion;
public string shadow; public string shadow;
public List<Clip> clips; public List<Clip> clips;
} }
+55 -100
View File
@@ -6,130 +6,85 @@ namespace dq8chr2glb.Core.MDSFormat;
public struct MDSHeader public struct MDSHeader
{ {
[FieldOffset(00)] public int MagicCode; [FieldOffset(00)] public int MagicCode;
[FieldOffset(04)] public int version; [FieldOffset(04)] public int Version;
[FieldOffset(08)] public int headerSize; [FieldOffset(08)] public int HeaderSize;
[FieldOffset(12)] public int nodesCount; [FieldOffset(12)] public int NodesCount;
[FieldOffset(20)] public int nodeSize; [FieldOffset(20)] public int NodeSize;
[FieldOffset(24)] public int offsetToNodes; [FieldOffset(24)] public int OffsetToNodes;
[FieldOffset(32)] public int meshCount; [FieldOffset(32)] public int MeshCount;
[FieldOffset(36)] public int offsetToMDT; [FieldOffset(36)] public int OffsetToMDT;
[FieldOffset(44)] public int materialsCount; [FieldOffset(44)] public int MaterialsCount;
[FieldOffset(48)] public int materialSize; [FieldOffset(48)] public int MaterialSize;
[FieldOffset(52)] public int offsetToMaterials; [FieldOffset(52)] public int OffsetToMaterials;
[FieldOffset(56)] public int offsetToNames; [FieldOffset(56)] public int OffsetToNames;
[FieldOffset(60)] public int namesBlockSize; [FieldOffset(60)] public int NamesBlockSize;
} }
[StructLayout(LayoutKind.Explicit, Size = 256)] [StructLayout(LayoutKind.Explicit, Size = 256)]
public struct MDTHeader public struct MDTHeader
{ {
[FieldOffset(00)] public int MagicCode; [FieldOffset(00)] public int MagicCode;
[FieldOffset(08)] public int toVertexCount; [FieldOffset(08)] public int ToVertexCount;
[FieldOffset(12)] public int totalSize; [FieldOffset(12)] public int TotalSize;
[FieldOffset(32)] public float ScaleX;
[FieldOffset(16)] public int value1; // always 2064? [FieldOffset(36)] public float ScaleY;
[FieldOffset(20)] public int value2; // always 4104? [FieldOffset(40)] public float ScaleZ;
[FieldOffset(24)] public int value3; // always 4112? [FieldOffset(80)] public float UvScaleX;
[FieldOffset(84)] public float UvScaleY;
[FieldOffset(32)] public float scaleFactorX; [FieldOffset(112)] public int TriangleGroupCount;
[FieldOffset(36)] public float scaleFactorY; [FieldOffset(116)] public int VertexAttrSize;
[FieldOffset(144)] public int VertsCount;
[FieldOffset(40)] public float scaleFactorZ; [FieldOffset(148)] public int BytesToFirstVertex;
[FieldOffset(152)] public int ColorsCount;
// [FieldOffset(64)] public Float3 someScale; // always 1.079? [FieldOffset(156)] public int BytesToFirstColor;
[FieldOffset(80)] public float uvScaleX; [FieldOffset(160)] public int UvCount;
[FieldOffset(84)] public float uvScaleY; [FieldOffset(164)] public int UvOffset;
[FieldOffset(176)] public int BonesPerVertex;
[FieldOffset(96)] public int value4; // always 144? [FieldOffset(180)] public int ToWeightsOffset;
[FieldOffset(184)] public int ToBoneIndicesOffset;
[FieldOffset(112)] public int triangleGroupCount; [FieldOffset(192)] public AABB Bounds;
[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)] [StructLayout(LayoutKind.Explicit, Size = 48)]
public struct AABB public struct AABB
{ {
[FieldOffset(00)] public float minX; [FieldOffset(00)] public float MinX;
[FieldOffset(04)] public float minY; [FieldOffset(04)] public float MinY;
[FieldOffset(08)] public float minZ; [FieldOffset(08)] public float MinZ;
[FieldOffset(12)] public int value1; // corner flag? [FieldOffset(16)] public float MaxX;
[FieldOffset(16)] public float maxX; [FieldOffset(20)] public float MaxY;
[FieldOffset(20)] public float maxY; [FieldOffset(24)] public float MaxZ;
[FieldOffset(24)] public float maxZ; [FieldOffset(32)] public float CenterX;
[FieldOffset(28)] public int value2; // corner flag? [FieldOffset(36)] public float CenterY;
[FieldOffset(32)] public float centerX; [FieldOffset(40)] public float CenterZ;
[FieldOffset(36)] public float centerY;
[FieldOffset(40)] public float centerZ;
[FieldOffset(44)] public float floatValue1; // ?
} }
[StructLayout(LayoutKind.Explicit, Size = 160)] [StructLayout(LayoutKind.Explicit, Size = 160)]
public struct Node public struct Node
{ {
[FieldOffset(00)] public int nameOffset; [FieldOffset(00)] public int NameOffset;
[FieldOffset(12)] public int meshIndex; [FieldOffset(12)] public int MeshIndex;
[FieldOffset(16)] public int parentIndex; [FieldOffset(16)] public int ParentIndex;
[FieldOffset(32)] public MDSMatrix matrix; [FieldOffset(32)] public MDSMatrix Matrix;
[FieldOffset(96)] public MDSMatrix bindpose; [FieldOffset(96)] public MDSMatrix Bindpose;
} }
[StructLayout(LayoutKind.Explicit, Size = 96)] [StructLayout(LayoutKind.Explicit, Size = 96)]
public struct Material public struct Material
{ {
[FieldOffset(00)] public float value1; [FieldOffset(48)] public int NameOffset;
[FieldOffset(04)] public float value2; [FieldOffset(52)] public int TextureNameOffset;
[FieldOffset(08)] public float value3; [FieldOffset(64)] public int NoName;
[FieldOffset(12)] public float value4; [FieldOffset(80)] public int MaterialType;
[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)] [StructLayout(LayoutKind.Explicit, Size = 32)]
public struct TriangleGroupHeader public struct TriangleGroupHeader
{ {
[FieldOffset(00)] public short toNext; [FieldOffset(00)] public short ToNext;
[FieldOffset(04)] public short readMode; [FieldOffset(04)] public short TriangleReadMode;
[FieldOffset(06)] public short IdsCount;
[FieldOffset(16)] public short HeaderSize;
[FieldOffset(06)] public short idsCount; public TriangleReadMode ReadMode => (TriangleReadMode)TriangleReadMode;
// [FieldOffset(06)]public short dataSize;
[FieldOffset(16)] public short headerSize;
public TriangleReadMode ReadMode => (TriangleReadMode)readMode;
} }
+87 -105
View File
@@ -2,19 +2,22 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using dq8chr2glb.Logger;
namespace dq8chr2glb.Core.MDSFormat; namespace dq8chr2glb.Core.MDSFormat;
public class Reader public class Reader
{ {
private const float GLOBAL_SCALE = 0.00003f; // Global Scale Constant
public static MDSScene Read(byte[] data, string filename) public static MDSScene Read(byte[] data, string filename)
{ {
var offset = 0; var offset = 0;
var header = Utils.ReadStruct<MDSHeader>(data, ref offset, true); var header = Utils.ReadStruct<MDSHeader>(data, ref offset, true);
if (header.version != 200) if (header.Version != 200)
{ {
Console.WriteLine($"Unknown version: {header.version}"); Log.Line($"Unknown version: {header.Version}.");
return null; return null;
} }
@@ -24,101 +27,74 @@ public class Reader
ReadNodes(scene, header, data, names); ReadNodes(scene, header, data, names);
ReadMaterials(scene, header, data, names); ReadMaterials(scene, header, data, names);
var mdtHeaders = new MDTHeader[header.meshCount]; var mdtHeaders = new MDTHeader[header.MeshCount];
for (var meshIndex = 0; meshIndex < header.meshCount; meshIndex++) for (var meshIndex = 0; meshIndex < header.MeshCount; meshIndex++)
{ {
offset = header.offsetToMDT + mdtHeaders.Sum(h => h.totalSize); offset = header.OffsetToMDT + mdtHeaders.Sum(h => h.TotalSize);
var mdtHeader = Utils.ReadStruct<MDTHeader>(data, ref offset); var mdtHeader = Utils.ReadStruct<MDTHeader>(data, ref offset);
mdtHeaders[meshIndex] = mdtHeader; mdtHeaders[meshIndex] = mdtHeader;
var mesh = Utils.GetMeshByIndex(scene, meshIndex); var mesh = Utils.GetMeshByIndex(scene, meshIndex);
var hasVertices = mdtHeader.vertsCount != 0; var hasVertices = mdtHeader.VertsCount != 0;
var hasColors = mdtHeader.colorsCount != 0; var hasColors = mdtHeader.ColorsCount != 0;
var hasUVs = mdtHeader.uvCount != 0; var hasUVs = mdtHeader.UvCount != 0;
var isSkinned = mdtHeader.bonesPerVertex != 0; var isSkinned = mdtHeader.BonesPerVertex != 0;
var features = Utils.GetFeaturesFlags(hasVertices, hasColors, hasUVs, isSkinned); var features = Utils.GetFeaturesFlags(hasVertices, hasColors, hasUVs, isSkinned);
mesh.features = features; mesh.Features = features;
mesh.bounds = mdtHeader.bounds; mesh.Bounds = mdtHeader.Bounds;
// Console.WriteLine($"Features {features}");
var gScale = 0.00003f;
var scale = new float[3] var scale = new float[3]
{ { mdtHeader.ScaleX * GLOBAL_SCALE, mdtHeader.ScaleY * GLOBAL_SCALE, mdtHeader.ScaleZ * GLOBAL_SCALE };
mdtHeader.scaleFactorX * gScale, mdtHeader.scaleFactorY * gScale, mdtHeader.scaleFactorZ * gScale var uvScale = new float[2] { mdtHeader.UvScaleX, mdtHeader.UvScaleY };
};
var uvScale = new float[2] { mdtHeader.uvScaleX, mdtHeader.uvScaleY };
// read vertices array // read vertices array
var vOffset = offset + mdtHeader.toVertexCount + mdtHeader.bytesToFirstVertex; var vOffset = offset + mdtHeader.ToVertexCount + mdtHeader.BytesToFirstVertex;
var vCount = mdtHeader.vertsCount; var vCount = mdtHeader.VertsCount;
var vertices = Utils.ReadArray<short[], float[]>(data, vOffset, vCount, 3, var vertices = Utils.ReadArray<short[], float[]>(data, vOffset, vCount, 3,
s => s => new[]
{ {
return new[] s[0] * scale[0], s[1] * scale[1], s[2] * scale[2]
{
s[0] * scale[0], s[1] * scale[1],
s[2] * scale[2]
};
}); });
// read colors array // read colors array
var colors = new float[mdtHeader.colorsCount][]; var colors = new float[mdtHeader.ColorsCount][];
if ((features & MeshFeatures.Colors) == MeshFeatures.Colors) if ((features & MeshFeatures.Colors) == MeshFeatures.Colors)
{ {
var cOffset = offset + mdtHeader.toVertexCount + mdtHeader.bytesToFirstColor; var cOffset = offset + mdtHeader.ToVertexCount + mdtHeader.BytesToFirstColor;
var cCount = mdtHeader.colorsCount; var cCount = mdtHeader.ColorsCount;
var maxByte = (float)byte.MaxValue;
colors = Utils.ReadArray<byte[], float[]>(data, cOffset, cCount, 3, colors = Utils.ReadArray<byte[], float[]>(data, cOffset, cCount, 3,
s => s => new[]
{ {
return new[] s[0] / maxByte, s[1] / maxByte, s[2] / maxByte
{ s[0] / 256f, s[1] / 256f, s[2] / 256f };
}); });
} }
// read texcoords array // read texcoords array
var tOffset = offset + mdtHeader.toVertexCount + mdtHeader.uvOffset; var tOffset = offset + mdtHeader.ToVertexCount + mdtHeader.UvOffset;
var tCount = mdtHeader.uvCount; var tCount = mdtHeader.UvCount;
var maxSByte = (float)sbyte.MaxValue;
var uvs = Utils.ReadArray<byte[], float[]>(data, tOffset, tCount, 2, var uvs = Utils.ReadArray<byte[], float[]>(data, tOffset, tCount, 2,
s => s => new[]
{ {
return new[] s[0] / maxSByte * uvScale[0], s[1] / maxSByte * uvScale[1]
{
s[0] / 127f * uvScale[0], s[1] / 127f * uvScale[1]
};
}); });
// read weights array // read weights array
var wOffset = offset + mdtHeader.toVertexCount + mdtHeader.toWeightsOffset; var wOffset = offset + mdtHeader.ToVertexCount + mdtHeader.ToWeightsOffset;
var weights = Utils.ReadArray<ushort[], float[]>(data, wOffset, vCount, 4, s => var weights = Utils.ReadArray<ushort[], float[]>(data, wOffset, vCount, 4,
{ s => Array.ConvertAll(s, x => (float)x / short.MaxValue));
var output = new float[4];
for (var i = 0; i < 4; i++)
{
output[i] = s[i] / 32768f;
}
return output;
});
// read bones per vertex array // read bones per vertex array
var bOffset = offset + mdtHeader.toVertexCount + mdtHeader.toBoneIndicesOffset; var bOffset = offset + mdtHeader.ToVertexCount + mdtHeader.ToBoneIndicesOffset;
var boneIndices = Utils.ReadArray<short[], int[]>(data, bOffset, vCount, 4, s => var boneIndices = Utils.ReadArray<short[], int[]>(data, bOffset, vCount, 4,
{ s => Array.ConvertAll(s, x => (int)x));
var output = new int[4];
for (var i = 0; i < 4; i++)
{
output[i] = s[i];
}
return output;
});
var submeshes = new List<SubMesh>(); var submeshes = new List<SubMesh>();
var triangleGroupOffset = offset + mdtHeader.vertexAttrSize; var triangleGroupOffset = offset + mdtHeader.VertexAttrSize;
var lastEndIndex = 0; var lastEndIndex = 0;
var verticesList = new List<float[]>(); var verticesList = new List<float[]>();
@@ -128,17 +104,25 @@ public class Reader
var weightsList = new List<float[]>(); var weightsList = new List<float[]>();
var bonesList = new List<int[]>(); var bonesList = new List<int[]>();
for (var submeshIndex = 0; submeshIndex < mdtHeader.triangleGroupCount; submeshIndex++) for (var submeshIndex = 0; submeshIndex < mdtHeader.TriangleGroupCount; submeshIndex++)
{ {
var triangleGroupHeader = Utils.ReadStruct<TriangleGroupHeader>(data, ref triangleGroupOffset); var triangleGroupHeader = Utils.ReadStruct<TriangleGroupHeader>(data, ref triangleGroupOffset);
if (triangleGroupHeader.headerSize == 32) if (triangleGroupHeader.HeaderSize == 32 && triangleGroupHeader.ToNext == 0)
{ {
// This is normal. Often, such blocks complete a sequence.
continue;
}
if (triangleGroupHeader.HeaderSize == 32 && triangleGroupHeader.ToNext != 0)
{
// This might be weird.
// Log.Line($"Skip 32-bit triangle group header on {triangleGroupOffset}");
continue; continue;
} }
var submesh = new SubMesh(); var submesh = new SubMesh();
var triangleIndexOffset = triangleGroupOffset + triangleGroupHeader.headerSize; var triangleIndexOffset = triangleGroupOffset + triangleGroupHeader.HeaderSize;
var triangles = ReadTriangles(triangleGroupHeader, triangleIndexOffset, data); var triangles = ReadTriangles(triangleGroupHeader, triangleIndexOffset, data);
var cornerVerts = GetCornerAttribute(vertices, triangles, triangleIndexOffset, "vertices"); var cornerVerts = GetCornerAttribute(vertices, triangles, triangleIndexOffset, "vertices");
verticesList.AddRange(cornerVerts); verticesList.AddRange(cornerVerts);
@@ -152,9 +136,9 @@ public class Reader
bonesList.AddRange(cornerBoneIndices); bonesList.AddRange(cornerBoneIndices);
} }
submesh.materialIndex = BitConverter.ToInt32(data, triangleGroupOffset + 32); submesh.MaterialIndex = BitConverter.ToInt32(data, triangleGroupOffset + 32);
submesh.startIndex = lastEndIndex; submesh.StartIndex = lastEndIndex;
submesh.indexCount = triangles.Length; submesh.IndexCount = triangles.Length;
lastEndIndex += cornerVerts.Length; lastEndIndex += cornerVerts.Length;
submeshes.Add(submesh); submeshes.Add(submesh);
@@ -184,16 +168,16 @@ public class Reader
} }
} }
triangleGroupOffset += triangleGroupHeader.toNext; triangleGroupOffset += triangleGroupHeader.ToNext;
} }
mesh.vertices = verticesList.ToArray(); mesh.Vertices = verticesList.ToArray();
mesh.triangles = trianglesList.ToArray(); mesh.Triangles = trianglesList.ToArray();
mesh.color = colorsList.ToArray(); mesh.Color = colorsList.ToArray();
mesh.uv = uvList.ToArray(); mesh.Uv = uvList.ToArray();
mesh.weights = weightsList.ToArray(); mesh.Weights = weightsList.ToArray();
mesh.bones = bonesList.ToArray(); mesh.Bones = bonesList.ToArray();
mesh.submeshes = submeshes.ToArray(); mesh.Submeshes = submeshes.ToArray();
} }
return scene; return scene;
@@ -201,27 +185,26 @@ public class Reader
private static string ReadNames(MDSHeader header, byte[] data) private static string ReadNames(MDSHeader header, byte[] data)
{ {
var offset = header.offsetToNames; var offset = header.OffsetToNames;
var decodedString = Encoding.UTF8.GetString(data, offset, header.namesBlockSize); var decodedString = Encoding.UTF8.GetString(data, offset, header.NamesBlockSize);
return decodedString.Replace("\0", " "); return decodedString.Replace("\0", " ");
} }
private static void ReadNodes(MDSScene scene, MDSHeader header, byte[] data, string names) private static void ReadNodes(MDSScene scene, MDSHeader header, byte[] data, string names)
{ {
scene.nodes = new MDSNode[header.nodesCount]; scene.Nodes = new MDSNode[header.NodesCount];
var offset = header.offsetToNodes; var offset = header.OffsetToNodes;
for (var i = 0; i < header.nodesCount; i++) for (var i = 0; i < header.NodesCount; i++)
{ {
var node = Utils.ReadStruct<Node>(data, ref offset, true); var node = Utils.ReadStruct<Node>(data, ref offset, true);
var type = node.meshIndex < 0 ? NodeType.Bone : NodeType.Mesh; var type = node.MeshIndex < 0 ? NodeType.Bone : NodeType.Mesh;
MDSNode mdsNode; MDSNode mdsNode;
if (type == NodeType.Bone) if (type == NodeType.Bone)
{ {
var boneNode = new MDSBone(); var boneNode = new MDSBone();
mdsNode = new MDSNode();
mdsNode = boneNode; mdsNode = boneNode;
} }
else else
@@ -229,42 +212,42 @@ public class Reader
mdsNode = new MDSMesh(); mdsNode = new MDSMesh();
} }
mdsNode.bindpose = node.bindpose; mdsNode.Bindpose = node.Bindpose;
mdsNode.name = Utils.GetName(names, node.nameOffset); mdsNode.Name = Utils.GetName(names, node.NameOffset);
mdsNode.transform = node.matrix; mdsNode.Transform = node.Matrix;
mdsNode.index = i; mdsNode.Index = i;
mdsNode.parentIndex = node.parentIndex; mdsNode.ParentIndex = node.ParentIndex;
mdsNode.type = type; mdsNode.Type = type;
mdsNode.meshIndex = node.meshIndex; mdsNode.MeshIndex = node.MeshIndex;
scene.nodes[i] = mdsNode; scene.Nodes[i] = mdsNode;
} }
} }
private static void ReadMaterials(MDSScene scene, MDSHeader header, byte[] data, string names) private static void ReadMaterials(MDSScene scene, MDSHeader header, byte[] data, string names)
{ {
var offset = header.offsetToMaterials; var offset = header.OffsetToMaterials;
scene.materials = new MDSMaterial[header.materialsCount]; scene.Materials = new MDSMaterial[header.MaterialsCount];
for (var i = 0; i < header.materialsCount; i++) for (var i = 0; i < header.MaterialsCount; i++)
{ {
var rawMaterial = Utils.ReadStruct<Material>(data, ref offset, true); var rawMaterial = Utils.ReadStruct<Material>(data, ref offset, true);
var mdsMaterial = new MDSMaterial(); var mdsMaterial = new MDSMaterial();
mdsMaterial.materialType = rawMaterial.value17; mdsMaterial.MaterialType = rawMaterial.MaterialType;
if (rawMaterial.value13 == 1) if (rawMaterial.NoName == 1)
{ {
mdsMaterial.name = "NONE"; mdsMaterial.Name = "NONE";
mdsMaterial.textureName = "NONE"; mdsMaterial.TextureName = "NONE";
} }
else else
{ {
mdsMaterial.name = Utils.GetName(names, rawMaterial.nameOffset); mdsMaterial.Name = Utils.GetName(names, rawMaterial.NameOffset);
mdsMaterial.textureName = Utils.GetName(names, rawMaterial.textureNameOffset); mdsMaterial.TextureName = Utils.GetName(names, rawMaterial.TextureNameOffset);
} }
scene.materials[i] = mdsMaterial; scene.Materials[i] = mdsMaterial;
} }
} }
@@ -273,7 +256,7 @@ public class Reader
var triangles = new List<int>(); var triangles = new List<int>();
if (header.ReadMode == TriangleReadMode.Triangle) if (header.ReadMode == TriangleReadMode.Triangle)
{ {
for (var i = 0; i < header.idsCount; i += 3) for (var i = 0; i < header.IdsCount; i += 3)
{ {
var triangle = Utils.ReadArray<short, int>(data, offset, 3, 0, i => i); var triangle = Utils.ReadArray<short, int>(data, offset, 3, 0, i => i);
offset += 6; offset += 6;
@@ -288,7 +271,7 @@ public class Reader
if (header.ReadMode == TriangleReadMode.TriangleStrip) if (header.ReadMode == TriangleReadMode.TriangleStrip)
{ {
var isDefaultOrder = true; var isDefaultOrder = true;
while (readCount < header.idsCount - 2) while (readCount < header.IdsCount - 2)
{ {
var triangle = Utils.ReadArray<short, int>(data, offset, 3, 0, i => i); var triangle = Utils.ReadArray<short, int>(data, offset, 3, 0, i => i);
readCount++; readCount++;
@@ -317,8 +300,7 @@ public class Reader
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"Bad index [{triangleVertexIndex}] in {content} array. Array start index: {address}, elements: {attribute.Length}"); Log.Line($"Bad index [{triangleVertexIndex}] in {content} array. Array start index: {address}, elements: {attribute.Length}");
break;
} }
} }
+45 -45
View File
@@ -4,19 +4,19 @@ namespace dq8chr2glb.Core.MDSFormat;
public class MDSScene public class MDSScene
{ {
public MDSNode[] nodes; public MDSNode[] Nodes;
public MDSMaterial[] materials; public MDSMaterial[] Materials;
} }
public class MDSNode public class MDSNode
{ {
public string name; public string Name;
public int index; public int Index;
public int parentIndex; public int ParentIndex;
public MDSMatrix transform; public MDSMatrix Transform;
public MDSMatrix bindpose; public MDSMatrix Bindpose;
public NodeType type; public NodeType Type;
public int meshIndex; public int MeshIndex;
} }
public class MDSBone : MDSNode public class MDSBone : MDSNode
@@ -25,57 +25,57 @@ public class MDSBone : MDSNode
public class MDSMesh : MDSNode public class MDSMesh : MDSNode
{ {
public SubMesh[] submeshes; public SubMesh[] Submeshes;
public MeshFeatures features; public MeshFeatures Features;
public AABB bounds; public AABB Bounds;
// merged data // merged data
public float[][] vertices; public float[][] Vertices;
public float[][] color; public float[][] Color;
public float[][] uv; public float[][] Uv;
public float[][] weights; public float[][] Weights;
public int[][] bones; public int[][] Bones;
public int[] triangles; public int[] Triangles;
} }
public class SubMesh public class SubMesh
{ {
public int materialIndex; public int MaterialIndex;
public int startIndex; public int StartIndex;
public int indexCount; public int IndexCount;
} }
public class MDSMaterial public class MDSMaterial
{ {
public string name; public string Name;
public string textureName; public string TextureName;
public int materialType; public int MaterialType;
} }
[StructLayout(LayoutKind.Explicit, Size=64)] [StructLayout(LayoutKind.Explicit, Size=64)]
public struct MDSMatrix public struct MDSMatrix
{ {
[FieldOffset(00)]public float m00; [FieldOffset(00)]public float M00;
[FieldOffset(04)]public float m01; [FieldOffset(04)]public float M01;
[FieldOffset(08)]public float m02; [FieldOffset(08)]public float M02;
[FieldOffset(12)]public float m03; [FieldOffset(12)]public float M03;
[FieldOffset(16)]public float m10; [FieldOffset(16)]public float M10;
[FieldOffset(20)]public float m11; [FieldOffset(20)]public float M11;
[FieldOffset(24)]public float m12; [FieldOffset(24)]public float M12;
[FieldOffset(28)]public float m13; [FieldOffset(28)]public float M13;
[FieldOffset(32)]public float m20; [FieldOffset(32)]public float M20;
[FieldOffset(36)]public float m21; [FieldOffset(36)]public float M21;
[FieldOffset(40)]public float m22; [FieldOffset(40)]public float M22;
[FieldOffset(44)]public float m23; [FieldOffset(44)]public float M23;
[FieldOffset(48)]public float m30; [FieldOffset(48)]public float M30;
[FieldOffset(52)]public float m31; [FieldOffset(52)]public float M31;
[FieldOffset(56)]public float m32; [FieldOffset(56)]public float M32;
[FieldOffset(60)]public float m33; [FieldOffset(60)]public float M33;
public float[] elements => new[] public float[] Elements => new[]
{ {
m00, m01, m02, m03, M00, M01, M02, M03,
m10, m11, m12, m13, M10, M11, M12, M13,
m20, m21, m22, m23, M20, M21, M22, M23,
m30, m31, m32, m33 M30, M31, M32, M33
}; };
} }
+76 -62
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using dq8chr2glb.Logger;
namespace dq8chr2glb.Core.MOTFormat; namespace dq8chr2glb.Core.MOTFormat;
@@ -11,9 +12,9 @@ public class Importer
{ {
var offset = 0; var offset = 0;
var header = Utils.ReadStruct<MOTHeader>(data, ref offset); var header = Utils.ReadStruct<MOTHeader>(data, ref offset);
offset += header.bonesOffset; offset += header.BonesOffset;
var boneCount = header.boneCount; var boneCount = header.BoneCount;
var animation = new List<MotionCurve>(); var animation = new List<MotionCurve>();
for (var i = 0; i < boneCount; i++) for (var i = 0; i < boneCount; i++)
{ {
@@ -26,7 +27,7 @@ public class Importer
private List<TransformGroup> GetTransformGroups(byte[] data, BoneHeader header, int offset, out int offsetAfter) private List<TransformGroup> GetTransformGroups(byte[] data, BoneHeader header, int offset, out int offsetAfter)
{ {
var attribCount = TransformFlagsHelper.GetAttribCount(header.transformFlags); var attribCount = TransformFlagsHelper.GetAttribCount(header.TransformFlags);
var attribHeadersOffset = attribCount > 1 ? 32 : 16; var attribHeadersOffset = attribCount > 1 ? 32 : 16;
var transformGroups = new List<TransformGroup>(); var transformGroups = new List<TransformGroup>();
@@ -44,31 +45,31 @@ public class Importer
private void ReadRotationKeyframes(byte[] data, MotionCurve curve, int offset) private void ReadRotationKeyframes(byte[] data, MotionCurve curve, int offset)
{ {
curve.curveType = KeyframeType.Quaternion; curve.CurveType = KeyframeType.Quaternion;
var headerOffset = offset; var headerOffset = offset;
var header = Utils.ReadStruct<KeyframesBlockHeader>(data, ref headerOffset); var header = Utils.ReadStruct<KeyframesBlockHeader>(data, ref headerOffset);
var valuesOffset = headerOffset + header.valuesOffset; var valuesOffset = headerOffset + header.ValuesOffset;
if (header.toFramesOffset == 0) if (header.ToFramesOffset == 0)
{ {
var count = header.framesCount; var count = header.FramesCount;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var keyframe = new KeyFrame(); var keyframe = new KeyFrame();
keyframe.frame = i; keyframe.Frame = i;
var scale = new Vector4(header.scaleX, header.scaleY, var scale = new Vector4(header.ScaleX, header.ScaleY,
header.scaleZ, header.scaleW); header.ScaleZ, header.ScaleW);
keyframe.rotation = ReadQuaternion(data, valuesOffset, scale); keyframe.Rotation = ReadQuaternion(data, valuesOffset, scale);
curve.keyframes.Add(keyframe); curve.Keyframes.Add(keyframe);
valuesOffset += 8; valuesOffset += 8;
} }
} }
else else
{ {
var framesCount = header.framesCount; var framesCount = header.FramesCount;
var toFramesOffset = headerOffset + header.toFramesOffset; var toFramesOffset = headerOffset + header.ToFramesOffset;
var frames = ReadFrames(data, toFramesOffset, framesCount); var frames = ReadFrames(data, toFramesOffset, framesCount);
if (frames.Length == 0) if (frames.Length == 0)
{ {
@@ -76,24 +77,32 @@ public class Importer
} }
var maxFrame = frames.Max(); var maxFrame = frames.Max();
curve.keyframes = new List<KeyFrame>(); curve.Keyframes = new List<KeyFrame>();
for (var i = 0; i < maxFrame + 1; i++) for (var i = 0; i < maxFrame + 1; i++)
{ {
curve.keyframes.Add(null); curve.Keyframes.Add(null);
} }
var toValues = header.valuesOffset; var toValues = header.ValuesOffset;
foreach (var frame in frames) foreach (var frame in frames)
{ {
var scale = new Vector4(header.scaleX, header.scaleY, var scale = new Vector4(header.ScaleX, header.ScaleY,
header.scaleZ, header.scaleW); header.ScaleZ, header.ScaleW);
var keyframe = new KeyFrame(); var keyframe = new KeyFrame();
keyframe.frame = frame; keyframe.Frame = frame;
keyframe.type = KeyframeType.Quaternion; keyframe.Type = KeyframeType.Quaternion;
keyframe.rotation = ReadQuaternion(data, valuesOffset, scale); keyframe.Rotation = ReadQuaternion(data, valuesOffset, scale);
try
{
curve.Keyframes[frame] = keyframe;
}
catch (Exception e)
{
Log.Line($"Skip negative frame number: {frame}", LogLevel.Warning);
}
curve.keyframes[frame] = keyframe;
valuesOffset += 8; valuesOffset += 8;
} }
} }
@@ -101,33 +110,33 @@ public class Importer
private void ReadPositionKeyframes(byte[] data, MotionCurve curve, int offset) private void ReadPositionKeyframes(byte[] data, MotionCurve curve, int offset)
{ {
curve.curveType = KeyframeType.Translation; curve.CurveType = KeyframeType.Translation;
var headerOffset = offset; var headerOffset = offset;
var header = Utils.ReadStruct<KeyframesBlockHeader>(data, ref offset, false); var header = Utils.ReadStruct<KeyframesBlockHeader>(data, ref offset, false);
var valuesOffset = headerOffset + header.valuesOffset; var valuesOffset = headerOffset + header.ValuesOffset;
if (header.toFramesOffset == 0) if (header.ToFramesOffset == 0)
{ {
var count = header.framesCount; var count = header.FramesCount;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var keyframe = new KeyFrame(); var keyframe = new KeyFrame();
keyframe.frame = i; keyframe.Frame = i;
var scale = new Vector3(header.scaleX, header.scaleY, var scale = new Vector3(header.ScaleX, header.ScaleY,
header.scaleZ); header.ScaleZ);
var translation = ReadVector3(data, valuesOffset, scale); var translation = ReadVector3(data, valuesOffset, scale);
keyframe.translation = translation; keyframe.Translation = translation;
curve.keyframes.Add(keyframe); curve.Keyframes.Add(keyframe);
valuesOffset += 6; valuesOffset += 6;
} }
} }
else else
{ {
var framesCount = header.framesCount; var framesCount = header.FramesCount;
var toFramesOffset = headerOffset + header.toFramesOffset; var toFramesOffset = headerOffset + header.ToFramesOffset;
var frames = ReadFrames(data, toFramesOffset, framesCount); var frames = ReadFrames(data, toFramesOffset, framesCount);
if (frames.Length == 0) if (frames.Length == 0)
{ {
@@ -135,24 +144,29 @@ public class Importer
} }
var maxFrame = frames.Max(); var maxFrame = frames.Max();
curve.keyframes = new List<KeyFrame>(); curve.Keyframes = new List<KeyFrame>();
for (var i = 0; i < maxFrame + 1; i++) for (var i = 0; i < maxFrame + 1; i++)
{ {
curve.keyframes.Add(null); curve.Keyframes.Add(null);
} }
var toValues = header.valuesOffset; var toValues = header.ValuesOffset;
foreach (var frame in frames) foreach (var frame in frames)
{ {
var scale = new Vector3(header.scaleX, header.scaleY, if (frame == null || frame < 0)
header.scaleZ); {
continue;
}
var scale = new Vector3(header.ScaleX, header.ScaleY,
header.ScaleZ);
var keyframe = new KeyFrame(); var keyframe = new KeyFrame();
keyframe.frame = frame; keyframe.Frame = frame;
keyframe.type = KeyframeType.Translation; keyframe.Type = KeyframeType.Translation;
keyframe.translation = ReadVector3(data, valuesOffset, scale); keyframe.Translation = ReadVector3(data, valuesOffset, scale);
curve.keyframes[frame] = keyframe; curve.Keyframes[frame] = keyframe;
valuesOffset += 6; valuesOffset += 6;
} }
} }
@@ -170,39 +184,39 @@ public class Importer
foreach (var group in transformGroups) foreach (var group in transformGroups)
{ {
var curve = new MotionCurve(); var curve = new MotionCurve();
curve.boneIndex = header.boneIndex; curve.BoneIndex = header.BoneIndex;
curve.keyframes = new List<KeyFrame>(); curve.Keyframes = new List<KeyFrame>();
switch (group.keyframeType) switch (group.KeyframeType)
{ {
case KeyframeType.Quaternion: case KeyframeType.Quaternion:
ReadRotationKeyframes(data, curve, headerPosition + group.offset); ReadRotationKeyframes(data, curve, headerPosition + group.Offset);
break; break;
case KeyframeType.Translation: case KeyframeType.Translation:
ReadPositionKeyframes(data, curve, headerPosition + group.offset); ReadPositionKeyframes(data, curve, headerPosition + group.Offset);
break; break;
default: default:
// Debug.Log($"[UNK KF TYPE]: {group.keyframeType}"); Log.Line($"[UNKNOWN KF TYPE]: {group.KeyframeType}");
break; break;
} }
boneCurves.Add(curve); boneCurves.Add(curve);
} }
switch (header.transformFlags) // switch (header.transformFlags)
{ // {
case TransformFlags.Rotation: // case TransformFlags.Rotation:
// Debug.Log($"Rotation: {(int)header.transformFlags}."); // // Debug.Log($"Rotation: {(int)header.transformFlags}.");
break; // break;
case TransformFlags.TR: // case TransformFlags.TR:
// Debug.Log($"Rot/Pos: {(int)header.transformFlags}"); // // Debug.Log($"Rot/Pos: {(int)header.transformFlags}");
break; // break;
default: // default:
// Debug.Log($"Unknown flag: {(int)header.transformFlags}, {header.transformFlags}"); // // Debug.Log($"Unknown flag: {(int)header.transformFlags}, {header.transformFlags}");
break; // break;
} // }
offset = headerPosition + header.toNext; offset = headerPosition + header.ToNext;
return boneCurves; return boneCurves;
} }
+26 -26
View File
@@ -9,39 +9,39 @@ namespace dq8chr2glb.Core.MOTFormat
public struct MOTHeader public struct MOTHeader
{ {
[FieldOffset(00)] public int MagicCode; [FieldOffset(00)] public int MagicCode;
[FieldOffset(08)] public int headerSize; [FieldOffset(08)] public int HeaderSize;
[FieldOffset(28)] public int boneCount; [FieldOffset(28)] public int BoneCount;
[FieldOffset(32)] public int bonesOffset; [FieldOffset(32)] public int BonesOffset;
[FieldOffset(48)] public int fileSizeClaim; [FieldOffset(48)] public int FileSize;
} }
[StructLayout(LayoutKind.Explicit, Size = 16)] [StructLayout(LayoutKind.Explicit, Size = 16)]
public struct BoneHeader public struct BoneHeader
{ {
[FieldOffset(00)] public int toNext; [FieldOffset(00)] public int ToNext;
[FieldOffset(04)] public int boneIndex; [FieldOffset(04)] public int BoneIndex;
[FieldOffset(12)] public TransformFlags transformFlags; [FieldOffset(12)] public TransformFlags TransformFlags;
} }
[StructLayout(LayoutKind.Explicit, Size = 8)] [StructLayout(LayoutKind.Explicit, Size = 8)]
public struct TransformGroup public struct TransformGroup
{ {
[FieldOffset(00)] public KeyframeType keyframeType; [FieldOffset(00)] public KeyframeType KeyframeType;
[FieldOffset(04)] public int offset; [FieldOffset(04)] public int Offset;
} }
[StructLayout(LayoutKind.Explicit, Size = 32)] [StructLayout(LayoutKind.Explicit, Size = 32)]
public struct KeyframesBlockHeader public struct KeyframesBlockHeader
{ {
[FieldOffset(00)] public short flag0; [FieldOffset(00)] public short Flag0;
[FieldOffset(02)] public short flag1; [FieldOffset(02)] public short Flag1;
[FieldOffset(06)] public short framesCount; [FieldOffset(06)] public short FramesCount;
[FieldOffset(08)] public short toFramesOffset; [FieldOffset(08)] public short ToFramesOffset;
[FieldOffset(12)] public short valuesOffset; [FieldOffset(12)] public short ValuesOffset;
[FieldOffset(16)] public float scaleX; [FieldOffset(16)] public float ScaleX;
[FieldOffset(20)] public float scaleY; [FieldOffset(20)] public float ScaleY;
[FieldOffset(24)] public float scaleZ; [FieldOffset(24)] public float ScaleZ;
[FieldOffset(28)] public float scaleW; [FieldOffset(28)] public float ScaleW;
} }
[Flags] [Flags]
@@ -68,18 +68,18 @@ namespace dq8chr2glb.Core.MOTFormat
public class MotionCurve public class MotionCurve
{ {
public int boneIndex; public int BoneIndex;
public KeyframeType curveType; public KeyframeType CurveType;
public List<KeyFrame> keyframes; public List<KeyFrame> Keyframes;
} }
public class KeyFrame public class KeyFrame
{ {
public int frame; public int Frame;
public KeyframeType type; public KeyframeType Type;
public Quaternion rotation; public Quaternion Rotation;
public Vector3 translation; public Vector3 Translation;
public Vector3 scale; public Vector3 Scale;
} }
public static class TransformFlagsHelper public static class TransformFlagsHelper
-2
View File
@@ -1,2 +0,0 @@
namespace dq8chr2glb.Core;
+3 -3
View File
@@ -68,12 +68,12 @@ public static class Utils
public static MDSMesh GetMeshByIndex(MDSScene scene, int index) public static MDSMesh GetMeshByIndex(MDSScene scene, int index)
{ {
foreach (var node in scene.nodes) foreach (var node in scene.Nodes)
{ {
if (node.type == NodeType.Mesh) if (node.Type == NodeType.Mesh)
{ {
var mesh = node as MDSMesh; var mesh = node as MDSMesh;
if (mesh.meshIndex == index) if (mesh.MeshIndex == index)
{ {
return mesh; return mesh;
} }
+60
View File
@@ -0,0 +1,60 @@
using System;
namespace dq8chr2glb.Logger;
public enum LogMode
{
ALL = 8, // show all debug info
DEBUG = 4, // show task name, .chr content and status with stacktrace
EXTEND = 2, // show task name, .chr content and status with general errors
MINIMAL = 1, // show task name and result status
NONE = 0, // squeaky clean console
}
[Flags]
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
None
}
public static class Log
{
// public const LogLevel LogLevel = Logger.LogLevel.Info;
public static void Line(object? data, LogLevel level = LogLevel.Debug)
{
var lastColor = Console.ForegroundColor;
var color = level switch
{
LogLevel.Error => ConsoleColor.Red,
LogLevel.Debug => ConsoleColor.White,
LogLevel.Info => ConsoleColor.White,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.None => ConsoleColor.White,
};
// if (level == LogLevel.Debug)
// {
// return;
// }
Console.ForegroundColor = color;
Console.WriteLine(data != null ? data.ToString() : "");
Console.ForegroundColor = lastColor;
}
public static void Error(Exception e, LogLevel level = LogLevel.Error)
{
var lastColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.Write($"[{level}] ");
Console.ForegroundColor = ConsoleColor.DarkRed;
Console.Write($"{e.Message}\n From:\n{e.Source}\nStacktrace:\n{e.StackTrace}\n");
Console.Write($"{e.Message}\n");
Console.ForegroundColor = lastColor;
}
}
+44 -69
View File
@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.CommandLine;
using System.IO; using System.IO;
using dq8chr2glb.Converter;
using dq8chr2glb.Logger;
namespace dq8chr2glb; namespace dq8chr2glb;
@@ -8,63 +10,57 @@ public class Program
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
if (args.Length < 2) var rootCommand = new RootCommand("Converts .CHR files from Dragon Quest VIII (Playstation 2) to .glb/.glTF format.")
{ {
ShowHelp(); new Option<string>("-i", "--input") { Description = "Input path", HelpName = "input path"},
new Option<string>("-o", "--output") { Description = "Output path", HelpName = "output path"},
new Option<string>("-d", "--data") { Description = "Data for append animations", HelpName = "Data"},
new Option<OutputFormat>("-f", "--format") {Description = "Output format", DefaultValueFactory = _ => OutputFormat.GLB},
new Option<bool>("-e", "--extract") {Description = "Extract only - unpack .chr without conversion"},
new Option<bool>("-b", "--batch") {Description = "Batch mode - process all .chr files in the input directory"},
// new Option<bool>("-a", "--append") { Description = "Append animations to input .glb/glTF file"},
};
var parser = rootCommand.Parse(args);
var inputPath = parser.GetValue<string>("-i");
var outputPath = parser.GetValue<string>("-o");
var dataPath = parser.GetValue<string>("-d");
var extractOnly = parser.GetValue<bool>("-e");
var textFormat = parser.GetValue<OutputFormat>("-f") == OutputFormat.GLTF;
var batchMode = parser.GetValue<bool>("-b");
// var appendMode = parser.GetValue<bool>("-a");
// if (appendMode)
// {
// var appendAnimations = new AppendAnimations();
// appendAnimations.Append(inputPath, dataPath);
// return;
// }
var chrFile = new ChrFile();
chrFile.Convert = !extractOnly;
chrFile.Extract = extractOnly;
chrFile.TextFormat = textFormat;
if (string.IsNullOrEmpty(inputPath))
{
Console.WriteLine("Input path is required! Use -h to see help screen.");
return; return;
} }
var extractOnly = false; if (string.IsNullOrEmpty(outputPath))
var textFormat = false;
var batchMode = false;
var processedArgs = new List<string>();
for (int i = 0; i < args.Length; i++)
{ {
var arg = args[i]; var dirName = Path.GetFileNameWithoutExtension(inputPath);
if (arg == "-e") outputPath = Path.Combine(Path.GetDirectoryName(inputPath), dirName);
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) if (batchMode)
{ {
var files = Directory.GetFiles(inputPath, "*.chr", SearchOption.TopDirectoryOnly); var files = Directory.GetFiles(inputPath, "*.chr", SearchOption.TopDirectoryOnly);
foreach (var file in files) foreach (var file in files)
{ {
Console.WriteLine($"Processing: {Path.GetFileName(file)}"); Log.Line($"Processing: {Path.GetFileName(file)}", LogLevel.Info);
try try
{ {
chrFile.Process(file, outputPath); chrFile.Process(file, outputPath);
@@ -74,8 +70,6 @@ public class Program
Console.WriteLine(e); Console.WriteLine(e);
throw; throw;
} }
chrFile.Clean();
} }
Console.WriteLine(files.Length == 0 ? "No .chr files found in the input directory." : "Done!"); Console.WriteLine(files.Length == 0 ? "No .chr files found in the input directory." : "Done!");
@@ -84,7 +78,7 @@ public class Program
{ {
if (!File.Exists(inputPath)) if (!File.Exists(inputPath))
{ {
Console.WriteLine($"Error: Input file not found: {inputPath}"); Console.WriteLine($"Input file not found: {inputPath}");
return; return;
} }
@@ -92,23 +86,4 @@ public class Program
Console.WriteLine("Done!"); 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(" dq8chr2glb.exe <input_file> <output_dir> [options]");
Console.WriteLine(" dq8chr2glb.exe <input_dir> -b (batch mode: output files are saved in the <input_dir>)");
Console.WriteLine("Examples:");
Console.WriteLine(" dq8chr2glb.exe \"C:\\Users\\Boris\\Desktop\\ChrFormatTest\\ap002.chr\" \"C:\\Users\\Boris\\Desktop\\ChrFormatTest\" -e");
Console.WriteLine(" dq8chr2glb.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();
}
}
+33 -7
View File
@@ -1,4 +1,7 @@
using System; using System;
using dq8chr2glb.Container;
using dq8chr2glb.Converter;
using dq8chr2glb.Logger;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@@ -7,11 +10,21 @@ namespace dq8chr2glb.TM2Format;
// Взято из https://github.com/Souzooka/TM2Unswizzler // Взято из https://github.com/Souzooka/TM2Unswizzler
public static class TM2Format public static class TM2Format
{ {
public static Texture GetImage(byte[] data, string name) public static IncludedFile CurrentFile;
public static Texture GetImage(IncludedFile file, string name)
{ {
CurrentFile = file;
var data = file.Data;
var header = TM2Header.GetHeader(data); var header = TM2Header.GetHeader(data);
var image = GetImageBuffer(header, data); var image = GetImageBuffer(header, data);
if (image == null)
{
Log.Line($"Failed to create texture {name}", LogLevel.Error);
return null;
}
var img = new Image<Rgba32>(header.Width, header.Height); var img = new Image<Rgba32>(header.Width, header.Height);
for (var y = 0; y < header.Height; y++) for (var y = 0; y < header.Height; y++)
@@ -45,15 +58,23 @@ public static class TM2Format
{ {
var palette = new byte[256][]; var palette = new byte[256][];
for (var i = 0; i < 256; ++i) try
{ {
var rgba = new byte[4]; for (var i = 0; i < 256; ++i)
Array.Copy(file, 0x40 + header.ImageSize + i * 4, rgba, 0, 4); {
rgba[3] = (byte)Math.Min(rgba[3] << 1, 0xFF); 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]); (rgba[2], rgba[0]) = (rgba[0], rgba[2]);
palette[(i & 0xe7) | ((i & 0x10) >> 1) | ((i & 0x08) << 1)] = rgba; palette[(i & 0xe7) | ((i & 0x10) >> 1) | ((i & 0x08) << 1)] = rgba;
}
}
catch (Exception e)
{
Context.Current.Errors.Add(new Error(CurrentFile.Name, "Error while import texture", e));
palette = null;
} }
return palette; return palette;
@@ -63,6 +84,11 @@ public static class TM2Format
{ {
var palette = GetPalette8bbp(tm2file, header); var palette = GetPalette8bbp(tm2file, header);
if (palette == null)
{
return null;
}
var imageBuf = new byte[4 * header.Width * header.Height]; var imageBuf = new byte[4 * header.Width * header.Height];
for (var y = header.Height - 1; y >= 0; --y) for (var y = header.Height - 1; y >= 0; --y)
{ {
+2 -2
View File
@@ -8,8 +8,8 @@ public struct TM2Header
public int Label; public int Label;
public byte Version; public byte Version;
public byte Format; public byte Format;
public int padding0; public int Padding0;
public int padding1; public int Padding1;
public uint TotalSize; public uint TotalSize;
public uint ClutSize; public uint ClutSize;
public uint ImageSize; public uint ImageSize;
+4 -4
View File
@@ -5,12 +5,12 @@ namespace dq8chr2glb.TM2Format;
public class Texture public class Texture
{ {
public readonly string name; public readonly string Name;
public readonly Image<Rgba32> data; public readonly Image<Rgba32> Data;
public Texture(string name, Image<Rgba32> data) public Texture(string name, Image<Rgba32> data)
{ {
this.name = name; Name = name;
this.data = data; Data = data;
} }
} }
+1
View File
@@ -13,6 +13,7 @@
<PackageReference Include="SharpGLTF.Core" Version="1.0.6" /> <PackageReference Include="SharpGLTF.Core" Version="1.0.6" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.6" /> <PackageReference Include="SharpGLTF.Toolkit" Version="1.0.6" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="System.CommandLine" Version="3.0.0-preview.1.26104.118" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="11.0.0-preview.1.26104.118" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="11.0.0-preview.1.26104.118" />
</ItemGroup> </ItemGroup>