Пример #1
0
        private void loadModelData(BlenderFile file)
        {
            models            = new List <BlenderModel>();
            transparentModels = new List <BlenderModel>();
            currentLayer      = 1;

            Structure curscene = file.GetStructuresOfType("FileGlobal")[0]["curscene"].Dereference()[0];
            ulong     next     = curscene["base.first"].Value;

            while (next != 0)
            {
                Structure objBase   = file.GetStructuresByAddress(next)[0];
                Structure obj       = objBase["object"].Dereference()[0];
                IField    data      = obj["data"];
                int       SDNAIndex = file.GetBlockByAddress((data as Field <ulong>).Value).SDNAIndex;
                while (file.StructureDNA.StructureList[SDNAIndex].StructureTypeName != "Mesh")
                {
                    ulong nextPointer = (objBase["next"] as Field <ulong>).Value;
                    if (nextPointer == 0)
                    {
                        return; // we've run out of objects in the list, and haven't found any meshes
                    }
                    objBase   = file.GetStructuresByAddress(nextPointer)[0];
                    obj       = objBase["object"].Dereference()[0];
                    data      = obj["data"];
                    SDNAIndex = file.GetBlockByAddress((data as Field <ulong>).Value).SDNAIndex;
                }

                Structure    mesh  = data.Dereference()[0];
                BlenderModel model = new BlenderModel(mesh, obj, GraphicsDevice, file);
                if (model.TextureHasTransparency)
                {
                    transparentModels.Add(model);
                }
                else
                {
                    models.Add(model);
                }

                next = (objBase["next"] as Field <ulong>).Value;
            }
        }
Пример #2
0
        internal Scene(BlenderFile file, PopulatedStructure scene)
        {
            Name = new string(scene["id.name"].GetValueAsCharArray()).Split('\0')[0].Substring(2);

            // todo: add lamp importing here
            string[]             validTypeNames = new[] { "Mesh" };
            List <BlenderObject> objects        = new List <BlenderObject>();
            int i = 0;

            ulong next   = scene["base.first"].GetValueAsPointer();
            ulong basact = scene["basact"].GetValueAsPointer(); // this is the address of the Base object

            while (next != 0)
            {
                if (next == basact)
                {
                    activeObjectIndex = i;
                }

                PopulatedStructure objBase = file.GetStructuresByAddress(next)[0];
                PopulatedStructure obj     = file.GetStructuresByAddress(objBase["object"].GetValueAsPointer())[0];
                Field data = obj["data"];
                ulong ptr  = data.GetValueAsPointer(); // this will be 0 for objects of the Empty type
                if (ptr == 0)
                {
                    objects.Add(new BlenderObject(file, obj));
                }
                else
                {
                    int SDNAIndex = file.GetBlockByAddress(ptr).SDNAIndex;
                    if (validTypeNames.Contains(file.StructureDNA.StructureList[SDNAIndex].StructureTypeName))
                    {
                        objects.Add(new BlenderObject(file, obj));
                    }
                }

                next = objBase["next"].GetValueAsPointer();
                i++;
            }
        }
