using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace OpenVIII { public class Debug_MCH { static float MODEL_SCALE = 20f; private const float TEX_SIZEW = 256f; private const float TEX_SIZEH = 256f; private Vector2[] textureSizes; private uint pBase; private MemoryStream ms; private BinaryReader br; [StructLayout(LayoutKind.Sequential, Size =64, Pack =1)] private struct Header { public uint cSkeletonBones; public uint cVertices; public uint cTexAnimations; public uint cFaces; public uint cUnk; public uint cSkinObjects; public uint Unk; public ushort cTris; public ushort cQuads; public uint pBones; public uint pVertices; public uint pTexAnimations; public uint pFaces; public uint pUnk; public uint pSkinObjects; public uint pAnimation; public uint Unk2; } /// /// Main anim struct. Model can contain AnimationEntry[] animations, which hold AnimFrame[] frames /// private struct Animation { public uint cAnimations; public AnimationEntry[] animations; } /// /// Animation struct- it holds all available animation frame keypoints for selected animation /// private struct AnimationEntry { public uint cAnimFrames; public AnimFrame[] animationFrames; } private struct AnimFrame { private Vector3 Bone0pos; public Vector3[] vecRot; public Matrix[] matrixRot; public Vector3 bone0pos { get => Bone0pos*.01f; set => Bone0pos = value; } } private struct Skeleton { public Bone[] bones; public SkinData[] skins; } [StructLayout(LayoutKind.Sequential, Size = 0x40, Pack = 1)] private struct Bone { public ushort parentBone; public ushort unk; public uint unk2; public short size; [MarshalAs(UnmanagedType.ByValArray, SizeConst =54)] public byte[] unkBuffer; public float GetSize() => size / MODEL_SCALE; } [StructLayout(LayoutKind.Sequential, Size = 64, Pack = 1)] private struct Face { public int polygonType; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] unk; public short unknown; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public byte[] unk2; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public short[] verticesA; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public short[] verticesB; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public int[] vertColor; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public TextureMap[] TextureMap; public ushort padding; public ushort texIndex; public ulong padding2; public bool BIsQuad => polygonType == 0x2d010709; } [StructLayout(LayoutKind.Sequential, Size = 2, Pack = 1)] private struct TextureMap { public byte u; public byte v; } private struct SkinData { public short vertIndex; public short cVerts; public short boneId; public short unk; } /// /// This is final combination of skin+vertices data. You query the class as the face is querying ABCD, but it has boneId paired to it /// private struct GroupedVertices { public byte boneId; public Vector3 vertex; } private Header header; private Animation animation; private Skeleton skeleton; private Face[] faces; private Vector4[] vertices; private GroupedVertices[] gVertices; public enum mchMode { World, FieldMain, FieldNPC } mchMode currentMchMode; public Debug_MCH(MemoryStream ms, BinaryReader br,mchMode mchMode = mchMode.World,float modelScale = 20f) { this.ms = ms; this.br = br; this.currentMchMode = mchMode; MODEL_SCALE = modelScale; pBase = (uint)ms.Position; header = Extended.ByteArrayToStructure
(br.ReadBytes(64)); if (header.Unk != 0) { ms.Seek(0, SeekOrigin.End); //rewind to end return; } ReadGeometry(); ReadSkeleton(); ReadAnimation(); PairSkinWithVertex(); } /// /// to be used for VRAM atlases- it provides the texture sizes relative to texIndexes in GetVertexPositions. If not called then default 256x256 range is used /// /// /// public void AssignTextureSizes(Texture2D[] textures, int[] textureIndexes) { textureSizes = new Vector2[textureIndexes.Length]; for (int i = 0; i < textureIndexes.Length; i++) textureSizes[i] = new Vector2(textures[textureIndexes[i]].Width, textures[textureIndexes[i]].Height); } /// /// to be used for VRAM atlases- it provides the texture sizes relative to texIndexes in GetVertexPositions. If not called then default 256x256 range is used /// /// /// public void AssignTextureSizes(TextureHandler[] textures, int[] textureIndexes) { textureSizes = new Vector2[textureIndexes.Length]; for (int i = 0; i < textureIndexes.Length; i++) textureSizes[i] = new Vector2(textures[textureIndexes[i]].ClassicWidth, textures[textureIndexes[i]].ClassicHeight); } public bool bValid() => header.Unk == 0; public uint GetAnimationCount() => animation.cAnimations; public uint GetAnimationFramesCount(int animId) => animation.animations[animId].cAnimFrames; /// /// as index data for skinning boneId is not in the same place as vertices, therefore we create sorted buffer with Vertex and their boneId /// private void PairSkinWithVertex() { gVertices = new GroupedVertices[header.cVertices]; int innerIndex = 0; for(int i = 0; i ms.Length) return; //error handler skeleton = new Skeleton(); skeleton.bones = new Bone[header.cSkeletonBones]; for (int i = 0; i < header.cSkeletonBones; i++) skeleton.bones[i] = Extended.ByteArrayToStructure(br.ReadBytes(64)); ReadSkinning(); return; } private void ReadSkinning() { ms.Seek(pBase + header.pSkinObjects, SeekOrigin.Begin); if (ms.Position > ms.Length) return; //error handler skeleton.skins = new SkinData[header.cSkinObjects]; for (int i = 0; i < header.cSkinObjects; i++) skeleton.skins[i] = Extended.ByteArrayToStructure(br.ReadBytes(8)); return; } private void ReadGeometry() { ms.Seek(pBase + header.pVertices, SeekOrigin.Begin); if (ms.Position > ms.Length || header.pVertices+ms.Position > ms.Length) //pvert error handler return; //error handler vertices = new Vector4[header.cVertices]; for (int i = 0; i < vertices.Length; i++) vertices[i] = new Vector4(br.ReadInt16(), -br.ReadInt16(), br.ReadInt16(), br.ReadInt16()); //change second to -Y because of warped geom ms.Seek(pBase + header.pFaces, SeekOrigin.Begin); List face = new List(); for(int i = 0; i(br.ReadBytes(64))); faces = face.ToArray(); return; } /// /// Method to parse available binary data to "animation" structure and calculates the final Matrix /// if true- then ms and br are custom/modified externally /// private void ReadAnimation(bool bIndependentStream = false) { if(!bIndependentStream) //if normal parsing, then jump to animation pointer ms.Seek(pBase + header.pAnimation, SeekOrigin.Begin); //if not, then do nothing- ms will point to animation buffer if (ms.Position > ms.Length) return; //error handler ushort animationCount = br.ReadUInt16(); int innerIndex = 0; animation = new Animation() { cAnimations = animationCount, animations= new AnimationEntry[animationCount] }; while (animationCount > 0) { ushort animationFramesCount = br.ReadUInt16(); ushort cBones = br.ReadUInt16(); if (animationFramesCount * cBones >= ms.Length || cBones == 0 || cBones>100) { Console.WriteLine($"Debug_MCH: Error at ReadAnimation()- animFrameCount was {animationFramesCount} and cBones were {cBones}, but that's more than file size!"); break; } animation.animations[innerIndex] = new AnimationEntry() { cAnimFrames = animationFramesCount, animationFrames = new AnimFrame[animationFramesCount] }; List animKeypoints = new List(); while (animationFramesCount > 0) { AnimFrame keyPoint = new AnimFrame() { bone0pos= new Vector3(x:br.ReadInt16(),z:-br.ReadInt16(), y:br.ReadInt16())}; Vector3[] vetRot = new Vector3[cBones]; Matrix[] matrixRot = new Matrix[cBones]; for (int i = 0; i < cBones; i++) { short x, y, z; if (currentMchMode == mchMode.World) { x = br.ReadInt16(); y = br.ReadInt16(); z = br.ReadInt16(); } else //Field NPC dataset - s16 4bytes simplified { byte rot1 = br.ReadByte(); byte rot2 = br.ReadByte(); byte rot3 = br.ReadByte(); byte rot4 = br.ReadByte(); x = (short)(rot1 << 2 | (rot4 >> 2 * 0 & 3) << 10); y = (short)(rot2 << 2 | (rot4 >> 2 * 1 & 3) << 10); z = (short)(rot3 << 2 | (rot4 >> 2 * 2 & 3) << 10); } Vector3 shortVector = new Vector3() { X = -y, Y = -x, Z = -z }; vetRot[i] = Extended.S16VectorToFloat(shortVector) * 360f; } animationFramesCount--; keyPoint.vecRot = vetRot; keyPoint.matrixRot = matrixRot; animKeypoints.Add(keyPoint); } animationCount--; animation.animations[innerIndex].animationFrames = animKeypoints.ToArray(); innerIndex++; } CalculateBoneMatrix(); return; } private void CalculateBoneMatrix() { for (int animId = 0; animId < animation.animations.Length; animId++) for (int frameId = 0; frameId < animation.animations[animId].cAnimFrames; frameId++) for (int boneId = 0; boneId < skeleton.bones.Length; boneId++) { var boneRotation = animation.animations[animId].animationFrames[frameId].vecRot[boneId]; Matrix xRot = Extended.GetRotationMatrixX(-boneRotation.X); Matrix yRot = Extended.GetRotationMatrixY(-boneRotation.Y); Matrix zRot = Extended.GetRotationMatrixZ(-boneRotation.Z); var MatrixZ = Extended.MatrixMultiply_transpose(yRot, xRot); MatrixZ = Extended.MatrixMultiply_transpose(zRot, MatrixZ); if (skeleton.bones[boneId].parentBone == 0) //if parentId is 0 then the current bone is core aka bone0 { MatrixZ.M41 = animation.animations[animId].animationFrames[frameId].bone0pos.X; MatrixZ.M42 = animation.animations[animId].animationFrames[frameId].bone0pos.Y; //up/down MatrixZ.M43 = animation.animations[animId].animationFrames[frameId].bone0pos.Z; MatrixZ.M44 = 1; } else { Matrix parentBone = animation.animations[animId].animationFrames[frameId].matrixRot[skeleton.bones[boneId].parentBone-1]; //gets the parent bone MatrixZ.M43 = skeleton.bones[skeleton.bones[boneId].parentBone-1].GetSize(); Matrix rMatrix = Matrix.Multiply(parentBone, MatrixZ); rMatrix.M41 = parentBone.M11 * MatrixZ.M41 + parentBone.M12 * MatrixZ.M42 + parentBone.M13 * MatrixZ.M43 + parentBone.M41; rMatrix.M42 = parentBone.M21 * MatrixZ.M41 + parentBone.M22 * MatrixZ.M42 + parentBone.M23 * MatrixZ.M43 + parentBone.M42; rMatrix.M43 = parentBone.M31 * MatrixZ.M41 + parentBone.M32 * MatrixZ.M42 + parentBone.M33 * MatrixZ.M43 + parentBone.M43; rMatrix.M44 = 1; MatrixZ = rMatrix; } animation.animations[animId].animationFrames[frameId].matrixRot[boneId] = MatrixZ; } } //public int xa = 0; //private static float xb = 0f; //private static float cx = 0f; /// /// [WIP] - this method should return vertices based on animation/skeleton, not 'as-is' /// /// abs X Y Z position to draw model /// absolute index of animation 0-based /// index of animation frame- 0-based, length vary /// Tuple{item1= VertexPositionColorTexture; item2= clutIndex public Tuple GetVertexPositions(Vector3 position, Quaternion rotation, int animationId, int animationFrame) { List facesVertices = new List(); List texIndexes = new List(); for(int i = 0; i x.x); //var max = a.Max(x => x.x); //var mina = a.Min(x => x.y); //var maxb = a.Max(x => x.y); return new Tuple(facesVertices.ToArray(), texIndexes.ToArray()); } private Vector3 CalculateFinalVertex(GroupedVertices groupedVertex, int animationId, int animationFrame) { var vertex = groupedVertex.vertex / MODEL_SCALE; Matrix faceMatrix = animation.animations[animationId].animationFrames[animationFrame].matrixRot[groupedVertex.boneId]; Vector3 face = new Vector3( faceMatrix.M11 * vertex.X + faceMatrix.M41 + faceMatrix.M12 * vertex.Z + faceMatrix.M13 * -vertex.Y, faceMatrix.M21 * vertex.X + faceMatrix.M42 + faceMatrix.M22 * vertex.Z + faceMatrix.M23 * -vertex.Y, faceMatrix.M31 * vertex.X + faceMatrix.M43 + faceMatrix.M32 * vertex.Z + faceMatrix.M33 * -vertex.Y ); return face; } /// /// This function takes animations data as input and merges it to current Mch instance. /// This is mandatory for main characters in fields, as their base does not contain animations /// /// public void MergeAnimations(MemoryStream ms, BinaryReader br) { this.ms = ms; this.br = br; ReadAnimation(true); } } }