Manager methods for water calm volumes. Tracks a global list of WaterCalmVolume components, updates a GPU buffer with their oriented bounding box data each frame, provides CPU evaluation of calm at a world position, and exposes attributes for rendering.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.WaterTool;
public partial class WaterManager
{
// Calm volumes are few (river/ocean junctions) and apply to every water surface,
// so — like ripples — they live in one shared buffer the manager updates once a
// frame, rather than the per-component distance-sorted exclusion-volume pattern.
private const int MAX_CALM_VOLUMES = 64;
private const int CALM_VOLUME_ROWS = 4;
public List<WaterCalmVolume> CalmVolumes { get; } = [];
private GpuBuffer<Vector4> m_CalmVolumeBuffer;
private readonly Vector4[] m_CalmVolumeData = new Vector4[MAX_CALM_VOLUMES * CALM_VOLUME_ROWS];
private int m_ActiveCalmCount;
public void RefreshWaterCalmVolumesList()
{
if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
return;
CalmVolumes.Clear();
CalmVolumes.AddRange(Scene.GetAll<WaterCalmVolume>());
}
private void UpdateCalmVolumes()
{
int count = 0;
foreach (var volume in CalmVolumes)
{
if (!volume.IsValid() || !volume.Active)
continue;
if (count >= MAX_CALM_VOLUMES)
break;
var (center, forward, up, half) = volume.GetWorldOBB();
int row = count * CALM_VOLUME_ROWS;
m_CalmVolumeData[row + 0] = new Vector4(forward.x, forward.y, forward.z, half.x);
m_CalmVolumeData[row + 1] = new Vector4(up.x, up.y, up.z, half.y);
m_CalmVolumeData[row + 2] = new Vector4(center.x, center.y, center.z, half.z);
m_CalmVolumeData[row + 3] = new Vector4(volume.Falloff, volume.Strength, 0.0f, 0.0f);
count++;
}
m_ActiveCalmCount = count;
EnsureCalmBuffer();
m_CalmVolumeBuffer.SetData(m_CalmVolumeData.AsSpan(0, count * CALM_VOLUME_ROWS));
}
private void EnsureCalmBuffer()
{
if (!m_CalmVolumeBuffer.IsValid())
m_CalmVolumeBuffer = new GpuBuffer<Vector4>(MAX_CALM_VOLUMES * CALM_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);
}
internal void ApplyCalmAttributes(RenderAttributes _Attributes)
{
_Attributes.Set("WaterCalmVolumeCount", m_ActiveCalmCount);
if (m_CalmVolumeBuffer.IsValid())
_Attributes.Set("WaterCalmVolumeData", m_CalmVolumeBuffer);
}
/// <summary>
/// CPU evaluation of the calm factor at a world position (0 = full waves, 1 = flat).
/// MUST mirror ComputeWaterCalm() in water_calm_volume.fxc so physics (buoyancy,
/// height queries) matches the flattened visual surface.
/// </summary>
public float ComputeCalm(Vector3 _WorldPosition)
{
if (CalmVolumes.Count == 0)
return 0.0f;
float calm = 0.0f;
foreach (var volume in CalmVolumes)
{
if (!volume.IsValid() || !volume.Active)
continue;
var (center, forward, up, half) = volume.GetWorldOBB();
Vector3 right = Vector3.Cross(up, forward);
Vector3 d = _WorldPosition - center;
float nx = MathF.Abs(Vector3.Dot(d, forward)) / MathF.Max(half.x, 0.001f);
float ny = MathF.Abs(Vector3.Dot(d, right)) / MathF.Max(half.y, 0.001f);
float nz = MathF.Abs(Vector3.Dot(d, up)) / MathF.Max(half.z, 0.001f);
float nmax = MathF.Max(nx, MathF.Max(ny, nz));
float falloffStart = Math.Clamp(1.0f - volume.Falloff, 0.0f, 1.0f);
float volumeCalm = (1.0f - SmoothStep(falloffStart, 1.0f, nmax)) * volume.Strength;
calm = MathF.Max(calm, volumeCalm);
}
return Math.Clamp(calm, 0.0f, 1.0f);
}
// Matches HLSL smoothstep().
private static float SmoothStep(float _Edge0, float _Edge1, float _X)
{
float t = Math.Clamp((_X - _Edge0) / MathF.Max(_Edge1 - _Edge0, 1e-6f), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
private void ClearCalmVolumes()
{
m_CalmVolumeBuffer?.Dispose();
m_CalmVolumeBuffer = null;
}
}