Code/OceanDebugView.cs
using System;
using System.Text;
using Sandbox.Rendering;
namespace OceanLib;
/// <summary>
/// Shows what the ocean is doing, over the game view: each cascade's tile from above - its heights, and where it folds
/// into breaking crests - the foam window round the camera, and the numbers behind the sea (the state and the blend,
/// the CPU replica's work, the spray). For tuning and checking; leave it off in a game.
/// </summary>
[Title( "Ocean Debug View" ), Category( "Ocean" ), Icon( "bug_report" )]
public sealed class OceanDebugView : Component
{
/// <summary>The sea to show. Empty takes the one on this object, else the scene's.</summary>
[Property] public Ocean Ocean { get; set; }
/// <summary>Each cascade's heights, longest waves first.</summary>
[Property] public bool ShowHeights { get; set; } = true;
/// <summary>Each cascade's folding: white where the surface bunches up, red where it folds over and breaks.</summary>
[Property] public bool ShowFolding { get; set; } = true;
/// <summary>The persistent foam window, centred on the camera.</summary>
[Property] public bool ShowFoam { get; set; } = true;
[Property] public bool ShowStats { get; set; } = true;
/// <summary>Pixels across each picture.</summary>
[Property, Range( 64, 512 )] public int PanelSize { get; set; } = 180;
/// <summary>Where the top left of the view sits on screen, in pixels.</summary>
[Property] public Vector2 Position { get; set; } = new( 16, 16 );
private const int Size = 256;
private const int Panels = Ocean.CascadeCount * 2 + 1;
private ComputeShader m_Shader;
private CameraCommands m_Commands;
private readonly Texture[] m_Panels = new Texture[Panels];
private readonly float[] m_Ranges = new float[Ocean.CascadeCount];
private readonly Dictionary<Ocean.SpectrumSet, float[]> m_Deviations = new();
private static readonly ProfilingSampler s_Profile = new( "Ocean Debug View" );
private Ocean Sea => Ocean.IsValid() ? Ocean : Components.Get<Ocean>() ?? Ocean.Current;
protected override void OnEnabled()
{
m_Shader = new ComputeShader( "ocean/shaders/ocean_debug_cs.shader" );
// After the FFT and the foam, on the same stage.
m_Commands = new CameraCommands( "Ocean Debug View", Stage.AfterUI, 2 );
for ( int i = 0; i < Panels; i++ )
m_Panels[i] = Texture.Create( Size, Size, ImageFormat.RGBA8888 ).WithUAVBinding().WithName( $"Ocean Debug {i}" ).Finish();
}
protected override void OnDisabled()
{
m_Commands?.Dispose();
m_Commands = null;
}
protected override void OnUpdate()
{
var ocean = Sea;
if ( m_Commands is null || !ocean.IsValid() || ocean.Cascades.Length == 0 )
return;
m_Commands.Follow( Scene );
Record( ocean );
Draw( ocean );
}
/// <summary>
/// Two standard deviations of each cascade's height as the sea is now: what its picture shows as full white or full
/// blue.
/// </summary>
private void UpdateRanges( Ocean ocean )
{
var from = Deviations( ocean.From );
var to = Deviations( ocean.To );
for ( int i = 0; i < m_Ranges.Length; i++ )
m_Ranges[i] = 2.0f * MathX.Lerp( from[i], to[i], ocean.Blend );
}
private float[] Deviations( Ocean.SpectrumSet set )
{
if ( m_Deviations.TryGetValue( set, out var deviations ) )
return deviations;
deviations = new float[Ocean.CascadeCount];
for ( int i = 0; i < deviations.Length; i++ )
{
double sum = 0.0;
foreach ( var t in set.Spectra[i].Texels )
sum += t.x * t.x + t.y * t.y;
deviations[i] = MathF.Sqrt( (float)(2.0 * sum) );
}
return m_Deviations[set] = deviations;
}
private void Record( Ocean ocean )
{
UpdateRanges( ocean );
var cl = m_Commands.List;
cl.Reset();
using ( cl.ProfileScope( s_Profile ) )
{
cl.Attributes.Set( "DebugSize", Size );
for ( int i = 0; i < Ocean.CascadeCount; i++ )
{
cl.Attributes.Set( "DebugDisplacement", ocean.Cascades[i].Displacement );
cl.Attributes.Set( "DebugDerivatives", ocean.Cascades[i].Derivatives );
cl.Attributes.Set( "DebugRange", m_Ranges[i] );
Panel( cl, i, 0.0f );
Panel( cl, Ocean.CascadeCount + i, 1.0f );
}
var foam = Scene.GetAllComponents<OceanFoam>().FirstOrDefault();
if ( foam.IsValid() && foam.Current is not null )
{
cl.Attributes.Set( "DebugFoam", foam.Current );
Panel( cl, Panels - 1, 2.0f );
}
}
}
private void Panel( CommandList cl, int index, float mode )
{
cl.Attributes.Set( "DebugMode", mode );
cl.Attributes.Set( "DebugOut", m_Panels[index] );
cl.DispatchCompute( m_Shader, Size, Size, 1 );
cl.UavBarrier( m_Panels[index] );
}
private void Draw( Ocean ocean )
{
var camera = m_Commands.Camera;
if ( !camera.IsValid() )
return;
var hud = camera.BeginHud();
try
{
DrawPanels( ref hud, ocean );
}
finally
{
hud.Dispose();
}
}
private void DrawPanels( ref Painter hud, Ocean ocean )
{
int row = 0;
string Tile( int i ) => $"{ocean.LengthOf( i ):0.#} m";
if ( ShowHeights )
Row( ref hud, row++, "Heights", 0, Ocean.CascadeCount, i => $"{Tile( i )} ±{m_Ranges[i]:0.##} m" );
if ( ShowFolding )
Row( ref hud, row++, "Folding (Jacobian)", Ocean.CascadeCount, Ocean.CascadeCount, Tile );
var foam = Scene.GetAllComponents<OceanFoam>().FirstOrDefault();
if ( ShowFoam && foam.IsValid() )
Row( ref hud, row++, "Foam window", Panels - 1, 1, _ => $"{foam.Extent:0} m round the camera" );
if ( ShowStats )
{
var sea = ocean.Sea;
var waves = ocean.Waves;
var text = new StringBuilder()
.AppendLine( $"Sea: wind {sea.WindSpeed:0.#} m/s · Hs {sea.SignificantHeight:0.##} m · choppiness {sea.Choppiness:0.##} · scatter {sea.Scatter:0.##} · crest foam {sea.CrestFoam:0.##}" )
.AppendLine( $"Blend: {ocean.Blend:P0} from Hs {ocean.From.Sea.SignificantHeight:0.##} m to {ocean.To.Sea.SignificantHeight:0.##} m · time {ocean.OceanTime:0.0} / {ocean.LoopPeriod:0} s" )
.AppendLine( waves is null ? "CPU replica: none" : $"CPU replica: {waves.Count} waves on {waves.GridSize}² · grids built {waves.BackgroundBuilds} in background, {waves.SynchronousBuilds} waiting" )
.AppendLine( $"Spray: {OceanSpray.Current?.Count ?? 0} particles · foam sources: {Scene.GetAllComponents<OceanFoamSource>().Count()}" )
.ToString();
Text( ref hud, text, 13, new Rect( Position.x, RowTop( row ), 1200, 100 ) );
}
}
private const float Gap = 8.0f;
private const float TitleHeight = 18.0f;
private float RowTop( int row ) => Position.y + row * (PanelSize + Gap + TitleHeight);
private void Row( ref Painter hud, int row, string title, int first, int count, Func<int, string> label )
{
float y = RowTop( row );
Text( ref hud, title, 13, new Rect( Position.x, y, 600, TitleHeight ) );
for ( int i = 0; i < count; i++ )
{
var rect = new Rect( Position.x + i * (PanelSize + Gap), y + TitleHeight, PanelSize, PanelSize );
hud.Texture( m_Panels[first + i], rect, null );
Text( ref hud, label( i ), 11, rect.Shrink( 4.0f ) );
}
}
private static void Text( ref Painter hud, string text, float size, Rect rect )
{
hud.TextStyle = TextStyle.Default with { FontSize = size, Color = Color.White, Alignment = TextFlag.LeftTop };
hud.Text( text, rect );
}
}