Example #1
0
        /// <summary>
        /// Check if the mesh contains the requested arrays.
        /// </summary>
        /// <param name="channels">A flag containing the array types that a ProBuilder mesh stores.</param>
        /// <returns>True if all arrays in the flag are present, false if not.</returns>
        public bool HasArrays(MeshArrays channels)
        {
            bool missing = false;

            int vc = vertexCount;

            missing |= (channels & MeshArrays.Position) == MeshArrays.Position && m_Positions == null;
            missing |= (channels & MeshArrays.Normal) == MeshArrays.Normal && (m_Normals == null || m_Normals.Length != vc);
            missing |= (channels & MeshArrays.Texture0) == MeshArrays.Texture0 && (m_Textures0 == null || m_Textures0.Length != vc);
            missing |= (channels & MeshArrays.Texture2) == MeshArrays.Texture2 && (m_Textures2 == null || m_Textures2.Count != vc);
            missing |= (channels & MeshArrays.Texture3) == MeshArrays.Texture3 && (m_Textures3 == null || m_Textures3.Count != vc);
            missing |= (channels & MeshArrays.Color) == MeshArrays.Color && (m_Colors == null || m_Colors.Length != vc);
            missing |= (channels & MeshArrays.Tangent) == MeshArrays.Tangent && (m_Tangents == null || m_Tangents.Length != vc);

            // UV2 is a special case. It is not stored in ProBuilderMesh, does not necessarily match the vertex count,
            // at it has a cost to check.
            if ((channels & MeshArrays.Texture1) == MeshArrays.Texture1 && mesh != null)
            {
#if UNITY_2019_3_OR_NEWER
                missing |= !mesh.HasVertexAttribute(VertexAttribute.TexCoord1);
#else
                var m_Textures1 = m_Mesh.uv2;
                missing |= (m_Textures1 == null || m_Textures1.Length < 3);
#endif
            }

            return(!missing);
        }
 protected override void AddMeshData(Chunk chunk, Pos pos, Block block, ref MeshData meshData)
 {
     foreach (var dir in DirectionUtils.Directions)
     {
         if (!chunk.GetBlock(pos + dir).GetBlockType(vm).IsSolid(chunk, pos, block, DirectionUtils.Opposites[dir]))
         {
             // Side
             meshData.verts.AddRange(MeshArrays.VertexCubeFaces(pos - chunk.Pos, chunk.BlockSize, dir));
             meshData.tris.AddRange(MeshArrays.TriCubeFaces(meshData.verts.Count));
             if (dir == Direction.up)
             {
                 // Foliage top texture
                 meshData.uvs.AddRange(MeshArrays.QuadFaceTexture(foliageTextureSet.GetTexture(chunk, pos, dir)));
                 // Add FoliageGrass
                 meshData.verts.AddRange(MeshArrays.VertexFoliageFaces(pos - chunk.Pos, chunk.BlockSize));
                 meshData.tris.AddRange(MeshArrays.TriFoliageFaces(meshData.verts.Count));
                 meshData.uvs.AddRange(MeshArrays.QuadFaceTexture(foliageTextureSet.GetTexture(chunk, pos, Direction.south)));
             }
             else
             {
                 // Normal sides
                 meshData.uvs.AddRange(MeshArrays.QuadFaceTexture(textureSet.GetTexture(chunk, pos, dir)));
             }
         }
     }
 }
Example #3
0
 protected override void AddMeshData(Chunk chunk, Pos pos, Block block, ref MeshData meshData)
 {
     foreach (var dir in DirectionUtils.Directions)
     {
         if (!chunk.GetBlock(pos + dir).GetBlockType(vm).IsSolid(chunk, pos, block, DirectionUtils.Opposite(dir)))
         {
             meshData.verts.AddRange(MeshArrays.VertexCubeFaces(pos - chunk.pos, chunk.blockSize, dir));
             meshData.tris.AddRange(MeshArrays.TriCubeFaces(meshData.verts.Count));
             meshData.uvs.AddRange(MeshArrays.QuadFaceTexture(textureSet.GetTexture(chunk, pos, dir)));
         }
     }
 }
