Effects/WaterReflection.cs

A component that renders a planar water reflection by recording a mirrored scene render into a render target and supplying it to the water shader. It records a CommandList for the main camera at a downscaled resolution, sets deprecated reflection attributes the shader reads, and attaches the list to run after the depth prepass; it also exposes a console command to toggle or add the effect to the lake model.

Native InteropFile AccessNetworking
using Sandbox;
using Sandbox.Rendering;
using System.Linq;

namespace NZombies;

/// <summary>
/// A planar reflection for water drawn with the engine's `water_simple.shader` (Laketown's lake,
/// 2026-10-02). Put it on the water's object: the surface is that object's height.
///
/// ⚠️ THE SHADER REFLECTS ONLY WHAT IT IS HANDED. It reads "ReflectionTexture" when
/// "HasReflectionTexture" is set, and nothing in the engine sets either, so without this the water
/// is lit shading alone: a flat sheet from any distance. This renders the main camera mirrored
/// about the surface (`CommandList.DrawReflection`, the engine's own mirror maths and oblique clip)
/// after the depth prepass, before the water draws, and passes the result down the view.
///
/// ⚠️ IT COSTS A SECOND RENDER OF THE SCENE, at 1/<see cref="Downscale"/> of the screen.
/// `nz_water_reflect` turns it off, or resizes it, live.
/// </summary>
public sealed class WaterReflection : Component
{
	/// <summary>The reflection's size: the screen divided by this (2 = half size).</summary>
	[Property, Range( 1, 4 )] public int Downscale { get; set; } = 2;

	/// <summary>How far under the surface the mirrored view is clipped, so the waterline isn't cut away.</summary>
	[Property] public float ClipOffset { get; set; } = 4f;

	CommandList _cmd;
	CameraComponent _camera;
	int _builtDownscale;
	float _builtZ;

	protected override void OnDisabled() => Detach();

	protected override void OnUpdate()
	{
		// THE LIST FOLLOWS THE MAIN CAMERA (a respawn or a spectator view swaps it), and is
		// recorded again when the size or the surface height changes.
		var cam = Scene.Camera;
		var z = WorldPosition.z;
		if ( cam == _camera && _cmd is not null && _builtDownscale == Downscale && _builtZ == z ) return;

		Detach();
		if ( !cam.IsValid() ) return;

		_cmd = Record( cam, z );
		_camera = cam;
		_builtDownscale = Downscale;
		_builtZ = z;
		_camera.AddCommandList( _cmd, Stage.AfterDepthPrepass, 0 );
	}

	CommandList Record( CameraComponent cam, float z )
	{
		var cmd = new CommandList( "nz water reflection" );
		var rt = cmd.GetRenderTarget( "nz_water_reflection", System.Math.Clamp( Downscale, 1, 4 ),
			ImageFormat.RGBA16161616F, ImageFormat.D24S8, MultisampleAmount.MultisampleNone, 1 );

		// The mirrored view is the scene's own HDR colour: no post-processing (the main view
		// tonemaps it once, as part of the water) and no UI.
		var setup = new ReflectionSetup { ClipOffset = ClipOffset, FallbackColor = Color.Black };
		setup.ViewSetup.EnablePostprocessing = false;
		setup.ViewSetup.EnableUI = false;

		cmd.DrawReflection( cam, new Plane( new Vector3( 0f, 0f, z ), Vector3.Up ), in rt, setup );

		// ⚠️ THE DEPRECATED VIEW ATTRIBUTES, KNOWINGLY (CS0618 silenced here, 2026-10-05). `water_simple.shader` reads
		// "ReflectionTexture" and "HasReflectionTexture" as attributes, and an attribute reaches the water's draw only from its
		// material, its object, or the view. The two replacements the warning names don't reach it:
		// - `SetPipelineTexture( PipelineTextureSlot.Reflections )` is the engine's screen-space reflection slot. The water
		//   shader never reads it, and every material with SSR does, so the lake's mirror image would show on all of them.
		// - `cmd.Attributes` belongs to this command list's own scope, not to the scene pass that draws the water.
		// Revisit when the engine gives views a replacement, or bind a persistent texture on the water's own object.
#pragma warning disable CS0618
		cmd.GlobalAttributes.Set( "ReflectionTexture", rt.ColorTexture );
		cmd.GlobalAttributes.Set( "HasReflectionTexture", true );
#pragma warning restore CS0618

		// ⚠️ BACK TO THE POOL IN THE SAME LIST. A temporary held by name is replaced, not freed,
		// when the list runs again, so one never released here is a texture lost every frame.
		cmd.ReleaseRenderTarget( rt );
		return cmd;
	}

	void Detach()
	{
		if ( _camera.IsValid() && _cmd is not null )
			_camera.RemoveCommandList( _cmd );

		_cmd = null;
		_camera = null;
	}

	/// <summary>
	/// `nz_water_reflect` — the reflecting water on this map. `on|off` (on adds one to the lake if the
	/// map has none); `size &lt;1-4&gt;` sets the reflection to 1/size of the screen. Live only: the
	/// map scene keeps its own.
	/// </summary>
	[ConCmd( "nz_water_reflect" )]
	public static void Command( string part = "", int value = 0 )
	{
		var scene = Game.ActiveScene;
		if ( scene is null ) return;

		var all = scene.Components.GetAll<WaterReflection>( FindMode.EverythingInSelfAndDescendants ).ToList();
		var verb = part.ToLowerInvariant();

		if ( verb == "on" && all.Count == 0 )
		{
			// The lake: whatever draws Laketown's lake plane (models/maps/laketown/lake_plane.vmdl).
			var lake = scene.GetAllComponents<ModelRenderer>()
				.FirstOrDefault( r => r.Model?.ResourcePath?.EndsWith( "lake_plane.vmdl" ) == true );
			if ( !lake.IsValid() )
			{
				Log.Warning( "[nz-water] no lake on this map to reflect in" );
				return;
			}

			all.Add( lake.GameObject.Components.Create<WaterReflection>() );
		}

		if ( all.Count == 0 )
		{
			Log.Info( "[nz-water] no reflecting water on this map - nz_water_reflect on adds it to the lake" );
			return;
		}

		foreach ( var w in all )
		{
			switch ( verb )
			{
				case "": break;
				case "on": w.Enabled = true; break;
				case "off": w.Enabled = false; break;
				case "size": w.Downscale = System.Math.Clamp( value, 1, 4 ); break;
				default:
					Log.Warning( $"[nz-water] no such part '{part}' - on, off, size" );
					return;
			}

			Log.Info( $"[nz-water] '{w.GameObject.Name}' at z {w.WorldPosition.z:0}  {( w.Enabled ? "reflecting" : "OFF" )}"
				+ $"  1/{w.Downscale} of the screen  on camera '{( w._camera.IsValid() ? w._camera.GameObject.Name : "none" )}'" );
		}


		if ( verb != "" )
			Log.Info( "[nz-water] live only - the map scene keeps its own" );
	}
}