Пример #3
0
        internal Mesh(PopulatedStructure mesh, BlenderFile file)
        {
            FileBlock materialArray;

            int pointerSize = mesh["mat"].Size;

            Name = new string(mesh["id.name"].GetValueAsCharArray()).Split('\0')[0].Substring(2);
            ulong mat = mesh["mat"].GetValueAsPointer();

            if (mat == 0 || (materialArray = file.GetBlockByAddress(mat)).Size % pointerSize != 0)
            {
                Materials = new Material[0];
            }
            else
            {
                int count = materialArray.Size % pointerSize;
                Materials = new Material[count];
                for (int i = 0; i < count; i++)
                {
                    Materials[i] = Material.GetOrCreateMaterial(
                        file,
                        file.GetStructuresByAddress(
                            pointerSize == 4 ? BitConverter.ToUInt32(materialArray.Data, count * pointerSize) :
                            BitConverter.ToUInt64(materialArray.Data, count * pointerSize)
                            )[0]
                        );
                }
            }
            float[] vectorTemp = mesh["loc"].GetValueAsFloatArray();
            Location   = new Vector3(vectorTemp[0], vectorTemp[1], vectorTemp[2]);
            vectorTemp = mesh["rot"].GetValueAsFloatArray();
            Rotation   = new Vector3(vectorTemp[0], vectorTemp[1], vectorTemp[2]);
            vectorTemp = mesh["size"].GetValueAsFloatArray();
            Size       = new Vector3(vectorTemp[0], vectorTemp[1], vectorTemp[2]);

            MeshBuilder primordialMesh = MeshBuilder.StartMesh(Name);

            // both structures use the same vertex structure
            List <Vector3> verts = new List <Vector3>();
            List <short[]> unconvertedNormals = new List <short[]>();

            foreach (PopulatedStructure s in file.GetStructuresByAddress(mesh["mvert"].GetValueAsPointer()))
            {
                float[] vector = s["co"].GetValueAsFloatArray();
                unconvertedNormals.Add(s["no"].GetValueAsShortArray());
                verts.Add(new Vector3(vector[0], vector[1], vector[2]));
            }
            List <Vector3> normals = convertNormals(unconvertedNormals);

            VertexPositionNormalTexture[] vertices;
            BasicMaterialContent          bmc;

            // todo: not yet sure which format versions of Blender between 2.62 and 2.65 use.
            if (float.Parse(file.VersionNumber) >= 2.66f) // uses edges, loops, and polys (Blender 2.66+)
            {
                vertices = loadNewModel(file, mesh, verts, normals, out bmc);
            }
            else // uses MFace (Blender 2.49-2.61)
            {
                vertices = loadOldModel(file, mesh, verts, normals, out bmc);
            }

            MeshBuilder mb = MeshBuilder.StartMesh(Name);

            foreach (VertexPositionNormalTexture v in vertices)
            {
                mb.CreatePosition(v.Position);
            }
            int uvChannel     = mb.CreateVertexChannel <Vector2>(VertexChannelNames.TextureCoordinate(0));
            int normalChannel = mb.CreateVertexChannel <Vector3>(VertexChannelNames.Normal());
            int j             = 0;

            foreach (VertexPositionNormalTexture v in vertices)
            {
                mb.SetVertexChannelData(uvChannel, v.TextureCoordinate);
                mb.SetVertexChannelData(normalChannel, v.Normal);

                mb.AddTriangleVertex(j++);
            }
        }
Пример #4
0
        private VertexPositionNormalTexture[] loadOldModel(BlenderFile file, PopulatedStructure mesh, List <Vector3> verts, List <Vector3> normals, out BasicMaterialContent bmc)
        {
            // I believe this function has a bug when used on a mesh that has unconnected chunks of vertices;
            // however the only file I currently have that exhibits this problem decompresses to 240MB when I use the HTML
            // renderer tool, so I can't feasibly poke through the data to see what's going wrong.

            List <VertexPositionNormalTexture> output = new List <VertexPositionNormalTexture>();

            bmc = new BasicMaterialContent();

            List <int[]>     faces  = new List <int[]>();
            List <float[, ]> tFaces = new List <float[, ]>();

            foreach (PopulatedStructure s in file.GetStructuresByAddress(mesh["mface"].GetValueAsPointer()))
            {
                faces.Add(new[] { s["v1"].GetValueAsInt(), s["v2"].GetValueAsInt(), s["v3"].GetValueAsInt(), s["v4"].GetValueAsInt() });
            }
            foreach (PopulatedStructure s in file.GetStructuresByAddress(mesh["mtface"].GetValueAsPointer()))
            {
                tFaces.Add((float[, ])s["uv"].GetValueAsMultidimensionalArray());
            }

            // assume all faces use same texture
            PopulatedStructure image = file.GetStructuresByAddress(file.GetStructuresByAddress(mesh["mtface"].GetValueAsPointer())[0]["tpage"].GetValueAsPointer())[0];

            if (image["packedfile"].GetValueAsPointer() != 0)
            {
                byte[] rawImage = file.GetBlockByAddress(file.GetStructuresByAddress(image["packedfile"].GetValueAsPointer())[0]["data"].GetValueAsPointer()).Data;
                string filename = Name + "_" + new string(image["id.name"].GetValueAsCharArray()).Split('\0')[0].Substring(2);
                using (BinaryWriter s = new BinaryWriter(File.Open(filename, FileMode.Create)))
                    s.Write(rawImage);
                bmc.Texture = new ExternalReference <TextureContent>(filename);
            }
            else
            {
                try
                {
                    string texturePath = image["name"].ToString().Split('\0')[0].Replace("/", "\\").Replace("\\\\", "\\");
                    string filePath    = file.GetStructuresOfType("FileGlobal")[0]["filename"].ToString();
                    filePath = filePath.Substring(0, filePath.LastIndexOf('\\'));
                    File.Copy((filePath + texturePath).Replace("\'", ""), Name + "_" + new string(image["id.name"].GetValueAsCharArray()).Split('\0')[0].Substring(2));
                }
                catch
                {
                    //texture = defaultTex;
                }
            }

            int j = 0;

            foreach (int[] face in faces)
            {
                Vector3[] faceVerts       = new Vector3[face.Length];
                Vector3[] faceVertNormals = new Vector3[face.Length];
                Vector2[] faceUVs         = new Vector2[face.Length];
                for (int i = 0; i < face.Length; i++)
                {
                    faceVerts[i]       = verts[face[i]];
                    faceVertNormals[i] = normals[face[i]];
                    faceUVs[i]         = new Vector2(tFaces[j][i, 0], tFaces[j][i, 1]);
                }
                j++;

                // 2, 1, 0
                for (int i = 2; i >= 0; i--)
                {
                    output.Add(new VertexPositionNormalTexture(faceVerts[i], faceVertNormals[i], faceUVs[i]));
                }

                // 3, 2, 0
                for (int i = 3; i >= 1; i--)
                {
                    output.Add(new VertexPositionNormalTexture(faceVerts[i == 1 ? 0 : i], faceVertNormals[i == 1 ? 0 : i], faceUVs[i == 1 ? 0 : i]));
                }
            }

            return(output.ToArray());
        }