Example #4
0
        public bool Equals(Vertex other, MeshArrays mask)
        {
            if (ReferenceEquals(other, null))
            {
                return(false);
            }

            return(((mask & MeshArrays.Position) != MeshArrays.Position || Math.Approx3(m_Position, other.m_Position)) &&
                   ((mask & MeshArrays.Color) != MeshArrays.Color || Math.ApproxC(m_Color, other.m_Color)) &&
                   ((mask & MeshArrays.Normal) != MeshArrays.Normal || Math.Approx3(m_Normal, other.m_Normal)) &&
                   ((mask & MeshArrays.Tangent) != MeshArrays.Tangent || Math.Approx4(m_Tangent, other.m_Tangent)) &&
                   ((mask & MeshArrays.Texture0) != MeshArrays.Texture0 || Math.Approx2(m_UV0, other.m_UV0)) &&
                   ((mask & MeshArrays.Texture1) != MeshArrays.Texture1 || Math.Approx2(m_UV2, other.m_UV2)) &&
                   ((mask & MeshArrays.Texture2) != MeshArrays.Texture2 || Math.Approx4(m_UV3, other.m_UV3)) &&
                   ((mask & MeshArrays.Texture3) != MeshArrays.Texture3 || Math.Approx4(m_UV4, other.m_UV4)));
        }
Example #5
0
        /// <summary>
        /// Compare two meshes for value-wise equality.
        /// </summary>
        /// <param name="expected"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool AssertAreEqual(Mesh expected, Mesh result, MeshArrays compare = k_DefaultMeshArraysCompare, string message = null)
        {
            int vertexCount  = expected.vertexCount;
            int subMeshCount = expected.subMeshCount;

            Assert.AreEqual(vertexCount, result.vertexCount, expected.name + " != " + result.name + " (submesh count)");
            Assert.AreEqual(subMeshCount, result.subMeshCount, expected.name + " != " + result.name + " (submesh count)");

            Vertex[] leftVertices  = expected.GetVertices();
            Vertex[] rightVertices = result.GetVertices();

            for (int i = 0; i < vertexCount; i++)
            {
                Assert.True(leftVertices[i].Equals(rightVertices[i], compare),
                            expected.name + " != " + result.name + "\nExpected\n" + leftVertices[i].ToString("F5") + "\n---\nReceived:\n" + rightVertices[i].ToString("F5"));
            }

            List <int> leftIndices  = new List <int>();
            List <int> rightIndices = new List <int>();

            for (int i = 0; i < subMeshCount; i++)
            {
                uint indexCount = expected.GetIndexCount(i);

                Assert.AreEqual(expected.GetTopology(i), result.GetTopology(i));
                Assert.AreEqual(indexCount, result.GetIndexCount(i));

                expected.GetIndices(leftIndices, i);
                result.GetIndices(rightIndices, i);

                for (int n = 0; n < indexCount; n++)
                {
                    Assert.AreEqual(leftIndices[n], rightIndices[n], message);
                }
            }

            return(true);
        }
