/// <summary> /// Creates a Wavefront format string from a triangle mesh. /// </summary> /// <param name="mesh">A valid triangle mesh.</param> /// <param name="reverseWrap">Reverse the wrap direction of the triangles.</param> /// <param name="invertXAxis">Invert the x-axis values.</param> /// <returns>A string representing the mesh in Wavefront format.</returns> public static string TranslateTo(TriangleMesh mesh , bool reverseWrap , bool invertXAxis) { StringBuilder sb = new StringBuilder(); float xFactor = (invertXAxis ? -1 : 1); for (int i = 0; i < mesh.vertCount; i += 1) { sb.Append("v " + (mesh.verts[i].x * xFactor).ToString(CultureInfo.InvariantCulture) + " " + mesh.verts[i].y.ToString(CultureInfo.InvariantCulture) + " " + mesh.verts[i].z.ToString(CultureInfo.InvariantCulture) + "\n"); } for (int p = 0; p < mesh.triCount * 3; p += 3) { // The +1 converts to a 1-based index. if (reverseWrap) { sb.Append("f " + (mesh.tris[p + 0] + 1).ToString(CultureInfo.InvariantCulture) + " " + (mesh.tris[p + 2] + 1).ToString(CultureInfo.InvariantCulture) + " " + (mesh.tris[p + 1] + 1).ToString(CultureInfo.InvariantCulture) + "\n"); } else { sb.Append("f " + (mesh.tris[p + 0] + 1).ToString(CultureInfo.InvariantCulture) + " " + (mesh.tris[p + 1] + 1).ToString(CultureInfo.InvariantCulture) + " " + (mesh.tris[p + 2] + 1).ToString(CultureInfo.InvariantCulture) + "\n"); } } return sb.ToString(); }
public static TriangleMesh TriangulateSurface(Terrain terrain, float resolution) { if (terrain == null || terrain.terrainData == null || resolution <= 0) return null; Vector3 origin = terrain.transform.position; int xCount; int zCount; Vector3 scale = DeriveScale(terrain, resolution, out xCount, out zCount); TriangleMesh m = new TriangleMesh(xCount * zCount, (xCount - 1) * (zCount - 1) * 2); TriangulateSurface(terrain, origin, scale, xCount, zCount, 0, m); return m; }
/// <summary> /// Validates the structure and, optionally, the content of the mesh. /// </summary> /// <remarks> /// <para> /// The basic structural validation includes null checks, array size checks, etc. /// </para> /// <para> /// The optional content validation checks that the indices refer to valid vertices /// and that triangles do not contain duplicate vertices. /// </para> /// </remarks> /// <param name="mesh">The mesh to check.</param> /// <param name="includeContent"> /// If true, the content will be checked. Otherwise only the structure will be checked. /// </param> /// <returns>True if the validation tests pass.</returns> public static bool IsValid(TriangleMesh mesh, bool includeContent) { if (mesh == null) return false; return IsValid(mesh.verts, mesh.vertCount , mesh.tris, mesh.triCount , includeContent); }
/// <summary> /// Extracts all input geometry for a particular bounds. /// </summary> /// <remarks> /// <para> /// This method exists to permit debugging. /// </para> /// <para> /// The returned result is only guarenteed to be the result 'seen' by the NMGen build /// process. /// </para> /// </remarks> /// <param name="xmin">The minimum x-axis bounds.</param> /// <param name="zmin">The minimum z-axis bounds.</param> /// <param name="xmax">The maximum x-axis bounds.</param> /// <param name="zmax">The maximum z-axis bounds.</param> /// <param name="areas">The triangle areas.</param> /// <returns>The triangle mesh.</returns> public TriangleMesh ExtractMesh(float xmin, float zmin, float xmax, float zmax , out byte[] areas) { byte[] lareas; TriangleMesh lmesh = mMesh.ExtractMesh(out lareas); List<ChunkyTriMeshNode> nodes = new List<ChunkyTriMeshNode>(); int triCount = mMesh.GetChunks(xmin, zmin, xmax, zmax, nodes); if (triCount == 0) { areas = new byte[0]; return new TriangleMesh(); } TriangleMesh result = new TriangleMesh(); result.verts = lmesh.verts; result.vertCount = lmesh.vertCount; result.tris = new int[triCount * 3]; result.triCount = triCount; areas = new byte[triCount]; int i = 0; foreach (ChunkyTriMeshNode node in nodes) { for (int j = 0; j < node.count; j++, i++) { result.tris[i * 3 + 0] = lmesh.tris[(node.i + j) * 3 + 0]; result.tris[i * 3 + 1] = lmesh.tris[(node.i + j) * 3 + 1]; result.tris[i * 3 + 2] = lmesh.tris[(node.i + j) * 3 + 2]; areas[i] = lareas[node.i + j]; } } return result; }
/// <summary> /// Adds a triangle mesh. /// </summary> /// <remarks> /// <para> /// All triangles will default to <see cref="NMGen.MaxArea"/> if the /// <paramref name="areas"/> parameter is null. /// </para> /// <para> /// Will return false if the mesh triangle count is zero. /// </para> /// </remarks> /// <param name="mesh">The triangle mesh.</param> /// <param name="areas"> /// The triangle areas. (Optional)[Length: >= mesh.triCount] /// </param> /// <returns>True if the triangles were successfully added.</returns> public bool AddTriangles(TriangleMesh mesh, byte[] areas) { if (mesh == null || mesh.triCount == 0) return false; return AddTriangles(mesh.verts, mesh.vertCount, mesh.tris, areas, mesh.triCount); }
public static TriangleMesh TriangulateSurface(Terrain terrain , float xmin, float zmin, float xmax, float zmax , float resolution , float yOffset) { if (terrain == null || terrain.terrainData == null || resolution <= 0 || xmin > xmax || zmin > zmax) { return null; } int xCount; int zCount; Vector3 scale = DeriveScale(terrain, resolution, out xCount, out zCount); Vector3 origin = terrain.transform.position; /* * We are generating part of a larger mesh. The vertices must match that of the larger * mesh. * * Convert input values to local grid space. * Clamp to the heightfield bounds. * Convert back to worldspace. */ xmin = origin.x + Mathf.Max(0, Mathf.Floor((xmin - origin.x) / scale.x)) * scale.x; zmin = origin.z + Mathf.Max(0, Mathf.Floor((zmin - origin.z) / scale.z)) * scale.z; xmax = origin.x + Mathf.Min(xCount, Mathf.Ceil((xmax - origin.x) / scale.x)) * scale.x; zmax = origin.z + Mathf.Min(xCount, Mathf.Ceil((zmax - origin.z) / scale.z)) * scale.z; if (xmin + scale.x > xmax || zmin + scale.z > zmax) // Triangulation zone is too small. return null; // Everyting is already snapped to the grid. But there may be floating point errors. // So round it. xCount = Mathf.RoundToInt((xmax - xmin) / scale.x); zCount = Mathf.RoundToInt((zmax - zmin) / scale.z); TriangleMesh m = new TriangleMesh(xCount * zCount, (xCount - 1) * (zCount - 1) * 2); TriangulateSurface(terrain , new Vector3(xmin, origin.y, zmin) , scale , xCount, zCount , yOffset , m); return m; }
private static void TriangulateSurface(Terrain terrain , Vector3 origin , Vector3 scale , int xCount , int zCount , float yOffset , TriangleMesh buffer) { // Create the vertices by sampling the terrain. for (int ix = 0; ix < xCount; ix++) { float x = origin.x + ix * scale.x; for (int iz = 0; iz < zCount; iz++) { float z = origin.z + iz * scale.z; Vector3 pos = new Vector3(x, origin.y, z); pos.y += terrain.SampleHeight(pos) + yOffset; buffer.verts[buffer.vertCount] = pos; buffer.vertCount++; } } // Triangulate surface sample points. for (int ix = 0; ix < xCount - 1; ix++) { for (int iz = 0; iz < zCount - 1; iz++) { int i = iz + (ix * zCount); int irow = i + zCount; buffer.tris[buffer.triCount * 3 + 0] = i; buffer.tris[buffer.triCount * 3 + 1] = irow + 1; buffer.tris[buffer.triCount * 3 + 2] = irow; buffer.triCount++; buffer.tris[buffer.triCount * 3 + 0] = i; buffer.tris[buffer.triCount * 3 + 1] = i + 1; buffer.tris[buffer.triCount * 3 + 2] = irow + 1; buffer.triCount++; } } }
/// <summary> /// Set the area of all triangles with a slope below the specified value to /// <see cref="MaxArea"/>. /// </summary> /// <param name="context">The context to use duing the operation.</param> /// <param name="mesh">The source mesh.</param> /// <param name="walkableSlope">The maximum walkable slope.</param> /// <param name="areas"> /// The areas associated with each triangle. [Length: >= mesh.triCount] (In/Out) /// </param> /// <returns>True if the operation was successful.</returns> public static bool MarkWalkableTriangles(BuildContext context, TriangleMesh mesh , float walkableSlope , byte[] areas) { if (mesh == null || context == null || areas == null || areas.Length < mesh.triCount) { return false; } NMGenEx.nmgMarkWalkableTriangles(context.root , walkableSlope , mesh.verts , mesh.vertCount , mesh.tris , mesh.triCount , areas); return true; }
/// <summary> /// Extracts all triangles from the mesh. (Not efficient.) /// </summary> /// <param name="areas">The areas for each triangle.</param> /// <returns>The triangle mesh data.</returns> public TriangleMesh ExtractMesh(out byte[] areas) { if (this.tris == IntPtr.Zero) { areas = null; return null; } TriangleMesh result = new TriangleMesh(); result.triCount = mTriCount; result.vertCount = mVertCount; float[] lverts = new float[mVertCount * 3]; Marshal.Copy(this.verts, lverts, 0, mVertCount * 3); result.verts = Vector3Util.GetVectors(lverts); result.tris = new int[mTriCount * 3]; Marshal.Copy(this.tris, result.tris, 0, mTriCount * 3); areas = new byte[mTriCount]; Marshal.Copy(this.areas, areas, 0, mTriCount); return result; }
/// <summary> /// Voxelizes the triangles in the provided mesh into the heightfield. /// </summary> /// <param name="context">The context to use for the operation</param> /// <param name="mesh">The triangle mesh.</param> /// <param name="areas"> /// The ids of the areas the triangles belong to. /// [Limit: <= <see cref="NMGen.MaxArea"/>] [Size: >= mesh.triCount] /// </param> /// <param name="flagMergeThreshold"> /// The distance where the walkable flag is favored over the non-walkable flag. /// [Limit: >= 0] [Normal: 1] /// </param> /// <returns>True if the operation was successful.</returns> public bool AddTriangles(BuildContext context, TriangleMesh mesh, byte[] areas , int flagMergeThreshold) { if (IsDisposed) return false; return HeightfieldEx.nmhfRasterizeTriMesh(context.root , mesh.verts , mesh.vertCount , mesh.tris , areas , mesh.triCount , root , flagMergeThreshold); }
/// <summary> /// Creates a builder. /// </summary> /// <remarks> /// <para> /// No validation is performed and the builder will use the parameters directly /// during the build. /// </para> /// <para> /// Builders created using this method are not guarenteed to produce a usable result. /// </para> /// <para> /// It is the responsibility of the caller to ensure thread safely if /// <paramref name="isThreadSafe"/> is set to true. /// </para> /// <para> /// <b>Warning:</b> If walkable slope if greather than zero then the builder will /// apply <see cref="NMGen.ClearUnwalkableTriangles"/> directly to the areas parameter. /// </para> /// </remarks> /// <param name="mesh">The triangle mesh to use for the build.</param> /// <param name="areas">The triangle areas. (Null not permitted.)</param> /// <param name="walkableSlope">The walkable slope. /// (See <see cref="NMGenParams.WalkableSlope"/>)</param> /// <param name="isThreadSafe">True if the builder can run safely on its own thread.</param> /// <returns>A builder, or null on error.</returns> public static InputGeometryBuilder UnsafeCreate(TriangleMesh mesh , byte[] areas , float walkableSlope , bool isThreadSafe) { if (mesh == null || areas == null || mesh.triCount < 0) return null; walkableSlope = System.Math.Min(NMGen.MaxAllowedSlope, walkableSlope); if (walkableSlope > 0) { BuildContext context = new BuildContext(); if (!NMGen.ClearUnwalkableTriangles(context, mesh, walkableSlope, areas)) return null; } ChunkyTriMeshBuilder builder = ChunkyTriMeshBuilder.Create(mesh, areas, 32768); if (builder == null) return null; Vector3 bmin; Vector3 bmax; mesh.GetBounds(out bmin, out bmax); return new InputGeometryBuilder(builder, bmin, bmax, isThreadSafe); }
/// <summary> /// Validates the mesh and areas. /// </summary> /// <remarks> /// <para> /// The <paramref name="areas"/> parameter is validated only if it is non-null. /// </para> /// </remarks> /// <param name="mesh">The mesh to validate.</param> /// <param name="areas">The area to validate. (If non-null.)</param> /// <returns>True if the structure and content of the parameters are considered valid. /// </returns> public static bool IsValid(TriangleMesh mesh, byte[] areas) { if (mesh == null || mesh.triCount == 0 || !TriangleMesh.IsValid(mesh, true)) { return false; } if (areas != null && (areas.Length != mesh.triCount || !NMGen.IsValidAreaBuffer(areas, mesh.triCount))) { return false; } return true; }
/// <summary> /// Creates a thread-safe, fully validated builder. /// </summary> /// <remarks> /// <para> /// The input mesh and area parameters are fully validated. /// </para> /// <para> /// Will return null if there are zero triangles. /// </para> /// <para> /// All triangleswill default to <see cref="NMGen.MaxArea"/> if the /// <paramref name="areas"/> parameter is null. /// </para> /// <para> /// If walkable slope if greather than zero then the builder will apply /// <see cref="NMGen.ClearUnwalkableTriangles"/> to the areas. /// </para> /// </remarks> /// <param name="mesh">The triangle mesh to use for the build.</param> /// <param name="areas">The triangle areas. (Null permitted.)</param> /// <param name="walkableSlope">The walkable slope. /// (See <see cref="NMGenParams.WalkableSlope"/>)</param> /// <returns>A thread-safe, fully validated builder. Or null on error.</returns> public static InputGeometryBuilder Create(TriangleMesh mesh , byte[] areas , float walkableSlope) { if (!IsValid(mesh, areas)) return null; TriangleMesh lmesh = new TriangleMesh(mesh.vertCount, mesh.triCount); lmesh.triCount = mesh.triCount; lmesh.vertCount = mesh.vertCount; System.Array.Copy(mesh.verts, 0, lmesh.verts, 0, lmesh.verts.Length); System.Array.Copy(mesh.tris, 0, lmesh.tris, 0, lmesh.tris.Length); byte[] lareas; if (areas == null) lareas = NMGen.CreateDefaultAreaBuffer(mesh.triCount); else { lareas = new byte[mesh.triCount]; System.Array.Copy(areas, 0, lareas, 0, lareas.Length); } return UnsafeCreate(lmesh, lareas, walkableSlope, true); }