Пример #5
0
        private void writeField(StreamWriter writer, IField field, bool odd, int fieldNumber)
        {
            string fieldVal = field.ToString();

            if (field.IsPointer)
            {
                if (field.IsArray)
                {
                    for (int i = 0; i < fieldVal.Length; i++)
                    {
                        if (fieldVal[i] == '0') // we can assume this is the start of a pointer, so the next few chars are valid
                        {
                            int j = 0;
                            do
                            {
                                j++;
                            } while(fieldVal[i + j] != ',' && fieldVal[i + j] != ' ');
                            if (fieldVal.Substring(i, j) != "0x0")
                            {
                                string newString = "<a href=\"#" + fieldVal.Substring(i, j) + "\">" + fieldVal.Substring(i, j) + "</a>";
                                fieldVal = fieldVal.Replace(fieldVal.Substring(i, j), newString);
                                j        = newString.Length;
                            }
                            i += j;
                        }
                    }
                }
                else
                {
                    if (fieldVal != "0x0")
                    {
                        fieldVal = "<a href=\"#" + fieldVal + "\">" + fieldVal + "</a>";
                        if (field.IsPointerToPointer)
                        {
                            fieldVal += " (pointer to pointer array: ";
                            FileBlock pointed = parsedFile.GetBlockByAddress((field as Field <ulong>).Value); // this is probably a safe cast
                            if (pointed != null && pointed.Size % parsedFile.PointerSize == 0)                // probably a pointer
                            {
                                ulong[] pointers = new ulong[pointed.Size / parsedFile.PointerSize];
                                int     index    = 0;
                                while (index != pointed.Size)
                                {
                                    pointers[index / parsedFile.PointerSize] = parsedFile.PointerSize == 4 ? BitConverter.ToUInt32(pointed.Data, index) : BitConverter.ToUInt64(pointed.Data, index);
                                    index += parsedFile.PointerSize;
                                }
                                string[] temp = pointers.Select(i => { return("0x" + (i == 0 ? "0" : i.ToString("X" + (parsedFile.PointerSize * 2)))); }).ToArray();
                                temp = temp.Select(i => { return(i == "0x0" ? i : "<a href=\"#" + i + "\">" + i + "</a>"); }).ToArray();

                                fieldVal += "{ " + string.Join(", ", temp) + " })";
                            }
                            else
                            {
                                fieldVal += "...but target doesn't look like a pointer array)"; // probably will crash instead of getting to this
                            }
                        }
                    }
                }
            }

            string typeName = field.TypeName + (field.IsArray ? (field.Is2DArray ? "[]" : "") + "[]" : "");

            if (!field.IsArray && field.IsPointer && field.IsPrimitive && (field as Field <ulong>).Value != 0)
            {
                FileBlock associatedBlock = parsedFile.GetBlockByAddress((field as Field <ulong>).Value);
                if (associatedBlock != null)
                {
                    typeName += " (points to " + (associatedBlock.Size == associatedBlock.Count * parsedFile.StructureDNA.StructureList[associatedBlock.SDNAIndex].StructureTypeSize ?
                                                  parsedFile.StructureDNA.StructureList[associatedBlock.SDNAIndex].StructureTypeName : "raw data") + ")";
                }
            }

            writeTableRow(odd ? "odd" : "even", writer, fieldNumber.ToString(), field.FullyQualifiedName, field.Parent.Parent == null ? "(this)" : field.ParentType, typeName, field.Length > 1 ? field.Size + " * " + field.Length + " (" + (field.Size * field.Length) + ")" : field.Size.ToString(), fieldVal);
        }