Example #6
0
        /// <summary>
        /// Allocate and fill the requested attribute arrays.
        /// </summary>
        /// <remarks>
        /// If you are using this function to rebuild a mesh, use SetMesh instead. SetMesh handles setting null arrays where appropriate for you.
        /// </remarks>
        /// <seealso cref="SetMesh"/>
        /// <param name="vertices">The source vertices.</param>
        /// <param name="position">A new array of the vertex position values if requested by the attributes parameter, or null.</param>
        /// <param name="color">A new array of the vertex color values if requested by the attributes parameter, or null.</param>
        /// <param name="uv0">A new array of the vertex uv0 values if requested by the attributes parameter, or null.</param>
        /// <param name="normal">A new array of the vertex normal values if requested by the attributes parameter, or null.</param>
        /// <param name="tangent">A new array of the vertex tangent values if requested by the attributes parameter, or null.</param>
        /// <param name="uv2">A new array of the vertex uv2 values if requested by the attributes parameter, or null.</param>
        /// <param name="uv3">A new array of the vertex uv3 values if requested by the attributes parameter, or null.</param>
        /// <param name="uv4">A new array of the vertex uv4 values if requested by the attributes parameter, or null.</param>
        /// <param name="attributes">A flag with the MeshAttributes requested.</param>
        /// <seealso cref="HasArrays"/>
        public static void GetArrays(
            IList <CSG_Vertex> vertices,
            out Vector3[] position,
            out Color[] color,
            out Vector2[] uv0,
            out Vector3[] normal,
            out Vector4[] tangent,
            out Vector2[] uv2,
            out List <Vector4> uv3,
            out List <Vector4> uv4,
            MeshArrays attributes)
        {
            if (vertices == null)
            {
                throw new ArgumentNullException("vertices");
            }

            int vc    = vertices.Count;
            var first = vc < 1 ? new CSG_Vertex() : vertices[0];

            bool hasPosition = ((attributes & MeshArrays.Position) == MeshArrays.Position) && first.hasPosition;
            bool hasColor    = ((attributes & MeshArrays.Color) == MeshArrays.Color) && first.hasColor;
            bool hasUv0      = ((attributes & MeshArrays.Texture0) == MeshArrays.Texture0) && first.hasUV0;
            bool hasNormal   = ((attributes & MeshArrays.Normal) == MeshArrays.Normal) && first.hasNormal;
            bool hasTangent  = ((attributes & MeshArrays.Tangent) == MeshArrays.Tangent) && first.hasTangent;
            bool hasUv2      = ((attributes & MeshArrays.Texture1) == MeshArrays.Texture1) && first.hasUV2;
            bool hasUv3      = ((attributes & MeshArrays.Texture2) == MeshArrays.Texture2) && first.hasUV3;
            bool hasUv4      = ((attributes & MeshArrays.Texture3) == MeshArrays.Texture3) && first.hasUV4;

            position = hasPosition ? new Vector3[vc] : null;
            color    = hasColor ? new Color[vc] : null;
            uv0      = hasUv0 ? new Vector2[vc] : null;
            normal   = hasNormal ? new Vector3[vc] : null;
            tangent  = hasTangent ? new Vector4[vc] : null;
            uv2      = hasUv2 ? new Vector2[vc] : null;
            uv3      = hasUv3 ? new List <Vector4>(vc) : null;
            uv4      = hasUv4 ? new List <Vector4>(vc) : null;

            for (int i = 0; i < vc; i++)
            {
                if (hasPosition)
                {
                    position[i] = vertices[i].position;
                }
                if (hasColor)
                {
                    color[i] = vertices[i].color;
                }
                if (hasUv0)
                {
                    uv0[i] = vertices[i].uv0;
                }
                if (hasNormal)
                {
                    normal[i] = vertices[i].normal;
                }
                if (hasTangent)
                {
                    tangent[i] = vertices[i].tangent;
                }
                if (hasUv2)
                {
                    uv2[i] = vertices[i].uv2;
                }
                if (hasUv3)
                {
                    uv3.Add(vertices[i].uv3);
                }
                if (hasUv4)
                {
                    uv4.Add(vertices[i].uv4);
                }
            }
        }
