{"TotalCount":27,"Files":[{"Ident":"schumi.ocean","Path":"Code/CameraCommands.cs","FileName":"CameraCommands.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// A command list kept on whichever camera the scene renders with, moving to the new one when the scene\u0027s camera\n/// changes. Call \u003Csee cref=\u0022Follow\u0022/\u003E every frame and \u003Csee cref=\u0022Dispose\u0022/\u003E when done.\n/// \u003C/summary\u003E\ninternal sealed class CameraCommands\n{\n\tpublic CommandList List { get; }\n\tpublic CameraComponent Camera { get; private set; }\n\n\tprivate readonly Stage m_Stage;\n\tprivate readonly int m_Order;\n\n\tpublic CameraCommands( string name, Stage stage, int order )\n\t{\n\t\tList = new CommandList( name );\n\t\tm_Stage = stage;\n\t\tm_Order = order;\n\t}\n\n\tpublic void Follow( Scene scene )\n\t{\n\t\tvar camera = scene.Camera;\n\n\t\tif ( camera == Camera )\n\t\t\treturn;\n\n\t\tif ( Camera.IsValid() )\n\t\t\tCamera.RemoveCommandList( List );\n\n\t\tCamera = camera;\n\t\tCamera?.AddCommandList( List, m_Stage, m_Order );\n\t}\n\n\tpublic void Dispose()\n\t{\n\t\tif ( Camera.IsValid() )\n\t\t\tCamera.RemoveCommandList( List );\n\n\t\tCamera = null;\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanSurface.cs","FileName":"OceanSurface.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Draws the ocean an \u003Csee cref=\u0022Ocean\u0022/\u003E simulates, as a clipmap: rings of grid around the camera, each twice as\n/// coarse and twice as wide as the one inside, out to the horizon. Every ring sits on a grid fixed in the world - its\n/// centre snapped to two of its cells - so the waves stay put under a moving camera instead of swimming. Two meshes do\n/// all the rings: a full square for the innermost, and a square with a hole for the rest; the shader scales them,\n/// pushes them into waves, folds each ring\u0027s edge onto the next ring\u0027s grid and clips the overlap between them.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Surface\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022grid_on\u0022 )]\npublic sealed class OceanSurface : Component\n{\n\t/// \u003Csummary\u003EThe sea to draw. Empty takes the one on this object, else the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003EA material with the ocean surface shader. Empty uses the library\u0027s.\u003C/summary\u003E\n\t[Property] public Material Material { get; set; }\n\n\t/// \u003Csummary\u003EThe persistent foam to draw. Empty takes the scene\u0027s first.\u003C/summary\u003E\n\t[Property] public OceanFoam Foam { get; set; }\n\n\t/// \u003Csummary\u003EMetres across one bubble of the foam\u0027s pattern.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0.1f, 5 )] public float FoamBubbleSize { get; set; } = 1.0f;\n\n\t[Property, Group( \u0022Look\u0022 )] public Color FoamColor { get; set; } = new( 0.82f, 0.88f, 0.88f );\n\n\t/// \u003Csummary\u003EMetres between vertices in the innermost ring.\u003C/summary\u003E\n\t[Property, Group( \u0022Rings\u0022 ), Range( 0.1f, 4 )] public float CellSize { get; set; } = 0.5f;\n\n\t/// \u003Csummary\u003ECells across each ring. A multiple of four, so every ring\u0027s edge falls on the next one\u0027s grid.\u003C/summary\u003E\n\t[Property, Group( \u0022Rings\u0022 ), Range( 32, 256 )] public int GridCells { get; set; } = 192;\n\n\t/// \u003Csummary\u003EHow many rings: each doubles the distance the sea reaches.\u003C/summary\u003E\n\t[Property, Group( \u0022Rings\u0022 ), Range( 1, 12 )] public int Levels { get; set; } = 8;\n\n\t/// \u003Csummary\u003EThe outer share of each ring over which its vertices fold onto the coarser grid outside.\u003C/summary\u003E\n\t[Property, Group( \u0022Rings\u0022 ), Range( 0.05f, 0.5f )] public float MorphBand { get; set; } = 0.2f;\n\n\t/// \u003Csummary\u003EMetres from the camera by which each cascade\u0027s waves have faded out of the geometry, longest first.\u003C/summary\u003E\n\t[Property, Group( \u0022Rings\u0022 )] public Vector3 CascadeFade { get; set; } = new( 100000.0f, 1200.0f, 180.0f );\n\n\t/// \u003Csummary\u003EWhere the crests count as folding for the foam tint, as a Jacobian.\u003C/summary\u003E\n\t[Property, Range( -1, 1 )] public float FoamThreshold { get; set; } = 0.4f;\n\n\t/// \u003Csummary\u003EThe sun the water scatters and glints. Empty takes the scene\u0027s first directional light.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 )] public DirectionalLight Sun { get; set; }\n\n\t/// \u003Csummary\u003EThe colour of deep water, where no light comes back out of it.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 )] public Color DeepColor { get; set; } = new( 0.006f, 0.04f, 0.075f );\n\n\t/// \u003Csummary\u003EThe turquoise of light scattering out of the crests.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 )] public Color ScatterColor { get; set; } = new( 0.03f, 0.42f, 0.38f );\n\n\t/// \u003Csummary\u003EHow high a crest stands, in metres, before it glows its fullest.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0.2f, 10 )] public float ScatterHeight { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EHow wide the sun\u0027s glare spreads over the waves: 0 a tight glint, 1 a broad band of light.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0, 1 )] public float SunSpread { get; set; } = 0.75f;\n\n\t/// \u003Csummary\u003EThe soft halo of light round the glare.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0, 2 )] public float SunGlow { get; set; } = 0.25f;\n\n\t/// \u003Csummary\u003EDrops of rain landing on the water and the rings they spread: 0 dry, 1 a downpour. For weather to drive.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0, 2 )] public float RainRipples { get; set; } = 0.0f;\n\n\t/// \u003Csummary\u003EHow rough the surface is to the sky\u0027s reflection.\u003C/summary\u003E\n\t[Property, Group( \u0022Look\u0022 ), Range( 0, 0.5f )] public float Roughness { get; set; } = 0.06f;\n\n\t// The hull masks for the shader, one buffer: a fixed block of six rows per shape - three rows of its world-to-model\n\t// transform, (first plane, plane count, first triangle, triangle count), its box\u0027s low and high corners - then the\n\t// planes and the triangles\u0027 points they index.\n\tprivate const int MaxHullShapes = 16;\n\tprivate const int HullShapeRows = 6;\n\tprivate const int MaxHullData = 8192;\n\tprivate GpuBuffer\u003CVector4\u003E m_HullMasks;\n\tprivate readonly Vector4[] m_HullMaskData = new Vector4[MaxHullShapes * HullShapeRows \u002B MaxHullData];\n\n\tprivate readonly List\u003CSceneObject\u003E m_Rings = new();\n\n\t/// \u003Csummary\u003EPacks every hull the sea is kept out of for the shader; returns how many shapes.\u003C/summary\u003E\n\tprivate int GatherHullMasks()\n\t{\n\t\tint shapes = 0;\n\t\tint cursor = MaxHullShapes * HullShapeRows;\n\n\t\tvoid Add( (Vector4, Vector4, Vector4) rows, BBox box, int planeStart, int planes, int triStart, int tris )\n\t\t{\n\t\t\tint meta = shapes * HullShapeRows;\n\t\t\tm_HullMaskData[meta \u002B 0] = rows.Item1;\n\t\t\tm_HullMaskData[meta \u002B 1] = rows.Item2;\n\t\t\tm_HullMaskData[meta \u002B 2] = rows.Item3;\n\t\t\tm_HullMaskData[meta \u002B 3] = new Vector4( planeStart, planes, triStart, tris );\n\t\t\tm_HullMaskData[meta \u002B 4] = new Vector4( box.Mins, 0 );\n\t\t\tm_HullMaskData[meta \u002B 5] = new Vector4( box.Maxs, 0 );\n\t\t\tshapes\u002B\u002B;\n\t\t}\n\n\t\tforeach ( var mask in Scene.GetAllComponents\u003COceanHullMask\u003E() )\n\t\t{\n\t\t\tif ( mask.IsEmpty )\n\t\t\t\tcontinue;\n\n\t\t\tvar rows = mask.WorldToLocalRows();\n\n\t\t\t// Each convex part is its own shape, tested against its planes.\n\t\t\tforeach ( var (start, count) in mask.ConvexParts )\n\t\t\t{\n\t\t\t\tif ( shapes \u003E= MaxHullShapes || cursor \u002B count \u003E m_HullMaskData.Length )\n\t\t\t\t\tbreak;\n\n\t\t\t\tfor ( int i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t\t\tm_HullMaskData[cursor \u002B i] = mask.Planes[start \u002B i];\n\n\t\t\t\tAdd( rows, mask.LocalBounds, cursor, count, 0, 0 );\n\t\t\t\tcursor \u002B= count;\n\t\t\t}\n\n\t\t\t// The rest, if any, as triangles for the crossing count.\n\t\t\tint points = mask.Triangles.Count;\n\n\t\t\tif ( points \u003E 0 \u0026\u0026 shapes \u003C MaxHullShapes \u0026\u0026 cursor \u002B points \u003C= m_HullMaskData.Length )\n\t\t\t{\n\t\t\t\tfor ( int i = 0; i \u003C points; i\u002B\u002B )\n\t\t\t\t\tm_HullMaskData[cursor \u002B i] = new Vector4( mask.Triangles[i], 0 );\n\n\t\t\t\tAdd( rows, mask.LocalBounds, 0, 0, cursor, points / 3 );\n\t\t\t\tcursor \u002B= points;\n\t\t\t}\n\t\t}\n\n\t\tif ( shapes == 0 )\n\t\t\treturn 0;\n\n\t\tm_HullMasks ??= new GpuBuffer\u003CVector4\u003E( m_HullMaskData.Length, GpuBuffer.UsageFlags.Structured, \u0022Ocean Hull Masks\u0022 );\n\t\tm_HullMasks.SetData( m_HullMaskData.AsSpan( 0, cursor ), 0 );\n\t\treturn shapes;\n\t}\n\tprivate (float, int, int, Material) m_Built;\n\tprivate Material m_Material;\n\n\t/// \u003Csummary\u003EHalf the width of a ring, in world units.\u003C/summary\u003E\n\tpublic float RingHalfWidth( int level ) =\u003E GridCells / 2 * LevelCellSize( level );\n\n\tprivate float LevelCellSize( int level ) =\u003E CellSize * Ocean.UnitsPerMetre * (1 \u003C\u003C level);\n\n\tprotected override void OnEnabled()\n\t{\n\t\tBuild();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tClear();\n\t}\n\n\tprivate void Clear()\n\t{\n\t\tforeach ( var ring in m_Rings )\n\t\t\tring.Delete();\n\n\t\tm_Rings.Clear();\n\t}\n\n\tprivate void Build()\n\t{\n\t\tClear();\n\n\t\tint cells = Math.Max( 4, GridCells / 4 * 4 );\n\t\tm_Built = (CellSize, cells, Levels, Material);\n\t\tm_Material = Material ?? Material.Load( \u0022ocean/materials/ocean_surface.vmat\u0022 );\n\n\t\tif ( m_Material is null )\n\t\t\treturn;\n\n\t\tvar full = BuildGrid( cells, 0 );\n\n\t\t// The finer ring inside is half this one\u0027s width, but its centre may sit up to a cell and a half off this\n\t\t// one\u0027s: the hole leaves two cells of margin on every side, and the shader clips what overlaps.\n\t\tvar ring = BuildGrid( cells, cells / 2 - 4 );\n\n\t\tfor ( int level = 0; level \u003C Levels; level\u002B\u002B )\n\t\t{\n\t\t\tvar so = new SceneObject( Scene.SceneWorld, level == 0 ? full : ring, global::Transform.Zero );\n\t\t\tso.Flags.CastShadows = false;\n\t\t\tm_Rings.Add( so );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA square of cells about the origin, positions in cells, with a square hole of the given width in the middle.\u003C/summary\u003E\n\tprivate Model BuildGrid( int cells, int hole )\n\t{\n\t\tvar vertices = new List\u003CVertex\u003E( (cells \u002B 1) * (cells \u002B 1) );\n\t\tvar indices = new List\u003Cint\u003E( cells * cells * 6 );\n\t\tint half = cells / 2;\n\n\t\tfor ( int y = 0; y \u003C= cells; y\u002B\u002B )\n\t\t\tfor ( int x = 0; x \u003C= cells; x\u002B\u002B )\n\t\t\t\tvertices.Add( new Vertex( new Vector3( x - half, y - half, 0 ), Vector3.Up, Vector3.Forward, Vector4.Zero ) );\n\n\t\tint holeLow = half - hole / 2;\n\t\tint holeHigh = half \u002B hole / 2;\n\n\t\tfor ( int y = 0; y \u003C cells; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C cells; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( hole \u003E 0 \u0026\u0026 x \u003E= holeLow \u0026\u0026 x \u003C holeHigh \u0026\u0026 y \u003E= holeLow \u0026\u0026 y \u003C holeHigh )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tint a = y * (cells \u002B 1) \u002B x;\n\t\t\t\tint b = a \u002B cells \u002B 1;\n\t\t\t\tindices.AddRange( new[] { a, a \u002B 1, b, a \u002B 1, b \u002B 1, b } );\n\t\t\t}\n\t\t}\n\n\t\tvar mesh = new Mesh( m_Material );\n\t\tmesh.CreateVertexBuffer( vertices.Count, vertices );\n\t\tmesh.CreateIndexBuffer( indices.Count, indices );\n\n\t\t// The shader puts the grid wherever the camera is: never cull it.\n\t\tmesh.Bounds = new BBox( new Vector3( -1e7f, -1e7f, -1e5f ), new Vector3( 1e7f, 1e7f, 1e5f ) );\n\t\treturn Model.Builder.AddMesh( mesh ).Create();\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( m_Built != (CellSize, Math.Max( 4, GridCells / 4 * 4 ), Levels, Material) )\n\t\t\tBuild();\n\n\t\tvar ocean = Ocean.IsValid() ? Ocean : Components.Get\u003COcean\u003E() ?? Ocean.Current;\n\n\t\tif ( m_Rings.Count == 0 || !ocean.IsValid() || ocean.Cascades.Length == 0 )\n\t\t\treturn;\n\n\t\tvar camera = Scene.Camera;\n\t\tvar eye = camera.IsValid() ? camera.WorldPosition : WorldPosition;\n\t\tint cells = m_Built.Item2;\n\t\tvar fade = CascadeFade * Ocean.UnitsPerMetre;\n\t\tVector2 innerCenter = default;\n\t\tfloat innerHalf = 0.0f;\n\n\t\tvar sun = Sun.IsValid() ? Sun : Scene.GetAllComponents\u003CDirectionalLight\u003E().FirstOrDefault();\n\t\tvar toSun = sun.IsValid() ? -sun.WorldRotation.Forward : new Vector3( 0.9f, 0.0f, 0.42f ).Normal;\n\t\tvar sunColor = sun.IsValid() ? sun.LightColor : Color.White;\n\t\tvar foam = Foam.IsValid() ? Foam : Scene.GetAllComponents\u003COceanFoam\u003E().FirstOrDefault();\n\t\tint masks = GatherHullMasks();\n\n\t\tfor ( int level = 0; level \u003C m_Rings.Count; level\u002B\u002B )\n\t\t{\n\t\t\tvar ring = m_Rings[level];\n\t\t\tfloat cell = LevelCellSize( level );\n\n\t\t\t// On the grid two of this ring\u0027s cells apart, so its even vertices are the next ring\u0027s vertices.\n\t\t\tfloat snap = cell * 2.0f;\n\t\t\tvar center = new Vector2( MathF.Round( eye.x / snap ) * snap, MathF.Round( eye.y / snap ) * snap );\n\n\t\t\tvar attributes = ring.Attributes;\n\t\t\tocean.Bind( attributes );\n\t\t\tattributes.Set( \u0022LevelCellSize\u0022, cell );\n\t\t\tattributes.Set( \u0022LevelCenter\u0022, center );\n\t\t\tattributes.Set( \u0022LevelGridHalf\u0022, cells / 2.0f );\n\t\t\tattributes.Set( \u0022LevelMorphStart\u0022, cells / 2.0f * (1.0f - MorphBand) );\n\t\t\tattributes.Set( \u0022LevelInnerCenter\u0022, innerCenter );\n\t\t\tattributes.Set( \u0022LevelInnerHalf\u0022, innerHalf );\n\n\t\t\t// The nearest any of this ring\u0027s pixels can be is the edge of the ring inside, give or take its snapping;\n\t\t\t// a cascade whose shading has faded out by then isn\u0027t fetched for this ring at all.\n\t\t\tfloat nearest = MathF.Max( innerHalf - cell, 0.0f );\n\t\t\tattributes.Set( \u0022LevelCascades\u0022, new Vector3(\n\t\t\t\tnearest \u003C fade.x * 4.0f ? 1 : 0,\n\t\t\t\tnearest \u003C fade.y * 4.0f ? 1 : 0,\n\t\t\t\tnearest \u003C fade.z * 4.0f ? 1 : 0 ) );\n\t\t\tattributes.Set( \u0022OceanFadeDistances\u0022, fade );\n\t\t\tattributes.Set( \u0022OceanCameraXY\u0022, new Vector2( eye.x, eye.y ) );\n\t\t\tattributes.Set( \u0022OceanFoamThreshold\u0022, FoamThreshold );\n\t\t\tattributes.Set( \u0022OceanSunDirection\u0022, toSun );\n\t\t\tattributes.Set( \u0022OceanSunColor\u0022, new Vector3( sunColor.r, sunColor.g, sunColor.b ) );\n\t\t\tattributes.Set( \u0022OceanDeepColor\u0022, new Vector3( DeepColor.r, DeepColor.g, DeepColor.b ) );\n\t\t\tattributes.Set( \u0022OceanScatterColor\u0022, new Vector3( ScatterColor.r, ScatterColor.g, ScatterColor.b ) );\n\t\t\tattributes.Set( \u0022OceanScatterStrength\u0022, ocean.Sea.Scatter );\n\t\t\tattributes.Set( \u0022OceanScatterHeight\u0022, ScatterHeight );\n\t\t\tattributes.Set( \u0022OceanSunSpread\u0022, SunSpread );\n\t\t\tattributes.Set( \u0022OceanSunGlow\u0022, SunGlow );\n\t\t\tattributes.Set( \u0022OceanRoughness\u0022, Roughness );\n\t\t\tattributes.Set( \u0022OceanRainRipples\u0022, RainRipples );\n\t\t\tattributes.Set( \u0022OceanFoamBubbleScale\u0022, FoamBubbleSize );\n\t\t\tattributes.Set( \u0022OceanFoamColor\u0022, new Vector3( FoamColor.r, FoamColor.g, FoamColor.b ) );\n\n\t\t\tattributes.Set( \u0022OceanHullMaskCount\u0022, (float)masks );\n\t\t\tif ( masks \u003E 0 )\n\t\t\t\tattributes.Set( \u0022OceanHullMasks\u0022, m_HullMasks );\n\n\t\t\tif ( foam.IsValid() \u0026\u0026 foam.Current is not null )\n\t\t\t\tfoam.Bind( attributes );\n\t\t\telse\n\t\t\t\tattributes.Set( \u0022OceanFoamExtent\u0022, 0.0f );\n\n\t\t\tinnerCenter = center;\n\t\t\tinnerHalf = cells / 2.0f * cell;\n\t\t}\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/Ocean.cs","FileName":"Ocean.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Simulates the open ocean on the GPU, after Tessendorf\u0027s FFT method. One wind spectrum (Phillips, see\n/// \u003Csee cref=\u0022OceanSpectrum\u0022/\u003E) is split by wave length across three cascades of different tile sizes - the swell, the\n/// waves on it and the chop on those - so the sea has detail from hundreds of metres down to a hand\u0027s width, and the\n/// tiles, being of unrelated sizes, never visibly repeat. Each cascade\u0027s spectrum is moved to the current time and\n/// turned into a surface by an inverse FFT in compute shaders, giving two maps per cascade:\n/// \u003Csee cref=\u0022Cascade.Displacement\u0022/\u003E (choppy x and y shift, height, shear) and \u003Csee cref=\u0022Cascade.Derivatives\u0022/\u003E\n/// (slopes and stretch, mip-mapped for distant shading). The sea depends only on these settings and the time, so every\n/// machine sees the same waves.\n///\n/// The sea state - wind, wave height, look - comes from an \u003Csee cref=\u0022OceanPreset\u0022/\u003E. The ocean can hold two at once\n/// and blend between them (\u003Csee cref=\u0022BlendTo\u0022/\u003E, \u003Csee cref=\u0022SetBlend\u0022/\u003E): both are built once and kept, and the blend\n/// is a number the GPU and the CPU replica each apply as they go, so a calm can build into a storm without anything\n/// being rebuilt.\n///\n/// For physics, \u003Csee cref=\u0022HeightAt\u0022/\u003E and \u003Csee cref=\u0022WaterVelocityAt\u0022/\u003E read a CPU replica of the swell\n/// (\u003Csee cref=\u0022OceanWaves\u0022/\u003E). The GPU work runs from a command list on the main camera at the end of each frame, for\n/// the time of the next one, timed as \u0022Ocean FFT\u0022 in the GPU profiler.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022waves\u0022 )]\npublic sealed class Ocean : Component\n{\n\t/// \u003Csummary\u003EWaves per side of a cascade\u0027s tile. Fixed by the FFT shader\u0027s shared memory.\u003C/summary\u003E\n\tpublic const int Size = 256;\n\n\tpublic const int CascadeCount = 3;\n\n\t/// \u003Csummary\u003Es\u0026amp;box units (inches) per metre. Every length the ocean exposes is in metres unless it says otherwise.\u003C/summary\u003E\n\tpublic const float UnitsPerMetre = 39.37f;\n\n\t/// \u003Csummary\u003EThe sea state to start from. Empty is a moderate sea (\u003Csee cref=\u0022OceanSea.Default\u0022/\u003E).\u003C/summary\u003E\n\t[Property, Group( \u0022Sea\u0022 )] public OceanPreset Preset { get; set; }\n\n\t/// \u003Csummary\u003EWhich way the wind blows, in degrees from \u002Bx.\u003C/summary\u003E\n\t[Property, Group( \u0022Sea\u0022 ), Range( 0, 360 )] public float WindAngle { get; set; } = 0.0f;\n\n\t/// \u003Csummary\u003EHow tightly the waves follow the wind: 2 is Tessendorf\u0027s, higher lines them up more.\u003C/summary\u003E\n\t[Property, Group( \u0022Sea\u0022 ), Range( 1, 8 )] public float Directionality { get; set; } = 2.0f;\n\n\t/// \u003Csummary\u003EWaves much shorter than this, in metres, are damped out.\u003C/summary\u003E\n\t[Property, Group( \u0022Sea\u0022 ), Range( 0, 2 )] public float SmallWaveCutoff { get; set; } = 0.1f;\n\n\t[Property, Group( \u0022Sea\u0022 )] public int Seed { get; set; } = 1;\n\n\t/// \u003Csummary\u003EMetres covered by each cascade\u0027s tile, longest first. Unrelated sizes keep the tiles from lining up.\u003C/summary\u003E\n\t[Property, Group( \u0022Cascades\u0022 )] public Vector3 CascadeLengths { get; set; } = new( 500.0f, 97.0f, 19.3f );\n\n\t/// \u003Csummary\u003E\n\t/// Where one cascade hands over to the next, in waves per tile of the finer one: each cascade keeps the waves too\n\t/// long for the next to hold this many of.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Cascades\u0022 ), Range( 2, 16 )] public float BandSplit { get; set; } = 12.0f;\n\n\t/// \u003Csummary\u003ESeconds after which the sea repeats exactly - long enough that nobody notices.\u003C/summary\u003E\n\t[Property, Group( \u0022Time\u0022 )] public float LoopPeriod { get; set; } = 200.0f;\n\n\t[Property, Group( \u0022Time\u0022 ), Range( 0, 4 )] public float TimeScale { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EOne tile size of the sea and the maps the FFT writes for it.\u003C/summary\u003E\n\tpublic sealed class Cascade\n\t{\n\t\t/// \u003Csummary\u003EMetres covered by the tile.\u003C/summary\u003E\n\t\tpublic float TileLength { get; init; }\n\n\t\t/// \u003Csummary\u003EChoppy x and y shift and height, in metres, and the shear dDx/dy in w.\u003C/summary\u003E\n\t\tpublic Texture Displacement { get; init; }\n\n\t\t/// \u003Csummary\u003ESlope of the height in x and y, and the stretch of the shift along x and y. Mip-mapped.\u003C/summary\u003E\n\t\tpublic Texture Derivatives { get; init; }\n\n\t\tinternal Texture SpectrumA { get; init; }\n\t\tinternal Texture SpectrumB { get; init; }\n\t}\n\n\t/// \u003Csummary\u003EOne sea state, built: each cascade\u0027s initial spectrum, on the CPU and on the GPU.\u003C/summary\u003E\n\tpublic sealed class SpectrumSet\n\t{\n\t\tpublic OceanSpectrum[] Spectra { get; init; }\n\t\tpublic OceanSea Sea { get; init; }\n\t\tinternal Texture[] Textures { get; init; }\n\n\t\tinternal static SpectrumSet Create( OceanSpectrum[] spectra, OceanSea sea )\n\t\t{\n\t\t\tvar textures = new Texture[spectra.Length];\n\n\t\t\tfor ( int i = 0; i \u003C spectra.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\ttextures[i] = Texture.Create( Size, Size, ImageFormat.RGBA32323232F )\n\t\t\t\t\t.WithUAVBinding()\n\t\t\t\t\t.WithData\u003CVector4\u003E( spectra[i].Texels.AsSpan() )\n\t\t\t\t\t.WithName( $\u0022Ocean Initial Spectrum {i}\u0022 )\n\t\t\t\t\t.Finish();\n\t\t\t}\n\n\t\t\treturn new SpectrumSet { Spectra = spectra, Sea = sea, Textures = textures };\n\t\t}\n\n\t\t/// \u003Csummary\u003EThe sea part way between two states, exactly as the blend shows it.\u003C/summary\u003E\n\t\tinternal static SpectrumSet Lerp( SpectrumSet a, SpectrumSet b, float t )\n\t\t{\n\t\t\tvar spectra = new OceanSpectrum[a.Spectra.Length];\n\n\t\t\tfor ( int i = 0; i \u003C spectra.Length; i\u002B\u002B )\n\t\t\t\tspectra[i] = OceanSpectrum.Lerp( a.Spectra[i], b.Spectra[i], t );\n\n\t\t\treturn Create( spectra, OceanSea.Lerp( a.Sea, b.Sea, t ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EThe cascades, longest waves first.\u003C/summary\u003E\n\tpublic Cascade[] Cascades { get; private set; } = Array.Empty\u003CCascade\u003E();\n\n\t/// \u003Csummary\u003EThe two sea states being blended, and how far from the first to the second.\u003C/summary\u003E\n\tpublic SpectrumSet From { get; private set; }\n\tpublic SpectrumSet To { get; private set; }\n\tpublic float Blend { get; private set; }\n\n\t/// \u003Csummary\u003EThe sea as it is now, between the two states being blended.\u003C/summary\u003E\n\tpublic OceanSea Sea =\u003E From is null ? StartSea : OceanSea.Lerp( From.Sea, To.Sea, Blend );\n\n\t/// \u003Csummary\u003EThe time, wrapped to the loop period, the maps currently show.\u003C/summary\u003E\n\tpublic float OceanTime { get; private set; }\n\n\t/// \u003Csummary\u003EThe sea replayed on the CPU, for physics: see \u003Csee cref=\u0022OceanWaves\u0022/\u003E.\u003C/summary\u003E\n\tpublic OceanWaves Waves { get; private set; }\n\n\t/// \u003Csummary\u003EThe first active ocean in the scene: what floating things float on.\u003C/summary\u003E\n\tpublic static Ocean Current { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// The sea\u0027s time at this very moment, wrapped to the loop period: unlike \u003Csee cref=\u0022OceanTime\u0022/\u003E, which steps\n\t/// once a frame, it moves on between the physics steps of a frame too.\n\t/// \u003C/summary\u003E\n\tpublic float PhysicsTime =\u003E (float)((m_Time \u002B (Time.Now - m_TimeTakenAt) * TimeScale) % LoopPeriod \u002B LoopPeriod) % LoopPeriod;\n\n\t/// \u003Csummary\u003EWhich way the wind blows, as a unit vector across the sea.\u003C/summary\u003E\n\tpublic Vector3 WindDirection =\u003E Rotation.FromYaw( WindAngle ).Forward;\n\n\t/// \u003Csummary\u003EThe height of the sea\u0027s surface over a point in the world, in world units. The mean level is z = 0.\u003C/summary\u003E\n\tpublic float HeightAt( Vector3 world )\n\t{\n\t\tif ( Waves is null )\n\t\t\treturn 0.0f;\n\n\t\treturn Waves.HeightAt( ToMetres( world ), PhysicsTime, Sea.Choppiness ) * UnitsPerMetre;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Where the water that lies over a point at rest has been carried by the waves, in world units: sideways along\n\t/// the orbit and up or down.\n\t/// \u003C/summary\u003E\n\tpublic Vector3 DisplacementAt( Vector3 world )\n\t{\n\t\tif ( Waves is null )\n\t\t\treturn Vector3.Zero;\n\n\t\treturn Waves.Displacement( ToMetres( world ), PhysicsTime, Sea.Choppiness ) * UnitsPerMetre;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// How fast the water at the surface near a point is moving, in world units per second: the orbit each bit of\n\t/// water swings round as a wave passes - forward on the crest, back in the trough - which is what carries a\n\t/// floating thing along. From the swell\u0027s displacement a moment apart.\n\t/// \u003C/summary\u003E\n\tpublic Vector3 WaterVelocityAt( Vector3 world )\n\t{\n\t\tif ( Waves is null )\n\t\t\treturn Vector3.Zero;\n\n\t\t// Without the choppiness: that pinches the crests for the eye, and would carry the water round its orbit nearly\n\t\t// twice as fast as a real sea does.\n\t\tconst float dt = 0.05f;\n\t\tvar metres = ToMetres( world );\n\t\tfloat t = PhysicsTime;\n\t\tvar now = Waves.Displacement( metres, t, 1.0f );\n\t\tvar next = Waves.Displacement( metres, t \u002B dt, 1.0f );\n\t\treturn (next - now) / dt * UnitsPerMetre;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Moves the sea to another state over some seconds, from wherever it is now - even part way through another\n\t/// blend. Zero seconds switches at once.\n\t/// \u003C/summary\u003E\n\tpublic void BlendTo( OceanPreset preset, float seconds )\n\t{\n\t\tif ( !m_Driven )\n\t\t\tm_From = StartSea;\n\n\t\t// Part way through a blend: what shows now becomes the start of the new one, so nothing jumps.\n\t\tif ( From is not null \u0026\u0026 Blend \u003E 0.0f )\n\t\t{\n\t\t\tvar snapshot = SpectrumSet.Lerp( From, To, Blend );\n\t\t\tDropSnapshot();\n\t\t\tm_Snapshot = snapshot;\n\t\t\tm_From = snapshot.Sea;\n\t\t}\n\n\t\tm_To = preset?.Sea ?? OceanSea.Default;\n\t\tm_Blend = 0.0f;\n\t\tm_Rate = seconds \u003E 0.0f ? 1.0f / seconds : float.PositiveInfinity;\n\t\tm_Driven = true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Holds the sea at a fixed point between two states: 0 is all of the first, 1 all of the second. For a slider or\n\t/// anything else that drives the blend itself every frame.\n\t/// \u003C/summary\u003E\n\tpublic void SetBlend( OceanPreset from, OceanPreset to, float blend )\n\t{\n\t\tDropSnapshot();\n\t\tm_From = from?.Sea ?? OceanSea.Default;\n\t\tm_To = to?.Sea ?? OceanSea.Default;\n\t\tm_Blend = blend.Clamp( 0.0f, 1.0f );\n\t\tm_Rate = 0.0f;\n\t\tm_Driven = true;\n\t}\n\n\t/// \u003Csummary\u003EThe tile length of a cascade, in metres.\u003C/summary\u003E\n\tpublic float LengthOf( int cascade ) =\u003E cascade switch { 0 =\u003E CascadeLengths.x, 1 =\u003E CascadeLengths.y, _ =\u003E CascadeLengths.z };\n\n\t/// \u003Csummary\u003EHands the maps and their tile sizes to anything drawn or dispatched with the ocean\u0027s shaders.\u003C/summary\u003E\n\tpublic void Bind( RenderAttributes attributes )\n\t{\n\t\tfor ( int i = 0; i \u003C Cascades.Length; i\u002B\u002B )\n\t\t{\n\t\t\tattributes.Set( $\u0022OceanDisplacement{i}\u0022, Cascades[i].Displacement );\n\t\t\tattributes.Set( $\u0022OceanDerivatives{i}\u0022, Cascades[i].Derivatives );\n\t\t}\n\n\t\tattributes.Set( \u0022OceanTileLengths\u0022, CascadeLengths * UnitsPerMetre );\n\t\tattributes.Set( \u0022OceanUnitsPerMetre\u0022, UnitsPerMetre );\n\t}\n\n\t/// \u003Csummary\u003EThe same, for a command list.\u003C/summary\u003E\n\tpublic void Bind( CommandList commands )\n\t{\n\t\tfor ( int i = 0; i \u003C Cascades.Length; i\u002B\u002B )\n\t\t{\n\t\t\tcommands.Attributes.Set( $\u0022OceanDisplacement{i}\u0022, Cascades[i].Displacement );\n\t\t\tcommands.Attributes.Set( $\u0022OceanDerivatives{i}\u0022, Cascades[i].Derivatives );\n\t\t}\n\n\t\tcommands.Attributes.Set( \u0022OceanTileLengths\u0022, CascadeLengths * UnitsPerMetre );\n\t\tcommands.Attributes.Set( \u0022OceanUnitsPerMetre\u0022, UnitsPerMetre );\n\t}\n\n\tprivate ComputeShader m_TimeShader;\n\tprivate ComputeShader m_FftShader;\n\tprivate ComputeShader m_AssembleShader;\n\tprivate CameraCommands m_Commands;\n\tprivate double m_Time;\n\tprivate double m_TimeTakenAt;\n\n\t// Every sea state built so far, and the CPU replica for every pair blended so far.\n\tprivate readonly Dictionary\u003COceanSea, SpectrumSet\u003E m_Sets = new();\n\tprivate readonly Dictionary\u003C(SpectrumSet, SpectrumSet), OceanWaves\u003E m_Replicas = new();\n\tprivate int m_LayoutHash;\n\n\t// What BlendTo or SetBlend asked for; until either is called, the preset is the one sea state.\n\tprivate bool m_Driven;\n\tprivate OceanSea m_From, m_To;\n\tprivate float m_Blend, m_Rate;\n\n\t// The sea as it stood when a blend was interrupted: built from the two it was between, not from settings.\n\tprivate SpectrumSet m_Snapshot;\n\n\tprivate static readonly ProfilingSampler s_Profile = new( \u0022Ocean FFT\u0022 );\n\n\tprivate OceanSea StartSea =\u003E Preset?.Sea ?? OceanSea.Default;\n\n\tprivate static Vector2 ToMetres( Vector3 world ) =\u003E new Vector2( world.x, world.y ) / UnitsPerMetre;\n\n\tprivate void DropSnapshot()\n\t{\n\t\tif ( m_Snapshot is null )\n\t\t\treturn;\n\n\t\tforeach ( var key in m_Replicas.Keys.Where( k =\u003E k.Item1 == m_Snapshot ).ToList() )\n\t\t\tm_Replicas.Remove( key );\n\n\t\tm_Snapshot = null;\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\tm_TimeShader = new ComputeShader( \u0022ocean/shaders/ocean_timespectrum_cs.shader\u0022 );\n\t\tm_FftShader = new ComputeShader( \u0022ocean/shaders/ocean_fft_cs.shader\u0022 );\n\t\tm_AssembleShader = new ComputeShader( \u0022ocean/shaders/ocean_assemble_cs.shader\u0022 );\n\n\t\tm_Commands = new CameraCommands( \u0022Ocean FFT\u0022, Stage.AfterUI, 0 );\n\t\tm_LayoutHash = 0;\n\t\tRefresh();\n\t\tCurrent ??= this;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( Current == this )\n\t\t\tCurrent = null;\n\n\t\tm_Commands?.Dispose();\n\t\tm_Commands = null;\n\t}\n\n\tprivate static Texture CreateTarget( string name, ImageFormat format, bool mips = false )\n\t{\n\t\tvar builder = Texture.Create( Size, Size, format ).WithUAVBinding().WithName( name );\n\t\treturn (mips ? builder.WithMips() : builder).Finish();\n\t}\n\n\t/// \u003Csummary\u003EBrings the cascades, the two sea states and the CPU replica up to date with the settings.\u003C/summary\u003E\n\tprivate void Refresh()\n\t{\n\t\t// The tiles and the spectrum\u0027s shape set every sea state: when they change, everything built so far is stale.\n\t\tint layout = HashCode.Combine( CascadeLengths, BandSplit, Seed, LoopPeriod, WindAngle, Directionality, SmallWaveCutoff );\n\n\t\tif ( layout != m_LayoutHash || Cascades.Length != CascadeCount )\n\t\t{\n\t\t\tm_LayoutHash = layout;\n\t\t\tm_Sets.Clear();\n\t\t\tm_Replicas.Clear();\n\n\t\t\t// An interrupted blend\u0027s snapshot was built from the old shape: start again from where it stood.\n\t\t\tif ( m_Snapshot is not null )\n\t\t\t{\n\t\t\t\tm_From = m_Snapshot.Sea;\n\t\t\t\tm_Snapshot = null;\n\t\t\t}\n\n\t\t\tvar cascades = new Cascade[CascadeCount];\n\t\t\tfor ( int i = 0; i \u003C CascadeCount; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar old = i \u003C Cascades.Length ? Cascades[i] : null;\n\t\t\t\tcascades[i] = new Cascade\n\t\t\t\t{\n\t\t\t\t\tTileLength = LengthOf( i ),\n\t\t\t\t\tSpectrumA = old?.SpectrumA ?? CreateTarget( $\u0022Ocean Spectrum A {i}\u0022, ImageFormat.RGBA32323232F ),\n\t\t\t\t\tSpectrumB = old?.SpectrumB ?? CreateTarget( $\u0022Ocean Spectrum B {i}\u0022, ImageFormat.RGBA32323232F ),\n\t\t\t\t\t// Half floats: a few millimetres of precision on waves of metres, half the bandwidth to read them.\n\t\t\t\t\tDisplacement = old?.Displacement ?? CreateTarget( $\u0022Ocean Displacement {i}\u0022, ImageFormat.RGBA16161616F ),\n\t\t\t\t\tDerivatives = old?.Derivatives ?? CreateTarget( $\u0022Ocean Derivatives {i}\u0022, ImageFormat.RGBA16161616F, mips: true ),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tCascades = cascades;\n\t\t}\n\n\t\tif ( !m_Driven )\n\t\t\tm_From = m_To = StartSea;\n\n\t\tFrom = m_Snapshot ?? SetFor( m_From );\n\t\tTo = SetFor( m_To );\n\t\tBlend = From == To ? 0.0f : m_Blend;\n\n\t\tif ( !m_Replicas.TryGetValue( (From, To), out var replica ) )\n\t\t\tm_Replicas[(From, To)] = replica = new OceanWaves( From.Spectra[0], To.Spectra[0], LoopPeriod );\n\n\t\tWaves = replica;\n\t\tWaves.Blend = Blend;\n\t}\n\n\t/// \u003Csummary\u003EThe built sea state for some settings, building it the first time.\u003C/summary\u003E\n\tprivate SpectrumSet SetFor( OceanSea sea )\n\t{\n\t\tif ( m_Sets.TryGetValue( sea, out var set ) )\n\t\t\treturn set;\n\n\t\tvar settings = new OceanSpectrum.Settings( sea.WindSpeed, WindAngle, Directionality, SmallWaveCutoff, Seed );\n\t\tvar spectra = new OceanSpectrum[CascadeCount];\n\t\tdouble variance = 0.0;\n\n\t\tfor ( int i = 0; i \u003C CascadeCount; i\u002B\u002B )\n\t\t{\n\t\t\t// Each cascade takes the waves too long for the next one to hold BandSplit of; the last takes the rest.\n\t\t\tfloat minK = i == 0 ? 0.0f : 2.0f * MathF.PI / LengthOf( i ) * BandSplit;\n\t\t\tfloat maxK = i == CascadeCount - 1 ? float.MaxValue : 2.0f * MathF.PI / LengthOf( i \u002B 1 ) * BandSplit;\n\n\t\t\tspectra[i] = new OceanSpectrum( Size, LengthOf( i ), minK, maxK, i, settings );\n\t\t\tvariance \u002B= spectra[i].RawVariance;\n\t\t}\n\n\t\tfloat scale = OceanSpectrum.ScaleFor( sea.SignificantHeight, variance );\n\n\t\tfor ( int i = 0; i \u003C CascadeCount; i\u002B\u002B )\n\t\t\tspectra[i].Scale( scale );\n\n\t\tset = SpectrumSet.Create( spectra, sea );\n\t\tm_Sets[sea] = set;\n\t\treturn set;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( m_Commands is null )\n\t\t\treturn;\n\n\t\tAdvanceBlend();\n\t\tRefresh();\n\t\tm_Commands.Follow( Scene );\n\n\t\t// The maps are built at the end of this frame, for the moment the next one will show.\n\t\tm_Time \u002B= Time.Delta * TimeScale;\n\t\tm_TimeTakenAt = Time.Now;\n\t\tOceanTime = (float)((m_Time \u002B Time.Delta * TimeScale) % LoopPeriod);\n\t\tCurrent ??= this;\n\n\t\t// The CPU replica\u0027s next grid gets started on a worker thread now, so it\u0027s ready before the physics needs it.\n\t\tWaves.Prefetch( PhysicsTime );\n\n\t\tRecord();\n\t}\n\n\tprivate void AdvanceBlend()\n\t{\n\t\tif ( m_Rate \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tm_Blend = float.IsInfinity( m_Rate ) ? 1.0f : m_Blend.Approach( 1.0f, Time.Delta * m_Rate );\n\n\t\tif ( m_Blend \u003C 1.0f )\n\t\t\treturn;\n\n\t\t// Arrived: the target is now the one sea state.\n\t\tm_From = m_To;\n\t\tDropSnapshot();\n\t\tm_Blend = 0.0f;\n\t\tm_Rate = 0.0f;\n\t}\n\n\tprivate void Record()\n\t{\n\t\tvar cl = m_Commands.List;\n\t\tcl.Reset();\n\n\t\tusing ( cl.ProfileScope( s_Profile ) )\n\t\t{\n\t\t\tcl.Attributes.Set( \u0022OceanSize\u0022, Size );\n\t\t\tcl.Attributes.Set( \u0022OceanTime\u0022, OceanTime );\n\t\t\tcl.Attributes.Set( \u0022LoopPeriod\u0022, LoopPeriod );\n\t\t\tcl.Attributes.Set( \u0022Choppiness\u0022, Sea.Choppiness );\n\t\t\tcl.Attributes.Set( \u0022SpectrumBlend\u0022, Blend );\n\n\t\t\tfor ( int i = 0; i \u003C Cascades.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar cascade = Cascades[i];\n\n\t\t\t\t// 1. The spectrum at this time, blended between the two sea states.\n\t\t\t\tcl.Attributes.Set( \u0022InitialSpectrum\u0022, From.Textures[i] );\n\t\t\t\tcl.Attributes.Set( \u0022InitialSpectrumB\u0022, To.Textures[i] );\n\t\t\t\tcl.Attributes.Set( \u0022SpectrumA\u0022, cascade.SpectrumA );\n\t\t\t\tcl.Attributes.Set( \u0022SpectrumB\u0022, cascade.SpectrumB );\n\t\t\t\tcl.Attributes.Set( \u0022TileLength\u0022, cascade.TileLength );\n\t\t\t\tcl.DispatchCompute( m_TimeShader, Size, Size, 1 );\n\t\t\t\tcl.UavBarrier( cascade.SpectrumA );\n\t\t\t\tcl.UavBarrier( cascade.SpectrumB );\n\n\t\t\t\t// 2. Inverse FFT along x, then along y: one thread group per line.\n\t\t\t\tfor ( int direction = 0; direction \u003C 2; direction\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tcl.Attributes.Set( \u0022Direction\u0022, direction );\n\t\t\t\t\tcl.DispatchCompute( m_FftShader, Size, Size, 1 );\n\t\t\t\t\tcl.UavBarrier( cascade.SpectrumA );\n\t\t\t\t\tcl.UavBarrier( cascade.SpectrumB );\n\t\t\t\t}\n\n\t\t\t\t// 3. The maps the surface reads, the slopes mip-mapped so distant waves shade without shimmering.\n\t\t\t\tcl.Attributes.Set( \u0022Displacement\u0022, cascade.Displacement );\n\t\t\t\tcl.Attributes.Set( \u0022Derivatives\u0022, cascade.Derivatives );\n\t\t\t\tcl.DispatchCompute( m_AssembleShader, Size, Size, 1 );\n\t\t\t\tcl.UavBarrier( cascade.Displacement );\n\t\t\t\tcl.UavBarrier( cascade.Derivatives );\n\t\t\t\tcl.GenerateMipMaps( cascade.Derivatives, Graphics.DownsampleMethod.Box );\n\t\t\t}\n\t\t}\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"},{"Ident":"schumi.ocean","Path":"OceanFoamSource.cs","FileName":"OceanFoamSource.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Makes foam where this object cuts the sea: a band round its outline at the waterline, stronger the faster it moves,\n/// which \u003Csee cref=\u0022OceanFoam\u0022/\u003E carries on after it - a ship\u0027s bow wave and wake, the white water round a rock. The\n/// outline is an ellipse in the object\u0027s own space; left at zero it is fitted to the model\u0027s bounds.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Foam Source\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022bubble_chart\u0022 )]\npublic sealed class OceanFoamSource : Component\n{\n\t/// \u003Csummary\u003EHalf the length and half the width of the outline, in metres. Zero fits it to the model.\u003C/summary\u003E\n\t[Property] public Vector2 HalfSize { get; set; }\n\n\t/// \u003Csummary\u003EFoam added per second at a standstill, where the band is thickest.\u003C/summary\u003E\n\t[Property, Range( 0, 10 )] public float Strength { get; set; } = 0.6f;\n\n\t/// \u003Csummary\u003EExtra foam per second for each metre per second the object moves.\u003C/summary\u003E\n\t[Property, Range( 0, 5 )] public float SpeedStrength { get; set; } = 0.6f;\n\n\t/// \u003Csummary\u003EHow far out from the outline the foam reaches, in metres.\u003C/summary\u003E\n\t[Property, Range( 0.1f, 10 )] public float BandWidth { get; set; } = 1.5f;\n\n\t/// \u003Csummary\u003EThe outline in world units: half length and half width.\u003C/summary\u003E\n\tpublic Vector2 WorldHalfSize { get; private set; }\n\n\t/// \u003Csummary\u003EThe lowest and highest point of the object, in world z: it only foams where the sea lies between them.\u003C/summary\u003E\n\tpublic Vector2 HeightRange { get; private set; }\n\n\t/// \u003Csummary\u003EHow fast it moves across the sea, in metres per second.\u003C/summary\u003E\n\tpublic float Speed { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// Where the object\u0027s upright axis crosses the mean sea level: the middle of its waterline. A leaning rock whose\n\t/// centre lies deep under water cuts the surface metres away from that centre.\n\t/// \u003C/summary\u003E\n\tpublic Vector3 WaterlineCenter { get; private set; }\n\n\tprivate Vector3 m_LastPosition;\n\tprivate bool m_HasLast;\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar bounds = Components.Get\u003CModelRenderer\u003E()?.Model?.Bounds ?? BBox.FromPositionAndSize( Vector3.Zero, 32 );\n\t\tvar scale = WorldScale;\n\t\tvar half = HalfSize.Length \u003E 0.0f ? HalfSize * Ocean.UnitsPerMetre : new Vector2( bounds.Extents.x, bounds.Extents.y );\n\t\tWorldHalfSize = new Vector2( half.x * scale.x, half.y * scale.y );\n\n\t\tvar world = bounds.Transform( WorldTransform );\n\t\tHeightRange = new Vector2( world.Mins.z, world.Maxs.z );\n\n\t\tvar centre = WorldTransform.PointToWorld( bounds.Center );\n\t\tvar up = WorldRotation.Up;\n\t\tWaterlineCenter = MathF.Abs( up.z ) \u003E 0.2f ? centre - up * (centre.z / up.z) : centre;\n\n\t\tif ( m_HasLast \u0026\u0026 Time.Delta \u003E 0.0f )\n\t\t\tSpeed = MathX.Lerp( Speed, (WorldPosition - m_LastPosition).WithZ( 0 ).Length / Time.Delta / Ocean.UnitsPerMetre, 0.2f );\n\n\t\tm_LastPosition = WorldPosition;\n\t\tm_HasLast = true;\n\t}\n\n\t/// \u003Csummary\u003EFoam per second this frame: the steady band, more for speed, and any splash still settling.\u003C/summary\u003E\n\tpublic float Rate =\u003E Strength \u002B SpeedStrength * Speed \u002B m_Splash;\n\n\tprivate float m_Splash;\n\n\t/// \u003Csummary\u003EA burst of white water round the hull - it slammed into a wave - fading over half a second.\u003C/summary\u003E\n\tpublic void Splash( float amount )\n\t{\n\t\tm_Splash = MathF.Max( m_Splash, amount );\n\t}\n\n\tprotected override void OnFixedUpdate()\n\t{\n\t\tm_Splash *= MathF.Exp( -Time.Delta / 0.5f );\n\t}\n\n\tprotected override void DrawGizmos()\n\t{\n\t\t// The outline the foam forms round, at the waterline.\n\t\tvar bounds = Components.Get\u003CModelRenderer\u003E()?.Model?.Bounds ?? BBox.FromPositionAndSize( Vector3.Zero, 32 );\n\t\tvar half = HalfSize.Length \u003E 0.0f ? HalfSize * Ocean.UnitsPerMetre : new Vector2( bounds.Extents.x, bounds.Extents.y );\n\t\tvar centre = bounds.Center.WithZ( 0 );\n\n\t\tGizmo.Draw.Color = Color.White.WithAlpha( 0.6f );\n\n\t\tfor ( int i = 0; i \u003C 48; i\u002B\u002B )\n\t\t{\n\t\t\tfloat a = i / 48.0f * MathF.PI * 2.0f, b = (i \u002B 1) / 48.0f * MathF.PI * 2.0f;\n\t\t\tGizmo.Draw.Line( centre \u002B new Vector3( MathF.Cos( a ) * half.x, MathF.Sin( a ) * half.y, 0 ),\n\t\t\t\tcentre \u002B new Vector3( MathF.Cos( b ) * half.x, MathF.Sin( b ) * half.y, 0 ) );\n\t\t}\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanHullMask.cs","FileName":"OceanHullMask.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Keeps the sea out of a hull, to the shape of its collision mesh: \u003Csee cref=\u0022OceanSurface\u0022/\u003E doesn\u0027t draw the ocean\n/// anywhere inside it, so an open boat sitting low isn\u0027t shown full of sea. Read once from the model, handed to the GPU\n/// in the hull\u0027s own space; only where the hull is changes from frame to frame.\n///\n/// After the Water Tool\u0027s HullWaterExclusionVolume, with one change: a convex hull - what s\u0026box\u0027s collision usually is -\n/// is kept as its face planes, and a point is inside when it is behind every one of them, which the shader can give up\n/// on at the first face it\u0027s in front of. Only meshes that aren\u0027t convex fall back to counting how many triangles a\n/// ray from the point crosses. Either way a box round the hull turns away everything not near it first.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Hull Mask\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022format_color_reset\u0022 )]\npublic sealed class OceanHullMask : Component, Component.ExecuteInEditor\n{\n\t/// \u003Csummary\u003EThe model whose collision shapes the mask. Empty takes the ModelRenderer\u0027s.\u003C/summary\u003E\n\t[Property] public Model CustomModel { get; set; }\n\n\t/// \u003Csummary\u003EThe convex parts\u0027 face planes in model space: (normal, offset), inside where dot(normal, p) \u002B offset \u0026lt;= 0.\u003C/summary\u003E\n\tpublic IReadOnlyList\u003C(int Start, int Count)\u003E ConvexParts =\u003E m_ConvexParts;\n\n\tpublic IReadOnlyList\u003CVector4\u003E Planes =\u003E m_Planes;\n\n\t/// \u003Csummary\u003EThe rest of the collision, as triangles in model space, three points each.\u003C/summary\u003E\n\tpublic IReadOnlyList\u003CVector3\u003E Triangles =\u003E m_Triangles;\n\n\t/// \u003Csummary\u003EThe box round it all, in model space.\u003C/summary\u003E\n\tpublic BBox LocalBounds { get; private set; }\n\n\tpublic bool IsEmpty =\u003E m_Planes.Count == 0 \u0026\u0026 m_Triangles.Count == 0;\n\n\tprivate readonly List\u003C(int, int)\u003E m_ConvexParts = new();\n\tprivate readonly List\u003CVector4\u003E m_Planes = new();\n\tprivate readonly List\u003CVector3\u003E m_Triangles = new();\n\tprivate Model m_Built;\n\n\tprivate Model ResolveModel() =\u003E CustomModel ?? Components.Get\u003CModelRenderer\u003E()?.Model;\n\n\tprotected override void OnEnabled() =\u003E Rebuild();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( ResolveModel() != m_Built )\n\t\t\tRebuild();\n\t}\n\n\tprivate void Rebuild()\n\t{\n\t\tvar model = ResolveModel();\n\t\tm_Built = model;\n\t\tm_ConvexParts.Clear();\n\t\tm_Planes.Clear();\n\t\tm_Triangles.Clear();\n\t\tLocalBounds = default;\n\n\t\tif ( model?.Physics is null )\n\t\t\treturn;\n\n\t\tvar min = new Vector3( float.MaxValue );\n\t\tvar max = new Vector3( float.MinValue );\n\n\t\tvoid Grow( Vector3 p )\n\t\t{\n\t\t\tmin = Vector3.Min( min, p );\n\t\t\tmax = Vector3.Max( max, p );\n\t\t}\n\n\t\tforeach ( var part in model.Physics.Parts )\n\t\t{\n\t\t\tforeach ( var hull in part.Hulls )\n\t\t\t{\n\t\t\t\tvar points = hull.GetPoints()?.ToArray();\n\n\t\t\t\tif ( points is null || points.Length \u003C 4 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tint start = m_Planes.Count;\n\t\t\t\tm_Planes.AddRange( FacePlanes( points ) );\n\t\t\t\tm_ConvexParts.Add( (start, m_Planes.Count - start) );\n\n\t\t\t\tforeach ( var p in points )\n\t\t\t\t\tGrow( p );\n\t\t\t}\n\n\t\t\tforeach ( var mesh in part.Meshes )\n\t\t\t{\n\t\t\t\tforeach ( var t in mesh.GetTriangles() )\n\t\t\t\t{\n\t\t\t\t\tm_Triangles.Add( t.A ); m_Triangles.Add( t.B ); m_Triangles.Add( t.C );\n\t\t\t\t\tGrow( t.A ); Grow( t.B ); Grow( t.C );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( !IsEmpty )\n\t\t\tLocalBounds = new BBox( min, max );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The faces of a convex hull as planes, outward-facing, each once: a plane through three of its points with every\n\t/// other point on one side of it is a face. Nearly coplanar duplicates are merged.\n\t/// \u003C/summary\u003E\n\tprivate static List\u003CVector4\u003E FacePlanes( Vector3[] points )\n\t{\n\t\tvar planes = new List\u003CVector4\u003E();\n\t\tvar centroid = points.Aggregate( Vector3.Zero, ( a, b ) =\u003E a \u002B b ) / points.Length;\n\t\tint n = points.Length;\n\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\n\t\tfor ( int j = i \u002B 1; j \u003C n; j\u002B\u002B )\n\t\tfor ( int k = j \u002B 1; k \u003C n; k\u002B\u002B )\n\t\t{\n\t\t\tvar normal = Vector3.Cross( points[j] - points[i], points[k] - points[i] );\n\n\t\t\tif ( normal.LengthSquared \u003C 1e-8f )\n\t\t\t\tcontinue;\n\n\t\t\tnormal = normal.Normal;\n\n\t\t\t// Outward: away from the middle.\n\t\t\tif ( Vector3.Dot( normal, centroid - points[i] ) \u003E 0.0f )\n\t\t\t\tnormal = -normal;\n\n\t\t\tfloat offset = -Vector3.Dot( normal, points[i] );\n\t\t\tbool face = true;\n\n\t\t\tfor ( int m = 0; m \u003C n \u0026\u0026 face; m\u002B\u002B )\n\t\t\t\tface = Vector3.Dot( normal, points[m] ) \u002B offset \u003C= 0.01f;\n\n\t\t\tif ( !face )\n\t\t\t\tcontinue;\n\n\t\t\tbool known = planes.Any( p =\u003E Vector3.Dot( new Vector3( p.x, p.y, p.z ), normal ) \u003E 0.9999f \u0026\u0026 MathF.Abs( p.w - offset ) \u003C 0.01f );\n\n\t\t\tif ( !known )\n\t\t\t\tplanes.Add( new Vector4( normal, offset ) );\n\t\t}\n\n\t\treturn planes;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The world-to-model transform\u0027s three rows, for dot( row, float4( position, 1 ) ) in the shader. Matches\n\t/// WorldTransform.PointToLocal: forward is model x, left model y, up model z.\n\t/// \u003C/summary\u003E\n\tpublic (Vector4, Vector4, Vector4) WorldToLocalRows()\n\t{\n\t\tvar rotation = WorldRotation;\n\t\tvar position = WorldPosition;\n\t\tvar scale = WorldScale;\n\t\tvar ax = rotation.Forward / scale.x;\n\t\tvar ay = rotation.Left / scale.y;\n\t\tvar az = rotation.Up / scale.z;\n\n\t\treturn (\n\t\t\tnew Vector4( ax, -Vector3.Dot( rotation.Forward, position ) / scale.x ),\n\t\t\tnew Vector4( ay, -Vector3.Dot( rotation.Left, position ) / scale.y ),\n\t\t\tnew Vector4( az, -Vector3.Dot( rotation.Up, position ) / scale.z ) );\n\t}\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tif ( !Gizmo.IsSelected || IsEmpty )\n\t\t\treturn;\n\n\t\tGizmo.Draw.Color = Color.Cyan.WithAlpha( 0.5f );\n\t\tGizmo.Draw.LineBBox( LocalBounds );\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Ocean\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022ocean\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022local\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022local.ocean\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002232\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-10-28T21:27:46.3546420Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.114.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.114.0\u0022)]"},{"Ident":"schumi.ocean","Path":"OceanDebugView.cs","FileName":"OceanDebugView.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing System.Text;\nusing Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Shows what the ocean is doing, over the game view: each cascade\u0027s tile from above - its heights, and where it folds\n/// into breaking crests - the foam window round the camera, and the numbers behind the sea (the state and the blend,\n/// the CPU replica\u0027s work, the spray). For tuning and checking; leave it off in a game.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Debug View\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022bug_report\u0022 )]\npublic sealed class OceanDebugView : Component\n{\n\t/// \u003Csummary\u003EThe sea to show. Empty takes the one on this object, else the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003EEach cascade\u0027s heights, longest waves first.\u003C/summary\u003E\n\t[Property] public bool ShowHeights { get; set; } = true;\n\n\t/// \u003Csummary\u003EEach cascade\u0027s folding: white where the surface bunches up, red where it folds over and breaks.\u003C/summary\u003E\n\t[Property] public bool ShowFolding { get; set; } = true;\n\n\t/// \u003Csummary\u003EThe persistent foam window, centred on the camera.\u003C/summary\u003E\n\t[Property] public bool ShowFoam { get; set; } = true;\n\n\t[Property] public bool ShowStats { get; set; } = true;\n\n\t/// \u003Csummary\u003EPixels across each picture.\u003C/summary\u003E\n\t[Property, Range( 64, 512 )] public int PanelSize { get; set; } = 180;\n\n\t/// \u003Csummary\u003EWhere the top left of the view sits on screen, in pixels.\u003C/summary\u003E\n\t[Property] public Vector2 Position { get; set; } = new( 16, 16 );\n\n\tprivate const int Size = 256;\n\tprivate const int Panels = Ocean.CascadeCount * 2 \u002B 1;\n\n\tprivate ComputeShader m_Shader;\n\tprivate CameraCommands m_Commands;\n\tprivate readonly Texture[] m_Panels = new Texture[Panels];\n\tprivate readonly float[] m_Ranges = new float[Ocean.CascadeCount];\n\tprivate readonly Dictionary\u003COcean.SpectrumSet, float[]\u003E m_Deviations = new();\n\n\tprivate static readonly ProfilingSampler s_Profile = new( \u0022Ocean Debug View\u0022 );\n\n\tprivate Ocean Sea =\u003E Ocean.IsValid() ? Ocean : Components.Get\u003COcean\u003E() ?? Ocean.Current;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tm_Shader = new ComputeShader( \u0022ocean/shaders/ocean_debug_cs.shader\u0022 );\n\t\t// After the FFT and the foam, on the same stage.\n\t\tm_Commands = new CameraCommands( \u0022Ocean Debug View\u0022, Stage.AfterUI, 2 );\n\n\t\tfor ( int i = 0; i \u003C Panels; i\u002B\u002B )\n\t\t\tm_Panels[i] = Texture.Create( Size, Size, ImageFormat.RGBA8888 ).WithUAVBinding().WithName( $\u0022Ocean Debug {i}\u0022 ).Finish();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tm_Commands?.Dispose();\n\t\tm_Commands = null;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar ocean = Sea;\n\n\t\tif ( m_Commands is null || !ocean.IsValid() || ocean.Cascades.Length == 0 )\n\t\t\treturn;\n\n\t\tm_Commands.Follow( Scene );\n\t\tRecord( ocean );\n\t\tDraw( ocean );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Two standard deviations of each cascade\u0027s height as the sea is now: what its picture shows as full white or full\n\t/// blue.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateRanges( Ocean ocean )\n\t{\n\t\tvar from = Deviations( ocean.From );\n\t\tvar to = Deviations( ocean.To );\n\n\t\tfor ( int i = 0; i \u003C m_Ranges.Length; i\u002B\u002B )\n\t\t\tm_Ranges[i] = 2.0f * MathX.Lerp( from[i], to[i], ocean.Blend );\n\t}\n\n\tprivate float[] Deviations( Ocean.SpectrumSet set )\n\t{\n\t\tif ( m_Deviations.TryGetValue( set, out var deviations ) )\n\t\t\treturn deviations;\n\n\t\tdeviations = new float[Ocean.CascadeCount];\n\n\t\tfor ( int i = 0; i \u003C deviations.Length; i\u002B\u002B )\n\t\t{\n\t\t\tdouble sum = 0.0;\n\t\t\tforeach ( var t in set.Spectra[i].Texels )\n\t\t\t\tsum \u002B= t.x * t.x \u002B t.y * t.y;\n\n\t\t\tdeviations[i] = MathF.Sqrt( (float)(2.0 * sum) );\n\t\t}\n\n\t\treturn m_Deviations[set] = deviations;\n\t}\n\n\tprivate void Record( Ocean ocean )\n\t{\n\t\tUpdateRanges( ocean );\n\n\t\tvar cl = m_Commands.List;\n\t\tcl.Reset();\n\n\t\tusing ( cl.ProfileScope( s_Profile ) )\n\t\t{\n\t\t\tcl.Attributes.Set( \u0022DebugSize\u0022, Size );\n\n\t\t\tfor ( int i = 0; i \u003C Ocean.CascadeCount; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcl.Attributes.Set( \u0022DebugDisplacement\u0022, ocean.Cascades[i].Displacement );\n\t\t\t\tcl.Attributes.Set( \u0022DebugDerivatives\u0022, ocean.Cascades[i].Derivatives );\n\t\t\t\tcl.Attributes.Set( \u0022DebugRange\u0022, m_Ranges[i] );\n\n\t\t\t\tPanel( cl, i, 0.0f );\n\t\t\t\tPanel( cl, Ocean.CascadeCount \u002B i, 1.0f );\n\t\t\t}\n\n\t\t\tvar foam = Scene.GetAllComponents\u003COceanFoam\u003E().FirstOrDefault();\n\n\t\t\tif ( foam.IsValid() \u0026\u0026 foam.Current is not null )\n\t\t\t{\n\t\t\t\tcl.Attributes.Set( \u0022DebugFoam\u0022, foam.Current );\n\t\t\t\tPanel( cl, Panels - 1, 2.0f );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void Panel( CommandList cl, int index, float mode )\n\t{\n\t\tcl.Attributes.Set( \u0022DebugMode\u0022, mode );\n\t\tcl.Attributes.Set( \u0022DebugOut\u0022, m_Panels[index] );\n\t\tcl.DispatchCompute( m_Shader, Size, Size, 1 );\n\t\tcl.UavBarrier( m_Panels[index] );\n\t}\n\n\tprivate void Draw( Ocean ocean )\n\t{\n\t\tvar camera = m_Commands.Camera;\n\n\t\tif ( !camera.IsValid() )\n\t\t\treturn;\n\n\t\tvar hud = camera.BeginHud();\n\n\t\ttry\n\t\t{\n\t\t\tDrawPanels( ref hud, ocean );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\thud.Dispose();\n\t\t}\n\t}\n\n\tprivate void DrawPanels( ref Painter hud, Ocean ocean )\n\t{\n\t\tint row = 0;\n\t\tstring Tile( int i ) =\u003E $\u0022{ocean.LengthOf( i ):0.#} m\u0022;\n\n\t\tif ( ShowHeights )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Heights\u0022, 0, Ocean.CascadeCount, i =\u003E $\u0022{Tile( i )}  \u00B1{m_Ranges[i]:0.##} m\u0022 );\n\n\t\tif ( ShowFolding )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Folding (Jacobian)\u0022, Ocean.CascadeCount, Ocean.CascadeCount, Tile );\n\n\t\tvar foam = Scene.GetAllComponents\u003COceanFoam\u003E().FirstOrDefault();\n\n\t\tif ( ShowFoam \u0026\u0026 foam.IsValid() )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Foam window\u0022, Panels - 1, 1, _ =\u003E $\u0022{foam.Extent:0} m round the camera\u0022 );\n\n\t\tif ( ShowStats )\n\t\t{\n\t\t\tvar sea = ocean.Sea;\n\t\t\tvar waves = ocean.Waves;\n\t\t\tvar text = new StringBuilder()\n\t\t\t\t.AppendLine( $\u0022Sea: wind {sea.WindSpeed:0.#} m/s \u00B7 Hs {sea.SignificantHeight:0.##} m \u00B7 choppiness {sea.Choppiness:0.##} \u00B7 scatter {sea.Scatter:0.##} \u00B7 crest foam {sea.CrestFoam:0.##}\u0022 )\n\t\t\t\t.AppendLine( $\u0022Blend: {ocean.Blend:P0} from Hs {ocean.From.Sea.SignificantHeight:0.##} m to {ocean.To.Sea.SignificantHeight:0.##} m \u00B7 time {ocean.OceanTime:0.0} / {ocean.LoopPeriod:0} s\u0022 )\n\t\t\t\t.AppendLine( waves is null ? \u0022CPU replica: none\u0022 : $\u0022CPU replica: {waves.Count} waves on {waves.GridSize}\u00B2 \u00B7 grids built {waves.BackgroundBuilds} in background, {waves.SynchronousBuilds} waiting\u0022 )\n\t\t\t\t.AppendLine( $\u0022Spray: {OceanSpray.Current?.Count ?? 0} particles \u00B7 foam sources: {Scene.GetAllComponents\u003COceanFoamSource\u003E().Count()}\u0022 )\n\t\t\t\t.ToString();\n\n\t\t\tText( ref hud, text, 13, new Rect( Position.x, RowTop( row ), 1200, 100 ) );\n\t\t}\n\t}\n\n\tprivate const float Gap = 8.0f;\n\tprivate const float TitleHeight = 18.0f;\n\n\tprivate float RowTop( int row ) =\u003E Position.y \u002B row * (PanelSize \u002B Gap \u002B TitleHeight);\n\n\tprivate void Row( ref Painter hud, int row, string title, int first, int count, Func\u003Cint, string\u003E label )\n\t{\n\t\tfloat y = RowTop( row );\n\t\tText( ref hud, title, 13, new Rect( Position.x, y, 600, TitleHeight ) );\n\n\t\tfor ( int i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\tvar rect = new Rect( Position.x \u002B i * (PanelSize \u002B Gap), y \u002B TitleHeight, PanelSize, PanelSize );\n\t\t\thud.Texture( m_Panels[first \u002B i], rect, null );\n\t\t\tText( ref hud, label( i ), 11, rect.Shrink( 4.0f ) );\n\t\t}\n\t}\n\n\tprivate static void Text( ref Painter hud, string text, float size, Rect rect )\n\t{\n\t\thud.TextStyle = TextStyle.Default with { FontSize = size, Color = Color.White, Alignment = TextFlag.LeftTop };\n\t\thud.Text( text, rect );\n\t}\n}"},{"Ident":"schumi.ocean","Path":"Code/Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"},{"Ident":"schumi.ocean","Path":"OceanFoam.cs","FileName":"OceanFoam.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Persistent foam for the sea an \u003Csee cref=\u0022Ocean\u0022/\u003E simulates: a window of foam centred on the camera, carried over\n/// from frame to frame in a compute shader. Each frame the foam already there fades and blurs into its neighbours -\n/// dispersing - while folding crests (at the sea state\u0027s \u003Csee cref=\u0022OceanSea.CrestFoam\u0022/\u003E rate) and every\n/// \u003Csee cref=\u0022OceanFoamSource\u0022/\u003E (hulls, rocks) add new foam, so whitecaps linger after the wave has passed and a\n/// moving ship leaves a wake. The window steps with the camera by whole texels, so the foam stays put on the sea.\n/// \u003Csee cref=\u0022OceanSurface\u0022/\u003E draws it; beyond the window, the surface falls back on the crests\u0027 own foam.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Foam\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022bubble_chart\u0022 )]\npublic sealed class OceanFoam : Component\n{\n\t/// \u003Csummary\u003EThe sea to foam. Empty takes the one on this object, else the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003ETexels per side of the window.\u003C/summary\u003E\n\t[Property, Group( \u0022Window\u0022 )] public int Resolution { get; set; } = 1024;\n\n\t/// \u003Csummary\u003EMetres the window covers.\u003C/summary\u003E\n\t[Property, Group( \u0022Window\u0022 ), Range( 50, 2000 )] public float Extent { get; set; } = 512.0f;\n\n\t/// \u003Csummary\u003ESeconds for foam to fade to a third.\u003C/summary\u003E\n\t[Property, Group( \u0022Behaviour\u0022 ), Range( 0.2f, 30 )] public float Lifetime { get; set; } = 4.0f;\n\n\t/// \u003Csummary\u003EHow fast foam spreads into its neighbours: soft, dispersing edges.\u003C/summary\u003E\n\t[Property, Group( \u0022Behaviour\u0022 ), Range( 0, 1 )] public float Spread { get; set; } = 0.15f;\n\n\t/// \u003Csummary\u003EThe Jacobian below which a crest counts as breaking.\u003C/summary\u003E\n\t[Property, Group( \u0022Behaviour\u0022 ), Range( -1, 1 )] public float CrestThreshold { get; set; } = 0.45f;\n\n\t/// \u003Csummary\u003E\n\t/// The foam drawn this frame, one channel from 0 to 1: the window the last frame\u0027s pass wrote, since this frame\u0027s\n\t/// pass runs after the surface has drawn.\n\t/// \u003C/summary\u003E\n\tpublic Texture Current { get; private set; }\n\n\t/// \u003Csummary\u003EWorld xy of the corner of the window \u003Csee cref=\u0022Current\u0022/\u003E holds.\u003C/summary\u003E\n\tpublic Vector2 Origin { get; private set; }\n\n\t/// \u003Csummary\u003EWorld units the window covers.\u003C/summary\u003E\n\tpublic float WorldExtent =\u003E m_Texel * m_Size;\n\n\tprivate const int MaxSources = 32;\n\n\t// Ping-pong: each pass reads one and writes the other; m_Flip says which was written last.\n\tprivate Texture m_A, m_B;\n\tprivate bool m_Flip;\n\tprivate int m_Size;\n\tprivate float m_Texel;\n\tprivate Vector2 m_LastOrigin;\n\tprivate bool m_HasLast;\n\tprivate ComputeShader m_Shader;\n\tprivate CameraCommands m_Commands;\n\tprivate GpuBuffer\u003CVector4\u003E m_Sources;\n\tprivate readonly Vector4[] m_SourceData = new Vector4[MaxSources * 3];\n\n\tprivate static readonly ProfilingSampler s_Profile = new( \u0022Ocean Foam\u0022 );\n\n\tprivate Ocean Sea =\u003E Ocean.IsValid() ? Ocean : Components.Get\u003COcean\u003E() ?? Ocean.Current;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tm_Shader = new ComputeShader( \u0022ocean/shaders/ocean_foam_cs.shader\u0022 );\n\t\t// After the FFT, which is on the same stage at order 0.\n\t\tm_Commands = new CameraCommands( \u0022Ocean Foam\u0022, Stage.AfterUI, 1 );\n\t\tm_Sources = new GpuBuffer\u003CVector4\u003E( MaxSources * 3, GpuBuffer.UsageFlags.Structured, \u0022Ocean Foam Sources\u0022 );\n\t\tAllocate();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tm_Commands?.Dispose();\n\t\tm_Commands = null;\n\t\tm_Sources?.Dispose();\n\t\tm_Sources = null;\n\t}\n\n\tprivate void Allocate()\n\t{\n\t\tm_Size = Math.Clamp( Resolution / 8 * 8, 64, 4096 );\n\t\tm_A = Texture.Create( m_Size, m_Size, ImageFormat.R32F ).WithUAVBinding().WithName( \u0022Ocean Foam A\u0022 ).Finish();\n\t\tm_B = Texture.Create( m_Size, m_Size, ImageFormat.R32F ).WithUAVBinding().WithName( \u0022Ocean Foam B\u0022 ).Finish();\n\t\tm_HasLast = false;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar ocean = Sea;\n\n\t\tif ( m_Commands is null || !ocean.IsValid() || ocean.Cascades.Length == 0 )\n\t\t\treturn;\n\n\t\tif ( m_Size != Math.Clamp( Resolution / 8 * 8, 64, 4096 ) )\n\t\t\tAllocate();\n\n\t\tm_Commands.Follow( Scene );\n\n\t\t// What the last pass wrote is what this frame shows.\n\t\tCurrent = m_Flip ? m_B : m_A;\n\t\tOrigin = m_LastOrigin;\n\n\t\t// The next window, centred on the camera and snapped to whole texels.\n\t\tm_Texel = Extent * Ocean.UnitsPerMetre / m_Size;\n\t\tvar camera = m_Commands.Camera;\n\t\tvar eye = camera.IsValid() ? camera.WorldPosition : WorldPosition;\n\t\tfloat half = m_Size * 0.5f * m_Texel;\n\t\tvar origin = new Vector2( MathF.Round( (eye.x - half) / m_Texel ) * m_Texel, MathF.Round( (eye.y - half) / m_Texel ) * m_Texel );\n\n\t\tvar shift = m_HasLast\n\t\t\t? new Vector2( MathF.Round( (origin.x - m_LastOrigin.x) / m_Texel ), MathF.Round( (origin.y - m_LastOrigin.y) / m_Texel ) )\n\t\t\t: new Vector2( m_Size, m_Size );\n\n\t\tm_LastOrigin = origin;\n\t\tm_HasLast = true;\n\n\t\tint count = GatherSources();\n\t\tRecord( ocean, origin, shift, count );\n\t}\n\n\tprivate int GatherSources()\n\t{\n\t\tint count = 0;\n\n\t\tforeach ( var source in Scene.GetAllComponents\u003COceanFoamSource\u003E() )\n\t\t{\n\t\t\tif ( count \u003E= MaxSources )\n\t\t\t\tbreak;\n\n\t\t\tvar forward = source.WorldRotation.Forward.WithZ( 0 );\n\t\t\tforward = forward.Length \u003E 1e-3f ? forward.Normal : Vector3.Forward;\n\n\t\t\tm_SourceData[count * 3 \u002B 0] = new Vector4( source.WaterlineCenter, source.Rate );\n\t\t\tm_SourceData[count * 3 \u002B 1] = new Vector4( forward.x, forward.y, source.WorldHalfSize.x, source.WorldHalfSize.y );\n\t\t\tm_SourceData[count * 3 \u002B 2] = new Vector4( source.HeightRange.x, source.HeightRange.y, source.BandWidth * Ocean.UnitsPerMetre, 0 );\n\t\t\tcount\u002B\u002B;\n\t\t}\n\n\t\tif ( count \u003E 0 )\n\t\t\tm_Sources.SetData( m_SourceData.AsSpan( 0, count * 3 ), 0 );\n\n\t\treturn count;\n\t}\n\n\tprivate void Record( Ocean ocean, Vector2 origin, Vector2 shift, int sources )\n\t{\n\t\tvar cl = m_Commands.List;\n\t\tcl.Reset();\n\n\t\tvar previous = m_Flip ? m_B : m_A;\n\t\tm_Flip = !m_Flip;\n\t\tvar next = m_Flip ? m_B : m_A;\n\n\t\tusing ( cl.ProfileScope( s_Profile ) )\n\t\t{\n\t\t\tocean.Bind( cl );\n\n\t\t\tcl.Attributes.Set( \u0022FoamPrevious\u0022, previous );\n\t\t\tcl.Attributes.Set( \u0022FoamNext\u0022, next );\n\t\t\tcl.Attributes.Set( \u0022FoamSize\u0022, m_Size );\n\t\t\tcl.Attributes.Set( \u0022FoamOrigin\u0022, origin );\n\t\t\tcl.Attributes.Set( \u0022FoamTexel\u0022, m_Texel );\n\t\t\tcl.Attributes.Set( \u0022FoamShift\u0022, shift );\n\n\t\t\tfloat delta = Time.Delta * ocean.TimeScale;\n\t\t\tcl.Attributes.Set( \u0022FoamDecay\u0022, MathF.Exp( -delta / MathF.Max( Lifetime, 0.01f ) ) );\n\t\t\tcl.Attributes.Set( \u0022FoamBlur\u0022, MathF.Min( Spread * delta * 30.0f, 0.9f ) );\n\t\t\tcl.Attributes.Set( \u0022FoamThreshold\u0022, CrestThreshold );\n\t\t\tcl.Attributes.Set( \u0022FoamCrestRate\u0022, ocean.Sea.CrestFoam );\n\t\t\tcl.Attributes.Set( \u0022FoamDelta\u0022, delta );\n\t\t\tcl.Attributes.Set( \u0022FoamSourceCount\u0022, sources );\n\t\t\tcl.Attributes.Set( \u0022FoamSources\u0022, m_Sources );\n\n\t\t\tcl.DispatchCompute( m_Shader, m_Size, m_Size, 1 );\n\t\t\tcl.UavBarrier( next );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EHands the foam window to anything drawn with the ocean surface shader.\u003C/summary\u003E\n\tpublic void Bind( RenderAttributes attributes )\n\t{\n\t\tif ( Current is null )\n\t\t\treturn;\n\n\t\tattributes.Set( \u0022OceanFoamMap\u0022, Current );\n\t\tattributes.Set( \u0022OceanFoamOrigin\u0022, Origin );\n\t\tattributes.Set( \u0022OceanFoamExtent\u0022, WorldExtent );\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"OceanSpectrum.cs","FileName":"OceanSpectrum.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// The initial wave spectrum h0(k) of one ocean cascade, after Tessendorf: a Phillips spectrum for a wind of a given\n/// speed and direction, with each wave\u0027s amplitude and phase drawn from a Gaussian. A cascade only keeps the waves in\n/// its own band of wave numbers, so several cascades of different tile sizes add up to one sea without counting any\n/// wave twice; amplitudes carry the cascade\u0027s frequency spacing (dk), so the bands join smoothly whatever their tile.\n///\n/// Everything comes from integer hashes of the seed and the wave\u0027s index - never from a shared random generator - so\n/// every machine, the GPU\u0027s surface and any CPU-side replica of it for physics build exactly the same sea.\n/// \u003C/summary\u003E\npublic sealed class OceanSpectrum\n{\n\tpublic const float Gravity = 9.81f;\n\n\t/// \u003Csummary\u003EFFT resolution: waves per side of the tile.\u003C/summary\u003E\n\tpublic int Size { get; }\n\n\t/// \u003Csummary\u003EMetres covered by one tile.\u003C/summary\u003E\n\tpublic float TileLength { get; }\n\n\t/// \u003Csummary\u003Eh0(k) in .xy and conj(h0(-k)) in .zw, row-major, wave vectors centred on index Size / 2.\u003C/summary\u003E\n\tpublic Vector4[] Texels { get; }\n\n\t/// \u003Csummary\u003ESum of |h0|^2 before any scaling: this cascade\u0027s share of the sea\u0027s height variance, halved.\u003C/summary\u003E\n\tpublic double RawVariance { get; }\n\n\t/// \u003Csummary\u003EThe settings shared by every cascade of one sea.\u003C/summary\u003E\n\tpublic readonly record struct Settings( float WindSpeed, float WindAngle, float Directionality, float SmallWaveCutoff, int Seed );\n\n\t/// \u003Csummary\u003E\n\t/// Builds the waves with wave numbers in [minK, maxK) - radians per metre - at their raw Phillips amplitude. Call\n\t/// \u003Csee cref=\u0022Scale\u0022/\u003E afterwards with the factor that brings all cascades together to the height wanted.\n\t/// \u003C/summary\u003E\n\tpublic OceanSpectrum( int size, float tileLength, float minK, float maxK, int cascade, Settings settings )\n\t{\n\t\tSize = size;\n\t\tTileLength = tileLength;\n\n\t\tvar h0 = new Vector2[size * size];\n\t\t// The leading term of h(k, t) - h0(k) e^{i w t} - runs against its wave vector, so the waves that should travel\n\t\t// downwind are the ones whose k points upwind: the spectrum is laid out against the wind.\n\t\tvar wind = -new Vector2( MathF.Cos( settings.WindAngle.DegreeToRadian() ), MathF.Sin( settings.WindAngle.DegreeToRadian() ) );\n\n\t\t// The longest wave a wind of this speed raises; the spectrum falls away below it.\n\t\tfloat longest = settings.WindSpeed * settings.WindSpeed / Gravity;\n\t\tfloat shortest = settings.SmallWaveCutoff;\n\t\tfloat dk = 2.0f * MathF.PI / tileLength;\n\t\tdouble variance = 0.0;\n\n\t\tfor ( int y = 0; y \u003C size; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C size; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar k = WaveVector( x, y );\n\t\t\t\tfloat kLength = k.Length;\n\n\t\t\t\t// The first row and column hold the Nyquist frequency, which is its own mirror image: a displacement\n\t\t\t\t// along it can\u0027t be represented by a real field, and would leak into the height packed beside it.\n\t\t\t\tif ( kLength \u003C 1e-6f || x == 0 || y == 0 || kLength \u003C minK || kLength \u003E= maxK )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar direction = k / kLength;\n\t\t\t\tfloat alignment = Vector2.Dot( direction, wind );\n\n\t\t\t\t// Phillips: P(k) = exp(-1/(kL)^2) / k^4 * |k.w|^p, small waves damped by exp(-k^2 l^2). Waves running\n\t\t\t\t// against the wind keep only a little of their energy.\n\t\t\t\tfloat kl = kLength * longest;\n\t\t\t\tfloat phillips = MathF.Exp( -1.0f / (kl * kl) ) / (kLength * kLength * kLength * kLength);\n\t\t\t\tphillips *= MathF.Pow( MathF.Abs( alignment ), settings.Directionality );\n\t\t\t\tphillips *= MathF.Exp( -kLength * kLength * shortest * shortest );\n\n\t\t\t\tif ( alignment \u003C 0.0f )\n\t\t\t\t\tphillips *= 0.07f;\n\n\t\t\t\tvar gauss = Gaussian( settings.Seed, cascade, x, y );\n\t\t\t\th0[y * size \u002B x] = gauss * (MathF.Sqrt( phillips * 0.5f ) * dk);\n\t\t\t\tvariance \u002B= h0[y * size \u002B x].LengthSquared;\n\t\t\t}\n\t\t}\n\n\t\tRawVariance = variance;\n\t\tTexels = new Vector4[size * size];\n\n\t\tfor ( int y = 0; y \u003C size; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C size; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = h0[y * size \u002B x];\n\t\t\t\tvar b = h0[((size - y) % size) * size \u002B (size - x) % size];\n\t\t\t\tTexels[y * size \u002B x] = new Vector4( a.x, a.y, b.x, -b.y );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate OceanSpectrum( int size, float tileLength, Vector4[] texels )\n\t{\n\t\tSize = size;\n\t\tTileLength = tileLength;\n\t\tTexels = texels;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The spectrum part way between two of the same tile: the sea is linear in its amplitudes, so this is exactly the\n\t/// sea a blend between them shows.\n\t/// \u003C/summary\u003E\n\tpublic static OceanSpectrum Lerp( OceanSpectrum a, OceanSpectrum b, float t )\n\t{\n\t\tvar texels = new Vector4[a.Texels.Length];\n\n\t\tfor ( int i = 0; i \u003C texels.Length; i\u002B\u002B )\n\t\t\ttexels[i] = a.Texels[i] \u002B (b.Texels[i] - a.Texels[i]) * t;\n\n\t\treturn new OceanSpectrum( a.Size, a.TileLength, texels );\n\t}\n\n\t/// \u003Csummary\u003EMultiplies every amplitude.\u003C/summary\u003E\n\tpublic void Scale( float factor )\n\t{\n\t\tfor ( int i = 0; i \u003C Texels.Length; i\u002B\u002B )\n\t\t\tTexels[i] *= factor;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The factor that brings a set of cascades to a significant wave height (four standard deviations of the height,\n\t/// in metres). h(x) sums h0(k) and conj(h0(-k)) for every k, so its variance is twice the sum of |h0|^2.\n\t/// \u003C/summary\u003E\n\tpublic static float ScaleFor( float significantHeight, double totalRawVariance )\n\t{\n\t\treturn totalRawVariance \u003E 0.0 ? significantHeight / (4.0f * MathF.Sqrt( (float)(2.0 * totalRawVariance) )) : 0.0f;\n\t}\n\n\t/// \u003Csummary\u003EThe wave vector at a texel, in radians per metre.\u003C/summary\u003E\n\tpublic Vector2 WaveVector( int x, int y ) =\u003E new Vector2( x - Size / 2, y - Size / 2 ) * (2.0f * MathF.PI / TileLength);\n\n\t/// \u003Csummary\u003EA pair of independent standard Gaussians for one wave, by Box-Muller over two hashed uniforms.\u003C/summary\u003E\n\tprivate static Vector2 Gaussian( int seed, int cascade, int x, int y )\n\t{\n\t\tfloat u1 = MathF.Max( Uniform( seed, cascade, x, y, 0 ), 1e-7f );\n\t\tfloat u2 = Uniform( seed, cascade, x, y, 1 );\n\t\tfloat r = MathF.Sqrt( -2.0f * MathF.Log( u1 ) );\n\t\treturn new Vector2( r * MathF.Cos( 2.0f * MathF.PI * u2 ), r * MathF.Sin( 2.0f * MathF.PI * u2 ) );\n\t}\n\n\t/// \u003Csummary\u003EA uniform in [0, 1) from a PCG-style integer hash: the same on every machine.\u003C/summary\u003E\n\tprivate static float Uniform( int seed, int cascade, int x, int y, int channel )\n\t{\n\t\tuint h = (uint)seed * 747796405u \u002B 2891336453u \u002B (uint)cascade * 0x27D4EB2Fu;\n\t\th ^= (uint)x * 0x9E3779B1u;\n\t\th = Pcg( h ) ^ ((uint)y * 0x85EBCA77u);\n\t\th = Pcg( h ) ^ ((uint)channel * 0xC2B2AE3Du);\n\t\th = Pcg( h );\n\t\treturn (h \u003E\u003E 8) * (1.0f / 16777216.0f);\n\t}\n\n\tprivate static uint Pcg( uint v )\n\t{\n\t\tuint state = v * 747796405u \u002B 2891336453u;\n\t\tuint word = ((state \u003E\u003E (int)((state \u003E\u003E 28) \u002B 4u)) ^ state) * 277803737u;\n\t\treturn (word \u003E\u003E 22) ^ word;\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"CameraCommands.cs","FileName":"CameraCommands.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// A command list kept on whichever camera the scene renders with, moving to the new one when the scene\u0027s camera\n/// changes. Call \u003Csee cref=\u0022Follow\u0022/\u003E every frame and \u003Csee cref=\u0022Dispose\u0022/\u003E when done.\n/// \u003C/summary\u003E\ninternal sealed class CameraCommands\n{\n\tpublic CommandList List { get; }\n\tpublic CameraComponent Camera { get; private set; }\n\n\tprivate readonly Stage m_Stage;\n\tprivate readonly int m_Order;\n\n\tpublic CameraCommands( string name, Stage stage, int order )\n\t{\n\t\tList = new CommandList( name );\n\t\tm_Stage = stage;\n\t\tm_Order = order;\n\t}\n\n\tpublic void Follow( Scene scene )\n\t{\n\t\tvar camera = scene.Camera;\n\n\t\tif ( camera == Camera )\n\t\t\treturn;\n\n\t\tif ( Camera.IsValid() )\n\t\t\tCamera.RemoveCommandList( List );\n\n\t\tCamera = camera;\n\t\tCamera?.AddCommandList( List, m_Stage, m_Order );\n\t}\n\n\tpublic void Dispose()\n\t{\n\t\tif ( Camera.IsValid() )\n\t\t\tCamera.RemoveCommandList( List );\n\n\t\tCamera = null;\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanWaves.cs","FileName":"OceanWaves.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace OceanLib;\r\n\r\n/// \u003Csummary\u003E\r\n/// The sea an \u003Csee cref=\u0022Ocean\u0022/\u003E draws, replayed on the CPU for physics. It takes the swell cascade - every wave\r\n/// longer than a few metres, the ones that lift and roll a hull - and rebuilds it exactly with an FFT of its own,\r\n/// small enough for the CPU because the swell\u0027s waves all sit near the middle of the spectrum. The chop of the finer\r\n/// cascades isn\u0027t replayed: a hull rides over it. No GPU readback and no latency, and it runs the same on a server\r\n/// with no GPU, so every machine floats its boats on the same water.\r\n///\r\n/// It matches the GPU wave for wave: same wave vectors, frequencies snapped to the loop period in the same float\r\n/// arithmetic, and the same half-texel shift the GPU\u0027s sampler applies to the cascade\u0027s map. The grid is rebuilt\r\n/// \u003Csee cref=\u0022Rate\u0022/\u003E times a second of sea time and blended in between - the swell\u0027s shortest waves take over two\r\n/// seconds to pass - and each one is built ahead of time on a worker thread (\u003Csee cref=\u0022Prefetch\u0022/\u003E), so the physics\r\n/// never waits for one. The sea is linear in its amplitudes, so each grid holds both sea states apart and the blend\r\n/// between them is applied as it\u0027s read: exact, whatever the blend has moved to since the grid was built.\r\n/// \u003C/summary\u003E\r\npublic sealed class OceanWaves\r\n{\r\n\t/// \u003Csummary\u003EGrid rebuilds per second of sea time.\u003C/summary\u003E\r\n\tpublic const float Rate = 20.0f;\r\n\r\n\t/// \u003Csummary\u003EPoints per side of the CPU\u0027s grid.\u003C/summary\u003E\r\n\tpublic int GridSize { get; }\r\n\r\n\t/// \u003Csummary\u003EHow many waves it replays.\u003C/summary\u003E\r\n\tpublic int Count { get; }\r\n\r\n\t/// \u003Csummary\u003EMetres the grid covers: the swell cascade\u0027s tile.\u003C/summary\u003E\r\n\tpublic float TileLength { get; }\r\n\r\n\t/// \u003Csummary\u003EHow far from the first sea state to the second, as on the GPU.\u003C/summary\u003E\r\n\tpublic float Blend { get; set; }\r\n\r\n\t/// \u003Csummary\u003EGrids built on the main thread because none was ready - ideally none after the first.\u003C/summary\u003E\r\n\tpublic int SynchronousBuilds { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EGrids built on worker threads.\u003C/summary\u003E\r\n\tpublic int BackgroundBuilds { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EThe sea at one moment, on the grid, for each sea state: height, x-shift and y-shift in metres.\u003C/summary\u003E\r\n\tprivate sealed class Grid\r\n\t{\r\n\t\t// The moment held, as a count of steps: whole numbers, so a moment found and a moment asked for always match.\r\n\t\tpublic int Step = int.MinValue;\r\n\r\n\t\t// Built on the main thread (Pending null and Filled), or by a worker whose task has finished - which is also\r\n\t\t// what makes its writes visible here.\r\n\t\tpublic bool Filled;\r\n\t\tpublic Task Pending;\r\n\t\tpublic bool Ready =\u003E Filled \u0026\u0026 (Pending is null || Pending.IsCompleted);\r\n\r\n\t\tpublic float[][] Height, ShiftX, ShiftY;\r\n\t\tpublic float[] WorkRe, WorkIm, PairRe, PairIm, LineRe, LineIm;\r\n\t}\r\n\r\n\t// The two in use, and the next three: a velocity reads the sea a step ahead, and the physics can run a little ahead\r\n\t// of the frame that asked for the prefetch.\r\n\tprivate const int GridCount = 5;\r\n\tprivate const int Lookahead = 3;\r\n\r\n\tprivate readonly int m_Log;\r\n\tprivate readonly float m_Shift;\r\n\tprivate readonly float m_Loop;\r\n\tprivate readonly bool m_Same;\r\n\tprivate readonly int[] m_Index;\r\n\tprivate readonly float[] m_Kx, m_Ky, m_Omega;\r\n\r\n\t// Each wave\u0027s h0 and conj(h0(-k)), per sea state.\r\n\tprivate readonly float[][] m_Are, m_Aim, m_Cre, m_Cim;\r\n\r\n\tprivate readonly float[] m_TwiddleRe, m_TwiddleIm;\r\n\tprivate readonly int[] m_Reverse;\r\n\tprivate readonly List\u003CGrid\u003E m_Grids = new();\r\n\r\n\tpublic OceanWaves( OceanSpectrum from, OceanSpectrum to, float loopPeriod )\r\n\t{\r\n\t\tint n = from.Size;\r\n\t\tTileLength = from.TileLength;\r\n\t\tm_Loop = loopPeriod;\r\n\t\tm_Same = ReferenceEquals( from, to );\r\n\r\n\t\t// Every wave either sea state has, and how far from the middle of the spectrum they reach: the grid just has\r\n\t\t// to hold them.\r\n\t\tint reach = 0, count = 0;\r\n\r\n\t\tfor ( int i = 0; i \u003C from.Texels.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( from.Texels[i] == Vector4.Zero \u0026\u0026 to.Texels[i] == Vector4.Zero )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tcount\u002B\u002B;\r\n\t\t\treach = Math.Max( reach, Math.Max( Math.Abs( i % n - n / 2 ), Math.Abs( i / n - n / 2 ) ) );\r\n\t\t}\r\n\r\n\t\tGridSize = 16;\r\n\t\twhile ( GridSize / 2 \u003C= reach )\r\n\t\t\tGridSize *= 2;\r\n\r\n\t\tGridSize = Math.Min( GridSize, n );\r\n\t\tm_Log = (int)Math.Round( Math.Log2( GridSize ) );\r\n\r\n\t\t// The GPU samples its map with texel centres half a texel in, so the map it shows at a spot is the sea half a\r\n\t\t// texel back.\r\n\t\tm_Shift = 0.5f * TileLength / n;\r\n\r\n\t\tCount = count;\r\n\t\tm_Index = new int[count];\r\n\t\tm_Kx = new float[count]; m_Ky = new float[count]; m_Omega = new float[count];\r\n\t\tm_Are = new[] { new float[count], new float[count] };\r\n\t\tm_Aim = new[] { new float[count], new float[count] };\r\n\t\tm_Cre = new[] { new float[count], new float[count] };\r\n\t\tm_Cim = new[] { new float[count], new float[count] };\r\n\r\n\t\tfloat omega0 = 2.0f * MathF.PI / loopPeriod;\r\n\t\tint w = 0;\r\n\r\n\t\tfor ( int i = 0; i \u003C from.Texels.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar a = from.Texels[i];\r\n\t\t\tvar b = to.Texels[i];\r\n\r\n\t\t\tif ( a == Vector4.Zero \u0026\u0026 b == Vector4.Zero )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tint ix = i % n - n / 2, iy = i / n - n / 2;\r\n\t\t\tvar k = from.WaveVector( i % n, i / n );\r\n\r\n\t\t\tm_Index[w] = ((iy \u002B GridSize) % GridSize) * GridSize \u002B (ix \u002B GridSize) % GridSize;\r\n\t\t\tm_Kx[w] = k.x;\r\n\t\t\tm_Ky[w] = k.y;\r\n\r\n\t\t\t// As the GPU works it out, in floats, so a frequency on the edge of a step snaps the same way.\r\n\t\t\tm_Omega[w] = MathF.Floor( MathF.Sqrt( 9.81f * k.Length ) / omega0 ) * omega0;\r\n\t\t\tm_Are[0][w] = a.x; m_Aim[0][w] = a.y; m_Cre[0][w] = a.z; m_Cim[0][w] = a.w;\r\n\t\t\tm_Are[1][w] = b.x; m_Aim[1][w] = b.y; m_Cre[1][w] = b.z; m_Cim[1][w] = b.w;\r\n\t\t\tw\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tm_TwiddleRe = new float[GridSize / 2];\r\n\t\tm_TwiddleIm = new float[GridSize / 2];\r\n\r\n\t\t// Inverse transform: e^{\u002Bi 2 pi j / N}.\r\n\t\tfor ( int j = 0; j \u003C GridSize / 2; j\u002B\u002B )\r\n\t\t\t(m_TwiddleIm[j], m_TwiddleRe[j]) = MathF.SinCos( 2.0f * MathF.PI * j / GridSize );\r\n\r\n\t\tm_Reverse = new int[GridSize];\r\n\t\tfor ( int j = 0; j \u003C GridSize; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tint r = 0;\r\n\t\t\tfor ( int bit = 0; bit \u003C m_Log; bit\u002B\u002B )\r\n\t\t\t\tr |= ((j \u003E\u003E bit) \u0026 1) \u003C\u003C (m_Log - 1 - bit);\r\n\t\t\tm_Reverse[j] = r;\r\n\t\t}\r\n\r\n\t\tfor ( int g = 0; g \u003C GridCount; g\u002B\u002B )\r\n\t\t\tm_Grids.Add( NewGrid() );\r\n\t}\r\n\r\n\tprivate Grid NewGrid()\r\n\t{\r\n\t\tint cells = GridSize * GridSize;\r\n\t\tint states = m_Same ? 1 : 2;\r\n\r\n\t\tvar grid = new Grid\r\n\t\t{\r\n\t\t\tHeight = new float[states][], ShiftX = new float[states][], ShiftY = new float[states][],\r\n\t\t\tWorkRe = new float[cells], WorkIm = new float[cells], PairRe = new float[cells], PairIm = new float[cells],\r\n\t\t\tLineRe = new float[GridSize], LineIm = new float[GridSize],\r\n\t\t};\r\n\r\n\t\tfor ( int s = 0; s \u003C states; s\u002B\u002B )\r\n\t\t{\r\n\t\t\tgrid.Height[s] = new float[cells];\r\n\t\t\tgrid.ShiftX[s] = new float[cells];\r\n\t\t\tgrid.ShiftY[s] = new float[cells];\r\n\t\t}\r\n\r\n\t\treturn grid;\r\n\t}\r\n\r\n\tprivate static int StepOf( float time ) =\u003E (int)MathF.Floor( time * Rate );\r\n\r\n\tprivate Grid Find( int step )\r\n\t{\r\n\t\tforeach ( var g in m_Grids )\r\n\t\t\tif ( g.Ready \u0026\u0026 g.Step == step )\r\n\t\t\t\treturn g;\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\tprivate bool Pending( int step )\r\n\t{\r\n\t\tforeach ( var g in m_Grids )\r\n\t\t\tif ( g.Pending is { IsCompleted: false } \u0026\u0026 g.Step == step )\r\n\t\t\t\treturn true;\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA grid not being built and not one of the moments in use: the oldest.\u003C/summary\u003E\r\n\tprivate Grid FreeGrid( int keepA, int keepB )\r\n\t{\r\n\t\tGrid best = null;\r\n\r\n\t\tforeach ( var g in m_Grids )\r\n\t\t{\r\n\t\t\tif ( g.Pending is { IsCompleted: false } )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( g.Ready \u0026\u0026 (g.Step == keepA || g.Step == keepB) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( best is null || !g.Ready || (best.Ready \u0026\u0026 g.Step \u003C best.Step) )\r\n\t\t\t\tbest = g;\r\n\t\t}\r\n\r\n\t\treturn best;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Starts building, on worker threads, the grids the physics will need next after this time - call once a frame.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Prefetch( float time )\r\n\t{\r\n\t\tint before = StepOf( time );\r\n\r\n\t\tfor ( int ahead = 1; ahead \u003C= Lookahead; ahead\u002B\u002B )\r\n\t\t{\r\n\t\t\tint step = before \u002B ahead;\r\n\r\n\t\t\tif ( Find( step ) is not null || Pending( step ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar grid = FreeGrid( before, before \u002B 1 );\r\n\r\n\t\t\tif ( grid is null )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tgrid.Step = step;\r\n\t\t\tgrid.Filled = true;\r\n\t\t\tBackgroundBuilds\u002B\u002B;\r\n\t\t\tgrid.Pending = GameTask.RunInThreadAsync( () =\u003E Build( grid, step / Rate ) );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe grids either side of a time - built here only if no worker got to them - and how far between.\u003C/summary\u003E\r\n\tprivate (Grid, Grid, float) Prepare( float time )\r\n\t{\r\n\t\tint before = StepOf( time );\r\n\r\n\t\tvar a = Find( before ) ?? BuildNow( before, before \u002B 1 );\r\n\t\tvar b = Find( before \u002B 1 ) ?? BuildNow( before \u002B 1, before );\r\n\t\treturn (a, b, time * Rate - before);\r\n\t}\r\n\r\n\tprivate Grid BuildNow( int step, int keep )\r\n\t{\r\n\t\t// Never waits on a worker: its task completes through the main thread, so waiting for it here never ends. A step\r\n\t\t// a worker is still on is built again here, and a spare grid is added if every one is busy - rare, and kept.\r\n\t\tvar grid = FreeGrid( keep, keep );\r\n\r\n\t\tif ( grid is null )\r\n\t\t{\r\n\t\t\tgrid = NewGrid();\r\n\t\t\tm_Grids.Add( grid );\r\n\t\t}\r\n\r\n\t\tgrid.Pending = null;\r\n\t\tgrid.Step = step;\r\n\t\tBuild( grid, step / Rate );\r\n\t\tgrid.Filled = true;\r\n\t\tSynchronousBuilds\u002B\u002B;\r\n\t\treturn grid;\r\n\t}\r\n\r\n\tprivate void Build( Grid grid, float time )\r\n\t{\r\n\t\tfor ( int state = 0; state \u003C grid.Height.Length; state\u002B\u002B )\r\n\t\t\tBuildState( grid, time, state );\r\n\t}\r\n\r\n\tprivate void BuildState( Grid grid, float time, int state )\r\n\t{\r\n\t\tArray.Clear( grid.WorkRe ); Array.Clear( grid.WorkIm ); Array.Clear( grid.PairRe ); Array.Clear( grid.PairIm );\r\n\r\n\t\t// The waves repeat over the loop period, so any time past it is the same sea as the time it wraps to.\r\n\t\tfloat t = time % m_Loop;\r\n\t\tvar are = m_Are[state]; var aim = m_Aim[state]; var cre = m_Cre[state]; var cim = m_Cim[state];\r\n\r\n\t\tfor ( int w = 0; w \u003C Count; w\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar (s, c) = MathF.SinCos( m_Omega[w] * t );\r\n\r\n\t\t\t// h(k, t) = h0(k) e^{i w t} \u002B conj(h0(-k)) e^{-i w t}\r\n\t\t\tfloat re = are[w] * c - aim[w] * s \u002B cre[w] * c \u002B cim[w] * s;\r\n\t\t\tfloat im = are[w] * s \u002B aim[w] * c - cre[w] * s \u002B cim[w] * c;\r\n\r\n\t\t\tfloat kl = MathF.Sqrt( m_Kx[w] * m_Kx[w] \u002B m_Ky[w] * m_Ky[w] );\r\n\t\t\tfloat nx = m_Kx[w] / kl, ny = m_Ky[w] / kl;\r\n\r\n\t\t\t// Two real fields share one complex FFT as a \u002B i b: height plus i times the x-shift, whose spectrum is\r\n\t\t\t// i kx/|k| h - so i times it is -kx/|k| h. The y-shift goes alone: i ky/|k| h.\r\n\t\t\tint at = m_Index[w];\r\n\t\t\tgrid.WorkRe[at] \u002B= re * (1.0f - nx);\r\n\t\t\tgrid.WorkIm[at] \u002B= im * (1.0f - nx);\r\n\t\t\tgrid.PairRe[at] \u002B= -im * ny;\r\n\t\t\tgrid.PairIm[at] \u002B= re * ny;\r\n\t\t}\r\n\r\n\t\tTransform( grid, grid.WorkRe, grid.WorkIm );\r\n\t\tTransform( grid, grid.PairRe, grid.PairIm );\r\n\r\n\t\tint cells = GridSize * GridSize;\r\n\t\tArray.Copy( grid.WorkRe, grid.Height[state], cells );\r\n\t\tArray.Copy( grid.WorkIm, grid.ShiftX[state], cells );\r\n\t\tArray.Copy( grid.PairRe, grid.ShiftY[state], cells );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EInverse 2D FFT in place: every row, then every column, radix 2.\u003C/summary\u003E\r\n\tprivate void Transform( Grid grid, float[] re, float[] im )\r\n\t{\r\n\t\tint n = GridSize;\r\n\r\n\t\tfor ( int row = 0; row \u003C n; row\u002B\u002B )\r\n\t\t\tLine( grid, re, im, row * n, 1 );\r\n\r\n\t\tfor ( int column = 0; column \u003C n; column\u002B\u002B )\r\n\t\t\tLine( grid, re, im, column, n );\r\n\t}\r\n\r\n\tprivate void Line( Grid grid, float[] re, float[] im, int start, int stride )\r\n\t{\r\n\t\tint n = GridSize;\r\n\t\tvar lr = grid.LineRe;\r\n\t\tvar li = grid.LineIm;\r\n\r\n\t\tfor ( int j = 0; j \u003C n; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tint from = start \u002B m_Reverse[j] * stride;\r\n\t\t\tlr[j] = re[from];\r\n\t\t\tli[j] = im[from];\r\n\t\t}\r\n\r\n\t\tfor ( int size = 2; size \u003C= n; size \u003C\u003C= 1 )\r\n\t\t{\r\n\t\t\tint half = size \u003E\u003E 1;\r\n\t\t\tint step = n / size;\r\n\r\n\t\t\tfor ( int block = 0; block \u003C n; block \u002B= size )\r\n\t\t\t{\r\n\t\t\t\tfor ( int j = 0; j \u003C half; j\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat wr = m_TwiddleRe[j * step], wi = m_TwiddleIm[j * step];\r\n\t\t\t\t\tint a = block \u002B j, b = a \u002B half;\r\n\t\t\t\t\tfloat tr = lr[b] * wr - li[b] * wi;\r\n\t\t\t\t\tfloat ti = lr[b] * wi \u002B li[b] * wr;\r\n\t\t\t\t\tlr[b] = lr[a] - tr; li[b] = li[a] - ti;\r\n\t\t\t\t\tlr[a] \u002B= tr; li[a] \u002B= ti;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor ( int j = 0; j \u003C n; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tre[start \u002B j * stride] = lr[j];\r\n\t\t\tim[start \u002B j * stride] = li[j];\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The swell\u0027s displacement at a point of the undisplaced grid, in metres: x and y the choppy shift, z the height.\r\n\t/// Read off the grids with a Catmull-Rom curve, smooth between their points, blended between the two sea states\r\n\t/// and between the two moments either side.\r\n\t/// \u003C/summary\u003E\r\n\tpublic Vector3 Displacement( Vector2 metres, float time, float choppiness )\r\n\t{\r\n\t\tvar (a, b, moment) = Prepare( time );\r\n\r\n\t\tint n = GridSize;\r\n\t\tfloat u = (metres.x - m_Shift) / TileLength * n;\r\n\t\tfloat v = (metres.y - m_Shift) / TileLength * n;\r\n\t\tint x0 = (int)MathF.Floor( u ), y0 = (int)MathF.Floor( v );\r\n\r\n\t\tSpan\u003Cfloat\u003E wx = stackalloc float[4];\r\n\t\tSpan\u003Cfloat\u003E wy = stackalloc float[4];\r\n\t\tCatmullRom( u - x0, wx );\r\n\t\tCatmullRom( v - y0, wy );\r\n\r\n\t\t// The two moments, each blended between the sea states, then between each other.\r\n\t\tfloat state = m_Same ? 0.0f : Blend;\r\n\t\tint last = m_Same ? 0 : 1;\r\n\t\tfloat wa0 = (1.0f - moment) * (1.0f - state), wa1 = (1.0f - moment) * state;\r\n\t\tfloat wb0 = moment * (1.0f - state), wb1 = moment * state;\r\n\t\tvar ah0 = a.Height[0]; var ah1 = a.Height[last]; var bh0 = b.Height[0]; var bh1 = b.Height[last];\r\n\t\tvar ax0 = a.ShiftX[0]; var ax1 = a.ShiftX[last]; var bx0 = b.ShiftX[0]; var bx1 = b.ShiftX[last];\r\n\t\tvar ay0 = a.ShiftY[0]; var ay1 = a.ShiftY[last]; var by0 = b.ShiftY[0]; var by1 = b.ShiftY[last];\r\n\r\n\t\tfloat h = 0.0f, dx = 0.0f, dy = 0.0f;\r\n\r\n\t\tfor ( int j = 0; j \u003C 4; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tint row = ((y0 - 1 \u002B j) % n \u002B n) % n * n;\r\n\r\n\t\t\tfor ( int i = 0; i \u003C 4; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tint at = row \u002B ((x0 - 1 \u002B i) % n \u002B n) % n;\r\n\t\t\t\tfloat weight = wx[i] * wy[j];\r\n\t\t\t\th \u002B= (ah0[at] * wa0 \u002B ah1[at] * wa1 \u002B bh0[at] * wb0 \u002B bh1[at] * wb1) * weight;\r\n\t\t\t\tdx \u002B= (ax0[at] * wa0 \u002B ax1[at] * wa1 \u002B bx0[at] * wb0 \u002B bx1[at] * wb1) * weight;\r\n\t\t\t\tdy \u002B= (ay0[at] * wa0 \u002B ay1[at] * wa1 \u002B by0[at] * wb0 \u002B by1[at] * wb1) * weight;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn new Vector3( dx * choppiness, dy * choppiness, h );\r\n\t}\r\n\r\n\tprivate static void CatmullRom( float t, Span\u003Cfloat\u003E weights )\r\n\t{\r\n\t\tfloat t2 = t * t, t3 = t2 * t;\r\n\t\tweights[0] = -0.5f * t3 \u002B t2 - 0.5f * t;\r\n\t\tweights[1] = 1.5f * t3 - 2.5f * t2 \u002B 1.0f;\r\n\t\tweights[2] = -1.5f * t3 \u002B 2.0f * t2 \u002B 0.5f * t;\r\n\t\tweights[3] = 0.5f * t3 - 0.5f * t2;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The height of the swell\u0027s surface above a point in the world, in metres. The waves carry the water sideways as\r\n\t/// well as up, so the water over a spot started out elsewhere: a few rounds of \u0022where would it have come from\u0022\r\n\t/// find it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float HeightAt( Vector2 metres, float time, float choppiness, int iterations = 3 )\r\n\t{\r\n\t\tvar start = metres;\r\n\r\n\t\tfor ( int i = 0; i \u003C iterations; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar d = Displacement( start, time, choppiness );\r\n\t\t\tstart = metres - new Vector2( d.x, d.y );\r\n\t\t}\r\n\r\n\t\treturn Displacement( start, time, choppiness ).z;\r\n\t}\r\n}\r\n"},{"Ident":"schumi.ocean","Path":"OceanSpray.cs","FileName":"OceanSpray.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Spray where a hull cuts into the sea: when a bow slams into a wave face, the water it shoves aside is thrown up and\n/// out in torn white sheets, a scatter of droplets and a drifting mist, which arc over, fall back and are gone in a\n/// second or two - the wave breaking on the boat. The sea itself doesn\u0027t deform; the spray\n/// and the foam (\u003Csee cref=\u0022OceanFoamSource.Splash\u0022/\u003E) sell it. \u003Csee cref=\u0022OceanBuoyancy\u0022/\u003E calls \u003Csee cref=\u0022Emit\u0022/\u003E.\n///\n/// The particles live on the CPU - a few thousand at most, cheap to move - and are drawn as one batch of camera-facing\n/// quads with the ocean_spray shader, from a camera command list after the scene\u0027s transparent things. They are lit by\n/// the scene\u0027s sun and fog colours, so they go grey with the storm.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Spray\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022water_drop\u0022 )]\npublic sealed class OceanSpray : Component\n{\n\t/// \u003Csummary\u003EA material on the ocean_spray shader. Empty uses the library\u0027s.\u003C/summary\u003E\n\t[Property] public Material Material { get; set; }\n\n\t/// \u003Csummary\u003EThe most particles alive at once; older ones make way when it\u0027s full.\u003C/summary\u003E\n\t[Property, Range( 100, 8000 )] public int MaxParticles { get; set; } = 3000;\n\n\t/// \u003Csummary\u003EScales every burst: more sheets, more droplets.\u003C/summary\u003E\n\t[Property, Range( 0, 4 )] public float Amount { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EScales how fast and high the spray is thrown.\u003C/summary\u003E\n\t[Property, Range( 0.2f, 3 )] public float Force { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EThe sun lighting the spray. Empty takes the scene\u0027s first.\u003C/summary\u003E\n\t[Property] public DirectionalLight Sun { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// The fog whose colour shades the spray\u0027s underside, when it has a constant colour. Empty takes the scene\u0027s;\n\t/// without one - or with a fog coloured by the sky - the sun\u0027s sky light shades it instead.\n\t/// \u003C/summary\u003E\n\t[Property] public Fog Fog { get; set; }\n\n\t/// \u003Csummary\u003EThe first active spray in the scene, for anything that wants to throw some.\u003C/summary\u003E\n\tpublic static OceanSpray Current { get; private set; }\n\n\t/// \u003Csummary\u003EParticles alive now.\u003C/summary\u003E\n\tpublic int Count =\u003E m_Count;\n\n\tprivate enum Kind { Sheet = 0, Droplet = 1, Mist = 2 }\n\n\tprivate struct Particle\n\t{\n\t\tpublic Vector3 Position, Velocity;\n\t\tpublic float Age, Life, Size, Grow, Opacity, Seed, Floor, Drag;\n\t\tpublic Kind Kind;\n\t}\n\n\tprivate const float UnitsPerMetre = Ocean.UnitsPerMetre;\n\tprivate const int DrawOrder = 105;\n\n\tprivate Particle[] m_Particles = Array.Empty\u003CParticle\u003E();\n\tprivate int m_Count;\n\tprivate Vertex[] m_Vertices = Array.Empty\u003CVertex\u003E();\n\tprivate Mesh m_Mesh;\n\tprivate Model m_Model;\n\tprivate readonly CameraCommands m_Commands = new( \u0022Ocean Spray\u0022, Stage.AfterTransparent, DrawOrder );\n\tprivate readonly RenderAttributes m_Attributes = new();\n\n\tprotected override void OnEnabled()\n\t{\n\t\tCurrent ??= this;\n\t\tAllocate();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( Current == this )\n\t\t\tCurrent = null;\n\n\t\tm_Commands.Dispose();\n\t\tm_Model = null;\n\t}\n\n\tprivate void Allocate()\n\t{\n\t\tint max = Math.Clamp( MaxParticles, 100, 8000 );\n\t\tm_Particles = new Particle[max];\n\t\tm_Vertices = new Vertex[max * 4];\n\t\tm_Count = 0;\n\n\t\tvar material = Material ?? Material.Load( \u0022ocean/materials/ocean_spray.vmat\u0022 );\n\n\t\tif ( !material.IsValid() )\n\t\t\treturn;\n\n\t\tvar indices = new int[max * 6];\n\t\tfor ( int p = 0; p \u003C max; p\u002B\u002B )\n\t\t{\n\t\t\tint v = p * 4, i = p * 6;\n\t\t\tindices[i] = v; indices[i \u002B 1] = v \u002B 1; indices[i \u002B 2] = v \u002B 2;\n\t\t\tindices[i \u002B 3] = v; indices[i \u002B 4] = v \u002B 2; indices[i \u002B 5] = v \u002B 3;\n\t\t}\n\n\t\tm_Mesh = new Mesh( material );\n\t\tm_Mesh.CreateVertexBuffer\u003CVertex\u003E( m_Vertices.Length, m_Vertices.AsSpan() );\n\t\tm_Mesh.CreateIndexBuffer( indices.Length, indices.AsSpan() );\n\t\tm_Mesh.Bounds = new BBox( new Vector3( -1e7f ), new Vector3( 1e7f ) );\n\t\tm_Model = Model.Builder.AddMesh( m_Mesh ).Create();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Throws a burst of spray from a point on the waterline: up, out along \u003Cparamref name=\u0022outward\u0022/\u003E (away from the\n\t/// hull) and carried on with the hull\u0027s own velocity. Strength 1 is a solid slam into a wave; 3 is the biggest.\n\t/// \u003C/summary\u003E\n\tpublic void Emit( Vector3 at, Vector3 hullVelocity, Vector3 outward, float strength )\n\t{\n\t\tif ( m_Particles.Length == 0 || strength \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tstrength = MathF.Min( strength, 3.0f );\n\t\toutward = outward.WithZ( 0 );\n\t\toutward = outward.Length \u003E 1e-3f ? outward.Normal : Vector3.Forward;\n\t\tvar along = new Vector3( -outward.y, outward.x, 0 );\n\t\tfloat force = Force * UnitsPerMetre * MathF.Sqrt( strength );\n\t\tvar carried = hullVelocity * 0.7f;\n\n\t\tint sheets = Spread( 3.0f * strength * Amount );\n\t\tint droplets = Spread( 14.0f * strength * Amount );\n\t\tint mists = Spread( 1.5f * strength * Amount );\n\n\t\tfor ( int i = 0; i \u003C sheets; i\u002B\u002B )\n\t\t{\n\t\t\tvar dir = outward * Game.Random.Float( 1.5f, 4.0f ) \u002B along * Game.Random.Float( -1.5f, 1.5f ) \u002B Vector3.Up * Game.Random.Float( 3.0f, 6.5f );\n\t\t\tAdd( Kind.Sheet, at \u002B along * Game.Random.Float( -40, 40 ), carried \u002B dir * force, Game.Random.Float( 0.7f, 1.2f ),\n\t\t\t\tGame.Random.Float( 1.2f, 2.2f ) * UnitsPerMetre * MathF.Sqrt( strength ), 3.0f * UnitsPerMetre, 1.0f, 0.6f );\n\t\t}\n\n\t\tfor ( int i = 0; i \u003C droplets; i\u002B\u002B )\n\t\t{\n\t\t\tvar dir = outward * Game.Random.Float( 1.0f, 5.0f ) \u002B along * Game.Random.Float( -2.0f, 2.0f ) \u002B Vector3.Up * Game.Random.Float( 3.0f, 8.5f );\n\t\t\tAdd( Kind.Droplet, at \u002B along * Game.Random.Float( -50, 50 ), carried \u002B dir * force, Game.Random.Float( 0.9f, 1.8f ),\n\t\t\t\tGame.Random.Float( 0.08f, 0.16f ) * UnitsPerMetre, 0.0f, 1.0f, 0.05f );\n\t\t}\n\n\t\tfor ( int i = 0; i \u003C mists; i\u002B\u002B )\n\t\t{\n\t\t\tvar dir = outward * Game.Random.Float( 0.5f, 1.5f ) \u002B Vector3.Up * Game.Random.Float( 1.0f, 2.5f );\n\t\t\tAdd( Kind.Mist, at \u002B Vector3.Up * 40, carried * 0.5f \u002B dir * force * 0.6f, Game.Random.Float( 1.8f, 3.0f ),\n\t\t\t\tGame.Random.Float( 2.0f, 3.5f ) * UnitsPerMetre, 2.5f * UnitsPerMetre, 0.5f, 1.2f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA count from an average: the fraction becomes the chance of one more.\u003C/summary\u003E\n\tprivate static int Spread( float average ) =\u003E (int)average \u002B (Game.Random.Float() \u003C average - MathF.Floor( average ) ? 1 : 0);\n\n\tprivate void Add( Kind kind, Vector3 at, Vector3 velocity, float life, float size, float grow, float opacity, float drag )\n\t{\n\t\t// Full: the oldest particle makes way.\n\t\tint slot = m_Count;\n\n\t\tif ( m_Count \u003E= m_Particles.Length )\n\t\t{\n\t\t\tslot = 0;\n\t\t\tfor ( int i = 1; i \u003C m_Count; i\u002B\u002B )\n\t\t\t\tif ( m_Particles[i].Age / m_Particles[i].Life \u003E m_Particles[slot].Age / m_Particles[slot].Life )\n\t\t\t\t\tslot = i;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tm_Count\u002B\u002B;\n\t\t}\n\n\t\tm_Particles[slot] = new Particle\n\t\t{\n\t\t\tKind = kind, Position = at, Velocity = velocity, Life = life, Size = size, Grow = grow, Opacity = opacity,\n\t\t\tDrag = drag, Seed = Game.Random.Float(), Floor = at.z - 40.0f, Age = 0.0f\n\t\t};\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( m_Particles.Length != Math.Clamp( MaxParticles, 100, 8000 ) || (m_Model is null \u0026\u0026 Material.IsValid()) )\n\t\t\tAllocate();\n\n\t\tfloat dt = Time.Delta;\n\t\tvar gravity = Scene.PhysicsWorld.Gravity;\n\n\t\tfor ( int i = 0; i \u003C m_Count; )\n\t\t{\n\t\t\tref var p = ref m_Particles[i];\n\t\t\tp.Age \u002B= dt;\n\t\t\tp.Velocity \u002B= gravity * (p.Kind == Kind.Mist ? 0.05f : 1.0f) * dt;\n\t\t\tp.Velocity *= MathF.Max( 0.0f, 1.0f - p.Drag * dt );\n\t\t\tp.Position \u002B= p.Velocity * dt;\n\t\t\tp.Size \u002B= p.Grow * dt;\n\n\t\t\t// Gone once its time is up, or once it has fallen back into the sea.\n\t\t\tif ( p.Age \u003E= p.Life || (p.Velocity.z \u003C 0 \u0026\u0026 p.Position.z \u003C p.Floor) )\n\t\t\t{\n\t\t\t\tm_Particles[i] = m_Particles[--m_Count];\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ti\u002B\u002B;\n\t\t}\n\t}\n\n\tprotected override void OnPreRender()\n\t{\n\t\tm_Commands.List.Reset();\n\t\tm_Commands.Follow( Scene );\n\t\tvar camera = m_Commands.Camera;\n\n\t\tif ( m_Model is null || m_Mesh is null || m_Count == 0 || !camera.IsValid() )\n\t\t\treturn;\n\n\t\tvar right = camera.WorldRotation.Right;\n\t\tvar up = camera.WorldRotation.Up;\n\t\tvar forward = camera.WorldRotation.Forward;\n\n\t\tfor ( int i = 0; i \u003C m_Count; i\u002B\u002B )\n\t\t{\n\t\t\tref var p = ref m_Particles[i];\n\t\t\tfloat t = p.Age / p.Life;\n\t\t\tfloat half = p.Size * 0.5f;\n\n\t\t\t// Fades in at once and out over its last part; sheets thin out as they spread.\n\t\t\tfloat fade = MathF.Min( t * 12.0f, 1.0f ) * MathF.Min( (1.0f - t) * 3.0f, 1.0f );\n\t\t\tfloat opacity = p.Opacity * fade * (p.Kind == Kind.Sheet ? 1.0f - t * 0.5f : 1.0f);\n\n\t\t\tvar axisX = right * half;\n\t\t\tvar axisY = up * half;\n\n\t\t\t// Droplets stretch along their flight as seen from the camera.\n\t\t\tif ( p.Kind == Kind.Droplet )\n\t\t\t{\n\t\t\t\tvar flight = p.Velocity - forward * Vector3.Dot( p.Velocity, forward );\n\t\t\t\tfloat speed = flight.Length;\n\n\t\t\t\tif ( speed \u003E 1.0f )\n\t\t\t\t{\n\t\t\t\t\tvar dir = flight / speed;\n\t\t\t\t\taxisY = dir * half * (1.0f \u002B speed / UnitsPerMetre * 0.25f);\n\t\t\t\t\taxisX = Vector3.Cross( dir, forward ).Normal * half;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar packed = new Vector4( 0, 0, opacity, (int)p.Kind \u002B p.Seed * 0.099f );\n\t\t\tint v = i * 4;\n\t\t\tm_Vertices[v \u002B 0] = Corner( p.Position - axisX - axisY, -1, -1, opacity, packed.w );\n\t\t\tm_Vertices[v \u002B 1] = Corner( p.Position \u002B axisX - axisY, 1, -1, opacity, packed.w );\n\t\t\tm_Vertices[v \u002B 2] = Corner( p.Position \u002B axisX \u002B axisY, 1, 1, opacity, packed.w );\n\t\t\tm_Vertices[v \u002B 3] = Corner( p.Position - axisX \u002B axisY, -1, 1, opacity, packed.w );\n\t\t}\n\n\t\tm_Mesh.SetVertexBufferData\u003CVertex\u003E( m_Vertices.AsSpan( 0, m_Count * 4 ), 0 );\n\t\tm_Mesh.SetIndexRange( 0, m_Count * 6 );\n\n\t\t// Lit by the sun on top, the fog\u0027s or the sky\u0027s grey underneath: white in the sunshine, slate in a storm.\n\t\tvar sun = Sun.IsValid() ? Sun : Scene.GetAllComponents\u003CDirectionalLight\u003E().FirstOrDefault();\n\t\tvar fog = Fog.IsValid() ? Fog : Scene.GetAllComponents\u003CFog\u003E().FirstOrDefault();\n\t\tvar light = sun.IsValid() ? sun.LightColor : Color.White;\n\t\tvar shade = fog.IsValid() \u0026\u0026 fog.ColorSource == FogColorSource.Constant ? fog.Color\n\t\t\t: sun.IsValid() ? sun.SkyColor\n\t\t\t: new Color( 0.55f, 0.62f, 0.68f );\n\t\t// White water stays the brightest thing on a grey sea: a floor of light keeps it from sinking into the gloom.\n\t\tvar sky = new Vector3( shade.r, shade.g, shade.b );\n\t\tvar lit = new Vector3( light.r, light.g, light.b ) * 0.7f \u002B sky * 0.6f \u002B new Vector3( 0.12f );\n\n\t\tm_Attributes.Set( \u0022SprayLight\u0022, lit );\n\t\tm_Attributes.Set( \u0022SprayShadow\u0022, sky * 0.9f \u002B new Vector3( 0.05f ) );\n\t\tm_Commands.List.DrawModel( m_Model, global::Transform.Zero, m_Attributes );\n\t}\n\n\tprivate static Vertex Corner( Vector3 position, float u, float v, float opacity, float kind )\n\t{\n\t\treturn new Vertex\n\t\t{\n\t\t\tPosition = position,\n\t\t\tNormal = Vector3.Up,\n\t\t\tTangent = new Vector4( 1, 0, 0, 1 ),\n\t\t\tTexCoord0 = new Vector4( u, v, 0, 0 ),\n\t\t\tTexCoord1 = new Vector4( opacity, kind, 0, 0 ),\n\t\t\tColor = Color.White,\n\t\t};\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanDebugView.cs","FileName":"OceanDebugView.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\nusing System.Text;\nusing Sandbox.Rendering;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Shows what the ocean is doing, over the game view: each cascade\u0027s tile from above - its heights, and where it folds\n/// into breaking crests - the foam window round the camera, and the numbers behind the sea (the state and the blend,\n/// the CPU replica\u0027s work, the spray). For tuning and checking; leave it off in a game.\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Debug View\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022bug_report\u0022 )]\npublic sealed class OceanDebugView : Component\n{\n\t/// \u003Csummary\u003EThe sea to show. Empty takes the one on this object, else the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003EEach cascade\u0027s heights, longest waves first.\u003C/summary\u003E\n\t[Property] public bool ShowHeights { get; set; } = true;\n\n\t/// \u003Csummary\u003EEach cascade\u0027s folding: white where the surface bunches up, red where it folds over and breaks.\u003C/summary\u003E\n\t[Property] public bool ShowFolding { get; set; } = true;\n\n\t/// \u003Csummary\u003EThe persistent foam window, centred on the camera.\u003C/summary\u003E\n\t[Property] public bool ShowFoam { get; set; } = true;\n\n\t[Property] public bool ShowStats { get; set; } = true;\n\n\t/// \u003Csummary\u003EPixels across each picture.\u003C/summary\u003E\n\t[Property, Range( 64, 512 )] public int PanelSize { get; set; } = 180;\n\n\t/// \u003Csummary\u003EWhere the top left of the view sits on screen, in pixels.\u003C/summary\u003E\n\t[Property] public Vector2 Position { get; set; } = new( 16, 16 );\n\n\tprivate const int Size = 256;\n\tprivate const int Panels = Ocean.CascadeCount * 2 \u002B 1;\n\n\tprivate ComputeShader m_Shader;\n\tprivate CameraCommands m_Commands;\n\tprivate readonly Texture[] m_Panels = new Texture[Panels];\n\tprivate readonly float[] m_Ranges = new float[Ocean.CascadeCount];\n\tprivate readonly Dictionary\u003COcean.SpectrumSet, float[]\u003E m_Deviations = new();\n\n\tprivate static readonly ProfilingSampler s_Profile = new( \u0022Ocean Debug View\u0022 );\n\n\tprivate Ocean Sea =\u003E Ocean.IsValid() ? Ocean : Components.Get\u003COcean\u003E() ?? Ocean.Current;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tm_Shader = new ComputeShader( \u0022ocean/shaders/ocean_debug_cs.shader\u0022 );\n\t\t// After the FFT and the foam, on the same stage.\n\t\tm_Commands = new CameraCommands( \u0022Ocean Debug View\u0022, Stage.AfterUI, 2 );\n\n\t\tfor ( int i = 0; i \u003C Panels; i\u002B\u002B )\n\t\t\tm_Panels[i] = Texture.Create( Size, Size, ImageFormat.RGBA8888 ).WithUAVBinding().WithName( $\u0022Ocean Debug {i}\u0022 ).Finish();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tm_Commands?.Dispose();\n\t\tm_Commands = null;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar ocean = Sea;\n\n\t\tif ( m_Commands is null || !ocean.IsValid() || ocean.Cascades.Length == 0 )\n\t\t\treturn;\n\n\t\tm_Commands.Follow( Scene );\n\t\tRecord( ocean );\n\t\tDraw( ocean );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Two standard deviations of each cascade\u0027s height as the sea is now: what its picture shows as full white or full\n\t/// blue.\n\t/// \u003C/summary\u003E\n\tprivate void UpdateRanges( Ocean ocean )\n\t{\n\t\tvar from = Deviations( ocean.From );\n\t\tvar to = Deviations( ocean.To );\n\n\t\tfor ( int i = 0; i \u003C m_Ranges.Length; i\u002B\u002B )\n\t\t\tm_Ranges[i] = 2.0f * MathX.Lerp( from[i], to[i], ocean.Blend );\n\t}\n\n\tprivate float[] Deviations( Ocean.SpectrumSet set )\n\t{\n\t\tif ( m_Deviations.TryGetValue( set, out var deviations ) )\n\t\t\treturn deviations;\n\n\t\tdeviations = new float[Ocean.CascadeCount];\n\n\t\tfor ( int i = 0; i \u003C deviations.Length; i\u002B\u002B )\n\t\t{\n\t\t\tdouble sum = 0.0;\n\t\t\tforeach ( var t in set.Spectra[i].Texels )\n\t\t\t\tsum \u002B= t.x * t.x \u002B t.y * t.y;\n\n\t\t\tdeviations[i] = MathF.Sqrt( (float)(2.0 * sum) );\n\t\t}\n\n\t\treturn m_Deviations[set] = deviations;\n\t}\n\n\tprivate void Record( Ocean ocean )\n\t{\n\t\tUpdateRanges( ocean );\n\n\t\tvar cl = m_Commands.List;\n\t\tcl.Reset();\n\n\t\tusing ( cl.ProfileScope( s_Profile ) )\n\t\t{\n\t\t\tcl.Attributes.Set( \u0022DebugSize\u0022, Size );\n\n\t\t\tfor ( int i = 0; i \u003C Ocean.CascadeCount; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcl.Attributes.Set( \u0022DebugDisplacement\u0022, ocean.Cascades[i].Displacement );\n\t\t\t\tcl.Attributes.Set( \u0022DebugDerivatives\u0022, ocean.Cascades[i].Derivatives );\n\t\t\t\tcl.Attributes.Set( \u0022DebugRange\u0022, m_Ranges[i] );\n\n\t\t\t\tPanel( cl, i, 0.0f );\n\t\t\t\tPanel( cl, Ocean.CascadeCount \u002B i, 1.0f );\n\t\t\t}\n\n\t\t\tvar foam = Scene.GetAllComponents\u003COceanFoam\u003E().FirstOrDefault();\n\n\t\t\tif ( foam.IsValid() \u0026\u0026 foam.Current is not null )\n\t\t\t{\n\t\t\t\tcl.Attributes.Set( \u0022DebugFoam\u0022, foam.Current );\n\t\t\t\tPanel( cl, Panels - 1, 2.0f );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void Panel( CommandList cl, int index, float mode )\n\t{\n\t\tcl.Attributes.Set( \u0022DebugMode\u0022, mode );\n\t\tcl.Attributes.Set( \u0022DebugOut\u0022, m_Panels[index] );\n\t\tcl.DispatchCompute( m_Shader, Size, Size, 1 );\n\t\tcl.UavBarrier( m_Panels[index] );\n\t}\n\n\tprivate void Draw( Ocean ocean )\n\t{\n\t\tvar camera = m_Commands.Camera;\n\n\t\tif ( !camera.IsValid() )\n\t\t\treturn;\n\n\t\tvar hud = camera.BeginHud();\n\n\t\ttry\n\t\t{\n\t\t\tDrawPanels( ref hud, ocean );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\thud.Dispose();\n\t\t}\n\t}\n\n\tprivate void DrawPanels( ref Painter hud, Ocean ocean )\n\t{\n\t\tint row = 0;\n\t\tstring Tile( int i ) =\u003E $\u0022{ocean.LengthOf( i ):0.#} m\u0022;\n\n\t\tif ( ShowHeights )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Heights\u0022, 0, Ocean.CascadeCount, i =\u003E $\u0022{Tile( i )}  \u00B1{m_Ranges[i]:0.##} m\u0022 );\n\n\t\tif ( ShowFolding )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Folding (Jacobian)\u0022, Ocean.CascadeCount, Ocean.CascadeCount, Tile );\n\n\t\tvar foam = Scene.GetAllComponents\u003COceanFoam\u003E().FirstOrDefault();\n\n\t\tif ( ShowFoam \u0026\u0026 foam.IsValid() )\n\t\t\tRow( ref hud, row\u002B\u002B, \u0022Foam window\u0022, Panels - 1, 1, _ =\u003E $\u0022{foam.Extent:0} m round the camera\u0022 );\n\n\t\tif ( ShowStats )\n\t\t{\n\t\t\tvar sea = ocean.Sea;\n\t\t\tvar waves = ocean.Waves;\n\t\t\tvar text = new StringBuilder()\n\t\t\t\t.AppendLine( $\u0022Sea: wind {sea.WindSpeed:0.#} m/s \u00B7 Hs {sea.SignificantHeight:0.##} m \u00B7 choppiness {sea.Choppiness:0.##} \u00B7 scatter {sea.Scatter:0.##} \u00B7 crest foam {sea.CrestFoam:0.##}\u0022 )\n\t\t\t\t.AppendLine( $\u0022Blend: {ocean.Blend:P0} from Hs {ocean.From.Sea.SignificantHeight:0.##} m to {ocean.To.Sea.SignificantHeight:0.##} m \u00B7 time {ocean.OceanTime:0.0} / {ocean.LoopPeriod:0} s\u0022 )\n\t\t\t\t.AppendLine( waves is null ? \u0022CPU replica: none\u0022 : $\u0022CPU replica: {waves.Count} waves on {waves.GridSize}\u00B2 \u00B7 grids built {waves.BackgroundBuilds} in background, {waves.SynchronousBuilds} waiting\u0022 )\n\t\t\t\t.AppendLine( $\u0022Spray: {OceanSpray.Current?.Count ?? 0} particles \u00B7 foam sources: {Scene.GetAllComponents\u003COceanFoamSource\u003E().Count()}\u0022 )\n\t\t\t\t.ToString();\n\n\t\t\tText( ref hud, text, 13, new Rect( Position.x, RowTop( row ), 1200, 100 ) );\n\t\t}\n\t}\n\n\tprivate const float Gap = 8.0f;\n\tprivate const float TitleHeight = 18.0f;\n\n\tprivate float RowTop( int row ) =\u003E Position.y \u002B row * (PanelSize \u002B Gap \u002B TitleHeight);\n\n\tprivate void Row( ref Painter hud, int row, string title, int first, int count, Func\u003Cint, string\u003E label )\n\t{\n\t\tfloat y = RowTop( row );\n\t\tText( ref hud, title, 13, new Rect( Position.x, y, 600, TitleHeight ) );\n\n\t\tfor ( int i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\tvar rect = new Rect( Position.x \u002B i * (PanelSize \u002B Gap), y \u002B TitleHeight, PanelSize, PanelSize );\n\t\t\thud.Texture( m_Panels[first \u002B i], rect, null );\n\t\t\tText( ref hud, label( i ), 11, rect.Shrink( 4.0f ) );\n\t\t}\n\t}\n\n\tprivate static void Text( ref Painter hud, string text, float size, Rect rect )\n\t{\n\t\thud.TextStyle = TextStyle.Default with { FontSize = size, Color = Color.White, Alignment = TextFlag.LeftTop };\n\t\thud.Text( text, rect );\n\t}\n}"},{"Ident":"schumi.ocean","Path":"Code/OceanSpectrum.cs","FileName":"OceanSpectrum.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// The initial wave spectrum h0(k) of one ocean cascade, after Tessendorf: a Phillips spectrum for a wind of a given\n/// speed and direction, with each wave\u0027s amplitude and phase drawn from a Gaussian. A cascade only keeps the waves in\n/// its own band of wave numbers, so several cascades of different tile sizes add up to one sea without counting any\n/// wave twice; amplitudes carry the cascade\u0027s frequency spacing (dk), so the bands join smoothly whatever their tile.\n///\n/// Everything comes from integer hashes of the seed and the wave\u0027s index - never from a shared random generator - so\n/// every machine, the GPU\u0027s surface and any CPU-side replica of it for physics build exactly the same sea.\n/// \u003C/summary\u003E\npublic sealed class OceanSpectrum\n{\n\tpublic const float Gravity = 9.81f;\n\n\t/// \u003Csummary\u003EFFT resolution: waves per side of the tile.\u003C/summary\u003E\n\tpublic int Size { get; }\n\n\t/// \u003Csummary\u003EMetres covered by one tile.\u003C/summary\u003E\n\tpublic float TileLength { get; }\n\n\t/// \u003Csummary\u003Eh0(k) in .xy and conj(h0(-k)) in .zw, row-major, wave vectors centred on index Size / 2.\u003C/summary\u003E\n\tpublic Vector4[] Texels { get; }\n\n\t/// \u003Csummary\u003ESum of |h0|^2 before any scaling: this cascade\u0027s share of the sea\u0027s height variance, halved.\u003C/summary\u003E\n\tpublic double RawVariance { get; }\n\n\t/// \u003Csummary\u003EThe settings shared by every cascade of one sea.\u003C/summary\u003E\n\tpublic readonly record struct Settings( float WindSpeed, float WindAngle, float Directionality, float SmallWaveCutoff, int Seed );\n\n\t/// \u003Csummary\u003E\n\t/// Builds the waves with wave numbers in [minK, maxK) - radians per metre - at their raw Phillips amplitude. Call\n\t/// \u003Csee cref=\u0022Scale\u0022/\u003E afterwards with the factor that brings all cascades together to the height wanted.\n\t/// \u003C/summary\u003E\n\tpublic OceanSpectrum( int size, float tileLength, float minK, float maxK, int cascade, Settings settings )\n\t{\n\t\tSize = size;\n\t\tTileLength = tileLength;\n\n\t\tvar h0 = new Vector2[size * size];\n\t\t// The leading term of h(k, t) - h0(k) e^{i w t} - runs against its wave vector, so the waves that should travel\n\t\t// downwind are the ones whose k points upwind: the spectrum is laid out against the wind.\n\t\tvar wind = -new Vector2( MathF.Cos( settings.WindAngle.DegreeToRadian() ), MathF.Sin( settings.WindAngle.DegreeToRadian() ) );\n\n\t\t// The longest wave a wind of this speed raises; the spectrum falls away below it.\n\t\tfloat longest = settings.WindSpeed * settings.WindSpeed / Gravity;\n\t\tfloat shortest = settings.SmallWaveCutoff;\n\t\tfloat dk = 2.0f * MathF.PI / tileLength;\n\t\tdouble variance = 0.0;\n\n\t\tfor ( int y = 0; y \u003C size; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C size; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar k = WaveVector( x, y );\n\t\t\t\tfloat kLength = k.Length;\n\n\t\t\t\t// The first row and column hold the Nyquist frequency, which is its own mirror image: a displacement\n\t\t\t\t// along it can\u0027t be represented by a real field, and would leak into the height packed beside it.\n\t\t\t\tif ( kLength \u003C 1e-6f || x == 0 || y == 0 || kLength \u003C minK || kLength \u003E= maxK )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar direction = k / kLength;\n\t\t\t\tfloat alignment = Vector2.Dot( direction, wind );\n\n\t\t\t\t// Phillips: P(k) = exp(-1/(kL)^2) / k^4 * |k.w|^p, small waves damped by exp(-k^2 l^2). Waves running\n\t\t\t\t// against the wind keep only a little of their energy.\n\t\t\t\tfloat kl = kLength * longest;\n\t\t\t\tfloat phillips = MathF.Exp( -1.0f / (kl * kl) ) / (kLength * kLength * kLength * kLength);\n\t\t\t\tphillips *= MathF.Pow( MathF.Abs( alignment ), settings.Directionality );\n\t\t\t\tphillips *= MathF.Exp( -kLength * kLength * shortest * shortest );\n\n\t\t\t\tif ( alignment \u003C 0.0f )\n\t\t\t\t\tphillips *= 0.07f;\n\n\t\t\t\tvar gauss = Gaussian( settings.Seed, cascade, x, y );\n\t\t\t\th0[y * size \u002B x] = gauss * (MathF.Sqrt( phillips * 0.5f ) * dk);\n\t\t\t\tvariance \u002B= h0[y * size \u002B x].LengthSquared;\n\t\t\t}\n\t\t}\n\n\t\tRawVariance = variance;\n\t\tTexels = new Vector4[size * size];\n\n\t\tfor ( int y = 0; y \u003C size; y\u002B\u002B )\n\t\t{\n\t\t\tfor ( int x = 0; x \u003C size; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = h0[y * size \u002B x];\n\t\t\t\tvar b = h0[((size - y) % size) * size \u002B (size - x) % size];\n\t\t\t\tTexels[y * size \u002B x] = new Vector4( a.x, a.y, b.x, -b.y );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate OceanSpectrum( int size, float tileLength, Vector4[] texels )\n\t{\n\t\tSize = size;\n\t\tTileLength = tileLength;\n\t\tTexels = texels;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The spectrum part way between two of the same tile: the sea is linear in its amplitudes, so this is exactly the\n\t/// sea a blend between them shows.\n\t/// \u003C/summary\u003E\n\tpublic static OceanSpectrum Lerp( OceanSpectrum a, OceanSpectrum b, float t )\n\t{\n\t\tvar texels = new Vector4[a.Texels.Length];\n\n\t\tfor ( int i = 0; i \u003C texels.Length; i\u002B\u002B )\n\t\t\ttexels[i] = a.Texels[i] \u002B (b.Texels[i] - a.Texels[i]) * t;\n\n\t\treturn new OceanSpectrum( a.Size, a.TileLength, texels );\n\t}\n\n\t/// \u003Csummary\u003EMultiplies every amplitude.\u003C/summary\u003E\n\tpublic void Scale( float factor )\n\t{\n\t\tfor ( int i = 0; i \u003C Texels.Length; i\u002B\u002B )\n\t\t\tTexels[i] *= factor;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The factor that brings a set of cascades to a significant wave height (four standard deviations of the height,\n\t/// in metres). h(x) sums h0(k) and conj(h0(-k)) for every k, so its variance is twice the sum of |h0|^2.\n\t/// \u003C/summary\u003E\n\tpublic static float ScaleFor( float significantHeight, double totalRawVariance )\n\t{\n\t\treturn totalRawVariance \u003E 0.0 ? significantHeight / (4.0f * MathF.Sqrt( (float)(2.0 * totalRawVariance) )) : 0.0f;\n\t}\n\n\t/// \u003Csummary\u003EThe wave vector at a texel, in radians per metre.\u003C/summary\u003E\n\tpublic Vector2 WaveVector( int x, int y ) =\u003E new Vector2( x - Size / 2, y - Size / 2 ) * (2.0f * MathF.PI / TileLength);\n\n\t/// \u003Csummary\u003EA pair of independent standard Gaussians for one wave, by Box-Muller over two hashed uniforms.\u003C/summary\u003E\n\tprivate static Vector2 Gaussian( int seed, int cascade, int x, int y )\n\t{\n\t\tfloat u1 = MathF.Max( Uniform( seed, cascade, x, y, 0 ), 1e-7f );\n\t\tfloat u2 = Uniform( seed, cascade, x, y, 1 );\n\t\tfloat r = MathF.Sqrt( -2.0f * MathF.Log( u1 ) );\n\t\treturn new Vector2( r * MathF.Cos( 2.0f * MathF.PI * u2 ), r * MathF.Sin( 2.0f * MathF.PI * u2 ) );\n\t}\n\n\t/// \u003Csummary\u003EA uniform in [0, 1) from a PCG-style integer hash: the same on every machine.\u003C/summary\u003E\n\tprivate static float Uniform( int seed, int cascade, int x, int y, int channel )\n\t{\n\t\tuint h = (uint)seed * 747796405u \u002B 2891336453u \u002B (uint)cascade * 0x27D4EB2Fu;\n\t\th ^= (uint)x * 0x9E3779B1u;\n\t\th = Pcg( h ) ^ ((uint)y * 0x85EBCA77u);\n\t\th = Pcg( h ) ^ ((uint)channel * 0xC2B2AE3Du);\n\t\th = Pcg( h );\n\t\treturn (h \u003E\u003E 8) * (1.0f / 16777216.0f);\n\t}\n\n\tprivate static uint Pcg( uint v )\n\t{\n\t\tuint state = v * 747796405u \u002B 2891336453u;\n\t\tuint word = ((state \u003E\u003E (int)((state \u003E\u003E 28) \u002B 4u)) ^ state) * 277803737u;\n\t\treturn (word \u003E\u003E 22) ^ word;\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"OceanPreset.cs","FileName":"OceanPreset.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"namespace OceanLib;\n\n/// \u003Csummary\u003E\n/// One state of the sea, everything that changes between a calm and a storm. The ocean blends between two of these\n/// (\u003Csee cref=\u0022Ocean.BlendTo\u0022/\u003E), so the weather can build or ease off while the sea keeps moving.\n/// \u003C/summary\u003E\npublic readonly record struct OceanSea( float WindSpeed, float SignificantHeight, float Choppiness, float Scatter, float CrestFoam )\n{\n\t/// \u003Csummary\u003EA moderate, fresh sea: what an ocean shows with no preset.\u003C/summary\u003E\n\tpublic static OceanSea Default =\u003E new( 9.0f, 2.5f, 1.3f, 0.8f, 1.0f );\n\n\tpublic static OceanSea Lerp( OceanSea a, OceanSea b, float t ) =\u003E new(\n\t\tMathX.Lerp( a.WindSpeed, b.WindSpeed, t ),\n\t\tMathX.Lerp( a.SignificantHeight, b.SignificantHeight, t ),\n\t\tMathX.Lerp( a.Choppiness, b.Choppiness, t ),\n\t\tMathX.Lerp( a.Scatter, b.Scatter, t ),\n\t\tMathX.Lerp( a.CrestFoam, b.CrestFoam, t ) );\n}\n\n/// \u003Csummary\u003EA sea state saved as an asset, for an \u003Csee cref=\u0022Ocean\u0022/\u003E to start from or blend to.\u003C/summary\u003E\n[AssetType( Name = \u0022Ocean Preset\u0022, Extension = \u0022ocnsea\u0022, Category = \u0022Ocean\u0022 )]\npublic sealed class OceanPreset : GameResource\n{\n\t/// \u003Csummary\u003EWind speed in metres per second: sets how long the biggest waves are.\u003C/summary\u003E\n\t[Property, Range( 1, 40 )] public float WindSpeed { get; set; } = 9.0f;\n\n\t/// \u003Csummary\u003EThe mean height of the biggest third of the waves, trough to crest, in metres.\u003C/summary\u003E\n\t[Property, Range( 0, 15 )] public float SignificantHeight { get; set; } = 2.5f;\n\n\t/// \u003Csummary\u003EHow far the crests lean and pinch: 0 is smooth rolling waves, around 1.5 sharp peaks.\u003C/summary\u003E\n\t[Property, Range( 0, 2.5f )] public float Choppiness { get; set; } = 1.3f;\n\n\t/// \u003Csummary\u003EHow strongly light scattering out of the crests glows.\u003C/summary\u003E\n\t[Property, Range( 0, 4 )] public float Scatter { get; set; } = 0.8f;\n\n\t/// \u003Csummary\u003EFoam per second a fully breaking crest makes. 0 leaves foam only round what\u0027s in the water.\u003C/summary\u003E\n\t[Property, Range( 0, 20 )] public float CrestFoam { get; set; } = 1.0f;\n\n\tpublic OceanSea Sea =\u003E new( WindSpeed, SignificantHeight, Choppiness, Scatter, CrestFoam );\n\n\tprotected override Bitmap CreateAssetTypeIcon( int width, int height )\n\t{\n\t\treturn CreateSimpleAssetTypeIcon( \u0022waves\u0022, width, height, \u0022#1f6f8b\u0022, \u0022white\u0022 );\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"OceanBuoyancy.cs","FileName":"OceanBuoyancy.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Floats a rigidbody on the \u003Csee cref=\u0022Ocean\u0022/\u003E sea, through its CPU replica (\u003Csee cref=\u0022OceanWaves\u0022/\u003E). A grid of\n/// points over the hull, each a spring pushing up by how far under the surface it is, so the hull tilts with the swell\n/// under it; then everything the water does to a hull besides holding it up:\n///\n/// - Lift that knows its limits: it stiffens as the hull sinks towards its deck (the reserve a real hull has in its\n///   sides), then stops growing once it is under - it can only push aside the water it fills - so no crest can fling\n///   the boat out of the sea.\n/// - Inertia: one slider from a cork that sits on the surface to a heavy hull that carries its fall into the water and\n///   wallows up out of it, the waterline kept where it was.\n/// - The water moving: every point is damped against the water\u0027s own motion, not the world\u0027s. Vertically that lets the\n///   hull ride each wave rather than bounce off it; across the sea it is the wave\u0027s orbit carrying the hull - forward\n///   on the crests, back in the troughs - and the surface drifting downwind, so a boat left alone in a storm is pushed\n///   about and off downwind like the real thing.\n/// - Drag shaped like a hull: a boat slips along its length but its keel and sides fight being pushed sideways, both\n///   growing with the square of the speed, as water drag does; the turning is damped the same way.\n/// - Slamming: a point driving into the water fast throws spray off the hull\u0027s side (see \u003Csee cref=\u0022OceanSpray\u0022/\u003E), and a\n///   hard landing throws up a burst of foam round it (see \u003Csee cref=\u0022OceanFoamSource.Splash\u0022/\u003E).\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Buoyancy\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022sailing\u0022 )]\npublic sealed class OceanBuoyancy : Component\n{\n\t/// \u003Csummary\u003EThe sea to float on. Empty takes the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003EPoints along the hull and across it.\u003C/summary\u003E\n\t[Property, Group( \u0022Points\u0022 ), Range( 1, 8 )] public int PointsAlong { get; set; } = 4;\n\t[Property, Group( \u0022Points\u0022 ), Range( 1, 6 )] public int PointsAcross { get; set; } = 3;\n\n\t/// \u003Csummary\u003EHow far out to the hull\u0027s ends and sides the points reach, as a fraction of its half-size.\u003C/summary\u003E\n\t[Property, Group( \u0022Points\u0022 ), Range( 0.1f, 1 )] public Vector2 Spread { get; set; } = new( 0.8f, 0.75f );\n\n\t/// \u003Csummary\u003E\n\t/// Raises the waterline the points answer to, in metres: higher floats the boat higher. The points sit at the\n\t/// middle of the hull\u0027s height.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( -3, 3 )] public float SurfaceOffset { get; set; } = 0.0f;\n\n\t/// \u003Csummary\u003EUpward acceleration per unit of depth with no inertia: stiffer settles shallower and quicker.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 5, 200 )] public float Stiffness { get; set; } = 40.0f;\n\n\t/// \u003Csummary\u003EDamping of each point\u0027s rise and fall against the water\u0027s own, with no inertia.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 20 )] public float Damping { get; set; } = 6.0f;\n\n\t/// \u003Csummary\u003E\n\t/// 0 floats like a cork on the surface; 1 is a heavy hull that ploughs into the water as it drops off a crest and\n\t/// rises back slowly. The waterline at rest stays the same.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 1 )] public float Inertia { get; set; } = 0.4f;\n\n\t/// \u003Csummary\u003EHow much harder the water pushes back as the hull sinks past its resting depth: its sides\u0027 reserve.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 4 )] public float Reserve { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EThe most the water can lift the hull, as a multiple of its weight.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 1, 10 )] public float MaxLift { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EThe fastest the hull is made to follow the surface rising or falling, in metres per second.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0.5f, 10 )] public float MaxWaterRise { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EHow hard the water\u0027s own movement carries the hull along: 0 ignores the waves\u0027 orbit and the drift.\u003C/summary\u003E\n\t[Property, Group( \u0022Water Movement\u0022 ), Range( 0, 1 )] public float WaveTransport { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EHow fast the surface drifts downwind in a storm, in metres per second - less in a calmer sea.\u003C/summary\u003E\n\t[Property, Group( \u0022Water Movement\u0022 ), Range( 0, 3 )] public float WindDrift { get; set; } = 0.6f;\n\n\t/// \u003Csummary\u003EDrag along the hull: low, a hull is shaped to slip through the water that way. Per metre.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 2 )] public float LengthwiseDrag { get; set; } = 0.06f;\n\n\t/// \u003Csummary\u003EDrag across the hull: high, the keel and sides fight being pushed sideways. Per metre.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 5 )] public float SidewaysDrag { get; set; } = 0.8f;\n\n\t/// \u003Csummary\u003EDrag of the water on the hull\u0027s turning, per second.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 10 )] public float AngularDrag { get; set; } = 1.5f;\n\n\t/// \u003Csummary\u003EHow fast a point must hit the water, in metres per second, to throw up foam.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0.5f, 10 )] public float SplashSpeed { get; set; } = 2.0f;\n\n\t/// \u003Csummary\u003EHow much foam a hard slam throws up.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0, 10 )] public float SplashFoam { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EHow fast a point must drive into the water, in metres per second, to throw spray (see \u003Csee cref=\u0022OceanSpray\u0022/\u003E).\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0.5f, 10 )] public float SprayImpact { get; set; } = 1.5f;\n\n\t/// \u003Csummary\u003EScales the spray this hull throws.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0, 4 )] public float SprayAmount { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EHow much of the hull\u0027s points are under water, 0 to 1.\u003C/summary\u003E\n\tpublic float Submerged { get; private set; }\n\n\t/// \u003Csummary\u003EThe sea\u0027s height under the hull\u0027s centre, in world units.\u003C/summary\u003E\n\tpublic float WaterHeight { get; private set; }\n\n\t/// \u003Csummary\u003EThe water\u0027s movement under the hull, in world units per second.\u003C/summary\u003E\n\tpublic Vector3 WaterVelocity { get; private set; }\n\n\tprivate Rigidbody m_Body;\n\tprivate Collider m_Collider;\n\tprivate OceanFoamSource m_Foam;\n\tprivate float[] m_LastWater = Array.Empty\u003Cfloat\u003E();\n\tprivate bool[] m_WasUnder = Array.Empty\u003Cbool\u003E();\n\tprivate float[] m_SprayCooldown = Array.Empty\u003Cfloat\u003E();\n\tprivate Vector3[] m_Points = Array.Empty\u003CVector3\u003E();\n\n\tprivate const float UnitsPerMetre = Ocean.UnitsPerMetre;\n\n\tprotected override void OnFixedUpdate()\n\t{\n\t\tif ( IsProxy )\n\t\t\treturn;\n\n\t\tvar ocean = Ocean.IsValid() ? Ocean : Ocean.Current;\n\t\tm_Body ??= Components.Get\u003CRigidbody\u003E();\n\t\tm_Collider ??= Components.Get\u003CCollider\u003E();\n\t\tm_Foam ??= Components.Get\u003COceanFoamSource\u003E();\n\n\t\tif ( !ocean.IsValid() || ocean.Waves is null || !m_Body.IsValid() || !m_Body.MotionEnabled || !m_Collider.IsValid() )\n\t\t\treturn;\n\n\t\tLayOutPoints();\n\n\t\tfloat gravity = Scene.PhysicsWorld.Gravity.Length;\n\t\tfloat mass = m_Body.Mass;\n\t\tfloat share = mass / m_Points.Length;\n\n\t\t// Inertia softens the spring and the damper together, geometrically - halfway is halfway in feel - and lifts the\n\t\t// waterline by however much further the softer spring sags, so only the plunge changes, not where it floats.\n\t\tfloat stiffness = Stiffness * MathF.Pow( 0.2f, Inertia );\n\t\tfloat damping = Damping * MathF.Pow( 0.2f, Inertia );\n\t\tfloat rest = gravity / stiffness;\n\t\tfloat offset = SurfaceOffset * UnitsPerMetre \u002B rest - gravity / Stiffness;\n\t\tfloat maxLift = MaxLift * gravity * share;\n\n\t\tWaterHeight = ocean.HeightAt( WorldPosition );\n\n\t\t// The water\u0027s own motion under the hull: the waves\u0027 orbit, plus the surface drifting downwind in a big sea.\n\t\tvar orbit = ocean.WaterVelocityAt( WorldPosition ).WithZ( 0 );\n\t\t// Full drift in a storm of eight-metre waves.\n\t\tfloat drift = WindDrift * UnitsPerMetre * (ocean.Sea.SignificantHeight / 8.0f).Clamp( 0.0f, 1.5f );\n\t\tWaterVelocity = (orbit \u002B ocean.WindDirection * drift) * WaveTransport;\n\n\t\tint under = 0;\n\t\tfloat slam = 0.0f;\n\n\t\tfor ( int i = 0; i \u003C m_Points.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar point = WorldTransform.PointToWorld( m_Points[i] );\n\t\t\tfloat water = ocean.HeightAt( point );\n\n\t\t\t// How fast the sea is rising or falling right here - held to what a real sea\u0027s surface does. A sharp crest\n\t\t\t// sweeping under a point makes the height there jump faster than any water moves, and following that\n\t\t\t// would throw the hull clear of the crest.\n\t\t\tfloat maxRise = MaxWaterRise * UnitsPerMetre;\n\t\t\tfloat waterSpeed = float.IsNaN( m_LastWater[i] ) ? 0.0f : ((water - m_LastWater[i]) / Time.Delta).Clamp( -maxRise, maxRise );\n\t\t\tm_LastWater[i] = water;\n\n\t\t\tfloat depth = water \u002B offset - point.z;\n\t\t\tfloat pointSpeed = m_Body.GetVelocityAtPoint( point ).z;\n\n\t\t\tif ( depth \u003C= 0.0f )\n\t\t\t{\n\t\t\t\tm_WasUnder[i] = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tunder\u002B\u002B;\n\n\t\t\t// Hitting the water: how fast this point is going down into it, relative to the water\u0027s own rise.\n\t\t\tfloat impact = (waterSpeed - pointSpeed) / UnitsPerMetre;\n\n\t\t\tif ( !m_WasUnder[i] )\n\t\t\t\tslam = MathF.Max( slam, impact );\n\n\t\t\tm_WasUnder[i] = true;\n\n\t\t\t// Cutting into a wave hard enough throws spray off the hull\u0027s side here.\n\t\t\tm_SprayCooldown[i] -= Time.Delta;\n\t\t\tif ( impact \u003E SprayImpact \u0026\u0026 m_SprayCooldown[i] \u003C= 0.0f )\n\t\t\t{\n\t\t\t\tm_SprayCooldown[i] = 0.15f;\n\t\t\t\tThrowSpray( m_Points[i], water, impact / SprayImpact - 1.0f );\n\t\t\t}\n\n\t\t\t// Linear to the resting depth, then stiffening the further past it - the hull\u0027s sides. The water can\u0027t push\n\t\t\t// up harder than the cap, lift and damping together, whatever the wave is doing.\n\t\t\tfloat past = MathF.Max( 0.0f, depth - rest );\n\t\t\tfloat lift = (depth \u002B Reserve * past * past / rest) * stiffness * share;\n\t\t\tfloat damper = (waterSpeed - pointSpeed) * damping * share;\n\n\t\t\tm_Body.ApplyForceAt( point, Vector3.Up * MathF.Min( lift \u002B damper, maxLift ) );\n\t\t}\n\n\t\tSubmerged = (float)under / m_Points.Length;\n\n\t\tif ( Submerged \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tApplyDrag( mass );\n\n\t\tif ( slam \u003E SplashSpeed \u0026\u0026 m_Foam.IsValid() )\n\t\t\tm_Foam.Splash( SplashFoam * MathF.Min( slam / SplashSpeed - 1.0f, 2.0f ) );\n\t}\n\n\tprotected override void DrawGizmos()\n\t{\n\t\t// The points the sea pushes on: blue under water, white out of it, and the water\u0027s movement under the hull.\n\t\tfor ( int i = 0; i \u003C m_Points.Length; i\u002B\u002B )\n\t\t{\n\t\t\tGizmo.Draw.Color = Game.IsPlaying \u0026\u0026 m_WasUnder[i] ? Color.Cyan : Color.White;\n\t\t\tGizmo.Draw.SolidSphere( m_Points[i], 3.0f );\n\t\t}\n\n\t\tif ( Game.IsPlaying \u0026\u0026 Submerged \u003E 0.0f )\n\t\t{\n\t\t\tGizmo.Draw.Color = Color.Cyan;\n\t\t\tGizmo.Draw.Arrow( Vector3.Zero, WorldTransform.NormalToLocal( WaterVelocity ), 6.0f, 3.0f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Spray off the hull\u0027s side nearest a point, at the waterline: thrown out from the side, forward too at the bow, and\n\t/// carried along with the hull.\n\t/// \u003C/summary\u003E\n\tprivate void ThrowSpray( Vector3 localPoint, float waterZ, float strength )\n\t{\n\t\tvar spray = OceanSpray.Current;\n\n\t\tif ( !spray.IsValid() || strength \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tvar bounds = m_Collider.LocalBounds;\n\t\tfloat side = MathF.Abs( localPoint.y - bounds.Center.y ) \u003E 1.0f ? MathF.Sign( localPoint.y - bounds.Center.y ) : (Game.Random.Float() \u003C 0.5f ? -1.0f : 1.0f);\n\t\tvar edge = new Vector3( localPoint.x, bounds.Center.y \u002B side * bounds.Extents.y * 0.95f, bounds.Center.z );\n\t\tvar at = WorldTransform.PointToWorld( edge ).WithZ( waterZ );\n\n\t\t// Out from the side, and from the bow forward as well: the bow throws its spray ahead and to both sides.\n\t\tfloat bow = ((localPoint.x - bounds.Center.x) / bounds.Extents.x).Clamp( 0.0f, 1.0f );\n\t\tvar outward = WorldRotation.Left * side \u002B WorldRotation.Forward * bow * 0.8f;\n\n\t\tspray.Emit( at, m_Body.GetVelocityAtPoint( at ), outward, strength * SprayAmount );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The water\u0027s drag on the hull moving through it: slight along the hull, strong across it, both with the square of\n\t/// the speed against the water - so the water\u0027s own movement pulls the hull along with it.\n\t/// \u003C/summary\u003E\n\tprivate void ApplyDrag( float mass )\n\t{\n\t\tvar relative = (m_Body.Velocity - WaterVelocity).WithZ( 0 ) / UnitsPerMetre;\n\t\tvar forward = WorldRotation.Forward.WithZ( 0 );\n\t\tforward = forward.Length \u003E 1e-3f ? forward.Normal : Vector3.Forward;\n\t\tvar side = new Vector3( -forward.y, forward.x, 0 );\n\n\t\tfloat along = Vector3.Dot( relative, forward );\n\t\tfloat across = Vector3.Dot( relative, side );\n\n\t\t// Quadratic, as water drag is, with a small linear part so a hull nearly at rest still settles.\n\t\tfloat fAlong = -(LengthwiseDrag * MathF.Abs( along ) \u002B 0.05f) * along;\n\t\tfloat fAcross = -(SidewaysDrag * MathF.Abs( across ) \u002B 0.3f) * across;\n\n\t\tvar force = (forward * fAlong \u002B side * fAcross) * mass * UnitsPerMetre * Submerged;\n\t\tm_Body.ApplyForce( force );\n\n\t\tvar angular = m_Body.AngularVelocity;\n\t\tm_Body.AngularVelocity = angular * MathF.Max( 0.0f, 1.0f - AngularDrag * Submerged * Time.Delta * (1.0f \u002B angular.Length * 0.5f) );\n\t}\n\n\tprivate void LayOutPoints()\n\t{\n\t\tint count = PointsAlong * PointsAcross;\n\n\t\tif ( m_Points.Length == count )\n\t\t\treturn;\n\n\t\tvar bounds = m_Collider.LocalBounds;\n\t\tm_Points = new Vector3[count];\n\t\tm_LastWater = new float[count];\n\t\tm_WasUnder = new bool[count];\n\t\tm_SprayCooldown = new float[count];\n\t\tArray.Fill( m_LastWater, float.NaN );\n\t\tArray.Fill( m_WasUnder, true );\n\n\t\tfor ( int a = 0; a \u003C PointsAlong; a\u002B\u002B )\n\t\t{\n\t\t\tfor ( int b = 0; b \u003C PointsAcross; b\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat x = PointsAlong == 1 ? 0.0f : (a / (PointsAlong - 1.0f) * 2.0f - 1.0f) * Spread.x;\n\t\t\t\tfloat y = PointsAcross == 1 ? 0.0f : (b / (PointsAcross - 1.0f) * 2.0f - 1.0f) * Spread.y;\n\t\t\t\tm_Points[a * PointsAcross \u002B b] = bounds.Center \u002B new Vector3( x * bounds.Extents.x, y * bounds.Extents.y, 0.0f );\n\t\t\t}\n\t\t}\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanPreset.cs","FileName":"OceanPreset.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"namespace OceanLib;\n\n/// \u003Csummary\u003E\n/// One state of the sea, everything that changes between a calm and a storm. The ocean blends between two of these\n/// (\u003Csee cref=\u0022Ocean.BlendTo\u0022/\u003E), so the weather can build or ease off while the sea keeps moving.\n/// \u003C/summary\u003E\npublic readonly record struct OceanSea( float WindSpeed, float SignificantHeight, float Choppiness, float Scatter, float CrestFoam )\n{\n\t/// \u003Csummary\u003EA moderate, fresh sea: what an ocean shows with no preset.\u003C/summary\u003E\n\tpublic static OceanSea Default =\u003E new( 9.0f, 2.5f, 1.3f, 0.8f, 1.0f );\n\n\tpublic static OceanSea Lerp( OceanSea a, OceanSea b, float t ) =\u003E new(\n\t\tMathX.Lerp( a.WindSpeed, b.WindSpeed, t ),\n\t\tMathX.Lerp( a.SignificantHeight, b.SignificantHeight, t ),\n\t\tMathX.Lerp( a.Choppiness, b.Choppiness, t ),\n\t\tMathX.Lerp( a.Scatter, b.Scatter, t ),\n\t\tMathX.Lerp( a.CrestFoam, b.CrestFoam, t ) );\n}\n\n/// \u003Csummary\u003EA sea state saved as an asset, for an \u003Csee cref=\u0022Ocean\u0022/\u003E to start from or blend to.\u003C/summary\u003E\n[AssetType( Name = \u0022Ocean Preset\u0022, Extension = \u0022ocnsea\u0022, Category = \u0022Ocean\u0022 )]\npublic sealed class OceanPreset : GameResource\n{\n\t/// \u003Csummary\u003EWind speed in metres per second: sets how long the biggest waves are.\u003C/summary\u003E\n\t[Property, Range( 1, 40 )] public float WindSpeed { get; set; } = 9.0f;\n\n\t/// \u003Csummary\u003EThe mean height of the biggest third of the waves, trough to crest, in metres.\u003C/summary\u003E\n\t[Property, Range( 0, 15 )] public float SignificantHeight { get; set; } = 2.5f;\n\n\t/// \u003Csummary\u003EHow far the crests lean and pinch: 0 is smooth rolling waves, around 1.5 sharp peaks.\u003C/summary\u003E\n\t[Property, Range( 0, 2.5f )] public float Choppiness { get; set; } = 1.3f;\n\n\t/// \u003Csummary\u003EHow strongly light scattering out of the crests glows.\u003C/summary\u003E\n\t[Property, Range( 0, 4 )] public float Scatter { get; set; } = 0.8f;\n\n\t/// \u003Csummary\u003EFoam per second a fully breaking crest makes. 0 leaves foam only round what\u0027s in the water.\u003C/summary\u003E\n\t[Property, Range( 0, 20 )] public float CrestFoam { get; set; } = 1.0f;\n\n\tpublic OceanSea Sea =\u003E new( WindSpeed, SignificantHeight, Choppiness, Scatter, CrestFoam );\n\n\tprotected override Bitmap CreateAssetTypeIcon( int width, int height )\n\t{\n\t\treturn CreateSimpleAssetTypeIcon( \u0022waves\u0022, width, height, \u0022#1f6f8b\u0022, \u0022white\u0022 );\n\t}\n}\n"},{"Ident":"schumi.ocean","Path":"Code/OceanBuoyancy.cs","FileName":"OceanBuoyancy.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":440536,"IsPrivate":false,"Code":"using System;\n\nnamespace OceanLib;\n\n/// \u003Csummary\u003E\n/// Floats a rigidbody on the \u003Csee cref=\u0022Ocean\u0022/\u003E sea, through its CPU replica (\u003Csee cref=\u0022OceanWaves\u0022/\u003E). A grid of\n/// points over the hull, each a spring pushing up by how far under the surface it is, so the hull tilts with the swell\n/// under it; then everything the water does to a hull besides holding it up:\n///\n/// - Lift that knows its limits: it stiffens as the hull sinks towards its deck (the reserve a real hull has in its\n///   sides), then stops growing once it is under - it can only push aside the water it fills - so no crest can fling\n///   the boat out of the sea.\n/// - Inertia: one slider from a cork that sits on the surface to a heavy hull that carries its fall into the water and\n///   wallows up out of it, the waterline kept where it was.\n/// - The water moving: every point is damped against the water\u0027s own motion, not the world\u0027s. Vertically that lets the\n///   hull ride each wave rather than bounce off it; across the sea it is the wave\u0027s orbit carrying the hull - forward\n///   on the crests, back in the troughs - and the surface drifting downwind, so a boat left alone in a storm is pushed\n///   about and off downwind like the real thing.\n/// - Drag shaped like a hull: a boat slips along its length but its keel and sides fight being pushed sideways, both\n///   growing with the square of the speed, as water drag does; the turning is damped the same way.\n/// - Slamming: a point driving into the water fast throws spray off the hull\u0027s side (see \u003Csee cref=\u0022OceanSpray\u0022/\u003E), and a\n///   hard landing throws up a burst of foam round it (see \u003Csee cref=\u0022OceanFoamSource.Splash\u0022/\u003E).\n/// \u003C/summary\u003E\n[Title( \u0022Ocean Buoyancy\u0022 ), Category( \u0022Ocean\u0022 ), Icon( \u0022sailing\u0022 )]\npublic sealed class OceanBuoyancy : Component\n{\n\t/// \u003Csummary\u003EThe sea to float on. Empty takes the scene\u0027s.\u003C/summary\u003E\n\t[Property] public Ocean Ocean { get; set; }\n\n\t/// \u003Csummary\u003EPoints along the hull and across it.\u003C/summary\u003E\n\t[Property, Group( \u0022Points\u0022 ), Range( 1, 8 )] public int PointsAlong { get; set; } = 4;\n\t[Property, Group( \u0022Points\u0022 ), Range( 1, 6 )] public int PointsAcross { get; set; } = 3;\n\n\t/// \u003Csummary\u003EHow far out to the hull\u0027s ends and sides the points reach, as a fraction of its half-size.\u003C/summary\u003E\n\t[Property, Group( \u0022Points\u0022 ), Range( 0.1f, 1 )] public Vector2 Spread { get; set; } = new( 0.8f, 0.75f );\n\n\t/// \u003Csummary\u003E\n\t/// Raises the waterline the points answer to, in metres: higher floats the boat higher. The points sit at the\n\t/// middle of the hull\u0027s height.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( -3, 3 )] public float SurfaceOffset { get; set; } = 0.0f;\n\n\t/// \u003Csummary\u003EUpward acceleration per unit of depth with no inertia: stiffer settles shallower and quicker.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 5, 200 )] public float Stiffness { get; set; } = 40.0f;\n\n\t/// \u003Csummary\u003EDamping of each point\u0027s rise and fall against the water\u0027s own, with no inertia.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 20 )] public float Damping { get; set; } = 6.0f;\n\n\t/// \u003Csummary\u003E\n\t/// 0 floats like a cork on the surface; 1 is a heavy hull that ploughs into the water as it drops off a crest and\n\t/// rises back slowly. The waterline at rest stays the same.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 1 )] public float Inertia { get; set; } = 0.4f;\n\n\t/// \u003Csummary\u003EHow much harder the water pushes back as the hull sinks past its resting depth: its sides\u0027 reserve.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0, 4 )] public float Reserve { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EThe most the water can lift the hull, as a multiple of its weight.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 1, 10 )] public float MaxLift { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EThe fastest the hull is made to follow the surface rising or falling, in metres per second.\u003C/summary\u003E\n\t[Property, Group( \u0022Float\u0022 ), Range( 0.5f, 10 )] public float MaxWaterRise { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EHow hard the water\u0027s own movement carries the hull along: 0 ignores the waves\u0027 orbit and the drift.\u003C/summary\u003E\n\t[Property, Group( \u0022Water Movement\u0022 ), Range( 0, 1 )] public float WaveTransport { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EHow fast the surface drifts downwind in a storm, in metres per second - less in a calmer sea.\u003C/summary\u003E\n\t[Property, Group( \u0022Water Movement\u0022 ), Range( 0, 3 )] public float WindDrift { get; set; } = 0.6f;\n\n\t/// \u003Csummary\u003EDrag along the hull: low, a hull is shaped to slip through the water that way. Per metre.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 2 )] public float LengthwiseDrag { get; set; } = 0.06f;\n\n\t/// \u003Csummary\u003EDrag across the hull: high, the keel and sides fight being pushed sideways. Per metre.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 5 )] public float SidewaysDrag { get; set; } = 0.8f;\n\n\t/// \u003Csummary\u003EDrag of the water on the hull\u0027s turning, per second.\u003C/summary\u003E\n\t[Property, Group( \u0022Drag\u0022 ), Range( 0, 10 )] public float AngularDrag { get; set; } = 1.5f;\n\n\t/// \u003Csummary\u003EHow fast a point must hit the water, in metres per second, to throw up foam.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0.5f, 10 )] public float SplashSpeed { get; set; } = 2.0f;\n\n\t/// \u003Csummary\u003EHow much foam a hard slam throws up.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0, 10 )] public float SplashFoam { get; set; } = 3.0f;\n\n\t/// \u003Csummary\u003EHow fast a point must drive into the water, in metres per second, to throw spray (see \u003Csee cref=\u0022OceanSpray\u0022/\u003E).\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0.5f, 10 )] public float SprayImpact { get; set; } = 1.5f;\n\n\t/// \u003Csummary\u003EScales the spray this hull throws.\u003C/summary\u003E\n\t[Property, Group( \u0022Splash\u0022 ), Range( 0, 4 )] public float SprayAmount { get; set; } = 1.0f;\n\n\t/// \u003Csummary\u003EHow much of the hull\u0027s points are under water, 0 to 1.\u003C/summary\u003E\n\tpublic float Submerged { get; private set; }\n\n\t/// \u003Csummary\u003EThe sea\u0027s height under the hull\u0027s centre, in world units.\u003C/summary\u003E\n\tpublic float WaterHeight { get; private set; }\n\n\t/// \u003Csummary\u003EThe water\u0027s movement under the hull, in world units per second.\u003C/summary\u003E\n\tpublic Vector3 WaterVelocity { get; private set; }\n\n\tprivate Rigidbody m_Body;\n\tprivate Collider m_Collider;\n\tprivate OceanFoamSource m_Foam;\n\tprivate float[] m_LastWater = Array.Empty\u003Cfloat\u003E();\n\tprivate bool[] m_WasUnder = Array.Empty\u003Cbool\u003E();\n\tprivate float[] m_SprayCooldown = Array.Empty\u003Cfloat\u003E();\n\tprivate Vector3[] m_Points = Array.Empty\u003CVector3\u003E();\n\n\tprivate const float UnitsPerMetre = Ocean.UnitsPerMetre;\n\n\tprotected override void OnFixedUpdate()\n\t{\n\t\tif ( IsProxy )\n\t\t\treturn;\n\n\t\tvar ocean = Ocean.IsValid() ? Ocean : Ocean.Current;\n\t\tm_Body ??= Components.Get\u003CRigidbody\u003E();\n\t\tm_Collider ??= Components.Get\u003CCollider\u003E();\n\t\tm_Foam ??= Components.Get\u003COceanFoamSource\u003E();\n\n\t\tif ( !ocean.IsValid() || ocean.Waves is null || !m_Body.IsValid() || !m_Body.MotionEnabled || !m_Collider.IsValid() )\n\t\t\treturn;\n\n\t\tLayOutPoints();\n\n\t\tfloat gravity = Scene.PhysicsWorld.Gravity.Length;\n\t\tfloat mass = m_Body.Mass;\n\t\tfloat share = mass / m_Points.Length;\n\n\t\t// Inertia softens the spring and the damper together, geometrically - halfway is halfway in feel - and lifts the\n\t\t// waterline by however much further the softer spring sags, so only the plunge changes, not where it floats.\n\t\tfloat stiffness = Stiffness * MathF.Pow( 0.2f, Inertia );\n\t\tfloat damping = Damping * MathF.Pow( 0.2f, Inertia );\n\t\tfloat rest = gravity / stiffness;\n\t\tfloat offset = SurfaceOffset * UnitsPerMetre \u002B rest - gravity / Stiffness;\n\t\tfloat maxLift = MaxLift * gravity * share;\n\n\t\tWaterHeight = ocean.HeightAt( WorldPosition );\n\n\t\t// The water\u0027s own motion under the hull: the waves\u0027 orbit, plus the surface drifting downwind in a big sea.\n\t\tvar orbit = ocean.WaterVelocityAt( WorldPosition ).WithZ( 0 );\n\t\t// Full drift in a storm of eight-metre waves.\n\t\tfloat drift = WindDrift * UnitsPerMetre * (ocean.Sea.SignificantHeight / 8.0f).Clamp( 0.0f, 1.5f );\n\t\tWaterVelocity = (orbit \u002B ocean.WindDirection * drift) * WaveTransport;\n\n\t\tint under = 0;\n\t\tfloat slam = 0.0f;\n\n\t\tfor ( int i = 0; i \u003C m_Points.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar point = WorldTransform.PointToWorld( m_Points[i] );\n\t\t\tfloat water = ocean.HeightAt( point );\n\n\t\t\t// How fast the sea is rising or falling right here - held to what a real sea\u0027s surface does. A sharp crest\n\t\t\t// sweeping under a point makes the height there jump faster than any water moves, and following that\n\t\t\t// would throw the hull clear of the crest.\n\t\t\tfloat maxRise = MaxWaterRise * UnitsPerMetre;\n\t\t\tfloat waterSpeed = float.IsNaN( m_LastWater[i] ) ? 0.0f : ((water - m_LastWater[i]) / Time.Delta).Clamp( -maxRise, maxRise );\n\t\t\tm_LastWater[i] = water;\n\n\t\t\tfloat depth = water \u002B offset - point.z;\n\t\t\tfloat pointSpeed = m_Body.GetVelocityAtPoint( point ).z;\n\n\t\t\tif ( depth \u003C= 0.0f )\n\t\t\t{\n\t\t\t\tm_WasUnder[i] = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tunder\u002B\u002B;\n\n\t\t\t// Hitting the water: how fast this point is going down into it, relative to the water\u0027s own rise.\n\t\t\tfloat impact = (waterSpeed - pointSpeed) / UnitsPerMetre;\n\n\t\t\tif ( !m_WasUnder[i] )\n\t\t\t\tslam = MathF.Max( slam, impact );\n\n\t\t\tm_WasUnder[i] = true;\n\n\t\t\t// Cutting into a wave hard enough throws spray off the hull\u0027s side here.\n\t\t\tm_SprayCooldown[i] -= Time.Delta;\n\t\t\tif ( impact \u003E SprayImpact \u0026\u0026 m_SprayCooldown[i] \u003C= 0.0f )\n\t\t\t{\n\t\t\t\tm_SprayCooldown[i] = 0.15f;\n\t\t\t\tThrowSpray( m_Points[i], water, impact / SprayImpact - 1.0f );\n\t\t\t}\n\n\t\t\t// Linear to the resting depth, then stiffening the further past it - the hull\u0027s sides. The water can\u0027t push\n\t\t\t// up harder than the cap, lift and damping together, whatever the wave is doing.\n\t\t\tfloat past = MathF.Max( 0.0f, depth - rest );\n\t\t\tfloat lift = (depth \u002B Reserve * past * past / rest) * stiffness * share;\n\t\t\tfloat damper = (waterSpeed - pointSpeed) * damping * share;\n\n\t\t\tm_Body.ApplyForceAt( point, Vector3.Up * MathF.Min( lift \u002B damper, maxLift ) );\n\t\t}\n\n\t\tSubmerged = (float)under / m_Points.Length;\n\n\t\tif ( Submerged \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tApplyDrag( mass );\n\n\t\tif ( slam \u003E SplashSpeed \u0026\u0026 m_Foam.IsValid() )\n\t\t\tm_Foam.Splash( SplashFoam * MathF.Min( slam / SplashSpeed - 1.0f, 2.0f ) );\n\t}\n\n\tprotected override void DrawGizmos()\n\t{\n\t\t// The points the sea pushes on: blue under water, white out of it, and the water\u0027s movement under the hull.\n\t\tfor ( int i = 0; i \u003C m_Points.Length; i\u002B\u002B )\n\t\t{\n\t\t\tGizmo.Draw.Color = Game.IsPlaying \u0026\u0026 m_WasUnder[i] ? Color.Cyan : Color.White;\n\t\t\tGizmo.Draw.SolidSphere( m_Points[i], 3.0f );\n\t\t}\n\n\t\tif ( Game.IsPlaying \u0026\u0026 Submerged \u003E 0.0f )\n\t\t{\n\t\t\tGizmo.Draw.Color = Color.Cyan;\n\t\t\tGizmo.Draw.Arrow( Vector3.Zero, WorldTransform.NormalToLocal( WaterVelocity ), 6.0f, 3.0f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Spray off the hull\u0027s side nearest a point, at the waterline: thrown out from the side, forward too at the bow, and\n\t/// carried along with the hull.\n\t/// \u003C/summary\u003E\n\tprivate void ThrowSpray( Vector3 localPoint, float waterZ, float strength )\n\t{\n\t\tvar spray = OceanSpray.Current;\n\n\t\tif ( !spray.IsValid() || strength \u003C= 0.0f )\n\t\t\treturn;\n\n\t\tvar bounds = m_Collider.LocalBounds;\n\t\tfloat side = MathF.Abs( localPoint.y - bounds.Center.y ) \u003E 1.0f ? MathF.Sign( localPoint.y - bounds.Center.y ) : (Game.Random.Float() \u003C 0.5f ? -1.0f : 1.0f);\n\t\tvar edge = new Vector3( localPoint.x, bounds.Center.y \u002B side * bounds.Extents.y * 0.95f, bounds.Center.z );\n\t\tvar at = WorldTransform.PointToWorld( edge ).WithZ( waterZ );\n\n\t\t// Out from the side, and from the bow forward as well: the bow throws its spray ahead and to both sides.\n\t\tfloat bow = ((localPoint.x - bounds.Center.x) / bounds.Extents.x).Clamp( 0.0f, 1.0f );\n\t\tvar outward = WorldRotation.Left * side \u002B WorldRotation.Forward * bow * 0.8f;\n\n\t\tspray.Emit( at, m_Body.GetVelocityAtPoint( at ), outward, strength * SprayAmount );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The water\u0027s drag on the hull moving through it: slight along the hull, strong across it, both with the square of\n\t/// the speed against the water - so the water\u0027s own movement pulls the hull along with it.\n\t/// \u003C/summary\u003E\n\tprivate void ApplyDrag( float mass )\n\t{\n\t\tvar relative = (m_Body.Velocity - WaterVelocity).WithZ( 0 ) / UnitsPerMetre;\n\t\tvar forward = WorldRotation.Forward.WithZ( 0 );\n\t\tforward = forward.Length \u003E 1e-3f ? forward.Normal : Vector3.Forward;\n\t\tvar side = new Vector3( -forward.y, forward.x, 0 );\n\n\t\tfloat along = Vector3.Dot( relative, forward );\n\t\tfloat across = Vector3.Dot( relative, side );\n\n\t\t// Quadratic, as water drag is, with a small linear part so a hull nearly at rest still settles.\n\t\tfloat fAlong = -(LengthwiseDrag * MathF.Abs( along ) \u002B 0.05f) * along;\n\t\tfloat fAcross = -(SidewaysDrag * MathF.Abs( across ) \u002B 0.3f) * across;\n\n\t\tvar force = (forward * fAlong \u002B side * fAcross) * mass * UnitsPerMetre * Submerged;\n\t\tm_Body.ApplyForce( force );\n\n\t\tvar angular = m_Body.AngularVelocity;\n\t\tm_Body.AngularVelocity = angular * MathF.Max( 0.0f, 1.0f - AngularDrag * Submerged * Time.Delta * (1.0f \u002B angular.Length * 0.5f) );\n\t}\n\n\tprivate void LayOutPoints()\n\t{\n\t\tint count = PointsAlong * PointsAcross;\n\n\t\tif ( m_Points.Length == count )\n\t\t\treturn;\n\n\t\tvar bounds = m_Collider.LocalBounds;\n\t\tm_Points = new Vector3[count];\n\t\tm_LastWater = new float[count];\n\t\tm_WasUnder = new bool[count];\n\t\tm_SprayCooldown = new float[count];\n\t\tArray.Fill( m_LastWater, float.NaN );\n\t\tArray.Fill( m_WasUnder, true );\n\n\t\tfor ( int a = 0; a \u003C PointsAlong; a\u002B\u002B )\n\t\t{\n\t\t\tfor ( int b = 0; b \u003C PointsAcross; b\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat x = PointsAlong == 1 ? 0.0f : (a / (PointsAlong - 1.0f) * 2.0f - 1.0f) * Spread.x;\n\t\t\t\tfloat y = PointsAcross == 1 ? 0.0f : (b / (PointsAcross - 1.0f) * 2.0f - 1.0f) * Spread.y;\n\t\t\t\tm_Points[a * PointsAcross \u002B b] = bounds.Center \u002B new Vector3( x * bounds.Extents.x, y * bounds.Extents.y, 0.0f );\n\t\t\t}\n\t\t}\n\t}\n}\n"}]}