diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 5a8f698..d793545 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -29,6 +29,14 @@ public static class Extensions return BinaryPrimitives.ReadUInt16LittleEndian(buf); } + public static short ReadInt16LittleEndian(this Stream fs) + { + Span buf = stackalloc byte[2]; + fs.ReadExactly(buf); + + return BinaryPrimitives.ReadInt16LittleEndian(buf); + } + public static float ReadFloatLittleEndian(this Stream fs) { Span buf = stackalloc byte[4]; diff --git a/Common/Quaternion.cs b/Common/Quaternion.cs index 9f93a67..8030cb1 100644 --- a/Common/Quaternion.cs +++ b/Common/Quaternion.cs @@ -2,7 +2,7 @@ public record Quaternion(float X, float Y, float Z, float W); -public record UShortQuaternion(ushort X, ushort Y, ushort Z, ushort W) +public record UShortQuaternion(short W, short X, short Y, short Z) { public Quaternion ToRegular() { diff --git a/MshLib/Msh0x08.cs b/MshLib/Msh0x08.cs index 44ed397..0ad2ad6 100644 --- a/MshLib/Msh0x08.cs +++ b/MshLib/Msh0x08.cs @@ -26,10 +26,10 @@ public static class Msh0x08 mshFs.ReadFloatLittleEndian()), mshFs.ReadFloatLittleEndian(), new UShortQuaternion( - mshFs.ReadUInt16LittleEndian(), - mshFs.ReadUInt16LittleEndian(), - mshFs.ReadUInt16LittleEndian(), - mshFs.ReadUInt16LittleEndian() + mshFs.ReadInt16LittleEndian(), + mshFs.ReadInt16LittleEndian(), + mshFs.ReadInt16LittleEndian(), + mshFs.ReadInt16LittleEndian() ) )); } diff --git a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs index ca20e1d..8631d80 100644 --- a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs +++ b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs @@ -226,7 +226,7 @@ public static unsafe class PrimitiveMeshes BufferUsageARB.StaticDraw); } - const uint stride = 6 * sizeof(float); + const uint stride = 9 * sizeof(float); gl.EnableVertexAttribArray(0); gl.VertexAttribPointer( @@ -245,6 +245,16 @@ public static unsafe class PrimitiveMeshes false, stride, (void*)(3 * sizeof(float))); + + gl.EnableVertexAttribArray(2); + gl.VertexAttribPointer( + 2, + 3, + VertexAttribPointerType.Float, + false, + stride, + (void*)(6 * sizeof(float)) + ); gl.BindVertexArray(0); diff --git a/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs new file mode 100644 index 0000000..27e0ed5 --- /dev/null +++ b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs @@ -0,0 +1,156 @@ +using System.Collections; +using System.Numerics; +using Common; +using MshLib; +using Quaternion = System.Numerics.Quaternion; +using Vector3 = System.Numerics.Vector3; + +namespace NResUI.Rendering.Viewport.Msh; + +public static class MshRestPoseBuilder +{ + public static IReadOnlyList BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, List animationDescriptors) + { + var nodeList = nodesComponent.Nodes; + var animationList = animationDescriptors; + + var poses = new MshPieceRestPose[nodeList.Count]; + var state = new byte[nodeList.Count]; + + for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++) + BuildNodePose(nodeIndex, nodeList, animationList, poses, state); + + return poses; + } + + private static Matrix4x4 BuildNodePose( + int nodeIndex, + List nodes, + List animationDescriptors, + MshPieceRestPose[] poses, + byte[] state) + { + if (state[nodeIndex] == 2) + return poses[nodeIndex].MeshSpaceTransform; + + if (state[nodeIndex] == 1) + { + // Broken/cyclic parent chain. Keep this node local so the viewer remains usable. + return Matrix4x4.Identity; + } + state[nodeIndex] = 1; + + var node = nodes[nodeIndex]!; + var parentIndex = GetParentIndex(node); + var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node); + var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count; + + // var localTransform = Matrix4x4.Identity; + Matrix4x4 localTransform; + if (hasFallbackPose) + { + localTransform = BuildTransformFromAnimationDescriptor(animationDescriptors[fallbackKeyframeIndex]!); + } + else + { + localTransform = Matrix4x4.Identity; + + Console.WriteLine($"Node {nodeIndex} has no fallback"); + } + + if (parentIndex == -1) + { + localTransform.Translation = Vector3.Zero; + + Console.WriteLine($"Node {nodeIndex} has no parent"); + } + + var meshSpaceTransform = localTransform; + if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex) + { + var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, poses, state); + meshSpaceTransform = localTransform * parentTransform; + } + + poses[nodeIndex] = new MshPieceRestPose( + NodeIndex: nodeIndex, + ParentIndex: parentIndex, + FallbackKeyframeIndex: fallbackKeyframeIndex, + HasFallbackPose: hasFallbackPose, + LocalTransform: localTransform, + MeshSpaceTransform: meshSpaceTransform); + + state[nodeIndex] = 2; + return meshSpaceTransform; + } + + private static int GetParentIndex(Msh0x01.Node node) + { + try + { + return Convert.ToInt32(node.ParentIndexOrMinusOne); + } + catch + { + var rawParent = Convert.ToUInt16(node.ParentIndexOrLink); + return rawParent == ushort.MaxValue ? -1 : rawParent; + } + } + + private static int GetFallbackKeyframeIndex(Msh0x01.Node node) + { + try + { + var value = Convert.ToUInt16(node.FallbackKey0x08); + return value == ushort.MaxValue ? -1 : value; + } + catch + { + return -1; + } + } + + private static Matrix4x4 BuildTransformFromAnimationDescriptor(Msh0x08.AnimationDescriptor descriptor) + { + var position = ToSystemVector3(descriptor.Position); + var rotation = ToSystemQuaternion(descriptor.Rotation); + + var matrix = Matrix4x4.CreateFromQuaternion(rotation); + matrix.Translation = position; + return matrix; + } + + private static Vector3 ToSystemVector3(dynamic vector) + { + return new Vector3( + Convert.ToSingle(vector.X), + Convert.ToSingle(vector.Y), + Convert.ToSingle(vector.Z)); + } + + private static Quaternion ToSystemQuaternion(UShortQuaternion packedQuaternion) + { + // MSH 0x08 stores quaternion as W, X, Y, Z. + // System.Numerics.Quaternion constructor expects X, Y, Z, W. + + var q = new Quaternion( + packedQuaternion.X / 32767f, + packedQuaternion.Y / 32767f, + packedQuaternion.Z / 32767f, + packedQuaternion.W / 32767f + ); + + if (q.LengthSquared() < 1e-8f) + return Quaternion.Identity; + + return Quaternion.Normalize(q); + } +} + +public readonly record struct MshPieceRestPose( + int NodeIndex, + int ParentIndex, + int FallbackKeyframeIndex, + bool HasFallbackPose, + Matrix4x4 LocalTransform, + Matrix4x4 MeshSpaceTransform); diff --git a/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs b/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs index f786e68..1776a68 100644 --- a/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs +++ b/NResUI/Rendering/Viewport/Msh/MshViewportLoader.cs @@ -49,6 +49,9 @@ public static class MshViewportLoader var positions = Msh0x03.ReadComponent(fs, archive); var indices = Msh0x06.ReadComponent(fs, archive); var batches = Msh0x0D.ReadComponent(fs, archive); + var animationDescriptors = Msh0x08.ReadComponent(fs, archive); + var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors); + var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f); var names = TryReadNames(fs, archive); var pieces = new List(); @@ -56,6 +59,7 @@ public static class MshViewportLoader for (var nodeIndex = 0; nodeIndex < nodes.Nodes.Count; nodeIndex++) { var node = nodes.Nodes[nodeIndex]; + var restPose = restPoses[nodeIndex]; var slotIndex = node.ResolveSlotIndex(DefaultModelState, DefaultLod); if (slotIndex == ushort.MaxValue) continue; @@ -72,24 +76,24 @@ public static class MshViewportLoader continue; var name = ResolvePieceName(names, nodeIndex); - var parentIndex = node.ParentIndexOrMinusOne; - pieces.Add(new ViewportPiece( id: nodeIndex, name: name, mesh: meshBuildResult.Mesh, - localTransform: Matrix4x4.Identity, + localTransform: restPose.MeshSpaceTransform * mshToViewportTransform, boundsMin: meshBuildResult.BoundsMin, boundsMax: meshBuildResult.BoundsMax, debugInfo: new ViewportPieceDebugInfo { SourceKind = "MSH 0x01 piece", SourcePieceIndex = nodeIndex, - SourceParentIndex = parentIndex, + SourceParentIndex = restPose.ParentIndex, GeometrySlotIndex = slotIndex, Msh01Flags = (uint)node.Flags, BatchCount = meshBuildResult.BatchCount, - TriangleCount = meshBuildResult.TriangleCount + TriangleCount = meshBuildResult.TriangleCount, + FallbackKeyframeIndex = restPose.FallbackKeyframeIndex, + HasRestPose = restPose.HasFallbackPose })); } @@ -135,9 +139,19 @@ public static class MshViewportLoader if (!IsValidTriangle(positions.Count, vertexIndex0, vertexIndex1, vertexIndex2)) continue; - AddTriangleVertex(positions[vertexIndex0], color, vertices, outIndices, ref boundsMin, ref boundsMax); - AddTriangleVertex(positions[vertexIndex1], color, vertices, outIndices, ref boundsMin, ref boundsMax); - AddTriangleVertex(positions[vertexIndex2], color, vertices, outIndices, ref boundsMin, ref boundsMax); + var p0 = ToNumericsVector3(positions[vertexIndex0]); + var p1 = ToNumericsVector3(positions[vertexIndex1]); + var p2 = ToNumericsVector3(positions[vertexIndex2]); + + var normal = Vector3.Cross(p1 - p0, p2 - p0); + if (normal.LengthSquared() < 1e-8f) + normal = Vector3.UnitY; + else + normal = Vector3.Normalize(normal); + + AddTriangleVertex(p0, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); + AddTriangleVertex(p1, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); + AddTriangleVertex(p2, color, normal, vertices, outIndices, ref boundsMin, ref boundsMax); triangleCount++; } } @@ -148,14 +162,21 @@ public static class MshViewportLoader var mesh = PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles); return new PieceMeshBuildResult(mesh, boundsMin, boundsMax, batchCount, triangleCount); } + + private static Vector3 ToNumericsVector3(Common.Vector3 position) + { + return new Vector3(position.X, position.Y, position.Z); + } private static void AddTriangleVertex( - Common.Vector3 position, + Vector3 position, Vector3 color, + Vector3 normal, List vertices, List indices, ref Vector3 boundsMin, - ref Vector3 boundsMax) + ref Vector3 boundsMax + ) { var vertexIndex = (uint)(vertices.Count / 6); @@ -165,6 +186,9 @@ public static class MshViewportLoader vertices.Add(color.X); vertices.Add(color.Y); vertices.Add(color.Z); + vertices.Add(normal.X); + vertices.Add(normal.Y); + vertices.Add(normal.Z); indices.Add(vertexIndex); diff --git a/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs b/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs index 7472a6c..04ba547 100644 --- a/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs +++ b/NResUI/Rendering/Viewport/OpenGL/ShaderProgram.cs @@ -49,6 +49,11 @@ public sealed unsafe class ShaderProgram _gl.UniformMatrix4(location, 1, false, (float*)&matrix); } + public void SetVector3(int location, Vector3 value) + { + _gl.Uniform3(location, value.X, value.Y, value.Z); + } + public void SetVector4(int location, Vector4 value) { _gl.Uniform4(location, value.X, value.Y, value.Z, value.W); diff --git a/NResUI/Rendering/Viewport/ViewportPieceDebugInfo.cs b/NResUI/Rendering/Viewport/ViewportPieceDebugInfo.cs index dd1c07c..3e6d6b4 100644 --- a/NResUI/Rendering/Viewport/ViewportPieceDebugInfo.cs +++ b/NResUI/Rendering/Viewport/ViewportPieceDebugInfo.cs @@ -9,5 +9,8 @@ public sealed class ViewportPieceDebugInfo public int BatchCount { get; init; } public int TriangleCount { get; init; } + public int FallbackKeyframeIndex { get; init; } = -1; + public bool HasRestPose { get; init; } + public string SourceKind { get; init; } = "MSH"; } diff --git a/NResUI/Rendering/Viewport/ViewportRenderer.cs b/NResUI/Rendering/Viewport/ViewportRenderer.cs index 913f448..b1bdc11 100644 --- a/NResUI/Rendering/Viewport/ViewportRenderer.cs +++ b/NResUI/Rendering/Viewport/ViewportRenderer.cs @@ -20,7 +20,10 @@ public sealed class ViewportRenderer private GpuMesh? _unitWireBoxMesh; private GpuMesh? _axesMesh; - private int _meshMvpLocation; + private int _modelLocation; + private int _mvpLocation; + private int _lightDirectionLocation; + private int _outlineMvpLocation; private int _outlineColorLocation; @@ -44,8 +47,7 @@ public sealed class ViewportRenderer _framebuffer.BindForRender(); _gl.Enable(EnableCap.DepthTest); - _gl.Enable(EnableCap.CullFace); - _gl.CullFace(TriangleFace.Back); + _gl.Disable(EnableCap.CullFace); _gl.ClearColor(0.12f, 0.13f, 0.15f, 1.0f); _gl.ClearStencil(0); @@ -66,7 +68,6 @@ public sealed class ViewportRenderer _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _framebuffer.Resolve(); - _gl.Disable(EnableCap.CullFace); _gl.Disable(EnableCap.DepthTest); _gl.Disable(EnableCap.StencilTest); _gl.UseProgram(0); @@ -96,14 +97,12 @@ public sealed class ViewportRenderer var mvp = model * view * projection; _gl.Disable(EnableCap.StencilTest); - _gl.Disable(EnableCap.CullFace); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _meshShader.Use(); - _meshShader.SetMatrix4(_meshMvpLocation, mvp); + _meshShader.SetMatrix4(_mvpLocation, mvp); grid.Mesh.Draw(); - _gl.Enable(EnableCap.CullFace); } private void DrawScene( @@ -166,7 +165,6 @@ public sealed class ViewportRenderer throw new InvalidOperationException("Viewport debug resources are not initialized."); _gl.Disable(EnableCap.StencilTest); - _gl.Disable(EnableCap.CullFace); _gl.Disable(EnableCap.DepthTest); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _gl.LineWidth(2.0f); @@ -203,7 +201,6 @@ public sealed class ViewportRenderer _gl.LineWidth(1.0f); _gl.Enable(EnableCap.DepthTest); - _gl.Enable(EnableCap.CullFace); } private void DrawPiece( @@ -248,8 +245,13 @@ public sealed class ViewportRenderer var mvp = model * view * projection; + var lightDirection = Vector3.Normalize(new Vector3(-0.35f, -0.75f, -0.55f)); + _meshShader.Use(); - _meshShader.SetMatrix4(_meshMvpLocation, mvp); + _meshShader.SetMatrix4(_mvpLocation, mvp); + _meshShader.SetMatrix4(_modelLocation, model); + _meshShader.SetVector3(_lightDirectionLocation, lightDirection); + mesh.Draw(); } @@ -276,30 +278,49 @@ public sealed class ViewportRenderer { const string meshVertexShaderSource = """ #version 330 core - + layout (location = 0) in vec3 aPosition; layout (location = 1) in vec3 aColor; - + layout (location = 2) in vec3 aNormal; + + uniform mat4 uModel; uniform mat4 uMvp; - + out vec3 vColor; - + out vec3 vNormalWorld; + void main() { vColor = aColor; + vNormalWorld = mat3(transpose(inverse(uModel))) * aNormal; gl_Position = uMvp * vec4(aPosition, 1.0); } """; const string meshFragmentShaderSource = """ #version 330 core - + in vec3 vColor; + in vec3 vNormalWorld; + + uniform vec3 uLightDirectionWorld; + out vec4 FragColor; - + void main() { - FragColor = vec4(vColor, 1.0); + vec3 normal = normalize(vNormalWorld); + + // Useful when backface display is enabled. + if (!gl_FrontFacing) + normal = -normal; + + vec3 lightDir = normalize(-uLightDirectionWorld); + + float diffuse = max(dot(normal, lightDir), 0.0); + float lighting = 0.35 + diffuse * 0.65; + + FragColor = vec4(vColor * lighting, 1.0); } """; @@ -329,9 +350,13 @@ public sealed class ViewportRenderer """; _meshShader = new ShaderProgram(_gl, meshVertexShaderSource, meshFragmentShaderSource, "Viewport mesh"); - _outlineShader = new ShaderProgram(_gl, outlineVertexShaderSource, outlineFragmentShaderSource, "Viewport outline"); + _outlineShader = new ShaderProgram(_gl, outlineVertexShaderSource, outlineFragmentShaderSource, + "Viewport outline"); + + _modelLocation = _meshShader.GetUniformLocation("uModel"); + _mvpLocation = _meshShader.GetUniformLocation("uMvp"); + _lightDirectionLocation = _meshShader.GetUniformLocation("uLightDirectionWorld"); - _meshMvpLocation = _meshShader.GetUniformLocation("uMvp"); _outlineMvpLocation = _outlineShader.GetUniformLocation("uMvp"); _outlineColorLocation = _outlineShader.GetUniformLocation("uColor"); }