private Vector3 CalcFaceNormal(SMMesh mesh, SMTriangle triangle)
        {
            var v1 = mesh.Vertices[triangle.v1];
            var v2 = mesh.Vertices[triangle.v2];
            var v3 = mesh.Vertices[triangle.v3];

            var UX = v2.Position.X - v1.Position.X;
            var UY = v2.Position.Y - v1.Position.Y;
            var UZ = v2.Position.Z - v1.Position.Z;

            var VX = v3.Position.X - v1.Position.X;
            var VY = v3.Position.Y - v1.Position.Y;
            var VZ = v3.Position.Z - v1.Position.Z;

            var NX = UY * VZ - UZ * VY;
            var NY = UZ * VX - UX * VZ;
            var NZ = UX * VY - UY * VX;

            var len = Math.Sqrt(NX * NX + NY * NY + NZ * NZ);

            return(new Vector3
            {
                X = (float)(NX / len),
                Y = (float)(NY / len),
                Z = (float)(NZ / len)
            });
        }
        // This REQUIRES a backing CGFX file and doesn't contain enough data to regenerate one from scratch
        public SimplifiedModel(CGFX cgfx)
        {
            this.cgfx = cgfx;

            // Models are always the first entry per CGFX standard
            var models = cgfx.Data.Entries[0]?.Entries;

            // Textures are the second
            var textures = cgfx.Data.Entries[1]?.Entries.Select(e => e.EntryObject).Cast <DICTObjTexture>().ToList();

            if (textures != null && textures.Count > 0)
            {
                Textures = new SMTexture[textures.Count];

                for (var t = 0; t < textures.Count; t++)
                {
                    Bitmap textureBitmap = null;
                    var    name          = textures[t].Name;
                    var    textureData   = textures[t];

                    if (textureData != null)
                    {
                        var textureRGBA = TextureCodec.ConvertTextureToRGBA(new Utility(null, null, Endianness.Little), textureData.TextureCGFXData, textureData.TextureFormat, (int)textureData.Width, (int)textureData.Height);

                        textureBitmap = new Bitmap((int)textureData.Width, (int)textureData.Height, PixelFormat.Format32bppArgb);
                        var imgData = textureBitmap.LockBits(new Rectangle(0, 0, textureBitmap.Width, textureBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
                        Marshal.Copy(textureRGBA, 0, imgData.Scan0, textureRGBA.Length);
                        textureBitmap.UnlockBits(imgData);
                        textureBitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
                    }

                    var smTexture = new SMTexture(name, textureBitmap);
                    Textures[t] = smTexture;
                }
            }

            if (models != null && models.Count > 0)
            {
                // This probably isn't a difficult problem to work around, but it's out of my scope at this time
                if (models.Count != 1)
                {
                    throw new InvalidOperationException("File contains more than one model; only supporting one for now.");
                }

                var model = (DICTObjModel)models.First().EntryObject;

                // NOTE: Currently NOT committing the skeleton back, we're just keeping it for model software
                var bones = model.Skeleton?.Bones.Entries.Select(e => e.EntryObject).Cast <DICTObjBone>().ToList() ?? new List <DICTObjBone>();

                Bones = bones.Select(b => new SMBone
                {
                    Name           = b.Name,
                    ParentName     = b.Parent?.Name,
                    Rotation       = b.Rotation,
                    Translation    = b.Translation,
                    Scale          = b.Scale,
                    LocalTransform = b.LocalTransform
                }).ToArray();

                Meshes = new SMMesh[model.Meshes.Length];

                for (var m = 0; m < model.Meshes.Length; m++)
                {
                    var mesh  = model.Meshes[m];
                    var shape = model.Shapes[mesh.ShapeIndex];

                    // There might be some clever way of handling multiple vertex buffers
                    // (if it actually happens) but I'm not worried about it. Only looking
                    // for a single one that is VertexBufferInterleaved.
                    var vertexBuffersInterleaved = shape.VertexBuffers.Where(vb => vb is VertexBufferInterleaved);
                    if (vertexBuffersInterleaved.Count() != 1)
                    {
                        throw new InvalidOperationException("Unsupported count of VertexBuffers in VertexBufferInterleaved format");
                    }

                    // Only expecting / supporting 1 SubMesh entry
                    if (shape.SubMeshes.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of SubMeshes");
                    }

                    var subMesh = shape.SubMeshes[0];

                    // The BoneReferences in the SubMesh are what the vertex's local index references.
                    var boneReferences = subMesh.BoneReferences;

                    // These aren't "faces" in the geometrical sense, but rather a header of sorts
                    if (subMesh.Faces.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of Faces");
                    }

                    var faceHeader = subMesh.Faces[0];

                    // Again, just one FaceDescriptor...
                    if (faceHeader.FaceDescriptors.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of FaceDescriptors");
                    }

                    var faceDescriptor = faceHeader.FaceDescriptors[0];

                    // We're also only supporting triangles at this point; the model format probably
                    // allows for more groups of geometry, but again, out of my scope
                    if (faceDescriptor.PrimitiveMode != FaceDescriptor.PICAPrimitiveMode.Triangles)
                    {
                        throw new InvalidOperationException("Only supporting triangles format");
                    }

                    // Vertices are stored (in GPU-compatible form)
                    var vertexBuffer      = (VertexBufferInterleaved)vertexBuffersInterleaved.Single();
                    var vertexBufferIndex = shape.VertexBuffers.IndexOf(vertexBuffer);
                    var attributes        = vertexBuffer.Attributes.Select(a => VertexBufferCodec.PICAAttribute.GetPICAAttribute(a)).ToList();

                    // The following are the only VertexAttributes we are supporting at this time
                    var supportedAttributes = new List <VertexBuffer.PICAAttributeName>
                    {
                        VertexBuffer.PICAAttributeName.Position,
                        VertexBuffer.PICAAttributeName.Normal,
                        VertexBuffer.PICAAttributeName.TexCoord0,
                        VertexBuffer.PICAAttributeName.BoneIndex,
                        VertexBuffer.PICAAttributeName.BoneWeight,

                        // Caution: Vertex color may not be supported by all model editors!
                        VertexBuffer.PICAAttributeName.Color
                    };

                    // Check if any unsupported attributes are in use
                    var unsupportedAttributes = attributes.Where(a => !supportedAttributes.Contains(a.Name)).Select(a => a.Name);
                    if (unsupportedAttributes.Any())
                    {
                        throw new InvalidOperationException($"This model is using the following unsupported attributes: {string.Join(", ", unsupportedAttributes)}");
                    }

                    var nativeVertices = VertexBufferCodec.GetVertices(shape, vertexBufferIndex);

                    // Convert to the simplified vertices
                    var boneIndexCount  = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneIndex);  // How many bone indices are actually used
                    var boneWeightCount = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneWeight); // How many bone weights are actually used

                    // FIXME? There seems to be, on occasion, a bone relationship that points to
                    // the entire mesh but not assigned to any of the vertices. So basically the
                    // vertices are recorded as having no bone indices but in fact are all dependent
                    // upon associating with bone index 0 (?) This will force it to use at least
                    // one bone index even if zero is specified for this case.
                    if (boneReferences != null && boneReferences.Count > 0)
                    {
                        boneIndexCount = Math.Max(boneIndexCount, 1);
                    }

                    var vertices = nativeVertices.Select(v => new SMVertex
                    {
                        Position    = new Vector3(v.Position.X, v.Position.Y, v.Position.Z),
                        Normal      = new Vector3(v.Normal.X, v.Normal.Y, v.Normal.Z),
                        TexCoord    = new Vector3(v.TexCoord0.X, v.TexCoord0.Y, v.TexCoord0.Z),
                        BoneIndices = (new[] { v.Indices.b0, v.Indices.b1, v.Indices.b2, v.Indices.b3 }).Take(boneIndexCount).ToArray(),
                        Weights     = (new[] { v.Weights.w0, v.Weights.w1, v.Weights.w2, v.Weights.w3 }).Take(boneWeightCount).ToArray(),
                        Color       = v.Color // Caution! Not all 3D model editors may support vertex color!
                    }).ToList();

                    // The vertices use relative bone indices based on the SubMesh definitions,
                    // which we're going to make absolute now
                    for (var v = 0; v < vertices.Count; v++)
                    {
                        var vertex = vertices[v];

                        for (var i = 0; i < vertex.BoneIndices.Length; i++)
                        {
                            vertex.BoneIndices[i] = boneReferences[vertex.BoneIndices[i]].Index;

                            // Also, if no bone weights are available, assign a weight of 1 to the first bone.
                            // This won't be stored ultimately as the PICA attributes won't specify it.
                            if (vertex.Weights.Length == 0)
                            {
                                vertex.Weights = new float[] { 1.0f };
                            }
                        }
                    }

                    // Deconstruct into triangle faces
                    var triangles = new List <SMTriangle>();
                    var indices   = faceDescriptor.Indices;
                    for (var i = 0; i < indices.Count; i += 3)
                    {
                        triangles.Add(new SMTriangle
                        {
                            v1 = indices[i + 0],
                            v2 = indices[i + 1],
                            v3 = indices[i + 2]
                        });
                    }

                    // Finally, assign material, if available (mostly for the model editor's benefit)
                    var material  = model.ModelMaterials.Entries.Select(e => e.EntryObject).Cast <DICTObjModelMaterial>().ToList()[mesh.MaterialId];
                    var texture   = material.TextureMappers.First().TextureReference;
                    var name      = texture.ReferenceName;
                    var smTexture = (Textures != null) ? Textures.Where(t => t.Name == name).SingleOrDefault() : null;
                    if (smTexture == null)
                    {
                        smTexture = new SMTexture(name, null);
                    }

                    Meshes[m] = new SMMesh(vertices, triangles, shape, vertexBufferIndex, smTexture);
                }
            }
        }