Example #7
0
        static string WriteObjContents(string name, IEnumerable <Model> models, Dictionary <Material, string> materialMap, ObjOptions options)
        {
            // Empty names in OBJ groups can crash some 3d programs (meshlab)
            if (string.IsNullOrEmpty(name))
            {
                name = "ProBuilderModel";
            }

            StringBuilder sb = new StringBuilder();

            SemVer version;

            if (Version.TryGetPackageVersion(out version))
            {
                sb.AppendLine("# ProBuilder " + version.MajorMinorPatch);
            }
            else
            {
                sb.AppendLine("# ProBuilder");
            }
            sb.AppendLine("# https://unity3d.com/unity/features/worldbuilding/probuilder");
            sb.AppendLine(string.Format("# {0}", System.DateTime.Now));
            sb.AppendLine();
            sb.AppendLine(string.Format("mtllib ./{0}.mtl", name.Replace(" ", "_")));
            sb.AppendLine(string.Format("o {0}", name));
            sb.AppendLine();

            // obj orders indices 1 indexed
            int positionOffset = 1;
            int normalOffset   = 1;
            int textureOffset  = 1;

            bool  reverseWinding = options.handedness == ObjOptions.Handedness.Right;
            float handedness     = options.handedness == ObjOptions.Handedness.Left ? 1f : -1f;

            foreach (Model model in models)
            {
                int       subMeshCount = model.submeshCount;
                Matrix4x4 matrix       = options.applyTransforms ? model.matrix : Matrix4x4.identity;

                int vertexCount = model.vertexCount;

                Vector3[]      positions;
                Color[]        colors;
                Vector2[]      textures0;
                Vector3[]      normals;
                Vector4[]      tangent;
                Vector2[]      uv2;
                List <Vector4> uv3;
                List <Vector4> uv4;

                MeshArrays attribs = MeshArrays.Position | MeshArrays.Normal | MeshArrays.Texture0;

                if (options.vertexColors)
                {
                    attribs = attribs | MeshArrays.Color;
                }

                Vertex.GetArrays(model.vertices, out positions, out colors, out textures0, out normals, out tangent, out uv2, out uv3, out uv4, attribs);

                // Can skip this entirely if handedness matches Unity & not applying transforms.
                // matrix is set to identity if not applying transforms.
                if (options.handedness != ObjOptions.Handedness.Left || options.applyTransforms)
                {
                    for (int i = 0; i < vertexCount; i++)
                    {
                        if (positions != null)
                        {
                            positions[i]    = matrix.MultiplyPoint3x4(positions[i]);
                            positions[i].x *= handedness;
                        }

                        if (normals != null)
                        {
                            normals[i]    = matrix.MultiplyVector(normals[i]);
                            normals[i].x *= handedness;
                        }
                    }
                }

                sb.AppendLine(string.Format("g {0}", model.name));

                Dictionary <int, int> positionIndexMap;
                var positionCount = AppendPositions(sb, positions, colors, true, options.vertexColors, out positionIndexMap);

                sb.AppendLine();

                Dictionary <int, int> textureIndexMap;
                var textureCount = AppendArrayVec2(sb, textures0, "vt", true, out textureIndexMap);

                sb.AppendLine();

                Dictionary <int, int> normalIndexMap;
                var normalCount = AppendArrayVec3(sb, normals, "vn", true, out normalIndexMap);

                sb.AppendLine();

                // Material assignment
                for (int submeshIndex = 0; submeshIndex < subMeshCount; submeshIndex++)
                {
                    Submesh submesh = model.submeshes[submeshIndex];

                    string materialName = "";

                    if (materialMap.TryGetValue(model.materials[submeshIndex], out materialName))
                    {
                        sb.AppendLine(string.Format("usemtl {0}", materialName));
                    }
                    else
                    {
                        sb.AppendLine(string.Format("usemtl {0}", "null"));
                    }

                    int[] indexes = submesh.m_Indexes;
                    int   inc     = submesh.m_Topology == MeshTopology.Quads ? 4 : 3;
                    int   inc1    = inc - 1;

                    int o0 = reverseWinding ? inc1 : 0;
                    int o1 = reverseWinding ? inc1 - 1 : 1;
                    int o2 = reverseWinding ? inc1 - 2 : 2;
                    int o3 = reverseWinding ? inc1 - 3 : 3;

                    for (int ff = 0; ff < indexes.Length; ff += inc)
                    {
                        int p0 = positionIndexMap[indexes[ff + o0]] + positionOffset;
                        int p1 = positionIndexMap[indexes[ff + o1]] + positionOffset;
                        int p2 = positionIndexMap[indexes[ff + o2]] + positionOffset;

                        int t0 = textureIndexMap[indexes[ff + o0]] + textureOffset;
                        int t1 = textureIndexMap[indexes[ff + o1]] + textureOffset;
                        int t2 = textureIndexMap[indexes[ff + o2]] + textureOffset;

                        int n0 = normalIndexMap[indexes[ff + o0]] + normalOffset;
                        int n1 = normalIndexMap[indexes[ff + o1]] + normalOffset;
                        int n2 = normalIndexMap[indexes[ff + o2]] + normalOffset;

                        if (inc == 4)
                        {
                            int p3 = positionIndexMap[indexes[ff + o3]] + positionOffset;
                            int n3 = normalIndexMap[indexes[ff + o3]] + normalOffset;
                            int t3 = textureIndexMap[indexes[ff + o3]] + textureOffset;

                            sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
                                                        "f {0}/{4}/{8} {1}/{5}/{9} {2}/{6}/{10} {3}/{7}/{11}",
                                                        p0, p1, p2, p3,
                                                        t0, t1, t2, t3,
                                                        n0, n1, n2, n3
                                                        ));
                        }
                        else
                        {
                            sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
                                                        "f {0}/{3}/{6} {1}/{4}/{7} {2}/{5}/{8}",
                                                        p0, p1, p2,
                                                        t0, t1, t2,
                                                        n0, n1, n2
                                                        ));
                        }
                    }

                    sb.AppendLine();
                }

                positionOffset += positionCount;
                normalOffset   += normalCount;
                textureOffset  += textureCount;
            }

            return(sb.ToString());
        }
        /// <summary>
        /// Update the MeshFilter data for this chunk.
        /// </summary>
        /// <param name="chunksToSync">The chunks that need to have their mesh filter updated in the same frame as
        /// this. Null if no chunk sync is required.</param>
        private void UpdateMeshFilterJob(Vector2I[] chunksToSync)
        {
            // The chunk GameObject (this) is destroyed after the logical instance, so check that it still exists
            TerrainChunk chunk;
            if (chunksToSync == null)
            {
                if (TerrainSystem.Instance.Terrain.TryGetChunk(this.Chunk, out chunk))
                {
                    // Copy the mesh data into arrays
                    var mesh = new MeshArrays(
                        this.Chunk,
                        chunk.Mesh.Data.Vertices.ToArray(),
                        chunk.Mesh.Data.Normals.ToArray(),
                        chunk.Mesh.Data.Indices.ToArray(),
                        chunk.Mesh.Data.Light.ToArray());

                    // Update the mesh filter geometry
                    GameScheduler.Instance.Invoke(
                        () =>
                        {
                            this.cMeshFilter.mesh.Clear();
                            this.cMeshFilter.mesh.vertices = mesh.Vertices;
                            this.cMeshFilter.mesh.normals = mesh.Normals;
                            this.cMeshFilter.mesh.triangles = mesh.Triangles;
                            this.cMeshFilter.mesh.colors = mesh.Colors;
                        });
                }
            }
            else
            {
                var meshes = new MeshArrays[chunksToSync.Length];
                for (int i = 0; i < meshes.Length; i++)
                {
                    Vector2I chunkIndex = chunksToSync[i];
                    if (TerrainSystem.Instance.Terrain.TryGetChunk(chunkIndex, out chunk))
                    {
                        // Copy the mesh data into arrays
                        meshes[i] = new MeshArrays(
                            chunkIndex,
                            chunk.Mesh.Data.Vertices.ToArray(),
                            chunk.Mesh.Data.Normals.ToArray(),
                            chunk.Mesh.Data.Indices.ToArray(),
                            chunk.Mesh.Data.Light.ToArray());
                    }
                }

                // Update the mesh filter geometry
                GameScheduler.Instance.Invoke(
                    () =>
                    {
                        foreach (MeshArrays mesh in meshes)
                        {
                            if (mesh == null)
                            {
                                continue;
                            }

                            Transform chunkTransform =
                                this.transform.parent.FindChild(TerrainChunkComponent.GetLabel(mesh.Chunk));
                            if (chunkTransform == null)
                            {
                                continue;
                            }

                            TerrainChunkComponent cChunk = chunkTransform.GetComponent<TerrainChunkComponent>();
                            cChunk.cMeshFilter.mesh.Clear();
                            cChunk.cMeshFilter.mesh.vertices = mesh.Vertices;
                            cChunk.cMeshFilter.mesh.normals = mesh.Normals;
                            cChunk.cMeshFilter.mesh.triangles = mesh.Triangles;
                            cChunk.cMeshFilter.mesh.colors = mesh.Colors;
                        }
                    });
            }
        }