Пример #6
0
        public BlenderModel(Structure mesh, Structure obj, GraphicsDevice GraphicsDevice, BlenderFile file)
        {
            // If I was less sloppy, I would have used casts to generics instead of the raw dynamic Value on IField,
            // but I'm lazy and this code is soon to go away anyway.
            if(defaultTex == null)
            {
                defaultTex = new Texture2D(GraphicsDevice, 1, 1);
                defaultTex.SetData(new Color[] { Color.Gray });
            }

            this.GraphicsDevice = GraphicsDevice;

            // both structures use the same vertex structure
            List<Vector3> verts = new List<Vector3>();
            List<short[]> unconvertedNormals = new List<short[]>();
            foreach(Structure s in mesh["mvert"].Dereference())
            {
                float[] vector = s["co"].Value;
                unconvertedNormals.Add(s["no"].Value);
                verts.Add(new Vector3(vector[0], vector[1], vector[2]));
            }

            List<Vector3> normals = convertNormals(unconvertedNormals);
            VertexPositionNormalTexture[] vertices;
            Texture2D texture;
            // todo: not yet sure which format versions of Blender between 2.62 and 2.65 use.
            if(float.Parse(file.VersionNumber) >= 2.66f) // uses edges, loops, and polys (Blender 2.66+)
                vertices = loadNewModel(file, mesh, verts, normals, out texture);
            else // uses MFace (Blender 2.49-2.61)
                vertices = loadOldModel(file, mesh, verts, normals, out texture);

            VertexPositionColor[] normalVerts = new VertexPositionColor[normals.Count * 2];
            for(int i = 0; i < verts.Count * 2; i += 2)
            {
                normalVerts[i] = new VertexPositionColor(verts[i / 2], Color.MidnightBlue);
                normalVerts[i + 1] = new VertexPositionColor(verts[i / 2] + normals[i / 2] * 0.25f, Color.MidnightBlue);
            }

            float[] posVector = obj["loc"].Value;
            this.Position = new Vector3(posVector[0], posVector[1], posVector[2]);
            float[] scaleVector = obj["size"].Value;
            this.Scale = new Vector3(scaleVector[0], scaleVector[1], scaleVector[2]);
            float[] rotVector = obj["rot"].Value;
            this.Rotation = Quaternion.CreateFromYawPitchRoll(rotVector[1], rotVector[0], rotVector[2]);
            this.Vertices = vertices;
            this.NormalVerts = normalVerts;
            this.VertexBuffer = new VertexBuffer(GraphicsDevice, VertexPositionNormalTexture.VertexDeclaration, this.Vertices.Length, BufferUsage.None);
            this.NormalBuffer = new VertexBuffer(GraphicsDevice, VertexPositionColor.VertexDeclaration, this.NormalVerts.Length, BufferUsage.None);
            this.Texture = texture;
            this.Name = new string(obj["id.name"].Value).Split('\0')[0].Substring(2); // remove null term, remove first two characters

            // LSB on represents layer 1, next bit is layer 2, etc
            this.Layer = obj["lay"].Value;

            // the "mat" field is a pointer to a pointer (technically, a pointer to an array of pointers)
            // I'm not sure what to do with multiple materials, so just use the first one
            ulong blockaddr = mesh["mat"].Value;
            if(blockaddr != 0)
            {
                Structure mat = file.GetStructuresByAddress(BitConverter.ToUInt32(file.GetBlockByAddress(blockaddr).Data, 0))[0];
                this.TextureHasTransparency = mat["game.alpha_blend"].Value != 0;

                int mode = mat["mode"].Value;
                this.LightingEnabled = (mode & 4) == 0; // as far as I can tell, this is where "shadeless" is stored.
            }
            else
                this.LightingEnabled = true;
        }
