Files
DTrierFlood_New/Assets/FloodSWE/Scripts/SweTileSimulator.cs

1535 lines
48 KiB
C#

using System;
using FloodSWE.Preprocess;
using FloodSWE.Streaming;
using FloodSWE.TileGraph;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
public sealed class SweTileSimulator : MonoBehaviour
{
[Header("Resources")]
public ComputeShader ghostExchangeShader;
public ComputeShader fluxShader;
[Header("Grid")]
public int gridRes = 256;
public float tileSizeMeters = 1000f;
[Header("Initial Conditions")]
public bool damBreak = true;
public float initialDepthLeft = 1.0f;
public float initialDepthRight = 0.0f;
public Vector2 initialVelocity = Vector2.zero;
[Header("Rain")]
public float rainRateMmPerHr = 0.0f;
[Tooltip("Additional depth-rate (m/s) injected after rain. Positive adds water, negative drains water.")]
public float externalDepthRateMps = 0.0f;
[Header("Simulation")]
public float cfl = 0.5f;
public float tickSeconds = 0.5f;
public bool logCfl = true;
public bool useFullPrecisionTextures = true;
[Header("Static Data")]
public Texture2D terrainHeight;
public Texture2D porosity;
public Texture2D sourceIdMask;
public Texture2D sinkIdMask;
public float flatTerrainHeightMeters = 0.0f;
public float defaultPorosity = 1.0f;
public bool resampleTerrainToGrid = true;
public bool resamplePorosityToGrid = true;
public bool resampleBoundaryMasksToGrid = true;
[Header("Streaming (Debug)")]
public bool emitHeightmapPackets = false;
public int packetEveryNTicks = 1;
public TileId packetTileId = new TileId(1, 0, 0);
public HeightmapPacket lastHeightmapPacket;
public event Action<HeightmapPacket> HeightmapPacketReady;
[Header("Debug Outputs")]
public RenderTexture debugWater;
public RenderTexture debugVelocity;
[Header("Debug - Conservation")]
public bool logConservation = false;
public bool debugClampStats = true;
public bool applyMassCorrection = false;
[Range(0.5f, 2.0f)]
public float massCorrectionMinScale = 0.8f;
[Range(0.5f, 2.0f)]
public float massCorrectionMaxScale = 1.2f;
private const float Gravity = 9.81f;
private RenderTexture waterA;
private RenderTexture waterB;
private RenderTexture velA;
private RenderTexture velB;
private RenderTexture waterGhost;
private RenderTexture velGhost;
private Texture2D resolvedTerrain;
private Texture2D resolvedPorosity;
private Texture2D resolvedSourceIdMask;
private Texture2D resolvedSinkIdMask;
private Texture2D externalDepthRateMap;
private float[] externalDepthRateMapData;
private bool useExternalDepthRateMap;
private Texture2D ghostOverrideMask;
private Texture2D ghostOverrideWater;
private Texture2D ghostOverrideVelocity;
private float[] ghostOverrideMaskData;
private float[] ghostOverrideWaterData;
private Vector2[] ghostOverrideVelocityData;
private bool useGhostBoundaryOverrides;
private Texture2D ghostFreeOutflowMask;
private float[] ghostFreeOutflowMaskData;
private bool useGhostFreeOutflowMask;
private RenderTexture clampMask;
private RenderTexture dummyRw;
private int ghostKernel;
private int fluxKernel;
private int initKernel;
private float dx;
private float accumulatedTime;
private float lastMaxDepth;
private float lastMaxSpeed;
private float lastTotalDepth;
private float lastTotalVolume;
private int lastClampedCells;
private float lastClampedRatio;
private int lastNanCells;
private float lastDtMax;
private float lastDt;
private int lastSubsteps;
private bool useA = true;
private bool isInitialized;
private int tickIndex;
private float pendingMassScale = 1.0f;
public float LastMaxDepth
{
get { return lastMaxDepth; }
}
public float LastMaxSpeed
{
get { return lastMaxSpeed; }
}
public float LastTotalDepth
{
get { return lastTotalDepth; }
}
public float LastTotalVolume
{
get { return lastTotalVolume; }
}
public int LastClampedCells
{
get { return lastClampedCells; }
}
public float LastClampedRatio
{
get { return lastClampedRatio; }
}
public int LastNanCells
{
get { return lastNanCells; }
}
public float LastDtMax
{
get { return lastDtMax; }
}
public float LastDt
{
get { return lastDt; }
}
public int LastSubsteps
{
get { return lastSubsteps; }
}
public float CellSizeMeters
{
get { return dx; }
}
public void SetExternalDepthRate(float metersPerSecond)
{
externalDepthRateMps = metersPerSecond;
}
public void ApplyTileStaticData(
Texture2D terrain,
Texture2D porosityTexture,
Texture2D sourceMask,
Texture2D sinkMaskTexture,
int resolution,
float tileSizeM)
{
if (terrain != null)
{
terrainHeight = terrain;
}
if (porosityTexture != null)
{
porosity = porosityTexture;
}
sourceIdMask = sourceMask;
sinkIdMask = sinkMaskTexture;
int targetResolution = Mathf.Max(4, resolution);
float targetTileSize = Mathf.Max(1.0f, tileSizeM);
bool requiresReinit = targetResolution != gridRes || Mathf.Abs(targetTileSize - tileSizeMeters) > 0.001f;
gridRes = targetResolution;
tileSizeMeters = targetTileSize;
if (!isInitialized)
{
return;
}
if (requiresReinit)
{
Release();
Initialize();
return;
}
ResolveStaticTextures();
ResetSimulationState();
}
public bool SetExternalDepthRateMap(float[] metersPerSecondPerCell)
{
if (!isInitialized || metersPerSecondPerCell == null || metersPerSecondPerCell.Length != gridRes * gridRes)
{
return false;
}
EnsureExternalDepthRateMap();
Array.Copy(metersPerSecondPerCell, externalDepthRateMapData, externalDepthRateMapData.Length);
externalDepthRateMap.SetPixelData(externalDepthRateMapData, 0);
externalDepthRateMap.Apply(false, false);
useExternalDepthRateMap = true;
return true;
}
public void ClearExternalDepthRateMap()
{
if (!isInitialized)
{
return;
}
EnsureExternalDepthRateMap();
Array.Clear(externalDepthRateMapData, 0, externalDepthRateMapData.Length);
externalDepthRateMap.SetPixelData(externalDepthRateMapData, 0);
externalDepthRateMap.Apply(false, false);
useExternalDepthRateMap = false;
}
public bool SetGhostBoundaryOverrides(int[] ghostIndices, float[] waterLevels, Vector2[] velocities)
{
if (!isInitialized)
{
return false;
}
if (ghostIndices == null || waterLevels == null || velocities == null)
{
return false;
}
if (ghostIndices.Length == 0 || ghostIndices.Length != waterLevels.Length || ghostIndices.Length != velocities.Length)
{
return false;
}
EnsureGhostOverrideTextures();
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
bool any = false;
int maxIndex = ghostOverrideMaskData.Length - 1;
for (int i = 0; i < ghostIndices.Length; i++)
{
int idx = ghostIndices[i];
if (idx < 0 || idx > maxIndex)
{
continue;
}
any = true;
ghostOverrideMaskData[idx] = 1.0f;
ghostOverrideWaterData[idx] = Mathf.Max(0.0f, waterLevels[i]);
ghostOverrideVelocityData[idx] = velocities[i];
}
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
ghostOverrideMask.Apply(false, false);
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
ghostOverrideWater.Apply(false, false);
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
ghostOverrideVelocity.Apply(false, false);
useGhostBoundaryOverrides = any;
return any;
}
public void ClearGhostBoundaryOverrides()
{
if (!isInitialized)
{
return;
}
EnsureGhostOverrideTextures();
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
ghostOverrideMask.Apply(false, false);
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
ghostOverrideWater.Apply(false, false);
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
ghostOverrideVelocity.Apply(false, false);
useGhostBoundaryOverrides = false;
EnsureGhostFreeOutflowMask();
Array.Clear(ghostFreeOutflowMaskData, 0, ghostFreeOutflowMaskData.Length);
ghostFreeOutflowMask.SetPixelData(ghostFreeOutflowMaskData, 0);
ghostFreeOutflowMask.Apply(false, false);
useGhostFreeOutflowMask = false;
}
public bool SetGhostFreeOutflowCells(int[] ghostIndices)
{
if (!isInitialized)
{
return false;
}
if (ghostIndices == null || ghostIndices.Length == 0)
{
return false;
}
EnsureGhostFreeOutflowMask();
Array.Clear(ghostFreeOutflowMaskData, 0, ghostFreeOutflowMaskData.Length);
bool any = false;
int maxIndex = ghostFreeOutflowMaskData.Length - 1;
for (int i = 0; i < ghostIndices.Length; i++)
{
int idx = ghostIndices[i];
if (idx < 0 || idx > maxIndex)
{
continue;
}
ghostFreeOutflowMaskData[idx] = 1.0f;
any = true;
}
ghostFreeOutflowMask.SetPixelData(ghostFreeOutflowMaskData, 0);
ghostFreeOutflowMask.Apply(false, false);
useGhostFreeOutflowMask = any;
return any;
}
public void ClearGhostFreeOutflowCells()
{
if (!isInitialized)
{
return;
}
EnsureGhostFreeOutflowMask();
Array.Clear(ghostFreeOutflowMaskData, 0, ghostFreeOutflowMaskData.Length);
ghostFreeOutflowMask.SetPixelData(ghostFreeOutflowMaskData, 0);
ghostFreeOutflowMask.Apply(false, false);
useGhostFreeOutflowMask = false;
}
public bool TryGetBoundaryIdMasks(out int[] sourceIds, out int[] sinkIds)
{
sourceIds = null;
sinkIds = null;
bool any = false;
if (resolvedSourceIdMask != null && resolvedSourceIdMask.isReadable)
{
sourceIds = ReadIdTexture(resolvedSourceIdMask);
any = sourceIds != null;
}
if (resolvedSinkIdMask != null && resolvedSinkIdMask.isReadable)
{
sinkIds = ReadIdTexture(resolvedSinkIdMask);
any = any || sinkIds != null;
}
return any;
}
public bool TryCaptureWaterHeights(out float[] heights)
{
heights = null;
if (!isInitialized)
{
return false;
}
if (!HeightmapExtractor.TryExtractFromRenderTexture(CurrentWater, tickIndex, packetTileId, out HeightmapPacket packet))
{
return false;
}
return packet.TryDecodeHeights(out heights);
}
public bool TryRestoreWaterHeights(float[] heights, bool clearVelocity = true)
{
if (!isInitialized || heights == null || heights.Length != gridRes * gridRes)
{
return false;
}
Texture2D temp = new Texture2D(gridRes, gridRes, TextureFormat.RFloat, false, true);
temp.SetPixelData(heights, 0);
temp.Apply(false, false);
Graphics.CopyTexture(temp, 0, 0, CurrentWater, 0, 0);
Destroy(temp);
if (clearVelocity)
{
ClearRenderTexture(CurrentVelocity);
}
debugWater = CurrentWater;
debugVelocity = CurrentVelocity;
return true;
}
public bool TryCapturePorosity(out float[] porosityValues)
{
porosityValues = null;
if (resolvedPorosity == null || !resolvedPorosity.isReadable)
{
return false;
}
Color[] pixels = resolvedPorosity.GetPixels();
porosityValues = new float[pixels.Length];
for (int i = 0; i < pixels.Length; i++)
{
porosityValues[i] = pixels[i].r;
}
return true;
}
public bool TryRestorePorosity(float[] porosityValues)
{
if (resolvedPorosity == null || !resolvedPorosity.isReadable || porosityValues == null || porosityValues.Length != gridRes * gridRes)
{
return false;
}
resolvedPorosity.SetPixelData(porosityValues, 0);
resolvedPorosity.Apply(false, false);
return true;
}
public bool ApplyPorosityStampNormalized(Vector2 uv, float radiusNormalized, float porosityValue)
{
if (resolvedPorosity == null || !resolvedPorosity.isReadable)
{
return false;
}
int cx = Mathf.RoundToInt(Mathf.Clamp01(uv.x) * (gridRes - 1));
int cy = Mathf.RoundToInt(Mathf.Clamp01(uv.y) * (gridRes - 1));
int radius = Mathf.Max(1, Mathf.RoundToInt(Mathf.Clamp01(radiusNormalized) * gridRes));
float clampedPorosity = Mathf.Clamp01(porosityValue);
int radiusSq = radius * radius;
Color[] pixels = resolvedPorosity.GetPixels();
for (int y = Mathf.Max(0, cy - radius); y <= Mathf.Min(gridRes - 1, cy + radius); y++)
{
int dy = y - cy;
for (int x = Mathf.Max(0, cx - radius); x <= Mathf.Min(gridRes - 1, cx + radius); x++)
{
int dxp = x - cx;
if ((dxp * dxp) + (dy * dy) > radiusSq)
{
continue;
}
int idx = y * gridRes + x;
pixels[idx].r = clampedPorosity;
pixels[idx].g = clampedPorosity;
pixels[idx].b = clampedPorosity;
pixels[idx].a = 1.0f;
}
}
resolvedPorosity.SetPixels(pixels);
resolvedPorosity.Apply(false, false);
return true;
}
private void OnEnable()
{
Initialize();
}
private void OnDisable()
{
Release();
}
private void Update()
{
if (!isInitialized)
{
return;
}
accumulatedTime += Time.deltaTime;
if (accumulatedTime < tickSeconds)
{
return;
}
accumulatedTime -= tickSeconds;
float maxDepth = Mathf.Max(lastMaxDepth, 1e-4f);
float maxSpeed = Mathf.Max(lastMaxSpeed, 0.0f);
float dtMax = cfl * dx / (maxSpeed + Mathf.Sqrt(Gravity * maxDepth));
if (float.IsNaN(dtMax) || dtMax <= 0.0f)
{
dtMax = tickSeconds;
}
int substeps = Mathf.Max(1, Mathf.CeilToInt(tickSeconds / dtMax));
float dt = tickSeconds / substeps;
lastDtMax = dtMax;
lastSubsteps = substeps;
lastDt = dt;
if (logCfl)
{
Debug.Log($"SWE CFL: maxDepth={maxDepth:F3}m maxSpeed={maxSpeed:F3}m/s dtMax={dtMax:F4}s substeps={substeps} dt={dt:F4}s");
}
float rainRate = rainRateMmPerHr / 1000.0f / 3600.0f;
for (int i = 0; i < substeps; i++)
{
DispatchGhost();
DispatchFlux(dt, rainRate, externalDepthRateMps);
SwapBuffers();
}
if (applyMassCorrection && Mathf.Abs(pendingMassScale - 1.0f) > 0.001f)
{
DispatchScale(pendingMassScale);
pendingMassScale = 1.0f;
}
debugWater = CurrentWater;
debugVelocity = CurrentVelocity;
tickIndex++;
TryEmitHeightmapPacket();
RequestReadback();
}
private RenderTexture CurrentWater
{
get { return useA ? waterA : waterB; }
}
private RenderTexture CurrentVelocity
{
get { return useA ? velA : velB; }
}
private RenderTexture NextWater
{
get { return useA ? waterB : waterA; }
}
private RenderTexture NextVelocity
{
get { return useA ? velB : velA; }
}
private void Initialize()
{
if (ghostExchangeShader == null || fluxShader == null)
{
Debug.LogError("SweTileSimulator: missing compute shaders.");
return;
}
if (useFullPrecisionTextures && SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3)
{
Debug.LogWarning("SweTileSimulator: full precision UAVs are unsupported on GLES3; falling back to half precision.");
useFullPrecisionTextures = false;
}
gridRes = Mathf.Max(4, gridRes);
tileSizeMeters = Mathf.Max(1.0f, tileSizeMeters);
dx = tileSizeMeters / gridRes;
ghostKernel = ghostExchangeShader.FindKernel("GhostExchange");
fluxKernel = fluxShader.FindKernel("FluxUpdate");
initKernel = fluxShader.FindKernel("InitDamBreak");
RenderTextureFormat waterFormat = useFullPrecisionTextures ? RenderTextureFormat.RFloat : RenderTextureFormat.RHalf;
RenderTextureFormat velFormat = useFullPrecisionTextures ? RenderTextureFormat.RGFloat : RenderTextureFormat.RGHalf;
waterA = CreateRenderTexture(gridRes, gridRes, waterFormat, "SWE_WaterA");
waterB = CreateRenderTexture(gridRes, gridRes, waterFormat, "SWE_WaterB");
velA = CreateRenderTexture(gridRes, gridRes, velFormat, "SWE_VelA");
velB = CreateRenderTexture(gridRes, gridRes, velFormat, "SWE_VelB");
waterGhost = CreateRenderTexture(gridRes + 2, gridRes + 2, waterFormat, "SWE_WaterGhost");
velGhost = CreateRenderTexture(gridRes + 2, gridRes + 2, velFormat, "SWE_VelGhost");
dummyRw = CreateRenderTexture(1, 1, waterFormat, "SWE_DummyRW");
if (debugClampStats)
{
clampMask = CreateRenderTexture(gridRes, gridRes, waterFormat, "SWE_ClampMask");
}
ResolveStaticTextures();
isInitialized = true;
ResetSimulationState();
}
private void Release()
{
isInitialized = false;
ReleaseRenderTexture(waterA);
ReleaseRenderTexture(waterB);
ReleaseRenderTexture(velA);
ReleaseRenderTexture(velB);
ReleaseRenderTexture(waterGhost);
ReleaseRenderTexture(velGhost);
ReleaseRenderTexture(clampMask);
ReleaseRenderTexture(dummyRw);
waterA = null;
waterB = null;
velA = null;
velB = null;
waterGhost = null;
velGhost = null;
clampMask = null;
dummyRw = null;
ReleaseResolvedStaticTextures();
if (externalDepthRateMap != null)
{
Destroy(externalDepthRateMap);
}
if (ghostOverrideMask != null)
{
Destroy(ghostOverrideMask);
}
if (ghostOverrideWater != null)
{
Destroy(ghostOverrideWater);
}
if (ghostOverrideVelocity != null)
{
Destroy(ghostOverrideVelocity);
}
if (ghostFreeOutflowMask != null)
{
Destroy(ghostFreeOutflowMask);
}
resolvedTerrain = null;
resolvedPorosity = null;
resolvedSourceIdMask = null;
resolvedSinkIdMask = null;
externalDepthRateMap = null;
externalDepthRateMapData = null;
useExternalDepthRateMap = false;
ghostOverrideMask = null;
ghostOverrideWater = null;
ghostOverrideVelocity = null;
ghostOverrideMaskData = null;
ghostOverrideWaterData = null;
ghostOverrideVelocityData = null;
useGhostBoundaryOverrides = false;
ghostFreeOutflowMask = null;
ghostFreeOutflowMaskData = null;
useGhostFreeOutflowMask = false;
}
private void ResolveStaticTextures()
{
ReleaseResolvedStaticTextures();
resolvedTerrain = ResolveTerrainTexture();
resolvedPorosity = ResolvePorosityTexture();
resolvedSourceIdMask = ResolveBoundaryMaskTexture(sourceIdMask, "SWE_SourceIds");
resolvedSinkIdMask = ResolveBoundaryMaskTexture(sinkIdMask, "SWE_SinkIds");
useExternalDepthRateMap = false;
EnsureExternalDepthRateMap();
Array.Clear(externalDepthRateMapData, 0, externalDepthRateMapData.Length);
externalDepthRateMap.SetPixelData(externalDepthRateMapData, 0);
externalDepthRateMap.Apply(false, false);
EnsureGhostOverrideTextures();
Array.Clear(ghostOverrideMaskData, 0, ghostOverrideMaskData.Length);
Array.Clear(ghostOverrideWaterData, 0, ghostOverrideWaterData.Length);
Array.Clear(ghostOverrideVelocityData, 0, ghostOverrideVelocityData.Length);
ghostOverrideMask.SetPixelData(ghostOverrideMaskData, 0);
ghostOverrideMask.Apply(false, false);
ghostOverrideWater.SetPixelData(ghostOverrideWaterData, 0);
ghostOverrideWater.Apply(false, false);
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
ghostOverrideVelocity.Apply(false, false);
useGhostBoundaryOverrides = false;
}
private void ReleaseResolvedStaticTextures()
{
if (resolvedTerrain != null && resolvedTerrain != terrainHeight)
{
Destroy(resolvedTerrain);
}
if (resolvedPorosity != null && resolvedPorosity != porosity)
{
Destroy(resolvedPorosity);
}
if (resolvedSourceIdMask != null && resolvedSourceIdMask != sourceIdMask)
{
Destroy(resolvedSourceIdMask);
}
if (resolvedSinkIdMask != null && resolvedSinkIdMask != sinkIdMask)
{
Destroy(resolvedSinkIdMask);
}
}
private void ResetSimulationState()
{
if (waterA == null || velA == null || waterB == null || velB == null)
{
return;
}
ClearRenderTexture(waterA);
ClearRenderTexture(velA);
ClearRenderTexture(waterB);
ClearRenderTexture(velB);
ClearRenderTexture(waterGhost);
ClearRenderTexture(velGhost);
fluxShader.SetInt("_GridRes", gridRes);
fluxShader.SetFloat("_InitDepthLeft", initialDepthLeft);
fluxShader.SetFloat("_InitDepthRight", initialDepthRight);
fluxShader.SetVector("_InitVelocity", initialVelocity);
fluxShader.SetInt("_InitDamBreak", damBreak ? 1 : 0);
fluxShader.SetTexture(initKernel, "_WaterOut", waterA);
fluxShader.SetTexture(initKernel, "_VelOut", velA);
DispatchKernel(fluxShader, initKernel, gridRes, gridRes);
useA = true;
tickIndex = 0;
pendingMassScale = 1.0f;
lastMaxDepth = Mathf.Max(initialDepthLeft, initialDepthRight);
lastMaxSpeed = Mathf.Sqrt(Gravity * Mathf.Max(lastMaxDepth, 0.0f));
debugWater = CurrentWater;
debugVelocity = CurrentVelocity;
}
private Texture2D ResolveTerrainTexture()
{
if (terrainHeight == null)
{
return CreateFlatTexture(gridRes, flatTerrainHeightMeters);
}
if (!resampleTerrainToGrid)
{
if (terrainHeight.width == gridRes && terrainHeight.height == gridRes)
{
return terrainHeight;
}
Debug.LogWarning("SweTileSimulator: terrainHeight size mismatch and resampling disabled. Using flat terrain.");
return CreateFlatTexture(gridRes, flatTerrainHeightMeters);
}
if (!terrainHeight.isReadable)
{
Debug.LogWarning("SweTileSimulator: terrainHeight texture must be readable. Using flat terrain.");
return CreateFlatTexture(gridRes, flatTerrainHeightMeters);
}
if (terrainHeight.width == gridRes && terrainHeight.height == gridRes)
{
return terrainHeight;
}
float minHeight;
float maxHeight;
float[] resampled = DemResampler.ResampleHeights(
terrainHeight,
gridRes,
flatTerrainHeightMeters,
out minHeight,
out maxHeight,
true);
return CreateTextureFromFloats(resampled, gridRes, gridRes, "SWE_TerrainResampled");
}
private Texture2D ResolvePorosityTexture()
{
if (porosity == null)
{
return CreateFlatTexture(gridRes, Mathf.Clamp01(defaultPorosity));
}
if (!resamplePorosityToGrid)
{
if (porosity.width == gridRes && porosity.height == gridRes)
{
return CloneTextureToRuntime(porosity, "SWE_PorosityRuntime");
}
Debug.LogWarning("SweTileSimulator: porosity size mismatch and resampling disabled. Using default porosity.");
return CreateFlatTexture(gridRes, Mathf.Clamp01(defaultPorosity));
}
if (!porosity.isReadable)
{
Debug.LogWarning("SweTileSimulator: porosity texture must be readable. Using default porosity.");
return CreateFlatTexture(gridRes, Mathf.Clamp01(defaultPorosity));
}
if (porosity.width == gridRes && porosity.height == gridRes)
{
return CloneTextureToRuntime(porosity, "SWE_PorosityRuntime");
}
float minValue;
float maxValue;
float[] resampled = DemResampler.ResampleHeights(
porosity,
gridRes,
Mathf.Clamp01(defaultPorosity),
out minValue,
out maxValue,
false);
Clamp01(resampled);
return CreateTextureFromFloats(resampled, gridRes, gridRes, "SWE_PorosityResampled");
}
private Texture2D ResolveBoundaryMaskTexture(Texture2D mask, string runtimeName)
{
if (mask == null)
{
return null;
}
Texture2D readableMask = mask;
if (!mask.isReadable)
{
readableMask = CopyTextureToReadable(mask, runtimeName + "_Readable");
if (readableMask == null)
{
Debug.LogWarning($"SweTileSimulator: {runtimeName} texture is not readable and GPU copy failed. Ignoring boundary mask.");
return null;
}
}
if (!resampleBoundaryMasksToGrid)
{
if (readableMask.width == gridRes && readableMask.height == gridRes)
{
return CloneTextureToRuntime(readableMask, runtimeName);
}
Debug.LogWarning($"SweTileSimulator: {runtimeName} size mismatch and resampling disabled. Ignoring boundary mask.");
return null;
}
if (readableMask.width == gridRes && readableMask.height == gridRes)
{
return CloneTextureToRuntime(readableMask, runtimeName);
}
int length = gridRes * gridRes;
float[] data = new float[length];
float denom = Mathf.Max(1.0f, gridRes - 1.0f);
float sourceMaxX = readableMask.width - 1.0f;
float sourceMaxY = readableMask.height - 1.0f;
int idx = 0;
for (int y = 0; y < gridRes; y++)
{
int sy = Mathf.Clamp(Mathf.RoundToInt((y / denom) * sourceMaxY), 0, readableMask.height - 1);
for (int x = 0; x < gridRes; x++)
{
int sx = Mathf.Clamp(Mathf.RoundToInt((x / denom) * sourceMaxX), 0, readableMask.width - 1);
data[idx++] = readableMask.GetPixel(sx, sy).r;
}
}
if (!ReferenceEquals(readableMask, mask))
{
Destroy(readableMask);
}
return CreateTextureFromFloats(data, gridRes, gridRes, runtimeName);
}
private void TryEmitHeightmapPacket()
{
if (!emitHeightmapPackets)
{
return;
}
int interval = Mathf.Max(1, packetEveryNTicks);
if (tickIndex % interval != 0)
{
return;
}
if (HeightmapExtractor.TryExtractFromRenderTexture(debugWater, tickIndex, packetTileId, out HeightmapPacket packet))
{
lastHeightmapPacket = packet;
HeightmapPacketReady?.Invoke(packet);
}
}
private void DispatchGhost()
{
if (ghostOverrideMask == null || ghostOverrideWater == null || ghostOverrideVelocity == null)
{
EnsureGhostOverrideTextures();
}
if (ghostFreeOutflowMask == null)
{
EnsureGhostFreeOutflowMask();
}
ghostExchangeShader.SetInt("_GridRes", gridRes);
ghostExchangeShader.SetInt("_HasNorth", 0);
ghostExchangeShader.SetInt("_HasSouth", 0);
ghostExchangeShader.SetInt("_HasEast", 0);
ghostExchangeShader.SetInt("_HasWest", 0);
ghostExchangeShader.SetInt("_UseGhostOverride", useGhostBoundaryOverrides ? 1 : 0);
ghostExchangeShader.SetInt("_UseGhostFreeOutflow", useGhostFreeOutflowMask ? 1 : 0);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterIn", CurrentWater);
ghostExchangeShader.SetTexture(ghostKernel, "_VelIn", CurrentVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterNorth", CurrentWater);
ghostExchangeShader.SetTexture(ghostKernel, "_VelNorth", CurrentVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterSouth", CurrentWater);
ghostExchangeShader.SetTexture(ghostKernel, "_VelSouth", CurrentVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterEast", CurrentWater);
ghostExchangeShader.SetTexture(ghostKernel, "_VelEast", CurrentVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterWest", CurrentWater);
ghostExchangeShader.SetTexture(ghostKernel, "_VelWest", CurrentVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_WaterOut", waterGhost);
ghostExchangeShader.SetTexture(ghostKernel, "_VelOut", velGhost);
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideMask", ghostOverrideMask);
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideWater", ghostOverrideWater);
ghostExchangeShader.SetTexture(ghostKernel, "_GhostOverrideVel", ghostOverrideVelocity);
ghostExchangeShader.SetTexture(ghostKernel, "_GhostFreeOutflowMask", ghostFreeOutflowMask);
DispatchKernel(ghostExchangeShader, ghostKernel, gridRes + 2, gridRes + 2);
}
private void DispatchFlux(float dt, float rainRate, float externalDepthRate)
{
fluxShader.SetInt("_GridRes", gridRes);
fluxShader.SetFloat("_Dx", dx);
fluxShader.SetFloat("_Dt", dt);
fluxShader.SetFloat("_Gravity", Gravity);
fluxShader.SetFloat("_RainRate", Mathf.Max(0.0f, rainRate));
fluxShader.SetFloat("_ExternalDepthRate", externalDepthRate);
fluxShader.SetInt("_UseExternalDepthRateTex", useExternalDepthRateMap ? 1 : 0);
fluxShader.SetInt("_UsePorosity", resolvedPorosity != null ? 1 : 0);
fluxShader.SetTexture(fluxKernel, "_WaterIn", waterGhost);
fluxShader.SetTexture(fluxKernel, "_VelIn", velGhost);
fluxShader.SetTexture(fluxKernel, "_WaterOut", NextWater);
fluxShader.SetTexture(fluxKernel, "_VelOut", NextVelocity);
fluxShader.SetTexture(fluxKernel, "_TerrainHeight", resolvedTerrain);
fluxShader.SetTexture(fluxKernel, "_Porosity", resolvedPorosity);
fluxShader.SetTexture(fluxKernel, "_ExternalDepthRateTex", externalDepthRateMap != null ? externalDepthRateMap : resolvedTerrain);
fluxShader.SetInt("_DebugClamp", debugClampStats ? 1 : 0);
fluxShader.SetTexture(fluxKernel, "_ClampMask", debugClampStats && clampMask != null ? clampMask : dummyRw);
DispatchKernel(fluxShader, fluxKernel, gridRes, gridRes);
}
private void DispatchScale(float scale)
{
int scaleKernel = fluxShader.FindKernel("ScaleWater");
fluxShader.SetInt("_GridRes", gridRes);
fluxShader.SetFloat("_Scale", Mathf.Clamp(scale, 0.0f, 10.0f));
fluxShader.SetTexture(scaleKernel, "_WaterOut", CurrentWater);
DispatchKernel(fluxShader, scaleKernel, gridRes, gridRes);
}
private void SwapBuffers()
{
useA = !useA;
}
private void RequestReadback()
{
if (!SystemInfo.supportsAsyncGPUReadback)
{
return;
}
RenderTexture water = CurrentWater;
RenderTexture vel = CurrentVelocity;
if (useFullPrecisionTextures)
{
AsyncGPUReadback.Request(water, 0, request =>
{
if (!request.hasError)
{
DepthStats stats = ComputeDepthStats(request.GetData<float>());
if (!float.IsNaN(stats.Max) && !float.IsInfinity(stats.Max))
{
lastMaxDepth = stats.Max;
}
if (!float.IsNaN(stats.Sum) && !float.IsInfinity(stats.Sum))
{
float previousTotal = lastTotalDepth > 0.0f ? lastTotalDepth : stats.Sum;
lastTotalDepth = stats.Sum;
lastTotalVolume = stats.Sum * (dx * dx);
if (applyMassCorrection && stats.Sum > 1e-6f)
{
float scale = previousTotal / stats.Sum;
pendingMassScale = Mathf.Clamp(scale, massCorrectionMinScale, massCorrectionMaxScale);
}
if (logConservation)
{
Debug.Log($"SWE mass: totalDepth={lastTotalDepth:F3}m volume={lastTotalVolume:F3}m^3 scaleNext={pendingMassScale:F3}");
}
}
}
});
AsyncGPUReadback.Request(vel, 0, request =>
{
if (!request.hasError)
{
float speed = ComputeMaxSpeed(request.GetData<float>());
if (!float.IsNaN(speed) && !float.IsInfinity(speed))
{
lastMaxSpeed = speed;
}
}
});
}
else
{
AsyncGPUReadback.Request(water, 0, request =>
{
if (!request.hasError)
{
DepthStats stats = ComputeDepthStats(request.GetData<ushort>());
if (!float.IsNaN(stats.Max) && !float.IsInfinity(stats.Max))
{
lastMaxDepth = stats.Max;
}
if (!float.IsNaN(stats.Sum) && !float.IsInfinity(stats.Sum))
{
float previousTotal = lastTotalDepth > 0.0f ? lastTotalDepth : stats.Sum;
lastTotalDepth = stats.Sum;
lastTotalVolume = stats.Sum * (dx * dx);
if (applyMassCorrection && stats.Sum > 1e-6f)
{
float scale = previousTotal / stats.Sum;
pendingMassScale = Mathf.Clamp(scale, massCorrectionMinScale, massCorrectionMaxScale);
}
if (logConservation)
{
Debug.Log($"SWE mass: totalDepth={lastTotalDepth:F3}m volume={lastTotalVolume:F3}m^3 scaleNext={pendingMassScale:F3}");
}
}
}
});
AsyncGPUReadback.Request(vel, 0, request =>
{
if (!request.hasError)
{
float speed = ComputeMaxSpeed(request.GetData<ushort>());
if (!float.IsNaN(speed) && !float.IsInfinity(speed))
{
lastMaxSpeed = speed;
}
}
});
}
if (debugClampStats && clampMask != null)
{
AsyncGPUReadback.Request(clampMask, 0, request =>
{
if (!request.hasError)
{
ClampStats stats = useFullPrecisionTextures
? CountClampStats(request.GetData<float>())
: CountClampStats(request.GetData<ushort>());
lastClampedCells = stats.Clamped;
lastNanCells = stats.NaNs;
lastClampedRatio = stats.Clamped / (float)(gridRes * gridRes);
}
});
}
}
private static DepthStats ComputeDepthStats(NativeArray<ushort> data)
{
float max = 0.0f;
double sum = 0.0;
for (int i = 0; i < data.Length; i++)
{
float h = HalfToFloat(data[i]);
if (h > max)
{
max = h;
}
if (h > 0.0f)
{
sum += h;
}
}
return new DepthStats(max, (float)sum);
}
private static DepthStats ComputeDepthStats(NativeArray<float> data)
{
float max = 0.0f;
double sum = 0.0;
for (int i = 0; i < data.Length; i++)
{
float h = data[i];
if (h > max)
{
max = h;
}
if (h > 0.0f)
{
sum += h;
}
}
return new DepthStats(max, (float)sum);
}
private static float ComputeMaxSpeed(NativeArray<ushort> data)
{
float max = 0.0f;
int count = data.Length / 2;
for (int i = 0; i < count; i++)
{
float u = HalfToFloat(data[i * 2]);
float v = HalfToFloat(data[i * 2 + 1]);
float speed = Mathf.Sqrt(u * u + v * v);
if (speed > max)
{
max = speed;
}
}
return max;
}
private static float ComputeMaxSpeed(NativeArray<float> data)
{
float max = 0.0f;
int count = data.Length / 2;
for (int i = 0; i < count; i++)
{
float u = data[i * 2];
float v = data[i * 2 + 1];
float speed = Mathf.Sqrt(u * u + v * v);
if (speed > max)
{
max = speed;
}
}
return max;
}
private static ClampStats CountClampStats(NativeArray<ushort> data)
{
int clamped = 0;
int nans = 0;
for (int i = 0; i < data.Length; i++)
{
float v = HalfToFloat(data[i]);
if (v > 1.5f)
{
nans++;
}
else if (v > 0.5f)
{
clamped++;
}
}
return new ClampStats(clamped, nans);
}
private static ClampStats CountClampStats(NativeArray<float> data)
{
int clamped = 0;
int nans = 0;
for (int i = 0; i < data.Length; i++)
{
float v = data[i];
if (v > 1.5f)
{
nans++;
}
else if (v > 0.5f)
{
clamped++;
}
}
return new ClampStats(clamped, nans);
}
private readonly struct ClampStats
{
public readonly int Clamped;
public readonly int NaNs;
public ClampStats(int clamped, int nans)
{
Clamped = clamped;
NaNs = nans;
}
}
private static float HalfToFloat(ushort half)
{
uint sign = (uint)(half >> 15) & 0x00000001u;
int exp = (half >> 10) & 0x0000001F;
int mant = half & 0x000003FF;
if (exp == 0)
{
if (mant == 0)
{
return sign == 1 ? -0.0f : 0.0f;
}
exp = 1;
while ((mant & 0x00000400) == 0)
{
mant <<= 1;
exp--;
}
mant &= 0x000003FF;
}
else if (exp == 31)
{
uint infNaN = (sign << 31) | 0x7F800000u | ((uint)mant << 13);
return BitConverter.ToSingle(BitConverter.GetBytes(infNaN), 0);
}
uint fexp = (uint)(exp + (127 - 15));
uint fmant = (uint)(mant << 13);
uint bits = (sign << 31) | (fexp << 23) | fmant;
return BitConverter.ToSingle(BitConverter.GetBytes(bits), 0);
}
private readonly struct DepthStats
{
public readonly float Max;
public readonly float Sum;
public DepthStats(float max, float sum)
{
Max = max;
Sum = sum;
}
}
private static Texture2D CreateTextureFromFloats(float[] data, int width, int height, string name)
{
Texture2D texture = new Texture2D(width, height, TextureFormat.RFloat, false, true)
{
name = name,
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Point
};
texture.SetPixelData(data, 0);
texture.Apply(false, false);
return texture;
}
private static Texture2D CloneTextureToRuntime(Texture2D source, string runtimeName)
{
if (source == null || !source.isReadable)
{
return source;
}
int length = source.width * source.height;
Color[] pixels = source.GetPixels();
float[] data = new float[length];
for (int i = 0; i < length; i++)
{
data[i] = pixels[i].r;
}
return CreateTextureFromFloats(data, source.width, source.height, runtimeName);
}
private static Texture2D CreateFlatTexture(int resolution, float value)
{
int length = resolution * resolution;
float[] data = new float[length];
for (int i = 0; i < length; i++)
{
data[i] = value;
}
return CreateTextureFromFloats(data, resolution, resolution, "SWE_Flat");
}
private static void Clamp01(float[] data)
{
for (int i = 0; i < data.Length; i++)
{
data[i] = Mathf.Clamp01(data[i]);
}
}
private void EnsureExternalDepthRateMap()
{
int length = gridRes * gridRes;
if (externalDepthRateMap == null || externalDepthRateMap.width != gridRes || externalDepthRateMap.height != gridRes)
{
if (externalDepthRateMap != null)
{
Destroy(externalDepthRateMap);
}
externalDepthRateMap = CreateTextureFromFloats(new float[length], gridRes, gridRes, "SWE_ExternalDepthRateMap");
externalDepthRateMapData = new float[length];
}
else if (externalDepthRateMapData == null || externalDepthRateMapData.Length != length)
{
externalDepthRateMapData = new float[length];
}
}
private void EnsureGhostOverrideTextures()
{
int ghostRes = gridRes + 2;
int length = ghostRes * ghostRes;
if (ghostOverrideMask == null || ghostOverrideMask.width != ghostRes || ghostOverrideMask.height != ghostRes)
{
if (ghostOverrideMask != null)
{
Destroy(ghostOverrideMask);
}
ghostOverrideMask = CreateTextureFromFloats(new float[length], ghostRes, ghostRes, "SWE_GhostOverrideMask");
ghostOverrideMaskData = new float[length];
}
else if (ghostOverrideMaskData == null || ghostOverrideMaskData.Length != length)
{
ghostOverrideMaskData = new float[length];
}
if (ghostOverrideWater == null || ghostOverrideWater.width != ghostRes || ghostOverrideWater.height != ghostRes)
{
if (ghostOverrideWater != null)
{
Destroy(ghostOverrideWater);
}
ghostOverrideWater = CreateTextureFromFloats(new float[length], ghostRes, ghostRes, "SWE_GhostOverrideWater");
ghostOverrideWaterData = new float[length];
}
else if (ghostOverrideWaterData == null || ghostOverrideWaterData.Length != length)
{
ghostOverrideWaterData = new float[length];
}
if (ghostOverrideVelocity == null || ghostOverrideVelocity.width != ghostRes || ghostOverrideVelocity.height != ghostRes)
{
if (ghostOverrideVelocity != null)
{
Destroy(ghostOverrideVelocity);
}
ghostOverrideVelocity = new Texture2D(ghostRes, ghostRes, TextureFormat.RGFloat, false, true)
{
name = "SWE_GhostOverrideVelocity",
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Point
};
ghostOverrideVelocityData = new Vector2[length];
ghostOverrideVelocity.SetPixelData(ghostOverrideVelocityData, 0);
ghostOverrideVelocity.Apply(false, false);
}
else if (ghostOverrideVelocityData == null || ghostOverrideVelocityData.Length != length)
{
ghostOverrideVelocityData = new Vector2[length];
}
}
private void EnsureGhostFreeOutflowMask()
{
int ghostRes = gridRes + 2;
int length = ghostRes * ghostRes;
if (ghostFreeOutflowMask == null || ghostFreeOutflowMask.width != ghostRes || ghostFreeOutflowMask.height != ghostRes)
{
if (ghostFreeOutflowMask != null)
{
Destroy(ghostFreeOutflowMask);
}
ghostFreeOutflowMask = CreateTextureFromFloats(new float[length], ghostRes, ghostRes, "SWE_GhostFreeOutflowMask");
ghostFreeOutflowMaskData = new float[length];
}
else if (ghostFreeOutflowMaskData == null || ghostFreeOutflowMaskData.Length != length)
{
ghostFreeOutflowMaskData = new float[length];
}
}
private static int[] ReadIdTexture(Texture2D texture)
{
if (texture == null || !texture.isReadable)
{
return null;
}
int length = texture.width * texture.height;
Color[] pixels = texture.GetPixels();
int[] ids = new int[length];
for (int i = 0; i < length; i++)
{
ids[i] = Mathf.Max(0, Mathf.RoundToInt(pixels[i].r));
}
return ids;
}
private static Texture2D CopyTextureToReadable(Texture source, string name)
{
if (source == null)
{
return null;
}
int width = source.width;
int height = source.height;
RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear);
RenderTexture prev = RenderTexture.active;
try
{
Graphics.Blit(source, rt);
RenderTexture.active = rt;
Texture2D tex = new Texture2D(width, height, TextureFormat.RFloat, false, true)
{
name = name,
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Point
};
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
tex.Apply(false, false);
return tex;
}
catch
{
return null;
}
finally
{
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
}
}
private static RenderTexture CreateRenderTexture(int width, int height, RenderTextureFormat format, string name)
{
RenderTexture rt = new RenderTexture(width, height, 0, format)
{
name = name,
enableRandomWrite = true,
filterMode = FilterMode.Point,
wrapMode = TextureWrapMode.Clamp
};
rt.Create();
return rt;
}
private static void ClearRenderTexture(RenderTexture rt)
{
if (rt == null)
{
return;
}
RenderTexture prev = RenderTexture.active;
RenderTexture.active = rt;
GL.Clear(false, true, Color.clear);
RenderTexture.active = prev;
}
private static void ReleaseRenderTexture(RenderTexture rt)
{
if (rt != null)
{
rt.Release();
}
}
private static void DispatchKernel(ComputeShader shader, int kernel, int width, int height)
{
uint x;
uint y;
uint z;
shader.GetKernelThreadGroupSizes(kernel, out x, out y, out z);
int groupsX = Mathf.CeilToInt(width / (float)x);
int groupsY = Mathf.CeilToInt(height / (float)y);
shader.Dispatch(kernel, groupsX, groupsY, 1);
}
}