// Copyright (c) 2013-2015 Robert Rouhani and other contributors (see CONTRIBUTORS file). // Licensed under the MIT License - https://raw.github.com/Robmaister/SharpNav/master/LICENSE using System; using System.Collections.Generic; using SharpNav.Geometry; namespace SharpNav { /// /// A more memory-compact heightfield that stores open spans of voxels instead of closed ones. /// public class CompactHeightfield { private BBox3 bounds; private int width, height, length; private float cellSize, cellHeight; private CompactCell[] cells; private CompactSpan[] spans; private Area[] areas; //distance field private int[] distances; private int maxDistance; //region private int maxRegions; private int borderSize; /// /// Initializes a new instance of the class. /// /// A to build from. /// The settings to build with. public CompactHeightfield(Heightfield field, NavMeshGenerationSettings settings) : this(field, settings.VoxelAgentHeight, settings.VoxelMaxClimb) { } /// /// Initializes a new instance of the class. /// /// A to build from. /// The maximum difference in height to filter. /// The maximum difference in slope to filter. public CompactHeightfield(Heightfield field, int walkableHeight, int walkableClimb) { this.bounds = field.Bounds; this.width = field.Width; this.height = field.Height; this.length = field.Length; this.cellSize = field.CellSizeXZ; this.cellHeight = field.CellHeight; int spanCount = field.SpanCount; cells = new CompactCell[width * length]; spans = new CompactSpan[spanCount]; areas = new Area[spanCount]; //iterate over the Heightfield's cells int spanIndex = 0; for (int i = 0; i < cells.Length; i++) { //get the heightfield span list, skip if empty var fs = field[i].Spans; if (fs.Count == 0) continue; CompactCell c = new CompactCell(spanIndex, 0); //convert the closed spans to open spans int lastInd = fs.Count - 1; for (int j = 0; j < lastInd; j++) { var s = fs[j]; if (s.Area.IsWalkable) { CompactSpan.FromMinMax(s.Maximum, fs[j + 1].Minimum, out spans[spanIndex]); areas[spanIndex] = s.Area; spanIndex++; c.Count++; } } //the last closed span that has an "infinite" height var lastS = fs[lastInd]; if (lastS.Area.IsWalkable) { spans[spanIndex] = new CompactSpan(fs[lastInd].Maximum, int.MaxValue); areas[spanIndex] = lastS.Area; spanIndex++; c.Count++; } cells[i] = c; } //set neighbor connections for (int z = 0; z < length; z++) { for (int x = 0; x < width; x++) { CompactCell c = cells[z * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; for (var dir = Direction.West; dir <= Direction.South; dir++) { int dx = x + dir.GetHorizontalOffset(); int dz = z + dir.GetVerticalOffset(); if (dx < 0 || dz < 0 || dx >= width || dz >= length) continue; CompactCell dc = cells[dz * width + dx]; for (int j = dc.StartIndex, cellEnd = dc.StartIndex + dc.Count; j < cellEnd; j++) { CompactSpan ds = spans[j]; int overlapBottom, overlapTop; CompactSpan.OverlapMin(ref s, ref ds, out overlapBottom); CompactSpan.OverlapMax(ref s, ref ds, out overlapTop); //Make sure that the agent can walk to the next span and that the span isn't a huge drop or climb if ((overlapTop - overlapBottom) >= walkableHeight && Math.Abs(ds.Minimum - s.Minimum) <= walkableClimb) { int con = j - dc.StartIndex; CompactSpan.SetConnection(dir, con, ref spans[i]); break; } } } } } } } /// /// Gets the width of the in voxel units. /// public int Width { get { return width; } } /// /// Gets the height of the in voxel units. /// public int Height { get { return height; } } /// /// Gets the length of the in voxel units. /// public int Length { get { return length; } } /// /// Gets the world-space bounding box. /// public BBox3 Bounds { get { return bounds; } } /// /// Gets the world-space size of a cell in the XZ plane. /// public float CellSize { get { return cellSize; } } /// /// Gets the world-space size of a cell in the Y direction. /// public float CellHeight { get { return cellHeight; } } /// /// Gets the maximum distance to a border based on the distance field. This value is undefined prior to /// calling . /// public int MaxDistance { get { return maxDistance; } } /// /// Gets an array of distances from a span to the nearest border. This value is undefined prior to calling /// . /// public int[] Distances { get { return distances; } } /// /// Gets the size of the border. /// public int BorderSize { get { return borderSize; } } /// /// Gets the maximum number of allowed regions. /// public int MaxRegions { get { return maxRegions; } } /// /// Gets the cells. /// public CompactCell[] Cells { get { return cells; } } /// /// Gets the spans. /// public CompactSpan[] Spans { get { return spans; } } /// /// Gets the area flags. /// public Area[] Areas { get { return areas; } } /// /// Gets an of of the spans at a specified coordiante. /// /// The X coordinate. /// The Y coordinate. /// An of . public IEnumerable this[int x, int y] { get { if (x < 0 || x >= width || y < 0 || y >= length) throw new IndexOutOfRangeException(); CompactCell c = cells[y * width + x]; int end = c.StartIndex + c.Count; for (int i = c.StartIndex; i < end; i++) yield return spans[i]; } } /// /// Gets an of s at a specified index. /// /// The index. /// An of . public IEnumerable this[int i] { get { CompactCell c = cells[i]; int end = c.StartIndex + c.Count; for (int j = c.StartIndex; j < end; j++) yield return spans[j]; } } /// /// Gets the specified by the reference. /// /// A reference to a span in this . /// The referenced span. public CompactSpan this[CompactSpanReference spanRef] { get { return spans[spanRef.Index]; } } /// /// Builds a distance field, or the distance to the nearest unwalkable area. /// public void BuildDistanceField() { if (distances == null) distances = new int[spans.Length]; //fill up all the values in src CalculateDistanceField(distances); //blur the distances BoxBlur(distances, 1, null); //find the maximum distance this.maxDistance = 0; for (int i = 0; i < distances.Length; i++) this.maxDistance = Math.Max(distances[i], this.maxDistance); } /// /// Erodes the walkable areas in the map. /// /// /// If you have already called , it will automatically be called again after /// erosion because it needs to be recalculated. /// /// The radius to erode from unwalkable areas. public void Erode(int radius) { radius *= 2; //get a distance field int[] dists = new int[spans.Length]; CalculateDistanceField(dists); //erode close-to-null areas to null areas. for (int i = 0; i < spans.Length; i++) if (dists[i] < radius) areas[i] = Area.Null; //marking areas as null changes the distance field, so recalculate it. if (distances != null) BuildDistanceField(); } /// /// The central method for building regions, which consists of connected, non-overlapping walkable spans. /// /// The border size /// If smaller than this value, region will be null /// Reduce unneccesarily small regions public void BuildRegions(int borderSize, int minRegionArea, int mergeRegionArea) { if (distances == null) BuildDistanceField(); const int LogStackCount = 3; const int StackCount = 1 << LogStackCount; List[] stacks = new List[StackCount]; for (int i = 0; i < stacks.Length; i++) stacks[i] = new List(1024); RegionId[] regions = new RegionId[spans.Length]; int[] floodDistances = new int[spans.Length]; RegionId[] regionBuffer = new RegionId[spans.Length]; int[] distanceBuffer = new int[spans.Length]; int regionIndex = 1; int level = ((maxDistance + 1) / 2) * 2; const int ExpandIters = 8; if (borderSize > 0) { //make sure border doesn't overflow int borderWidth = Math.Min(width, borderSize); int borderHeight = Math.Min(length, borderSize); //fill regions FillRectangleRegion(regions, new RegionId(regionIndex++, RegionFlags.Border), 0, borderWidth, 0, length); FillRectangleRegion(regions, new RegionId(regionIndex++, RegionFlags.Border), width - borderWidth, width, 0, length); FillRectangleRegion(regions, new RegionId(regionIndex++, RegionFlags.Border), 0, width, 0, borderHeight); FillRectangleRegion(regions, new RegionId(regionIndex++, RegionFlags.Border), 0, width, length - borderHeight, length); this.borderSize = borderSize; } int stackId = -1; while (level > 0) { level = level >= 2 ? level - 2 : 0; stackId = (stackId + 1) & (StackCount - 1); if (stackId == 0) SortCellsByLevel(regions, stacks, level, StackCount, 1); else AppendStacks(stacks[stackId - 1], stacks[stackId], regions); //expand current regions until no new empty connected cells found ExpandRegions(regions, floodDistances, ExpandIters, level, stacks[stackId], regionBuffer, distanceBuffer); //mark new regions with ids for (int j = 0; j < stacks[stackId].Count; j++) { var spanRef = stacks[stackId][j]; if (spanRef.Index >= 0 && regions[spanRef.Index] == 0) if (FloodRegion(regions, floodDistances, regionIndex, level, ref spanRef)) regionIndex++; } } //expand current regions until no new empty connected cells found ExpandRegions(regions, floodDistances, ExpandIters * 8, 0, null, regionBuffer, distanceBuffer); //filter out small regions this.maxRegions = FilterSmallRegions(regions, minRegionArea, mergeRegionArea, regionIndex); //write the result out for (int i = 0; i < spans.Length; i++) spans[i].Region = regions[i]; } /// /// Merge two stacks to get a single stack. /// /// The original stack /// The new stack /// Region ids private static void AppendStacks(List source, List destination, RegionId[] regions) { for (int j = 0; j < source.Count; j++) { var spanRef = source[j]; if (spanRef.Index < 0 || regions[spanRef.Index] != 0) continue; destination.Add(spanRef); } } /// /// Discards regions that are too small. /// /// region data /// The minimum area a region can have /// The size of the regions after merging /// determines the number of regions available /// The reduced number of regions. private int FilterSmallRegions(RegionId[] regionIds, int minRegionArea, int mergeRegionSize, int maxRegionId) { int numRegions = maxRegionId + 1; Region[] regions = new Region[numRegions]; //construct regions for (int i = 0; i < numRegions; i++) regions[i] = new Region(i); //find edge of a region and find connections around a contour for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[x + y * width]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpanReference spanRef = new CompactSpanReference(x, y, i); //HACK since the border region flag makes r negative, I changed r == 0 to r <= 0. Figure out exactly what maxRegionId's purpose is and see if Region.IsBorderOrNull is all we need. int r = (int)regionIds[i]; if (r <= 0 || (int)r >= numRegions) continue; Region reg = regions[(int)r]; reg.SpanCount++; //update floors for (int j = c.StartIndex; j < end; j++) { if (i == j) continue; RegionId floorId = regionIds[j]; if (floorId == 0 || (int)floorId >= numRegions) continue; reg.AddUniqueFloorRegion(floorId); } //have found contour if (reg.Connections.Count > 0) continue; reg.AreaType = areas[i]; //check if this cell is next to a border for (var dir = Direction.West; dir <= Direction.South; dir++) { if (IsSolidEdge(regionIds, ref spanRef, dir)) { //The cell is at a border. //Walk around contour to find all neighbors WalkContour(regionIds, spanRef, dir, reg.Connections); break; } } } } } //Remove too small regions Stack stack = new Stack(); List trace = new List(); for (int i = 0; i < numRegions; i++) { Region reg = regions[i]; if (reg.IsBorderOrNull || reg.SpanCount == 0 || reg.Visited) continue; //count the total size of all connected regions //also keep track of the regions connections to a tile border bool connectsToBorder = false; int spanCount = 0; stack.Clear(); trace.Clear(); reg.Visited = true; stack.Push(reg.Id); while (stack.Count > 0) { //pop RegionId ri = stack.Pop(); Region creg = regions[(int)ri]; spanCount += creg.SpanCount; trace.Add(ri); for (int j = 0; j < creg.Connections.Count; j++) { if (RegionId.HasFlags(creg.Connections[j], RegionFlags.Border)) { connectsToBorder = true; continue; } Region neiReg = regions[(int)creg.Connections[j]]; if (neiReg.Visited || neiReg.IsBorderOrNull) continue; //visit stack.Push(neiReg.Id); neiReg.Visited = true; } } //if the accumulated region size is too small, remove it //do not remove areas which connect to tile borders as their size can't be estimated correctly //and removing them can potentially remove necessary areas if (spanCount < minRegionArea && !connectsToBorder) { //kill all visited regions for (int j = 0; j < trace.Count; j++) { int index = (int)trace[j]; regions[index].SpanCount = 0; regions[index].Id = RegionId.Null; } } } //Merge too small regions to neighbor regions int mergeCount = 0; do { mergeCount = 0; for (int i = 0; i < numRegions; i++) { Region reg = regions[i]; if (reg.IsBorderOrNull || reg.SpanCount == 0) continue; //check to see if region should be merged if (reg.SpanCount > mergeRegionSize && reg.IsConnectedToBorder()) continue; //small region with more than one connection or region which is not connected to border at all //find smallest neighbor that connects to this one int smallest = int.MaxValue; RegionId mergeId = reg.Id; for (int j = 0; j < reg.Connections.Count; j++) { if (RegionId.HasFlags(reg.Connections[j], RegionFlags.Border)) continue; Region mreg = regions[(int)reg.Connections[j]]; if (mreg.IsBorderOrNull) continue; if (mreg.SpanCount < smallest && reg.CanMergeWith(mreg) && mreg.CanMergeWith(reg)) { smallest = mreg.SpanCount; mergeId = mreg.Id; } } //found new id if (mergeId != reg.Id) { RegionId oldId = reg.Id; Region target = regions[(int)mergeId]; //merge regions if (target.MergeWithRegion(reg)) { //fix regions pointing to current region for (int j = 0; j < numRegions; j++) { if (regions[j].IsBorderOrNull) continue; //if another regions was already merged into current region //change the nid of the previous region too if (regions[j].Id == oldId) regions[j].Id = mergeId; //replace current region with new one if current region is neighbor regions[j].ReplaceNeighbour(oldId, mergeId); } mergeCount++; } } } } while (mergeCount > 0); //Compress region ids for (int i = 0; i < numRegions; i++) { regions[i].Remap = false; if (regions[i].IsBorderOrNull) continue; regions[i].Remap = true; } int regIdGen = 0; for (int i = 0; i < numRegions; i++) { if (!regions[i].Remap) continue; RegionId oldId = regions[i].Id; RegionId newId = new RegionId(++regIdGen); for (int j = i; j < numRegions; j++) { if (regions[j].Id == oldId) { regions[j].Id = newId; regions[j].Remap = false; } } } //Remap regions for (int i = 0; i < spans.Length; i++) { if (!RegionId.HasFlags(regionIds[i], RegionFlags.Border)) regionIds[i] = regions[(int)regionIds[i]].Id; } return regIdGen; } /// /// A distance field estimates how far each span is from its nearest border span. This data is needed for region generation. /// /// Array of values, each corresponding to an individual span private void CalculateDistanceField(int[] src) { //initialize distance and points for (int i = 0; i < spans.Length; i++) src[i] = int.MaxValue; //mark boundary cells for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[y * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; Area area = areas[i]; bool isBoundary = false; if (s.ConnectionCount != 4) isBoundary = true; else { for (var dir = Direction.West; dir <= Direction.South; dir++) { int dx = x + dir.GetHorizontalOffset(); int dy = y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); if (area != areas[di]) { isBoundary = true; break; } } } if (isBoundary) src[i] = 0; } } } //pass 1 for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[y * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; if (s.IsConnected(Direction.West)) { //(-1, 0) int dx = x + Direction.West.GetHorizontalOffset(); int dy = y + Direction.West.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, Direction.West); CompactSpan ds = spans[di]; if (src[di] + 2 < src[i]) src[i] = src[di] + 2; //(-1, -1) if (ds.IsConnected(Direction.South)) { int ddx = dx + Direction.South.GetHorizontalOffset(); int ddy = dy + Direction.South.GetVerticalOffset(); int ddi = cells[ddx + ddy * width].StartIndex + CompactSpan.GetConnection(ref ds, Direction.South); if (src[ddi] + 3 < src[i]) src[i] = src[ddi] + 3; } } if (s.IsConnected(Direction.South)) { //(0, -1) int dx = x + Direction.South.GetHorizontalOffset(); int dy = y + Direction.South.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, Direction.South); CompactSpan ds = spans[di]; if (src[di] + 2 < src[i]) src[i] = src[di] + 2; //(1, -1) if (ds.IsConnected(Direction.East)) { int ddx = dx + Direction.East.GetHorizontalOffset(); int ddy = dy + Direction.East.GetVerticalOffset(); int ddi = cells[ddx + ddy * width].StartIndex + CompactSpan.GetConnection(ref ds, Direction.East); if (src[ddi] + 3 < src[i]) src[i] = src[ddi] + 3; } } } } } //pass 2 for (int y = length - 1; y >= 0; y--) { for (int x = width - 1; x >= 0; x--) { CompactCell c = cells[y * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; if (s.IsConnected(Direction.East)) { //(1, 0) int dx = x + Direction.East.GetHorizontalOffset(); int dy = y + Direction.East.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, Direction.East); CompactSpan ds = spans[di]; if (src[di] + 2 < src[i]) src[i] = src[di] + 2; //(1, 1) if (ds.IsConnected(Direction.North)) { int ddx = dx + Direction.North.GetHorizontalOffset(); int ddy = dy + Direction.North.GetVerticalOffset(); int ddi = cells[ddx + ddy * width].StartIndex + CompactSpan.GetConnection(ref ds, Direction.North); if (src[ddi] + 3 < src[i]) src[i] = src[ddi] + 3; } } if (s.IsConnected(Direction.North)) { //(0, 1) int dx = x + Direction.North.GetHorizontalOffset(); int dy = y + Direction.North.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, Direction.North); CompactSpan ds = spans[di]; if (src[di] + 2 < src[i]) src[i] = src[di] + 2; //(-1, 1) if (ds.IsConnected(Direction.West)) { int ddx = dx + Direction.West.GetHorizontalOffset(); int ddy = dy + Direction.West.GetVerticalOffset(); int ddi = cells[ddx + ddy * width].StartIndex + CompactSpan.GetConnection(ref ds, Direction.West); if (src[ddi] + 3 < src[i]) src[i] = src[ddi] + 3; } } } } } } /// /// Part of building the distance field. It may or may not return an array equal to src. /// /// The original distances. /// The distance threshold below which no blurring occurs. /// A buffer that is at least the same length as for working memory. private void BoxBlur(int[] distances, int threshold, int[] buffer) { threshold *= 2; //if the optional buffer parameter wasn't passed in, or is too small, make a new one. if (buffer == null || buffer.Length < distances.Length) buffer = new int[distances.Length]; Buffer.BlockCopy(distances, 0, buffer, 0, distances.Length * sizeof(int)); //horizontal pass for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[y * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; int cellDist = buffer[i]; //if the distance is below the threshold, skip the span. if (cellDist <= threshold) continue; //iterate the full neighborhood of 8 spans. int d = cellDist; for (Direction dir = Direction.West; dir <= Direction.South; dir++) { if (s.IsConnected(dir)) { int dx = x + dir.GetHorizontalOffset(); int dy = y + dir.GetVerticalOffset(); int di = cells[dy * width + dx].StartIndex + CompactSpan.GetConnection(ref s, dir); d += buffer[di]; CompactSpan ds = spans[di]; Direction dir2 = dir.NextClockwise(); if (ds.IsConnected(dir2)) { int dx2 = dx + dir2.GetHorizontalOffset(); int dy2 = dy + dir2.GetVerticalOffset(); int di2 = cells[dy2 * width + dx2].StartIndex + CompactSpan.GetConnection(ref ds, dir2); d += buffer[di2]; } else { d += cellDist; } } else { //add the center span if there's no connection. d += cellDist * 2; } } //save new value to destination distances[i] = (d + 5) / 9; } } } } /// /// Expands regions to include spans above a specified water level. /// /// The array of region IDs. /// The array of flooding distances. /// The maximum number of allowed iterations before breaking. /// The current water level. /// A stack of span references that are being expanded. /// A buffer to store region IDs. Must be at least the same size as regions. /// A buffer to store flood distances. Must be at least the same size as floodDistances. private void ExpandRegions(RegionId[] regions, int[] floodDistances, int maxIterations, int level, List stack, RegionId[] regionBuffer, int[] distanceBuffer) { //generate buffers if they're not passed in or if they're too small. if (regionBuffer == null || regionBuffer.Length < regions.Length) regionBuffer = new RegionId[regions.Length]; if (distanceBuffer == null || distanceBuffer.Length < floodDistances.Length) distanceBuffer = new int[floodDistances.Length]; //copy existing data into the buffers. Array.Copy(regions, 0, regionBuffer, 0, regions.Length); Array.Copy(floodDistances, 0, distanceBuffer, 0, floodDistances.Length); //find cells that are being expanded to. if (stack == null) { stack = new List(); for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[x + y * width]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { //a cell is being expanded to if it's distance is greater than the current level, //but no region has been asigned yet. It must also not be in a null area. if (this.distances[i] >= level && regions[i] == 0 && areas[i].IsWalkable) stack.Add(new CompactSpanReference(x, y, i)); } } } } else { for (int j = 0; j < stack.Count; j++) { if (regions[stack[j].Index] != 0) stack[j] = CompactSpanReference.Null; } } //assign regions to all the cells that are being expanded to. //will run until it's done or it runs maxIterations times. int iter = 0; while (stack.Count > 0) { //spans in the stack that are skipped: // - assigned a region ID in an earlier iteration // - not neighboring any spans with region IDs int skipped = 0; for (int j = 0; j < stack.Count; j++) { CompactSpanReference spanRef = stack[j]; int x = spanRef.X; int y = spanRef.Y; int i = spanRef.Index; //skip regions already assigned to if (i < 0) { skipped++; continue; } RegionId r = regions[i]; Area area = areas[i]; CompactSpan s = spans[i]; //search direct neighbors for the one with the smallest distance value int minDist = int.MaxValue; for (var dir = Direction.West; dir <= Direction.South; dir++) { if (!s.IsConnected(dir)) continue; int dx = x + dir.GetHorizontalOffset(); int dy = y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); if (areas[di] != area) continue; //compare distance to previous best RegionId ri = regions[di]; int dist = floodDistances[di]; if (!(ri.IsNull || RegionId.HasFlags(ri, RegionFlags.Border))) { //set region and distance if better if (dist + 2 < minDist) { r = ri; minDist = dist + 2; } } } if (r != 0) { //set the region and distance for this span regionBuffer[i] = r; distanceBuffer[i] = minDist; //mark this item in the stack as assigned for the next iteration. stack[j] = CompactSpanReference.Null; } else { //skip spans that don't neighbor any regions skipped++; } } //if the entire stack is being skipped, we're done. if (skipped == stack.Count) break; //Copy from the buffers back to the original arrays. This is done after each iteration //because changing it in-place has some side effects for the other spans in the stack. Array.Copy(regionBuffer, 0, regions, 0, regions.Length); Array.Copy(distanceBuffer, 0, floodDistances, 0, floodDistances.Length); if (level > 0) { //if we hit maxIterations before expansion is done, break out anyways. ++iter; if (iter >= maxIterations) break; } } } /// /// Floods the regions at a certain level /// /// source region /// source distances /// region id /// current level /// A reference to the starting span. /// Always true. private bool FloodRegion(RegionId[] regions, int[] floodDistances, int regionIndex, int level, ref CompactSpanReference start) { //TODO this method should always return true, make it not return a bool? //flood fill mark region Stack stack = new Stack(); stack.Push(start); Area area = areas[start.Index]; regions[start.Index] = new RegionId(regionIndex); floodDistances[start.Index] = 0; int lev = level >= 2 ? level - 2 : 0; int count = 0; while (stack.Count > 0) { CompactSpanReference cell = stack.Pop(); CompactSpan cs = spans[cell.Index]; //check if any of the neighbors already have a valid reigon set RegionId ar = RegionId.Null; for (var dir = Direction.West; dir <= Direction.South; dir++) { //8 connected if (cs.IsConnected(dir)) { int dx = cell.X + dir.GetHorizontalOffset(); int dy = cell.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref cs, dir); if (areas[di] != area) continue; RegionId nr = regions[di]; if (RegionId.HasFlags(nr, RegionFlags.Border)) //skip borders continue; if (nr != 0 && nr != regionIndex) { ar = nr; break; } CompactSpan ds = spans[di]; Direction dir2 = dir.NextClockwise(); if (ds.IsConnected(dir2)) { int dx2 = dx + dir2.GetHorizontalOffset(); int dy2 = dy + dir2.GetVerticalOffset(); int di2 = cells[dx2 + dy2 * width].StartIndex + CompactSpan.GetConnection(ref ds, dir2); if (areas[di2] != area) continue; RegionId nr2 = regions[di2]; if (nr2 != 0 && nr2 != regionIndex) { ar = nr2; break; } } } } if (ar != 0) { regions[cell.Index] = RegionId.Null; continue; } count++; //expand neighbors for (var dir = Direction.West; dir <= Direction.South; dir++) { if (cs.IsConnected(dir)) { int dx = cell.X + dir.GetHorizontalOffset(); int dy = cell.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref cs, dir); if (areas[di] != area) continue; if (distances[di] >= lev && regions[di] == 0) { regions[di] = new RegionId(regionIndex); floodDistances[di] = 0; stack.Push(new CompactSpanReference(dx, dy, di)); } } } } return count > 0; } /// /// Checks whether the edge from a span in a direction is a solid edge. /// A solid edge is an edge between two regions. /// /// The region ID array. /// A reference to the span connected to the edge. /// The direction of the edge. /// A value indicating whether the described edge is solid. private bool IsSolidEdge(RegionId[] regions, ref CompactSpanReference spanRef, Direction dir) { CompactSpan s = spans[spanRef.Index]; RegionId r = RegionId.Null; if (s.IsConnected(dir)) { int dx = spanRef.X + dir.GetHorizontalOffset(); int dy = spanRef.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); r = regions[di]; } if (r == regions[spanRef.Index]) return false; return true; } /// /// Try to visit all the spans. May be needed in filtering small regions. /// /// an array of region values /// The span to start walking from. /// The direction to start walking in. /// A collection of regions to append to. private void WalkContour(RegionId[] regions, CompactSpanReference spanRef, Direction dir, List cont) { Direction startDir = dir; int starti = spanRef.Index; CompactSpan ss = spans[starti]; RegionId curReg = RegionId.Null; if (ss.IsConnected(dir)) { int dx = spanRef.X + dir.GetHorizontalOffset(); int dy = spanRef.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref ss, dir); curReg = regions[di]; } cont.Add(curReg); int iter = 0; while (++iter < 40000) { CompactSpan s = spans[spanRef.Index]; if (IsSolidEdge(regions, ref spanRef, dir)) { //choose the edge corner RegionId r = RegionId.Null; if (s.IsConnected(dir)) { int dx = spanRef.X + dir.GetHorizontalOffset(); int dy = spanRef.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); r = regions[di]; } if (r != curReg) { curReg = r; cont.Add(curReg); } dir = dir.NextClockwise(); //rotate clockwise } else { int di = -1; int dx = spanRef.X + dir.GetHorizontalOffset(); int dy = spanRef.Y + dir.GetVerticalOffset(); if (s.IsConnected(dir)) { CompactCell dc = cells[dx + dy * width]; di = dc.StartIndex + CompactSpan.GetConnection(ref s, dir); } if (di == -1) { //shouldn't happen return; } spanRef = new CompactSpanReference(dx, dy, di); dir = dir.NextCounterClockwise(); //rotate counterclockwise } if (starti == spanRef.Index && startDir == dir) break; } //remove adjacent duplicates if (cont.Count > 1) { for (int j = 0; j < cont.Count;) { //next element int nj = (j + 1) % cont.Count; //adjacent duplicate found if (cont[j] == cont[nj]) cont.RemoveAt(j); else j++; } } } /// /// Fill in a rectangular area with a region ID. Spans in a null area are skipped. /// /// The region ID array. /// The ID to fill in. /// The left edge of the rectangle. /// The right edge of the rectangle. /// The bottom edge of the rectangle. /// The top edge of the rectangle. private void FillRectangleRegion(RegionId[] regions, RegionId newRegionId, int left, int right, int bottom, int top) { for (int y = bottom; y < top; y++) { for (int x = left; x < right; x++) { CompactCell c = cells[x + y * width]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { if (areas[i].IsWalkable) regions[i] = newRegionId; } } } } /// /// Sort the compact spans /// /// Region data /// Temporary stack of CompactSpanReference values /// Starting level /// The number of layers /// log base 2 of stack levels private void SortCellsByLevel(RegionId[] regions, List[] stacks, int startlevel, int numStacks, int logLevelsPerStack) { startlevel = startlevel >> logLevelsPerStack; for (int j = 0; j < numStacks; j++) stacks[j].Clear(); for (int y = 0; y < length; y++) { for (int x = 0; x < width; x++) { CompactCell c = cells[y * width + x]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { if (!areas[i].IsWalkable || !regions[i].IsNull) continue; int level = distances[i] >> logLevelsPerStack; int sId = startlevel - level; if (sId >= numStacks) continue; if (sId < 0) sId = 0; stacks[sId].Add(new CompactSpanReference(x, y, i)); } } } } /// /// Builds a set of s around the generated regions. Must be called after regions are generated. /// /// Settings for building the . /// A containing one contour per region. public ContourSet BuildContourSet(NavMeshGenerationSettings settings) { return BuildContourSet(settings.MaxEdgeError, settings.MaxEdgeLength, settings.ContourFlags); } /// /// Builds a set of s around the generated regions. Must be called after regions are generated. /// /// The maximum allowed deviation in a simplified contour from a raw one. /// The maximum edge length. /// Flags that change settings for the build process. /// A containing one contour per region. public ContourSet BuildContourSet(float maxError, int maxEdgeLength, ContourBuildFlags buildFlags) { BBox3 contourSetBounds = bounds; if (borderSize > 0) { //remove offset float pad = borderSize * cellSize; contourSetBounds.Min.X += pad; contourSetBounds.Min.Z += pad; contourSetBounds.Max.X -= pad; contourSetBounds.Max.Z -= pad; } int contourSetWidth = width - borderSize * 2; int contourSetLength = length - borderSize * 2; int maxContours = Math.Max(maxRegions, 8); var contours = new List(maxContours); EdgeFlags[] flags = new EdgeFlags[spans.Length]; //Modify flags array by using the CompactHeightfield data //mark boundaries for (int z = 0; z < length; z++) { for (int x = 0; x < width; x++) { //loop through all the spans in the cell CompactCell c = cells[x + z * width]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { CompactSpan s = spans[i]; //set the flag to 0 if the region is a border region or null. if (s.Region.IsNull || RegionId.HasFlags(s.Region, RegionFlags.Border)) { flags[i] = 0; continue; } //go through all the neighboring cells for (var dir = Direction.West; dir <= Direction.South; dir++) { //obtain region id RegionId r = RegionId.Null; if (s.IsConnected(dir)) { int dx = x + dir.GetHorizontalOffset(); int dz = z + dir.GetVerticalOffset(); int di = cells[dx + dz * width].StartIndex + CompactSpan.GetConnection(ref s, dir); r = spans[di].Region; } //region ids are equal if (r == s.Region) { //res marks all the internal edges EdgeFlagsHelper.AddEdge(ref flags[i], dir); } } //flags represents all the nonconnected edges, edges that are only internal //the edges need to be between different regions EdgeFlagsHelper.FlipEdges(ref flags[i]); } } } var verts = new List(); var simplified = new List(); for (int z = 0; z < length; z++) { for (int x = 0; x < width; x++) { CompactCell c = cells[x + z * width]; for (int i = c.StartIndex, end = c.StartIndex + c.Count; i < end; i++) { //flags is either 0000 or 1111 //in other words, not connected at all //or has all connections, which means span is in the middle and thus not an edge. if (flags[i] == EdgeFlags.None || flags[i] == EdgeFlags.All) { flags[i] = EdgeFlags.None; continue; } var spanRef = new CompactSpanReference(x, z, i); RegionId reg = this[spanRef].Region; if (reg.IsNull || RegionId.HasFlags(reg, RegionFlags.Border)) continue; //reset each iteration verts.Clear(); simplified.Clear(); //Walk along a contour, then build it WalkContour(spanRef, flags, verts); Contour.Simplify(verts, simplified, maxError, maxEdgeLength, buildFlags); Contour.RemoveDegenerateSegments(simplified); Contour contour = new Contour(simplified, reg, areas[i], borderSize); if (!contour.IsNull) contours.Add(contour); } } } //Check and merge bad contours for (int i = 0; i < contours.Count; i++) { Contour cont = contours[i]; //Check if contour is backwards if (cont.Area2D < 0) { //Find another contour to merge with int mergeIndex = -1; for (int j = 0; j < contours.Count; j++) { if (i == j) continue; //Must have at least one vertex, the same region ID, and be going forwards. Contour contj = contours[j]; if (contj.Vertices.Length != 0 && contj.RegionId == cont.RegionId && contj.Area2D > 0) { mergeIndex = j; break; } } //Merge if found. if (mergeIndex != -1) { contours[mergeIndex].MergeWith(cont); contours.RemoveAt(i); i--; } } } return new ContourSet(contours, contourSetBounds, contourSetWidth, contourSetLength); } /// /// Initial generation of the contours /// /// A referecne to the span to start walking from. /// An array of flags determinining /// The vertices of a contour. private void WalkContour(CompactSpanReference spanReference, EdgeFlags[] flags, List points) { Direction dir = Direction.West; //find the first direction that has a connection while (!EdgeFlagsHelper.IsConnected(ref flags[spanReference.Index], dir)) dir++; Direction startDir = dir; int startIndex = spanReference.Index; Area area = areas[startIndex]; //TODO make the max iterations value a variable int iter = 0; while (++iter < 40000) { // this direction is connected if (EdgeFlagsHelper.IsConnected(ref flags[spanReference.Index], dir)) { // choose the edge corner bool isBorderVertex; bool isAreaBorder = false; int px = spanReference.X; int py = GetCornerHeight(spanReference, dir, out isBorderVertex); int pz = spanReference.Y; switch (dir) { case Direction.West: pz++; break; case Direction.North: px++; pz++; break; case Direction.East: px++; break; } RegionId r = RegionId.Null; CompactSpan s = this[spanReference]; if (s.IsConnected(dir)) { int dx = spanReference.X + dir.GetHorizontalOffset(); int dy = spanReference.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); r = spans[di].Region; if (area != areas[di]) isAreaBorder = true; } // apply flags if neccessary if (isBorderVertex) r = RegionId.WithFlags(r, RegionFlags.VertexBorder); if (isAreaBorder) r = RegionId.WithFlags(r, RegionFlags.AreaBorder); //save the point points.Add(new ContourVertex(px, py, pz, r)); EdgeFlagsHelper.RemoveEdge(ref flags[spanReference.Index], dir); // remove visited edges dir = dir.NextClockwise(); // rotate clockwise } else { //get a new cell(x, y) and span index(i) int di = -1; int dx = spanReference.X + dir.GetHorizontalOffset(); int dy = spanReference.Y + dir.GetVerticalOffset(); CompactSpan s = this[spanReference]; if (s.IsConnected(dir)) { CompactCell dc = cells[dx + dy * width]; di = dc.StartIndex + CompactSpan.GetConnection(ref s, dir); } if (di == -1) { // shouldn't happen // TODO if this shouldn't happen, this check shouldn't be necessary. throw new InvalidOperationException("Something went wrong"); } spanReference = new CompactSpanReference(dx, dy, di); dir = dir.NextCounterClockwise(); // rotate counterclockwise } if (startIndex == spanReference.Index && startDir == dir) break; } } /// /// Helper method for WalkContour function /// /// The span to get the corner height for. /// The direction to get the corner height from. /// Determine whether the vertex is a border or not. /// The corner height. private int GetCornerHeight(CompactSpanReference sr, Direction dir, out bool isBorderVertex) { isBorderVertex = false; CompactSpan s = this[sr]; int cornerHeight = s.Minimum; Direction dirp = dir.NextClockwise(); //new clockwise direction RegionId[] cornerRegs = new RegionId[4]; Area[] cornerAreas = new Area[4]; //combine region and area codes in order to prevent border vertices, which are in between two areas, to be removed cornerRegs[0] = s.Region; cornerAreas[0] = areas[sr.Index]; if (s.IsConnected(dir)) { //get neighbor span int dx = sr.X + dir.GetHorizontalOffset(); int dy = sr.Y + dir.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dir); CompactSpan ds = spans[di]; cornerHeight = Math.Max(cornerHeight, ds.Minimum); cornerRegs[1] = spans[di].Region; cornerAreas[1] = areas[di]; //get neighbor of neighbor's span if (ds.IsConnected(dirp)) { int dx2 = dx + dirp.GetHorizontalOffset(); int dy2 = dy + dirp.GetVerticalOffset(); int di2 = cells[dx2 + dy2 * width].StartIndex + CompactSpan.GetConnection(ref ds, dirp); CompactSpan ds2 = spans[di2]; cornerHeight = Math.Max(cornerHeight, ds2.Minimum); cornerRegs[2] = ds2.Region; cornerAreas[2] = areas[di2]; } } //get neighbor span if (s.IsConnected(dirp)) { int dx = sr.X + dirp.GetHorizontalOffset(); int dy = sr.Y + dirp.GetVerticalOffset(); int di = cells[dx + dy * width].StartIndex + CompactSpan.GetConnection(ref s, dirp); CompactSpan ds = spans[di]; cornerHeight = Math.Max(cornerHeight, ds.Minimum); cornerRegs[3] = ds.Region; cornerAreas[3] = areas[di]; //get neighbor of neighbor's span if (ds.IsConnected(dir)) { int dx2 = dx + dir.GetHorizontalOffset(); int dy2 = dy + dir.GetVerticalOffset(); int di2 = cells[dx2 + dy2 * width].StartIndex + CompactSpan.GetConnection(ref ds, dir); CompactSpan ds2 = spans[di2]; cornerHeight = Math.Max(cornerHeight, ds2.Minimum); cornerRegs[2] = ds2.Region; cornerAreas[2] = areas[di2]; } } //check if vertex is special edge vertex //if so, these vertices will be removed later for (int j = 0; j < 4; j++) { int a = j; int b = (j + 1) % 4; int c = (j + 2) % 4; int d = (j + 3) % 4; RegionId ra = cornerRegs[a], rb = cornerRegs[b], rc = cornerRegs[c], rd = cornerRegs[d]; Area aa = cornerAreas[a], ab = cornerAreas[b], ac = cornerAreas[c], ad = cornerAreas[d]; //the vertex is a border vertex if: //two same exterior cells in a row followed by two interior cells and none of the regions are out of bounds bool twoSameExteriors = RegionId.HasFlags(ra, RegionFlags.Border) && RegionId.HasFlags(rb, RegionFlags.Border) && (ra == rb && aa == ab); bool twoSameInteriors = !(RegionId.HasFlags(rc, RegionFlags.Border) || RegionId.HasFlags(rd, RegionFlags.Border)); bool intsSameArea = ac == ad; bool noZeros = ra != 0 && rb != 0 && rc != 0 && rd != 0 && aa != 0 && ab != 0 && ac != 0 && ad != 0; if (twoSameExteriors && twoSameInteriors && intsSameArea && noZeros) { isBorderVertex = true; break; } } return cornerHeight; } } }