sandpypi/Sand.ChunkPrototype/PrototypeSparseSandAdapter.FieldsAndRendering.cs

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);
}
}