1535 lines
48 KiB
C#
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);
|
|
}
|
|
}
|