Пример #1
0
        //We're not realistically going to fully convert everything, but we can get vertex data and bones if nothing else
        //Returns an aqp ready for the ConvertToNGSPSO2Mesh method
        public static AquaObject ReadAXS(string filePath, out AquaNode aqn)
        {
            AquaObject aqp = new NGSAquaObject();

            aqn = new AquaNode();

            using (Stream stream = (Stream) new FileStream(filePath, FileMode.Open))
                using (var streamReader = new BufferedStreamReader(stream, 8192))
                {
                    Debug.WriteLine(Path.GetFileName(filePath));
                    long                            last__oaPos      = 0;
                    eertStruct                      eertNodes        = null;
                    ipnbStruct                      tempLpnbList     = null;
                    List <ffubStruct>               ffubList         = new List <ffubStruct>();
                    List <XgmiStruct>               xgmiList         = new List <XgmiStruct>();
                    List <string>                   texNames         = new List <string>();
                    List <MeshDefinitions>          meshDefList      = new List <MeshDefinitions>();
                    List <stamData>                 stamList         = new List <stamData>();
                    Dictionary <string, rddaStruct> rddaList         = new Dictionary <string, rddaStruct>();
                    Dictionary <string, rddaStruct> imgRddaList      = new Dictionary <string, rddaStruct>();
                    Dictionary <string, rddaStruct> vertRddaList     = new Dictionary <string, rddaStruct>();
                    Dictionary <string, rddaStruct> faceRddaList     = new Dictionary <string, rddaStruct>();
                    Dictionary <string, int>        xgmiIdByCombined = new Dictionary <string, int>();
                    Dictionary <string, int>        xgmiIdByUnique   = new Dictionary <string, int>();
                    ffubStruct                      imgFfub          = new ffubStruct();
                    ffubStruct                      vertFfub         = new ffubStruct();
                    ffubStruct                      faceFfub         = new ffubStruct();

                    var fType = streamReader.Read <int>();

                    var fsaLen = streamReader.Read <int>();
                    streamReader.Seek(0x8, SeekOrigin.Current);
                    //Go to Vert definition, node, material, and misc data
                    while (streamReader.Position() < fsaLen)
                    {
                        var tag  = streamReader.Peek <int>();
                        var test = Encoding.UTF8.GetString(BitConverter.GetBytes(tag));
                        Debug.WriteLine(streamReader.Position().ToString("X"));
                        Debug.WriteLine(test);
                        switch (tag)
                        {
                        case __oa:
                            last__oaPos = streamReader.Position();
                            streamReader.Seek(0xD0, SeekOrigin.Current);
                            break;

                        case FIA:
                            streamReader.Seek(0x10, SeekOrigin.Current);
                            break;

                        case __lm:
                            var stam = streamReader.ReadLM();
                            if (stam != null && stam.Count > 0)
                            {
                                stamList = stam;
                            }
                            break;

                        case __bm:
                            streamReader.ReadBM(meshDefList, tempLpnbList, stamList, last__oaPos);
                            break;

                        case lpnb:
                            tempLpnbList = streamReader.ReadIpnb();
                            break;

                        case eert:
                            eertNodes = streamReader.ReadEert();
                            break;

                        //case ssem:
                        //  streamReader.SkipBasicAXSStruct(); //Maybe use for material data later. Remember to store ordered id for _bm mesh entries for this
                        // break;
                        case Xgmi:
                            var xgmiData = streamReader.ReadXgmi();
                            if (!xgmiIdByCombined.ContainsKey(xgmiData.stamCombinedId))
                            {
                                xgmiIdByCombined.Add(xgmiData.stamCombinedId, xgmiList.Count);
                            }
                            xgmiIdByUnique.Add(xgmiData.stamUniqueId, xgmiList.Count);
                            xgmiList.Add(xgmiData);
                            break;

                        default:
                            streamReader.SkipBasicAXSStruct();
                            break;
                        }
                    }

                    //Assemble aqn from eert
                    if (eertNodes != null)
                    {
                        for (int i = 0; i < eertNodes.boneCount; i++)
                        {
                            var       rttaNode = eertNodes.rttaList[i];
                            Matrix4x4 mat      = Matrix4x4.Identity;

                            mat *= Matrix4x4.CreateScale(rttaNode.scale);

                            var rotation = Matrix4x4.CreateFromQuaternion(rttaNode.quatRot);

                            mat *= rotation;

                            mat *= Matrix4x4.CreateTranslation(rttaNode.pos);

                            var parentId = rttaNode.parentNodeId;

                            //If there's a parent, multiply by it
                            if (i != 0)
                            {
                                if (rttaNode.parentNodeId == -1)
                                {
                                    parentId = 0;
                                }
                                var pn           = aqn.nodeList[parentId];
                                var parentInvTfm = new Matrix4x4(pn.m1.X, pn.m1.Y, pn.m1.Z, pn.m1.W,
                                                                 pn.m2.X, pn.m2.Y, pn.m2.Z, pn.m2.W,
                                                                 pn.m3.X, pn.m3.Y, pn.m3.Z, pn.m3.W,
                                                                 pn.m4.X, pn.m4.Y, pn.m4.Z, pn.m4.W);
                                Matrix4x4.Invert(parentInvTfm, out var invParentInvTfm);
                                mat = mat * invParentInvTfm;
                            }
                            else
                            {
                                parentId = -1;
                            }
                            rttaNode.nodeMatrix = mat;

                            //Create AQN node
                            NODE aqNode = new NODE();
                            aqNode.animatedFlag = 1;
                            aqNode.parentId     = parentId;
                            aqNode.unkNode      = -1;
                            aqNode.pos          = rttaNode.pos;
                            aqNode.eulRot       = QuaternionToEuler(rttaNode.quatRot);

                            if (Math.Abs(aqNode.eulRot.Y) > 120)
                            {
                                aqNode.scale = new Vector3(-1, -1, -1);
                            }
                            else
                            {
                                aqNode.scale = new Vector3(1, 1, 1);
                            }
                            Matrix4x4.Invert(mat, out var invMat);
                            aqNode.m1       = new Vector4(invMat.M11, invMat.M12, invMat.M13, invMat.M14);
                            aqNode.m2       = new Vector4(invMat.M21, invMat.M22, invMat.M23, invMat.M24);
                            aqNode.m3       = new Vector4(invMat.M31, invMat.M32, invMat.M33, invMat.M34);
                            aqNode.m4       = new Vector4(invMat.M41, invMat.M42, invMat.M43, invMat.M44);
                            aqNode.boneName = rttaNode.nodeName;
                            Debug.WriteLine($"{i} " + aqNode.boneName.GetString());
                            aqn.nodeList.Add(aqNode);
                        }
                    }


                    //Go to mesh buffers
                    streamReader.Seek(fsaLen, SeekOrigin.Begin);
                    if (streamReader.Position() >= stream.Length)
                    {
                        return(null);
                    }

                    var fType2 = streamReader.Read <int>();
                    //Read mesh data
                    if (fType2 != FMA)
                    {
                        Debug.WriteLine("Unexpected struct in location of FMA!");
                        return(null);
                    }
                    streamReader.Seek(0xC, SeekOrigin.Current);

                    //Skip daeh
                    int meshSettingLen = streamReader.ReadDAEH();

                    //Read ffub and rdda
                    //Count mesh count here for now and store starts and ends of data
                    long meshSettingStart = streamReader.Position();
                    while (streamReader.Position() < meshSettingStart + meshSettingLen)
                    {
                        streamReader.ReadFFUBorRDDA(ffubList, rddaList, imgRddaList, vertRddaList, faceRddaList, ref imgFfub, ref vertFfub, ref faceFfub);
                    }

                    int meshCount = meshDefList.Count;

                    //Read image data
                    var ext = Path.GetExtension(filePath);
                    for (int i = 0; i < xgmiList.Count; i++)
                    {
                        var xgmiData      = xgmiList[i];
                        var imgBufferInfo = imgRddaList[$"{xgmiData.md5_1.ToString("X")}{xgmiData.md5_2.ToString("X")}"];
                        Debug.WriteLine($"Image set {i}: " + imgBufferInfo.md5_1.ToString("X") + " " + imgBufferInfo.dataStartOffset.ToString("X") + " " + imgBufferInfo.toTagStruct.ToString("X") + " " + (meshSettingStart + imgFfub.dataStartOffset + imgBufferInfo.dataStartOffset).ToString("X"));

                        var position     = meshSettingStart + imgFfub.dataStartOffset + imgBufferInfo.dataStartOffset;
                        var buffer       = streamReader.ReadBytes(position, imgBufferInfo.dataSize);
                        var outImagePath = filePath.Replace(ext, $"_tex_{i}" + ".dds");
                        texNames.Add(Path.GetFileName(outImagePath));
                        try
                        {
                            string name = Path.GetFileName(filePath);
                            Debug.WriteLine($"{name}_xgmi_{ i}");
                            var image = AIFMethods.GetImage(xgmiData, buffer);
                            File.WriteAllBytes(filePath.Replace(ext, $"_tex_{i}" + ".dds"), image);
                        }
                        catch (Exception exc)
                        {
#if DEBUG
                            string name = Path.GetFileName(filePath);
                            Debug.WriteLine($"Extract tex {i} failed.");
                            File.WriteAllBytes($"C:\\{name}_xgmiHeader_{i}.bin", xgmiData.GetBytes());
                            File.WriteAllBytes($"C:\\{name}_xgmiBuffer_{i}.bin", buffer);
                            Debug.WriteLine(exc.Message);
#endif
                        }
                        buffer = null;
                    }


                    //Read model data - Since ffubs are initialized, they default to 0.
                    int vertFfubPadding = imgFfub.structSize;
                    int faceFfubPadding = imgFfub.structSize + vertFfub.structSize;

                    for (int i = 0; i < meshCount; i++)
                    {
                        var mesh       = meshDefList[i];
                        var nodeMatrix = Matrix4x4.Identity;
                        for (int bn = 0; bn < eertNodes.boneCount; bn++)
                        {
                            var node = eertNodes.rttaList[bn];
                            if (node.meshNodePtr == mesh.oaPos)
                            {
                                nodeMatrix = node.nodeMatrix;
                                break;
                            }
                        }
                        var vertBufferInfo = vertRddaList[$"{mesh.salvStr.md5_1.ToString("X")}{mesh.salvStr.md5_2.ToString("X")}"];
                        var faceBufferInfo = faceRddaList[$"{mesh.lxdiStr.md5_1.ToString("X")}{mesh.lxdiStr.md5_2.ToString("X")}"];
                        Debug.WriteLine($"Vert set {i}: " + vertBufferInfo.md5_1.ToString("X") + " " + vertBufferInfo.dataStartOffset.ToString("X") + " " + vertBufferInfo.toTagStruct.ToString("X") + " " + (meshSettingStart + vertFfubPadding + vertFfub.dataStartOffset + vertBufferInfo.dataStartOffset).ToString("X"));
                        Debug.WriteLine($"Face set {i}: " + faceBufferInfo.md5_1.ToString("X") + " " + faceBufferInfo.dataStartOffset.ToString("X") + " " + faceBufferInfo.toTagStruct.ToString("X") + " " + (meshSettingStart + faceFfubPadding + faceFfub.dataStartOffset + faceBufferInfo.dataStartOffset).ToString("X"));

                        //Vert data
                        var             vertCount = vertBufferInfo.dataSize / mesh.salvStr.vertLen;
                        AquaObject.VTXL vtxl      = new AquaObject.VTXL();

                        streamReader.Seek((meshSettingStart + vertFfubPadding + vertFfub.dataStartOffset + vertBufferInfo.dataStartOffset), SeekOrigin.Begin);
                        AquaObjectMethods.ReadVTXL(streamReader, mesh.vtxe, vtxl, vertCount, mesh.vtxe.vertDataTypes.Count);
                        vtxl.convertToLegacyTypes();

                        //Fix vert transforms
                        for (int p = 0; p < vtxl.vertPositions.Count; p++)
                        {
                            vtxl.vertPositions[p] = Vector3.Transform(vtxl.vertPositions[p], nodeMatrix);
                            if (vtxl.vertNormals.Count > 0)
                            {
                                vtxl.vertNormals[p] = Vector3.TransformNormal(vtxl.vertNormals[p], nodeMatrix);
                            }
                        }


                        //Handle bone indices
                        if (mesh.ipnbStr != null && mesh.ipnbStr.shortList.Count > 0)
                        {
                            vtxl.bonePalette = (mesh.ipnbStr.shortList.ConvertAll(delegate(short num) {
                                return((ushort)num);
                            }));

                            //Convert the indices based on the global bone list as pso2 will expect
                            for (int bn = 0; bn < vtxl.bonePalette.Count; bn++)
                            {
                                vtxl.bonePalette[bn] = (ushort)mesh.lpnbStr.shortList[vtxl.bonePalette[bn]];
                            }
                        }

                        aqp.vtxlList.Add(vtxl);

                        //Face data
                        AquaObject.GenericTriangles genMesh = new AquaObject.GenericTriangles();

                        int        faceIndexCount = faceBufferInfo.dataSize / 2;
                        List <int> faceList       = new List <int>();

                        streamReader.Seek((meshSettingStart + faceFfubPadding + faceFfub.dataStartOffset + faceBufferInfo.dataStartOffset), SeekOrigin.Begin);
                        int maxStep = streamReader.Read <ushort>();
                        for (int fId = 0; fId < faceIndexCount - 1; fId++)
                        {
                            faceList.Add(streamReader.Read <ushort>());
                        }

                        //Convert the data to something usable with this algorithm and then destripify it.
                        List <ushort> triList = unpackInds(inverseWatermarkTransform(faceList, maxStep)).ConvertAll(delegate(int num) {
                            return((ushort)num);
                        });
                        var tempFaceData = new AquaObject.stripData()
                        {
                            triStrips = triList, format0xC33 = true, triIdCount = triList.Count
                        };
                        genMesh.triList = tempFaceData.GetTriangles();

                        //Extra
                        genMesh.vertCount = vertCount;
                        genMesh.matIdList = new List <int>(new int[genMesh.triList.Count]);
                        for (int j = 0; j < genMesh.matIdList.Count; j++)
                        {
                            genMesh.matIdList[j] = aqp.tempMats.Count;
                        }
                        aqp.tempTris.Add(genMesh);

                        //Material
                        var mat = new AquaObject.GenericMaterial();
                        mat.texNames = GetTexNames(mesh, xgmiIdByCombined, xgmiIdByUnique, texNames);
                        aqp.tempMats.Add(mat);
                    }

                    return(aqp);
                }
        }
        //Takes in bytes of a *n.rel file from PSO
        //To convert to PSO2's units, we set the scale to 1/10th scale
        public PSONRelConvert(byte[] file, string fileName = null, float scale = 0.1f, string outFolder = null)
        {
            fileSize  = file.Length;
            rootScale = scale;
            List <dSection> dSections = new List <dSection>();

            streamReader = new BufferedStreamReader(new MemoryStream(file), 8192);

            //Get header offset
            streamReader.Seek(file.Length - 0x10, SeekOrigin.Begin);

            //Check Endianness. No offset should ever come close to half of the int max value.
            be = streamReader.PeekBigEndianPrimitiveUInt32() < streamReader.Peek <uint>();
            if (be)
            {
                MessageBox.Show("Sorry, Gamecube n.rel files are not supported at this time.");
            }
            uint tableOfs = streamReader.ReadBE <uint>(be);

            //Read header
            streamReader.Seek(tableOfs, SeekOrigin.Begin);
            var header = ReadRelHeader(streamReader, be);

            //Read draw Sections
            streamReader.Seek(header.drawOffset, SeekOrigin.Begin);
            for (int i = 0; i < header.drawCount; i++)
            {
                dSection section = new dSection();
                section.id  = streamReader.ReadBE <int>(be);
                section.pos = streamReader.ReadBEV3(be);
                var rotX = streamReader.ReadBE <int>(be);
                var rotY = streamReader.ReadBE <int>(be);
                var rotZ = streamReader.ReadBE <int>(be);
                section.rot            = new Vector3((float)(rotX * BAMSvalue), (float)(rotY * BAMSvalue), (float)(rotZ * BAMSvalue));
                section.radius         = streamReader.ReadBE <float>(be);
                section.staticOffset   = streamReader.ReadBE <uint>(be);
                section.animatedOffset = streamReader.ReadBE <uint>(be);
                section.staticCount    = streamReader.ReadBE <uint>(be);
                section.animatedCount  = streamReader.ReadBE <uint>(be);
                section.end            = streamReader.ReadBE <uint>(be);

                dSections.Add(section);
            }

            //Get texture names
            streamReader.Seek(header.nameInfoOffset, SeekOrigin.Begin);
            var nameOffset = streamReader.ReadBE <uint>(be);
            var nameCount  = streamReader.ReadBE <uint>(be);

            streamReader.Seek(nameOffset, SeekOrigin.Begin);
            List <uint> nameOffsets = new List <uint>();

            for (int i = 0; i < nameCount; i++)
            {
                nameOffsets.Add(streamReader.ReadBE <uint>(be));
                var unk0 = streamReader.ReadBE <uint>(be);
                var unk1 = streamReader.ReadBE <uint>(be);

                if (unk0 != 0)
                {
                    Console.WriteLine($"Iteration {i} unk0 == {unk0}");
                }
                if (unk1 != 0)
                {
                    Console.WriteLine($"Iteration {i} unk1 == {unk1}");
                }
            }
            foreach (uint offset in nameOffsets)
            {
                streamReader.Seek(offset, SeekOrigin.Begin);
                texNames.Add(AquaObjectMethods.ReadCString(streamReader));
            }

            //If there's an .xvm, dump that too with texture names from the .rel
            if (fileName != null)
            {
                //Naming patterns for *n.rel files are *_12n.rel for example or *n.rel  vs *.xvm. We can determine which we have, edit, and proceed
                var    basename = fileName.Substring(0, fileName.Length - 5);
                string xvmName  = null;

                if (basename.ElementAt(basename.Length - 3) == '_')
                {
                    xvmName = basename.Substring(0, basename.Length - 3) + ".xvm";
                }
                else
                {
                    xvmName = basename + ".xvm";
                }

                ExtractXVM(xvmName, texNames, outFolder);
            }



            //Create root AQN node
            NODE aqNode = new NODE();

            aqNode.animatedFlag = 1;
            aqNode.parentId     = -1;
            aqNode.unkNode      = -1;
            aqNode.pos          = new Vector3();
            aqNode.eulRot       = new Vector3();
            aqNode.scale        = new Vector3(1, 1, 1);
            aqNode.m1           = new Vector4(1, 0, 0, 0);
            aqNode.m2           = new Vector4(0, 1, 0, 0);
            aqNode.m3           = new Vector4(0, 0, 1, 0);
            aqNode.m4           = new Vector4(0, 0, 0, 1);
            aqNode.boneName.SetString("RootNode");
            nodes.Add(aqNode);

            //Loop through nodes and parse geometry
            for (int i = 0; i < dSections.Count; i++)
            {
                var matrix = Matrix4x4.Identity;

                matrix *= Matrix4x4.CreateScale(1, 1, 1);

                var rotation = Matrix4x4.CreateRotationX(dSections[i].rot.X) *
                               Matrix4x4.CreateRotationY(dSections[i].rot.Y) *
                               Matrix4x4.CreateRotationZ(dSections[i].rot.Z);

                matrix *= rotation;

                matrix *= Matrix4x4.CreateTranslation(dSections[i].pos * rootScale);

                //Read static meshes
                List <staticMeshOffset> staticMeshOffsets = new List <staticMeshOffset>();
                streamReader.Seek(dSections[i].staticOffset, SeekOrigin.Begin);
                for (int st = 0; st < dSections[i].staticCount; st++)
                {
                    staticMeshOffsets.Add(ReadStaticMeshOffset(streamReader, be));
                }
                for (int ofs = 0; ofs < staticMeshOffsets.Count; ofs++)
                {
                    streamReader.Seek(staticMeshOffsets[ofs].offset, SeekOrigin.Begin);
                    readNode(matrix, 0);
                }


                //Read animated meshes
                List <animMeshOffset> animatedMeshOffsets = new List <animMeshOffset>();
                streamReader.Seek(dSections[i].animatedOffset, SeekOrigin.Begin);
                for (int st = 0; st < dSections[i].animatedCount; st++)
                {
                    animatedMeshOffsets.Add(ReadAnimMeshOffset(streamReader, be));
                }
                for (int ofs = 0; ofs < animatedMeshOffsets.Count; ofs++)
                {
                    streamReader.Seek(animatedMeshOffsets[ofs].offset, SeekOrigin.Begin);
                    readNode(matrix, 0);
                }
            }

            //Set material names
            for (int i = 0; i < aqObj.tempMats.Count; i++)
            {
                aqObj.tempMats[i].matName = $"PSOMat {i}";
            }
        }