Пример #7
0
        private VertexPositionNormalTexture[] loadNewModel(BlenderFile file, Structure mesh, List<Vector3> verts, List<Vector3> normals, out Texture2D texture)
        {
            List<VertexPositionNormalTexture> output = new List<VertexPositionNormalTexture>();

            List<Vector2> edges = new List<Vector2>(); // using x as index1 and y as index2
            foreach(Structure s in mesh["medge"].Dereference())
                edges.Add(new Vector2((s["v1"] as IField<int>).Value, (s["v2"] as IField<int>).Value));
            // a "loop" is a vertex index and an edge index. Groups of these are used to define a "poly", which is a face. 
            List<Vector2> loops = new List<Vector2>(); // using x as "v" and y as "e"
            foreach(Structure s in mesh["mloop"].Dereference())
                loops.Add(new Vector2((s["v"] as IField<int>).Value, (s["e"] as IField<int>).Value));
            List<Vector2> uvLoops = null; // using x as u and y as v
            Vector2[] backupUVs = new[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0) }; // in case uvLoops is null
            if(mesh["mloopuv"].Value != 0)
            {
                uvLoops = new List<Vector2>();
                foreach(Structure s in mesh["mloopuv"].Dereference())
                {
                    float[] uv = s["uv"].Value;
                    uvLoops.Add(new Vector2(uv[0], uv[1]));
                }
            }
            List<Vector2> polys = new List<Vector2>(); // using x as "loopstart" and y as "totloop" (loop length)
            foreach(Structure s in mesh["mpoly"].Dereference())
                polys.Add(new Vector2((s["loopstart"] as IField<int>).Value, (s["totloop"] as IField<int>).Value));
            // assume all faces use same texture for now
            if(mesh["mtpoly"].Value != 0)
            {
                try
                {
                    // todo: sometimes this line fails, probably due to "assume all faces use same texture"
                    Structure image = mesh["mtpoly"].Dereference()[0]["tpage"].Dereference()[0];
                    if(image["packedfile"].Value != 0)
                    {
                        byte[] rawImage = file.GetBlockByAddress(image["packedfile"].Dereference()[0]["data"].Value).Data;
                        using(Stream s = new MemoryStream(rawImage))
                            texture = Texture2D.FromStream(GraphicsDevice, s);
                    }
                    else
                    {
                        string texturePath = image["name"].ToString().Split('\0')[0].Replace("/", "\\").Replace("\\\\", "\\");
                        string filePath = file.GetStructuresOfType("FileGlobal")[0]["filename"].ToString();
                        filePath = filePath.Substring(0, filePath.LastIndexOf('\\'));
                        using(Stream s = File.Open((filePath + texturePath).Replace("\'", ""), FileMode.Open, FileAccess.Read))
                            texture = Texture2D.FromStream(GraphicsDevice, s);
                    }
                }
                catch
                {
                    texture = defaultTex;
                }
            }
            else
            {
                texture = new Texture2D(GraphicsDevice, 1, 1);
                texture.SetData(new Color[] { Color.Gray });
            }
            // loops of length 3 are triangles and can be directly added to the vertex list. loops of length 4
            // are quads, and have to be split into two triangles.
            foreach(Vector2 poly in polys)
            {
                Vector2[] faceEdges = new Vector2[(int)poly.Y];
                Vector2[] faceUVs = new Vector2[faceEdges.Length];
                int j = 0;
                int loopOffset = (int)poly.X;
                for(int i = loopOffset; i < (int)poly.Y + loopOffset; i++)
                {
                    faceEdges[j] = edges[(int)loops[i].Y];
                    faceUVs[j] = uvLoops == null ? backupUVs[i - loopOffset] : uvLoops[i];
                    j++;
                }
                Vector3[] faceVerts = new Vector3[faceEdges.Length];
                Vector3[] faceVertNormals = new Vector3[faceEdges.Length];
                for(int i = 0; i < faceEdges.Length; i++)
                {
                    int index = (int)(loops[loopOffset + i].X == faceEdges[i].X ? faceEdges[i].Y : faceEdges[i].X);
                    faceVerts[i] = verts[index];
                    faceVertNormals[i] = normals[index];
                }
                if(faceVerts.Length == 3) // already a triangle
                {
                    // push 0 to the end
                    Vector2 temp = faceUVs[0];
                    faceUVs[0] = faceUVs[1];
                    faceUVs[1] = temp;
                    temp = faceUVs[1];
                    faceUVs[1] = faceUVs[2];
                    faceUVs[2] = temp;

                    for(int i = 2; i >= 0; i--) // 2, 1, 0
                        output.Add(new VertexPositionNormalTexture(faceVerts[i], faceVertNormals[i], faceUVs[i]));
                }
                else if(faceVerts.Length == 4) // quad, split into tris
                {
                    // swap 3 with 1 and 2 with 3
                    Vector2 temp = faceUVs[1];
                    faceUVs[1] = faceUVs[3];
                    faceUVs[3] = temp;
                    temp = faceUVs[2];
                    faceUVs[2] = faceUVs[3];
                    faceUVs[3] = temp;

                    // 2, 1, 0
                    for(int i = 2; i >= 0; i--)
                        output.Add(new VertexPositionNormalTexture(faceVerts[i], faceVertNormals[i], faceUVs[i]));

                    // 3, 2, 0
                    for(int i = 3; i >= 1; i--)
                        output.Add(new VertexPositionNormalTexture(faceVerts[i == 1 ? 0 : i], faceVertNormals[i == 1 ? 0 : i], faceUVs[i == 1 ? 0 : i]));
                }
            }

            return output.ToArray();
        }
