using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace FSO.Common.WorldGeometry { public class SimplifiedHeightmap { public float HeightMultiplier = 1 / 40f; public int Size; public int Size1; public ushort[] Map; //10x resolution of ts1 terrain. public ushort[][] SecondDerivativePyramid; public SimplifiedHeightmap(int size, ushort[] data) { Size = size; Size1 = size - 1; Map = data; BuildSecondDerivative(); } public void BuildSecondDerivative() { //first, build the full res second derivative map var sd = new ushort[Map.Length]; int i = 0; //x derivative for (int y = 0; y < Size; y++) { ushort lastValue = Map[i++]; ushort firstDerivative = 0; for (int x = 1; x < Size; x++) { ushort value = Map[i]; ushort newFirstDerivative = (ushort)Math.Abs(value - lastValue); sd[(i++) - 1] = (ushort)Math.Abs(newFirstDerivative - firstDerivative); firstDerivative = newFirstDerivative; lastValue = value; } } //y derivative i = 0; for (int y = 0; y < Size; y++) { i = y; ushort lastValue = Map[i]; i += Size; ushort firstDerivative = 0; for (int x = 1; x < Size; x++) { ushort value = Map[i]; ushort newFirstDerivative = (ushort)Math.Abs(value - lastValue); sd[i-Size] = Math.Max(sd[i-Size], (ushort)Math.Abs(newFirstDerivative - firstDerivative)); i += Size; firstDerivative = newFirstDerivative; lastValue = value; } } //build mipLevels var levels = 7; //gen 2x2 through 64x64 SecondDerivativePyramid = new ushort[levels][]; SecondDerivativePyramid[0] = sd; var curLevel = sd; for (int mip = 1; mip < levels; mip++) { var size = (int)Math.Sqrt(curLevel.Length); var mipLevel = new ushort[curLevel.Length / 4]; i = 0; for (int y = 0; y < size; y++) { var target = (y / 2) * size / 2; for (int x = 0; x < size; x += 2) { mipLevel[target] = Math.Max(mipLevel[target], Math.Max(curLevel[i++], curLevel[i++])); target++; } } SecondDerivativePyramid[mip] = mipLevel; curLevel = mipLevel; } } //second derivitive required to subdivide further. public short[] HighDetailThresholds = { 20, //1x1 16, //2x2: at least one derivative about half as tall as a block 10, //4x4 7, //8x8 4, //16x16 2, //32x32 1, //64x64 }; List Chunks; private Dictionary PointToIndex; public List Indices; public List Vertices; //how the simplification works: //we generate a "maximum second derivative" map from the heightmap. //we then create a maximum value image pyramid with the maximum value in the 4 pixels beneath //when generating the mesh, we fall into quadrants below if the second derivative is above a threshold. public void GenerateFullTree() { //how the algorithm works: //build a base structure with quad trees Chunks = new List(); var levels = SecondDerivativePyramid.Length; var chunkSize = 1 << levels; var cw = Size / chunkSize; for (int y = 0; y < Size; y += chunkSize) { for (int x = 0; x < Size; x += chunkSize) { var chunk = new HeightmapChunk(new Rectangle(x, y, chunkSize, chunkSize), levels, (x == 0) ? null : Chunks.Last(), (y == 0) ? null : Chunks[Chunks.Count - cw]); Chunks.Add(chunk); } } var thresholds = HighDetailThresholds; var toTraverse = new Queue(Chunks); while (toTraverse.Count > 0) { var node = toTraverse.Dequeue(); var mipLevel = node.MipLevel; var sd = SecondDerivativePyramid[mipLevel-1]; var mipWidth = Size >> (mipLevel-1); var pos = (node.Range.X >> (mipLevel - 1)) + (node.Range.Y >> (mipLevel - 1)) * mipWidth; //check the max second derivative of the 4 potential derivatives. var threshold = HighDetailThresholds[mipLevel - 1]; if (sd[pos] >= threshold) //top left { var newNode = node.GetOrAdd(0, true); if (mipLevel > 1) toTraverse.Enqueue(newNode); } if (sd[pos+1] >= threshold) //top right { var newNode = node.GetOrAdd(1, true); if (mipLevel > 1) toTraverse.Enqueue(newNode); } if (sd[pos + mipWidth] >= threshold) //bottom left { var newNode = node.GetOrAdd(2, true); if (mipLevel > 1) toTraverse.Enqueue(newNode); } if (sd[pos + mipWidth + 1] >= threshold) //top right { var newNode = node.GetOrAdd(3, true); if (mipLevel > 1) toTraverse.Enqueue(newNode); } } } public void GenerateMesh() { //traverse the chunk tree, generating meshes for each. PointToIndex = new Dictionary(); Vertices = new List(); Indices = new List(); foreach (var chunk in Chunks) { chunk.Triangulate(this); } } public int GetVertex(Point pt) { int index; if (!PointToIndex.TryGetValue(pt, out index)) { index = Vertices.Count; var x = Math.Min(Size1, pt.X); var y = Math.Min(Size1, pt.Y); Vertices.Add(new Vector3(pt.X, Map[x + y * Size] * HeightMultiplier, pt.Y)); PointToIndex[pt] = index; } return index; } public void AddTri(int i1, int i2, int i3) { Indices.Add(i1); Indices.Add(i2); Indices.Add(i3); } } public class HeightmapQuadTreeNode { public bool Reduced; public int MipLevel; public int ParentInd = -1; //the HeightmapQuadTreeNode Parent; public Rectangle Range; public HeightmapQuadTreeNode[] Children = new HeightmapQuadTreeNode[] { null, null, null, null //top left, top right, bottom left, bottom right (row order) }; public HeightmapQuadTreeNode(HeightmapQuadTreeNode parent, Rectangle range) { Parent = parent; Range = range; MipLevel = (parent?.MipLevel ?? 6) - 1; } public HeightmapQuadTreeNode GetOrAdd(int index, bool doSpread) { HeightmapQuadTreeNode result; if (Children[index] == null) { var rect = Range; rect.Width /= 2; rect.Height /= 2; if ((index % 2) == 1) rect.X += rect.Width; if (index > 1) rect.Y += rect.Height; result = new HeightmapQuadTreeNode(this, rect); result.ParentInd = index; Children[index] = result; doSpread = true; } else { result = Children[index]; } if (doSpread) { if (Parent != null) { //find adjacent quad to add to. //for example if we are in index 0 (top left), make sure there is: // - a subdivision in the top right (1) of the tile to our left, // - a subdivision in the bottom left (2) of the tile above us, // - a subdivision in the bottom right (3) of the tile above and left //index 1 (top right switch (index) { case 0: //top left { var left = result.FindOrCreateQuadInDirection(3); var up = result.FindOrCreateQuadInDirection(0); if (up != null) up.FindOrCreateQuadInDirection(3); break; } case 1: //top right { var right = result.FindOrCreateQuadInDirection(1); var up = result.FindOrCreateQuadInDirection(0); if (up != null) up.FindOrCreateQuadInDirection(1); break; } case 2: //bottom left { var left = result.FindOrCreateQuadInDirection(3); var bottom = result.FindOrCreateQuadInDirection(2); if (bottom != null) bottom.FindOrCreateQuadInDirection(3); break; } case 3: //bottom right { var right = result.FindOrCreateQuadInDirection(1); var bottom = result.FindOrCreateQuadInDirection(2); if (bottom != null) bottom.FindOrCreateQuadInDirection(1); break; } } } } return result; } public virtual HeightmapQuadTreeNode FindOrCreateQuadInDirection(int dir) { //dir: up, right, down, left if (Parent == null) return null; switch (dir) { case 0: //up //if we're on the bottom row, finding the quad is easy. if (ParentInd > 1) { return Parent.GetOrAdd(ParentInd - 2, false); } else { //on the top row. we need to break out to add a quad above. var aboveParent = Parent.FindOrCreateQuadInDirection(dir); //our adjacent should be on the above parent's bottom row. return aboveParent?.GetOrAdd(ParentInd + 2, false); } case 1: //right //if we're on the left row, finding the quad is easy. if ((ParentInd % 2) == 0) { return Parent.GetOrAdd(ParentInd + 1, false); } else { //on the right row. we need to break out to add a quad above. var rightParent = Parent.FindOrCreateQuadInDirection(dir); //our adjacent should be on the right parent's left row. return rightParent?.GetOrAdd(ParentInd - 1, false); } case 2: //down //if we're on the top row, finding the quad is easy. if (ParentInd < 2) { return Parent.GetOrAdd(ParentInd + 2, false); } else { //on the right row. we need to break out to add a quad above. var belowParent = Parent.FindOrCreateQuadInDirection(dir); //our adjacent should be on the below parent's top row. return belowParent?.GetOrAdd(ParentInd - 2, false); } case 3: //left //if we're on the right row, finding the quad is easy. if ((ParentInd % 2) == 1) { return Parent.GetOrAdd(ParentInd - 1, false); } else { //on the left row. we need to break out to add a quad above. var leftParent = Parent.FindOrCreateQuadInDirection(dir); //our adjacent should be on the left parent's right row. return leftParent?.GetOrAdd(ParentInd + 1, false); } } return null; } public void Triangulate(SimplifiedHeightmap parent) { var cTriangulated = 0; foreach (var child in Children) { if (child != null) { child.Triangulate(parent); cTriangulated++; } } if (cTriangulated == 0) { //no children means we are a leaf. triangulate, cause nobody else is doing it for me. var lt = parent.GetVertex(Range.Location); var rt = parent.GetVertex(Range.Location + new Point(Range.Width, 0)); var rb = parent.GetVertex(Range.Location + Range.Size); var lb = parent.GetVertex(Range.Location + new Point(0, Range.Height)); parent.AddTri(lt, rt, rb); parent.AddTri(lt, rb, lb); } else if (cTriangulated < 4) { //complex: we have children, but we also need to make our own geometry. var ctr = parent.GetVertex(Range.Location + new Point(Range.Width/2, Range.Height/2)); var lt = parent.GetVertex(Range.Location); var rt = parent.GetVertex(Range.Location + new Point(Range.Width, 0)); var rb = parent.GetVertex(Range.Location + Range.Size); var lb = parent.GetVertex(Range.Location + new Point(0, Range.Height)); if (Children[0] == null) //from top left { if (Children[1] == null) //top right { //triangle lt to rt: \/ parent.AddTri(lt, rt, ctr); } else { //triangle lt to mt: \| var mt = parent.GetVertex(Range.Location + new Point(Range.Width / 2, 0)); parent.AddTri(lt, mt, ctr); } if (Children[2] == null) //bottom left { //triangle lt to lb: \ // / parent.AddTri(lt, ctr, lb); } else { //triangle lt to lm: _\ var lm = parent.GetVertex(Range.Location + new Point(0, Range.Height / 2)); parent.AddTri(lt, ctr, lm); } } else { if (Children[1] == null) //top right but no top left { //triangle mt to rt: |/ var mt = parent.GetVertex(Range.Location + new Point(Range.Width / 2, 0)); parent.AddTri(mt, rt, ctr); } if (Children[2] == null) //bottom left but no top left { //triangle lm to lb: _ // / var lm = parent.GetVertex(Range.Location + new Point(0, Range.Height / 2)); parent.AddTri(lm, ctr, lb); } } if (Children[3] == null) //from bottom right { if (Children[1] == null) //top right { //triangle rt to rb: / // \ parent.AddTri(rt, rb, ctr); } else { //triangle rm to rb: _ // \ var rm = parent.GetVertex(Range.Location + new Point(Range.Width, Range.Height / 2)); parent.AddTri(rm, rb, ctr); } if (Children[2] == null) //bottom left { //triangle lb to rb: /\ parent.AddTri(lb, ctr, rb); } else { //triangle mb to rb: |\ var mb = parent.GetVertex(Range.Location + new Point(Range.Width / 2, Range.Height)); parent.AddTri(mb, ctr, rb); } } else { if (Children[1] == null) //top right, no bottom right { //triangle rt to rm: /_ var rm = parent.GetVertex(Range.Location + new Point(Range.Width, Range.Height / 2)); parent.AddTri(rt, rm, ctr); } if (Children[2] == null) //bottom left, no bottom right { //triangle mb to lb: /| var mb = parent.GetVertex(Range.Location + new Point(Range.Width / 2, Range.Height)); parent.AddTri(mb, lb, ctr); } } } } } public class HeightmapChunk : HeightmapQuadTreeNode { public HeightmapChunk[] Adjacent = new HeightmapChunk[] { null, null, null, null //up, right, down, left }; public HeightmapChunk(Rectangle range, int mipLevel, HeightmapChunk left, HeightmapChunk top) : base(null, range) { Adjacent[3] = left; Adjacent[0] = top; if (left != null) left.Adjacent[1] = this; if (top != null) top.Adjacent[2] = this; MipLevel = mipLevel; } public override HeightmapQuadTreeNode FindOrCreateQuadInDirection(int dir) { return Adjacent[dir]; } } }