545 lines
18 KiB
C#
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];
|
|
}
|
|
}
|