/// <summary> /// Use the HeightPatch data to obtain a height for a certain location. /// </summary> /// <param name="loc">The location</param> /// <param name="invCellSize">Reciprocal of cell size</param> /// <param name="cellHeight">Cell height</param> /// <param name="hp">Height patch</param> /// <returns>The height</returns> private int GetHeight(Vector3 loc, float invCellSize, float cellHeight, HeightPatch hp) { int ix = (int)Math.Floor(loc.X * invCellSize + 0.01f); int iz = (int)Math.Floor(loc.Z * invCellSize + 0.01f); ix = MathHelper.Clamp(ix - hp.X, 0, hp.Width - 1); iz = MathHelper.Clamp(iz - hp.Y, 0, hp.Length - 1); int h; if (!hp.TryGetHeight(ix, iz, out h)) { //go in counterclockwise direction starting from west, ending in northwest int[] off = { -1, 0, -1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1 }; float dmin = float.MaxValue; for (int i = 0; i < 8; i++) { int nx = ix + off[i * 2 + 0]; int nz = iz + off[i * 2 + 1]; if (nx < 0 || nz < 0 || nx >= hp.Width || nz >= hp.Length) continue; int nh; if (!hp.TryGetHeight(nx, nz, out nh)) continue; float d = Math.Abs(nh * cellHeight - loc.Y); if (d < dmin) { h = nh; dmin = d; } } } return h; }
/// <summary> /// Initializes a new instance of the <see cref="PolyMeshDetail"/> class. /// </summary> /// <remarks> /// <see cref="PolyMeshDetail"/> uses a <see cref="CompactHeightfield"/> to add in details to a /// <see cref="PolyMesh"/>. This detail is triangulated into a new mesh and can be used to approximate height in the walkable /// areas of a scene. /// </remarks> /// <param name="mesh">The <see cref="PolyMesh"/>.</param> /// <param name="compactField">The <see cref="CompactHeightfield"/> used to add height detail.</param> /// <param name="sampleDist">The sampling distance.</param> /// <param name="sampleMaxError">The maximum sampling error allowed.</param> public PolyMeshDetail(PolyMesh mesh, CompactHeightfield compactField, float sampleDist, float sampleMaxError) { if (mesh.VertCount == 0 || mesh.PolyCount == 0) return; Vector3 origin = mesh.Bounds.Min; int maxhw = 0, maxhh = 0; BBox2i[] bounds = new BBox2i[mesh.PolyCount]; Vector3[] poly = new Vector3[mesh.NumVertsPerPoly]; var storedVertices = new List<Vector3>(); var storedTriangles = new List<TriangleData>(); //find max size for polygon area for (int i = 0; i < mesh.PolyCount; i++) { var p = mesh.Polys[i]; int xmin = compactField.Width; int xmax = 0; int zmin = compactField.Length; int zmax = 0; for (int j = 0; j < mesh.NumVertsPerPoly; j++) { var pj = p.Vertices[j]; if (pj == PolyMesh.NullId) break; var v = mesh.Verts[pj]; xmin = Math.Min(xmin, v.X); xmax = Math.Max(xmax, v.X); zmin = Math.Min(zmin, v.Z); zmax = Math.Max(zmax, v.Z); } xmin = Math.Max(0, xmin - 1); xmax = Math.Min(compactField.Width, xmax + 1); zmin = Math.Max(0, zmin - 1); zmax = Math.Min(compactField.Length, zmax + 1); if (xmin >= xmax || zmin >= zmax) continue; maxhw = Math.Max(maxhw, xmax - xmin); maxhh = Math.Max(maxhh, zmax - zmin); bounds[i] = new BBox2i(xmin, zmin, xmax, zmax); } HeightPatch hp = new HeightPatch(0, 0, maxhw, maxhh); this.meshes = new MeshData[mesh.PolyCount]; for (int i = 0; i < mesh.PolyCount; i++) { var p = mesh.Polys[i]; //store polygon vertices for processing int npoly = 0; for (int j = 0; j < mesh.NumVertsPerPoly; j++) { int pvi = p.Vertices[j]; if (pvi == PolyMesh.NullId) break; PolyVertex pv = mesh.Verts[pvi]; Vector3 v = new Vector3(pv.X, pv.Y, pv.Z); v.X *= mesh.CellSize; v.Y *= mesh.CellHeight; v.Z *= mesh.CellSize; poly[j] = v; npoly++; } //get height data from area of polygon BBox2i bound = bounds[i]; hp.Resize(bound.Min.X, bound.Min.Y, bound.Max.X - bound.Min.X, bound.Max.Y - bound.Min.Y); GetHeightData(compactField, p, npoly, mesh.Verts, mesh.BorderSize, hp); List<Vector3> tempVerts = new List<Vector3>(); List<TriangleData> tempTris = new List<TriangleData>(128); List<EdgeInfo> edges = new List<EdgeInfo>(16); List<SamplingData> samples = new List<SamplingData>(128); BuildPolyDetail(poly, npoly, sampleDist, sampleMaxError, compactField, hp, tempVerts, tempTris, edges, samples); //more detail verts for (int j = 0; j < tempVerts.Count; j++) { Vector3 tv = tempVerts[j]; Vector3 v; v.X = tv.X + origin.X; v.Y = tv.Y + origin.Y + compactField.CellHeight; v.Z = tv.Z + origin.Z; tempVerts[j] = v; } for (int j = 0; j < npoly; j++) { Vector3 po = poly[j]; po.X += origin.X; po.Y += origin.Y; po.Z += origin.Z; poly[j] = po; } //save data this.meshes[i].VertexIndex = storedVertices.Count; this.meshes[i].VertexCount = tempVerts.Count; this.meshes[i].TriangleIndex = storedTriangles.Count; this.meshes[i].TriangleCount = tempTris.Count; //store vertices storedVertices.AddRange(tempVerts); //store triangles for (int j = 0; j < tempTris.Count; j++) { storedTriangles.Add(new TriangleData(tempTris[j], tempVerts, poly, npoly)); } } this.verts = storedVertices.ToArray(); this.tris = storedTriangles.ToArray(); }
/// <summary> /// Floodfill heightfield to get 2D height data, starting at vertex locations /// </summary> /// <param name="compactField">Original heightfield data</param> /// <param name="poly">Polygon in PolyMesh</param> /// <param name="polyCount">Number of vertices per polygon</param> /// <param name="verts">PolyMesh Vertices</param> /// <param name="borderSize">Heightfield border size</param> /// <param name="hp">HeightPatch which extracts heightfield data</param> /// <param name="stack">Temporary stack of CompactSpanReferences</param> private void GetHeightDataSeedsFromVertices(CompactHeightfield compactField, PolyMesh.Polygon poly, int polyCount, PolyVertex[] verts, int borderSize, HeightPatch hp, List<CompactSpanReference> stack) { hp.SetAll(0); //use poly vertices as seed points for (int j = 0; j < polyCount; j++) { var csr = new CompactSpanReference(0, 0, -1); int dmin = int.MaxValue; var v = verts[poly.Vertices[j]]; for (int k = 0; k < 9; k++) { //get vertices and offset x and z coordinates depending on current drection int ax = v.X + VertexOffset[k * 2 + 0]; int ay = v.Y; int az = v.Z + VertexOffset[k * 2 + 1]; //skip if out of bounds if (ax < hp.X || ax >= hp.X + hp.Width || az < hp.Y || az >= hp.Y + hp.Length) continue; //get new cell CompactCell c = compactField.Cells[(az + borderSize) * compactField.Width + (ax + borderSize)]; //loop through all the spans for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = compactField.Spans[i]; //find minimum y-distance int d = Math.Abs(ay - s.Minimum); if (d < dmin) { csr = new CompactSpanReference(ax, az, i); dmin = d; } } } //only add if something new found if (csr.Index != -1) { stack.Add(csr); } } //find center of polygon using flood fill int pcx = 0, pcz = 0; for (int j = 0; j < polyCount; j++) { var v = verts[poly.Vertices[j]]; pcx += v.X; pcz += v.Z; } pcx /= polyCount; pcz /= polyCount; //stack groups 3 elements as one part foreach (var cell in stack) { int idx = (cell.Y - hp.Y) * hp.Width + (cell.X - hp.X); hp[idx] = 1; } //process the entire stack while (stack.Count > 0) { var cell = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); //check if close to center of polygon if (Math.Abs(cell.X - pcx) <= 1 && Math.Abs(cell.Y - pcz) <= 1) { //clear the stack and add a new group stack.Clear(); stack.Add(cell); break; } CompactSpan cs = compactField[cell]; //check all four directions for (var dir = Direction.West; dir <= Direction.South; dir++) { //skip if disconnected if (!cs.IsConnected(dir)) continue; //get neighbor int ax = cell.X + dir.GetHorizontalOffset(); int ay = cell.Y + dir.GetVerticalOffset(); //skip if out of bounds if (ax < hp.X || ax >= (hp.X + hp.Width) || ay < hp.Y || ay >= (hp.Y + hp.Length)) continue; if (hp[(ay - hp.Y) * hp.Width + (ax - hp.X)] != 0) continue; //get the new index int ai = compactField.Cells[(ay + borderSize) * compactField.Width + (ax + borderSize)].StartIndex + CompactSpan.GetConnection(ref cs, dir); //save data int idx = (ay - hp.Y) * hp.Width + (ax - hp.X); hp[idx] = 1; //push to stack stack.Add(new CompactSpanReference(ax, ay, ai)); } } //clear the heightpatch hp.Clear(); //mark start locations for (int i = 0; i < stack.Count; i++) { var c = stack[i]; //set new heightpatch data int idx = (c.Y - hp.Y) * hp.Width + (c.X - hp.X); CompactSpan cs = compactField.Spans[c.Index]; hp[idx] = cs.Minimum; stack[i] = new CompactSpanReference(c.X + borderSize, c.Y + borderSize, c.Index); } }
/// <summary> /// Generate the PolyMeshDetail using the PolyMesh and HeightPatch /// </summary> /// <param name="polyMeshVerts">PolyMesh Vertex data</param> /// <param name="numMeshVerts">Number of PolyMesh vertices</param> /// <param name="sampleDist">Sampling distance</param> /// <param name="sampleMaxError">Maximum sampling error</param> /// <param name="compactField">THe compactHeightfield</param> /// <param name="hp">The heightPatch</param> /// <param name="verts">Detail verts</param> /// <param name="tris">Detail triangles</param> /// <param name="edges">The edge array</param> /// <param name="samples">The samples array</param> private void BuildPolyDetail(Vector3[] polyMeshVerts, int numMeshVerts, float sampleDist, float sampleMaxError, CompactHeightfield compactField, HeightPatch hp, List<Vector3> verts, List<TriangleData> tris, List<EdgeInfo> edges, List<SamplingData> samples) { const int MAX_VERTS = 127; const int MAX_TRIS = 255; const int MAX_VERTS_PER_EDGE = 32; Vector3[] edge = new Vector3[MAX_VERTS_PER_EDGE + 1]; List<int> hull = new List<int>(MAX_VERTS); //fill up vertex array for (int i = 0; i < numMeshVerts; ++i) verts.Add(polyMeshVerts[i]); float cs = compactField.CellSize; float ics = 1.0f / cs; float minExtent = PolyMinExtent(polyMeshVerts); //tessellate outlines if (sampleDist > 0) { for (int i = 0, j = verts.Count - 1; i < verts.Count; j = i++) { Vector3 vi = verts[i]; Vector3 vj = verts[j]; bool swapped = false; //make sure order is correct, otherwise swap data if (Math.Abs(vj.X - vi.X) < 1E-06f) { if (vj.Z > vi.Z) { Vector3 temp = vj; vj = vi; vi = temp; swapped = true; } } else if (vj.X > vi.X) { Vector3 temp = vj; vj = vi; vi = temp; swapped = true; } //create samples along the edge Vector3 dv; Vector3.Subtract(ref vi, ref vj, out dv); float d = (float)Math.Sqrt(dv.X * dv.X + dv.Z * dv.Z); int nn = 1 + (int)Math.Floor(d / sampleDist); if (nn >= MAX_VERTS_PER_EDGE) nn = MAX_VERTS_PER_EDGE - 1; if (verts.Count + nn >= MAX_VERTS) nn = MAX_VERTS - 1 - verts.Count; for (int k = 0; k <= nn; k++) { float u = (float)k / (float)nn; Vector3 pos; Vector3 tmp; Vector3.Multiply(ref dv, u, out tmp); Vector3.Add(ref vj, ref tmp, out pos); pos.Y = GetHeight(pos, ics, compactField.CellHeight, hp) * compactField.CellHeight; edge[k] = pos; } //simplify samples int[] idx = new int[MAX_VERTS_PER_EDGE]; idx[0] = 0; idx[1] = nn; int nidx = 2; for (int k = 0; k < nidx - 1;) { int a = idx[k]; int b = idx[k + 1]; Vector3 va = edge[a]; Vector3 vb = edge[b]; //find maximum deviation along segment float maxd = 0; int maxi = -1; for (int m = a + 1; m < b; m++) { float dev = Distance.PointToSegmentSquared(ref edge[m], ref va, ref vb); if (dev > maxd) { maxd = dev; maxi = m; } } if (maxi != -1 && maxd > (sampleMaxError * sampleMaxError)) { //shift data to the right for (int m = nidx; m > k; m--) idx[m] = idx[m - 1]; //set new value idx[k + 1] = maxi; nidx++; } else { k++; } } hull.Add(j); //add new vertices if (swapped) { for (int k = nidx - 2; k > 0; k--) { hull.Add(verts.Count); verts.Add(edge[idx[k]]); } } else { for (int k = 1; k < nidx - 1; k++) { hull.Add(verts.Count); verts.Add(edge[idx[k]]); } } } } //tesselate base mesh edges.Clear(); tris.Clear(); if (minExtent < sampleDist * 2) { TriangulateHull(verts, hull, tris); return; } TriangulateHull(verts, hull, tris); if (tris.Count == 0) { Console.WriteLine("Can't triangulate polygon, adding default data."); return; } if (sampleDist > 0) { //create sample locations BBox3 bounds = new BBox3(); bounds.Min = polyMeshVerts[0]; bounds.Max = polyMeshVerts[0]; for (int i = 1; i < numMeshVerts; i++) { Vector3Extensions.ComponentMin(ref bounds.Min, ref polyMeshVerts[i], out bounds.Min); Vector3Extensions.ComponentMax(ref bounds.Max, ref polyMeshVerts[i], out bounds.Max); } int x0 = (int)Math.Floor(bounds.Min.X / sampleDist); int x1 = (int)Math.Ceiling(bounds.Max.X / sampleDist); int z0 = (int)Math.Floor(bounds.Min.Z / sampleDist); int z1 = (int)Math.Ceiling(bounds.Max.Z / sampleDist); samples.Clear(); for (int z = z0; z < z1; z++) { for (int x = x0; x < x1; x++) { Vector3 pt = new Vector3(x * sampleDist, (bounds.Max.Y + bounds.Min.Y) * 0.5f, z * sampleDist); //make sure samples aren't too close to edge if (Distance.PointToPolygonSquared(pt, polyMeshVerts, numMeshVerts) > -sampleDist * 0.5f) continue; SamplingData sd = new SamplingData(x, GetHeight(pt, ics, compactField.CellHeight, hp), z, false); samples.Add(sd); } } //added samples for (int iter = 0; iter < samples.Count; iter++) { if (verts.Count >= MAX_VERTS) break; //find sample with most error Vector3 bestPt = Vector3.Zero; float bestDistance = 0; int bestIndex = -1; for (int i = 0; i < samples.Count; i++) { SamplingData sd = samples[i]; if (sd.IsSampled) continue; //jitter sample location to remove effects of bad triangulation Vector3 pt; pt.X = sd.X * sampleDist + GetJitterX(i) * compactField.CellSize * 0.1f; pt.Y = sd.Y * compactField.CellHeight; pt.Z = sd.Z * sampleDist + GetJitterY(i) * compactField.CellSize * 0.1f; float d = DistanceToTriMesh(pt, verts, tris); if (d < 0) continue; if (d > bestDistance) { bestDistance = d; bestIndex = i; bestPt = pt; } } if (bestDistance <= sampleMaxError || bestIndex == -1) break; SamplingData bsd = samples[bestIndex]; bsd.IsSampled = true; samples[bestIndex] = bsd; verts.Add(bestPt); //create new triangulation edges.Clear(); tris.Clear(); DelaunayHull(verts, hull, tris, edges); } } int ntris = tris.Count; if (ntris > MAX_TRIS) { //TODO we're using lists... let the user have super detailed meshes? //Perhaps just a warning saying there's a lot of tris? //tris.RemoveRange(MAX_TRIS + 1, tris.Count - MAX_TRIS); //Console.WriteLine("WARNING: shrinking number of triangles."); } }
private void GetHeightData(CompactHeightfield compactField, PolyMesh.Polygon poly, int polyCount, PolyVertex[] verts, int borderSize, HeightPatch hp) { var stack = new List<CompactSpanReference>(); bool empty = true; hp.Clear(); for (int y = 0; y < hp.Length; y++) { int hy = hp.Y + y + borderSize; for (int x = 0; x < hp.Width; x++) { int hx = hp.X + x + borderSize; var cells = compactField.Cells[hy * compactField.Width + hx]; for (int i = cells.StartIndex, end = cells.StartIndex + cells.Count; i < end; i++) { var span = compactField.Spans[i]; if (span.Region == poly.RegionId) { hp[x, y] = span.Minimum; empty = false; bool border = false; for (var dir = Direction.West; dir <= Direction.South; dir++) { if (span.IsConnected(dir)) { int ax = hx + dir.GetHorizontalOffset(); int ay = hy + dir.GetVerticalOffset(); int ai = compactField.Cells[ay * compactField.Width + ax].StartIndex + CompactSpan.GetConnection(ref span, dir); if (compactField.Spans[ai].Region != poly.RegionId) { border = true; break; } } } if (border) stack.Add(new CompactSpanReference(hx, hy, i)); break; } } } } if (empty) GetHeightDataSeedsFromVertices(compactField, poly, polyCount, verts, borderSize, hp, stack); const int RetractSize = 256; int head = 0; while (head < stack.Count) { var cell = stack[head++]; var cs = compactField[cell]; if (head >= RetractSize) { head = 0; if (stack.Count > RetractSize) { for (int i = 0; i < stack.Count - RetractSize; i++) stack[i] = stack[i + RetractSize]; } int targetSize = stack.Count % RetractSize; while (stack.Count > targetSize) stack.RemoveAt(stack.Count - 1); } //loop in all four directions for (var dir = Direction.West; dir <= Direction.South; dir++) { //skip if (!cs.IsConnected(dir)) continue; int ax = cell.X + dir.GetHorizontalOffset(); int ay = cell.Y + dir.GetVerticalOffset(); int hx = ax - hp.X - borderSize; int hy = ay - hp.Y - borderSize; if (hx < 0 || hx >= hp.Width || hy < 0 || hy >= hp.Length) continue; //only continue if height is unset if (hp.IsSet(hy * hp.Width + hx)) continue; //get new span int ai = compactField.Cells[ay * compactField.Width + ax].StartIndex + CompactSpan.GetConnection(ref cs, dir); CompactSpan ds = compactField.Spans[ai]; hp[hx, hy] = ds.Minimum; stack.Add(new CompactSpanReference(ax, ay, ai)); } } }