SuzanneWalker

This commit is contained in:
ackwell 2024-01-04 19:27:04 +11:00
parent e8e87cc6cb
commit b7edf521b6
4 changed files with 461 additions and 6 deletions

@ -1 +1 @@
Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6
Subproject commit 821194d0650a2dac98b7cbba9ff4a79e32b32d4d

View file

@ -1,9 +1,14 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Files;
using Penumbra.Import.Models.Export;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Materials;
using SharpGLTF.Scenes;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models;
@ -54,6 +59,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
public Task<MdlFile> ImportGltf()
{
var action = new ImportGltfAction();
return Enqueue(action).ContinueWith(_ => action.Out!);
}
private class ExportToGltfAction : IAction
{
private readonly ModelManager _manager;
@ -109,4 +120,430 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
return true;
}
}
private class ImportGltfAction : IAction
{
public MdlFile? Out;
public ImportGltfAction()
{
//
}
private ModelRoot Build()
{
// Build a super simple plane as a fake gltf input.
var material = new MaterialBuilder();
var mesh = new MeshBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>("mesh 0.0");
var prim = mesh.UsePrimitive(material);
var tangent = new Vector4(.5f, .5f, 0, 1);
var vert1 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(-1, 0, 1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.UnitY, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert2 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(1, 0, 1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.One, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert3 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(-1, 0, -1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.Zero, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert4 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(1, 0, -1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.UnitX, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
prim.AddTriangle(vert2, vert3, vert1);
prim.AddTriangle(vert2, vert4, vert3);
var jKosi = new NodeBuilder("j_kosi");
var scene = new SceneBuilder();
scene.AddNode(jKosi);
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, [jKosi]);
var model = scene.ToGltf2();
return model;
}
private (MdlStructs.VertexElement, Action<int, List<byte>>) GetPositionWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("POSITION", out var accessor))
throw new Exception("todo: some error about position being hard required");
var element = new MdlStructs.VertexElement()
{
Stream = 0,
Type = (byte)MdlFile.VertexType.Single3,
Usage = (byte)MdlFile.VertexUsage.Position,
};
IList<Vector3> values = accessor.AsVector3Array();
return (
element,
(index, bytes) => WriteSingle3(values[index], bytes)
);
}
// TODO: probably should sanity check that if there's weights or indexes, both are available? game is always symmetric
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetBlendWeightWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("WEIGHTS_0", out var accessor))
return null;
var element = new MdlStructs.VertexElement()
{
Stream = 0,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Usage = (byte)MdlFile.VertexUsage.BlendWeights,
};
var values = accessor.AsVector4Array();
return (
element,
(index, bytes) => WriteByteFloat4(values[index], bytes)
);
}
// TODO: this will need to take in a skeleton mapping of some kind so i can persist the bones used and wire up the joints correctly. hopefully by the "write vertex buffer" stage of building, we already know something about the skeleton.
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetBlendIndexWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("JOINTS_0", out var accessor))
return null;
var element = new MdlStructs.VertexElement()
{
Stream = 0,
Type = (byte)MdlFile.VertexType.UInt,
Usage = (byte)MdlFile.VertexUsage.BlendIndices,
};
var values = accessor.AsVector4Array();
return (
element,
(index, bytes) => WriteUInt(values[index], bytes)
);
}
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetNormalWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("NORMAL", out var accessor))
return null;
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Type = (byte)MdlFile.VertexType.Half4,
Usage = (byte)MdlFile.VertexUsage.Normal,
};
var values = accessor.AsVector3Array();
return (
element,
(index, bytes) => WriteHalf4(new Vector4(values[index], 0), bytes)
);
}
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetUvWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("TEXCOORD_0", out var accessor1))
return null;
// We're omitting type here, and filling it in on return, as there's two different types we might use.
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Usage = (byte)MdlFile.VertexUsage.UV,
};
var values1 = accessor1.AsVector2Array();
if (!accessors.TryGetValue("TEXCOORD_1", out var accessor2))
return (
element with {Type = (byte)MdlFile.VertexType.Half2},
(index, bytes) => WriteHalf2(values1[index], bytes)
);
var values2 = accessor2.AsVector2Array();
return (
element with {Type = (byte)MdlFile.VertexType.Half4},
(index, bytes) => {
var value1 = values1[index];
var value2 = values2[index];
WriteHalf4(new Vector4(value1.X, value1.Y, value2.X, value2.Y), bytes);
}
);
}
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetTangent1Writer(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("TANGENT", out var accessor))
return null;
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Usage = (byte)MdlFile.VertexUsage.Tangent1,
};
var values = accessor.AsVector4Array();
return (
element,
(index, bytes) => WriteByteFloat4(values[index], bytes)
);
}
private (MdlStructs.VertexElement, Action<int, List<byte>>)? GetColorWriter(IReadOnlyDictionary<string, Accessor> accessors)
{
if (!accessors.TryGetValue("COLOR_0", out var accessor))
return null;
var element = new MdlStructs.VertexElement()
{
Stream = 1,
Type = (byte)MdlFile.VertexType.ByteFloat4,
Usage = (byte)MdlFile.VertexUsage.Color,
};
var values = accessor.AsVector4Array();
return (
element,
(index, bytes) => WriteByteFloat4(values[index], bytes)
);
}
private void WriteSingle3(Vector3 input, List<byte> bytes)
{
bytes.AddRange(BitConverter.GetBytes(input.X));
bytes.AddRange(BitConverter.GetBytes(input.Y));
bytes.AddRange(BitConverter.GetBytes(input.Z));
}
private void WriteUInt(Vector4 input, List<byte> bytes)
{
bytes.Add((byte)input.X);
bytes.Add((byte)input.Y);
bytes.Add((byte)input.Z);
bytes.Add((byte)input.W);
}
private void WriteByteFloat4(Vector4 input, List<byte> bytes)
{
bytes.Add((byte)Math.Round(input.X * 255f));
bytes.Add((byte)Math.Round(input.Y * 255f));
bytes.Add((byte)Math.Round(input.Z * 255f));
bytes.Add((byte)Math.Round(input.W * 255f));
}
private void WriteHalf2(Vector2 input, List<byte> bytes)
{
bytes.AddRange(BitConverter.GetBytes((Half)input.X));
bytes.AddRange(BitConverter.GetBytes((Half)input.Y));
}
private void WriteHalf4(Vector4 input, List<byte> bytes)
{
bytes.AddRange(BitConverter.GetBytes((Half)input.X));
bytes.AddRange(BitConverter.GetBytes((Half)input.Y));
bytes.AddRange(BitConverter.GetBytes((Half)input.Z));
bytes.AddRange(BitConverter.GetBytes((Half)input.W));
}
private byte TypeSize(MdlFile.VertexType type)
{
return type switch
{
MdlFile.VertexType.Single3 => 12,
MdlFile.VertexType.Single4 => 16,
MdlFile.VertexType.UInt => 4,
MdlFile.VertexType.ByteFloat4 => 4,
MdlFile.VertexType.Half2 => 4,
MdlFile.VertexType.Half4 => 8,
_ => throw new Exception($"Unhandled vertex type {type}"),
};
}
public void Execute(CancellationToken cancel)
{
var model = Build();
// ---
// todo this'll need to check names and such. also loop. i'm relying on a single mesh here which is Wrong:tm:
var mesh = model.LogicalNodes
.Where(node => node.Mesh != null)
.Select(node => node.Mesh)
.First();
// todo check how many prims there are - maybe throw if more than one? not sure
var prim = mesh.Primitives[0];
var accessors = prim.VertexAccessors;
var rawWriters = new[] {
GetPositionWriter(accessors),
GetBlendWeightWriter(accessors),
GetBlendIndexWriter(accessors),
GetNormalWriter(accessors),
GetTangent1Writer(accessors),
GetColorWriter(accessors),
GetUvWriter(accessors),
};
var writers = new List<(MdlStructs.VertexElement, Action<int, List<byte>>)>();
var offsets = new byte[] {0, 0, 0};
foreach (var writer in rawWriters)
{
if (writer == null) continue;
var element = writer.Value.Item1;
writers.Add((
element with {Offset = offsets[element.Stream]},
writer.Value.Item2
));
offsets[element.Stream] += TypeSize((MdlFile.VertexType)element.Type);
}
var strides = offsets;
var streams = new List<byte>[3];
for (var i = 0; i < 3; i++)
streams[i] = new List<byte>();
// todo: this is a bit lmao but also... probably the most sane option? getting the count that is
var vertexCount = prim.VertexAccessors["POSITION"].Count;
for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
{
foreach (var (element, writer) in writers)
{
writer(vertexIndex, streams[element.Stream]);
}
}
// indices
var indexCount = prim.GetIndexAccessor().Count;
var indices = prim.GetIndices()
.SelectMany(index => BitConverter.GetBytes((ushort)index))
.ToArray();
var dataBuffer = streams[0].Concat(streams[1]).Concat(streams[2]).Concat(indices);
var lod1VertLen = (uint)(streams[0].Count + streams[1].Count + streams[2].Count);
var mdl = new MdlFile()
{
Radius = 1,
// todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow.
VertexOffset = [0, 0, 0],
IndexOffset = [lod1VertLen, 0, 0],
VertexBufferSize = [lod1VertLen, 0, 0],
IndexBufferSize = [(uint)indices.Length, 0, 0],
LodCount = 1,
BoundingBoxes = new MdlStructs.BoundingBoxStruct()
{
Min = [-1, 0, -1, 1],
Max = [1, 0, 1, 1],
},
VertexDeclarations = [new MdlStructs.VertexDeclarationStruct()
{
VertexElements = writers.Select(x => x.Item1).ToArray(),
}],
Meshes = [new MdlStructs.MeshStruct()
{
VertexCount = (ushort)vertexCount,
IndexCount = (uint)indexCount,
MaterialIndex = 0,
SubMeshIndex = 0,
SubMeshCount = 1,
BoneTableIndex = 0,
StartIndex = 0,
// todo: this will need to be composed down across multiple submeshes. given submeshes store contiguous buffers
VertexBufferOffset = [0, (uint)streams[0].Count, (uint)(streams[0].Count + streams[1].Count)],
VertexBufferStride = strides,
VertexStreamCount = 2,
}],
BoneTables = [new MdlStructs.BoneTableStruct()
{
BoneCount = 1,
// this needs to be the full 64. this should be fine _here_ with 0s because i only have one bone, but will need to be fully populated properly. in real files.
BoneIndex = new ushort[64],
}],
BoneBoundingBoxes = [
// new MdlStructs.BoundingBoxStruct()
// {
// Min = [
// -0.081672676f,
// -0.113717034f,
// -0.11905348f,
// 1.0f,
// ],
// Max = [
// 0.03941727f,
// 0.09845419f,
// 0.107391916f,
// 1.0f,
// ],
// },
// _would_ be nice if i didn't need to fill out this
new MdlStructs.BoundingBoxStruct()
{
Min = [0, 0, 0, 0],
Max = [0, 0, 0, 0],
}
],
SubMeshes = [new MdlStructs.SubmeshStruct()
{
IndexOffset = 0,
IndexCount = (uint)indexCount,
AttributeIndexMask = 0,
BoneStartIndex = 0,
BoneCount = 1,
}],
// TODO pretty sure this is garbage data as far as textools functions
// game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh
SubMeshBoneMap = [0],
Lods = [new MdlStructs.LodStruct()
{
MeshIndex = 0,
MeshCount = 1,
ModelLodRange = 0,
TextureLodRange = 0,
VertexBufferSize = lod1VertLen,
VertexDataOffset = 0,
IndexBufferSize = (uint)indexCount,
IndexDataOffset = lod1VertLen,
},
],
Bones = [
"j_kosi",
],
Materials = [
"/mt_c0201e6180_top_b.mtrl",
],
RemainingData = dataBuffer.ToArray(),
};
Out = mdl;
}
public bool Equals(IAction? other)
{
if (other is not ImportGltfAction rhs)
return false;
return true;
}
}
}

