Code/Rendering/WaterPiece.cs

Defines water-related types and a WaterPiece class that holds CPU mesh data and uploads/draws it on the GPU. It defines WaterPieceKey, a GPU vertex struct (WaterVertex), and WaterPiece which converts mesh arrays into GPU buffers, tracks bounds and depth, and provides Upload, Release and Draw methods.

Native InteropFile Access
using System;
using System.Runtime.InteropServices;
using Sandbox;
using Sandbox.Rendering;

namespace RedSnail.WaterTool2;

public enum WaterPieceKind
{
	/// <summary>
	/// A stretch of a river.
	/// </summary>
	River,

	/// <summary>
	/// The water where rivers meet at a node.
	/// </summary>
	Junction,

	/// <summary>
	/// A tile of a lake.
	/// </summary>
	Lake,

	/// <summary>
	/// Where a river opens into the sea: its block of the coast zone.
	/// </summary>
	Estuary,

	/// <summary>
	/// A chunk of the coast zone: the sea's water round the land.
	/// </summary>
	Coast
}

/// <summary>
/// What a piece of water is: which river, node or lake it belongs to, and which of that owner's stretches or tiles.
/// </summary>
internal readonly record struct WaterPieceKey(WaterPieceKind Kind, int Owner, int Index);

/// <summary>
/// The vertex the water shader reads: the rest position, in the colour the water's state there (its flow in x and y,
/// in units per second, its wave strength and its foam), and in the first texture coordinate how far the water has
/// handed over to the sea (0 its own, 1 exactly the sea's surface). The sea's rings use the position as a grid index.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal 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;
}

/// <summary>
/// One generated piece of water: its mesh on the CPU (which gameplay queries) and, wherever something renders, the
/// same mesh on the GPU. Pieces are not saved; the network builds them from the graph, and rebuilds one only when the
/// inputs it was built from (its signature) change.
/// </summary>
internal sealed class WaterPiece
{
	public WaterPieceKey Key;
	public long Signature;
	public WaterMeshData Mesh;

	/// <summary>
	/// How deep the water is under this piece, for gameplay.
	/// </summary>
	public float Depth;

	/// <summary>
	/// The piece's world bounds, down to its bed: what it is culled with.
	/// </summary>
	public BBox Bounds;

	private GpuBuffer<WaterVertex> m_VertexBuffer;
	private GpuBuffer<uint> m_IndexBuffer;
	private int m_IndexCount;

	public bool IsUploaded => m_VertexBuffer.IsValid() && m_IndexBuffer.IsValid() && m_IndexCount > 0;



	public WaterPiece(WaterPieceKey _Key, long _Signature, WaterMeshData _Mesh, float _Depth)
	{
		Key = _Key;
		Signature = _Signature;
		Mesh = _Mesh;
		Depth = _Depth;

		var bounds = _Mesh.Bounds;
		Bounds = new BBox(bounds.Mins - new Vector3(0.0f, 0.0f, _Depth), bounds.Maxs);
	}



	/// <summary>
	/// Puts the mesh on the GPU. Main thread only.
	/// </summary>
	public void Upload()
	{
		Release();

		int count = Mesh.VertexCount;

		if (count == 0 || Mesh.Indices is null || Mesh.Indices.Length == 0)
			return;

		var vertices = new WaterVertex[count];

		for (int i = 0; i < count; i++)
		{
			Vector3 position = Mesh.Positions[i];
			Vector2 velocity = Mesh.Velocities[i];

			vertices[i] = new WaterVertex
			{
				Position = position,
				Normal = Vector3.Up,
				Tangent = new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
				TexCoord = new Vector2(Mesh.SeaBlend[i], 0.0f),
				Color = new Color(velocity.x, velocity.y, Mesh.Waves[i], Mesh.Foams[i])
			};
		}

		var indices = new uint[Mesh.Indices.Length];

		for (int i = 0; i < indices.Length; i++)
			indices[i] = (uint)Mesh.Indices[i];

		m_VertexBuffer = new GpuBuffer<WaterVertex>(count, GpuBuffer.UsageFlags.Vertex);
		m_VertexBuffer.SetData(vertices);

		m_IndexBuffer = new GpuBuffer<uint>(indices.Length, GpuBuffer.UsageFlags.Index);
		m_IndexBuffer.SetData(indices);

		m_IndexCount = indices.Length;
	}



	public void Release()
	{
		m_VertexBuffer?.Dispose();
		m_VertexBuffer = null;

		m_IndexBuffer?.Dispose();
		m_IndexBuffer = null;

		m_IndexCount = 0;
	}



	public void Draw(CommandList _CommandList, Material _Material, RenderAttributes _Attributes)
	{
		if (!IsUploaded)
			return;

		_CommandList.DrawIndexed(m_VertexBuffer, m_IndexBuffer, _Material, 0, m_IndexCount, _Attributes);
	}
}