Example #9
0
 /// <summary>
 /// Find if a vertex attribute has been set.
 /// </summary>
 /// <param name="attribute">The attribute or attributes to test for.</param>
 /// <returns>True if this vertex has the specified attributes set, false if they are default values.</returns>
 public bool HasArrays(MeshArrays attribute)
 {
     return((m_Attributes & attribute) == attribute);
 }
Example #10
0
        static string WriteObjContents(string name, IEnumerable <Model> models, Dictionary <Material, string> materialMap, ObjOptions options)
        {
            // Empty names in OBJ groups can crash some 3d programs (meshlab)
            if (string.IsNullOrEmpty(name))
            {
                name = "ProBuilderModel";
            }

            StringBuilder sb = new StringBuilder();

            sb.AppendLine("# Exported from ProBuilder");
            sb.AppendLine("# http://www.procore3d.com/probuilder");
            sb.AppendLine(string.Format("# {0}", System.DateTime.Now));
            sb.AppendLine();
            sb.AppendLine(string.Format("mtllib ./{0}.mtl", name.Replace(" ", "_")));
            sb.AppendLine(string.Format("o {0}", name));
            sb.AppendLine();

            int triangleOffset = 1;

            bool  reverseWinding = options.handedness == ObjOptions.Handedness.Right;
            float handedness     = options.handedness == ObjOptions.Handedness.Left ? 1f : -1f;

            foreach (Model model in models)
            {
                int       subMeshCount = model.submeshCount;
                Matrix4x4 matrix       = options.applyTransforms ? model.matrix : Matrix4x4.identity;

                int vertexCount = model.vertexCount;

                Vector3[]      positions;
                Color[]        colors;
                Vector2[]      textures0;
                Vector3[]      normals;
                Vector4[]      tangent;
                Vector2[]      uv2;
                List <Vector4> uv3;
                List <Vector4> uv4;

                MeshArrays attribs = MeshArrays.Position | MeshArrays.Normal | MeshArrays.Texture0;
                if (options.vertexColors)
                {
                    attribs = attribs | MeshArrays.Color;
                }
                Vertex.GetArrays(model.vertices, out positions, out colors, out textures0, out normals, out tangent, out uv2, out uv3, out uv4, attribs);

                // Can skip this entirely if handedness matches Unity & not applying transforms.
                // matrix is set to identity if not applying transforms.
                if (options.handedness != ObjOptions.Handedness.Left || options.applyTransforms)
                {
                    for (int i = 0; i < vertexCount; i++)
                    {
                        if (positions != null)
                        {
                            positions[i]    = matrix.MultiplyPoint3x4(positions[i]);
                            positions[i].x *= handedness;
                        }

                        if (normals != null)
                        {
                            normals[i]    = matrix.MultiplyVector(normals[i]);
                            normals[i].x *= handedness;
                        }
                    }
                }

                sb.AppendLine(string.Format("g {0}", model.name));

                if (options.vertexColors && colors != null && colors.Length == vertexCount)
                {
                    for (int i = 0; i < vertexCount; i++)
                    {
                        sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "v {0} {1} {2} {3} {4} {5}",
                                                    positions[i].x, positions[i].y, positions[i].z,
                                                    colors[i].r, colors[i].g, colors[i].b));
                    }
                }
                else
                {
                    for (int i = 0; i < vertexCount; i++)
                    {
                        sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "v {0} {1} {2}", positions[i].x, positions[i].y, positions[i].z));
                    }
                }

                sb.AppendLine();

                for (int i = 0; normals != null && i < vertexCount; i++)
                {
                    sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "vn {0} {1} {2}", normals[i].x, normals[i].y, normals[i].z));
                }

                sb.AppendLine();

                for (int i = 0; textures0 != null && i < vertexCount; i++)
                {
                    sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "vt {0} {1}", textures0[i].x, textures0[i].y));
                }

                sb.AppendLine();

                // Material assignment
                for (int submeshIndex = 0; submeshIndex < subMeshCount; submeshIndex++)
                {
                    Submesh submesh = model.submeshes[submeshIndex];

                    if (subMeshCount > 1)
                    {
                        sb.AppendLine(string.Format("g {0}_{1}", model.name, submeshIndex));
                    }
                    else
                    {
                        sb.AppendLine(string.Format("g {0}", model.name));
                    }

                    string materialName = "";

                    if (materialMap.TryGetValue(model.materials[submeshIndex], out materialName))
                    {
                        sb.AppendLine(string.Format("usemtl {0}", materialName));
                    }
                    else
                    {
                        sb.AppendLine(string.Format("usemtl {0}", "null"));
                    }

                    int[] indexes = submesh.m_Indexes;
                    int   inc     = submesh.m_Topology == MeshTopology.Quads ? 4 : 3;

                    for (int ff = 0; ff < indexes.Length; ff += inc)
                    {
                        if (inc == 4)
                        {
                            if (reverseWinding)
                            {
                                sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2} {3}/{3}/{3}",
                                                            indexes[ff + 3] + triangleOffset,
                                                            indexes[ff + 2] + triangleOffset,
                                                            indexes[ff + 1] + triangleOffset,
                                                            indexes[ff + 0] + triangleOffset));
                            }
                            else
                            {
                                sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2} {3}/{3}/{3}",
                                                            indexes[ff + 0] + triangleOffset,
                                                            indexes[ff + 1] + triangleOffset,
                                                            indexes[ff + 2] + triangleOffset,
                                                            indexes[ff + 3] + triangleOffset));
                            }
                        }
                        else
                        {
                            if (reverseWinding)
                            {
                                sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}",
                                                            indexes[ff + 2] + triangleOffset,
                                                            indexes[ff + 1] + triangleOffset,
                                                            indexes[ff + 0] + triangleOffset));
                            }
                            else
                            {
                                sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}",
                                                            indexes[ff + 0] + triangleOffset,
                                                            indexes[ff + 1] + triangleOffset,
                                                            indexes[ff + 2] + triangleOffset));
                            }
                        }
                    }

                    sb.AppendLine();
                }

                triangleOffset += vertexCount;
            }

            return(sb.ToString());
        }