Пример #3
0
        public static void ExportObj(string fileName, AquaObject aqo)
        {
            //We have to split these if this is an NGS model because obj only supports one material per mesh
            if (aqo.objc.type >= 0xC32)
            {
                aqo.splitVSETPerMesh();
            }
            //Running this this way ensures we have normals to grab.
            AquaObjectMethods.ComputeTangentSpace(aqo, false, false);

            //Ensure there's UV data, even if it's not actually used.
            for (int i = 0; i < aqo.vtxlList.Count; i++)
            {
                if (aqo.vtxlList[i].uv1List == null || aqo.vtxlList[i].uv1List.Count == 0)
                {
                    aqo.vtxlList[i].uv1List = new List <Vector2>(new Vector2[aqo.vtxlList[i].vertPositions.Count]);
                }
            }

            var mtlfile = Path.ChangeExtension(fileName, ".mtl");
            var mtls    = ExportMtl(mtlfile, aqo);

            mtlfile = Path.GetFileName(mtlfile);

            using (var w = new StreamWriter(fileName, false, Encoding.UTF8))
            {
                w.WriteLine("# {0}", Path.GetFileName(fileName));
                w.WriteLine("mtllib {0}", Path.GetFileName(mtlfile));
                w.WriteLine("");

                StringBuilder pos   = new StringBuilder();
                StringBuilder nrm   = new StringBuilder();
                StringBuilder tex   = new StringBuilder();
                StringBuilder faces = new StringBuilder();

                var offpos = 1;
                for (int mshId = 0; mshId < aqo.meshList.Count; mshId++)
                {
                    var mesh = aqo.meshList[mshId];
                    foreach (var i in aqo.vtxlList[mesh.vsetIndex].vertPositions)
                    {
                        pos.AppendLine(String.Format("v  {0:F8} {1:F8} {2:F8}", i.X * 100, -i.Z * 100, i.Y * 100));
                    }

                    foreach (var i in aqo.vtxlList[mesh.vsetIndex].vertNormals)
                    {
                        nrm.AppendLine(String.Format("vn  {0:F8} {1:F8} {2:F8}", i.X, -i.Z, i.Y));
                    }

                    foreach (var i in aqo.vtxlList[mesh.vsetIndex].uv1List)
                    {
                        tex.AppendLine(String.Format("vt  {0:F8} {1:F8} {2:F8}", i.X, -i.Y, 0));
                    }

                    var meshName = string.Format("mesh_{0}_{1}_{2}_{3}", mesh.mateIndex, mesh.rendIndex, mesh.shadIndex, mesh.tsetIndex);
                    faces.AppendLine("");
                    faces.AppendLine(String.Format("o {0}", meshName));
                    faces.AppendLine(String.Format("g {0}", meshName));
                    faces.AppendLine(String.Format("usemtl {0}", mtls[mshId]));
                    faces.AppendLine(String.Format("s {0}", 0));

                    //Write faces
                    var tris = aqo.strips[mesh.psetIndex].GetTriangles();
                    foreach (var tri in tris)
                    {
                        faces.AppendLine(String.Format("f {0}/{2}/{1} {3}/{5}/{4} {6}/{8}/{7}",
                                                       tri.X + offpos, tri.X + offpos, tri.X + offpos,
                                                       tri.Y + offpos, tri.Y + offpos, tri.Y + offpos,
                                                       tri.Z + offpos, tri.Z + offpos, tri.Z + offpos));
                    }

                    offpos += aqo.vtxlList[mesh.vsetIndex].vertPositions.Count;
                }
                pos.AppendLine();
                nrm.AppendLine();
                tex.AppendLine();

                w.Write(pos);
                w.Write(nrm);
                w.Write(tex);

                w.Write(faces);

                w.Flush();
                w.BaseStream.SetLength(w.BaseStream.Position);
            }
        }
