Code/Water/WaterBodyRenderer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.Rendering;
namespace RedSnail.WaterTool;
[Icon("water"), Group("Environment"), Title("Water Body Renderer")]
public sealed class WaterBodyRenderer : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer
{
#pragma warning disable CS0649
private struct WaterVertex
{
[VertexLayout.Position] public Vector3 Position;
[VertexLayout.Normal] public Vector3 Normal;
[VertexLayout.Tangent] public Vector4 Tangent;
[VertexLayout.TexCoord] public Vector2 TexCoord;
[VertexLayout.Color] public Color Color;
}
#pragma warning restore CS0649
private const float BASE_TILE_SIZE = 100.0f;
private const int MAX_RINGS = 8;
private const int MAX_WATER_INCLUSION_VOLUMES = 1024;
private const int WATER_INCLUSION_VOLUME_ROWS = 3;
private const int MAX_WATER_EXCLUSION_VOLUMES = 512;
private const int WATER_EXCLUSION_VOLUME_ROWS = 3;
private const int MAX_HULL_EXCLUSION_VOLUMES = 8;
private const int HULL_EXCLUSION_META_ROWS = 6;
private const int HULL_EXCLUSION_META_SIZE = MAX_HULL_EXCLUSION_VOLUMES * HULL_EXCLUSION_META_ROWS;
private const int MAX_HULL_EXCLUSION_TRIS = 16384;
private GpuBuffer<WaterVertex> m_VertexBuffer;
private GpuBuffer<uint> m_IndexBuffer;
private GpuBuffer<Vector4> m_WaterInclusionVolumeBuffer;
private GpuBuffer<Vector4> m_WaterExclusionVolumeBuffer;
private int m_TotalIndexCount;
private readonly RenderAttributes m_DrawAttributes = new();
private CommandList m_CommandList;
private int m_LastConfigHash;
private readonly Vector4[] m_WaterInclusionVolumeData = new Vector4[MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS];
private readonly Vector4[] m_WaterExclusionVolumeData = new Vector4[MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS];
private GpuBuffer<Vector4> m_HullExclusionBuffer;
private readonly Vector4[] m_HullExclusionData = new Vector4[HULL_EXCLUSION_META_SIZE + MAX_HULL_EXCLUSION_TRIS * 3];
[Property, Group("General"), Order(0)] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;
[Property, Group("General"), Order(0)] public Material Material { get; set; }
[Property, Group("General"), Order(0)] public float Width { get; set; } = 10000.0f;
[Property, Group("General"), Order(0)] public float Length { get; set; } = 10000.0f;
[Property, Group("General"), Order(0)] public float Depth { get; set; } = 300.0f;
[Property(Title = "Infinite Rendering"), Group("General"), Order(0)] public bool UseHybridInclusionBounds { get; set; } = true;
[Property, Group("Clipmap"), Order(1)] public float BaseCellSize { get; set; } = 8.0f;
[Property, Group("Clipmap"), Order(1), Range(16, 512)] public int CellsPerRing { get; set; } = 64;
[Property(Title = "Use Camera For Clipmap"), Group("Clipmap"), Order(1)] public bool FollowCameraForClipmap { get; set; } = true;
[Property, Group("Texture"), Order(2), Range(0.1f, 2.0f)] public float TextureTilingMultiplier { get; set; } = 1.0f;
private int VerticesPerRing => (CellsPerRing + 1) * (CellsPerRing + 1);
private float OuterExtent => CellsPerRing * BaseCellSize * (1 << (ComputeRingCount() - 1));
internal bool ParticipatesInRendering => Active && Material.IsValid();
internal bool HasValidBuffers => m_VertexBuffer.IsValid() && m_IndexBuffer.IsValid();
protected override void OnEnabled()
{
if (!ParticipatesInRendering)
return;
CreateBuffers();
m_LastConfigHash = ComputeConfigHash();
WaterManager.Current?.Register(this);
}
protected override void OnDisabled()
{
WaterManager.Current?.Unregister(this);
m_VertexBuffer = default;
m_IndexBuffer = default;
m_CommandList = null;
m_WaterInclusionVolumeBuffer?.Dispose();
m_WaterInclusionVolumeBuffer = null;
m_WaterExclusionVolumeBuffer?.Dispose();
m_WaterExclusionVolumeBuffer = null;
m_HullExclusionBuffer?.Dispose();
m_HullExclusionBuffer = null;
}
protected override void OnUpdate()
{
if (!ParticipatesInRendering)
return;
int configHash = ComputeConfigHash();
if (!HasValidBuffers || configHash != m_LastConfigHash)
{
CreateBuffers();
m_LastConfigHash = configHash;
}
UpdateShaderAttributes();
}
public void CacheCommandList(CommandList commandList)
{
m_CommandList ??= commandList;
}
internal BBox GetWorldBounds2D()
{
Vector3 right = WorldRotation.Right * (Length / 2.0f);
Vector3 forward = WorldRotation.Forward * (Width / 2.0f);
Vector3 c0 = WorldPosition + right + forward;
Vector3 c1 = WorldPosition - right + forward;
Vector3 c2 = WorldPosition + right - forward;
Vector3 c3 = WorldPosition - right - forward;
float minX = MathF.Min(MathF.Min(c0.x, c1.x), MathF.Min(c2.x, c3.x));
float maxX = MathF.Max(MathF.Max(c0.x, c1.x), MathF.Max(c2.x, c3.x));
float minY = MathF.Min(MathF.Min(c0.y, c1.y), MathF.Min(c2.y, c3.y));
float maxY = MathF.Max(MathF.Max(c0.y, c1.y), MathF.Max(c2.y, c3.y));
return new BBox(new Vector3(minX, minY, WorldPosition.z - Depth), new Vector3(maxX, maxY, WorldPosition.z));
}
internal void DispatchCompute(ComputeShader shader, Vector3 cameraPosition)
{
if (!ParticipatesInRendering || !HasValidBuffers)
return;
int ringCount = ComputeRingCount();
int verticesPerRing = VerticesPerRing;
var localBounds = GetWorldBounds2D();
for (int ring = 0; ring < ringCount; ring++)
{
float cellSize = BaseCellSize * (1 << ring);
Vector3 clipmapAnchor = FollowCameraForClipmap ? cameraPosition : WorldPosition;
float snapX = MathF.Floor(clipmapAnchor.x / cellSize) * cellSize;
float snapY = MathF.Floor(clipmapAnchor.y / cellSize) * cellSize;
shader.Attributes.Set("DiscMode", false);
shader.Attributes.Set("VertexBuffer", m_VertexBuffer);
shader.Attributes.Set("VertexOffset", ring * verticesPerRing);
shader.Attributes.Set("GridWidth", CellsPerRing);
shader.Attributes.Set("CellSize", cellSize);
shader.Attributes.Set("SnapPosition", new Vector2(snapX, snapY));
shader.Attributes.Set("WaterZ", WorldPosition.z);
shader.Attributes.Set("TilingScale", 1.0f / OuterExtent);
shader.Attributes.Set("ClampToBounds", false);
shader.Attributes.Set("BoundsMin", new Vector2(localBounds.Mins.x, localBounds.Mins.y));
shader.Attributes.Set("BoundsMax", new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));
shader.Dispatch(verticesPerRing, 1, 1);
}
}
internal void BarrierTransition()
{
if (m_VertexBuffer.IsValid())
m_CommandList.ResourceBarrierTransition(m_VertexBuffer, ResourceState.UnorderedAccess, ResourceState.VertexOrIndexBuffer);
}
internal void Draw()
{
if (!ParticipatesInRendering || !HasValidBuffers)
return;
m_CommandList.DrawIndexed(m_VertexBuffer, m_IndexBuffer, Material, 0, m_TotalIndexCount, m_DrawAttributes);
}
private void UpdateShaderAttributes()
{
BBox localBounds = GetWorldBounds2D();
m_DrawAttributes.Set("RequireWaterInclusionVolumes", UseHybridInclusionBounds);
m_DrawAttributes.Set("UseHybridInclusionBounds", UseHybridInclusionBounds);
m_DrawAttributes.Set("HybridInclusionBoundsMin", new Vector2(localBounds.Mins.x, localBounds.Mins.y));
m_DrawAttributes.Set("HybridInclusionBoundsMax", new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));
WaterDefinition profile = WaterManager.GetWaveProfile(WaterType);
if (profile.IsValid())
profile.ApplyTo(m_DrawAttributes);
m_DrawAttributes.Set("WaterTime", Time.Now);
m_DrawAttributes.Set("DepthMax", Depth);
float tilingScalar = (OuterExtent / BASE_TILE_SIZE) * TextureTilingMultiplier;
m_DrawAttributes.Set("NormalTiling", new Vector2(tilingScalar, tilingScalar));
SetWaterInclusionVolumes(Scene.Camera.WorldPosition);
SetWaterExclusionVolumes(Scene.Camera.WorldPosition);
SetHullExclusionVolumes();
}
private void SetWaterInclusionVolumes(Vector3 referencePosition)
{
EnsureWaterInclusionVolumeBuffer();
var volumes = WaterManager.Current.Bodies
.Where(v => v.IsValid() && v.Active && v.WaterType == WaterType)
.OrderBy(v => v.WorldPosition.DistanceSquared(referencePosition))
.Take(MAX_WATER_INCLUSION_VOLUMES)
.ToList();
for (int i = 0; i < volumes.Count; i++)
{
var (center, forward, up, half) = volumes[i].GetWorldOBB();
int rowOffset = i * WATER_INCLUSION_VOLUME_ROWS;
m_WaterInclusionVolumeData[rowOffset + 0] = new Vector4(forward.x, forward.y, forward.z, half.x);
m_WaterInclusionVolumeData[rowOffset + 1] = new Vector4(up.x, up.y, up.z, half.y);
m_WaterInclusionVolumeData[rowOffset + 2] = new Vector4(center.x, center.y, center.z, half.z);
}
m_WaterInclusionVolumeBuffer.SetData(m_WaterInclusionVolumeData.AsSpan(0, volumes.Count * WATER_INCLUSION_VOLUME_ROWS));
m_DrawAttributes.Set("WaterInclusionVolumeCount", volumes.Count);
m_DrawAttributes.Set("WaterInclusionVolumeRows", m_WaterInclusionVolumeBuffer);
}
private void SetWaterExclusionVolumes(Vector3 referencePosition)
{
EnsureWaterExclusionVolumeBuffer();
var volumes = WaterManager.Current.ExclusionVolumes
.Where(v => v.IsValid() && v.Enabled && v.Active)
.OrderBy(v => v.WorldPosition.DistanceSquared(referencePosition))
.Take(MAX_WATER_EXCLUSION_VOLUMES)
.ToList();
for (int i = 0; i < volumes.Count; i++)
{
var (center, forward, up, half) = volumes[i].GetWorldOBB();
int rowOffset = i * WATER_EXCLUSION_VOLUME_ROWS;
m_WaterExclusionVolumeData[rowOffset + 0] = new Vector4(forward.x, forward.y, forward.z, half.x);
m_WaterExclusionVolumeData[rowOffset + 1] = new Vector4(up.x, up.y, up.z, half.y);
m_WaterExclusionVolumeData[rowOffset + 2] = new Vector4(center.x, center.y, center.z, half.z);
}
m_WaterExclusionVolumeBuffer.SetData(m_WaterExclusionVolumeData.AsSpan(0, volumes.Count * WATER_EXCLUSION_VOLUME_ROWS));
m_DrawAttributes.Set("WaterExclusionVolumeCount", volumes.Count);
m_DrawAttributes.Set("WaterExclusionVolumeRows", m_WaterExclusionVolumeBuffer);
}
private void EnsureWaterExclusionVolumeBuffer()
{
if (m_WaterExclusionVolumeBuffer.IsValid())
return;
m_WaterExclusionVolumeBuffer = new GpuBuffer<Vector4>(MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);
}
private void SetHullExclusionVolumes()
{
if (WaterManager.Current == null)
return;
var hulls = WaterManager.Current.HullExclusionVolumes
.Where(h => h.IsValid() && h.Active && h.LocalTriangles.Length > 0)
.Take(MAX_HULL_EXCLUSION_VOLUMES)
.ToList();
if (hulls.Count == 0)
{
m_DrawAttributes.Set("WaterHullExclusionCount", 0);
return;
}
EnsureHullExclusionBuffers();
int triWriteCursor = HULL_EXCLUSION_META_SIZE;
for (int h = 0; h < hulls.Count; h++)
{
var hull = hulls[h];
var tris = hull.LocalTriangles;
int triCount = tris.Length / 3;
if (triWriteCursor + tris.Length > m_HullExclusionData.Length)
break;
hull.GetWorldToLocalRows(out var r0, out var r1, out var r2, out var r3);
int meta = h * HULL_EXCLUSION_META_ROWS;
m_HullExclusionData[meta + 0] = r0;
m_HullExclusionData[meta + 1] = r1;
m_HullExclusionData[meta + 2] = r2;
m_HullExclusionData[meta + 3] = r3;
var aabb = hull.LocalAABB;
m_HullExclusionData[meta + 4] = new Vector4(triWriteCursor, triCount, aabb.Mins.x, aabb.Mins.y);
m_HullExclusionData[meta + 5] = new Vector4(aabb.Mins.z, aabb.Maxs.x, aabb.Maxs.y, aabb.Maxs.z);
for (int i = 0; i < tris.Length; i++)
m_HullExclusionData[triWriteCursor + i] = new Vector4(tris[i].x, tris[i].y, tris[i].z, 0f);
triWriteCursor += tris.Length;
}
m_HullExclusionBuffer.SetData(m_HullExclusionData.AsSpan(0, triWriteCursor));
m_DrawAttributes.Set("WaterHullExclusionCount", hulls.Count);
m_DrawAttributes.Set("WaterHullExclusionData", m_HullExclusionBuffer);
}
private void EnsureHullExclusionBuffers()
{
if (!m_HullExclusionBuffer.IsValid())
m_HullExclusionBuffer = new GpuBuffer<Vector4>(HULL_EXCLUSION_META_SIZE + MAX_HULL_EXCLUSION_TRIS * 3, GpuBuffer.UsageFlags.Structured);
}
private void EnsureWaterInclusionVolumeBuffer()
{
if (m_WaterInclusionVolumeBuffer.IsValid())
return;
m_WaterInclusionVolumeBuffer = new GpuBuffer<Vector4>(MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);
}
private int ComputeConfigHash()
{
return HashCode.Combine(Width, Length, BaseCellSize, CellsPerRing);
}
private int ComputeRingCount()
{
return ComputeRingCount(Width, Length);
}
private int ComputeRingCount(float width, float length)
{
float maxDim = MathF.Max(length, width);
float innerExtent = CellsPerRing * BaseCellSize;
float requiredExtent = maxDim * 2.0f;
if (requiredExtent <= innerExtent)
return 1;
int rings = (int)MathF.Ceiling(MathF.Log2(requiredExtent / innerExtent)) + 1;
return Math.Clamp(rings, 1, MAX_RINGS);
}
private void CreateBuffers()
{
int ringCount = ComputeRingCount();
int n = CellsPerRing;
int verticesPerRing = VerticesPerRing;
int innerStart = n / 4 + 1;
int innerEnd = n * 3 / 4 - 1;
int innerBlockSize = innerEnd - innerStart;
int filledCells = n * n;
int hollowCells = filledCells - (innerBlockSize * innerBlockSize);
int totalIndices = filledCells * 6;
totalIndices += (ringCount - 1) * hollowCells * 6;
m_VertexBuffer = new GpuBuffer<WaterVertex>(ringCount * verticesPerRing, GpuBuffer.UsageFlags.Vertex | GpuBuffer.UsageFlags.Structured);
m_IndexBuffer = new GpuBuffer<uint>(totalIndices, GpuBuffer.UsageFlags.Index | GpuBuffer.UsageFlags.Structured);
UploadIndexBuffer(ringCount);
}
private void UploadIndexBuffer(int ringCount)
{
int n = CellsPerRing;
int verticesPerRing = VerticesPerRing;
int innerStart = n / 4 + 1;
int innerEnd = n * 3 / 4 - 1;
var indices = new List<uint>();
for (int ring = 0; ring < ringCount; ring++)
{
uint baseVertex = (uint)(ring * verticesPerRing);
for (int y = 0; y < n; y++)
{
for (int x = 0; x < n; x++)
{
if (ring > 0 && x >= innerStart && x < innerEnd && y >= innerStart && y < innerEnd)
continue;
uint i0 = baseVertex + (uint)(y * (n + 1) + x);
uint i1 = i0 + 1;
uint i2 = i0 + (uint)(n + 1);
uint i3 = i2 + 1;
indices.Add(i0);
indices.Add(i1);
indices.Add(i2);
indices.Add(i1);
indices.Add(i3);
indices.Add(i2);
}
}
}
m_IndexBuffer.SetData(indices);
m_TotalIndexCount = indices.Count;
}
}