Пример #8
0
        private VertexPositionNormalTexture[] loadOldModel(BlenderFile file, Structure mesh, List<Vector3> verts, List<Vector3> normals, out Texture2D texture)
        {
            // I believe this function has a bug when used on a mesh that has unconnected chunks of vertices;
            // however the only file I currently have that exhibits this problem decompresses to 240MB when I use the HTML
            // renderer tool, so I can't feasibly poke through the data to see what's going wrong.

            List<VertexPositionNormalTexture> output = new List<VertexPositionNormalTexture>();

            List<int[]> faces = new List<int[]>();
            List<float[][]> tFaces = new List<float[][]>();
            foreach(Structure s in mesh["mface"].Dereference())
                faces.Add(new int[] { s["v1"].Value, s["v2"].Value, s["v3"].Value, s["v4"].Value });
            foreach(Structure s in mesh["mtface"].Dereference())
                tFaces.Add((float[][])s["uv"].Value);

            // assume all faces use same texture
            Structure image = mesh["mtface"].Dereference()[0]["tpage"].Dereference()[0];
            if(image["packedfile"].Value != 0)
            {
                byte[] rawImage = file.GetBlockByAddress(image["packedfile"].Dereference()[0]["data"].Value).Data;
                using(Stream s = new MemoryStream(rawImage))
                    texture = Texture2D.FromStream(GraphicsDevice, s);
            }
            else
            {
                try
                {
                    string texturePath = image["name"].ToString().Split('\0')[0].Replace("/", "\\").Replace("\\\\", "\\");
                    string filePath = file.GetStructuresOfType("FileGlobal")[0]["filename"].ToString();
                    filePath = filePath.Substring(0, filePath.LastIndexOf('\\'));
                    using(Stream s = File.Open((filePath + texturePath).Replace("\'", ""), FileMode.Open, FileAccess.Read))
                        texture = Texture2D.FromStream(GraphicsDevice, s);
                }
                catch
                {
                    texture = defaultTex;
                }
            }

            int j = 0;
            foreach(int[] face in faces)
            {
                Vector3[] faceVerts = new Vector3[face.Length];
                Vector3[] faceVertNormals = new Vector3[face.Length];
                Vector2[] faceUVs = new Vector2[face.Length];
                for(int i = 0; i < face.Length; i++)
                {
                    faceVerts[i] = verts[face[i]];
                    faceVertNormals[i] = normals[face[i]];
                    faceUVs[i] = new Vector2(tFaces[j][i][0], tFaces[j][i][1]);
                }
                j++;

                // 2, 1, 0
                for(int i = 2; i >= 0; i--)
                    output.Add(new VertexPositionNormalTexture(faceVerts[i], faceVertNormals[i], faceUVs[i]));

                // 3, 2, 0
                for(int i = 3; i >= 1; i--)
                    output.Add(new VertexPositionNormalTexture(faceVerts[i == 1 ? 0 : i], faceVertNormals[i == 1 ? 0 : i], faceUVs[i == 1 ? 0 : i]));
            }

            return output.ToArray();
        }