mirror of
https://github.com/simtactics/mysimulation.git
synced 2025-03-21 17:11:30 +00:00
- NioTSO client isn't needed because we're using RayLib - Added FreeSO's API server to handle most backend operations
493 lines
19 KiB
C#
Executable file
493 lines
19 KiB
C#
Executable file
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<HeightmapChunk> Chunks;
|
|
|
|
private Dictionary<Point, int> PointToIndex;
|
|
public List<int> Indices;
|
|
public List<Vector3> 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<HeightmapChunk>();
|
|
|
|
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<HeightmapQuadTreeNode>(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<Point, int>();
|
|
Vertices = new List<Vector3>();
|
|
Indices = new List<int>();
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|