Пример #4
0
        //Import obj data, gather and reconstruct weighting data, reconstruct model with new geometry data, delete LOD models loaded
        public static AquaObject ImportObj(string fileName, AquaObject aqo)
        {
            bool            doBitangent = false;
            List <SkinData> skinData    = new List <SkinData>();

            AquaObjectMethods.GenerateGlobalBonePalette(aqo);

            //Add a local version of the global bonepalette. We're reworking things so we need to do this.
            for (int i = 0; i < aqo.vtxlList.Count; i++)
            {
                aqo.vtxlList[i].bonePalette = new List <ushort>();
                for (int b = 0; b < aqo.bonePalette.Count; b++)
                {
                    aqo.vtxlList[i].bonePalette.Add((ushort)aqo.bonePalette[b]);
                }
            }

            //Assign skin data for comparison later and make it relativitize it to the GlobalBonePalette
            if (aqo.vtxlList[0].vertWeights.Count > 0)
            {
                foreach (var vtxl in aqo.vtxlList)
                {
                    for (int i = 0; i < vtxl.vertPositions.Count; i++)
                    {
                        SkinData skin = new SkinData();
                        skin.pos = vtxl.vertPositions[i];
                        List <byte> indices = new List <byte>(new byte[4]);
                        for (int id = 0; id < 4; id++)
                        {
                            var temp = (byte)aqo.bonePalette.IndexOf(vtxl.bonePalette[vtxl.vertWeightIndices[i][id]]);
                            if (indices.Contains(temp)) //Repeats should only occur for index 0
                            {
                                temp = 0;
                            }
                            indices[id] = temp;
                        }
                        skin.indices = indices.ToArray();
                        skin.weights = AquaObject.VTXL.SumWeightsTo1(vtxl.vertWeights[i]);
                        skinData.Add(skin);
                    }
                }
            }

            if (aqo.vtxlList[0].vertBinormalList.Count > 0)
            {
                doBitangent = true;
            }

            //House cleaning since we can't really redo these this way
            aqo.tempTris.Clear();
            aqo.strips3.Clear();
            aqo.strips2.Clear();
            aqo.strips.Clear();
            aqo.pset2List.Clear();
            aqo.psetList.Clear();
            aqo.mesh2List.Clear();
            aqo.strips3Lengths.Clear();
            aqo.objc.pset2Count = 0;
            aqo.objc.mesh2Count = 0;

            var obj         = ObjFile.FromFile(fileName);
            var subMeshes   = obj.Meshes.SelectMany(i => i.SubMeshes).ToArray();
            var oldMESHList = aqo.meshList;

            aqo.meshList = new List <AquaObject.MESH>();
            int        totalStripsShorts = 0;
            int        totalVerts        = 0;
            int        boneLimit;
            AquaObject tempModel;

            if (aqo.objc.type >= 0xC32)
            {
                tempModel = new NGSAquaObject();
                boneLimit = 255;
            }
            else
            {
                tempModel = new ClassicAquaObject();
                boneLimit = 16;
            }

            //Assemble face and vertex data. Vert data is stored with faces so that it can be split as needed for proper UV mapping etc.
            foreach (var mesh in subMeshes)
            {
                var tempMesh = new AquaObject.GenericTriangles();
                tempMesh.name        = mesh.Name;
                tempMesh.bonePalette = aqo.bonePalette;
                List <int>            vertIds     = new List <int>();
                Dictionary <int, int> vertIdRemap = new Dictionary <int, int>();
                int greatestId = 0;

                for (int f = 0; f < mesh.PositionFaces.Count; ++f)
                {
                    var pf = mesh.PositionFaces[f];
                    var nf = mesh.NormalFaces[f];
                    var tf = mesh.TexCoordFaces[f];

                    tempMesh.triList.Add(new Vector3(pf.A, pf.B, pf.C)); //faces are 1 based
                    if (!vertIds.Contains(pf.A))
                    {
                        greatestId = pf.A > greatestId ? pf.A : greatestId;
                        vertIds.Add(pf.A);
                    }
                    if (!vertIds.Contains(pf.B))
                    {
                        greatestId = pf.B > greatestId ? pf.B : greatestId;
                        vertIds.Add(pf.B);
                    }
                    if (!vertIds.Contains(pf.C))
                    {
                        greatestId = pf.C > greatestId ? pf.C : greatestId;
                        vertIds.Add(pf.C);
                    }
                    tempMesh.matIdList.Add(0);

                    var vtxl = new AquaObject.VTXL();
                    vtxl.rawVertId = new List <int>()
                    {
                        pf.A, pf.B, pf.C
                    };
                    vtxl.rawFaceId = new List <int>()
                    {
                        f, f, f
                    };

                    //Undo scaling and rotate to Y up
                    vtxl.vertPositions = new List <Vector3>()
                    {
                        new Vector3(obj.Positions[pf.A].X / 100, obj.Positions[pf.A].Z / 100, -obj.Positions[pf.A].Y / 100),
                        new Vector3(obj.Positions[pf.B].X / 100, obj.Positions[pf.B].Z / 100, -obj.Positions[pf.B].Y / 100),
                        new Vector3(obj.Positions[pf.C].X / 100, obj.Positions[pf.C].Z / 100, -obj.Positions[pf.C].Y / 100)
                    };
                    vtxl.vertNormals = new List <Vector3>()
                    {
                        new Vector3(obj.Normals[nf.A].X, obj.Normals[nf.A].Z, -obj.Normals[nf.A].Y),
                        new Vector3(obj.Normals[nf.B].X, obj.Normals[nf.B].Z, -obj.Normals[nf.B].Y),
                        new Vector3(obj.Normals[nf.C].X, obj.Normals[nf.C].Z, -obj.Normals[nf.C].Y)
                    };

                    vtxl.uv1List = new List <Vector2>()
                    {
                        new Vector2(obj.TexCoords[tf.A].X, -obj.TexCoords[tf.A].Y), new Vector2(obj.TexCoords[tf.B].X, -obj.TexCoords[tf.B].Y),
                        new Vector2(obj.TexCoords[tf.C].X, -obj.TexCoords[tf.C].Y),
                    };

                    if (aqo.vtxlList[0].vertWeights.Count > 0)
                    {
                        //Autoskin magic - Essentially, iterate through and get weight values from the closest match to the original
                        for (int vt = 0; vt < vtxl.vertPositions.Count; vt++)
                        {
                            var maxDistance = float.MaxValue;
                            vtxl.vertWeights.Add(new Vector4());
                            vtxl.vertWeightIndices.Add(null);

                            SkinData tempWeight = new SkinData();
                            //Go through each original vertex and compare distances. Adjust references if it's closer
                            foreach (var skin in skinData)
                            {
                                var distance = (vtxl.vertPositions[vt] - skin.pos).LengthSquared(); //Not a full true distance, just to be quicker

                                if (distance < maxDistance)
                                {
                                    maxDistance = distance;
                                    tempWeight  = skin;
                                    if (distance == 0)
                                    {
                                        break;
                                    }
                                }
                            }
                            vtxl.vertWeights[vt]       = tempWeight.weights;
                            vtxl.vertWeightIndices[vt] = tempWeight.indices;

                            if (aqo.vtxlList[0].vertWeightsNGS.Count > 0)
                            {
                                ushort[] shortWeights = new ushort[] { (ushort)(tempWeight.weights.X * ushort.MaxValue), (ushort)(tempWeight.weights.Y * ushort.MaxValue),
                                                                       (ushort)(tempWeight.weights.Z * ushort.MaxValue), (ushort)(tempWeight.weights.W * ushort.MaxValue), };
                                vtxl.vertWeightsNGS.Add(shortWeights);
                            }
                        }
                    }
                    tempMesh.faceVerts.Add(vtxl);
                }

                //Set up remapp ids
                for (int i = 0; i < vertIds.Count; i++)
                {
                    var id = vertIds[i];
                    vertIdRemap.Add(id, i);
                }

                //Remap Ids to verts
                for (int f = 0; f < tempMesh.faceVerts.Count; f++)
                {
                    for (int v = 0; v < tempMesh.faceVerts[f].rawVertId.Count; v++)
                    {
                        tempMesh.faceVerts[f].rawVertId[v] = vertIdRemap[tempMesh.faceVerts[f].rawVertId[v]];
                    }
                }

                //Remap Ids to face ids
                for (int f = 0; f < tempMesh.triList.Count; f++)
                {
                    var tri = tempMesh.triList[f];
                    if (vertIdRemap.ContainsKey((int)tri.X))
                    {
                        tri.X = vertIdRemap[(int)tri.X];
                    }
                    if (vertIdRemap.ContainsKey((int)tri.Y))
                    {
                        tri.Y = vertIdRemap[(int)tri.Y];
                    }
                    if (vertIdRemap.ContainsKey((int)tri.Z))
                    {
                        tri.Z = vertIdRemap[(int)tri.Z];
                    }

                    tempMesh.triList[f] = tri;
                }

                tempMesh.vertCount = vertIds.Count;
                totalVerts        += vertIds.Count;

                aqo.tempTris.Add(tempMesh);
            }
            AquaObjectMethods.VTXLFromFaceVerts(aqo);
            if (aqo.objc.type < 0xC32)
            {
                AquaObjectMethods.BatchSplitByBoneCount(aqo, tempModel, boneLimit);
                aqo.tempTris = tempModel.tempTris;
                aqo.vtxlList = tempModel.vtxlList;
                tempModel    = null;

                AquaObjectMethods.RemoveAllUnusedBones(aqo);
            }

            //AquaObjectMethods.CalcUNRMs(aqo, aqo.applyNormalAveraging, aqo.objc.unrmOffset != 0);

            //Set up PSETs and strips, and other per mesh data
            for (int i = 0; i < aqo.tempTris.Count; i++)
            {
                //strips
                AquaObject.stripData strips;
                if (aqo.objc.type >= 0xC32)
                {
                    strips             = new AquaObject.stripData();
                    strips.format0xC33 = true;
                    strips.triStrips   = new List <ushort>(aqo.tempTris[i].toUshortArray());
                    strips.triIdCount  = strips.triStrips.Count;
                    strips.faceGroups.Add(strips.triStrips.Count);
                }
                else
                {
                    strips = new AquaObject.stripData(aqo.tempTris[i].toUshortArray());
                }
                aqo.strips.Add(strips);

                //PSET
                var pset = new AquaObject.PSET();
                pset.faceGroupCount = 0x1;
                pset.psetFaceCount  = strips.triIdCount;
                if (aqo.objc.type >= 0xC32)
                {
                    pset.tag             = 0x1000;
                    pset.stripStartCount = totalStripsShorts;
                }
                else
                {
                    pset.tag = 0x2100;
                }
                aqo.psetList.Add(pset);
                totalStripsShorts += strips.triIdCount; //Update this *after* setting the strip start count so that we don't direct to bad data.

                //MESH
                Match m;
                int   idx_MATE = 0;
                int   idx_REND = 0;
                int   idx_SHAD = 0;
                int   idx_TSET = 0;
                if (null == aqo.tempTris[i].name)
                {
                    m = null;
                }
                else
                {
                    m = RE_ObjName.Match(aqo.tempTris[i].name);
                    if (!m.Success)
                    {
                        m = null;
                    }
                    else
                    {
                        idx_MATE = int.Parse(m.Groups[1].Value);
                        idx_REND = int.Parse(m.Groups[2].Value);
                        idx_SHAD = int.Parse(m.Groups[3].Value);
                        idx_TSET = int.Parse(m.Groups[4].Value);
                    }
                }
                if (m == null)
                {
                    idx_MATE = oldMESHList[0].mateIndex;
                    idx_REND = oldMESHList[0].rendIndex;
                    idx_SHAD = oldMESHList[0].shadIndex;
                    idx_TSET = oldMESHList[0].tsetIndex;
                }

                var             mesh         = new AquaObject.MESH();
                AquaObject.MESH oldMesh      = new AquaObject.MESH();
                bool            oldMeshFound = false;

                //Compare
                for (int msh = 0; msh < oldMESHList.Count; msh++)
                {
                    var tempMesh = oldMESHList[msh];
                    if (tempMesh.mateIndex == idx_MATE && tempMesh.rendIndex == idx_REND && tempMesh.shadIndex == idx_SHAD && tempMesh.tsetIndex == idx_TSET)
                    {
                        oldMesh      = tempMesh;
                        oldMeshFound = true;
                        break;
                    }
                }

                if (oldMeshFound == false)
                {
                    mesh.flags           = 0x17; //No idea what this really does. Seems to vary a lot, but also not matter a lot.
                    mesh.unkShort0       = 0x0;
                    mesh.unkByte0        = 0x80;
                    mesh.unkByte1        = 0x64;
                    mesh.unkShort1       = 0;
                    mesh.mateIndex       = idx_MATE;
                    mesh.rendIndex       = idx_REND;
                    mesh.shadIndex       = idx_SHAD;
                    mesh.tsetIndex       = idx_TSET;
                    mesh.baseMeshNodeId  = 0;
                    mesh.baseMeshDummyId = 0;
                    mesh.unkInt0         = 0;
                }
                else
                {
                    mesh.flags           = oldMesh.flags;
                    mesh.unkShort0       = oldMesh.unkShort0;
                    mesh.unkByte0        = oldMesh.unkByte0;
                    mesh.unkByte1        = oldMesh.unkByte1;
                    mesh.unkShort1       = oldMesh.unkShort1;
                    mesh.mateIndex       = idx_MATE;
                    mesh.rendIndex       = idx_REND;
                    mesh.shadIndex       = idx_SHAD;
                    mesh.tsetIndex       = idx_TSET;
                    mesh.baseMeshNodeId  = oldMesh.baseMeshNodeId;
                    mesh.baseMeshDummyId = oldMesh.baseMeshDummyId;
                    mesh.unkInt0         = oldMesh.unkInt0;
                }
                mesh.vsetIndex = i;
                mesh.psetIndex = i;
                mesh.reserve0  = 0;
                aqo.meshList.Add(mesh);
            }

            //Generate VTXEs and VSETs
            int largestVertSize = 0;
            int vertCounter     = 0;

            totalVerts = 0;
            aqo.vsetList.Clear();
            aqo.vtxeList.Clear();
            for (int i = 0; i < aqo.vtxlList.Count; i++)
            {
                totalVerts += aqo.vtxlList[i].vertPositions.Count;
                AquaObject.VTXE vtxe = AquaObjectMethods.ConstructClassicVTXE(aqo.vtxlList[i], out int size);
                aqo.vtxeList.Add(vtxe);

                //Track this for objc
                if (size > largestVertSize)
                {
                    largestVertSize = size;
                }

                AquaObject.VSET vset = new AquaObject.VSET();
                vset.vertDataSize   = size;
                vset.vtxlCount      = aqo.vtxlList[i].vertPositions.Count;
                vset.edgeVertsCount = aqo.vtxlList[i].edgeVerts.Count;

                if (aqo.objc.type >= 0xC32)
                {
                    vset.vtxeCount     = aqo.vtxeList.Count - 1;
                    vset.vtxlStartVert = vertCounter;
                    vertCounter       += vset.vtxlCount;

                    vset.bonePaletteCount = -1;
                }
                else
                {
                    vset.vtxeCount        = vtxe.vertDataTypes.Count;
                    vset.bonePaletteCount = aqo.vtxlList[i].bonePalette.Count;
                }

                aqo.vsetList.Add(vset);
            }

            //Update OBJC
            aqo.objc.largetsVtxl             = largestVertSize;
            aqo.objc.totalStripFaces         = totalStripsShorts;
            aqo.objc.totalVTXLCount          = totalVerts;
            aqo.objc.unkStructCount          = aqo.vtxlList.Count;
            aqo.objc.vsetCount               = aqo.vsetList.Count;
            aqo.objc.psetCount               = aqo.psetList.Count;
            aqo.objc.meshCount               = aqo.meshList.Count;
            aqo.objc.mateCount               = aqo.mateList.Count;
            aqo.objc.rendCount               = aqo.rendList.Count;
            aqo.objc.shadCount               = aqo.shadList.Count;
            aqo.objc.tstaCount               = aqo.tstaList.Count;
            aqo.objc.tsetCount               = aqo.tsetList.Count;
            aqo.objc.texfCount               = aqo.texfList.Count;
            aqo.objc.vtxeCount               = aqo.vtxeList.Count;
            aqo.objc.fBlock0                 = -1;
            aqo.objc.fBlock1                 = -1;
            aqo.objc.fBlock2                 = -1;
            aqo.objc.fBlock3                 = -1;
            aqo.objc.globalStrip3LengthCount = 1;
            aqo.objc.unkCount3               = 1;
            aqo.objc.bounds = AquaObjectMethods.GenerateBounding(aqo.vtxlList);
            if (doBitangent)
            {
                AquaObjectMethods.ComputeTangentSpace(aqo, false, true);
            }

            return(aqo);
        }
        public static Assimp.Scene AssimpExport(string filePath, AquaObject aqp, AquaNode aqn)
        {
            if (aqp is NGSAquaObject)
            {
                //NGS aqps will give lots of isolated vertices if we don't handle them
                //Since we're not actually altering the data so much as rearranging references, we can just do this
                aqp = aqp.Clone();
                aqp.splitVSETPerMesh();
            }
            Assimp.Scene aiScene = new Assimp.Scene();

            //Create an array to hold references to these since Assimp lacks a way to grab these by order or id
            //We don't need the nodo count in this since they can't be parents
            Assimp.Node[] boneArray = new Assimp.Node[aqn.nodeList.Count];

            //Set up root node
            var root       = aqn.nodeList[0];
            var aiRootNode = new Assimp.Node("RootNode", null);

            aiRootNode.Transform = Assimp.Matrix4x4.Identity;

            aiScene.RootNode = aiRootNode;

            //Assign bones
            for (int i = 0; i < aqn.nodeList.Count; i++)
            {
                var         bn = aqn.nodeList[i];
                Assimp.Node parentNode;
                var         parentTfm = Matrix4x4.Identity;
                if (bn.parentId == -1)
                {
                    parentNode = aiRootNode;
                }
                else
                {
                    parentNode = boneArray[bn.parentId];
                    var pn = aqn.nodeList[bn.parentId];
                    parentTfm = new Matrix4x4(pn.m1.X, pn.m1.Y, pn.m1.Z, pn.m1.W,
                                              pn.m2.X, pn.m2.Y, pn.m2.Z, pn.m2.W,
                                              pn.m3.X, pn.m3.Y, pn.m3.Z, pn.m3.W,
                                              pn.m4.X * 100, pn.m4.Y * 100, pn.m4.Z * 100, pn.m4.W);
                }
                var aiNode = new Assimp.Node($"({i})" + bn.boneName.GetString(), parentNode);

                //Use inverse bind matrix as base
                var bnMat = new Matrix4x4(bn.m1.X, bn.m1.Y, bn.m1.Z, bn.m1.W,
                                          bn.m2.X, bn.m2.Y, bn.m2.Z, bn.m2.W,
                                          bn.m3.X, bn.m3.Y, bn.m3.Z, bn.m3.W,
                                          bn.m4.X * 100, bn.m4.Y * 100, bn.m4.Z * 100, bn.m4.W);
                Matrix4x4.Invert(bnMat, out bnMat);

                //Get local transform
                aiNode.Transform = GetAssimpMat4(bnMat * parentTfm);

                parentNode.Children.Add(aiNode);
                boneArray[i] = aiNode;
            }

            foreach (AquaNode.NODO bn in aqn.nodoList)
            {
                var parentNodo = boneArray[bn.parentId];
                var aiNode     = new Assimp.Node(bn.boneName.GetString(), parentNodo);

                //NODOs are a bit more primitive. We need to generate the matrix for these ones.
                var matrix   = Assimp.Matrix4x4.Identity;
                var rotation = Assimp.Matrix4x4.FromRotationX(bn.eulRot.X) *
                               Assimp.Matrix4x4.FromRotationY(bn.eulRot.Y) *
                               Assimp.Matrix4x4.FromRotationZ(bn.eulRot.Z);

                matrix          *= rotation;
                matrix          *= Assimp.Matrix4x4.FromTranslation(new Assimp.Vector3D(bn.pos.X * 100, bn.pos.Y * 100, bn.pos.Z * 100));
                aiNode.Transform = matrix;

                parentNodo.Children.Add(aiNode);
            }

            //Assign meshes and materials
            foreach (AquaObject.MESH msh in aqp.meshList)
            {
                var vtxl = aqp.vtxlList[msh.vsetIndex];

                //Mesh
                var  aiMeshName       = string.Format("mesh[{4}]_{0}_{1}_{2}_{3}_mesh", msh.mateIndex, msh.rendIndex, msh.shadIndex, msh.tsetIndex, aiScene.Meshes.Count);
                bool hasVertexWeights = aqp.vtxlList[msh.vsetIndex].vertWeightIndices.Count > 0;

                var aiMesh = new Assimp.Mesh(aiMeshName, Assimp.PrimitiveType.Triangle);

                //Vertex face data - PSO2 Actually doesn't do this, it just has per vertex data so we can just map a vertice's data to each face using it
                //It may actually be possible to add this to the previous loop, but my reference didn't so I'm doing it in a separate loop for safety
                //Reference: https://github.com/TGEnigma/Amicitia/blob/master/Source/AmicitiaLibrary/Graphics/RenderWare/RWClumpNode.cs
                //UVs will have dummied data to ensure that if the game arbitrarily writes them, they will still be exported back in the same order
                for (int vertId = 0; vertId < vtxl.vertPositions.Count; vertId++)
                {
                    if (vtxl.vertPositions.Count > 0)
                    {
                        var pos = vtxl.vertPositions[vertId] * 100;
                        aiMesh.Vertices.Add(new Assimp.Vector3D(pos.X, pos.Y, pos.Z));
                    }

                    if (vtxl.vertNormals.Count > 0)
                    {
                        var nrm = vtxl.vertNormals[vertId];
                        aiMesh.Normals.Add(new Assimp.Vector3D(nrm.X, nrm.Y, nrm.Z));
                    }

                    if (vtxl.vertColors.Count > 0)
                    {
                        //Vert colors are bgra
                        var rawClr = vtxl.vertColors[vertId];
                        var clr    = new Assimp.Color4D(clrToFloat(rawClr[2]), clrToFloat(rawClr[1]), clrToFloat(rawClr[0]), clrToFloat(rawClr[3]));
                        aiMesh.VertexColorChannels[0].Add(clr);
                    }

                    if (vtxl.vertColor2s.Count > 0)
                    {
                        //Vert colors are bgra
                        var rawClr = vtxl.vertColor2s[vertId];
                        var clr    = new Assimp.Color4D(clrToFloat(rawClr[2]), clrToFloat(rawClr[1]), clrToFloat(rawClr[0]), clrToFloat(rawClr[3]));
                        aiMesh.VertexColorChannels[1].Add(clr);
                    }

                    if (vtxl.uv1List.Count > 0)
                    {
                        var textureCoordinate   = vtxl.uv1List[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(textureCoordinate.X, textureCoordinate.Y, 0f);
                        aiMesh.TextureCoordinateChannels[0].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[0].Add(aiTextureCoordinate);
                    }

                    if (vtxl.uv2List.Count > 0)
                    {
                        var textureCoordinate   = vtxl.uv2List[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(textureCoordinate.X, textureCoordinate.Y, 0f);
                        aiMesh.TextureCoordinateChannels[1].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[1].Add(aiTextureCoordinate);
                    }

                    if (vtxl.uv3List.Count > 0)
                    {
                        var textureCoordinate   = vtxl.uv3List[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(textureCoordinate.X, textureCoordinate.Y, 0f);
                        aiMesh.TextureCoordinateChannels[2].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[2].Add(aiTextureCoordinate);
                    }

                    if (vtxl.uv4List.Count > 0)
                    {
                        var textureCoordinate   = vtxl.uv4List[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(textureCoordinate.X, textureCoordinate.Y, 0f);
                        aiMesh.TextureCoordinateChannels[3].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[3].Add(aiTextureCoordinate);
                    }

                    if (vtxl.vert0x22.Count > 0)
                    {
                        var textureCoordinate   = vtxl.vert0x22[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(uvShortToFloat(textureCoordinate[0]), uvShortToFloat(textureCoordinate[1]), 0f);
                        aiMesh.TextureCoordinateChannels[4].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[4].Add(aiTextureCoordinate);
                    }

                    if (vtxl.vert0x23.Count > 0)
                    {
                        var textureCoordinate   = vtxl.vert0x23[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(uvShortToFloat(textureCoordinate[0]), uvShortToFloat(textureCoordinate[1]), 0f);
                        aiMesh.TextureCoordinateChannels[5].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[5].Add(aiTextureCoordinate);
                    }

                    if (vtxl.vert0x24.Count > 0)
                    {
                        var textureCoordinate   = vtxl.vert0x24[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(uvShortToFloat(textureCoordinate[0]), uvShortToFloat(textureCoordinate[1]), 0f);
                        aiMesh.TextureCoordinateChannels[6].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[6].Add(aiTextureCoordinate);
                    }

                    if (vtxl.vert0x25.Count > 0)
                    {
                        var textureCoordinate   = vtxl.vert0x25[vertId];
                        var aiTextureCoordinate = new Assimp.Vector3D(uvShortToFloat(textureCoordinate[0]), uvShortToFloat(textureCoordinate[1]), 0f);
                        aiMesh.TextureCoordinateChannels[7].Add(aiTextureCoordinate);
                    }
                    else
                    {
                        var aiTextureCoordinate = new Assimp.Vector3D(0, 0, 0f);
                        aiMesh.TextureCoordinateChannels[7].Add(aiTextureCoordinate);
                    }
                }

                //Assimp Bones - Assimp likes to store vertex weights in bones and bones references in meshes
                if (hasVertexWeights)
                {
                    //Get bone palette
                    List <uint> bonePalette;
                    if (aqp.objc.bonePaletteOffset > 0)
                    {
                        bonePalette = aqp.bonePalette;
                    }
                    else
                    {
                        bonePalette = new List <uint>();
                        for (int bn = 0; bn < vtxl.bonePalette.Count; bn++)
                        {
                            bonePalette.Add(vtxl.bonePalette[bn]);
                        }
                    }
                    var aiBoneMap = new Dictionary <int, Assimp.Bone>();

                    //Iterate through vertices
                    for (int vertId = 0; vertId < vtxl.vertWeightIndices.Count; vertId++)
                    {
                        var boneIndices = vtxl.vertWeightIndices[vertId];
                        var boneWeights = Vector4ToFloatArray(vtxl.vertWeights[vertId]);

                        //Iterate through weights
                        for (int wt = 0; wt < 4; wt++)
                        {
                            var boneIndex  = boneIndices[wt];
                            var boneWeight = boneWeights[wt];

                            if (boneWeight == 0.0f)
                            {
                                continue;
                            }

                            if (!aiBoneMap.Keys.Contains(boneIndex))
                            {
                                var aiBone  = new Assimp.Bone();
                                var aqnBone = boneArray[bonePalette[boneIndex]];
                                var rawBone = aqn.nodeList[(int)bonePalette[boneIndex]];

                                aiBone.Name = $"({bonePalette[boneIndex]})" + rawBone.boneName.GetString();
                                aiBone.VertexWeights.Add(new Assimp.VertexWeight(vertId, boneWeight));

                                var invTransform = new Assimp.Matrix4x4(rawBone.m1.X, rawBone.m2.X, rawBone.m3.X, rawBone.m4.X,
                                                                        rawBone.m1.Y, rawBone.m2.Y, rawBone.m3.Y, rawBone.m4.Y,
                                                                        rawBone.m1.Z, rawBone.m2.Z, rawBone.m3.Z, rawBone.m4.Z,
                                                                        rawBone.m1.W, rawBone.m2.W, rawBone.m3.W, rawBone.m4.W);

                                aiBone.OffsetMatrix = invTransform;

                                aiBoneMap[boneIndex] = aiBone;
                            }

                            if (!aiBoneMap[boneIndex].VertexWeights.Any(x => x.VertexID == vertId))
                            {
                                aiBoneMap[boneIndex].VertexWeights.Add(new Assimp.VertexWeight(vertId, boneWeight));
                            }
                        }
                    }

                    //Add the bones to the mesh
                    aiMesh.Bones.AddRange(aiBoneMap.Values);
                }
                else   //Handle rigid meshes
                {
                    var aiBone  = new Assimp.Bone();
                    var aqnBone = boneArray[msh.baseMeshNodeId];

                    // Name
                    aiBone.Name = aqnBone.Name;

                    // VertexWeights
                    for (int i = 0; i < aiMesh.Vertices.Count; i++)
                    {
                        var aiVertexWeight = new Assimp.VertexWeight(i, 1f);
                        aiBone.VertexWeights.Add(aiVertexWeight);
                    }

                    aiBone.OffsetMatrix = Assimp.Matrix4x4.Identity;

                    aiMesh.Bones.Add(aiBone);
                }

                //Faces
                foreach (var face in aqp.strips[msh.vsetIndex].GetTriangles(true))
                {
                    aiMesh.Faces.Add(new Assimp.Face(new int[] { (int)face.X, (int)face.Y, (int)face.Z }));
                }

                //Material
                var             mat        = aqp.mateList[msh.mateIndex];
                var             shaderSet  = AquaObjectMethods.GetShaderNames(aqp, msh.shadIndex);
                var             textureSet = AquaObjectMethods.GetTexListNames(aqp, msh.tsetIndex);
                Assimp.Material mate       = new Assimp.Material();

                mate.ColorDiffuse = new Assimp.Color4D(mat.diffuseRGBA.X, mat.diffuseRGBA.Y, mat.diffuseRGBA.Z, mat.diffuseRGBA.W);
                if (mat.alphaType.GetString().Equals("add"))
                {
                    mate.BlendMode = Assimp.BlendMode.Additive;
                }
                mate.Name = "|[]{}~`!@#$%^&*;:'\"?><,./(" + shaderSet[0] + "," + shaderSet[1] + ")" + "{" + mat.alphaType.GetString() + "}" + mat.matName.GetString();

                //Set textures - PSO2 Texture slots are NOT consistent and depend entirely on the selected shader. As such, slots will be somewhat arbitrary after albedo/diffuse
                for (int i = 0; i < textureSet.Count; i++)
                {
                    switch (i)
                    {
                    case 0:
                        mate.TextureDiffuse = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Diffuse, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 1:
                        mate.TextureSpecular = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Specular, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 2:
                        mate.TextureNormal = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Normals, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 3:
                        mate.TextureLightMap = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Lightmap, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 4:
                        mate.TextureDisplacement = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Displacement, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 5:
                        mate.TextureOpacity = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Opacity, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 6:
                        mate.TextureHeight = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Height, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 7:
                        mate.TextureEmissive = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Emissive, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 8:
                        mate.TextureAmbient = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Ambient, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    case 9:
                        mate.TextureReflection = new Assimp.TextureSlot(
                            textureSet[i], Assimp.TextureType.Reflection, i, Assimp.TextureMapping.FromUV, aqp.tstaList[aqp.tsetList[msh.tsetIndex].tstaTexIDs[i]].modelUVSet, 0,
                            Assimp.TextureOperation.Add, Assimp.TextureWrapMode.Wrap, Assimp.TextureWrapMode.Wrap, 0);
                        break;

                    default:
                        break;
                    }
                }

                mate.ShadingMode = Assimp.ShadingMode.Phong;


                var meshNodeName = string.Format("mesh[{4}]_{0}_{1}_{2}_{3}#{4}#{5}", msh.mateIndex, msh.rendIndex, msh.shadIndex, msh.tsetIndex, aiScene.Meshes.Count, msh.baseMeshNodeId, msh.baseMeshDummyId);

                // Add mesh to meshes
                aiScene.Meshes.Add(aiMesh);

                // Add material to materials
                aiScene.Materials.Add(mate);

                // MaterialIndex
                aiMesh.MaterialIndex = aiScene.Materials.Count - 1;

                // Set up mesh node and add this mesh's index to it (This tells assimp to export it as a mesh for various formats)
                var meshNode = new Assimp.Node(meshNodeName, aiScene.RootNode);
                meshNode.Transform = Assimp.Matrix4x4.Identity;

                aiScene.RootNode.Children.Add(meshNode);

                meshNode.MeshIndices.Add(aiScene.Meshes.Count - 1);
            }

            return(aiScene);
        }
        //Takes in an Assimp model and generates a full PSO2 model and skeleton from it.
        public static AquaObject AssimpAquaConvertFull(string initialFilePath, float scaleFactor, bool preAssignNodeIds, bool isNGS)
        {
            AquaUtil aquaUtil  = new AquaUtil();
            float    baseScale = 1f / 100f * scaleFactor; //We assume that this will be 100x the true scale because 1 unit to 1 meter isn't the norm

            Assimp.AssimpContext context = new Assimp.AssimpContext();
            context.SetConfig(new Assimp.Configs.FBXPreservePivotsConfig(false));
            Assimp.Scene aiScene = context.ImportFile(initialFilePath, Assimp.PostProcessSteps.Triangulate | Assimp.PostProcessSteps.JoinIdenticalVertices | Assimp.PostProcessSteps.FlipUVs);

            AquaObject aqp;
            AquaNode   aqn = new AquaNode();

            if (isNGS)
            {
                aqp = new NGSAquaObject();
            }
            else
            {
                aqp = new ClassicAquaObject();
            }

            //Construct Materials
            Dictionary <string, int> matNameTracker = new Dictionary <string, int>();

            foreach (var aiMat in aiScene.Materials)
            {
                string name;
                if (matNameTracker.ContainsKey(aiMat.Name))
                {
                    name = $"{aiMat.Name} ({matNameTracker[aiMat.Name]})";
                    matNameTracker[aiMat.Name] += 1;
                }
                else
                {
                    name = aiMat.Name;
                    matNameTracker.Add(aiMat.Name, 1);
                }

                AquaObject.GenericMaterial genMat = new AquaObject.GenericMaterial();
                List <string> shaderList          = new List <string>();
                AquaObjectMethods.GetMaterialNameData(ref name, shaderList, out string alphaType, out string playerFlag);
                genMat.matName     = name;
                genMat.shaderNames = shaderList;
                genMat.blendType   = alphaType;
                genMat.specialType = playerFlag;
                genMat.texNames    = new List <string>();
                genMat.texUVSets   = new List <int>();

                //Texture assignments. Since we can't rely on these to export properly, we dummy them or just put diffuse if a playerFlag isn't defined.
                //We'll have the user set these later if needed.
                if (genMat.specialType != null)
                {
                    AquaObjectMethods.GenerateSpecialMaterialParameters(genMat);
                }
                else if (aiMat.TextureDiffuse.FilePath != null)
                {
                    genMat.texNames.Add(Path.GetFileName(aiMat.TextureDiffuse.FilePath));
                }
                else
                {
                    genMat.texNames.Add("tex0_d.dds");
                }
                genMat.texUVSets.Add(0);

                AquaObjectMethods.GenerateMaterial(aqp, genMat, true);
            }

            //Default to this so ids can be assigned by order if needed
            Dictionary <string, int> boneDict = new Dictionary <string, int>();

            if (aiScene.RootNode.Name == null || !aiScene.RootNode.Name.Contains("(") || preAssignNodeIds == true)
            {
                int nodeCounter = 0;
                BuildAiNodeDictionary(aiScene.RootNode, ref nodeCounter, boneDict);
            }

            IterateAiNodesAQP(aqp, aqn, aiScene, aiScene.RootNode, Matrix4x4.Transpose(GetMat4FromAssimpMat4(aiScene.RootNode.Transform)), baseScale);

            //Assimp data is gathered, proceed to processing model data for PSO2
            AquaUtil.ModelSet set = new AquaUtil.ModelSet();
            set.models.Add(aqp);
            aquaUtil.aquaModels.Add(set);
            aquaUtil.ConvertToNGSPSO2Mesh(false, false, false, true, false, false, true);

            //AQPs created this way will require more processing to finish.
            //-Texture lists in particular, MUST be generated as what exists is not valid without serious errors
            return(aqp);
        }