mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
SuzanneWalker
This commit is contained in:
parent
e8e87cc6cb
commit
b7edf521b6
4 changed files with 461 additions and 6 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6
|
||||
Subproject commit 821194d0650a2dac98b7cbba9ff4a79e32b32d4d
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue