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.
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 <1-4>` 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" );
}
}