529 lines
18 KiB
C#
529 lines
18 KiB
C#
using Sand.Core;
|
|
using System.Diagnostics;
|
|
|
|
namespace Sand.ChunkPrototype;
|
|
|
|
public sealed partial class PrototypeSparseSandAdapter
|
|
{
|
|
public void ClearFields()
|
|
{
|
|
_fieldPages.Clear();
|
|
foreach (var page in _cellPages.Values)
|
|
{
|
|
page.HasFieldActivity = false;
|
|
}
|
|
|
|
_fullFrameDirty = true;
|
|
_dirtyVisualChunks.Clear();
|
|
foreach (var coord in _cellPages.Keys)
|
|
{
|
|
_dirtyVisualChunks.Add(coord);
|
|
}
|
|
}
|
|
|
|
public (float X, float Y) GetWindAtCell(int x, int y)
|
|
{
|
|
if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return (0f, 0f);
|
|
}
|
|
|
|
var cell = page.GetCell(localX, localY);
|
|
return (cell.WindX, cell.WindY);
|
|
}
|
|
|
|
public (float X, float Y) GetForceAtCell(int x, int y)
|
|
{
|
|
if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return (0f, 0f);
|
|
}
|
|
|
|
var cell = page.GetCell(localX, localY);
|
|
return (cell.ForceX, cell.ForceY);
|
|
}
|
|
|
|
public float GetAirPressureAtCell(int x, int y)
|
|
{
|
|
if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return page.GetCell(localX, localY).Pressure;
|
|
}
|
|
|
|
public ReadOnlySpan<byte> BuildRgbaFrame(bool enableWindVisuals = false, bool enablePressureVisuals = false)
|
|
{
|
|
var renderStart = Stopwatch.GetTimestamp();
|
|
long bytesTouched;
|
|
if (_fullFrameDirty)
|
|
{
|
|
Array.Clear(_rgbaBuffer);
|
|
for (var i = 3; i < _rgbaBuffer.Length; i += 4)
|
|
{
|
|
_rgbaBuffer[i] = 255;
|
|
}
|
|
|
|
bytesTouched = _rgbaBuffer.Length;
|
|
var rendered = new HashSet<ChunkCoord>();
|
|
foreach (var coord in _cellPages.Keys)
|
|
{
|
|
RenderChunk(coord, enableWindVisuals, enablePressureVisuals);
|
|
rendered.Add(coord);
|
|
}
|
|
|
|
foreach (var coord in _fieldPages.Keys)
|
|
{
|
|
if (rendered.Add(coord))
|
|
{
|
|
RenderChunk(coord, enableWindVisuals, enablePressureVisuals);
|
|
}
|
|
}
|
|
|
|
_dirtyVisualChunks.Clear();
|
|
_fullFrameDirty = false;
|
|
}
|
|
else
|
|
{
|
|
bytesTouched = 0;
|
|
foreach (var coord in _dirtyVisualChunks)
|
|
{
|
|
bytesTouched += ClearChunkVisual(coord);
|
|
RenderChunk(coord, enableWindVisuals, enablePressureVisuals);
|
|
}
|
|
|
|
_dirtyVisualChunks.Clear();
|
|
}
|
|
|
|
var renderMicros = ToMicroseconds(renderStart, Stopwatch.GetTimestamp());
|
|
_lastStepStats = _lastStepStats with
|
|
{
|
|
FrameBuildBytesTouched = bytesTouched,
|
|
RenderTimeMicroseconds = renderMicros,
|
|
};
|
|
return _rgbaBuffer;
|
|
}
|
|
|
|
public ReadOnlySpan<byte> BuildRgbFrame()
|
|
{
|
|
var rgba = BuildRgbaFrame();
|
|
for (int sourceIndex = 0, targetIndex = 0; sourceIndex < rgba.Length; sourceIndex += 4, targetIndex += 3)
|
|
{
|
|
_rgbBuffer[targetIndex] = rgba[sourceIndex];
|
|
_rgbBuffer[targetIndex + 1] = rgba[sourceIndex + 1];
|
|
_rgbBuffer[targetIndex + 2] = rgba[sourceIndex + 2];
|
|
}
|
|
|
|
return _rgbBuffer;
|
|
}
|
|
|
|
public int TrimResidency(int marginChunks = 1)
|
|
{
|
|
var unloaded = World.UnloadEmptyChunks();
|
|
unloaded += World.UnloadInactiveChunks(marginChunks);
|
|
|
|
foreach (var coord in _cellPages.Keys.ToArray())
|
|
{
|
|
if (_cellPages[coord].OccupancyCount == 0 && !_fieldPages.ContainsKey(coord))
|
|
{
|
|
_cellPages.Remove(coord);
|
|
}
|
|
}
|
|
|
|
foreach (var coord in _fieldPages.Keys.ToArray())
|
|
{
|
|
if (_fieldPages[coord].IsEmpty())
|
|
{
|
|
_fieldPages.Remove(coord);
|
|
}
|
|
}
|
|
|
|
return unloaded;
|
|
}
|
|
|
|
public void ApplyWindBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY) =>
|
|
ApplyDirectionalField(centerX, centerY, brushRadius, forceX, forceY, writeWind: true, writePressure: false);
|
|
|
|
public void ApplyAirBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY) =>
|
|
ApplyDirectionalField(centerX, centerY, brushRadius, forceX, forceY, writeWind: true, writePressure: true);
|
|
|
|
public void ApplyGravityBrush(int centerX, int centerY, int brushRadius, float strength) =>
|
|
ApplyRadialField(centerX, centerY, brushRadius, MathF.Abs(strength), inward: true);
|
|
|
|
public void ApplyRepulsorBrush(int centerX, int centerY, int brushRadius, float strength) =>
|
|
ApplyRadialField(centerX, centerY, brushRadius, MathF.Abs(strength), inward: false);
|
|
|
|
private void ApplyDirectionalField(int centerX, int centerY, int brushRadius, float forceX, float forceY, bool writeWind, bool writePressure)
|
|
{
|
|
var radiusSquared = brushRadius * brushRadius;
|
|
for (var offsetY = -brushRadius; offsetY <= brushRadius; offsetY++)
|
|
{
|
|
for (var offsetX = -brushRadius; offsetX <= brushRadius; offsetX++)
|
|
{
|
|
var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY);
|
|
if (distanceSquared > radiusSquared)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var x = centerX + offsetX;
|
|
var y = centerY + offsetY;
|
|
if (!InBounds(x, y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = MathF.Sqrt(distanceSquared);
|
|
var falloff = brushRadius <= 0 ? 1f : MathF.Max(0.1f, 1f - (distance / Math.Max(1f, brushRadius)));
|
|
var coord = GetChunkCoord(x, y);
|
|
var (localX, localY) = GetLocalCoord(x, y);
|
|
var fieldPage = GetOrCreateFieldPage(coord);
|
|
var cell = fieldPage.GetCell(localX, localY);
|
|
fieldPage.SetCell(localX, localY, new FieldCellData(
|
|
writeWind ? cell.WindX + (forceX * falloff) : cell.WindX,
|
|
writeWind ? cell.WindY + (forceY * falloff) : cell.WindY,
|
|
writeWind ? cell.ForceX : cell.ForceX + (forceX * 0.25f * falloff),
|
|
writeWind ? cell.ForceY : cell.ForceY + (forceY * 0.25f * falloff),
|
|
writePressure ? cell.Pressure + (MathF.Sqrt((forceX * forceX) + (forceY * forceY)) * falloff) : cell.Pressure));
|
|
fieldPage.LastDecayedFrame = _stepCounter;
|
|
WakeChunk(coord);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyRadialField(int centerX, int centerY, int brushRadius, float strength, bool inward)
|
|
{
|
|
var radiusSquared = brushRadius * brushRadius;
|
|
for (var offsetY = -brushRadius; offsetY <= brushRadius; offsetY++)
|
|
{
|
|
for (var offsetX = -brushRadius; offsetX <= brushRadius; offsetX++)
|
|
{
|
|
var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY);
|
|
if (distanceSquared > radiusSquared)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var x = centerX + offsetX;
|
|
var y = centerY + offsetY;
|
|
if (!InBounds(x, y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = MathF.Sqrt(Math.Max(1f, distanceSquared));
|
|
var scale = MathF.Max(0.1f, 1f - (distance / Math.Max(1f, brushRadius)));
|
|
var directionX = offsetX / distance;
|
|
var directionY = offsetY / distance;
|
|
if (inward)
|
|
{
|
|
directionX = -directionX;
|
|
directionY = -directionY;
|
|
}
|
|
|
|
var coord = GetChunkCoord(x, y);
|
|
var (localX, localY) = GetLocalCoord(x, y);
|
|
var fieldPage = GetOrCreateFieldPage(coord);
|
|
var cell = fieldPage.GetCell(localX, localY);
|
|
fieldPage.SetCell(localX, localY, new FieldCellData(
|
|
cell.WindX,
|
|
cell.WindY,
|
|
cell.ForceX + (directionX * strength * scale),
|
|
cell.ForceY + (directionY * strength * scale),
|
|
cell.Pressure + ((inward ? 1f : -1f) * strength * 0.2f * scale)));
|
|
fieldPage.LastDecayedFrame = _stepCounter;
|
|
WakeChunk(coord);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DecayFields()
|
|
{
|
|
foreach (var coord in _fieldPages.Keys.ToArray())
|
|
{
|
|
var fieldPage = _fieldPages[coord];
|
|
var changed = false;
|
|
foreach (var (localX, localY, cell) in fieldPage.EnumerateActiveCells().ToArray())
|
|
{
|
|
var decayed = new FieldCellData(
|
|
cell.WindX * FieldDecayFactor,
|
|
cell.WindY * FieldDecayFactor,
|
|
cell.ForceX * FieldDecayFactor,
|
|
cell.ForceY * FieldDecayFactor,
|
|
cell.Pressure * FieldDecayFactor);
|
|
fieldPage.SetCell(localX, localY, decayed);
|
|
changed = true;
|
|
}
|
|
|
|
if (fieldPage.IsEmpty())
|
|
{
|
|
_fieldPages.Remove(coord);
|
|
if (_cellPages.TryGetValue(coord, out var cellPage))
|
|
{
|
|
cellPage.HasFieldActivity = false;
|
|
}
|
|
|
|
_dirtyVisualChunks.Add(coord);
|
|
continue;
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
fieldPage.LastDecayedFrame = _stepCounter;
|
|
_dirtyVisualChunks.Add(coord);
|
|
if (_cellPages.TryGetValue(coord, out var cellPage))
|
|
{
|
|
cellPage.HasFieldActivity = true;
|
|
cellPage.IsActive = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MarkChunkActive(ChunkCoord coord, ChunkCellPage page)
|
|
{
|
|
page.IsActive = true;
|
|
page.LastTouchedFrame = _stepCounter;
|
|
_dirtyVisualChunks.Add(coord);
|
|
}
|
|
|
|
private void WakeNeighborsForBorderTouch(ChunkCoord coord, int localX, int localY)
|
|
{
|
|
if (localX == 0)
|
|
{
|
|
WakeChunk(new ChunkCoord(coord.X - 1, coord.Y), _config.ChunkWidth - 1, localY);
|
|
}
|
|
|
|
if (localX == _config.ChunkWidth - 1)
|
|
{
|
|
WakeChunk(new ChunkCoord(coord.X + 1, coord.Y), 0, localY);
|
|
}
|
|
|
|
if (localY == 0)
|
|
{
|
|
WakeChunk(new ChunkCoord(coord.X, coord.Y - 1), localX, _config.ChunkHeight - 1);
|
|
}
|
|
|
|
if (localY == _config.ChunkHeight - 1)
|
|
{
|
|
WakeChunk(new ChunkCoord(coord.X, coord.Y + 1), localX, 0);
|
|
}
|
|
}
|
|
|
|
private void WakeChunk(ChunkCoord coord, int localX, int localY)
|
|
{
|
|
if (_cellPages.TryGetValue(coord, out var page))
|
|
{
|
|
page.IsActive = true;
|
|
page.LastTouchedFrame = _stepCounter;
|
|
page.RequestFutureSteps(1);
|
|
page.MarkDirtyBand(localX, localY);
|
|
}
|
|
|
|
_dirtyVisualChunks.Add(coord);
|
|
}
|
|
|
|
private void WakeChunk(ChunkCoord coord)
|
|
{
|
|
if (_cellPages.TryGetValue(coord, out var page))
|
|
{
|
|
page.IsActive = true;
|
|
page.HasFieldActivity = true;
|
|
page.LastTouchedFrame = _stepCounter;
|
|
page.RequestFutureSteps(1);
|
|
}
|
|
|
|
_dirtyVisualChunks.Add(coord);
|
|
}
|
|
|
|
private void RemoveEmptyCellPageIfUnused(ChunkCoord coord, ChunkCellPage page)
|
|
{
|
|
if (page.OccupancyCount == 0 && !_fieldPages.ContainsKey(coord))
|
|
{
|
|
_cellPages.Remove(coord);
|
|
}
|
|
}
|
|
|
|
private long ClearChunkVisual(ChunkCoord coord)
|
|
{
|
|
long bytesTouched = 0;
|
|
var startX = Math.Max(0, (coord.X * _config.ChunkWidth) - RenderHaloCells);
|
|
var startY = Math.Max(0, (coord.Y * _config.ChunkHeight) - RenderHaloCells);
|
|
var endX = Math.Min(Width - 1, ((coord.X + 1) * _config.ChunkWidth) + RenderHaloCells - 1);
|
|
var endY = Math.Min(Height - 1, ((coord.Y + 1) * _config.ChunkHeight) + RenderHaloCells - 1);
|
|
for (var worldY = startY; worldY <= endY; worldY++)
|
|
{
|
|
for (var worldX = startX; worldX <= endX; worldX++)
|
|
{
|
|
var rgbaIndex = ((worldY * Width) + worldX) * 4;
|
|
_rgbaBuffer[rgbaIndex] = 0;
|
|
_rgbaBuffer[rgbaIndex + 1] = 0;
|
|
_rgbaBuffer[rgbaIndex + 2] = 0;
|
|
_rgbaBuffer[rgbaIndex + 3] = 255;
|
|
bytesTouched += 4;
|
|
}
|
|
}
|
|
|
|
return bytesTouched;
|
|
}
|
|
|
|
private void RenderChunk(ChunkCoord coord, bool enableWindVisuals, bool enablePressureVisuals)
|
|
{
|
|
var startX = Math.Max(0, (coord.X * _config.ChunkWidth) - RenderHaloCells);
|
|
var startY = Math.Max(0, (coord.Y * _config.ChunkHeight) - RenderHaloCells);
|
|
var endX = Math.Min(Width - 1, ((coord.X + 1) * _config.ChunkWidth) + RenderHaloCells - 1);
|
|
var endY = Math.Min(Height - 1, ((coord.Y + 1) * _config.ChunkHeight) + RenderHaloCells - 1);
|
|
for (var worldY = startY; worldY <= endY; worldY++)
|
|
{
|
|
for (var worldX = startX; worldX <= endX; worldX++)
|
|
{
|
|
var rgbaIndex = ((worldY * Width) + worldX) * 4;
|
|
var particle = TryGetParticle(worldX, worldY, out var currentParticle) ? currentParticle : default;
|
|
if (particle.TypeId != 0)
|
|
{
|
|
_rgbaBuffer[rgbaIndex] = particle.R;
|
|
_rgbaBuffer[rgbaIndex + 1] = particle.G;
|
|
_rgbaBuffer[rgbaIndex + 2] = particle.B;
|
|
_rgbaBuffer[rgbaIndex + 3] = 255;
|
|
continue;
|
|
}
|
|
|
|
if (TryResolveGasSeamRenderColor(worldX, worldY, out var gasColor))
|
|
{
|
|
_rgbaBuffer[rgbaIndex] = gasColor.R;
|
|
_rgbaBuffer[rgbaIndex + 1] = gasColor.G;
|
|
_rgbaBuffer[rgbaIndex + 2] = gasColor.B;
|
|
_rgbaBuffer[rgbaIndex + 3] = 255;
|
|
continue;
|
|
}
|
|
|
|
var color = ResolveFieldColorAtWorld(worldX, worldY, enableWindVisuals, enablePressureVisuals);
|
|
_rgbaBuffer[rgbaIndex] = color.R;
|
|
_rgbaBuffer[rgbaIndex + 1] = color.G;
|
|
_rgbaBuffer[rgbaIndex + 2] = color.B;
|
|
_rgbaBuffer[rgbaIndex + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
|
|
private (byte R, byte G, byte B) ResolveFieldColorAtWorld(int x, int y, bool enableWindVisuals, bool enablePressureVisuals)
|
|
{
|
|
if (!TryGetFieldPage(x, y, out _, out var fieldPage, out var localX, out var localY))
|
|
{
|
|
return (0, 0, 0);
|
|
}
|
|
|
|
return ResolveFieldColor(fieldPage, localX, localY, enableWindVisuals, enablePressureVisuals);
|
|
}
|
|
|
|
private bool TryResolveGasSeamRenderColor(int x, int y, out (byte R, byte G, byte B) color)
|
|
{
|
|
color = default;
|
|
if (!IsNearChunkRowSeam(y))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var gasNeighborCount = 0;
|
|
var gasAboveCount = 0;
|
|
var gasBelowCount = 0;
|
|
var totalR = 0;
|
|
var totalG = 0;
|
|
var totalB = 0;
|
|
for (var offsetY = -1; offsetY <= 1; offsetY++)
|
|
{
|
|
for (var offsetX = -1; offsetX <= 1; offsetX++)
|
|
{
|
|
if (offsetX == 0 && offsetY == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var neighborX = x + offsetX;
|
|
var neighborY = y + offsetY;
|
|
if (!TryGetParticle(neighborX, neighborY, out var particle) || particle.MotionType != PrototypeParticleType.Steam)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
gasNeighborCount++;
|
|
if (offsetY < 0)
|
|
{
|
|
gasAboveCount++;
|
|
}
|
|
else if (offsetY > 0)
|
|
{
|
|
gasBelowCount++;
|
|
}
|
|
|
|
totalR += particle.R;
|
|
totalG += particle.G;
|
|
totalB += particle.B;
|
|
}
|
|
}
|
|
|
|
if (gasNeighborCount < 3 || gasAboveCount == 0 || gasBelowCount == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
color = ((byte)(totalR / gasNeighborCount), (byte)(totalG / gasNeighborCount), (byte)(totalB / gasNeighborCount));
|
|
return true;
|
|
}
|
|
|
|
private bool IsNearChunkRowSeam(int y)
|
|
{
|
|
var localY = y % _config.ChunkHeight;
|
|
return localY == 0 || localY == _config.ChunkHeight - 1;
|
|
}
|
|
|
|
private static (byte R, byte G, byte B) ResolveFieldColor(ChunkFieldPage? fieldPage, int localX, int localY, bool enableWindVisuals, bool enablePressureVisuals)
|
|
{
|
|
if (fieldPage is null || !fieldPage.TryGetCell(localX, localY, out var cell))
|
|
{
|
|
return (0, 0, 0);
|
|
}
|
|
|
|
byte r = 0;
|
|
byte g = 0;
|
|
byte b = 0;
|
|
if (enablePressureVisuals)
|
|
{
|
|
var pressure = MathF.Abs(cell.Pressure);
|
|
if (pressure > 0.08f)
|
|
{
|
|
var tint = (byte)Math.Clamp((int)(pressure * 32f), 10, 140);
|
|
if (cell.Pressure >= 0f)
|
|
{
|
|
r = (byte)Math.Max((int)r, 26 + tint);
|
|
g = (byte)Math.Max((int)g, 18 + (tint / 3));
|
|
b = (byte)Math.Max((int)b, 24);
|
|
}
|
|
else
|
|
{
|
|
r = (byte)Math.Max((int)r, 18);
|
|
g = (byte)Math.Max((int)g, 24 + (tint / 4));
|
|
b = (byte)Math.Max((int)b, 30 + tint);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (enableWindVisuals)
|
|
{
|
|
var totalFieldX = cell.WindX + cell.ForceX;
|
|
var totalFieldY = cell.WindY + cell.ForceY;
|
|
var windStrength = MathF.Sqrt((totalFieldX * totalFieldX) + (totalFieldY * totalFieldY));
|
|
if (windStrength > 0.08f)
|
|
{
|
|
var tint = (byte)Math.Clamp((int)(windStrength * 40f), 10, 120);
|
|
r = (byte)Math.Max((int)r, 16 + (tint / 3));
|
|
g = (byte)Math.Max((int)g, 24 + (tint / 2));
|
|
b = (byte)Math.Max((int)b, 32 + tint);
|
|
}
|
|
}
|
|
|
|
return (r, g, b);
|
|
}
|
|
}
|