Rendering-related partial of WaterNetwork. Manages command list registration, camera culling, prepares GPU buffers and render attributes, updates exclusion volumes and hull exclusions, records draw commands for sea and visible water pieces.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.Rendering;
using RenderStage = Sandbox.Rendering.Stage;
namespace RedSnail.WaterTool2;
public sealed partial class WaterNetwork
{
private const int MaxExclusionVolumes = 64;
private const int ExclusionVolumeRows = 3;
private const int MaxHullExclusions = 8;
private const int HullExclusionMetaRows = 6;
private const int HullExclusionMetaSize = MaxHullExclusions * HullExclusionMetaRows;
private const int MaxHullExclusionTriangles = 16384;
private readonly CommandList m_CommandList = new("Water Network");
private readonly RenderAttributes m_Attributes = new();
private readonly List<WaterPiece> m_Visible = new();
private CameraComponent m_RegisteredCamera;
private Vector3 m_CameraPosition;
private Frustum m_CullFrustum;
private bool m_HasCullFrustum;
private GpuBuffer<Vector4> m_ExclusionBuffer;
private readonly Vector4[] m_ExclusionData = new Vector4[MaxExclusionVolumes * ExclusionVolumeRows];
private int m_ExclusionCount;
private GpuBuffer<Vector4> m_HullExclusionBuffer;
private readonly Vector4[] m_HullExclusionData = new Vector4[HullExclusionMetaSize + MaxHullExclusionTriangles * 3];
private int m_HullExclusionCount;
/// <summary>
/// The camera, culling and shader inputs for this frame. Runs from the component's update, ahead of the recording
/// in <see cref="OnPreRender"/>.
/// </summary>
private void UpdateRendering()
{
UpdateCommandListRegistration();
// The camera we cull against: the game camera while playing, otherwise the editor viewport's, so culling follows
// whatever is actually being looked at.
CameraComponent cullCamera = Game.IsPlaying ? Scene.Camera : Application.Editor?.Camera;
if (cullCamera.IsValid())
{
m_CameraPosition = cullCamera.WorldPosition;
m_CullFrustum = cullCamera.GetFrustum();
m_HasCullFrustum = true;
}
else
{
m_CameraPosition = Vector3.Zero;
m_HasCullFrustum = false;
}
if (UnderwaterPostProcessVolume.IsValid())
UnderwaterPostProcessVolume.Enabled = m_HasCullFrustum && Sample(m_CameraPosition, out var sample) && m_CameraPosition.z <= sample.Height;
UploadRipples();
UpdateExclusionVolumes();
UpdateHullExclusions();
UpdateSea();
ApplySharedAttributes(m_Attributes);
m_Attributes.Set("OceanRing", false);
UpdateRingAttributes();
}
/// <summary>
/// Keeps the command list attached to a camera that will actually replay it. Checked every frame: a scene starting
/// without a camera would never register at all, and leaving play mode destroys the play camera under us.
/// </summary>
private void UpdateCommandListRegistration()
{
var camera = GetRenderCamera();
if (camera == m_RegisteredCamera && m_RegisteredCamera.IsValid())
return;
if (m_RegisteredCamera.IsValid())
m_RegisteredCamera.RemoveCommandList(m_CommandList);
m_RegisteredCamera = null;
if (camera.IsValid())
{
camera.AddCommandList(m_CommandList, RenderStage.AfterTransparent);
m_RegisteredCamera = camera;
}
}
/// <summary>
/// The camera whose command lists are replayed: the scene's camera when it has one (in the editor viewport as well
/// as in game), otherwise the editor's.
/// </summary>
private CameraComponent GetRenderCamera()
{
if (Application.IsHeadless)
return null;
if (Scene.Camera.IsValid())
return Scene.Camera;
if (Scene.IsEditor)
return Application.Editor?.Camera;
return null;
}
private void ReleaseRendering()
{
if (m_RegisteredCamera.IsValid())
m_RegisteredCamera.RemoveCommandList(m_CommandList);
m_RegisteredCamera = null;
m_ExclusionBuffer?.Dispose();
m_ExclusionBuffer = null;
m_HullExclusionBuffer?.Dispose();
m_HullExclusionBuffer = null;
ReleaseSea();
}
/// <summary>
/// Records the water once per frame, after every update has settled the pieces and the cameras are final. The camera
/// the list is attached to replays it after the transparent pass: the water refracts whatever was drawn before it.
/// The sea goes first, so the rivers and lakes in front of it blend over it.
/// </summary>
protected override void OnPreRender()
{
m_CommandList.Reset();
m_Visible.Clear();
foreach (var piece in m_Pieces.Values)
{
if (piece.IsUploaded && IsVisible(piece.Bounds))
m_Visible.Add(piece);
}
if (m_Visible.Count == 0 && m_RingCount == 0)
return;
var material = ResolvedMaterial;
if (!material.IsValid())
return;
m_CommandList.Attributes.GrabFrameTexture("FrameBufferCopyTexture");
DrawSea(m_CommandList, material);
foreach (var piece in m_Visible)
piece.Draw(m_CommandList, material, m_Attributes);
}
/// <summary>
/// Whether a piece is worth drawing: inside the camera's view and within the render distance. Everything is drawn
/// when there is no camera to cull with.
/// </summary>
private bool IsVisible(BBox _Bounds)
{
if (!m_HasCullFrustum)
return true;
if (MaxRenderDistance > 0.0f)
{
float distanceSquared = _Bounds.ClosestPoint(m_CameraPosition).DistanceSquared(m_CameraPosition);
if (distanceSquared > MaxRenderDistance * MaxRenderDistance)
return false;
}
if (FrustumCulling && !m_CullFrustum.IsInside(_Bounds.Grow(CullPadding), partially: true))
return false;
return true;
}
/// <summary>
/// What the water shader needs this frame, on every draw: connected water is one surface.
/// </summary>
private void ApplySharedAttributes(RenderAttributes _Attributes)
{
ResolvedProfile.ApplyTo(_Attributes);
_Attributes.Set("WaterTime", Time.Now);
_Attributes.Set("FlowCycle", FlowCycle);
_Attributes.Set("DepthMax", ColorDepth);
// The texture coordinates are world units, so the tiling alone sets the density: a repeat every 100 units.
float tiling = TextureScale / 100.0f;
_Attributes.Set("NormalTiling", new Vector2(tiling, tiling));
// The surface normal is reconstructed from the waves a cell either side, which band-limits it to what the mesh
// can actually show.
_Attributes.Set("WaveNormalEps", CellSize);
ApplyRippleAttributes(_Attributes);
ApplySeaAttributes(_Attributes);
_Attributes.Set("WaterExclusionVolumeCount", m_ExclusionCount);
if (m_ExclusionBuffer.IsValid())
_Attributes.Set("WaterExclusionVolumeRows", m_ExclusionBuffer);
_Attributes.Set("WaterHullExclusionCount", m_HullExclusionCount);
if (m_HullExclusionBuffer.IsValid())
_Attributes.Set("WaterHullExclusionData", m_HullExclusionBuffer);
}
/// <summary>
/// The box volumes the water is cut away inside (an underwater base, a submarine), nearest the camera first.
/// </summary>
private void UpdateExclusionVolumes()
{
var volumes = Scene.GetAll<WaterExclusionVolume>()
.Where(volume => volume.IsValid() && volume.Active)
.OrderBy(volume => volume.WorldPosition.DistanceSquared(m_CameraPosition))
.Take(MaxExclusionVolumes)
.ToList();
m_ExclusionCount = volumes.Count;
if (volumes.Count == 0)
return;
if (!m_ExclusionBuffer.IsValid())
m_ExclusionBuffer = new GpuBuffer<Vector4>(MaxExclusionVolumes * ExclusionVolumeRows, GpuBuffer.UsageFlags.Structured);
for (int i = 0; i < volumes.Count; i++)
{
var (centre, forward, up, half) = volumes[i].GetWorldOBB();
int row = i * ExclusionVolumeRows;
m_ExclusionData[row + 0] = new Vector4(forward.x, forward.y, forward.z, half.x);
m_ExclusionData[row + 1] = new Vector4(up.x, up.y, up.z, half.y);
m_ExclusionData[row + 2] = new Vector4(centre.x, centre.y, centre.z, volumes[i].IgnoreHeight ? -half.z : half.z);
}
m_ExclusionBuffer.SetData(m_ExclusionData.AsSpan(0, volumes.Count * ExclusionVolumeRows));
}
/// <summary>
/// The hulls the water is cut away inside, mesh-exact (the inside of a boat).
/// </summary>
private void UpdateHullExclusions()
{
var hulls = Scene.GetAll<HullWaterExclusionVolume>()
.Where(hull => hull.IsValid() && hull.Active && hull.LocalTriangles.Length > 0)
.Take(MaxHullExclusions)
.ToList();
m_HullExclusionCount = 0;
if (hulls.Count == 0)
return;
if (!m_HullExclusionBuffer.IsValid())
m_HullExclusionBuffer = new GpuBuffer<Vector4>(HullExclusionMetaSize + MaxHullExclusionTriangles * 3, GpuBuffer.UsageFlags.Structured);
// Triangles go after the fixed-size block of per-hull rows.
int cursor = HullExclusionMetaSize;
foreach (var hull in hulls)
{
var triangles = hull.LocalTriangles;
if (cursor + triangles.Length > m_HullExclusionData.Length)
break;
hull.GetWorldToLocalRows(out var r0, out var r1, out var r2, out var r3);
int meta = m_HullExclusionCount * HullExclusionMetaRows;
var bounds = hull.LocalAABB;
m_HullExclusionData[meta + 0] = r0;
m_HullExclusionData[meta + 1] = r1;
m_HullExclusionData[meta + 2] = r2;
m_HullExclusionData[meta + 3] = r3;
m_HullExclusionData[meta + 4] = new Vector4(cursor, triangles.Length / 3, bounds.Mins.x, bounds.Mins.y);
m_HullExclusionData[meta + 5] = new Vector4(bounds.Mins.z, bounds.Maxs.x, bounds.Maxs.y, bounds.Maxs.z);
for (int i = 0; i < triangles.Length; i++)
m_HullExclusionData[cursor + i] = new Vector4(triangles[i].x, triangles[i].y, triangles[i].z, 0.0f);
cursor += triangles.Length;
m_HullExclusionCount++;
}
m_HullExclusionBuffer.SetData(m_HullExclusionData.AsSpan(0, cursor));
}
}