//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}"; } }
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); } }
//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); }