private void loadFile(string filename) { loadedFile = new BlenderFile(filename); fileTree.Nodes.Clear(); fileTree.ShowRootLines = true; List <TreeNode> nodes = new List <TreeNode>(); foreach (Structure[] structure in loadedFile.Structures) { nodes.Add(loadNode(structure)); } TreeNode rawBlocks = new TreeNode("[Raw data blocks]"); foreach (string s in loadedFile.RawBlockMessages) { rawBlocks.Nodes.Add(new TreeNode("[" + s.Split(' ')[0] + "]: " + s.Split(' ')[1]) { Tag = s }); } nodes.Add(rawBlocks); unfilteredNodeList = nodes.ToArray(); // the nodes will get added to the TreeView when filterBox_LostFocus() is called List <string> temp = new List <string>(); foreach (TreeNode node in unfilteredNodeList) { Structure s = node.Tag as Structure; if (s != null) { string typename = s.TypeName; if (!temp.Contains(typename)) { temp.Add(typename); } } else if (node.Text.StartsWith("[List of")) { foreach (TreeNode child in node.Nodes) { if ((s = child.Tag as Structure) != null) { string typename = s.TypeName; if (!temp.Contains(typename)) { temp.Add(typename); } } } } } rootNodeTypeNames = temp.ToArray(); filterBox.AutoCompleteCustomSource = new AutoCompleteStringCollection(); filterBox.AutoCompleteCustomSource.AddRange(rootNodeTypeNames); filterBox.Enabled = true; filterBox_LostFocus(this, null); }
/*! This methode is called from LoadFile() and executes the actual loading * Runs in own thread */ private void LoadFileWorker(object sender, DoWorkEventArgs e) { BlenderFile b = new BlenderFile(Path); List <BlenderMesh> blenderMeshes = new List <BlenderMesh>(); blenderObjects = b.readObject(); blenderMeshes = b.readMesh(); unityMeshes = BlenderFile.createSubmeshesForUnity(blenderMeshes); return; }
/// <summary> /// Creates a new <pre>Material</pre> with the given arguments, unless the <pre>Material</pre> was created already. /// If the <pre>Material</pre> was already created, returns a reference to the previously created <pre>Material</pre>. /// </summary> internal static Material GetOrCreateMaterial(BlenderFile file, PopulatedStructure material) { if (materialDict.Keys.Contains(material.ContainingBlock.OldMemoryAddress)) { return(materialDict[material.ContainingBlock.OldMemoryAddress]); } Material m = new Material(file, material); materialDict.Add(material.ContainingBlock.OldMemoryAddress, m); return(m); }
static void Main(string[] args) { // gets path from args or user string path = getPath(args); Console.WriteLine("Processing..."); BlenderFile reader = new BlenderFile(path); // write html file writeFile(path, reader); // "press any key to continue" Console.ReadLine(); }
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; } }
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++; } }
internal BlendFile(BlenderFile file) { Version = file.VersionNumber; PopulatedStructure fileGlobal = file.GetStructuresOfType("FileGlobal")[0]; Filename = new string(fileGlobal["filename"].GetValueAsCharArray()).Split('\0')[0]; Revision = fileGlobal["revision"].GetValueAsInt(); ulong curSceneAddr = fileGlobal["curscene"].GetValueAsPointer(); PopulatedStructure[] scenes = file.GetStructuresOfType("Scene"); int i = 0; Scenes = new Scene[scenes.Length]; foreach (PopulatedStructure scene in scenes) { if (scene.ContainingBlock.OldMemoryAddress == curSceneAddr) { currentSceneIndex = i; } Scenes[i++] = new Scene(file, scene); } }
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++); } }
private VertexPositionNormalTexture[] loadNewModel(BlenderFile file, PopulatedStructure mesh, List <Vector3> verts, List <Vector3> normals, out BasicMaterialContent bmc) { List <VertexPositionNormalTexture> output = new List <VertexPositionNormalTexture>(); List <Vector2> edges = new List <Vector2>(); // using x as index1 and y as index2 foreach (PopulatedStructure s in file.GetStructuresByAddress(mesh["medge"].GetValueAsPointer())) { edges.Add(new Vector2(s["v1"].GetValueAsInt(), s["v2"].GetValueAsInt())); } // 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 (PopulatedStructure s in file.GetStructuresByAddress(mesh["mloop"].GetValueAsPointer())) { loops.Add(new Vector2(s["v"].GetValueAsInt(), s["e"].GetValueAsInt())); } 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"].GetValueAsPointer() != 0) { uvLoops = new List <Vector2>(); foreach (PopulatedStructure s in file.GetStructuresByAddress(mesh["mloopuv"].GetValueAsPointer())) { float[] uv = s["uv"].GetValueAsFloatArray(); 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 (PopulatedStructure s in file.GetStructuresByAddress(mesh["mpoly"].GetValueAsPointer())) { polys.Add(new Vector2(s["loopstart"].GetValueAsInt(), s["totloop"].GetValueAsInt())); } // assume all faces use same texture for now //if(mesh["mtpoly"].GetValueAsPointer() != 0) //{ // try // { // // todo: sometimes this line fails, probably due to "assume all faces use same texture" // PopulatedStructure image = file.GetStructuresByAddress(file.GetStructuresByAddress(mesh["mtpoly"].GetValueAsPointer())[0]["tpage"].GetValueAsPointer())[0]; // if(image["packedfile"].GetValueAsPointer() != 0) // { // byte[] rawImage = file.GetBlockByAddress(file.GetStructuresByAddress(image["packedfile"].GetValueAsPointer())[0]["data"].GetValueAsPointer()).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])); } } } bmc = new BasicMaterialContent(); return(output.ToArray()); }
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()); }
// There's a good number of additional fields relating to game properties; as well as logic bricks. // Might be interesting to someone, but not especially interesting to me. internal BlenderObject(BlenderFile file, PopulatedStructure obj) { }
/// <summary> /// Creates a new HtmlWriter. /// </summary> /// <param name="path">Output path.</param> /// <param name="friendlyFileName">Input file's friendly name ("cow.blend").</param> public HtmlWriter(BlenderFile parsedFile, string path, string friendlyFileName) { this.path = path; friendly = friendlyFileName; this.parsedFile = parsedFile; }
private static void writeFile(string path, BlenderFile reader) { new HtmlWriter(reader, path.Substring(path.LastIndexOf('\\') + 1, path.LastIndexOf('.') - path.LastIndexOf('\\') - 1) + ".html", path.Substring(path.LastIndexOf('\\') + 1, path.LastIndexOf('.') - path.LastIndexOf('\\') - 1) + ".html").WriteBlendFileToHtml(); Console.WriteLine("Processing complete. Output in " + path.Substring(path.LastIndexOf('\\') + 1, path.LastIndexOf('.') - path.LastIndexOf('\\') - 1) + ".html."); }
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; }
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(); }
private Material(BlenderFile file, PopulatedStructure material) { }