View file

@ -12,10 +12,10 @@ public partial class ModEditWindow
{
private ModEditWindow _edit;
public readonly MdlFile Mdl;
private readonly List<string>[] _attributes;
public MdlFile Mdl { get; private set; }
private List<string>[] _attributes;
public List<Utf8GamePath>? GamePaths { get; private set ;}
public List<Utf8GamePath>? GamePaths { get; private set; }
public int GamePathIndex;
public bool PendingIo { get; private set; } = false;
@ -34,13 +34,19 @@ public partial class ModEditWindow
{
_edit = edit;
Mdl = new MdlFile(bytes);
_attributes = CreateAttributes(Mdl);
Initialize(new MdlFile(bytes));
if (mod != null)
FindGamePaths(path, mod);
}
[MemberNotNull(nameof(Mdl), nameof(_attributes))]
private void Initialize(MdlFile mdl)
{
Mdl = mdl;
_attributes = CreateAttributes(Mdl);
}
/// <inheritdoc/>
public bool Valid
=> Mdl.Valid;
@ -72,6 +78,12 @@ public partial class ModEditWindow
});
}
public void Import()
{
// TODO: this needs to be fleshed out a bunch.
_edit._models.ImportGltf().ContinueWith(v => Initialize(v.Result));
}
/// <summary> Export model to an interchange format. </summary>
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
public void Export(string outputPath, Utf8GamePath mdlPath)

View file

@ -37,6 +37,12 @@ public partial class ModEditWindow
DrawExport(tab, disabled);
var ret = false;
if (ImGui.Button("import test"))
{
tab.Import();
ret |= true;
}
ret |= DrawModelMaterialDetails(tab, disabled);