using System; using System.IO; using System.Collections; using System.Collections.Generic; using NW4RTools.Models; namespace NW4RTools { public class ObjImporter { public enum LightmapType { None, Map, MapObj } public static void ImportModel(string basePath, TextReader tr, ResFile file, string modelName) { new ObjImporter(basePath, file, tr).ImportModel(modelName, LightmapType.None); } public static void ImportModel(string basePath, TextReader tr, ResFile file, string modelName, LightmapType lightmapType) { new ObjImporter(basePath, file, tr).ImportModel(modelName, lightmapType); } private ObjImporter(string basePath, ResFile file, TextReader tr) { BasePath = basePath; CurrentFile = file; Input = tr; } LightmapType Lightmap; string LightmapName1, LightmapName2; string BasePath; ResFile CurrentFile; Models.Model CurrentModel; TextReader Input; List Positions; List Normals; List TexCoords; class ShapeContext { public Shape Shape; public Material Material; public List Quads; public List Triangles; public BitArray UsedVertices; public BitArray UsedNormals; public BitArray UsedTexCoords; } ShapeContext CurrentShape; Util.OrderedDictionary KnownShapes; public void ImportModel(string modelName, LightmapType lmt) { var modelGroup = CurrentFile.CreateModelGroup(); var texGroup = CurrentFile.CreateTextureGroup(); Lightmap = lmt; string lmID = ""; if (lmt == ObjImporter.LightmapType.Map) lmID = "m"; else if (lmt == ObjImporter.LightmapType.MapObj) lmID = "mo"; LightmapName1 = "lm_01" + lmID; LightmapName2 = "lm_02" + lmID; // OK, so here's what I'm going to do: // I'll read the file into an internal array and process commands as they come. // One Shape will be created for each group and material pair. // I'll make the assumption that a "g" command will ALWAYS be followed by a "usemtl" command. KnownShapes = new Util.OrderedDictionary(); // Vertex position/normal/texcoord (v, vn, vt): // -- These will be added into a list. // Faces (f): // -- These will be added into a list for the current Shape. // Set group (g): // -- It will be stored as the current group name. // Load material library (mtllib): // -- The specified file will be loaded. // -- All materials in it (and their associated textures) will be added to the model. // Set current material (usemtl): // -- The current Shape will be changed. // -- If a Shape already exists for this material AND the current group, then it will become the current // Shape. Otherwise, a new one will be created. // At the end of the .obj parsing, all the shapes will be converted and written. if (Lightmap != LightmapType.None) { if (!texGroup.ContainsKey(LightmapName1)) { var lm01 = new Texture(); lm01.Images = new System.Drawing.Bitmap[1]; lm01.Images[0] = new System.Drawing.Bitmap(Path.Combine(BasePath, string.Format("images/{0}.png", LightmapName1))); lm01.Format = TextureFormat.I8; texGroup.Add(LightmapName1, lm01); } if (!texGroup.ContainsKey(LightmapName2)) { var lm02 = new Texture(); lm02.Images = new System.Drawing.Bitmap[1]; lm02.Images[0] = new System.Drawing.Bitmap(Path.Combine(BasePath, string.Format("images/{0}.png", LightmapName2))); lm02.Format = TextureFormat.I8; texGroup.Add(LightmapName2, lm02); } } CurrentModel = new Model(); modelGroup[modelName] = CurrentModel; // Before we start reading the OBJ file, prepare the model CurrentModel.ScaleMode = Model.ScaleModeType.Standard; CurrentModel.TexMatrixMode = Model.TexMatrixModeType.Maya; CurrentModel.UsesNrmMtxArray = true; // why? // Todo: vertex count, triangle count // Minimum, Maximum will be calc'd later var minimum = new Vec3(0, 0, 0); var maximum = new Vec3(0, 0, 0); // Create one node // Default settings from test_lift.brres var newNode = new Node(); newNode.Flags = 0x31F; newNode.Scale = new Vec3(1, 1, 1); newNode.BoxMin = new Vec3(-16.0f, -16.0f, 0.0f); newNode.BoxMax = new Vec3(16.0f, 16.0f, 0.0f); newNode.NodeMatrix.Identity(); newNode.NodeInvMatrix.v00 = 1; newNode.NodeInvMatrix.v01 = -0; newNode.NodeInvMatrix.v02 = 0; newNode.NodeInvMatrix.v03 = 0; newNode.NodeInvMatrix.v10 = -0; newNode.NodeInvMatrix.v11 = 1; newNode.NodeInvMatrix.v12 = -0; newNode.NodeInvMatrix.v13 = 0; newNode.NodeInvMatrix.v20 = 0; newNode.NodeInvMatrix.v21 = -0; newNode.NodeInvMatrix.v22 = 1; newNode.NodeInvMatrix.v23 = 0; CurrentModel.Nodes.Add("RootNode", newNode); // Map it correctly CurrentModel.MatrixIDtoNodeID = new int[1]; CurrentModel.MatrixIDtoNodeID[0] = 0; // Now put together the NodeTree var nodeTreeInsn = new ByteCode.AssignNodeToParentMtxInstruction(); nodeTreeInsn.NodeID = 0; nodeTreeInsn.ParentMatrixID = 0; var nodeTreeEndInsn = new ByteCode.DoneInstruction(); var nodeTree = new ByteCode(); nodeTree.Instructions.Add(nodeTreeInsn); nodeTree.Instructions.Add(nodeTreeEndInsn); CurrentModel.Bytecode.Add("NodeTree", nodeTree); // Also, DrawOpa var drawOpa = new ByteCode(); CurrentModel.Bytecode.Add("DrawOpa", drawOpa); // Initial setup is done, let's go! Positions = new List(); Normals = new List(); TexCoords = new List(); string line; string currentGroup = null; while ((line = Input.ReadLine()) != null) { line = line.Trim(); if (line.Length == 0 || line[0] == '#') continue; var parsed = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); switch (parsed[0]) { case "mtllib": LoadMaterialLibrary(string.Join(" ", parsed, 1, parsed.Length - 1)); break; case "v": var vEntry = ParseFloatArray(parsed, 1, 3); Positions.Add(vEntry); if (vEntry[0] < minimum.x) minimum.x = vEntry[0]; if (vEntry[1] < minimum.y) minimum.y = vEntry[1]; if (vEntry[2] < minimum.z) minimum.z = vEntry[2]; if (vEntry[0] > maximum.x) maximum.x = vEntry[0]; if (vEntry[1] > maximum.y) maximum.y = vEntry[1]; if (vEntry[2] > maximum.z) maximum.z = vEntry[2]; break; case "vn": Normals.Add(ParseFloatArray(parsed, 1, 3)); break; case "vt": var vtEntry = ParseFloatArray(parsed, 1, 2); vtEntry[1] = 1.0f - vtEntry[1]; TexCoords.Add(vtEntry); break; case "f": AddFace(parsed); break; case "g": Console.WriteLine("Current group is now {0}", parsed[1]); currentGroup = parsed[1]; break; case "usemtl": Console.WriteLine("Current material is now {0}", parsed[1]); SetCurrentShape(currentGroup, parsed[1]); break; default: Console.WriteLine("Unhandled OBJ command: {0}", parsed[0]); break; } } // Parsing is finished. Let's convert every shape and finish up DrawOpa! foreach (var pair in KnownShapes) { FinaliseShape(pair.Value); } drawOpa.Instructions.Add(new ByteCode.DoneInstruction()); CurrentModel.Minimum = minimum; CurrentModel.Maximum = maximum; } private static float[] ParseFloatArray(string[] src, int index) { return ParseFloatArray(src, index, src.Length - index); } private static float[] ParseFloatArray(string[] src, int index, int count) { var output = new float[count]; for (int i = 0; i < count; i++) { output[i] = float.Parse(src[i + index]); } return output; } #region Materials private void LoadMaterialLibrary(string libPath) { string realPath = Path.Combine(BasePath, libPath); Console.WriteLine("Loading material library from {0}", realPath); var reader = File.OpenText(realPath); string line; Material currentMaterial = null; while ((line = reader.ReadLine()) != null) { line = line.Trim(); if (line.Length == 0 || line[0] == '#') continue; var parsed = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); switch (parsed[0]) { case "newmtl": // Create a new material and initialise everything // Default settings taken from Test_Lift material currentMaterial = new Material(); currentMaterial.ChanCtrls[0] = new ChanCtrl(); currentMaterial.ChanCtrls[0].Flags = 0x3F; currentMaterial.ChanCtrls[0].MatColor.Rgba = 0xFFFFFFFF; currentMaterial.ChanCtrls[0].AmbColor.Rgba = 0xFFFFFFFF; currentMaterial.ChanCtrls[0].FlagC = (Lightmap != ObjImporter.LightmapType.None) ? (uint)0x701 : (uint)0x702; currentMaterial.ChanCtrls[0].FlagA = 0x700; currentMaterial.ChanCtrls[1] = new ChanCtrl(); currentMaterial.ChanCtrls[1].Flags = 0xF; currentMaterial.ChanCtrls[1].MatColor.Rgba = 0x000000FF; currentMaterial.ChanCtrls[1].AmbColor.Rgba = 0x00000000; currentMaterial.ChanCtrls[1].FlagC = 0; currentMaterial.ChanCtrls[1].FlagA = 0; if (Lightmap == ObjImporter.LightmapType.None) { currentMaterial.TexCoordGenCount = 1; currentMaterial.ChanCount = 1; currentMaterial.TevStageCount = 2; currentMaterial.IndStageCount = 0; // This might need changing currentMaterial.CullMode = 2; currentMaterial.LightSetID = 1; currentMaterial.SRTSettings[0] = new SRTSettingInfo(); // Default display lists, taken from test_lift var pixDL = new DisplayListWriter(); pixDL.LoadBPReg(0xF31EFF80); pixDL.LoadBPReg(0x40000017); pixDL.LoadBPReg(0xFE00FFE3); pixDL.LoadBPReg(0x410034A0); pixDL.LoadBPReg(0x42000000); pixDL.End(); currentMaterial.PixDL = pixDL.GetBuffer(); var tevColorDL = new DisplayListWriter(); tevColorDL.LoadBPReg(0xE20000FF); tevColorDL.LoadBPReg(0xE30FF0FF); tevColorDL.LoadBPReg(0xE30FF0FF); tevColorDL.LoadBPReg(0xE30FF0FF); tevColorDL.LoadBPReg(0xE4000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE6000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.AddPadding(4); tevColorDL.LoadBPReg(0xE0800000); tevColorDL.LoadBPReg(0xE1800000); tevColorDL.LoadBPReg(0xE2800000); tevColorDL.LoadBPReg(0xE3800000); tevColorDL.LoadBPReg(0xE4800000); tevColorDL.LoadBPReg(0xE5800000); tevColorDL.LoadBPReg(0xE6800000); tevColorDL.LoadBPReg(0xE7800000); tevColorDL.AddPadding(24); tevColorDL.End(); currentMaterial.TevColorDL = tevColorDL.GetBuffer(); var indMtxAndScaleDL = new DisplayListWriter(); indMtxAndScaleDL.LoadBPReg(0x25000000); indMtxAndScaleDL.LoadBPReg(0x26000000); indMtxAndScaleDL.AddPadding(54); indMtxAndScaleDL.End(); currentMaterial.IndMtxAndScaleDL = indMtxAndScaleDL.GetBuffer(); var texCoordGenDL = new DisplayListWriter(); texCoordGenDL.LoadXFReg(0x1040, new byte[] { 0x00, 0x00, 0x52, 0x80 }); texCoordGenDL.LoadXFReg(0x1050, new byte[] { 0x00, 0x00, 0x00, 0x00 }); texCoordGenDL.PadToSize(0xA0); texCoordGenDL.End(); currentMaterial.TexCoordGenDL = texCoordGenDL.GetBuffer(); } else { // Map: These are taken from mtGake on CS_W1 // MapObj: Taken from cobKinokoRed currentMaterial.TexCoordGenCount = 3; currentMaterial.ChanCount = 1; currentMaterial.TevStageCount = 3; currentMaterial.IndStageCount = 0; currentMaterial.CullMode = 2; currentMaterial.ZCompLoc = 1; currentMaterial.LightSetID = 0xFF; currentMaterial.FogID = 1; currentMaterial.SRTSettings[0] = new SRTSettingInfo(); currentMaterial.SRTSettings[1] = new SRTSettingInfo(); currentMaterial.SRTSettings[2] = new SRTSettingInfo(); currentMaterial.SRTSettings[1].MapType = 1; currentMaterial.SRTSettings[2].MapType = 1; var pixDL = new DisplayListWriter(); pixDL.LoadBPReg(0xF33F0000); pixDL.LoadBPReg(0x40000017); pixDL.LoadBPReg(0xFE00FFE3); pixDL.LoadBPReg(0x410034A0); pixDL.LoadBPReg(0x42000000); pixDL.End(); currentMaterial.PixDL = pixDL.GetBuffer(); var tevColorDL = new DisplayListWriter(); tevColorDL.LoadBPReg(0xE2000000); tevColorDL.LoadBPReg(0xE3000000); tevColorDL.LoadBPReg(0xE3000000); tevColorDL.LoadBPReg(0xE3000000); tevColorDL.LoadBPReg(0xE4000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE5000000); tevColorDL.LoadBPReg(0xE6000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.LoadBPReg(0xE7000000); tevColorDL.AddPadding(4); tevColorDL.LoadBPReg(0xE0800000); tevColorDL.LoadBPReg(0xE1800000); tevColorDL.LoadBPReg(0xE2800000); tevColorDL.LoadBPReg(0xE3800000); tevColorDL.LoadBPReg(0xE4800000); tevColorDL.LoadBPReg(0xE5800000); if (Lightmap == ObjImporter.LightmapType.Map) tevColorDL.LoadBPReg(0xE68FF000); else if (Lightmap == ObjImporter.LightmapType.MapObj) tevColorDL.LoadBPReg(0xE6800000); tevColorDL.LoadBPReg(0xE7800000); tevColorDL.PadToSize(0x80); tevColorDL.End(); currentMaterial.TevColorDL = tevColorDL.GetBuffer(); var indMtxAndScaleDL = new DisplayListWriter(); indMtxAndScaleDL.LoadBPReg(0x25000000); indMtxAndScaleDL.LoadBPReg(0x26000000); indMtxAndScaleDL.PadToSize(0x40); indMtxAndScaleDL.End(); currentMaterial.IndMtxAndScaleDL = indMtxAndScaleDL.GetBuffer(); var texCoordGenDL = new DisplayListWriter(); texCoordGenDL.LoadXFReg(0x1040, new byte[] { 0x00, 0x00, 0x52, 0x80 }); texCoordGenDL.LoadXFReg(0x1050, new byte[] { 0x00, 0x00, 0x00, 0x00 }); texCoordGenDL.LoadXFReg(0x1041, new byte[] { 0x00, 0x00, 0x50, 0x86 }); texCoordGenDL.LoadXFReg(0x1051, new byte[] { 0x00, 0x00, 0x01, 0x03 }); texCoordGenDL.LoadXFReg(0x1042, new byte[] { 0x00, 0x00, 0x50, 0x86 }); texCoordGenDL.LoadXFReg(0x1052, new byte[] { 0x00, 0x00, 0x01, 0x06 }); texCoordGenDL.PadToSize(0xA0); texCoordGenDL.End(); currentMaterial.TexCoordGenDL = texCoordGenDL.GetBuffer(); } CurrentModel.Materials.Add(parsed[1], currentMaterial); Shader thisShader = CreateShader(); CurrentModel.Shaders.Add(parsed[1], thisShader); currentMaterial.ShaderRef = thisShader; break; case "map_Kd": var rawTexName = string.Join(" ", parsed, 1, parsed.Length - 1); // TODO: fix this to use the mtllib path var texPath = Path.Combine(BasePath, rawTexName); var texName = Path.GetFileNameWithoutExtension(texPath); if (!CurrentFile.GetTextureGroup().ContainsKey(texName)) AddTexture(texName, texPath); var texInfo = new TextureInfo(); texInfo.TextureName = texName; texInfo.WrapS = TextureWrapType.REPEAT; texInfo.WrapT = TextureWrapType.REPEAT; texInfo.MinFilt = 1; texInfo.MagFilt = 1; currentMaterial.TextureInfos.Add(texInfo); // Now add the lightmaps if (Lightmap != ObjImporter.LightmapType.None) { var lm01 = new TextureInfo(); lm01.TextureName = LightmapName1; lm01.TexMapID = 1; lm01.TlutID = 1; lm01.MinFilt = lm01.MagFilt = 1; var lm02 = new TextureInfo(); lm02.TextureName = LightmapName2; lm02.TexMapID = 2; lm02.TlutID = 2; lm02.MinFilt = lm02.MagFilt = 1; currentMaterial.TextureInfos.Add(lm01); currentMaterial.TextureInfos.Add(lm02); } break; } } } private void AddTexture(string texName, string texPath) { var newTexture = new Texture(); newTexture.Images = new System.Drawing.Bitmap[1]; newTexture.Images[0] = new System.Drawing.Bitmap(texPath); newTexture.Format = TextureFormat.RGB5A3; CurrentFile.GetTextureGroup().Add(texName, newTexture); // TODO: Texture/Material pairing lookups } private Shader CreateShader() { var shader = new Shader(); var dl = new DisplayListWriter(); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xF6000004); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xF700000E); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xF8000000); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xF900000C); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xFA000005); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xFB00000D); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xFC00000A); dl.LoadBPReg(0xFE00000F); dl.LoadBPReg(0xFD00000E); dl.LoadBPReg(0x27FFFFFF); dl.AddPadding(11); if (Lightmap == ObjImporter.LightmapType.None) { shader.TevStageCount = 2; shader.Unk1 = 0x00FFFFFF; shader.Unk2 = 0xFFFFFFFF; dl.LoadBPReg(0xFEFFFFF0); dl.LoadBPReg(0xF6E338C0); dl.LoadBPReg(0x2803F040); dl.LoadBPReg(0xC008F8AF); dl.LoadBPReg(0xC208F20F); dl.LoadBPReg(0xC108F2F0); dl.LoadBPReg(0xC3081FF0); dl.LoadBPReg(0x10000000); dl.LoadBPReg(0x11000000); } else { shader.TevStageCount = 3; shader.Unk1 = 0x000102FF; shader.Unk2 = 0xFFFFFFFF; dl.LoadBPReg(0xFEFFFFF0); if (Lightmap == ObjImporter.LightmapType.Map) { dl.LoadBPReg(0xF6E338C0); dl.LoadBPReg(0x283C0049); dl.LoadBPReg(0xC008F8AF); dl.LoadBPReg(0xC208F80F); dl.LoadBPReg(0xC108FFD0); dl.LoadBPReg(0xC308E270); } else if (Lightmap == ObjImporter.LightmapType.MapObj) { dl.LoadBPReg(0xF6E3F8C0); dl.LoadBPReg(0x28052040); dl.LoadBPReg(0xC0C8F8AF); dl.LoadBPReg(0xC208F8AE); dl.LoadBPReg(0xC1C8FCF0); dl.LoadBPReg(0xC308FFF0); } dl.LoadBPReg(0x10000000); dl.LoadBPReg(0x11000000); dl.AddPadding(3); dl.LoadBPReg(0xFEFFFFF0); if (Lightmap == ObjImporter.LightmapType.Map) { dl.LoadBPReg(0xF7003EF0); dl.LoadBPReg(0x293BF052); dl.LoadBPReg(0xC4080A8E); dl.AddPadding(5); dl.LoadBPReg(0xC508E370); } else if (Lightmap == ObjImporter.LightmapType.MapObj) { dl.LoadBPReg(0xF70038C0); dl.LoadBPReg(0x293BF3C9); dl.LoadBPReg(0xC408F860); dl.AddPadding(5); dl.LoadBPReg(0xC508FFF0); } dl.AddPadding(5); dl.LoadBPReg(0x12000000); } dl.PadToSize(0x1E0); dl.End(); shader.DisplayList = dl.GetBuffer(); return shader; } #endregion #region Shapes private void SetCurrentShape(string groupName, string materialName) { string shapeName = groupName + "__" + materialName; if (KnownShapes.ContainsKey(shapeName)) { CurrentShape = KnownShapes[shapeName]; } else { // make a new one! var context = MakeNewShapeContext(shapeName, CurrentModel.Materials[materialName]); KnownShapes[shapeName] = context; CurrentShape = context; } } private ShapeContext MakeNewShapeContext(string name, Material material) { var context = new ShapeContext(); var shape = new Shape(); shape.Unk = new byte[] { 0, 0, 0x14, 0, 0, 0, 0, 0x02, 0, 0, 0, 0x14 }; // 0x2e00 maybe? shape.DataFlags = 0x2600; shape.ExtraData = new ushort[0]; CurrentModel.Shapes.Add(name, shape); context.Shape = shape; context.Material = material; context.Quads = new List(); context.Triangles = new List(); context.UsedVertices = new BitArray(Positions.Count); context.UsedTexCoords = new BitArray(TexCoords.Count); context.UsedNormals = new BitArray(Normals.Count); return context; } private void AddFace(string[] cmd) { int vtxCount = cmd.Length - 1; if (vtxCount != 3 && vtxCount != 4) { throw new NotSupportedException(string.Format("cannot deal with a {0} vertices primitive", vtxCount)); } var output = new ushort[vtxCount * 3]; for (int i = 0; i < vtxCount; i++) { var s = cmd[i + 1]; int pos1 = s.IndexOf('/'); int pos2 = s.IndexOf('/', pos1 + 1); // thanks for generating groups with no texcoords, Max bool hasTexCoord = (pos1 != pos2 - 1); int vnum = int.Parse(s.Substring(0, pos1)) - 1; int tcnum = hasTexCoord ? (int.Parse(s.Substring(pos1 + 1, pos2 - pos1 - 1)) - 1) : 0; int nnum = int.Parse(s.Substring(pos2 + 1)) - 1; CurrentShape.UsedVertices[vnum] = true; if (hasTexCoord) CurrentShape.UsedTexCoords[tcnum] = true; CurrentShape.UsedNormals[nnum] = true; output[i * 3] = (ushort)vnum; output[i * 3 + 1] = (ushort)nnum; output[i * 3 + 2] = (ushort)tcnum; } if (vtxCount == 3) CurrentShape.Triangles.Add(output); else CurrentShape.Quads.Add(output); } private void FinaliseShape(ShapeContext context) { var shape = context.Shape; // Let's assemble the display lists. // First off, compute the vertex data arrays so we can decide on whether // to use 8-bit or 16-bit indexes to vertex data ushort[] posIndexes, texCoordIndexes, normalIndexes; var posDataArray = ComputeVertexDataArray(Positions, context.UsedVertices, out posIndexes); var tcDataArray = ComputeVertexDataArray(TexCoords, context.UsedTexCoords, out texCoordIndexes); var nrmDataArray = ComputeVertexDataArray(Normals, context.UsedNormals, out normalIndexes); bool u16PosIdx = (posDataArray.Length > 255); bool u16TcIdx = (tcDataArray.Length > 255); bool u16NrmIdx = (nrmDataArray.Length > 255); // ** Reverse Engineering DL 1 ** // 0x0A : CP command: Vertex Descriptor part 1 // 0x10 : CP command: Vertex Descriptor part 2 // 0x16 : XF command: Address 0x1008 ["XFMEM_VTXSPECS"], Transfer: 0 // 0x1F : Padding: NOP // 0x20 : CP command: Vertex Attribute Format part 1 // 0x26 : CP command: Vertex Attribute Format part 2 // 0x2C : CP command: Vertex Attribute Format part 3 // 0x32 : Dynamic CP command: Position Array Pointer // 0x38 : Dynamic CP command: Position Array Stride // 0x3E : Dynamic CP command: Normal Array Pointer // 0x44 : Dynamic CP command: Normal Array Pointer // 0x4A : Dynamic CP command: Colour 0 Array Pointer // 0x50 : Dynamic CP command: Colour 0 Array Stride // 0x56 : Dynamic CP command: Colour 1 Array Pointer // 0x5C : Dynamic CP command: Colour 1 Array Stride // 0x62 : Dynamic CP command: TexCoord 0 Array Pointer // 0x68 : Dynamic CP command: TexCoord 0 Array Stride // 0x6E : Dynamic CP command: TexCoord 1 Array Pointer // 0x74 : Dynamic CP command: TexCoord 1 Array Stride // 0x7A : Dynamic CP command: TexCoord 2 Array Pointer // 0x80 : Dynamic CP command: TexCoord 2 Array Stride // 0x86 : Dynamic CP command: TexCoord 3 Array Pointer // 0x8C : Dynamic CP command: TexCoord 3 Array Stride // 0x92 : Dynamic CP command: TexCoord 4 Array Pointer // 0x98 : Dynamic CP command: TexCoord 4 Array Stride // 0x9E : Dynamic CP command: TexCoord 5 Array Pointer // 0xA4 : Dynamic CP command: TexCoord 5 Array Stride // 0xAA : Dynamic CP command: TexCoord 6 Array Pointer // 0xB0 : Dynamic CP command: TexCoord 6 Array Stride // 0xB6 : Dynamic CP command: TexCoord 7 Array Pointer // 0xBC : Dynamic CP command: TexCoord 7 Array Stride bool usingLightmaps = (Lightmap != ObjImporter.LightmapType.None); // Now create vertex settings var vs = new VertexSettings(); vs.PositionDesc = u16PosIdx ? VertexSettings.DescType.Index16 : VertexSettings.DescType.Index8; vs.PositionFormat = VertexSettings.CompType.Float32; vs.PositionCount = VertexSettings.CompCount.Position3; vs.TexCoordDesc[0] = u16TcIdx ? VertexSettings.DescType.Index16 : VertexSettings.DescType.Index8; vs.TexCoordFormat[0] = VertexSettings.CompType.Float32; vs.TexCoordCount[0] = VertexSettings.CompCount.TexCoord2; vs.NormalDesc = u16NrmIdx ? VertexSettings.DescType.Index16 : VertexSettings.DescType.Index8; vs.NormalFormat = VertexSettings.CompType.Float32; vs.NormalCount = VertexSettings.CompCount.Normal3; if (Lightmap != ObjImporter.LightmapType.None) { vs.ColorDesc[0] = VertexSettings.DescType.Index8; vs.ColorFormat[0] = VertexSettings.CompClrType.RGBA8; vs.ColorCount[0] = VertexSettings.CompCount.Color4; } uint vd1, vd2, vat1, vat2, vat3; vs.GetDesc(out vd1, out vd2); vs.GetAttrFmt(out vat1, out vat2, out vat3); // I might need to create XFMEM_VTXSPECS... // test_lift uses 0x14. According to Dolphin's src, this means: // numcolors = 0, numnormals = 1 (just normal), numtextures = 1. Makes sense. // If lightmaps, then use: numcolors = 1, numnormals = 1, numtextures = 1 // NOTE: Dolphin's source names that as "numtextures" -- but in reality it defines // the number of texture coords that are sent in the vertex data, NOT textures! // This little bug was the source of tons of trouble on real consoles, since // previously I was using 3 (one regular texture; two lightmaps). // Lightmap texcoords are auto generated, so we now use 1 here. byte vtxSpecs; if (usingLightmaps) vtxSpecs = 1 | (1 << 2) | (1 << 4); else vtxSpecs = 0 | (1 << 2) | (1 << 4); // setting up Unk in the hopes it will work, I think this is what it's for.. var unkStream = new OutputStream(); unkStream.WriteUInt32(vd1); unkStream.WriteUInt32(vd2); unkStream.WriteUInt32(vtxSpecs); shape.Unk = unkStream.GetBuffer(); // now make the display list var dl1 = new DisplayListWriter(); dl1.AddPadding(10); dl1.LoadCPReg(0x50, vd1); dl1.LoadCPReg(0x60, vd2); dl1.LoadXFReg(0x1008, new byte[] { 0x00, 0x00, 0x00, vtxSpecs }); dl1.Nop(); dl1.LoadCPReg(0x70, vat1); dl1.LoadCPReg(0x80, vat2); dl1.LoadCPReg(0x90, vat3); // extend it // should it be bigger if more texcoords are used? maybe... dl1.PadToSize(0x80); dl1.End(); shape.DLBufferSize1 = 0xE0; shape.DisplayList1 = dl1.GetBuffer(); // Display List 2 is where all the primitive-related fun happens // However, before we do that, let's create the vertex data arrays // todo: better names var posData = new VertexPosData(); posData.ComponentCount = VertexSettings.CompCount.Position3; posData.ComponentType = VertexSettings.CompType.Float32; posData.EntryCount = (ushort)posDataArray.Length; posData.EntrySize = 12; posData.Fraction = 0; posData.Data = posDataArray; posData.Save(); shape.PosData = posData; CurrentModel.VtxPosData.Add("P_" + CurrentModel.VtxPosData.Count.ToString(), posData); var tcData = new VertexTexCoordData(); // a Quick Hack if (tcDataArray.Length == 0) { tcDataArray = new float[][] { new float[] { 0, 0 } }; texCoordIndexes[0] = 0; } tcData.ComponentCount = VertexSettings.CompCount.TexCoord2; tcData.ComponentType = VertexSettings.CompType.Float32; tcData.EntryCount = (ushort)tcDataArray.Length; tcData.EntrySize = 8; tcData.Fraction = 0; tcData.Data = tcDataArray; tcData.Save(); shape.TexCoordData[0] = tcData; CurrentModel.VtxTexCoordData.Add("T_" + CurrentModel.VtxTexCoordData.Count.ToString(), tcData); var nrmData = new VertexNrmData(); nrmData.ComponentCount = VertexSettings.CompCount.Normal3; nrmData.ComponentType = VertexSettings.CompType.Float32; nrmData.EntryCount = (ushort)nrmDataArray.Length; nrmData.EntrySize = 12; nrmData.Fraction = 0; nrmData.Data = nrmDataArray; nrmData.Save(); shape.NrmData = nrmData; CurrentModel.VtxNrmData.Add("N_" + CurrentModel.VtxNrmData.Count.ToString(), nrmData); // For lightmaps, before we get baked lighting working if (usingLightmaps) { var clrData = new VertexClrData(); clrData.ComponentCount = VertexSettings.CompCount.Color4; clrData.ComponentType = VertexSettings.CompType.UInt8; clrData.EntryCount = 1; clrData.EntrySize = 4; clrData.Fraction = 0; clrData.RawData = new byte[] { 255, 255, 255, 255 }; CurrentModel.VtxClrData.Add("C_" + CurrentModel.VtxClrData.Count.ToString(), clrData); shape.ClrData[0] = clrData; } var dl = new DisplayListWriter(); // face writing here is reversed because the wind order seems to be wrong // dunno if it applies to models exported from everything, or just Maya // or maybe it's a setting specified in one of the structs // 0 bytes are for colour indexes if (context.Triangles.Count > 0) { dl.BeginPrimitives(PrimitiveType.Triangles, 0, (ushort)(context.Triangles.Count * 3)); foreach (var tri in context.Triangles) { if (u16PosIdx) dl.WriteUInt16(posIndexes[tri[6]]); else dl.WriteByte((byte)posIndexes[tri[6]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[tri[7]]); else dl.WriteByte((byte)normalIndexes[tri[7]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[tri[8]]); else dl.WriteByte((byte)texCoordIndexes[tri[8]]); if (u16PosIdx) dl.WriteUInt16(posIndexes[tri[3]]); else dl.WriteByte((byte)posIndexes[tri[3]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[tri[4]]); else dl.WriteByte((byte)normalIndexes[tri[4]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[tri[5]]); else dl.WriteByte((byte)texCoordIndexes[tri[5]]); if (u16PosIdx) dl.WriteUInt16(posIndexes[tri[0]]); else dl.WriteByte((byte)posIndexes[tri[0]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[tri[1]]); else dl.WriteByte((byte)normalIndexes[tri[1]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[tri[2]]); else dl.WriteByte((byte)texCoordIndexes[tri[2]]); } } if (context.Quads.Count > 0) { dl.BeginPrimitives(PrimitiveType.Quads, 0, (ushort)(context.Quads.Count * 4)); foreach (var quad in context.Quads) { if (u16PosIdx) dl.WriteUInt16(posIndexes[quad[9]]); else dl.WriteByte((byte)posIndexes[quad[9]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[quad[10]]); else dl.WriteByte((byte)normalIndexes[quad[10]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[quad[11]]); else dl.WriteByte((byte)texCoordIndexes[quad[11]]); if (u16PosIdx) dl.WriteUInt16(posIndexes[quad[6]]); else dl.WriteByte((byte)posIndexes[quad[6]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[quad[7]]); else dl.WriteByte((byte)normalIndexes[quad[7]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[quad[8]]); else dl.WriteByte((byte)texCoordIndexes[quad[8]]); if (u16PosIdx) dl.WriteUInt16(posIndexes[quad[3]]); else dl.WriteByte((byte)posIndexes[quad[3]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[quad[4]]); else dl.WriteByte((byte)normalIndexes[quad[4]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[quad[5]]); else dl.WriteByte((byte)texCoordIndexes[quad[5]]); if (u16PosIdx) dl.WriteUInt16(posIndexes[quad[0]]); else dl.WriteByte((byte)posIndexes[quad[0]]); if (u16NrmIdx) dl.WriteUInt16(normalIndexes[quad[1]]); else dl.WriteByte((byte)normalIndexes[quad[1]]); if (usingLightmaps) dl.WriteByte(0); if (u16TcIdx) dl.WriteUInt16(texCoordIndexes[quad[2]]); else dl.WriteByte((byte)texCoordIndexes[quad[2]]); } } dl.End(); shape.DisplayList2 = dl.GetBuffer(); shape.DLBufferSize2 = (uint)shape.DisplayList2.Length; // now add it to DrawOpa var newInsn = new ByteCode.DrawShapeInstruction(); newInsn.NodeID = 0; newInsn.MaterialID = (ushort)CurrentModel.Materials.GetIndexForValue(context.Material); newInsn.ShapeID = (ushort)CurrentModel.Shapes.GetIndexForValue(shape); CurrentModel.Bytecode["DrawOpa"].Instructions.Add(newInsn); } private static float[][] ComputeVertexDataArray(List objData, BitArray usedFields, out ushort[] indexes) { indexes = new ushort[usedFields.Count]; var output = new List(); // How this will work: // I'll loop through every used vertex index in the BitArray. // If it's used, I will compare it to the ones already in the output list. // If the vertex is already in the output list, then I'll take the index of it // and write it to the "indexes" array. If not, then I'll add it to the output list and do that. // The "indexes" array matches input vertex indexes to the indexes in the optimised array. int vertexCount = usedFields.Count; int elementCount = objData[0].Length; for (int i = 0; i < usedFields.Count; i++) { if (usedFields[i]) { // this one is used, let's try to find it var thisVtx = objData[i]; bool foundIt = false; int j; for (j = 0; j < output.Count; j++) { var check = output[j]; bool isEqual = true; for (int k = 0; k < elementCount; k++) isEqual &= (thisVtx[k] == check[k]); if (isEqual) { foundIt = true; break; } } if (foundIt) { // it already exists, just add it to the indexes list indexes[i] = (ushort)j; } else { // nope, add it to the computed list indexes[i] = (ushort)output.Count; output.Add(thisVtx); } } } return output.ToArray(); } #endregion } }