sandpypi/Sand.Core/SandSimulation.CellsAndForces.cs

545 lines
18 KiB
C#

namespace Sand.Core;
public sealed partial class SandSimulation
{
private void SetCell(int x, int y, ushort typeId, bool markProcessed = true, bool clearDynamics = false)
{
var wasEmpty = TypeId[x, y] == 0;
if (wasEmpty)
{
ParticleCount++;
}
TypeId[x, y] = typeId;
Temperature[x, y] = _initialTemperature[typeId];
BurnTime[x, y] = _burnDuration[typeId];
Burning[x, y] = _burningInit[typeId];
SparkTime[x, y] = 0;
Lifetime[x, y] = _defaultLifetime[typeId];
_pressureDuration[x, y] = 0f;
_cellAge[x, y] = 0f;
_integrity[x, y] = _durability[typeId];
if (clearDynamics)
{
ClearDynamicFieldsAt(x, y);
}
ExpandOccupiedBoundsToInclude(x, y);
MarkVisualDirty(x, y);
if (markProcessed)
{
MarkProcessed(x, y);
}
}
private void ForceSetCell(int x, int y, ushort typeId)
{
var wasEmpty = TypeId[x, y] == 0;
if (wasEmpty)
{
ParticleCount++;
}
TypeId[x, y] = typeId;
Temperature[x, y] = _initialTemperature[typeId];
BurnTime[x, y] = _burnDuration[typeId];
Burning[x, y] = _burningInit[typeId];
SparkTime[x, y] = 0;
Lifetime[x, y] = _defaultLifetime[typeId];
_pressureDuration[x, y] = 0f;
_cellAge[x, y] = 0f;
_integrity[x, y] = _durability[typeId];
ExpandOccupiedBoundsToInclude(x, y);
MarkVisualDirty(x, y);
}
private void ReplaceCell(int x, int y, ushort typeId)
{
TypeId[x, y] = typeId;
Temperature[x, y] = _initialTemperature[typeId];
BurnTime[x, y] = _burnDuration[typeId];
Burning[x, y] = _burningInit[typeId];
SparkTime[x, y] = 0;
Lifetime[x, y] = _defaultLifetime[typeId];
_pressureDuration[x, y] = 0f;
_cellAge[x, y] = 0f;
_integrity[x, y] = _durability[typeId];
ExpandOccupiedBoundsToInclude(x, y);
MarkVisualDirty(x, y);
MarkProcessed(x, y);
}
private void ResetCell(int x, int y)
{
var typeId = TypeId[x, y];
if (typeId != 0)
{
TrySpawnOnDeath(x, y, typeId);
}
if (TypeId[x, y] != 0)
{
ParticleCount = Math.Max(0, ParticleCount - 1);
}
MarkBoundsDirtyIfRemovingOccupiedCell(x, y);
ResetCellWithoutCountChange(x, y);
MarkVisualDirty(x, y);
}
private void MoveCell(int x, int y, int nx, int ny)
{
TypeId[nx, ny] = TypeId[x, y];
Temperature[nx, ny] = Temperature[x, y];
BurnTime[nx, ny] = BurnTime[x, y];
Burning[nx, ny] = Burning[x, y];
SparkTime[nx, ny] = SparkTime[x, y];
Lifetime[nx, ny] = Lifetime[x, y];
_pressureDuration[nx, ny] = _pressureDuration[x, y];
_cellAge[nx, ny] = _cellAge[x, y];
_integrity[nx, ny] = _integrity[x, y];
MarkBoundsDirtyIfRemovingOccupiedCell(x, y);
ResetCellWithoutCountChange(x, y);
ExpandOccupiedBoundsToInclude(nx, ny);
MarkVisualDirty(x, y);
MarkVisualDirty(nx, ny);
MarkProcessed(nx, ny);
InjectMovementWind(x, y, nx, ny, TypeId[nx, ny]);
InjectMovementPressure(x, y, nx, ny, TypeId[nx, ny]);
}
private void SwapCells(int x, int y, int nx, int ny)
{
(TypeId[x, y], TypeId[nx, ny]) = (TypeId[nx, ny], TypeId[x, y]);
(Temperature[x, y], Temperature[nx, ny]) = (Temperature[nx, ny], Temperature[x, y]);
(BurnTime[x, y], BurnTime[nx, ny]) = (BurnTime[nx, ny], BurnTime[x, y]);
(Burning[x, y], Burning[nx, ny]) = (Burning[nx, ny], Burning[x, y]);
(SparkTime[x, y], SparkTime[nx, ny]) = (SparkTime[nx, ny], SparkTime[x, y]);
(Lifetime[x, y], Lifetime[nx, ny]) = (Lifetime[nx, ny], Lifetime[x, y]);
(_pressureDuration[x, y], _pressureDuration[nx, ny]) = (_pressureDuration[nx, ny], _pressureDuration[x, y]);
(_cellAge[x, y], _cellAge[nx, ny]) = (_cellAge[nx, ny], _cellAge[x, y]);
(_integrity[x, y], _integrity[nx, ny]) = (_integrity[nx, ny], _integrity[x, y]);
ExpandOccupiedBoundsToInclude(x, y);
ExpandOccupiedBoundsToInclude(nx, ny);
MarkVisualDirty(x, y);
MarkVisualDirty(nx, ny);
MarkProcessed(x, y);
MarkProcessed(nx, ny);
}
private void ResetCellWithoutCountChange(int x, int y)
{
TypeId[x, y] = 0;
Temperature[x, y] = _settings.AmbientTemperature;
BurnTime[x, y] = 0f;
Burning[x, y] = 0;
SparkTime[x, y] = 0;
Lifetime[x, y] = 0f;
_pressureDuration[x, y] = 0f;
_cellAge[x, y] = 0f;
_integrity[x, y] = 0f;
MarkVisualDirty(x, y);
}
private void DecayFields(float dt)
{
if (!TryGetSimulationBounds(out var startX, out var startY, out var endX, out var endY))
{
return;
}
var windDecay = MathF.Max(0f, 1f - (dt * 3.5f));
var forceDecay = MathF.Max(0f, 1f - (dt * 1.2f));
var pressureDecay = MathF.Max(0f, 1f - (dt * 4.5f));
_hasActiveFieldBounds = false;
for (var y = startY; y <= endY; y++)
{
for (var x = startX; x <= endX; x++)
{
_windFieldX[x, y] *= windDecay;
_windFieldY[x, y] *= windDecay;
_forceFieldX[x, y] *= forceDecay;
_forceFieldY[x, y] *= forceDecay;
var localPressureDecay = pressureDecay * _pressureDecayMultiplier[Math.Min(TypeId[x, y], _pressureDecayMultiplier.Length - 1)];
_airPressure[x, y] *= localPressureDecay;
if (MathF.Abs(_windFieldX[x, y]) < 0.02f)
{
_windFieldX[x, y] = 0f;
}
if (MathF.Abs(_windFieldY[x, y]) < 0.02f)
{
_windFieldY[x, y] = 0f;
}
if (MathF.Abs(_forceFieldX[x, y]) < 0.02f)
{
_forceFieldX[x, y] = 0f;
}
if (MathF.Abs(_forceFieldY[x, y]) < 0.02f)
{
_forceFieldY[x, y] = 0f;
}
if (MathF.Abs(_airPressure[x, y]) < 0.02f)
{
_airPressure[x, y] = 0f;
}
if (TypeId[x, y] != 0)
{
_cellAge[x, y] += dt * 60f;
var maxIntegrity = _durability[TypeId[x, y]];
if (_integrity[x, y] < maxIntegrity)
{
_integrity[x, y] = MathF.Min(maxIntegrity, _integrity[x, y] + (dt * 60f * MathF.Max(0.02f, _hardness[TypeId[x, y]] * 0.03f)));
}
}
if (HasDynamicFieldAt(x, y))
{
ExpandActiveFieldBoundsToInclude(x, y);
}
}
}
_fieldBoundsDirty = false;
if (AreFieldVisualsEnabled())
{
MarkVisualDirtyRect(startX, startY, endX, endY);
}
}
private void InjectMovementWind(int x, int y, int nx, int ny, ushort typeId)
{
if (typeId == 0)
{
return;
}
var kind = (ParticleKind)_kind[typeId];
if (kind != ParticleKind.Solid && kind != ParticleKind.Liquid)
{
return;
}
var moveX = nx - x;
var moveY = ny - y;
if (moveX == 0 && moveY == 0)
{
return;
}
var strength = kind == ParticleKind.Solid ? 0.55f : 0.35f;
var impulseX = moveX * strength;
var impulseY = moveY * strength * 0.5f;
AddWindImpulse(nx, ny, impulseX * 0.5f, impulseY);
AddWindImpulse(nx - Math.Sign(moveX == 0 ? 1 : moveX), ny, impulseX * 0.25f, impulseY * 0.35f);
AddWindImpulse(nx + Math.Sign(moveX == 0 ? 1 : moveX), ny, impulseX * 0.25f, impulseY * 0.35f);
AddWindImpulse(nx, ny - 1, impulseX * 0.15f, impulseY * 0.2f);
}
private void AddWindImpulse(int x, int y, float impulseX, float impulseY)
{
if (!TryNormalizeCoordinate(ref x, ref y))
{
return;
}
if (_settings.OuterWall && IsBoundary(x, y))
{
return;
}
var hadDynamicBefore = HasDynamicFieldAt(x, y);
_windFieldX[x, y] = Math.Clamp(_windFieldX[x, y] + impulseX, -8f, 8f);
_windFieldY[x, y] = Math.Clamp(_windFieldY[x, y] + impulseY, -8f, 8f);
TrackDynamicFieldMutation(x, y, hadDynamicBefore);
}
private void InjectMovementPressure(int x, int y, int nx, int ny, ushort typeId)
{
if (typeId == 0)
{
return;
}
var kind = (ParticleKind)_kind[typeId];
if (kind != ParticleKind.Solid && kind != ParticleKind.Liquid)
{
return;
}
var moveY = ny - y;
if (moveY <= 0)
{
return;
}
var sourceAge = _cellAge[nx, ny];
var impulse = ComputeMovementPressureImpulse(typeId, (ParticleKind)_kind[typeId], moveY, sourceAge);
if (impulse <= 0f)
{
return;
}
AddPressureImpulse(nx, ny, impulse);
AddPressureImpulse(nx - 1, ny, impulse * 0.3f);
AddPressureImpulse(nx + 1, ny, impulse * 0.3f);
}
private void AddPressureImpulse(int x, int y, float amount)
{
if (!TryNormalizeCoordinate(ref x, ref y))
{
return;
}
if (_settings.OuterWall && IsBoundary(x, y))
{
return;
}
var hadDynamicBefore = HasDynamicFieldAt(x, y);
_airPressure[x, y] = Math.Clamp(_airPressure[x, y] + amount, -10f, 10f);
TrackDynamicFieldMutation(x, y, hadDynamicBefore);
}
private void TriggerExplosion(int x, int y, ushort typeId, ref uint seed)
{
var marker = _activeStepToken != 0 ? _activeStepToken : Frame + 1;
if (!InBounds(x, y))
{
return;
}
if (_explosionFrame[x, y] == marker)
{
return;
}
var pending = new Stack<(int X, int Y, ushort TypeId)>();
_explosionFrame[x, y] = marker;
pending.Push((x, y, typeId));
while (pending.Count > 0)
{
var current = pending.Pop();
var cx = current.X;
var cy = current.Y;
var currentTypeId = current.TypeId;
var radius = Math.Max(1, (int)_explosionRadius[currentTypeId]);
var force = MathF.Max(2f, _explosionForce[currentTypeId]);
for (var dx = -radius; dx <= radius; dx++)
{
for (var dy = -radius; dy <= radius; dy++)
{
var distanceSq = (dx * dx) + (dy * dy);
if (distanceSq > radius * radius)
{
continue;
}
var tx = cx + dx;
var ty = cy + dy;
if (!TryNormalizeCoordinate(ref tx, ref ty))
{
continue;
}
if (_settings.OuterWall && IsBoundary(tx, ty))
{
continue;
}
var distance = MathF.Max(1f, MathF.Sqrt(distanceSq));
var falloff = 1f - (distance / radius);
var hadDynamicBefore = HasDynamicFieldAt(tx, ty);
_airPressure[tx, ty] = Math.Clamp(_airPressure[tx, ty] + (force * falloff * 2.5f), -10f, 10f);
Temperature[tx, ty] += force * falloff * 25f;
if (distanceSq > 0)
{
_forceFieldX[tx, ty] = Math.Clamp(_forceFieldX[tx, ty] + (dx / distance) * force * falloff, -8f, 8f);
_forceFieldY[tx, ty] = Math.Clamp(_forceFieldY[tx, ty] + (dy / distance) * force * falloff, -8f, 8f);
}
TrackDynamicFieldMutation(tx, ty, hadDynamicBefore);
var neighborId = TypeId[tx, ty];
if (neighborId == 0)
{
if (_idSmoke != 0 && NextChance(ref seed, 0.08f))
{
SetCell(tx, ty, _idSmoke);
}
else if (_idFire != 0 && NextChance(ref seed, 0.05f))
{
SetCell(tx, ty, _idFire);
}
continue;
}
if (neighborId == _idWall || neighborId == _idGlass)
{
continue;
}
if (_explosive[neighborId] != 0 && NextChance(ref seed, 0.25f))
{
if (_explosionFrame[tx, ty] != marker)
{
_explosionFrame[tx, ty] = marker;
pending.Push((tx, ty, neighborId));
}
ResetCell(tx, ty);
continue;
}
if (_flamability[neighborId] > 0f && _idFire != 0 && NextChance(ref seed, 0.12f))
{
ReplaceCell(tx, ty, _idFire);
continue;
}
if (_mass[neighborId] < 0.5f && NextChance(ref seed, 0.18f + falloff * 0.22f))
{
var brokenType = _brokenTarget[neighborId];
if (brokenType != 0 && brokenType != neighborId)
{
ReplaceCell(tx, ty, brokenType);
}
else
{
ResetCell(tx, ty);
}
}
}
}
}
}
private void ApplyRadialForceBrushAtPixel(int centerX, int centerY, int brushRadius, float strength, bool inward, ToolProfile profile)
{
var gridCenterX = centerX / ParticleSize;
var gridCenterY = centerY / ParticleSize;
var effectiveRadius = Math.Max(brushRadius, profile.RadiusCells);
for (var dx = -effectiveRadius; dx <= effectiveRadius; dx++)
{
for (var dy = -effectiveRadius; dy <= effectiveRadius; dy++)
{
var radiusSq = (dx * dx) + (dy * dy);
if (radiusSq == 0 || radiusSq > effectiveRadius * effectiveRadius)
{
continue;
}
var x = gridCenterX + dx;
var y = gridCenterY + dy;
if (!TryNormalizeCoordinate(ref x, ref y))
{
continue;
}
if (_settings.OuterWall && IsBoundary(x, y))
{
continue;
}
var distance = MathF.Sqrt(radiusSq);
if (!ToolAffectsCell(profile.Affects, x, y))
{
continue;
}
var falloff = ComputeFalloff(distance, effectiveRadius, profile.Falloff);
var dirX = dx / distance;
var dirY = dy / distance;
if (inward)
{
dirX = -dirX;
dirY = -dirY;
}
var turbulence = SampleTurbulence(x, y, profile.Turbulence);
var hadDynamicBefore = HasDynamicFieldAt(x, y);
_forceFieldX[x, y] = Math.Clamp(_forceFieldX[x, y] + (dirX + turbulence.X) * falloff * strength * profile.Strength, -8f, 8f);
_forceFieldY[x, y] = Math.Clamp(_forceFieldY[x, y] + (dirY + turbulence.Y) * falloff * strength * profile.Strength, -8f, 8f);
TrackDynamicFieldMutation(x, y, hadDynamicBefore);
}
}
}
private float ComputeMovementPressureImpulse(ushort typeId, ParticleKind kind, int moveY, float cellAge)
{
if (moveY <= 0 || cellAge < 4f)
{
return 0f;
}
var impact = MathF.Max(0f, _mass[typeId] - 1.1f) * 0.08f;
impact *= MathF.Max(0.45f, _velocity[typeId] + 0.4f);
if (kind == ParticleKind.Liquid)
{
impact *= 0.3f;
}
else if (kind == ParticleKind.Solid)
{
impact *= _isStatic[typeId] != 0 ? 0f : 0.55f;
}
if (_isMolten[typeId] != 0)
{
impact *= 0.45f;
}
return Math.Clamp(impact, 0f, 0.4f);
}
private float GetNetForceX(int x, int y, ushort typeId)
{
var pressureGradient = SamplePressure(x - 1, y) - SamplePressure(x + 1, y);
var pressureInfluence = GetPressureForceInfluence(typeId, x, y);
return _settings.WindX + _windFieldX[x, y] + _forceFieldX[x, y] + (pressureGradient * pressureInfluence);
}
private float GetNetForceY(int x, int y, ushort typeId)
{
var pressureGradient = SamplePressure(x, y + 1) - SamplePressure(x, y - 1);
var pressureInfluence = GetPressureForceInfluence(typeId, x, y) * 0.85f;
return _settings.WindY + _windFieldY[x, y] + _forceFieldY[x, y] + (pressureGradient * pressureInfluence);
}
private float GetPressureForceInfluence(ushort typeId, int x, int y)
{
if (_cellAge[x, y] < 4f)
{
return 0f;
}
return ((ParticleBehaviorKind)_behaviorKind[typeId], (ParticleKind)_kind[typeId]) switch
{
(ParticleBehaviorKind.Fire, _) => 0.09f,
(ParticleBehaviorKind.Plasma, _) => 0.1f,
(_, ParticleKind.Gas) => 0.08f,
(_, ParticleKind.Liquid) => 0.025f,
(_, ParticleKind.Solid) => _isStatic[typeId] != 0 ? 0f : 0.015f,
_ => 0.03f,
};
}
private float SamplePressure(int x, int y)
{
if (!TryNormalizeCoordinate(ref x, ref y))
{
return 0f;
}
return _airPressure[x, y];
}
}