{"TotalCount":48,"Files":[{"Ident":"redsnail.watertool","Path":"Water/WaterCalmVolume.cs","FileName":"WaterCalmVolume.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.Volumes;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Calms the water inside a volume: wave displacement (and the surface normals that\r\n/// come from it) smoothly fade to flat. Affects every water surface \u2014 WaterQuad,\r\n/// WaterBodyRenderer and WaterFlow \u2014 so it\u0027s the clean way to blend two of them\r\n/// together. The classic use is a river mouth meeting an ocean: drop a calm volume\r\n/// over the junction, set both surfaces to the same height there, and the wave\r\n/// mismatch (ocean chop poking above the river, seams) disappears.\r\n///\r\n/// Purely visual \u2014 it doesn\u0027t touch buoyancy, swimming or the flow current.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Water Calm Volume\u0022)]\r\n[Category(\u0022Volumes\u0022)]\r\n[Icon(\u0022water\u0022)]\r\npublic sealed class WaterCalmVolume : VolumeComponent, Component.ExecuteInEditor\r\n{\r\n\t// 0 = no effect, 1 = perfectly flat at the core. Lets a volume only partially\r\n\t// settle the water if you want some residual motion.\r\n\t[Property, Range(0.0f, 1.0f)] public float Strength { get; set; } = 1.0f;\r\n\r\n\t// Fraction of the volume (from each face inward) over which the calming ramps in.\r\n\t// 0 = hard edge (a visible crease), 1 = ramps all the way from the center.\r\n\t[Property, Range(0.05f, 1.0f)] public float Falloff { get; set; } = 0.4f;\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterCalmVolumesList();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterCalmVolumesList();\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tbase.DrawGizmos();\r\n\r\n\t\tif (!Gizmo.IsSelected)\r\n\t\t\treturn;\r\n\r\n\t\t// Faint fill so calm volumes read differently from exclusion volumes\r\n\t\tBBox box = SceneVolume.GetBounds();\r\n\r\n\t\tGizmo.Draw.Color = Color.Cyan.WithAlpha(0.06f);\r\n\t\tGizmo.Draw.SolidBox(box);\r\n\t}\r\n\r\n\tpublic (Vector3 Center, Vector3 Forward, Vector3 Up, Vector3 HalfExtents) GetWorldOBB()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\t\tVector3 center = WorldTransform.PointToWorld(local.Center);\r\n\t\tVector3 halfExtents = local.Size * 0.5f;\r\n\r\n\t\treturn (center, WorldRotation.Forward, WorldTransform.Up, halfExtents);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterManager.cs","FileName":"WaterManager.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\nusing Sandbox.Rendering;\r\nusing RenderStage = Sandbox.Rendering.Stage;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Owns the scene\u0027s water: the registry of every surface and volume, the CPU queries gameplay asks (IsPositionInsideAny,\r\n/// GetWaterHeightAt...) and the drawing.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Not DontExecuteOnServer, on purpose: a dedicated server simulates drowning and the buoyancy of the bodies it owns, so it\r\n/// needs the same answers as everyone else. It runs there and skips only the GPU side, on Application.IsHeadless. OnPreRender,\r\n/// where the drawing is recorded, never runs there at all: the engine skips PreRender on that same test.\r\n/// \u003C/remarks\u003E\r\n[Title(\u0022Water Manager\u0022)]\r\npublic partial class WaterManager : Component, Component.ExecuteInEditor, IHotloadManaged\r\n{\r\n\t[SkipHotload] public static WaterManager Current { get; private set; } = null;\r\n\r\n\t[Property(Title = \u0022Ocean\u0022), Group(\u0022Profile\u0022), Order(0)] public WaterDefinition OceanWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Lake\u0022), Group(\u0022Profile\u0022)] public WaterDefinition LakeWaveProfile { get; set; }\r\n\t[Property(Title = \u0022River\u0022), Group(\u0022Profile\u0022)] public WaterDefinition RiverWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Pool\u0022), Group(\u0022Profile\u0022)] public WaterDefinition PoolWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Custom\u0022), Group(\u0022Profile\u0022)] public WaterDefinition CustomWaveProfile { get; set; }\r\n\r\n\t[Property(Title = \u0022Underwater Volume\u0022), Group(\u0022Post Processing\u0022)] public PostProcessVolume UnderwaterPostProcessVolume { get; set; }\r\n\r\n\t// Skips the whole compute \u002B draw for any bounded water surface (pools, rivers) whose\r\n\t// bounds fall outside the camera frustum. The single biggest win when a scene has many\r\n\t// separate WaterQuads scattered around. Infinite oceans (WaterBodyRenderer) are never culled.\r\n\t[Property(Title = \u0022Frustum Culling\u0022), Group(\u0022Performance\u0022)] public bool EnableFrustumCulling { get; set; } = true;\r\n\t// Extra slack (world units) added to each surface\u0027s bounds before the frustum test, so\r\n\t// surfaces at the screen edge don\u0027t pop when the camera turns quickly.\r\n\t[Property(Title = \u0022Cull Padding\u0022), Group(\u0022Performance\u0022)] public float CullPadding { get; set; } = 256.0f;\r\n\t// Beyond this distance (world units, measured to the nearest point of a surface\u0027s bounds)\r\n\t// the surface is skipped entirely. 0 = no distance limit. Independent of frustum culling.\r\n\t[Property(Title = \u0022Max Render Distance\u0022), Group(\u0022Performance\u0022)] public float MaxRenderDistance { get; set; } = 25000.0f;\r\n\r\n\t// Distance LOD: distant water quads drop tessellation instead of staying at full density.\r\n\t// Each level halves the cell count and doubles the cell size, so the surface covers exactly\r\n\t// the same area with 4x fewer vertices \u2014 coverage, ring layout and texture tiling are all\r\n\t// unchanged, only the triangle density falls off with distance.\r\n\t[Property(Title = \u0022Distance LOD\u0022), Group(\u0022Performance\u0022)] public bool EnableDistanceLod { get; set; } = true;\r\n\t// Distance at which LOD 1 begins; each level after that doubles (LOD 2 at 2x, LOD 3 at 4x).\r\n\t[Property(Title = \u0022LOD Start Distance\u0022), Group(\u0022Performance\u0022)] public float LodStartDistance { get; set; } = 1000.0f;\r\n\t[Property(Title = \u0022Max LOD Level\u0022), Group(\u0022Performance\u0022), Range(0, 4)] public int MaxLodLevel { get; set; } = 3;\r\n\r\n\tprivate ComputeShader m_ComputeShader;\r\n\r\n\tprivate CommandList m_CommandList = new(\u0022Water Rendering\u0022);\r\n\r\n\tprivate CameraComponent m_LastCamera;\r\n\tprivate Vector3 m_CameraPosition;\r\n\tprivate Frustum m_CullFrustum;\r\n\tprivate bool m_HasCullFrustum;\r\n\tprivate WaterDefinition m_DefaultProfile;\r\n\r\n\t// Rebuilt each frame by RecordCommandList: the bounded surfaces that survived frustum culling. Reused\r\n\t// across the compute / barrier / draw phases so the decision is made exactly once.\r\n\tprivate readonly List\u003CWaterQuad\u003E m_VisibleQuads = [];\r\n\tprivate readonly List\u003CWaterFlow\u003E m_VisibleFlows = [];\r\n\r\n\tprivate List\u003CWaterQuad\u003E Quads { get; } = [];\r\n\tprivate List\u003CWaterBodyRenderer\u003E QuadRenderers { get; } = [];\r\n\tpublic List\u003CWaterBody\u003E Bodies { get; } = [];\r\n\tpublic List\u003CWaterFlow\u003E Flows { get; } = [];\r\n\tpublic List\u003CWaterExclusionVolume\u003E ExclusionVolumes { get; } = [];\r\n\tpublic List\u003CHullWaterExclusionVolume\u003E HullExclusionVolumes { get; } = [];\r\n\r\n\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tCurrent = Scene.Get\u003CWaterManager\u003E();\r\n\r\n\t\t// Only the drawing dispatches it\r\n\t\tif (!Application.IsHeadless)\r\n\t\t\tm_ComputeShader = new ComputeShader(\u0022water_clipmap_cs\u0022);\r\n\r\n\t\tm_DefaultProfile = new WaterDefinition();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tUpdateCommandListRegistration();\r\n\r\n\t\tRefreshWaterQuadsList();\r\n\t\tRefreshWaterBodyRenderersList();\r\n\t\tRefreshWaterBodiesList();\r\n\t\tRefreshWaterExclusionVolumesList();\r\n\t\tRefreshWaterHullExclusionVolumesList();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tm_RippleBuffer?.Dispose();\r\n\t\tm_RippleBuffer = null;\r\n\r\n\t\tClearCalmVolumes();\r\n\r\n\t\t// Unregister from the camera we actually registered with. Scene.Camera can have changed\r\n\t\t// (or gone) since then, so asking for it again would leave the list attached to a camera\r\n\t\t// we never clean up.\r\n\t\tif (m_LastCamera.IsValid())\r\n\t\t\tm_LastCamera.RemoveCommandList(m_CommandList);\r\n\r\n\t\tm_LastCamera = null;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keeps the compute command list attached to a camera that will actually replay it. This has\r\n\t/// to run every frame, not just on enable: a scene starting without a camera would never\r\n\t/// register at all, and leaving play mode destroys the play camera without the reference here\r\n\t/// turning null, so comparing references alone would leave us bound to a dead camera forever.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void UpdateCommandListRegistration()\r\n\t{\r\n\t\tvar renderCamera = GetRenderCamera();\r\n\r\n\t\tif (renderCamera == m_LastCamera \u0026\u0026 m_LastCamera.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tif (m_LastCamera.IsValid())\r\n\t\t\tm_LastCamera.RemoveCommandList(m_CommandList);\r\n\r\n\t\tm_LastCamera = null;\r\n\r\n\t\tif (renderCamera.IsValid())\r\n\t\t{\r\n\t\t\trenderCamera.AddCommandList(m_CommandList, RenderStage.AfterTransparent);\r\n\t\t\tm_LastCamera = renderCamera;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The camera whose command list actually replays. A scene camera does so in the editor\r\n\t/// viewport as well as in game, so it wins when one exists; with no camera in the scene the\r\n\t/// editor camera is the only thing left that will replay ours.\r\n\t/// \u003C/summary\u003E\r\n\tprivate CameraComponent GetRenderCamera()\r\n\t{\r\n\t\t// Nothing replays a command list where nothing renders. A dedicated server still has a Scene.Camera (the scene\u0027s\r\n\t\t// own, parked wherever it was placed), so this has to be ruled out before asking for it.\r\n\t\tif (Application.IsHeadless)\r\n\t\t\treturn null;\r\n\r\n\t\tif (Scene.Camera.IsValid())\r\n\t\t\treturn Scene.Camera;\r\n\r\n\t\tif (Scene.IsEditor)\r\n\t\t\treturn Application.Editor?.Camera;\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// World position the water should treat as the viewer, for anything that culls or picks\r\n\t/// volumes by distance. While editing that has to be the viewport camera rather than the scene\r\n\t/// camera, or volumes are gathered around wherever the game camera happens to be parked and the\r\n\t/// water you are actually looking at gets the wrong set. Falls back when no camera exists at\r\n\t/// all, which is a real case - Scene.Camera excludes the editor camera and can be null.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 GetViewPosition(Scene scene, Vector3 fallback = default)\r\n\t{\r\n\t\tif (!scene.IsValid())\r\n\t\t\treturn fallback;\r\n\r\n\t\tif (scene.IsEditor)\r\n\t\t{\r\n\t\t\tvar editorCamera = Application.Editor?.Camera;\r\n\t\t\tif (editorCamera.IsValid())\r\n\t\t\t\treturn editorCamera.WorldPosition;\r\n\t\t}\r\n\r\n\t\treturn scene.Camera.IsValid() ? scene.Camera.WorldPosition : fallback;\r\n\t}\r\n\r\n\r\n\r\n\tvoid IHotloadManaged.Destroyed(Dictionary\u003Cstring, object\u003E _State)\r\n\t{\r\n\t\t_State[\u0022IsActive\u0022] = Current == this;\r\n\t}\r\n\r\n\r\n\r\n\tvoid IHotloadManaged.Created(IReadOnlyDictionary\u003Cstring, object\u003E _State)\r\n\t{\r\n\t\tif (_State.GetValueOrDefault(\u0022IsActive\u0022) is true)\r\n\t\t\tCurrent = this;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether a bounded water surface should render this frame: inside the cull camera\u0027s\r\n\t/// frustum and within the max render distance. Returns true \u2014 render it \u2014 when there\u0027s\r\n\t/// no viewer, or when both culls are disabled.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Csummary\u003EDistance at which the given LOD level starts (level 1 = LodStartDistance).\u003C/summary\u003E\r\n\tprivate float LodThreshold(int lod) =\u003E LodStartDistance * MathF.Pow(2.0f, lod - 1);\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Resolves the tessellation LOD for a surface from how far its bounds are from the viewer.\r\n\t/// Takes the surface\u0027s current level so the switch can be hysteretic: a level only changes\r\n\t/// once the distance is comfortably past the boundary, otherwise a camera hovering right on\r\n\t/// a threshold would rebuild that surface\u0027s GPU buffers every frame.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int ComputeLodLevel(BBox worldBounds, int currentLod)\r\n\t{\r\n\t\tif (!EnableDistanceLod || !m_HasCullFrustum || MaxLodLevel \u003C= 0 || LodStartDistance \u003C= 0.0f)\r\n\t\t\treturn 0;\r\n\r\n\t\tconst float hysteresis = 0.15f;\r\n\r\n\t\tfloat distance = worldBounds.ClosestPoint(m_CameraPosition).Distance(m_CameraPosition);\r\n\r\n\t\tint lod = Math.Clamp(currentLod, 0, MaxLodLevel);\r\n\r\n\t\t// Step out as the surface recedes, in as it approaches \u2014 one level at a time\r\n\t\twhile (lod \u003C MaxLodLevel \u0026\u0026 distance \u003E LodThreshold(lod \u002B 1) * (1.0f \u002B hysteresis))\r\n\t\t\tlod\u002B\u002B;\r\n\r\n\t\twhile (lod \u003E 0 \u0026\u0026 distance \u003C LodThreshold(lod) * (1.0f - hysteresis))\r\n\t\t\tlod--;\r\n\r\n\t\treturn lod;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsRenderVisible(BBox worldBounds)\r\n\t{\r\n\t\t// Both culls need a viewer; without one, don\u0027t cull anything.\r\n\t\tif (!m_HasCullFrustum)\r\n\t\t\treturn true;\r\n\r\n\t\t// Distance cull \u2014 measured to the nearest point of the bounds, so a large surface\r\n\t\t// whose centre is far but edge is near still renders.\r\n\t\tif (MaxRenderDistance \u003E 0.0f)\r\n\t\t{\r\n\t\t\tfloat distSq = worldBounds.ClosestPoint(m_CameraPosition).DistanceSquared(m_CameraPosition);\r\n\r\n\t\t\tif (distSq \u003E MaxRenderDistance * MaxRenderDistance)\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t// Frustum cull\r\n\t\tif (EnableFrustumCulling \u0026\u0026 !m_CullFrustum.IsInside(worldBounds.Grow(CullPadding), partially: true))\r\n\t\t\treturn false;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Records the water once per frame on the main thread, the way the engine\u0027s own command-list components do: after\r\n\t/// every surface\u0027s OnUpdate has settled its buffers and attributes, with the cameras final, before anything draws.\r\n\t/// The camera the list is attached to replays it at AfterTransparent.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cremarks\u003E\r\n\t/// This used to be the RenderOverride of a SceneCustomObject, which the renderer calls once for EVERY view that draws\r\n\t/// the scene world - the main camera, a mirror\u0027s camera, a realtime probe\u0027s captures - and views draw in parallel.\r\n\t/// Each call reset and re-recorded the one shared list, so whichever view recorded last, or two recording at once,\r\n\t/// decided what the main camera replayed: the water flickered, and two calls at once threw \u0022Collection was modified\u0022\r\n\t/// on the visible lists.\r\n\t/// \u003C/remarks\u003E\r\n\tprotected override void OnPreRender()\r\n\t{\r\n\t\tRecordCommandList();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RecordCommandList()\r\n\t{\r\n\t\tm_CommandList.Reset();\r\n\r\n\t\t// Frustum-cull the bounded surfaces once, up front. The compute / barrier / draw\r\n\t\t// phases below all iterate these lists, so a culled surface pays for nothing.\r\n\t\tm_VisibleQuads.Clear();\r\n\t\tforeach (var quad in Quads)\r\n\t\t{\r\n\t\t\tif (quad.IsValid() \u0026\u0026 quad.ParticipatesInRendering \u0026\u0026 IsRenderVisible(quad.GetWorldBounds2D()))\r\n\t\t\t\tm_VisibleQuads.Add(quad);\r\n\t\t}\r\n\r\n\t\tm_VisibleFlows.Clear();\r\n\t\tforeach (var flow in Flows)\r\n\t\t{\r\n\t\t\tif (flow.IsValid() \u0026\u0026 flow.ParticipatesInRendering \u0026\u0026 IsRenderVisible(flow.GetWorldBounds()))\r\n\t\t\t\tm_VisibleFlows.Add(flow);\r\n\t\t}\r\n\r\n\t\tbool hasAnythingToRender = false;\r\n\r\n\t\t// Renderers are the infinite ocean surfaces \u2014 never culled (their bounds are \u0022everywhere\u0022)\r\n\t\tforeach (var renderer in QuadRenderers)\r\n\t\t{\r\n\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\thasAnythingToRender = true;\r\n\t\t\trenderer.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);\r\n\t\t}\r\n\r\n\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t{\r\n\t\t\thasAnythingToRender = true;\r\n\t\t\tquad.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);\r\n\t\t}\r\n\r\n\t\t// Flows build their mesh on the CPU (no compute pass or barrier needed)\r\n\t\tif (m_VisibleFlows.Count \u003E 0)\r\n\t\t\thasAnythingToRender = true;\r\n\r\n\t\tif (hasAnythingToRender)\r\n\t\t{\r\n\t\t\tforeach (var renderer in QuadRenderers)\r\n\t\t\t{\r\n\t\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\trenderer.BarrierTransition(m_CommandList);\r\n\t\t\t}\r\n\r\n\t\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t\t\tquad.BarrierTransition(m_CommandList);\r\n\r\n\t\t\tm_CommandList.Attributes.GrabFrameTexture(\u0022FrameBufferCopyTexture\u0022);\r\n\r\n\t\t\tforeach (var renderer in QuadRenderers)\r\n\t\t\t{\r\n\t\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\trenderer.Draw(m_CommandList);\r\n\t\t\t}\r\n\r\n\t\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t\t\tquad.Draw(m_CommandList);\r\n\r\n\t\t\tforeach (var flow in m_VisibleFlows)\r\n\t\t\t\tflow.Draw(m_CommandList);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// We\u0027ve to make sure it\u0027s always correct while in the editor\r\n\t\t// (S\u0026box is a complete mess when it comes to managing a singleton properly on a component that execute in the editor, bcs its reference get constantly swapped between\r\n\t\t// gameplay and editor, we\u0027ve to do this non sense !)\r\n\t\tif (Scene.IsEditor)\r\n\t\t\tCurrent = Scene.Get\u003CWaterManager\u003E();\r\n\r\n\t\tUpdateRipples();\r\n\r\n\t\t// Everything below only feeds the drawing, which a dedicated server doesn\u0027t do: its queries read the volumes, and the\r\n\t\t// ripples kept above, directly.\r\n\t\tif (Application.IsHeadless)\r\n\t\t\treturn;\r\n\r\n\t\tUpdateCommandListRegistration();\r\n\r\n\t\t// The camera we cull and centre the clipmap against: the game camera while playing,\r\n\t\t// otherwise the editor viewport camera so culling follows what you\u0027re actually looking at.\r\n\t\tCameraComponent cullCamera = Game.IsPlaying ? Scene.Camera : Application.Editor?.Camera;\r\n\r\n\t\tif (cullCamera.IsValid())\r\n\t\t{\r\n\t\t\tm_CameraPosition = cullCamera.WorldPosition;\r\n\t\t\tm_CullFrustum = cullCamera.GetFrustum();\r\n\t\t\tm_HasCullFrustum = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tm_CameraPosition = Vector3.Zero;\r\n\t\t\tm_HasCullFrustum = false;\r\n\t\t}\r\n\r\n\t\tif (UnderwaterPostProcessVolume.IsValid())\r\n\t\t\tUnderwaterPostProcessVolume.Enabled = IsPositionInsideAny(m_CameraPosition);\r\n\r\n\t\tUploadRipples();\r\n\t\tUpdateCalmVolumes();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// We have to do all this non sense bcs using a Register/Unregister logic with OnEnabled/OnDisabled is a complete\r\n\t/// mess to manage when we enter play mode/stop play mode in the editor, the references get duplicated etc... Otherwise we\u0027ve to check by gameobject id...\r\n\t/// It\u0027s just way too annoying, refreshing the whole list is safer and we\u0027re always sure to have the proper count of components\r\n\t/// \u003C/summary\u003E\r\n\tpublic void RefreshWaterQuadsList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tQuads.Clear();\r\n\t\tQuads.AddRange(Scene.GetAll\u003CWaterQuad\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterBodyRenderersList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tQuadRenderers.Clear();\r\n\t\tQuadRenderers.AddRange(Scene.GetAll\u003CWaterBodyRenderer\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterBodiesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tBodies.Clear();\r\n\t\tBodies.AddRange(Scene.GetAll\u003CWaterBody\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterFlowsList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tFlows.Clear();\r\n\t\tFlows.AddRange(Scene.GetAll\u003CWaterFlow\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterExclusionVolumesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tExclusionVolumes.Clear();\r\n\t\tExclusionVolumes.AddRange(Scene.GetAll\u003CWaterExclusionVolume\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterHullExclusionVolumesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tHullExclusionVolumes.Clear();\r\n\t\tHullExclusionVolumes.AddRange(Scene.GetAll\u003CHullWaterExclusionVolume\u003E());\r\n\t}\r\n\r\n\tprivate WaterDefinition GetWaveProfileForType(WaterBodyType waterType) =\u003E waterType switch\r\n\t{\r\n\t\tWaterBodyType.Ocean =\u003E OceanWaveProfile,\r\n\t\tWaterBodyType.Lake =\u003E LakeWaveProfile,\r\n\t\tWaterBodyType.River =\u003E RiverWaveProfile,\r\n\t\tWaterBodyType.Pool =\u003E PoolWaveProfile,\r\n\t\t_ =\u003E CustomWaveProfile\r\n\t};\r\n\r\n\tpublic static WaterDefinition GetWaveProfile(WaterBodyType _WaterType)\r\n\t{\r\n\t\tif (Current == null)\r\n\t\t\treturn null;\r\n\r\n\t\tWaterDefinition profile = Current.GetWaveProfileForType(_WaterType);\r\n\r\n\t\tif (profile.IsValid())\r\n\t\t\treturn profile;\r\n\r\n\t\tLog.Warning(\u0022[WaterTool] No water profile found in the \u0027Water Manager\u0027, please add a water profile for the specified water type ! (Project Settings \u003E Water Manager \u003E \u0027Assign the profiles\u0027)\u0022);\r\n\r\n\t\treturn Current.m_DefaultProfile;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterManager.CalmVolumes.cs","FileName":"WaterManager.CalmVolumes.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\npublic partial class WaterManager\r\n{\r\n\t// Calm volumes are few (river/ocean junctions) and apply to every water surface,\r\n\t// so \u2014 like ripples \u2014 they live in one shared buffer the manager updates once a\r\n\t// frame, rather than the per-component distance-sorted exclusion-volume pattern.\r\n\r\n\tprivate const int MAX_CALM_VOLUMES = 64;\r\n\tprivate const int CALM_VOLUME_ROWS = 4;\r\n\r\n\tpublic List\u003CWaterCalmVolume\u003E CalmVolumes { get; } = [];\r\n\r\n\tprivate GpuBuffer\u003CVector4\u003E m_CalmVolumeBuffer;\r\n\tprivate readonly Vector4[] m_CalmVolumeData = new Vector4[MAX_CALM_VOLUMES * CALM_VOLUME_ROWS];\r\n\tprivate int m_ActiveCalmCount;\r\n\r\n\r\n\r\n\tpublic void RefreshWaterCalmVolumesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tCalmVolumes.Clear();\r\n\t\tCalmVolumes.AddRange(Scene.GetAll\u003CWaterCalmVolume\u003E());\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateCalmVolumes()\r\n\t{\r\n\t\tint count = 0;\r\n\r\n\t\tforeach (var volume in CalmVolumes)\r\n\t\t{\r\n\t\t\tif (!volume.IsValid() || !volume.Active)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (count \u003E= MAX_CALM_VOLUMES)\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tvar (center, forward, up, half) = volume.GetWorldOBB();\r\n\r\n\t\t\tint row = count * CALM_VOLUME_ROWS;\r\n\t\t\tm_CalmVolumeData[row \u002B 0] = new Vector4(forward.x, forward.y, forward.z, half.x);\r\n\t\t\tm_CalmVolumeData[row \u002B 1] = new Vector4(up.x, up.y, up.z, half.y);\r\n\t\t\tm_CalmVolumeData[row \u002B 2] = new Vector4(center.x, center.y, center.z, half.z);\r\n\t\t\tm_CalmVolumeData[row \u002B 3] = new Vector4(volume.Falloff, volume.Strength, 0.0f, 0.0f);\r\n\r\n\t\t\tcount\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tm_ActiveCalmCount = count;\r\n\r\n\t\tEnsureCalmBuffer();\r\n\r\n\t\tm_CalmVolumeBuffer.SetData(m_CalmVolumeData.AsSpan(0, count * CALM_VOLUME_ROWS));\r\n\t}\r\n\r\n\tprivate void EnsureCalmBuffer()\r\n\t{\r\n\t\tif (!m_CalmVolumeBuffer.IsValid())\r\n\t\t\tm_CalmVolumeBuffer = new GpuBuffer\u003CVector4\u003E(MAX_CALM_VOLUMES * CALM_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\tinternal void ApplyCalmAttributes(RenderAttributes _Attributes)\r\n\t{\r\n\t\t_Attributes.Set(\u0022WaterCalmVolumeCount\u0022, m_ActiveCalmCount);\r\n\r\n\t\tif (m_CalmVolumeBuffer.IsValid())\r\n\t\t\t_Attributes.Set(\u0022WaterCalmVolumeData\u0022, m_CalmVolumeBuffer);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// CPU evaluation of the calm factor at a world position (0 = full waves, 1 = flat).\r\n\t/// MUST mirror ComputeWaterCalm() in water_calm_volume.fxc so physics (buoyancy,\r\n\t/// height queries) matches the flattened visual surface.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float ComputeCalm(Vector3 _WorldPosition)\r\n\t{\r\n\t\tif (CalmVolumes.Count == 0)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tfloat calm = 0.0f;\r\n\r\n\t\tforeach (var volume in CalmVolumes)\r\n\t\t{\r\n\t\t\tif (!volume.IsValid() || !volume.Active)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar (center, forward, up, half) = volume.GetWorldOBB();\r\n\r\n\t\t\tVector3 right = Vector3.Cross(up, forward);\r\n\t\t\tVector3 d = _WorldPosition - center;\r\n\r\n\t\t\tfloat nx = MathF.Abs(Vector3.Dot(d, forward)) / MathF.Max(half.x, 0.001f);\r\n\t\t\tfloat ny = MathF.Abs(Vector3.Dot(d, right)) / MathF.Max(half.y, 0.001f);\r\n\t\t\tfloat nz = MathF.Abs(Vector3.Dot(d, up)) / MathF.Max(half.z, 0.001f);\r\n\r\n\t\t\tfloat nmax = MathF.Max(nx, MathF.Max(ny, nz));\r\n\r\n\t\t\tfloat falloffStart = Math.Clamp(1.0f - volume.Falloff, 0.0f, 1.0f);\r\n\t\t\tfloat volumeCalm = (1.0f - SmoothStep(falloffStart, 1.0f, nmax)) * volume.Strength;\r\n\r\n\t\t\tcalm = MathF.Max(calm, volumeCalm);\r\n\t\t}\r\n\r\n\t\treturn Math.Clamp(calm, 0.0f, 1.0f);\r\n\t}\r\n\r\n\t// Matches HLSL smoothstep().\r\n\tprivate static float SmoothStep(float _Edge0, float _Edge1, float _X)\r\n\t{\r\n\t\tfloat t = Math.Clamp((_X - _Edge0) / MathF.Max(_Edge1 - _Edge0, 1e-6f), 0.0f, 1.0f);\r\n\t\treturn t * t * (3.0f - 2.0f * t);\r\n\t}\r\n\r\n\tprivate void ClearCalmVolumes()\r\n\t{\r\n\t\tm_CalmVolumeBuffer?.Dispose();\r\n\t\tm_CalmVolumeBuffer = null;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterWaveUtility.cs","FileName":"WaterWaveUtility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\npublic enum WaterBodyType\r\n{\r\n\tOcean,\r\n\tLake,\r\n\tRiver,\r\n\tPool,\r\n\tCustom\r\n}\r\n\r\npublic static class WaterWaveUtility\r\n{\r\n\tpublic static Vector3 ComputeDisplacementAt(Vector2 worldXY, WaterDefinition profile)\r\n\t{\r\n\t\tVector3 detail = ComputeGerstner(worldXY, profile.WavesScale, profile.WavesSpeed, profile.WavesDirection, profile.WavesOctaves, profile.WavesLacunarity, profile.WavesPersistence, profile.WavesSteepness) * profile.WavesIntensity;\r\n\t\tVector3 swell = ComputeGerstner(worldXY, profile.SwellScale, profile.SwellSpeed, profile.SwellDirection, profile.SwellOctaves, profile.SwellLacunarity, profile.SwellPersistence, profile.SwellSteepness) * profile.SwellIntensity;\r\n\t\treturn detail \u002B swell;\r\n\t}\r\n\r\n\tpublic static Vector3 ComputeVelocityAt(Vector2 worldXY, WaterDefinition profile)\r\n\t{\r\n\t\tVector3 detail = ComputeGerstnerVelocity(worldXY, profile.WavesScale, profile.WavesSpeed, profile.WavesDirection, profile.WavesOctaves, profile.WavesLacunarity, profile.WavesPersistence, profile.WavesSteepness) * profile.WavesIntensity;\r\n\t\tVector3 swell = ComputeGerstnerVelocity(worldXY, profile.SwellScale, profile.SwellSpeed, profile.SwellDirection, profile.SwellOctaves, profile.SwellLacunarity, profile.SwellPersistence, profile.SwellSteepness) * profile.SwellIntensity;\r\n\t\treturn detail \u002B swell;\r\n\t}\r\n\r\n\tprivate static Vector3 ComputeGerstner(Vector2 worldXY, float scale, float speed, Vector2 direction, int octaves, float lacunarity, float persistence, float steepness)\r\n\t{\r\n\t\tif (scale \u003C= 0.0f || speed \u003C= 0.0f || octaves \u003C= 0)\r\n\t\t\treturn Vector3.Zero;\r\n\r\n\t\tVector2 waveDirection = direction.Normal;\r\n\t\tfloat t = Time.Now * speed;\r\n\r\n\t\tVector3 displacement = Vector3.Zero;\r\n\t\tfloat amp = 1.0f;\r\n\t\tfloat freq = scale;\r\n\t\tfloat maxAmp = 0f;\r\n\r\n\t\tfor (int oct = 0; oct \u003C octaves; oct\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat angle = oct * 1.2f;\r\n\t\t\tVector2 octDir = new(\r\n\t\t\t\twaveDirection.x * MathF.Cos(angle) - waveDirection.y * MathF.Sin(angle),\r\n\t\t\t\twaveDirection.x * MathF.Sin(angle) \u002B waveDirection.y * MathF.Cos(angle)\r\n\t\t\t);\r\n\r\n\t\t\tfloat phase = freq * (octDir.x * worldXY.x \u002B octDir.y * worldXY.y) \u002B t * freq * 0.5f;\r\n\t\t\tdisplacement.x \u002B= steepness * amp * octDir.x * MathF.Cos(phase);\r\n\t\t\tdisplacement.y \u002B= steepness * amp * octDir.y * MathF.Cos(phase);\r\n\t\t\tdisplacement.z \u002B= amp * MathF.Sin(phase);\r\n\r\n\t\t\tmaxAmp \u002B= amp;\r\n\t\t\tamp *= persistence;\r\n\t\t\tfreq *= lacunarity;\r\n\t\t}\r\n\r\n\t\treturn maxAmp \u003E 0.0f ? displacement / maxAmp : Vector3.Zero;\r\n\t}\r\n\r\n\tprivate static Vector3 ComputeGerstnerVelocity(Vector2 worldXY, float scale, float speed, Vector2 direction, int octaves, float lacunarity, float persistence, float steepness)\r\n\t{\r\n\t\tif (scale \u003C= 0.0f || speed \u003C= 0.0f || octaves \u003C= 0)\r\n\t\t\treturn Vector3.Zero;\r\n\r\n\t\tVector2 waveDirection = direction.Normal;\r\n\t\tfloat t = Time.Now * speed;\r\n\r\n\t\tVector3 velocity = Vector3.Zero;\r\n\t\tfloat amp = 1.0f;\r\n\t\tfloat freq = scale;\r\n\t\tfloat maxAmp = 0f;\r\n\r\n\t\tfor (int oct = 0; oct \u003C octaves; oct\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat angle = oct * 1.2f;\r\n\t\t\tVector2 octDir = new(\r\n\t\t\t\twaveDirection.x * MathF.Cos(angle) - waveDirection.y * MathF.Sin(angle),\r\n\t\t\t\twaveDirection.x * MathF.Sin(angle) \u002B waveDirection.y * MathF.Cos(angle)\r\n\t\t\t);\r\n\r\n\t\t\tfloat phase = freq * (octDir.x * worldXY.x \u002B octDir.y * worldXY.y) \u002B t * freq * 0.5f;\r\n\t\t\tfloat angularVelocity = freq * speed * 0.5f;\r\n\r\n\t\t\tvelocity.x -= steepness * amp * octDir.x * angularVelocity * MathF.Sin(phase);\r\n\t\t\tvelocity.y -= steepness * amp * octDir.y * angularVelocity * MathF.Sin(phase);\r\n\t\t\tvelocity.z \u002B= amp * angularVelocity * MathF.Cos(phase);\r\n\r\n\t\t\tmaxAmp \u002B= amp;\r\n\t\t\tamp *= persistence;\r\n\t\t\tfreq *= lacunarity;\r\n\t\t}\r\n\r\n\t\treturn maxAmp \u003E 0.0f ? velocity / maxAmp : Vector3.Zero;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/HullWaterExclusionVolume.cs","FileName":"HullWaterExclusionVolume.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Excludes the water surface inside a mesh hull rather than an approximated box volume.\r\n/// Place on the same GameObject as the ModelRenderer. The physics collision mesh is extracted\r\n/// once and uploaded to the GPU as a triangle list; only the WorldToLocal matrix is updated\r\n/// each frame as the object moves or rotates.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Hull Water Exclusion Volume\u0022), Group(\u0022Water\u0022), Icon(\u0022sailing\u0022)]\r\npublic sealed class HullWaterExclusionVolume : Component, Component.ExecuteInEditor\r\n{\r\n\t/// \u003Csummary\u003ETriangle vertices in model LOCAL space, flat (v0,v1,v2, v0,v1,v2 \u2026).\u003C/summary\u003E\r\n\tpublic Vector3[] LocalTriangles { get; private set; } = Array.Empty\u003CVector3\u003E();\r\n\r\n\t/// \u003Csummary\u003EAABB of all local triangles, used for early GPU rejection.\u003C/summary\u003E\r\n\tpublic BBox LocalAABB { get; private set; }\r\n\r\n\t[Property] private Model CustomModel { get; set; }\r\n\r\n\tprivate Model _lastModel;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tRebuildMesh();\r\n\r\n\t\tWaterManager.Current?.RefreshWaterHullExclusionVolumesList();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterHullExclusionVolumesList();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tvar model = CustomModel.IsValid() ? CustomModel : GetComponent\u003CModelRenderer\u003E()?.Model;\r\n\r\n\t\tif (model != _lastModel)\r\n\t\t\tRebuildMesh();\r\n\t}\r\n\r\n\tprivate void RebuildMesh()\r\n\t{\r\n\t\tvar model = CustomModel.IsValid() ? CustomModel : GetComponent\u003CModelRenderer\u003E()?.Model;\r\n\r\n\t\tif (model == null)\r\n\t\t{\r\n\t\t\tLocalTriangles = Array.Empty\u003CVector3\u003E();\r\n\t\t\tLocalAABB = default;\r\n\t\t\t_lastModel = null;\r\n\t\t\tLog.Warning($\u0022{nameof(HullWaterExclusionVolume)}: No ModelRenderer or Model found.\u0022);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_lastModel = model;\r\n\r\n\t\tvar tris = new List\u003CVector3\u003E();\r\n\t\tvar aabbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);\r\n\t\tvar aabbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);\r\n\r\n\t\t// Prefer the physics collision mesh \u2014 it\u0027s already simplified and watertight.\r\n\t\tvar physics = model.Physics;\r\n\t\tif (physics != null)\r\n\t\t{\r\n\t\t\tforeach (var part in physics.Parts)\r\n\t\t\t{\r\n\t\t\t\tforeach (var meshPart in part.Meshes)\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach (var tri in meshPart.GetTriangles())\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\ttris.Add(tri.A);\r\n\t\t\t\t\t\ttris.Add(tri.B);\r\n\t\t\t\t\t\ttris.Add(tri.C);\r\n\r\n\t\t\t\t\t\taabbMin = Vector3.Min(aabbMin, Vector3.Min(tri.A, Vector3.Min(tri.B, tri.C)));\r\n\t\t\t\t\t\taabbMax = Vector3.Max(aabbMax, Vector3.Max(tri.A, Vector3.Max(tri.B, tri.C)));\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Convex hull shapes have no MeshParts \u2014 triangulate each hull instead.\r\n\t\t\t\tforeach (var hullPart in part.Hulls)\r\n\t\t\t\t{\r\n\t\t\t\t\tvar pts = hullPart.GetPoints()?.ToArray();\r\n\t\t\t\t\tif (pts == null || pts.Length \u003C 4) continue;\r\n\t\t\t\t\tTriangulateConvexHull(pts, tris, ref aabbMin, ref aabbMax);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Fallback: render mesh (may have more triangles, less ideal for GPU iteration)\r\n\t\tif (tris.Count == 0)\r\n\t\t{\r\n\t\t\tvar vertices = model.GetVertices();\r\n\t\t\tvar indices = model.GetIndices();\r\n\r\n\t\t\tif (vertices != null \u0026\u0026 indices != null)\r\n\t\t\t{\r\n\t\t\t\tfor (int i = 0; i \u002B 2 \u003C indices.Length; i \u002B= 3)\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 v0 = vertices[indices[i \u002B 0]].Position;\r\n\t\t\t\t\tVector3 v1 = vertices[indices[i \u002B 1]].Position;\r\n\t\t\t\t\tVector3 v2 = vertices[indices[i \u002B 2]].Position;\r\n\r\n\t\t\t\t\ttris.Add(v0);\r\n\t\t\t\t\ttris.Add(v1);\r\n\t\t\t\t\ttris.Add(v2);\r\n\r\n\t\t\t\t\taabbMin = Vector3.Min(aabbMin, Vector3.Min(v0, Vector3.Min(v1, v2)));\r\n\t\t\t\t\taabbMax = Vector3.Max(aabbMax, Vector3.Max(v0, Vector3.Max(v1, v2)));\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tLocalTriangles = tris.ToArray();\r\n\t\tLocalAABB = tris.Count \u003E 0 ? new BBox(aabbMin, aabbMax) : default;\r\n\t}\r\n\r\n\t// N\u00B3 convex hull triangulation.\r\n\t// Finds each hull face by collecting ALL coplanar vertices, then fan-triangulates once per face.\r\n\t// Without this, rectangular faces (4 coplanar verts) emit C(4,3)=4 overlapping triangles,\r\n\t// flipping the ray parity and incorrectly marking exterior points as inside.\r\n\tprivate static void TriangulateConvexHull(Vector3[] verts, List\u003CVector3\u003E result, ref Vector3 aabbMin, ref Vector3 aabbMax)\r\n\t{\r\n\t\tint n = verts.Length;\r\n\t\tif (n \u003C 4) return;\r\n\r\n\t\tvar centroid = Vector3.Zero;\r\n\t\tforeach (var v in verts) centroid \u002B= v;\r\n\t\tcentroid /= n;\r\n\r\n\t\tvar processedFaces = new HashSet\u003Cstring\u003E();\r\n\r\n\t\tfor (int i = 0; i \u003C n; i\u002B\u002B)\r\n\t\t\tfor (int j = i \u002B 1; j \u003C n; j\u002B\u002B)\r\n\t\t\t\tfor (int k = j \u002B 1; k \u003C n; k\u002B\u002B)\r\n\t\t\t\t{\r\n\t\t\t\t\tVector3 A = verts[i], B = verts[j], C = verts[k];\r\n\t\t\t\t\tVector3 rawNormal = Vector3.Cross(B - A, C - A);\r\n\t\t\t\t\tif (rawNormal.LengthSquared \u003C 1e-8f) continue;\r\n\t\t\t\t\tVector3 normal = rawNormal.Normal; // normalize so d = actual distance in units\r\n\r\n\t\t\t\t\tbool pos = false, neg = false;\r\n\t\t\t\t\tvar faceIndices = new List\u003Cint\u003E { i, j, k };\r\n\r\n\t\t\t\t\tfor (int m = 0; m \u003C n; m\u002B\u002B)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif (m == i || m == j || m == k) continue;\r\n\t\t\t\t\t\tfloat d = Vector3.Dot(normal, verts[m] - A);\r\n\t\t\t\t\t\tif (MathF.Abs(d) \u003C 0.01f)\r\n\t\t\t\t\t\t\tfaceIndices.Add(m);   // coplanar \u2014 part of this face\r\n\t\t\t\t\t\telse if (d \u003E 0f) pos = true;\r\n\t\t\t\t\t\telse neg = true;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (pos \u0026\u0026 neg) continue;     // interior edge, not a hull face\r\n\t\t\t\t\tif (!pos \u0026\u0026 !neg) continue;   // degenerate \u2014 no non-coplanar vertices\r\n\r\n\t\t\t\t\t// Canonical key: sorted vertex indices \u2014 each face processed exactly once.\r\n\t\t\t\t\tfaceIndices.Sort();\r\n\t\t\t\t\tstring key = string.Join(\u0022,\u0022, faceIndices);\r\n\t\t\t\t\tif (!processedFaces.Add(key)) continue;\r\n\r\n\t\t\t\t\t// Collect face vertices and sort by angle around the face centroid.\r\n\t\t\t\t\tvar faceVerts = faceIndices.Select(idx =\u003E verts[idx]).ToList();\r\n\t\t\t\t\tvar fc = Vector3.Zero;\r\n\t\t\t\t\tforeach (var fv in faceVerts) fc \u002B= fv;\r\n\t\t\t\t\tfc /= faceVerts.Count;\r\n\r\n\t\t\t\t\t// Build a 2D frame in the face plane for angle sorting.\r\n\t\t\t\t\tvar outward = (Vector3.Dot(normal, centroid - A) \u003C 0f) ? normal : -normal;\r\n\t\t\t\t\tvar tan = faceVerts.Select(fv =\u003E fv - fc).FirstOrDefault(d =\u003E d.LengthSquared \u003E 1e-8f);\r\n\t\t\t\t\ttan = tan.Normal;\r\n\t\t\t\t\tvar bitan = Vector3.Cross(outward.Normal, tan);\r\n\r\n\t\t\t\t\tfaceVerts.Sort((p, q) =\u003E\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfloat ap = MathF.Atan2(Vector3.Dot(p - fc, bitan), Vector3.Dot(p - fc, tan));\r\n\t\t\t\t\t\tfloat aq = MathF.Atan2(Vector3.Dot(q - fc, bitan), Vector3.Dot(q - fc, tan));\r\n\t\t\t\t\t\treturn ap.CompareTo(aq);\r\n\t\t\t\t\t});\r\n\r\n\t\t\t\t\t// Fan triangulate the face.\r\n\t\t\t\t\tfor (int t = 1; t \u003C faceVerts.Count - 1; t\u002B\u002B)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar ta = faceVerts[0]; var tb = faceVerts[t]; var tc = faceVerts[t \u002B 1];\r\n\t\t\t\t\t\tresult.Add(ta); result.Add(tb); result.Add(tc);\r\n\t\t\t\t\t\taabbMin = Vector3.Min(aabbMin, Vector3.Min(ta, Vector3.Min(tb, tc)));\r\n\t\t\t\t\t\taabbMax = Vector3.Max(aabbMax, Vector3.Max(ta, Vector3.Max(tb, tc)));\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fills the 4 rows of the WorldToLocal matrix (row-major, for mul(M, float4(worldPos,1)) in HLSL).\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Csummary\u003E\r\n\t/// Matches WorldTransform.PointToLocal = Rotation.Inverse * (worldPt - Position) / Scale.\r\n\t/// In s\u0026box: Forward=(1,0,0)=localX, Left=-Right=(0,1,0)=localY, Up=(0,0,1)=localZ.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void GetWorldToLocalRows(out Vector4 r0, out Vector4 r1, out Vector4 r2, out Vector4 r3)\r\n\t{\r\n\t\tVector3 fwd = WorldRotation.Forward;        // world-space local X axis\r\n\t\tVector3 left = -WorldRotation.Right;          // world-space local Y axis  (Right = -Y in s\u0026box)\r\n\t\tVector3 up = WorldRotation.Up;             // world-space local Z axis\r\n\t\tVector3 pos = WorldPosition;\r\n\t\tVector3 scale = WorldScale;\r\n\r\n\t\tfloat isx = MathF.Abs(scale.x) \u003E 1e-6f ? 1f / scale.x : 0f;\r\n\t\tfloat isy = MathF.Abs(scale.y) \u003E 1e-6f ? 1f / scale.y : 0f;\r\n\t\tfloat isz = MathF.Abs(scale.z) \u003E 1e-6f ? 1f / scale.z : 0f;\r\n\r\n\t\tr0 = new Vector4(fwd.x * isx, fwd.y * isx, fwd.z * isx, -Vector3.Dot(fwd, pos) * isx);\r\n\t\tr1 = new Vector4(left.x * isy, left.y * isy, left.z * isy, -Vector3.Dot(left, pos) * isy);\r\n\t\tr2 = new Vector4(up.x * isz, up.y * isz, up.z * isz, -Vector3.Dot(up, pos) * isz);\r\n\t\tr3 = new Vector4(0f, 0f, 0f, 1f);\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (!Gizmo.IsSelected || LocalTriangles == null || LocalTriangles.Length == 0)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.Color = Color.Yellow.WithAlpha(0.5f);\r\n\t\tGizmo.Draw.LineBBox(LocalAABB);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterRippleEmitter.cs","FileName":"WaterRippleEmitter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Emits water ripples when this object crosses the water surface, and optionally\r\n/// while it moves across it. A generic, dependency-free alternative to the entry\r\n/// ripple built into \u003Csee cref=\u0022Buoyancy\u0022/\u003E \u2014 drop it on anything that doesn\u0027t have\r\n/// a Buoyancy component (players, NPCs, projectiles, debris...).\r\n///\r\n/// Velocity is derived from the object\u0027s own position delta, so it works with any\r\n/// movement system (CharacterController, custom controllers, animation, etc.) and\r\n/// needs no Rigidbody.\r\n/// \u003C/summary\u003E\r\n[Icon(\u0022water\u0022), Group(\u0022Water\u0022), Title(\u0022Water Ripple Emitter\u0022)]\r\npublic sealed class WaterRippleEmitter : Component\r\n{\r\n\t[Property, Group(\u0022Entry\u0022)] public bool EmitOnEntry { get; set; } = true;\r\n\t[Property, Group(\u0022Entry\u0022)] public float EntryStrength { get; set; } = 0.2f;\r\n\t// Ring spacing for the entry splash \u2014 smaller = tighter, more concentric rings.\r\n\t[Property, Group(\u0022Entry\u0022), Range(20.0f, 400.0f)] public float EntryWavelength { get; set; } = 120.0f;\r\n\t// Ring size for the entry splash \u2014 larger = a bigger, broader ripple.\r\n\t[Property, Group(\u0022Entry\u0022), Range(10.0f, 500.0f)] public float EntryRingWidth { get; set; } = 50.0f;\r\n\t// Minimum downward speed (units/s) needed to splash. Set to 0 to ripple on any crossing.\r\n\t[Property, Group(\u0022Entry\u0022)] public float MinImpactSpeed { get; set; } = 40.0f;\r\n\r\n\t[Property, Group(\u0022Wake\u0022)] public bool EmitWake { get; set; } = false;\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeStrength { get; set; } = 0.1f;\r\n\t// Ring spacing for wake ripples \u2014 smaller = tighter, more concentric rings.\r\n\t[Property, Group(\u0022Wake\u0022), Range(20.0f, 400.0f)] public float WakeWavelength { get; set; } = 120.0f;\r\n\t// Ring size for wake ripples \u2014 larger = a bigger, broader ripple.\r\n\t[Property, Group(\u0022Wake\u0022), Range(10.0f, 500.0f)] public float WakeRingWidth { get; set; } = 50.0f;\r\n\t// Minimum horizontal speed (units/s) before a moving object leaves a wake.\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeMinSpeed { get; set; } = 1.0f;\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeInterval { get; set; } = 0.0333f; // 30 fps\r\n\r\n\t// Local-space offset of the point tested against the surface (e.g. the feet).\r\n\t[Property, Group(\u0022General\u0022)] public Vector3 SampleOffset { get; set; } = Vector3.Zero;\r\n\r\n\tprivate bool m_Initialized;\r\n\tprivate bool m_WasBelowSurface;\r\n\tprivate Vector3 m_LastPosition;\r\n\tprivate float m_WakeTimer;\r\n\r\n\tprivate Vector3 SamplePosition =\u003E WorldPosition \u002B WorldRotation * SampleOffset;\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tm_LastPosition = SamplePosition;\r\n\t\tm_WasBelowSurface = false;\r\n\t\tm_Initialized = false;\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// If this gameobject is parented to anything, we don\u0027t want to play water ripple effects\r\n\t\t// (e.g. A player inside a boat)\r\n\t\tif (GameObject.Parent != Scene)\r\n\t\t\treturn;\r\n\r\n\t\tVector3 samplePos = SamplePosition;\r\n\r\n\t\t// Velocity from position delta \u2014 no Rigidbody required\r\n\t\tVector3 velocity = Time.Delta \u003E 0.0f ? (samplePos - m_LastPosition) / Time.Delta : Vector3.Zero;\r\n\t\tm_LastPosition = samplePos;\r\n\r\n\t\tfloat waterHeight = WaterManager.GetWaterHeightAt(samplePos);\r\n\r\n\t\t// Not over any water surface\r\n\t\tif (waterHeight \u003C= float.MinValue)\r\n\t\t{\r\n\t\t\tm_WasBelowSurface = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool belowSurface = samplePos.z \u003C= waterHeight;\r\n\r\n\t\t// Skip the first valid frame so an object spawned already in water doesn\u0027t splash\r\n\t\tif (!m_Initialized)\r\n\t\t{\r\n\t\t\tm_WasBelowSurface = belowSurface;\r\n\t\t\tm_Initialized = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Entry splash on the above -\u003E below surface crossing\r\n\t\tif (EmitOnEntry \u0026\u0026 belowSurface \u0026\u0026 !m_WasBelowSurface)\r\n\t\t{\r\n\t\t\tfloat impactSpeed = float.Max(0.0f, -velocity.z);\r\n\r\n\t\t\tif (impactSpeed \u003E= MinImpactSpeed)\r\n\t\t\t{\r\n\t\t\t\tfloat strength = (impactSpeed / 150.0f).Clamp(0.3f, 2.5f) * EntryStrength;\r\n\r\n\t\t\t\tWaterManager.AddRipple(samplePos.WithZ(waterHeight), strength, EntryWavelength, EntryRingWidth);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tm_WasBelowSurface = belowSurface;\r\n\r\n\t\tfloat horizontalSpeed = velocity.WithZ(0.0f).Length;\r\n\r\n\t\t// Continuous wake while skimming/swimming through the surface\r\n\t\tif (EmitWake \u0026\u0026 belowSurface)\r\n\t\t{\r\n\t\t\tif (horizontalSpeed \u003E= WakeMinSpeed)\r\n\t\t\t{\r\n\t\t\t\tm_WakeTimer -= Time.Delta;\r\n\r\n\t\t\t\tif (m_WakeTimer \u003C= 0.0f)\r\n\t\t\t\t{\r\n\t\t\t\t\tWaterManager.AddRipple(samplePos.WithZ(waterHeight), WakeStrength, WakeWavelength, WakeRingWidth);\r\n\t\t\t\t\tm_WakeTimer = WakeInterval;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Editor/WaterFlowTool.cs","FileName":"WaterFlowTool.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.WaterTool.Editor;\r\n\r\n/// \u003Csummary\u003E\r\n/// Scene editor tool for the WaterFlow component. Activates when a WaterFlow is\r\n/// selected and hosts the spline editor: select points, drag them and their In/Out\r\n/// tangent handles (for curved rivers), click on the river to insert a point, and\r\n/// shift-drag a point to extrude a new one. All edits are undo-aware and rebuild\r\n/// the river mesh live.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Water Flow\u0022)]\r\n[Icon(\u0022waves\u0022)]\r\n[Alias(\u0022water_flow\u0022)]\r\n[Group(\u00221\u0022)]\r\n[Order(1)]\r\npublic class WaterFlowTool : EditorTool\u003CWaterFlow\u003E\r\n{\r\n\tprivate WaterFlowWindow m_Window;\r\n\tprivate WaterFlow m_Selected;\r\n\r\n\r\n\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\t\tm_Window = new WaterFlowWindow();\r\n\r\n\t\tAddOverlay(m_Window, TextFlag.RightBottom, 10);\r\n\r\n\t\tOnSelectionChanged();\r\n\t}\r\n\r\n\r\n\r\n\tpublic override void OnDisabled()\r\n\t{\r\n\t\tm_Window?.OnDisabled();\r\n\t}\r\n\r\n\r\n\r\n\tpublic override void OnUpdate()\r\n\t{\r\n\t\tm_Window?.OnUpdate();\r\n\t}\r\n\r\n\r\n\r\n\tpublic override void OnSelectionChanged()\r\n\t{\r\n\t\tWaterFlow target = GetSelectedComponent\u003CWaterFlow\u003E();\r\n\r\n\t\tif (!target.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\t// Only re-target when the component itself changes \u2014 otherwise this fires on\r\n\t\t// every property edit and would reset the selected point each time.\r\n\t\tif (target != m_Selected)\r\n\t\t{\r\n\t\t\tm_Window?.OnSelectionChanged(target);\r\n\r\n\t\t\tm_Selected = target;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Editor/WaterFlowWindow.UI.cs","FileName":"WaterFlowWindow.UI.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\n\r\nnamespace RedSnail.WaterTool.Editor;\r\n\r\npublic partial class WaterFlowWindow\r\n{\r\n\tprivate const int HEADER_HEIGHT = 32;\r\n\r\n\r\n\r\n\tprivate void Rebuild()\r\n\t{\r\n\t\tLayout.Clear(true);\r\n\t\tLayout.Margin = 0;\r\n\r\n\t\tIcon = _isClosed ? \u0022\u0022 : \u0022waves\u0022;\r\n\t\tUpdateWindowTitle();\r\n\t\tIsGrabbable = !_isClosed;\r\n\r\n\t\tif (_isClosed)\r\n\t\t{\r\n\t\t\tBuildClosedState();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tMinimumWidth = 360;\r\n\t\tBuildHeader();\r\n\r\n\t\tif (_targetComponent.IsValid())\r\n\t\t\tBuildControlSheet();\r\n\r\n\t\tLayout.Margin = 4;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildClosedState()\r\n\t{\r\n\t\tvar closedRow = Layout.AddRow();\r\n\r\n\t\tclosedRow.Add(new IconButton(\u0022waves\u0022, () =\u003E { _isClosed = false; Rebuild(); })\r\n\t\t{\r\n\t\t\tToolTip = \u0022Open Water Flow Spline Editor\u0022,\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\tMinimumWidth = 0;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildHeader()\r\n\t{\r\n\t\tvar headerRow = Layout.AddRow();\r\n\r\n\t\theaderRow.AddStretchCell();\r\n\r\n\t\theaderRow.Add(new IconButton(\u0022info\u0022)\r\n\t\t{\r\n\t\t\tToolTip = GetInfoTooltip(),\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\r\n\t\theaderRow.Add(new IconButton(\u0022close\u0022, CloseWindow)\r\n\t\t{\r\n\t\t\tToolTip = \u0022Close Editor\u0022,\r\n\t\t\tFixedHeight = HEADER_HEIGHT,\r\n\t\t\tFixedWidth = HEADER_HEIGHT,\r\n\t\t\tBackground = Color.Transparent\r\n\t\t});\r\n\t}\r\n\r\n\r\n\r\n\tprivate string GetInfoTooltip()\r\n\t{\r\n\t\treturn \u0022Edit the river\u0027s spline.\\n\\n\u0022 \u002B\r\n\t\t\t   \u0022\u2022 Click a point to select it, then drag it or its In/Out tangent handles.\\n\u0022 \u002B\r\n\t\t\t   \u0022\u2022 Tangent Mode controls the curve: Auto smooths, Linear makes sharp corners,\\n\u0022 \u002B\r\n\t\t\t   \u0022  Mirrored/Split let you shape the bend by hand.\\n\u0022 \u002B\r\n\t\t\t   \u0022\u2022 Click anywhere on the river to insert a point there.\\n\u0022 \u002B\r\n\t\t\t   \u0022\u2022 Hold Shift while dragging a point to drag out a new one.\\n\\n\u0022 \u002B\r\n\t\t\t   \u0022The source point is green, the mouth is red.\u0022;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void BuildControlSheet()\r\n\t{\r\n\t\tvar serialized = this.GetSerialized();\r\n\t\tvar controlSheet = new ControlSheet();\r\n\r\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointTangentMode)));\r\n\t\t_positionControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointPosition)));\r\n\t\t_inTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointIn)));\r\n\t\t_outTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointOut)));\r\n\r\n\t\tcontrolSheet.AddLayout(BuildControlButtons());\r\n\r\n\t\tLayout.Add(controlSheet);\r\n\r\n\t\tToggleTangentInput();\r\n\t}\r\n\r\n\r\n\r\n\tprivate Layout BuildControlButtons()\r\n\t{\r\n\t\tvar row = Layout.Row();\r\n\t\trow.Spacing = 16;\r\n\t\trow.Margin = 8;\r\n\r\n\t\trow.Add(CreateNavigationButton(\u0022skip_previous\u0022, -1, \u0022Go to previous point\u0022));\r\n\t\trow.Add(CreateNavigationButton(\u0022skip_next\u0022, 1, \u0022Go to next point\u0022));\r\n\t\trow.Add(CreateDeleteButton());\r\n\t\trow.Add(CreateAddButton());\r\n\r\n\t\treturn row;\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateNavigationButton(string _Icon, int _Direction, string _Tooltip)\r\n\t{\r\n\t\treturn new IconButton(_Icon, () =\u003E\r\n\t\t{\r\n\t\t\tif (_Direction \u003C 0)\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\telse\r\n\t\t\t\tSelectedPointIndex = int.Min(_targetComponent.Spline.PointCount - 1, SelectedPointIndex \u002B 1);\r\n\r\n\t\t\tSelectPoint(SelectedPointIndex);\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = _Tooltip };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateDeleteButton()\r\n\t{\r\n\t\treturn new IconButton(\u0022delete\u0022, () =\u003E\r\n\t\t{\r\n\t\t\t// The source point can\u0027t be deleted, and rivers need at least two points\r\n\t\t\tif (IsSourcePointSelected || _targetComponent.Spline.PointCount \u003C= 2)\r\n\t\t\t\treturn;\r\n\r\n\t\t\tusing (CreateUndoScope(\u0022Delete Water Flow Point\u0022))\r\n\t\t\t{\r\n\t\t\t\t_targetComponent.Spline.RemovePoint(SelectedPointIndex);\r\n\t\t\t\tSelectedPointIndex = int.Max(0, SelectedPointIndex - 1);\r\n\t\t\t}\r\n\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{ ToolTip = \u0022Delete the selected point (the source point is locked; minimum 2 points)\u0022 };\r\n\t}\r\n\r\n\r\n\r\n\tprivate IconButton CreateAddButton()\r\n\t{\r\n\t\treturn new IconButton(\u0022add\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tusing (CreateUndoScope(\u0022Add Water Flow Point\u0022))\r\n\t\t\t{\r\n\t\t\t\tInsertNewPoint();\r\n\t\t\t\tSelectedPointIndex\u002B\u002B;\r\n\t\t\t}\r\n\r\n\t\t\tUpdateWindowTitle();\r\n\t\t\tFocus();\r\n\t\t})\r\n\t\t{\r\n\t\t\tToolTip = \u0022Insert a point after the selected one.\\n\u0022 \u002B\r\n\t\t\t\t\t  \u0022You can also click on the river, or Shift-drag a point.\u0022\r\n\t\t};\r\n\t}\r\n\r\n\r\n\r\n\tprivate void InsertNewPoint()\r\n\t{\r\n\t\tvar spline = _targetComponent.Spline;\r\n\r\n\t\tif (SelectedPointIndex == spline.PointCount - 1)\r\n\t\t{\r\n\t\t\t// Extend past the mouth, following the spline tangent\r\n\t\t\tfloat distance = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tVector3 tangent = spline.SampleAtDistance(distance).Tangent;\r\n\t\t\tVector3 newPosition = _selectedPoint.Position \u002B tangent * 256.0f;\r\n\r\n\t\t\tspline.InsertPoint(SelectedPointIndex \u002B 1, _selectedPoint with { Position = newPosition });\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// Split the segment toward the next point\r\n\t\t\tfloat currentDist = spline.GetDistanceAtPoint(SelectedPointIndex);\r\n\t\t\tfloat nextDist = spline.GetDistanceAtPoint(SelectedPointIndex \u002B 1);\r\n\r\n\t\t\tspline.AddPointAtDistance((currentDist \u002B nextDist) / 2.0f, true);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateWindowTitle()\r\n\t{\r\n\t\tWindowTitle = _isClosed\r\n\t\t\t? \u0022\u0022\r\n\t\t\t: $\u0022Water Flow \u2014 Point [{SelectedPointIndex}] \u2014 {_targetComponent?.GameObject?.Name ?? \u0022\u0022}\u0022;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CloseWindow()\r\n\t{\r\n\t\t_isClosed = true;\r\n\t\tRebuild();\r\n\t\tPosition = Parent.Size - 32;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Miscellaneous/BoatController.cs","FileName":"BoatController.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\nusing Sandbox.Movement;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Minimal demo boat controller.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Demo Boat Controller\u0022), Group(\u0022Water\u0022), Icon(\u0022directions_boat\u0022)]\r\npublic sealed class BoatController : Component, Component.IPressable, ISitTarget\r\n{\r\n\tprivate TimeSince m_TimeSinceLastUnderWave;\r\n\tprivate float m_LastHitTimer = 1.0f;\r\n\r\n\t[Property, Group(\u0022Seat\u0022)] public GameObject SeatPosition { get; set; }\r\n\t[Property, Group(\u0022Seat\u0022)] public GameObject EyePosition { get; set; }\r\n\t[Property, Group(\u0022Seat\u0022)] public GameObject ExitPoint { get; set; }\r\n\r\n\t[Property, Group(\u0022Movement\u0022)] public float ThrustForce { get; set; } = 200_000f;\r\n\t[Property, Group(\u0022Movement\u0022)] public float ReverseForce { get; set; } = 80_000f;\r\n\t[Property, Group(\u0022Movement\u0022)] public float TurnForce { get; set; } = 60_000f;\r\n\t[Property, Group(\u0022Movement\u0022)] public float Stability { get; set; } = 50_000f;\r\n\t[Property, Group(\u0022Movement\u0022)] public float TerminalSpeed { get; set; } = 800f;\r\n\r\n\t[Property, Group(\u0022Interaction\u0022)] public string TooltipTitle { get; set; } = \u0022Drive\u0022;\r\n\t[Property, Group(\u0022Interaction\u0022)] public string TooltipIcon { get; set; } = \u0022directions_boat\u0022;\r\n\r\n\t[Property, Group(\u0022Sounds\u0022)] public SoundEvent BoatUnderWaves { get; set; }\r\n\t[Property, Group(\u0022Sounds\u0022)] public SoundPointComponent BoatOnWaterLoop { get; set; }\r\n\r\n\tprivate Rigidbody m_Rigidbody;\r\n\tprivate Buoyancy m_Buoyancy;\r\n\r\n\tprivate float m_TargetThrust;\r\n\tprivate float m_TargetTurn;\r\n\r\n\tpublic bool IsOccupied =\u003E GetComponentInChildren\u003CPlayerController\u003E(false) != null;\r\n\r\n\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tm_Rigidbody = GetComponent\u003CRigidbody\u003E();\r\n\t\tm_Buoyancy = GetComponent\u003CBuoyancy\u003E();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif (!m_Rigidbody.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tHandleSounds();\r\n\t\tStabilize();\r\n\r\n\t\tif (IsOccupied)\r\n\t\t\tHandleMovement();\r\n\t\telse\r\n\t\t{\r\n\t\t\t// Smoothly reset forces when unmanned\r\n\t\t\tm_TargetThrust = 0f;\r\n\t\t\tm_TargetTurn = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tpublic bool CanPress(IPressable.Event e)\r\n\t{\r\n\t\treturn e.Source is PlayerController \u0026\u0026 !IsOccupied;\r\n\t}\r\n\r\n\tpublic bool Press(IPressable.Event e)\r\n\t{\r\n\t\tif (e.Source is not PlayerController player) return false;\r\n\t\tif (IsOccupied) return false;\r\n\r\n\t\tMountPlayer(player);\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic IPressable.Tooltip? GetTooltip(IPressable.Event e)\r\n\t{\r\n\t\tif (IsOccupied) return null;\r\n\r\n\t\tvar tooltip = new IPressable.Tooltip\r\n\t\t{\r\n\t\t\tTitle = TooltipTitle,\r\n\t\t\tIcon = TooltipIcon\r\n\t\t};\r\n\r\n\t\treturn tooltip;\r\n\t}\r\n\r\n\r\n\r\n\tpublic void AskToLeave(PlayerController player)\r\n\t{\r\n\t\tDismountPlayer(player);\r\n\t}\r\n\r\n\tpublic void UpdatePlayerAnimator(PlayerController controller, SkinnedModelRenderer renderer)\r\n\t{\r\n\t\tcontroller.LocalTransform = global::Transform.Zero;\r\n\t\trenderer.LocalRotation = Rotation.Identity;\r\n\t\trenderer.Set(\u0022sit\u0022, (int)BaseChair.AnimatorSitPose.ChairForward);\r\n\t\trenderer.Set(\u0022b_grounded\u0022, true);\r\n\t\trenderer.Set(\u0022b_climbing\u0022, false);\r\n\t\trenderer.Set(\u0022b_swim\u0022, false);\r\n\t\trenderer.Set(\u0022duck\u0022, false);\r\n\t}\r\n\r\n\tpublic Transform CalculateEyeTransform(PlayerController controller)\r\n\t{\r\n\t\tvar anchor = EyePosition ?? SeatPosition ?? GameObject;\r\n\r\n\t\t// Position follows the seat anchor so the camera rides with the boat.\r\n\t\t// Rotation uses the player\u0027s eye angles in pure world space, the boat\u0027s\r\n\t\t// pitch and roll are intentionally NOT applied so the view stays level\r\n\t\t// even when the hull bobs or banks.\r\n\t\treturn new Transform\r\n\t\t{\r\n\t\t\tPosition = anchor.WorldPosition,\r\n\t\t\tRotation = controller.EyeAngles.ToRotation()\r\n\t\t};\r\n\t}\r\n\r\n\r\n\r\n\tprivate void MountPlayer(PlayerController player)\r\n\t{\r\n\t\tvar seat = SeatPosition ?? GameObject;\r\n\r\n\t\t// Disable the player\u0027s own physics so they don\u0027t fight the boat\r\n\t\tif (player.Body.IsValid()) player.Body.Enabled = false;\r\n\t\tif (player.ColliderObject.IsValid()) player.ColliderObject.Enabled = false;\r\n\r\n\t\tplayer.GameObject.SetParent(seat, false);\r\n\t\tplayer.GameObject.LocalTransform = global::Transform.Zero;\r\n\t}\r\n\r\n\tprivate void DismountPlayer(PlayerController player)\r\n\t{\r\n\t\tplayer.GameObject.SetParent(null, true);\r\n\r\n\t\tif (player.Body.IsValid()) player.Body.Enabled = true;\r\n\t\tif (player.ColliderObject.IsValid()) player.ColliderObject.Enabled = true;\r\n\r\n\t\t// Move to exit point, or eject to the side if none is set\r\n\t\tplayer.WorldPosition = ExitPoint != null\r\n\t\t\t? ExitPoint.WorldPosition\r\n\t\t\t: WorldPosition \u002B WorldRotation.Right * 100f \u002B Vector3.Up * 30f;\r\n\r\n\t\tm_TargetThrust = 0f;\r\n\t\tm_TargetTurn = 0f;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleMovement()\r\n\t{\r\n\t\t// Only push when the hull is actually in the water\r\n\t\tif (m_Buoyancy is { IsTouchingWater: false })\r\n\t\t\treturn;\r\n\r\n\t\tfloat fwd = Input.AnalogMove.x; // W = \u002B1  S = -1\r\n\t\tfloat side = Input.AnalogMove.y; // D = \u002B1  A = -1\r\n\r\n\t\t// Thrust\r\n\t\tfloat wantedThrust = fwd \u003E 0.02f ? ThrustForce * fwd\r\n\t\t\t\t\t\t   : fwd \u003C -0.02f ? ReverseForce * fwd\r\n\t\t\t\t\t\t   : 0f;\r\n\r\n\t\tm_TargetThrust = float.Lerp(m_TargetThrust, wantedThrust, Time.Delta * 3f);\r\n\r\n\t\tfloat speed = m_Rigidbody.Velocity.WithZ(0).Length;\r\n\t\tfloat limiter = MathF.Min(1f, TerminalSpeed / (speed \u002B 0.001f));\r\n\r\n\t\tm_Rigidbody.ApplyForce(WorldRotation.Right * m_TargetThrust * limiter);\r\n\r\n\t\t// Turning\r\n\t\tfloat speedFactor = float.Clamp(speed / 200f, 0.2f, 1f);\r\n\t\tfloat wantedTurn = side * TurnForce * speedFactor;\r\n\t\tm_TargetTurn = float.Lerp(m_TargetTurn, wantedTurn, Time.Delta * 5f);\r\n\r\n\t\tVector3 bow = WorldPosition \u002B WorldRotation.Forward * 60f;\r\n\t\tm_Rigidbody.ApplyForceAt(bow, WorldRotation.Left * m_TargetTurn);\r\n\r\n\t\t// Speed dependent damping so the boat decelerates naturally\r\n\t\tfloat damping = (TerminalSpeed / (speed \u002B 0.001f)) * 0.5f;\r\n\t\tm_Rigidbody.LinearDamping = float.Clamp(damping, 0.5f, 5f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleSounds()\r\n\t{\r\n\t\tif (Scene.Camera is not CameraComponent camera)\r\n\t\t\treturn;\r\n\r\n\t\tHandleWavesSound(camera);\r\n\t\tHandleMovementSound(camera);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleWavesSound(CameraComponent _Camera)\r\n\t{\r\n\t\tif (!BoatUnderWaves.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tfloat distance = _Camera.WorldPosition.DistanceSquared(WorldPosition);\r\n\t\tfloat MaxDistanceSq = BoatUnderWaves.Distance * BoatUnderWaves.Distance;\r\n\r\n\t\tfloat speed = m_Rigidbody.Velocity.WithZ(0).Length;\r\n\r\n\t\tif (speed \u003C 10.0f \u0026\u0026 distance \u003C MaxDistanceSq \u0026\u0026 m_Buoyancy.IsTouchingWater \u0026\u0026 m_TimeSinceLastUnderWave \u003E m_LastHitTimer)\r\n\t\t{\r\n\t\t\tSound.Play(BoatUnderWaves, WorldPosition);\r\n\r\n\t\t\tm_TimeSinceLastUnderWave = 0;\r\n\t\t\tm_LastHitTimer = Game.Random.Float(2.0f, 10.0f);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void HandleMovementSound(CameraComponent _Camera)\r\n\t{\r\n\t\tif (!BoatOnWaterLoop.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tfloat distance = _Camera.WorldPosition.DistanceSquared(WorldPosition);\r\n\t\tfloat MaxDistanceSq = BoatOnWaterLoop.Distance * BoatOnWaterLoop.Distance;\r\n\r\n\t\tif (distance \u003E MaxDistanceSq)\r\n\t\t{\r\n\t\t\t// Disable the sound point if too far away from the camera (Avoid wasting resources)\r\n\t\t\tBoatOnWaterLoop.Enabled = false;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tBoatOnWaterLoop.SoundOverride = true;\r\n\t\t\tBoatOnWaterLoop.Volume = m_Rigidbody.Velocity.WithZ(0).Length.Remap(0.0f, 200.0f);\r\n\t\t\tBoatOnWaterLoop.Enabled = true;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void Stabilize()\r\n\t{\r\n\t\tVector3 torque = Vector3.Cross(WorldRotation.Up, Vector3.Up) * Stability;\r\n\t\tm_Rigidbody.ApplyTorque(torque);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterBody.cs","FileName":"WaterBody.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\nusing Sandbox.Volumes;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Defines a discrete body of water that participates in a renderer-driven water system.\r\n/// Provides volume bounds, a physics hull for buoyancy/swimming, and renderer inclusion in one component.\r\n/// Requires a WaterQuadRenderer present in the scene to produce a visible water surface.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Water Body\u0022)]\r\n[Category(\u0022Water\u0022)]\r\n[Icon(\u0022water_drop\u0022)]\r\npublic sealed class WaterBody : VolumeComponent, Component.ExecuteInEditor\r\n{\r\n\tprivate HullCollider m_HullCollider;\r\n\tprivate BBox m_LastLocalBounds;\r\n\r\n\t[Property, Group(\u0022General\u0022)] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterBodiesList();\r\n\r\n\t\tUpdateColliderState();\r\n\r\n\t\tm_LastLocalBounds = SceneVolume.GetBounds();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterBodiesList();\r\n\r\n\t\tm_HullCollider?.Destroy();\r\n\t\tm_HullCollider = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tBBox localBounds = SceneVolume.GetBounds();\r\n\r\n\t\tif (localBounds != m_LastLocalBounds)\r\n\t\t{\r\n\t\t\tUpdateColliderState();\r\n\r\n\t\t\tm_LastLocalBounds = localBounds;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (!Gizmo.IsSelected || !m_HullCollider.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.Color = Color.Cyan;\r\n\t\tGizmo.Draw.LineBBox(m_HullCollider.LocalBounds);\r\n\t}\r\n\r\n\t// Bounds\r\n\tpublic void SetBounds(BBox bounds)\r\n\t{\r\n\t\tSceneVolume = SceneVolume with { Box = bounds };\r\n\t}\r\n\r\n\tpublic float GetSurfaceHeight()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\r\n\t\treturn WorldTransform.PointToWorld(new Vector3(local.Center.x, local.Center.y, local.Maxs.z)).z;\r\n\t}\r\n\r\n\tpublic float GetBottomHeight()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\r\n\t\treturn WorldTransform.PointToWorld(new Vector3(local.Center.x, local.Center.y, local.Mins.z)).z;\r\n\t}\r\n\r\n\tpublic bool ContainsPointXY(Vector3 worldPosition)\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\t\tVector3 point = WorldTransform.PointToLocal(worldPosition);\r\n\t\tVector3 half = local.Size * 0.5f;\r\n\r\n\t\treturn MathF.Abs(point.x - local.Center.x) \u003C= half.x \u0026\u0026 MathF.Abs(point.y - local.Center.y) \u003C= half.y;\r\n\t}\r\n\r\n\tpublic bool ContainsPointInVolume(Vector3 worldPosition)\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\r\n\t\tVector3 point = WorldTransform.PointToLocal(worldPosition);\r\n\t\tVector3 half = local.Size * 0.5f;\r\n\r\n\t\treturn MathF.Abs(point.x - local.Center.x) \u003C= half.x \u0026\u0026\r\n\t\t\t   MathF.Abs(point.y - local.Center.y) \u003C= half.y \u0026\u0026\r\n\t\t\t   MathF.Abs(point.z - local.Center.z) \u003C= half.z;\r\n\t}\r\n\r\n\tpublic (Vector3 Center, Vector3 Forward, Vector3 Up, Vector3 HalfExtents) GetWorldOBB()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\r\n\t\treturn (WorldTransform.PointToWorld(local.Center), WorldRotation.Forward, WorldTransform.Up, local.Size * 0.5f);\r\n\t}\r\n\r\n\t// Wave queries\r\n\tpublic Vector3 GetWaveDisplacementAt(Vector3 _WorldPosition)\r\n\t{\r\n\t\tWaterDefinition profile = WaterManager.GetWaveProfile(WaterType);\r\n\r\n\t\treturn profile.IsValid() ? WaterWaveUtility.ComputeDisplacementAt(_WorldPosition, profile) : Vector3.Zero;\r\n\t}\r\n\r\n\tpublic Vector3 GetWaveVelocityAt(Vector3 _WorldPosition)\r\n\t{\r\n\t\tWaterDefinition profile = WaterManager.GetWaveProfile(WaterType);\r\n\r\n\t\treturn profile.IsValid() ? WaterWaveUtility.ComputeVelocityAt(_WorldPosition, profile) : Vector3.Zero;\r\n\t}\r\n\r\n\tpublic float GetWaveHeightAt(Vector3 _WorldPosition) =\u003E GetSurfaceHeight() \u002B GetWaveDisplacementAt(_WorldPosition).z;\r\n\r\n\tinternal float GetVerticalDistanceToSurface(Vector3 _WorldPosition) =\u003E MathF.Abs(_WorldPosition.z - GetSurfaceHeight());\r\n\r\n\tprivate void UpdateColliderState()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\r\n\t\tm_HullCollider = GetOrAddComponent\u003CHullCollider\u003E();\r\n\t\tm_HullCollider.Flags |= ComponentFlags.Hidden;\r\n\t\tm_HullCollider.Static = true;\r\n\t\tm_HullCollider.Type = HullCollider.PrimitiveType.Box;\r\n\t\tm_HullCollider.Center = local.Center;\r\n\t\tm_HullCollider.BoxSize = local.Size;\r\n\t\tm_HullCollider.IsTrigger = true;\r\n\r\n\t\tTags.Add(\u0022water\u0022);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"PostProcessing/SimpleFog.cs","FileName":"SimpleFog.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.Rendering;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n[Title(\u0022Simple Fog\u0022)]\r\n[Category(\u0022Post Processing\u0022)]\r\n[Icon(\u0022foggy\u0022)]\r\npublic sealed class SimpleFog : BasePostProcess\u003CSimpleFog\u003E\r\n{\r\n\t[Property] private Color Color { get; set; } = Color.White;\r\n\t[Property, Range(0, 1)] private float Intensity { get; set; } = 0.01f;\r\n\t[Property, Range(0, 1)] private float Opacity { get; set; } = 0.5f;\r\n\r\n\r\n\r\n\tpublic override void Render()\r\n\t{\r\n\t\tfloat opacity = GetWeighted(x =\u003E x.Opacity);\r\n\r\n\t\tif (opacity.AlmostEqual(0.0f))\r\n\t\t\treturn;\r\n\r\n\t\tAttributes.Set(\u0022Color\u0022, GetWeighted(x =\u003E x.Color));\r\n\t\tAttributes.Set(\u0022Intensity\u0022, GetWeighted(x =\u003E x.Intensity));\r\n\t\tAttributes.Set(\u0022Opacity\u0022, opacity);\r\n\r\n\t\tMaterial shader = Material.FromShader(\u0022pp_simplefog\u0022);\r\n\t\tBlitMode blit = BlitMode.WithBackbuffer(shader, Stage.BeforePostProcess, 60);\r\n\t\tBlit(blit, \u0022Simple Fog\u0022);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterDefinition.cs","FileName":"WaterDefinition.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n[AssetType(Name = \u0022Water Definition\u0022, Extension = \u0022wtdef\u0022, Category = \u0022Water\u0022)]\r\npublic sealed class WaterDefinition : GameResource\r\n{\r\n\t[Property, Group(\u0022Detail\u0022)] public float WavesIntensity { get; set; } = 4.0f;\r\n\t[Property, Group(\u0022Detail\u0022), Range(0, 5)] public float WavesSpeed { get; set; } = 0.3f;\r\n\t[Property, Group(\u0022Detail\u0022)] public float WavesScale { get; set; } = 0.05f;\r\n\t[Property, Group(\u0022Detail\u0022)] public Vector2 WavesDirection { get; set; } = new Vector2(1, 0.5f);\r\n\t[Property, Group(\u0022Detail\u0022), Range(1, 5)] public int WavesOctaves { get; set; } = 3;\r\n\t[Property, Group(\u0022Detail\u0022)] public float WavesLacunarity { get; set; } = 2.0f;\r\n\t[Property, Group(\u0022Detail\u0022), Range(0, 1)] public float WavesPersistence { get; set; } = 0.5f;\r\n\t[Property, Group(\u0022Detail\u0022), Range(0, 1)] public float WavesSteepness { get; set; } = 0.5f;\r\n\r\n\t[Property, Group(\u0022Swell\u0022)] public float SwellIntensity { get; set; } = 15.0f;\r\n\t[Property, Group(\u0022Swell\u0022), Range(0, 500)] public float SwellSpeed { get; set; } = 100.0f;\r\n\t[Property, Group(\u0022Swell\u0022)] public float SwellScale { get; set; } = 0.002f;\r\n\t[Property, Group(\u0022Swell\u0022)] public Vector2 SwellDirection { get; set; } = new Vector2(0.7f, 0.3f);\r\n\t[Property, Group(\u0022Swell\u0022), Range(1, 4)] public int SwellOctaves { get; set; } = 2;\r\n\t[Property, Group(\u0022Swell\u0022)] public float SwellLacunarity { get; set; } = 1.8f;\r\n\t[Property, Group(\u0022Swell\u0022), Range(0, 1)] public float SwellPersistence { get; set; } = 0.6f;\r\n\t[Property, Group(\u0022Swell\u0022), Range(0, 1)] public float SwellSteepness { get; set; } = 0.3f;\r\n\r\n\tpublic void ApplyTo(RenderAttributes attributes)\r\n\t{\r\n\t\tattributes.Set(\u0022WavesIntensity\u0022, WavesIntensity);\r\n\t\tattributes.Set(\u0022WavesSpeed\u0022, WavesSpeed);\r\n\t\tattributes.Set(\u0022WavesScale\u0022, WavesScale);\r\n\t\tattributes.Set(\u0022WavesDirection\u0022, WavesDirection);\r\n\t\tattributes.Set(\u0022WavesOctaves\u0022, WavesOctaves);\r\n\t\tattributes.Set(\u0022WavesLacunarity\u0022, WavesLacunarity);\r\n\t\tattributes.Set(\u0022WavesPersistence\u0022, WavesPersistence);\r\n\t\tattributes.Set(\u0022WavesSteepness\u0022, WavesSteepness);\r\n\r\n\t\tattributes.Set(\u0022SwellIntensity\u0022, SwellIntensity);\r\n\t\tattributes.Set(\u0022SwellSpeed\u0022, SwellSpeed);\r\n\t\tattributes.Set(\u0022SwellScale\u0022, SwellScale);\r\n\t\tattributes.Set(\u0022SwellDirection\u0022, SwellDirection);\r\n\t\tattributes.Set(\u0022SwellOctaves\u0022, SwellOctaves);\r\n\t\tattributes.Set(\u0022SwellLacunarity\u0022, SwellLacunarity);\r\n\t\tattributes.Set(\u0022SwellPersistence\u0022, SwellPersistence);\r\n\t\tattributes.Set(\u0022SwellSteepness\u0022, SwellSteepness);\r\n\t}\r\n\r\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\r\n\t{\r\n\t\treturn CreateSimpleAssetTypeIcon(\u0022water\u0022, _Width, _Height, \u0022#4287f5\u0022, \u0022white\u0022);\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterExclusionVolume.cs","FileName":"WaterExclusionVolume.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.Volumes;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Suppresses water surface rendering inside a volume. Has no effect on the physical water hull\r\n/// so buoyancy and swimming still work within the excluded area.\r\n/// Intended for enclosed spaces that sit in water, such as the interior of a boat or submarine.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Water Exclusion Volume\u0022)]\r\n[Category(\u0022Volumes\u0022)]\r\n[Icon(\u0022water\u0022)]\r\npublic sealed class WaterExclusionVolume : VolumeComponent, Component.ExecuteInEditor\r\n{\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterExclusionVolumesList();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterExclusionVolumesList();\r\n\t}\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tbase.DrawGizmos();\r\n\r\n\t\t/*\r\n\t\tSceneVolume sceneVolume = SceneVolume;\r\n\t\tGizmo.Draw.IgnoreDepth = false;\r\n\t\tGizmo.Draw.Color = Gizmo.Colors.Blue.WithAlpha(0.8f);\r\n\t\tGizmo.Draw.SolidBox(sceneVolume.Box);\r\n\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\tGizmo.Draw.Color = global::Color.White.WithAlpha(0.05f);\r\n\t\tGizmo.Draw.SolidBox(sceneVolume.Box);\r\n\t\t\r\n\t\tSceneVolume = sceneVolume;\r\n\t\t*/\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// DebugOverlay.Box(GetWorldBounds(), Color.Cyan, overlay: true);\r\n\t}\r\n\r\n\tpublic (Vector3 Center, Vector3 Forward, Vector3 Up, Vector3 HalfExtents) GetWorldOBB()\r\n\t{\r\n\t\tBBox local = SceneVolume.GetBounds();\r\n\t\tVector3 center = WorldTransform.PointToWorld(local.Center);\r\n\t\tVector3 halfExtents = local.Size * 0.5f;\r\n\r\n\t\treturn (center, WorldRotation.Forward, WorldTransform.Up, halfExtents);\r\n\t}\r\n\r\n\tpublic void SetLocalBounds(BBox localBounds)\r\n\t{\r\n\t\tvar sv = SceneVolume;\r\n\t\tsv.Box = localBounds;\r\n\t\tSceneVolume = sv;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterManager.Ripples.cs","FileName":"WaterManager.Ripples.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\npublic partial class WaterManager\r\n{\r\n\t// Interactive ripples \u2014 expanding radial wave packets stamped onto the surface\r\n\t// when something enters or moves on the water. Each emitter is uploaded as two\r\n\t// float4 rows: row0 = (Center.xy, StartTime, Strength), row1 = (Wavelength, Width, _, _).\r\n\t// Amplitude/Speed/Damping are global; Strength, Wavelength and Width are per-ripple.\r\n\t// The exact same formula runs in advancedwater.shader (VS) and in ComputeRippleHeight\r\n\t// (CPU) so buoyancy bobs over the visual ripples.\r\n\r\n\tprivate const int MAX_RIPPLES = 64;\r\n\tprivate const int RIPPLE_ROWS = 2;\r\n\r\n\t[Property(Title = \u0022Amplitude\u0022), Group(\u0022Ripples\u0022)] public float RippleAmplitude { get; set; } = 8.0f;\r\n\t[Property(Title = \u0022Expansion Speed\u0022), Group(\u0022Ripples\u0022)] public float RippleSpeed { get; set; } = 100.0f;\r\n\t// Default ring spacing used when a ripple is spawned without an explicit wavelength.\r\n\t// Smaller = tighter, more concentric rings. Larger = fewer, broader rings.\r\n\t[Property(Title = \u0022Default Wavelength\u0022), Group(\u0022Ripples\u0022)] public float RippleWavelength { get; set; } = 120.0f;\r\n\t// Default ring size used when a ripple is spawned without an explicit width.\r\n\t// Larger = bigger, broader ripple (the wave packet spans a wider radial band).\r\n\t[Property(Title = \u0022Default Ring Width\u0022), Group(\u0022Ripples\u0022)] public float RippleWidth { get; set; } = 50.0f;\r\n\t[Property(Title = \u0022Damping\u0022), Group(\u0022Ripples\u0022)] public float RippleDamping { get; set; } = 1.0f;\r\n\t[Property(Title = \u0022Lifetime\u0022), Group(\u0022Ripples\u0022)] public float RippleLifetime { get; set; } = 3.0f;\r\n\r\n\tprivate struct RippleEmitter\r\n\t{\r\n\t\tpublic Vector2 Center;\r\n\t\tpublic float StartTime;\r\n\t\tpublic float Strength;\r\n\t\tpublic float Wavelength;\r\n\t\tpublic float Width;\r\n\t}\r\n\r\n\tprivate readonly List\u003CRippleEmitter\u003E m_Ripples = [];\r\n\tprivate GpuBuffer\u003CVector4\u003E m_RippleBuffer;\r\n\tprivate readonly Vector4[] m_RippleData = new Vector4[MAX_RIPPLES * RIPPLE_ROWS];\r\n\tprivate int m_ActiveRippleCount;\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Spawn an expanding ripple on the water surface at the given world position.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022_WorldPosition\u0022\u003EWhere the ripple originates (only XY is used).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022_Strength\u0022\u003EScales the height of the ripple (1 = a normal splash).\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022_Wavelength\u0022\u003ERing spacing \u2014 smaller = more rings. Pass \u0026lt;= 0 to use the manager\u0027s Default Wavelength.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022_Width\u0022\u003ERing size \u2014 larger = a bigger, broader ripple. Pass \u0026lt;= 0 to use the manager\u0027s Default Ring Width.\u003C/param\u003E\r\n\tpublic static void AddRipple(Vector3 _WorldPosition, float _Strength = 1.0f, float _Wavelength = -1.0f, float _Width = -1.0f)\r\n\t{\r\n\t\tCurrent?.AddRippleInternal(_WorldPosition, _Strength, _Wavelength, _Width);\r\n\t}\r\n\r\n\tprivate void AddRippleInternal(Vector3 _WorldPosition, float _Strength, float _Wavelength, float _Width)\r\n\t{\r\n\t\tif (_Strength \u003C= 0.0f)\r\n\t\t\treturn;\r\n\r\n\t\t// Fall back to the global defaults when no per-ripple value is given\r\n\t\tif (_Wavelength \u003C= 0.0f)\r\n\t\t\t_Wavelength = RippleWavelength;\r\n\r\n\t\tif (_Width \u003C= 0.0f)\r\n\t\t\t_Width = RippleWidth;\r\n\r\n\t\t// Drop the oldest when full so the freshest splashes always survive\r\n\t\tif (m_Ripples.Count \u003E= MAX_RIPPLES)\r\n\t\t\tm_Ripples.RemoveAt(0);\r\n\r\n\t\tm_Ripples.Add(new RippleEmitter\r\n\t\t{\r\n\t\t\tCenter = new Vector2(_WorldPosition.x, _WorldPosition.y),\r\n\t\t\tStartTime = Time.Now,\r\n\t\t\tStrength = _Strength,\r\n\t\t\tWavelength = _Wavelength,\r\n\t\t\tWidth = _Width\r\n\t\t});\r\n\t}\r\n\r\n\r\n\r\n\tprivate void UpdateRipples()\r\n\t{\r\n\t\t// Prune expired emitters\r\n\t\tfor (int i = m_Ripples.Count - 1; i \u003E= 0; i--)\r\n\t\t{\r\n\t\t\tif (Time.Now - m_Ripples[i].StartTime \u003E RippleLifetime)\r\n\t\t\t\tm_Ripples.RemoveAt(i);\r\n\t\t}\r\n\t}\r\n\r\n\t// Packs the live emitters into the buffer the shader reads, so only where something draws. The pruning above runs on\r\n\t// every machine, a dedicated server included: ComputeRippleHeight walks the list itself, and that\u0027s what buoyancy reads.\r\n\tprivate void UploadRipples()\r\n\t{\r\n\t\tm_ActiveRippleCount = Math.Min(m_Ripples.Count, MAX_RIPPLES);\r\n\r\n\t\tfor (int i = 0; i \u003C m_ActiveRippleCount; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar r = m_Ripples[i];\r\n\t\t\tint row = i * RIPPLE_ROWS;\r\n\r\n\t\t\tm_RippleData[row \u002B 0] = new Vector4(r.Center.x, r.Center.y, r.StartTime, r.Strength);\r\n\t\t\tm_RippleData[row \u002B 1] = new Vector4(r.Wavelength, r.Width, 0.0f, 0.0f);\r\n\t\t}\r\n\r\n\t\tEnsureRippleBuffer();\r\n\r\n\t\tm_RippleBuffer.SetData(m_RippleData.AsSpan(0, m_ActiveRippleCount * RIPPLE_ROWS));\r\n\t}\r\n\r\n\tprivate void EnsureRippleBuffer()\r\n\t{\r\n\t\tif (!m_RippleBuffer.IsValid())\r\n\t\t\tm_RippleBuffer = new GpuBuffer\u003CVector4\u003E(MAX_RIPPLES * RIPPLE_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\r\n\r\n\tinternal void ApplyRippleAttributes(RenderAttributes _Attributes)\r\n\t{\r\n\t\t_Attributes.Set(\u0022RippleCount\u0022, m_ActiveRippleCount);\r\n\t\t_Attributes.Set(\u0022RippleAmplitude\u0022, RippleAmplitude);\r\n\t\t_Attributes.Set(\u0022RippleSpeed\u0022, RippleSpeed);\r\n\t\t_Attributes.Set(\u0022RippleDamping\u0022, RippleDamping);\r\n\r\n\t\tif (m_RippleBuffer.IsValid())\r\n\t\t\t_Attributes.Set(\u0022RippleData\u0022, m_RippleBuffer);\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// CPU evaluation of the ripple vertical displacement at a world XY position.\r\n\t/// MUST mirror ComputeRipples() in advancedwater.shader so physics matches visuals.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float ComputeRippleHeight(Vector2 _WorldXY)\r\n\t{\r\n\t\tif (m_Ripples.Count == 0)\r\n\t\t\treturn 0.0f;\r\n\r\n\t\tfloat z = 0.0f;\r\n\r\n\t\tfor (int i = 0; i \u003C m_Ripples.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar r = m_Ripples[i];\r\n\r\n\t\t\tfloat age = Time.Now - r.StartTime;\r\n\t\t\tif (age \u003C 0.0f || age \u003E RippleLifetime)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tfloat freq = r.Wavelength \u003E 0.001f ? (MathF.PI * 2.0f / r.Wavelength) : 0.0f;\r\n\t\t\tfloat invWidthSq = r.Width \u003E 0.001f ? 1.0f / (r.Width * r.Width) : 0.0f;\r\n\r\n\t\t\tfloat d = (_WorldXY - r.Center).Length;\r\n\t\t\tfloat ring = age * RippleSpeed;\r\n\t\t\tfloat ringDelta = d - ring;\r\n\r\n\t\t\tfloat spatialEnv = MathF.Exp(-ringDelta * ringDelta * invWidthSq);\r\n\t\t\tfloat timeEnv = MathF.Exp(-age * RippleDamping);\r\n\t\t\tfloat wave = MathF.Sin(ringDelta * freq);\r\n\r\n\t\t\tz \u002B= wave * spatialEnv * timeEnv * RippleAmplitude * r.Strength;\r\n\t\t}\r\n\r\n\t\treturn z;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterBodyRenderer.cs","FileName":"WaterBodyRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\nusing Sandbox.Rendering;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n[Icon(\u0022water\u0022), Group(\u0022Environment\u0022), Title(\u0022Water Body Renderer\u0022)]\r\npublic sealed class WaterBodyRenderer : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer\r\n{\r\n#pragma warning disable CS0649\r\n\r\n\tprivate struct WaterVertex\r\n\t{\r\n\t\t[VertexLayout.Position] public Vector3 Position;\r\n\t\t[VertexLayout.Normal] public Vector3 Normal;\r\n\t\t[VertexLayout.Tangent] public Vector4 Tangent;\r\n\t\t[VertexLayout.TexCoord] public Vector2 TexCoord;\r\n\t\t[VertexLayout.Color] public Color Color;\r\n\t}\r\n\r\n#pragma warning restore CS0649\r\n\r\n\tprivate const float BASE_TILE_SIZE = 100.0f;\r\n\r\n\tprivate const int MAX_RINGS = 8;\r\n\r\n\tprivate const int MAX_WATER_INCLUSION_VOLUMES = 1024;\r\n\tprivate const int WATER_INCLUSION_VOLUME_ROWS = 3;\r\n\r\n\tprivate const int MAX_WATER_EXCLUSION_VOLUMES = 512;\r\n\tprivate const int WATER_EXCLUSION_VOLUME_ROWS = 3;\r\n\r\n\tprivate const int MAX_HULL_EXCLUSION_VOLUMES = 8;\r\n\tprivate const int HULL_EXCLUSION_META_ROWS = 6;\r\n\tprivate const int HULL_EXCLUSION_META_SIZE = MAX_HULL_EXCLUSION_VOLUMES * HULL_EXCLUSION_META_ROWS;\r\n\tprivate const int MAX_HULL_EXCLUSION_TRIS = 16384;\r\n\r\n\tprivate GpuBuffer\u003CWaterVertex\u003E m_VertexBuffer;\r\n\tprivate GpuBuffer\u003Cuint\u003E m_IndexBuffer;\r\n\tprivate GpuBuffer\u003CVector4\u003E m_WaterInclusionVolumeBuffer;\r\n\tprivate GpuBuffer\u003CVector4\u003E m_WaterExclusionVolumeBuffer;\r\n\tprivate int m_TotalIndexCount;\r\n\tprivate readonly RenderAttributes m_DrawAttributes = new();\r\n\tprivate int m_LastConfigHash;\r\n\tprivate readonly Vector4[] m_WaterInclusionVolumeData = new Vector4[MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS];\r\n\tprivate readonly Vector4[] m_WaterExclusionVolumeData = new Vector4[MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS];\r\n\tprivate GpuBuffer\u003CVector4\u003E m_HullExclusionBuffer;\r\n\tprivate readonly Vector4[] m_HullExclusionData = new Vector4[HULL_EXCLUSION_META_SIZE \u002B MAX_HULL_EXCLUSION_TRIS * 3];\r\n\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public Material Material { get; set; }\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Width { get; set; } = 10000.0f;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Length { get; set; } = 10000.0f;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Depth { get; set; } = 300.0f;\r\n\t[Property(Title = \u0022Infinite Rendering\u0022), Group(\u0022General\u0022), Order(0)] public bool UseHybridInclusionBounds { get; set; } = true;\r\n\t[Property, Group(\u0022Clipmap\u0022), Order(1)] public float BaseCellSize { get; set; } = 8.0f;\r\n\t[Property, Group(\u0022Clipmap\u0022), Order(1), Range(16, 512)] public int CellsPerRing { get; set; } = 64;\r\n\t[Property(Title = \u0022Use Camera For Clipmap\u0022), Group(\u0022Clipmap\u0022), Order(1)] public bool FollowCameraForClipmap { get; set; } = true;\r\n\t[Property, Group(\u0022Texture\u0022), Order(2), Range(0.1f, 2.0f)] public float TextureTilingMultiplier { get; set; } = 1.0f;\r\n\r\n\tprivate int VerticesPerRing =\u003E (CellsPerRing \u002B 1) * (CellsPerRing \u002B 1);\r\n\tprivate float OuterExtent =\u003E CellsPerRing * BaseCellSize * (1 \u003C\u003C (ComputeRingCount() - 1));\r\n\r\n\tinternal bool ParticipatesInRendering =\u003E Active \u0026\u0026 Material.IsValid();\r\n\tinternal bool HasValidBuffers =\u003E m_VertexBuffer.IsValid() \u0026\u0026 m_IndexBuffer.IsValid();\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif (!ParticipatesInRendering)\r\n\t\t\treturn;\r\n\r\n\t\tCreateBuffers();\r\n\r\n\t\tm_LastConfigHash = ComputeConfigHash();\r\n\r\n\t\tWaterManager.Current?.RefreshWaterBodyRenderersList();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterBodyRenderersList();\r\n\r\n\t\tm_VertexBuffer = default;\r\n\t\tm_IndexBuffer = default;\r\n\t\tm_WaterInclusionVolumeBuffer?.Dispose();\r\n\t\tm_WaterInclusionVolumeBuffer = null;\r\n\t\tm_WaterExclusionVolumeBuffer?.Dispose();\r\n\t\tm_WaterExclusionVolumeBuffer = null;\r\n\t\tm_HullExclusionBuffer?.Dispose();\r\n\t\tm_HullExclusionBuffer = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif (!ParticipatesInRendering)\r\n\t\t\treturn;\r\n\r\n\t\tint configHash = ComputeConfigHash();\r\n\t\tif (!HasValidBuffers || configHash != m_LastConfigHash)\r\n\t\t{\r\n\t\t\tCreateBuffers();\r\n\t\t\tm_LastConfigHash = configHash;\r\n\t\t}\r\n\r\n\t\tUpdateShaderAttributes();\r\n\t}\r\n\r\n\tinternal BBox GetWorldBounds2D()\r\n\t{\r\n\t\tVector3 right = WorldRotation.Right * (Length / 2.0f);\r\n\t\tVector3 forward = WorldRotation.Forward * (Width / 2.0f);\r\n\r\n\t\tVector3 c0 = WorldPosition \u002B right \u002B forward;\r\n\t\tVector3 c1 = WorldPosition - right \u002B forward;\r\n\t\tVector3 c2 = WorldPosition \u002B right - forward;\r\n\t\tVector3 c3 = WorldPosition - right - forward;\r\n\r\n\t\tfloat minX = MathF.Min(MathF.Min(c0.x, c1.x), MathF.Min(c2.x, c3.x));\r\n\t\tfloat maxX = MathF.Max(MathF.Max(c0.x, c1.x), MathF.Max(c2.x, c3.x));\r\n\t\tfloat minY = MathF.Min(MathF.Min(c0.y, c1.y), MathF.Min(c2.y, c3.y));\r\n\t\tfloat maxY = MathF.Max(MathF.Max(c0.y, c1.y), MathF.Max(c2.y, c3.y));\r\n\r\n\t\treturn new BBox(new Vector3(minX, minY, WorldPosition.z - Depth), new Vector3(maxX, maxY, WorldPosition.z));\r\n\t}\r\n\r\n\t// Records the clipmap compute dispatches into the command list as DEFERRED commands.\r\n\t// They run later, on the render thread, when the camera executes the list - so the\r\n\t// per-ring attributes are set through the command list (which writes Graphics.Attributes\r\n\t// at execute time, exactly what CommandList.DispatchCompute reads) rather than on the\r\n\t// shared shader instance.\r\n\tinternal void RecordCompute(CommandList commandList, ComputeShader shader, Vector3 cameraPosition)\r\n\t{\r\n\t\tif (!ParticipatesInRendering || !HasValidBuffers)\r\n\t\t\treturn;\r\n\r\n\t\tint ringCount = ComputeRingCount();\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\r\n\t\tvar localBounds = GetWorldBounds2D();\r\n\r\n\t\tfor (int ring = 0; ring \u003C ringCount; ring\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat cellSize = BaseCellSize * (1 \u003C\u003C ring);\r\n\t\t\tVector3 clipmapAnchor = FollowCameraForClipmap ? cameraPosition : WorldPosition;\r\n\t\t\tfloat snapX = MathF.Floor(clipmapAnchor.x / cellSize) * cellSize;\r\n\t\t\tfloat snapY = MathF.Floor(clipmapAnchor.y / cellSize) * cellSize;\r\n\r\n\t\t\tcommandList.Attributes.Set(\u0022VertexBuffer\u0022, m_VertexBuffer);\r\n\t\t\tcommandList.Attributes.Set(\u0022VertexOffset\u0022, ring * verticesPerRing);\r\n\t\t\tcommandList.Attributes.Set(\u0022GridWidth\u0022, CellsPerRing);\r\n\t\t\tcommandList.Attributes.Set(\u0022CellSize\u0022, cellSize);\r\n\t\t\tcommandList.Attributes.Set(\u0022SnapPosition\u0022, new Vector2(snapX, snapY));\r\n\t\t\tcommandList.Attributes.Set(\u0022WaterZ\u0022, WorldPosition.z);\r\n\t\t\tcommandList.Attributes.Set(\u0022TilingScale\u0022, 1.0f / OuterExtent);\r\n\t\t\tcommandList.Attributes.Set(\u0022ClampToBounds\u0022, false);\r\n\t\t\tcommandList.Attributes.Set(\u0022BoundsMin\u0022, new Vector2(localBounds.Mins.x, localBounds.Mins.y));\r\n\t\t\tcommandList.Attributes.Set(\u0022BoundsMax\u0022, new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));\r\n\t\t\tcommandList.DispatchCompute(shader, verticesPerRing, 1, 1);\r\n\t\t}\r\n\t}\r\n\r\n\tinternal void BarrierTransition(CommandList _CommandList)\r\n\t{\r\n\t\tif (m_VertexBuffer.IsValid())\r\n\t\t\t_CommandList?.ResourceBarrierTransition(m_VertexBuffer, ResourceState.UnorderedAccess, ResourceState.VertexOrIndexBuffer);\r\n\t}\r\n\r\n\tinternal void Draw(CommandList _CommandList)\r\n\t{\r\n\t\tif (!ParticipatesInRendering || !HasValidBuffers)\r\n\t\t\treturn;\r\n\r\n\t\t_CommandList?.DrawIndexed(m_VertexBuffer, m_IndexBuffer, Material, 0, m_TotalIndexCount, m_DrawAttributes);\r\n\t}\r\n\r\n\tprivate void UpdateShaderAttributes()\r\n\t{\r\n\t\tBBox localBounds = GetWorldBounds2D();\r\n\r\n\t\tm_DrawAttributes.Set(\u0022RequireWaterInclusionVolumes\u0022, UseHybridInclusionBounds);\r\n\t\tm_DrawAttributes.Set(\u0022UseHybridInclusionBounds\u0022, UseHybridInclusionBounds);\r\n\t\tm_DrawAttributes.Set(\u0022HybridInclusionBoundsMin\u0022, new Vector2(localBounds.Mins.x, localBounds.Mins.y));\r\n\t\tm_DrawAttributes.Set(\u0022HybridInclusionBoundsMax\u0022, new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));\r\n\r\n\t\tWaterDefinition profile = WaterManager.GetWaveProfile(WaterType);\r\n\r\n\t\tif (profile.IsValid())\r\n\t\t\tprofile.ApplyTo(m_DrawAttributes);\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterTime\u0022, Time.Now);\r\n\t\tm_DrawAttributes.Set(\u0022DepthMax\u0022, Depth);\r\n\r\n\t\tfloat tilingScalar = (OuterExtent / BASE_TILE_SIZE) * TextureTilingMultiplier;\r\n\t\tm_DrawAttributes.Set(\u0022NormalTiling\u0022, new Vector2(tilingScalar, tilingScalar));\r\n\r\n\t\tWaterManager.Current?.ApplyRippleAttributes(m_DrawAttributes);\r\n\t\tWaterManager.Current?.ApplyCalmAttributes(m_DrawAttributes);\r\n\r\n\t\t// Band-limit the wave normal to the local clipmap vertex spacing (see shader)\r\n\t\tm_DrawAttributes.Set(\u0022WaveNormalEpsScale\u0022, 3.0f / CellsPerRing);\r\n\t\tm_DrawAttributes.Set(\u0022WaveNormalEpsMin\u0022, BaseCellSize);\r\n\r\n\t\tvar viewPosition = WaterManager.GetViewPosition(Scene, WorldPosition);\r\n\r\n\t\tSetWaterInclusionVolumes(viewPosition);\r\n\t\tSetWaterExclusionVolumes(viewPosition);\r\n\t\tSetHullExclusionVolumes();\r\n\t}\r\n\r\n\tprivate void SetWaterInclusionVolumes(Vector3 referencePosition)\r\n\t{\r\n\t\tEnsureWaterInclusionVolumeBuffer();\r\n\r\n\t\tvar volumes = WaterManager.Current.Bodies\r\n\t\t\t.Where(v =\u003E v.IsValid() \u0026\u0026 v.Active \u0026\u0026 v.WaterType == WaterType)\r\n\t\t\t.OrderBy(v =\u003E v.WorldPosition.DistanceSquared(referencePosition))\r\n\t\t\t.Take(MAX_WATER_INCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tfor (int i = 0; i \u003C volumes.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar (center, forward, up, half) = volumes[i].GetWorldOBB();\r\n\r\n\t\t\tint rowOffset = i * WATER_INCLUSION_VOLUME_ROWS;\r\n\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 0] = new Vector4(forward.x, forward.y, forward.z, half.x);\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 1] = new Vector4(up.x, up.y, up.z, half.y);\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 2] = new Vector4(center.x, center.y, center.z, half.z);\r\n\t\t}\r\n\r\n\t\tm_WaterInclusionVolumeBuffer.SetData(m_WaterInclusionVolumeData.AsSpan(0, volumes.Count * WATER_INCLUSION_VOLUME_ROWS));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterInclusionVolumeCount\u0022, volumes.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterInclusionVolumeRows\u0022, m_WaterInclusionVolumeBuffer);\r\n\t}\r\n\r\n\tprivate void SetWaterExclusionVolumes(Vector3 referencePosition)\r\n\t{\r\n\t\tEnsureWaterExclusionVolumeBuffer();\r\n\r\n\t\tvar volumes = WaterManager.Current.ExclusionVolumes\r\n\t\t\t.Where(v =\u003E v.IsValid() \u0026\u0026 v.Enabled \u0026\u0026 v.Active)\r\n\t\t\t.OrderBy(v =\u003E v.WorldPosition.DistanceSquared(referencePosition))\r\n\t\t\t.Take(MAX_WATER_EXCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tfor (int i = 0; i \u003C volumes.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar (center, forward, up, half) = volumes[i].GetWorldOBB();\r\n\r\n\t\t\tint rowOffset = i * WATER_EXCLUSION_VOLUME_ROWS;\r\n\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 0] = new Vector4(forward.x, forward.y, forward.z, half.x);\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 1] = new Vector4(up.x, up.y, up.z, half.y);\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 2] = new Vector4(center.x, center.y, center.z, half.z);\r\n\t\t}\r\n\r\n\t\tm_WaterExclusionVolumeBuffer.SetData(m_WaterExclusionVolumeData.AsSpan(0, volumes.Count * WATER_EXCLUSION_VOLUME_ROWS));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterExclusionVolumeCount\u0022, volumes.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterExclusionVolumeRows\u0022, m_WaterExclusionVolumeBuffer);\r\n\t}\r\n\r\n\tprivate void EnsureWaterExclusionVolumeBuffer()\r\n\t{\r\n\t\tif (m_WaterExclusionVolumeBuffer.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tm_WaterExclusionVolumeBuffer = new GpuBuffer\u003CVector4\u003E(MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void SetHullExclusionVolumes()\r\n\t{\r\n\t\tif (WaterManager.Current == null)\r\n\t\t\treturn;\r\n\r\n\t\tvar hulls = WaterManager.Current.HullExclusionVolumes\r\n\t\t\t.Where(h =\u003E h.IsValid() \u0026\u0026 h.Active \u0026\u0026 h.LocalTriangles.Length \u003E 0)\r\n\t\t\t.Take(MAX_HULL_EXCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tif (hulls.Count == 0)\r\n\t\t{\r\n\t\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionCount\u0022, 0);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureHullExclusionBuffers();\r\n\r\n\t\tint triWriteCursor = HULL_EXCLUSION_META_SIZE;\r\n\r\n\t\tfor (int h = 0; h \u003C hulls.Count; h\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar hull = hulls[h];\r\n\t\t\tvar tris = hull.LocalTriangles;\r\n\t\t\tint triCount = tris.Length / 3;\r\n\r\n\t\t\tif (triWriteCursor \u002B tris.Length \u003E m_HullExclusionData.Length)\r\n\t\t\t\tbreak;\r\n\r\n\t\t\thull.GetWorldToLocalRows(out var r0, out var r1, out var r2, out var r3);\r\n\r\n\t\t\tint meta = h * HULL_EXCLUSION_META_ROWS;\r\n\t\t\tm_HullExclusionData[meta \u002B 0] = r0;\r\n\t\t\tm_HullExclusionData[meta \u002B 1] = r1;\r\n\t\t\tm_HullExclusionData[meta \u002B 2] = r2;\r\n\t\t\tm_HullExclusionData[meta \u002B 3] = r3;\r\n\r\n\t\t\tvar aabb = hull.LocalAABB;\r\n\t\t\tm_HullExclusionData[meta \u002B 4] = new Vector4(triWriteCursor, triCount, aabb.Mins.x, aabb.Mins.y);\r\n\t\t\tm_HullExclusionData[meta \u002B 5] = new Vector4(aabb.Mins.z, aabb.Maxs.x, aabb.Maxs.y, aabb.Maxs.z);\r\n\r\n\t\t\tfor (int i = 0; i \u003C tris.Length; i\u002B\u002B)\r\n\t\t\t\tm_HullExclusionData[triWriteCursor \u002B i] = new Vector4(tris[i].x, tris[i].y, tris[i].z, 0f);\r\n\r\n\t\t\ttriWriteCursor \u002B= tris.Length;\r\n\t\t}\r\n\r\n\t\tm_HullExclusionBuffer.SetData(m_HullExclusionData.AsSpan(0, triWriteCursor));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionCount\u0022, hulls.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionData\u0022, m_HullExclusionBuffer);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureHullExclusionBuffers()\r\n\t{\r\n\t\tif (!m_HullExclusionBuffer.IsValid())\r\n\t\t\tm_HullExclusionBuffer = new GpuBuffer\u003CVector4\u003E(HULL_EXCLUSION_META_SIZE \u002B MAX_HULL_EXCLUSION_TRIS * 3, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureWaterInclusionVolumeBuffer()\r\n\t{\r\n\t\tif (m_WaterInclusionVolumeBuffer.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tm_WaterInclusionVolumeBuffer = new GpuBuffer\u003CVector4\u003E(MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\tprivate int ComputeConfigHash()\r\n\t{\r\n\t\treturn HashCode.Combine(Width, Length, BaseCellSize, CellsPerRing);\r\n\t}\r\n\r\n\tprivate int ComputeRingCount()\r\n\t{\r\n\t\treturn ComputeRingCount(Width, Length);\r\n\t}\r\n\r\n\tprivate int ComputeRingCount(float width, float length)\r\n\t{\r\n\t\tfloat maxDim = MathF.Max(length, width);\r\n\t\tfloat innerExtent = CellsPerRing * BaseCellSize;\r\n\t\tfloat requiredExtent = maxDim * 2.0f;\r\n\r\n\t\tif (requiredExtent \u003C= innerExtent)\r\n\t\t\treturn 1;\r\n\r\n\t\tint rings = (int)MathF.Ceiling(MathF.Log2(requiredExtent / innerExtent)) \u002B 1;\r\n\t\treturn Math.Clamp(rings, 1, MAX_RINGS);\r\n\t}\r\n\r\n\tprivate void CreateBuffers()\r\n\t{\r\n\t\tint ringCount = ComputeRingCount();\r\n\t\tint n = CellsPerRing;\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\r\n\t\tint innerStart = n / 4 \u002B 1;\r\n\t\tint innerEnd = n * 3 / 4 - 1;\r\n\t\tint innerBlockSize = innerEnd - innerStart;\r\n\t\tint filledCells = n * n;\r\n\t\tint hollowCells = filledCells - (innerBlockSize * innerBlockSize);\r\n\t\tint totalIndices = filledCells * 6;\r\n\t\ttotalIndices \u002B= (ringCount - 1) * hollowCells * 6;\r\n\r\n\t\tm_VertexBuffer = new GpuBuffer\u003CWaterVertex\u003E(ringCount * verticesPerRing, GpuBuffer.UsageFlags.Vertex | GpuBuffer.UsageFlags.Structured);\r\n\t\tm_IndexBuffer = new GpuBuffer\u003Cuint\u003E(totalIndices, GpuBuffer.UsageFlags.Index | GpuBuffer.UsageFlags.Structured);\r\n\t\tUploadIndexBuffer(ringCount);\r\n\t}\r\n\r\n\tprivate void UploadIndexBuffer(int ringCount)\r\n\t{\r\n\t\tint n = CellsPerRing;\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\t\tint innerStart = n / 4 \u002B 1;\r\n\t\tint innerEnd = n * 3 / 4 - 1;\r\n\r\n\t\tvar indices = new List\u003Cuint\u003E();\r\n\r\n\t\tfor (int ring = 0; ring \u003C ringCount; ring\u002B\u002B)\r\n\t\t{\r\n\t\t\tuint baseVertex = (uint)(ring * verticesPerRing);\r\n\r\n\t\t\tfor (int y = 0; y \u003C n; y\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tfor (int x = 0; x \u003C n; x\u002B\u002B)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (ring \u003E 0 \u0026\u0026 x \u003E= innerStart \u0026\u0026 x \u003C innerEnd \u0026\u0026 y \u003E= innerStart \u0026\u0026 y \u003C innerEnd)\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tuint i0 = baseVertex \u002B (uint)(y * (n \u002B 1) \u002B x);\r\n\t\t\t\t\tuint i1 = i0 \u002B 1;\r\n\t\t\t\t\tuint i2 = i0 \u002B (uint)(n \u002B 1);\r\n\t\t\t\t\tuint i3 = i2 \u002B 1;\r\n\r\n\t\t\t\t\tindices.Add(i0);\r\n\t\t\t\t\tindices.Add(i1);\r\n\t\t\t\t\tindices.Add(i2);\r\n\t\t\t\t\tindices.Add(i1);\r\n\t\t\t\t\tindices.Add(i3);\r\n\t\t\t\t\tindices.Add(i2);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tm_IndexBuffer.SetData(indices);\r\n\t\tm_TotalIndexCount = indices.Count;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterManager.cs","FileName":"WaterManager.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\nusing Sandbox.Rendering;\r\nusing RenderStage = Sandbox.Rendering.Stage;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Owns the scene\u0027s water: the registry of every surface and volume, the CPU queries gameplay asks (IsPositionInsideAny,\r\n/// GetWaterHeightAt...) and the drawing.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Not DontExecuteOnServer, on purpose: a dedicated server simulates drowning and the buoyancy of the bodies it owns, so it\r\n/// needs the same answers as everyone else. It runs there and skips only the GPU side, on Application.IsHeadless. OnPreRender,\r\n/// where the drawing is recorded, never runs there at all: the engine skips PreRender on that same test.\r\n/// \u003C/remarks\u003E\r\n[Title(\u0022Water Manager\u0022)]\r\npublic partial class WaterManager : Component, Component.ExecuteInEditor, IHotloadManaged\r\n{\r\n\t[SkipHotload] public static WaterManager Current { get; private set; } = null;\r\n\r\n\t[Property(Title = \u0022Ocean\u0022), Group(\u0022Profile\u0022), Order(0)] public WaterDefinition OceanWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Lake\u0022), Group(\u0022Profile\u0022)] public WaterDefinition LakeWaveProfile { get; set; }\r\n\t[Property(Title = \u0022River\u0022), Group(\u0022Profile\u0022)] public WaterDefinition RiverWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Pool\u0022), Group(\u0022Profile\u0022)] public WaterDefinition PoolWaveProfile { get; set; }\r\n\t[Property(Title = \u0022Custom\u0022), Group(\u0022Profile\u0022)] public WaterDefinition CustomWaveProfile { get; set; }\r\n\r\n\t[Property(Title = \u0022Underwater Volume\u0022), Group(\u0022Post Processing\u0022)] public PostProcessVolume UnderwaterPostProcessVolume { get; set; }\r\n\r\n\t// Skips the whole compute \u002B draw for any bounded water surface (pools, rivers) whose\r\n\t// bounds fall outside the camera frustum. The single biggest win when a scene has many\r\n\t// separate WaterQuads scattered around. Infinite oceans (WaterBodyRenderer) are never culled.\r\n\t[Property(Title = \u0022Frustum Culling\u0022), Group(\u0022Performance\u0022)] public bool EnableFrustumCulling { get; set; } = true;\r\n\t// Extra slack (world units) added to each surface\u0027s bounds before the frustum test, so\r\n\t// surfaces at the screen edge don\u0027t pop when the camera turns quickly.\r\n\t[Property(Title = \u0022Cull Padding\u0022), Group(\u0022Performance\u0022)] public float CullPadding { get; set; } = 256.0f;\r\n\t// Beyond this distance (world units, measured to the nearest point of a surface\u0027s bounds)\r\n\t// the surface is skipped entirely. 0 = no distance limit. Independent of frustum culling.\r\n\t[Property(Title = \u0022Max Render Distance\u0022), Group(\u0022Performance\u0022)] public float MaxRenderDistance { get; set; } = 25000.0f;\r\n\r\n\t// Distance LOD: distant water quads drop tessellation instead of staying at full density.\r\n\t// Each level halves the cell count and doubles the cell size, so the surface covers exactly\r\n\t// the same area with 4x fewer vertices \u2014 coverage, ring layout and texture tiling are all\r\n\t// unchanged, only the triangle density falls off with distance.\r\n\t[Property(Title = \u0022Distance LOD\u0022), Group(\u0022Performance\u0022)] public bool EnableDistanceLod { get; set; } = true;\r\n\t// Distance at which LOD 1 begins; each level after that doubles (LOD 2 at 2x, LOD 3 at 4x).\r\n\t[Property(Title = \u0022LOD Start Distance\u0022), Group(\u0022Performance\u0022)] public float LodStartDistance { get; set; } = 1000.0f;\r\n\t[Property(Title = \u0022Max LOD Level\u0022), Group(\u0022Performance\u0022), Range(0, 4)] public int MaxLodLevel { get; set; } = 3;\r\n\r\n\tprivate ComputeShader m_ComputeShader;\r\n\r\n\tprivate CommandList m_CommandList = new(\u0022Water Rendering\u0022);\r\n\r\n\tprivate CameraComponent m_LastCamera;\r\n\tprivate Vector3 m_CameraPosition;\r\n\tprivate Frustum m_CullFrustum;\r\n\tprivate bool m_HasCullFrustum;\r\n\tprivate WaterDefinition m_DefaultProfile;\r\n\r\n\t// Rebuilt each frame by RecordCommandList: the bounded surfaces that survived frustum culling. Reused\r\n\t// across the compute / barrier / draw phases so the decision is made exactly once.\r\n\tprivate readonly List\u003CWaterQuad\u003E m_VisibleQuads = [];\r\n\tprivate readonly List\u003CWaterFlow\u003E m_VisibleFlows = [];\r\n\r\n\tprivate List\u003CWaterQuad\u003E Quads { get; } = [];\r\n\tprivate List\u003CWaterBodyRenderer\u003E QuadRenderers { get; } = [];\r\n\tpublic List\u003CWaterBody\u003E Bodies { get; } = [];\r\n\tpublic List\u003CWaterFlow\u003E Flows { get; } = [];\r\n\tpublic List\u003CWaterExclusionVolume\u003E ExclusionVolumes { get; } = [];\r\n\tpublic List\u003CHullWaterExclusionVolume\u003E HullExclusionVolumes { get; } = [];\r\n\r\n\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tCurrent = Scene.Get\u003CWaterManager\u003E();\r\n\r\n\t\t// Only the drawing dispatches it\r\n\t\tif (!Application.IsHeadless)\r\n\t\t\tm_ComputeShader = new ComputeShader(\u0022water_clipmap_cs\u0022);\r\n\r\n\t\tm_DefaultProfile = new WaterDefinition();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tUpdateCommandListRegistration();\r\n\r\n\t\tRefreshWaterQuadsList();\r\n\t\tRefreshWaterBodyRenderersList();\r\n\t\tRefreshWaterBodiesList();\r\n\t\tRefreshWaterExclusionVolumesList();\r\n\t\tRefreshWaterHullExclusionVolumesList();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tm_RippleBuffer?.Dispose();\r\n\t\tm_RippleBuffer = null;\r\n\r\n\t\tClearCalmVolumes();\r\n\r\n\t\t// Unregister from the camera we actually registered with. Scene.Camera can have changed\r\n\t\t// (or gone) since then, so asking for it again would leave the list attached to a camera\r\n\t\t// we never clean up.\r\n\t\tif (m_LastCamera.IsValid())\r\n\t\t\tm_LastCamera.RemoveCommandList(m_CommandList);\r\n\r\n\t\tm_LastCamera = null;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keeps the compute command list attached to a camera that will actually replay it. This has\r\n\t/// to run every frame, not just on enable: a scene starting without a camera would never\r\n\t/// register at all, and leaving play mode destroys the play camera without the reference here\r\n\t/// turning null, so comparing references alone would leave us bound to a dead camera forever.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void UpdateCommandListRegistration()\r\n\t{\r\n\t\tvar renderCamera = GetRenderCamera();\r\n\r\n\t\tif (renderCamera == m_LastCamera \u0026\u0026 m_LastCamera.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tif (m_LastCamera.IsValid())\r\n\t\t\tm_LastCamera.RemoveCommandList(m_CommandList);\r\n\r\n\t\tm_LastCamera = null;\r\n\r\n\t\tif (renderCamera.IsValid())\r\n\t\t{\r\n\t\t\trenderCamera.AddCommandList(m_CommandList, RenderStage.AfterTransparent);\r\n\t\t\tm_LastCamera = renderCamera;\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The camera whose command list actually replays. A scene camera does so in the editor\r\n\t/// viewport as well as in game, so it wins when one exists; with no camera in the scene the\r\n\t/// editor camera is the only thing left that will replay ours.\r\n\t/// \u003C/summary\u003E\r\n\tprivate CameraComponent GetRenderCamera()\r\n\t{\r\n\t\t// Nothing replays a command list where nothing renders. A dedicated server still has a Scene.Camera (the scene\u0027s\r\n\t\t// own, parked wherever it was placed), so this has to be ruled out before asking for it.\r\n\t\tif (Application.IsHeadless)\r\n\t\t\treturn null;\r\n\r\n\t\tif (Scene.Camera.IsValid())\r\n\t\t\treturn Scene.Camera;\r\n\r\n\t\tif (Scene.IsEditor)\r\n\t\t\treturn Application.Editor?.Camera;\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// World position the water should treat as the viewer, for anything that culls or picks\r\n\t/// volumes by distance. While editing that has to be the viewport camera rather than the scene\r\n\t/// camera, or volumes are gathered around wherever the game camera happens to be parked and the\r\n\t/// water you are actually looking at gets the wrong set. Falls back when no camera exists at\r\n\t/// all, which is a real case - Scene.Camera excludes the editor camera and can be null.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Vector3 GetViewPosition(Scene scene, Vector3 fallback = default)\r\n\t{\r\n\t\tif (!scene.IsValid())\r\n\t\t\treturn fallback;\r\n\r\n\t\tif (scene.IsEditor)\r\n\t\t{\r\n\t\t\tvar editorCamera = Application.Editor?.Camera;\r\n\t\t\tif (editorCamera.IsValid())\r\n\t\t\t\treturn editorCamera.WorldPosition;\r\n\t\t}\r\n\r\n\t\treturn scene.Camera.IsValid() ? scene.Camera.WorldPosition : fallback;\r\n\t}\r\n\r\n\r\n\r\n\tvoid IHotloadManaged.Destroyed(Dictionary\u003Cstring, object\u003E _State)\r\n\t{\r\n\t\t_State[\u0022IsActive\u0022] = Current == this;\r\n\t}\r\n\r\n\r\n\r\n\tvoid IHotloadManaged.Created(IReadOnlyDictionary\u003Cstring, object\u003E _State)\r\n\t{\r\n\t\tif (_State.GetValueOrDefault(\u0022IsActive\u0022) is true)\r\n\t\t\tCurrent = this;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether a bounded water surface should render this frame: inside the cull camera\u0027s\r\n\t/// frustum and within the max render distance. Returns true \u2014 render it \u2014 when there\u0027s\r\n\t/// no viewer, or when both culls are disabled.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Csummary\u003EDistance at which the given LOD level starts (level 1 = LodStartDistance).\u003C/summary\u003E\r\n\tprivate float LodThreshold(int lod) =\u003E LodStartDistance * MathF.Pow(2.0f, lod - 1);\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Resolves the tessellation LOD for a surface from how far its bounds are from the viewer.\r\n\t/// Takes the surface\u0027s current level so the switch can be hysteretic: a level only changes\r\n\t/// once the distance is comfortably past the boundary, otherwise a camera hovering right on\r\n\t/// a threshold would rebuild that surface\u0027s GPU buffers every frame.\r\n\t/// \u003C/summary\u003E\r\n\tpublic int ComputeLodLevel(BBox worldBounds, int currentLod)\r\n\t{\r\n\t\tif (!EnableDistanceLod || !m_HasCullFrustum || MaxLodLevel \u003C= 0 || LodStartDistance \u003C= 0.0f)\r\n\t\t\treturn 0;\r\n\r\n\t\tconst float hysteresis = 0.15f;\r\n\r\n\t\tfloat distance = worldBounds.ClosestPoint(m_CameraPosition).Distance(m_CameraPosition);\r\n\r\n\t\tint lod = Math.Clamp(currentLod, 0, MaxLodLevel);\r\n\r\n\t\t// Step out as the surface recedes, in as it approaches \u2014 one level at a time\r\n\t\twhile (lod \u003C MaxLodLevel \u0026\u0026 distance \u003E LodThreshold(lod \u002B 1) * (1.0f \u002B hysteresis))\r\n\t\t\tlod\u002B\u002B;\r\n\r\n\t\twhile (lod \u003E 0 \u0026\u0026 distance \u003C LodThreshold(lod) * (1.0f - hysteresis))\r\n\t\t\tlod--;\r\n\r\n\t\treturn lod;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsRenderVisible(BBox worldBounds)\r\n\t{\r\n\t\t// Both culls need a viewer; without one, don\u0027t cull anything.\r\n\t\tif (!m_HasCullFrustum)\r\n\t\t\treturn true;\r\n\r\n\t\t// Distance cull \u2014 measured to the nearest point of the bounds, so a large surface\r\n\t\t// whose centre is far but edge is near still renders.\r\n\t\tif (MaxRenderDistance \u003E 0.0f)\r\n\t\t{\r\n\t\t\tfloat distSq = worldBounds.ClosestPoint(m_CameraPosition).DistanceSquared(m_CameraPosition);\r\n\r\n\t\t\tif (distSq \u003E MaxRenderDistance * MaxRenderDistance)\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t// Frustum cull\r\n\t\tif (EnableFrustumCulling \u0026\u0026 !m_CullFrustum.IsInside(worldBounds.Grow(CullPadding), partially: true))\r\n\t\t\treturn false;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Records the water once per frame on the main thread, the way the engine\u0027s own command-list components do: after\r\n\t/// every surface\u0027s OnUpdate has settled its buffers and attributes, with the cameras final, before anything draws.\r\n\t/// The camera the list is attached to replays it at AfterTransparent.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cremarks\u003E\r\n\t/// This used to be the RenderOverride of a SceneCustomObject, which the renderer calls once for EVERY view that draws\r\n\t/// the scene world - the main camera, a mirror\u0027s camera, a realtime probe\u0027s captures - and views draw in parallel.\r\n\t/// Each call reset and re-recorded the one shared list, so whichever view recorded last, or two recording at once,\r\n\t/// decided what the main camera replayed: the water flickered, and two calls at once threw \u0022Collection was modified\u0022\r\n\t/// on the visible lists.\r\n\t/// \u003C/remarks\u003E\r\n\tprotected override void OnPreRender()\r\n\t{\r\n\t\tRecordCommandList();\r\n\t}\r\n\r\n\r\n\r\n\tprivate void RecordCommandList()\r\n\t{\r\n\t\tm_CommandList.Reset();\r\n\r\n\t\t// Frustum-cull the bounded surfaces once, up front. The compute / barrier / draw\r\n\t\t// phases below all iterate these lists, so a culled surface pays for nothing.\r\n\t\tm_VisibleQuads.Clear();\r\n\t\tforeach (var quad in Quads)\r\n\t\t{\r\n\t\t\tif (quad.IsValid() \u0026\u0026 quad.ParticipatesInRendering \u0026\u0026 IsRenderVisible(quad.GetWorldBounds2D()))\r\n\t\t\t\tm_VisibleQuads.Add(quad);\r\n\t\t}\r\n\r\n\t\tm_VisibleFlows.Clear();\r\n\t\tforeach (var flow in Flows)\r\n\t\t{\r\n\t\t\tif (flow.IsValid() \u0026\u0026 flow.ParticipatesInRendering \u0026\u0026 IsRenderVisible(flow.GetWorldBounds()))\r\n\t\t\t\tm_VisibleFlows.Add(flow);\r\n\t\t}\r\n\r\n\t\tbool hasAnythingToRender = false;\r\n\r\n\t\t// Renderers are the infinite ocean surfaces \u2014 never culled (their bounds are \u0022everywhere\u0022)\r\n\t\tforeach (var renderer in QuadRenderers)\r\n\t\t{\r\n\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\thasAnythingToRender = true;\r\n\t\t\trenderer.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);\r\n\t\t}\r\n\r\n\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t{\r\n\t\t\thasAnythingToRender = true;\r\n\t\t\tquad.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);\r\n\t\t}\r\n\r\n\t\t// Flows build their mesh on the CPU (no compute pass or barrier needed)\r\n\t\tif (m_VisibleFlows.Count \u003E 0)\r\n\t\t\thasAnythingToRender = true;\r\n\r\n\t\tif (hasAnythingToRender)\r\n\t\t{\r\n\t\t\tforeach (var renderer in QuadRenderers)\r\n\t\t\t{\r\n\t\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\trenderer.BarrierTransition(m_CommandList);\r\n\t\t\t}\r\n\r\n\t\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t\t\tquad.BarrierTransition(m_CommandList);\r\n\r\n\t\t\tm_CommandList.Attributes.GrabFrameTexture(\u0022FrameBufferCopyTexture\u0022);\r\n\r\n\t\t\tforeach (var renderer in QuadRenderers)\r\n\t\t\t{\r\n\t\t\t\tif (!renderer.IsValid() || !renderer.ParticipatesInRendering)\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\trenderer.Draw(m_CommandList);\r\n\t\t\t}\r\n\r\n\t\t\tforeach (var quad in m_VisibleQuads)\r\n\t\t\t\tquad.Draw(m_CommandList);\r\n\r\n\t\t\tforeach (var flow in m_VisibleFlows)\r\n\t\t\t\tflow.Draw(m_CommandList);\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// We\u0027ve to make sure it\u0027s always correct while in the editor\r\n\t\t// (S\u0026box is a complete mess when it comes to managing a singleton properly on a component that execute in the editor, bcs its reference get constantly swapped between\r\n\t\t// gameplay and editor, we\u0027ve to do this non sense !)\r\n\t\tif (Scene.IsEditor)\r\n\t\t\tCurrent = Scene.Get\u003CWaterManager\u003E();\r\n\r\n\t\tUpdateRipples();\r\n\r\n\t\t// Everything below only feeds the drawing, which a dedicated server doesn\u0027t do: its queries read the volumes, and the\r\n\t\t// ripples kept above, directly.\r\n\t\tif (Application.IsHeadless)\r\n\t\t\treturn;\r\n\r\n\t\tUpdateCommandListRegistration();\r\n\r\n\t\t// The camera we cull and centre the clipmap against: the game camera while playing,\r\n\t\t// otherwise the editor viewport camera so culling follows what you\u0027re actually looking at.\r\n\t\tCameraComponent cullCamera = Game.IsPlaying ? Scene.Camera : Application.Editor?.Camera;\r\n\r\n\t\tif (cullCamera.IsValid())\r\n\t\t{\r\n\t\t\tm_CameraPosition = cullCamera.WorldPosition;\r\n\t\t\tm_CullFrustum = cullCamera.GetFrustum();\r\n\t\t\tm_HasCullFrustum = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tm_CameraPosition = Vector3.Zero;\r\n\t\t\tm_HasCullFrustum = false;\r\n\t\t}\r\n\r\n\t\tif (UnderwaterPostProcessVolume.IsValid())\r\n\t\t\tUnderwaterPostProcessVolume.Enabled = IsPositionInsideAny(m_CameraPosition);\r\n\r\n\t\tUploadRipples();\r\n\t\tUpdateCalmVolumes();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// We have to do all this non sense bcs using a Register/Unregister logic with OnEnabled/OnDisabled is a complete\r\n\t/// mess to manage when we enter play mode/stop play mode in the editor, the references get duplicated etc... Otherwise we\u0027ve to check by gameobject id...\r\n\t/// It\u0027s just way too annoying, refreshing the whole list is safer and we\u0027re always sure to have the proper count of components\r\n\t/// \u003C/summary\u003E\r\n\tpublic void RefreshWaterQuadsList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tQuads.Clear();\r\n\t\tQuads.AddRange(Scene.GetAll\u003CWaterQuad\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterBodyRenderersList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tQuadRenderers.Clear();\r\n\t\tQuadRenderers.AddRange(Scene.GetAll\u003CWaterBodyRenderer\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterBodiesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tBodies.Clear();\r\n\t\tBodies.AddRange(Scene.GetAll\u003CWaterBody\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterFlowsList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tFlows.Clear();\r\n\t\tFlows.AddRange(Scene.GetAll\u003CWaterFlow\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterExclusionVolumesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tExclusionVolumes.Clear();\r\n\t\tExclusionVolumes.AddRange(Scene.GetAll\u003CWaterExclusionVolume\u003E());\r\n\t}\r\n\r\n\tpublic void RefreshWaterHullExclusionVolumesList()\r\n\t{\r\n\t\tif (!Scene.IsValid()) // S\u0026box make this null while stopping play mode and entering back the editor mode (We need to guard this)\r\n\t\t\treturn;\r\n\r\n\t\tHullExclusionVolumes.Clear();\r\n\t\tHullExclusionVolumes.AddRange(Scene.GetAll\u003CHullWaterExclusionVolume\u003E());\r\n\t}\r\n\r\n\tprivate WaterDefinition GetWaveProfileForType(WaterBodyType waterType) =\u003E waterType switch\r\n\t{\r\n\t\tWaterBodyType.Ocean =\u003E OceanWaveProfile,\r\n\t\tWaterBodyType.Lake =\u003E LakeWaveProfile,\r\n\t\tWaterBodyType.River =\u003E RiverWaveProfile,\r\n\t\tWaterBodyType.Pool =\u003E PoolWaveProfile,\r\n\t\t_ =\u003E CustomWaveProfile\r\n\t};\r\n\r\n\tpublic static WaterDefinition GetWaveProfile(WaterBodyType _WaterType)\r\n\t{\r\n\t\tif (Current == null)\r\n\t\t\treturn null;\r\n\r\n\t\tWaterDefinition profile = Current.GetWaveProfileForType(_WaterType);\r\n\r\n\t\tif (profile.IsValid())\r\n\t\t\treturn profile;\r\n\r\n\t\tLog.Warning(\u0022[WaterTool] No water profile found in the \u0027Water Manager\u0027, please add a water profile for the specified water type ! (Project Settings \u003E Water Manager \u003E \u0027Assign the profiles\u0027)\u0022);\r\n\r\n\t\treturn Current.m_DefaultProfile;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterQuadBaker.cs","FileName":"WaterQuadBaker.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing Sandbox;\r\nusing Sandbox.Audio;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n[Icon(\u0022water_drop\u0022), Group(\u0022Water\u0022), Title(\u0022Water Quad Baker\u0022)]\r\npublic sealed class WaterQuadBaker : Component, Component.ExecuteInEditor\r\n{\r\n\tprivate const string BakedContainerName = \u0022Water Volumes\u0022;\r\n\tprivate const string BakedTag = \u0022water_quad_bake\u0022;\r\n\r\n\tprivate readonly List\u003CTerrain\u003E _terrains = new();\r\n\tprivate readonly HashSet\u003CCollider\u003E _solidColliders = new();\r\n\tprivate float _insideTraceDistance;\r\n\tprivate int _physicsCreatedCount;\r\n\tprivate int _skippedInsideCount;\r\n\tprivate int _subdividedCount;\r\n\r\n\t[Property, Group(\u0022Water\u0022), Order(0)] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;\r\n\r\n\t[Property, Group(\u0022Bake Bounds\u0022)] public Vector2 BakeSizeXY { get; set; } = new(10000.0f, 10000.0f);\r\n\t[Property, Group(\u0022Bake Bounds\u0022)] public float WaterSurfaceZ { get; set; } = 0.0f;\r\n\t[Property, Group(\u0022Bake Bounds\u0022)] public float WaterDepth { get; set; } = 1000.0f;\r\n\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(256.0f, 8192.0f), Order(2)] public float MinCellSize { get; set; } = 4096.0f;\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(1, 12)] public int MaxDepth { get; set; } = 6;\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(0.0f, 64.0f)] public float QuadInset { get; set; } = 0.0f;\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(1.0f, 128.0f)] public float SolidProbeRadius { get; set; } = 8.0f;\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(0.0f, 256.0f)] public float TerrainPadding { get; set; } = 16.0f;\r\n\t[Property, Group(\u0022Strict Pass\u0022)] public bool IgnoreTerrainBelowWaterSurface { get; set; } = true;\r\n\t[Property, Group(\u0022Strict Pass\u0022), Range(0.0f, 5000.0f)] public float TerrainDepthIgnoreDistance { get; set; } = 512.0f;\r\n\r\n\t[Property, Group(\u0022Coastal Fill\u0022), Order(3)] public bool EnableCoastalFill { get; set; } = true;\r\n\t[Property, Group(\u0022Coastal Fill\u0022), Range(256.0f, 8192.0f)] public float CoastalFillMaxCellSize { get; set; } = 4096.0f;\r\n\t[Property, Group(\u0022Coastal Fill\u0022), Range(0.0f, 5000.0f)] public float CoastalFillPenetrationDistance { get; set; } = 192.0f;\r\n\t[Property, Group(\u0022Coastal Fill\u0022), Range(0.1f, 1.0f)] public float CoastalFillInlandThreshold { get; set; } = 1.0f;\r\n\r\n\t[Property, ToggleGroup(\u0022Soundscape\u0022), Order(4)] public bool Soundscape { get; set; } = false;\r\n\t[Property, Group(\u0022Soundscape\u0022), Range(0.0f, 1000.0f)] public float SoundscapeExtraHeight { get; set; } = 250.0f;\r\n\t[Property, Group(\u0022Soundscape\u0022)] public Soundscape SoundscapeAsset { get; set; }\r\n\t[Property, Group(\u0022Soundscape\u0022)] public MixerHandle SoundscapeTargetMixer { get; set; }\r\n\t[Property, Group(\u0022Soundscape\u0022)] public bool SoundscapeStayActiveOnExit { get; set; } = true;\r\n\t[Property, Group(\u0022Soundscape\u0022), Range(0.0f, 2.0f)] public float SoundscapeVolume { get; set; } = 1.0f;\r\n\r\n\t[Property, Group(\u0022Miscellaneous\u0022)] public bool ExcludeMeshGeometry { get; set; } = false;\r\n\r\n\r\n\r\n\t[Button]\r\n\tprivate async Task Bake()\r\n\t{\r\n\t\tCacheSceneGeometry();\r\n\t\tClearBaked();\r\n\r\n\t\t_physicsCreatedCount = 0;\r\n\t\t_skippedInsideCount = 0;\r\n\t\t_subdividedCount = 0;\r\n\r\n\t\t// Traverse the octree synchronously to collect candidate boxes.\r\n\t\tvar pending = new List\u003CBBox\u003E();\r\n\r\n\t\tCollectPhysicsNodes(GetLocalBakeBox(), 0, pending);\r\n\r\n\t\t// Create volumes with an editor progress bar.\r\n\t\tvar container = GetOrCreateBakedContainer();\r\n\r\n\t\tawait Application.Editor.ForEachAsync(pending, \u0022Baking Water Volumes\u0022, async (box, ct) =\u003E\r\n\t\t{\r\n\t\t\tif (CreateWaterBody(container, box))\r\n\t\t\t\t_physicsCreatedCount\u002B\u002B;\r\n\r\n\t\t\tawait Task.Delay(1, ct);\r\n\t\t});\r\n\r\n\t\tLog.Info($\u0022{nameof(WaterQuadBaker)}: baked {_physicsCreatedCount} water volume set(s), skipped {_skippedInsideCount} node(s), subdivided {_subdividedCount} node(s).\u0022);\r\n\t}\r\n\r\n\r\n\r\n\t[Button]\r\n\tprivate void ClearBaked()\r\n\t{\r\n\t\tFindBakedContainer()?.Destroy();\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void DrawGizmos()\r\n\t{\r\n\t\tif (!Gizmo.IsSelected)\r\n\t\t\treturn;\r\n\r\n\t\tGizmo.Draw.Color = Color.Green;\r\n\t\tGizmo.Draw.LineBBox(GetLocalBakeBox());\r\n\r\n\t\tGizmo.Draw.Color = Color.Blue;\r\n\r\n\t\tforeach (var waterBody in GetComponentsInChildren\u003CWaterBody\u003E())\r\n\t\t{\r\n\t\t\tvar (center, forward, up, half) = waterBody.GetWorldOBB();\r\n\r\n\t\t\tGizmo.Draw.LineBBox(BBox.FromPositionAndSize(center, half * 2));\r\n\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CacheSceneGeometry()\r\n\t{\r\n\t\t_terrains.Clear();\r\n\t\t_solidColliders.Clear();\r\n\r\n\t\tforeach (var terrain in Scene.GetAllComponents\u003CTerrain\u003E())\r\n\t\t{\r\n\t\t\tif (!terrain.IsValid() || !terrain.Enabled || !terrain.Active || !terrain.EnableCollision || terrain.Storage is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t_terrains.Add(terrain);\r\n\t\t\t_solidColliders.Add(terrain);\r\n\t\t}\r\n\r\n\t\tforeach (var collider in Scene.GetAllComponents\u003CCollider\u003E())\r\n\t\t{\r\n\t\t\tif (!collider.IsValid() || !collider.Enabled || !collider.Active || collider.IsTrigger)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (collider.GameObject.Tags.Has(BakedTag))\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (ExcludeMeshGeometry \u0026\u0026 collider is not Terrain)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t_solidColliders.Add(collider);\r\n\t\t}\r\n\r\n\t\t_insideTraceDistance = Math.Max(BakeSizeXY.Length * 2.0f, 10000.0f);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CollectPhysicsNodes(BBox _LocalBox, int _Depth, List\u003CBBox\u003E _Pending)\r\n\t{\r\n\t\tvar sample = ClassifyNode(_LocalBox);\r\n\r\n\t\tbool terrainRejected = sample.TerrainAllInside || (sample.TerrainMixed \u0026\u0026 !sample.MeshHasAny);\r\n\t\tbool meshRejected = sample.MeshAllInside;\r\n\t\tbool overlapsNonTerrainSolid = BoxOverlapsNonTerrainSolid(_LocalBox);\r\n\r\n\t\tif (meshRejected)\r\n\t\t{\r\n\t\t\t_skippedInsideCount\u002B\u002B;\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (terrainRejected)\r\n\t\t{\r\n\t\t\tif (TryHandleCoastalNode(_LocalBox, _Depth, _Pending, sample))\r\n\t\t\t\treturn;\r\n\r\n\t\t\t_skippedInsideCount\u002B\u002B;\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool shouldSubdivide = sample.MeshMixed || sample.TerrainMixed || overlapsNonTerrainSolid;\r\n\r\n\t\tif (shouldSubdivide \u0026\u0026 CanSubdivide(_LocalBox, _Depth))\r\n\t\t{\r\n\t\t\t_subdividedCount\u002B\u002B;\r\n\r\n\t\t\tforeach (var child in Subdivide(_LocalBox))\r\n\t\t\t\tCollectPhysicsNodes(child, _Depth \u002B 1, _Pending);\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (shouldSubdivide)\r\n\t\t{\r\n\t\t\t_skippedInsideCount\u002B\u002B;\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_Pending.Add(_LocalBox);\r\n\t}\r\n\r\n\r\n\r\n\tprivate SampleSummary ClassifyNode(BBox _LocalBox)\r\n\t{\r\n\t\tint total = 0;\r\n\t\tint terrainInside = 0;\r\n\t\tint meshInside = 0;\r\n\r\n\t\tforeach (var localPoint in EnumerateSamplePoints(_LocalBox))\r\n\t\t{\r\n\t\t\ttotal\u002B\u002B;\r\n\r\n\t\t\tvar worldPoint = WorldTransform.PointToWorld(localPoint);\r\n\r\n\t\t\tif (IsPointInsideTerrainOnly(worldPoint))\r\n\t\t\t\tterrainInside\u002B\u002B;\r\n\r\n\t\t\tif (IsPointInsideSolidMeshOnly(worldPoint))\r\n\t\t\t\tmeshInside\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn new SampleSummary\r\n\t\t{\r\n\t\t\tTotal = total,\r\n\t\t\tTerrainInside = terrainInside,\r\n\t\t\tMeshInside = meshInside\r\n\t\t};\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool TryHandleCoastalNode(BBox _LocalBox, int _Depth, List\u003CBBox\u003E _Pending, SampleSummary _Sample)\r\n\t{\r\n\t\tif (!EnableCoastalFill || _Sample.MeshHasAny)\r\n\t\t\treturn false;\r\n\r\n\t\tfloat maxSize = Math.Max(_LocalBox.Size.x, _LocalBox.Size.y);\r\n\r\n\t\tif (maxSize \u003E CoastalFillMaxCellSize)\r\n\t\t{\r\n\t\t\t_subdividedCount\u002B\u002B;\r\n\r\n\t\t\tforeach (var child in Subdivide(_LocalBox))\r\n\t\t\t\tCollectPhysicsNodes(child, _Depth \u002B 1, _Pending);\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif (IsCellTooFarInland(_LocalBox))\r\n\t\t\treturn false;\r\n\r\n\t\t_Pending.Add(_LocalBox);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsCellTooFarInland(BBox _LocalBox)\r\n\t{\r\n\t\tint inlandCount = 0;\r\n\t\tint total = 0;\r\n\r\n\t\tforeach (var localPoint in EnumerateXYSamplePoints(_LocalBox))\r\n\t\t{\r\n\t\t\ttotal\u002B\u002B;\r\n\r\n\t\t\tvar worldPoint = WorldTransform.PointToWorld(localPoint);\r\n\r\n\t\t\tif (IsInlandAtXY(worldPoint))\r\n\t\t\t\tinlandCount\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn total \u003E 0 \u0026\u0026 ((float)inlandCount / total) \u003E= CoastalFillInlandThreshold;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsInlandAtXY(Vector3 _WorldPoint)\r\n\t{\r\n\t\tif (!IsLandAtXY(_WorldPoint))\r\n\t\t\treturn false;\r\n\r\n\t\tif (CoastalFillPenetrationDistance \u003C= 0.0f)\r\n\t\t\treturn true;\r\n\r\n\t\tVector3[] offsets =\r\n\t\t[\r\n\t\t\tVector3.Right * CoastalFillPenetrationDistance,\r\n\t\t\tVector3.Left * CoastalFillPenetrationDistance,\r\n\t\t\tVector3.Forward * CoastalFillPenetrationDistance,\r\n\t\t\tVector3.Backward * CoastalFillPenetrationDistance\r\n\t\t];\r\n\r\n\t\tforeach (var offset in offsets)\r\n\t\t{\r\n\t\t\tif (!IsLandAtXY(_WorldPoint \u002B offset))\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsLandAtXY(Vector3 _WorldPoint)\r\n\t{\r\n\t\tforeach (var terrain in _terrains)\r\n\t\t{\r\n\t\t\tif (TryGetTerrainSurfaceWorldHeight(terrain, _WorldPoint, out var worldHeight) \u0026\u0026 IsTerrainHeightBlocking(worldHeight))\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsPointInsideTerrainOnly(Vector3 _WorldPoint)\r\n\t{\r\n\t\tforeach (var terrain in _terrains)\r\n\t\t{\r\n\t\t\tif (TryGetTerrainSurfaceWorldHeight(terrain, _WorldPoint, out var worldHeight) \u0026\u0026 IsTerrainHeightBlocking(worldHeight))\r\n\t\t\t{\r\n\t\t\t\tif (_WorldPoint.z \u003C= worldHeight \u002B TerrainPadding)\r\n\t\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsTerrainHeightBlocking(float _SampledWorldHeight)\r\n\t{\r\n\t\tif (IgnoreTerrainBelowWaterSurface \u0026\u0026 _SampledWorldHeight \u003C= WaterSurfaceZ - TerrainDepthIgnoreDistance)\r\n\t\t\treturn false;\r\n\r\n\t\treturn _SampledWorldHeight \u003E= WaterSurfaceZ \u002B TerrainPadding;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool IsPointInsideSolidMeshOnly(Vector3 _WorldPoint)\r\n\t{\r\n\t\tif (ExcludeMeshGeometry)\r\n\t\t\treturn false;\r\n\r\n\t\tvar probe = Scene.Trace\r\n\t\t\t.Sphere(SolidProbeRadius, _WorldPoint, _WorldPoint)\r\n\t\t\t.WithoutTags(BakedTag)\r\n\t\t\t.Run();\r\n\r\n\t\tif (probe.StartedSolid \u0026\u0026 probe.Collider is not Terrain)\r\n\t\t\treturn true;\r\n\r\n\t\tint oddAxes = 0;\r\n\r\n\t\tif (HasOddHitCount(_WorldPoint, Vector3.Right)) oddAxes\u002B\u002B;\r\n\t\tif (HasOddHitCount(_WorldPoint, Vector3.Forward)) oddAxes\u002B\u002B;\r\n\t\tif (HasOddHitCount(_WorldPoint, Vector3.Up)) oddAxes\u002B\u002B;\r\n\r\n\t\treturn oddAxes \u003E= 2;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool HasOddHitCount(Vector3 _Start, Vector3 _Direction)\r\n\t{\r\n\t\tif (ExcludeMeshGeometry)\r\n\t\t\treturn false;\r\n\r\n\t\tvar end = _Start \u002B _Direction.Normal * _insideTraceDistance;\r\n\r\n\t\tvar hits = Scene.Trace\r\n\t\t\t.Ray(_Start, end)\r\n\t\t\t.WithoutTags(BakedTag)\r\n\t\t\t.RunAll();\r\n\r\n\t\tint hitCount = 0;\r\n\t\tCollider lastCollider = null;\r\n\t\tfloat lastFraction = -10.0f;\r\n\r\n\t\tforeach (var hit in hits)\r\n\t\t{\r\n\t\t\tif (!hit.Hit || hit.Collider is null)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (!_solidColliders.Contains(hit.Collider) || hit.Collider is Terrain)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif (hit.Collider == lastCollider \u0026\u0026 Math.Abs(hit.Fraction - lastFraction) \u003C 0.0001f)\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tlastCollider = hit.Collider;\r\n\t\t\tlastFraction = hit.Fraction;\r\n\t\t\thitCount\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn (hitCount \u0026 1) == 1;\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool BoxOverlapsNonTerrainSolid(BBox _LocalBox)\r\n\t{\r\n\t\tif (ExcludeMeshGeometry)\r\n\t\t\treturn false;\r\n\r\n\t\tvar center = WorldTransform.PointToWorld(_LocalBox.Center);\r\n\r\n\t\tvar hits = Scene.Trace\r\n\t\t\t.Box(_LocalBox.Size, center, center)\r\n\t\t\t.Rotated(WorldRotation)\r\n\t\t\t.WithoutTags(BakedTag)\r\n\t\t\t.RunAll();\r\n\r\n\t\tforeach (var hit in hits)\r\n\t\t{\r\n\t\t\tif (hit.Hit \u0026\u0026 hit.Collider is not null \u0026\u0026 hit.Collider is not Terrain)\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static bool TryGetTerrainSurfaceWorldHeight(Terrain _Terrain, Vector3 _WorldPoint, out float _SampledWorldHeight)\r\n\t{\r\n\t\t_SampledWorldHeight = 0.0f;\r\n\r\n\t\tvar storage = _Terrain.Storage;\r\n\r\n\t\tif (storage is null || storage.HeightMap is null || storage.ControlMap is null || storage.Resolution \u003C= 1)\r\n\t\t\treturn false;\r\n\r\n\t\tvar localPoint = _Terrain.WorldTransform.PointToLocal(_WorldPoint);\r\n\r\n\t\tif (localPoint.x \u003C 0.0f || localPoint.y \u003C 0.0f || localPoint.x \u003E storage.TerrainSize || localPoint.y \u003E storage.TerrainSize)\r\n\t\t\treturn false;\r\n\r\n\t\tint resolution = storage.Resolution;\r\n\t\tfloat gridX = (localPoint.x / storage.TerrainSize) * (resolution - 1);\r\n\t\tfloat gridY = (localPoint.y / storage.TerrainSize) * (resolution - 1);\r\n\r\n\t\tint x0 = (int)MathF.Floor(gridX).Clamp(0, resolution - 1);\r\n\t\tint y0 = (int)MathF.Floor(gridY).Clamp(0, resolution - 1);\r\n\t\tint x1 = (x0 \u002B 1).Clamp(0, resolution - 1);\r\n\t\tint y1 = (y0 \u002B 1).Clamp(0, resolution - 1);\r\n\r\n\t\tvar control = new CompactTerrainMaterial(storage.ControlMap[x0 \u002B y0 * resolution]);\r\n\r\n\t\tif (control.IsHole)\r\n\t\t\treturn false;\r\n\r\n\t\tfloat tx = gridX - x0;\r\n\t\tfloat ty = gridY - y0;\r\n\t\tfloat h00 = storage.HeightMap[x0 \u002B y0 * resolution];\r\n\t\tfloat h10 = storage.HeightMap[x1 \u002B y0 * resolution];\r\n\t\tfloat h01 = storage.HeightMap[x0 \u002B y1 * resolution];\r\n\t\tfloat h11 = storage.HeightMap[x1 \u002B y1 * resolution];\r\n\t\tfloat hx0 = MathX.Lerp(h00, h10, tx);\r\n\t\tfloat hx1 = MathX.Lerp(h01, h11, tx);\r\n\t\tfloat sampledLocalHeight = MathX.Lerp(hx0, hx1, ty) * (storage.TerrainHeight / ushort.MaxValue);\r\n\r\n\t\t_SampledWorldHeight = _Terrain.WorldTransform.PointToWorld(new Vector3(localPoint.x, localPoint.y, sampledLocalHeight)).z;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static IEnumerable\u003CVector3\u003E EnumerateSamplePoints(BBox _LocalBox)\r\n\t{\r\n\t\tfor (int ix = 0; ix \u003C 3; ix\u002B\u002B)\r\n\t\t\tfor (int iy = 0; iy \u003C 3; iy\u002B\u002B)\r\n\t\t\t\tfor (int iz = 0; iz \u003C 3; iz\u002B\u002B)\r\n\t\t\t\t{\r\n\t\t\t\t\tyield return new Vector3(\r\n\t\t\t\t\t\tMathX.Lerp(_LocalBox.Mins.x, _LocalBox.Maxs.x, ix * 0.5f),\r\n\t\t\t\t\t\tMathX.Lerp(_LocalBox.Mins.y, _LocalBox.Maxs.y, iy * 0.5f),\r\n\t\t\t\t\t\tMathX.Lerp(_LocalBox.Mins.z, _LocalBox.Maxs.z, iz * 0.5f)\r\n\t\t\t\t\t);\r\n\t\t\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate static IEnumerable\u003CVector3\u003E EnumerateXYSamplePoints(BBox _LocalBox)\r\n\t{\r\n\t\tfloat z = _LocalBox.Center.z;\r\n\r\n\t\tfor (int ix = 0; ix \u003C 5; ix\u002B\u002B)\r\n\t\t\tfor (int iy = 0; iy \u003C 5; iy\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tyield return new Vector3(\r\n\t\t\t\t\tMathX.Lerp(_LocalBox.Mins.x, _LocalBox.Maxs.x, ix / 4.0f),\r\n\t\t\t\t\tMathX.Lerp(_LocalBox.Mins.y, _LocalBox.Maxs.y, iy / 4.0f),\r\n\t\t\t\t\tz\r\n\t\t\t\t);\r\n\t\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool CanSubdivide(BBox _LocalBox, int _Depth)\r\n\t{\r\n\t\tif (_Depth \u003E= MaxDepth)\r\n\t\t\treturn false;\r\n\r\n\t\tvar size = _LocalBox.Size;\r\n\r\n\t\treturn size.x \u003E MinCellSize || size.y \u003E MinCellSize;\r\n\t}\r\n\r\n\r\n\r\n\tprivate static IEnumerable\u003CBBox\u003E Subdivide(BBox _LocalBox)\r\n\t{\r\n\t\tvar center = _LocalBox.Center;\r\n\t\tvar mins = _LocalBox.Mins;\r\n\t\tvar maxs = _LocalBox.Maxs;\r\n\r\n\t\tfor (int ix = 0; ix \u003C 2; ix\u002B\u002B)\r\n\t\t\tfor (int iy = 0; iy \u003C 2; iy\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tyield return new BBox(\r\n\t\t\t\t\tnew Vector3(ix == 0 ? mins.x : center.x, iy == 0 ? mins.y : center.y, mins.z),\r\n\t\t\t\t\tnew Vector3(ix == 0 ? center.x : maxs.x, iy == 0 ? center.y : maxs.y, maxs.z)\r\n\t\t\t\t);\r\n\t\t\t}\r\n\t}\r\n\r\n\r\n\r\n\tprivate bool CreateWaterBody(GameObject _Container, BBox _LocalBox)\r\n\t{\r\n\t\tfloat width = _LocalBox.Size.x - QuadInset * 2.0f;\r\n\t\tfloat length = _LocalBox.Size.y - QuadInset * 2.0f;\r\n\r\n\t\tif (width \u003C= 1.0f || length \u003C= 1.0f)\r\n\t\t\treturn false;\r\n\r\n\t\tvar go = new GameObject(_Container, true, \u0022Water Volume\u0022);\r\n\t\tgo.Tags.Add(BakedTag);\r\n\r\n\t\tvar worldPoint = WorldTransform.PointToWorld(_LocalBox.Center);\r\n\r\n\t\tgo.WorldPosition = new Vector3(worldPoint.x, worldPoint.y, WaterSurfaceZ - WaterDepth * 0.5f);\r\n\t\tgo.WorldRotation = WorldRotation;\r\n\t\tgo.WorldScale = 1.0f;\r\n\r\n\t\tvar bounds = new BBox\r\n\t\t(\r\n\t\t\tnew Vector3(-width * 0.5f, -length * 0.5f, -WaterDepth * 0.5f),\r\n\t\t\tnew Vector3(width * 0.5f, length * 0.5f, WaterDepth * 0.5f)\r\n\t\t);\r\n\r\n\t\tvar body = go.GetOrAddComponent\u003CWaterBody\u003E();\r\n\t\tbody.SetBounds(bounds);\r\n\t\tbody.WaterType = WaterType;\r\n\r\n\t\tif (Soundscape)\r\n\t\t\tCreateSoundscapeTrigger(go, width, length);\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\r\n\r\n\tprivate void CreateSoundscapeTrigger(GameObject _Parent, float _Width, float _Length)\r\n\t{\r\n\t\tvar finalExtents = new Vector3(_Width * 0.5f, _Length * 0.5f, (WaterDepth * 0.5f) \u002B SoundscapeExtraHeight);\r\n\r\n\t\tif (finalExtents.x \u003C= 1.0f || finalExtents.y \u003C= 1.0f || finalExtents.z \u003C= 1.0f)\r\n\t\t\treturn;\r\n\r\n\t\tvar go = new GameObject(_Parent, true, \u0022Water Soundscape\u0022);\r\n\t\tgo.Tags.Add(BakedTag);\r\n\t\tgo.LocalPosition = Vector3.Zero.WithZ(SoundscapeExtraHeight);\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\t\tgo.LocalScale = 1.0f;\r\n\r\n\t\tvar trigger = go.GetOrAddComponent\u003CSoundscapeTrigger\u003E();\r\n\t\ttrigger.Type = SoundscapeTrigger.TriggerType.Box;\r\n\t\ttrigger.Soundscape = SoundscapeAsset;\r\n\t\ttrigger.TargetMixer = SoundscapeTargetMixer;\r\n\t\ttrigger.StayActiveOnExit = SoundscapeStayActiveOnExit;\r\n\t\ttrigger.Volume = SoundscapeVolume;\r\n\t\ttrigger.BoxSize = finalExtents;\r\n\t}\r\n\r\n\r\n\r\n\tprivate BBox GetLocalBakeBox()\r\n\t{\r\n\t\tfloat minZ = WaterSurfaceZ - WaterDepth;\r\n\t\tfloat maxZ = WaterSurfaceZ;\r\n\r\n\t\tvar mins = new Vector3(-BakeSizeXY.x * 0.5f, -BakeSizeXY.y * 0.5f, minZ);\r\n\t\tvar maxs = new Vector3(BakeSizeXY.x * 0.5f, BakeSizeXY.y * 0.5f, maxZ);\r\n\r\n\t\treturn new BBox(mins, maxs);\r\n\t}\r\n\r\n\r\n\r\n\tprivate GameObject GetOrCreateBakedContainer()\r\n\t{\r\n\t\tvar existing = FindBakedContainer();\r\n\r\n\t\tif (existing.IsValid())\r\n\t\t\treturn existing;\r\n\r\n\t\tvar container = new GameObject(GameObject, true, BakedContainerName);\r\n\t\tcontainer.Tags.Add(\u0022container\u0022);\r\n\t\tcontainer.Tags.Add(BakedTag);\r\n\t\tcontainer.LocalPosition = Vector3.Zero;\r\n\t\tcontainer.LocalRotation = Rotation.Identity;\r\n\t\tcontainer.LocalScale = 1.0f;\r\n\r\n\t\treturn container;\r\n\t}\r\n\r\n\r\n\r\n\tprivate GameObject FindBakedContainer()\r\n\t{\r\n\t\treturn GameObject.Children.FirstOrDefault(child =\u003E child.IsValid() \u0026\u0026 child.Tags.Has(\u0022container\u0022));\r\n\t}\r\n\r\n\r\n\r\n\tprivate struct SampleSummary\r\n\t{\r\n\t\tpublic int Total;\r\n\t\tpublic int TerrainInside;\r\n\t\tpublic int MeshInside;\r\n\r\n\t\tpublic bool TerrainAllInside =\u003E Total \u003E 0 \u0026\u0026 TerrainInside == Total;\r\n\t\tpublic bool TerrainMixed =\u003E TerrainInside \u003E 0 \u0026\u0026 TerrainInside \u003C Total;\r\n\t\tpublic bool MeshAllInside =\u003E Total \u003E 0 \u0026\u0026 MeshInside == Total;\r\n\t\tpublic bool MeshHasAny =\u003E MeshInside \u003E 0;\r\n\t\tpublic bool TerrainHasAny =\u003E TerrainInside \u003E 0;\r\n\t\tpublic bool MeshMixed =\u003E MeshInside \u003E 0 \u0026\u0026 MeshInside \u003C Total;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterRippleEmitter.cs","FileName":"WaterRippleEmitter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n/// \u003Csummary\u003E\r\n/// Emits water ripples when this object crosses the water surface, and optionally\r\n/// while it moves across it. A generic, dependency-free alternative to the entry\r\n/// ripple built into \u003Csee cref=\u0022Buoyancy\u0022/\u003E \u2014 drop it on anything that doesn\u0027t have\r\n/// a Buoyancy component (players, NPCs, projectiles, debris...).\r\n///\r\n/// Velocity is derived from the object\u0027s own position delta, so it works with any\r\n/// movement system (CharacterController, custom controllers, animation, etc.) and\r\n/// needs no Rigidbody.\r\n/// \u003C/summary\u003E\r\n[Icon(\u0022water\u0022), Group(\u0022Water\u0022), Title(\u0022Water Ripple Emitter\u0022)]\r\npublic sealed class WaterRippleEmitter : Component\r\n{\r\n\t[Property, Group(\u0022Entry\u0022)] public bool EmitOnEntry { get; set; } = true;\r\n\t[Property, Group(\u0022Entry\u0022)] public float EntryStrength { get; set; } = 0.2f;\r\n\t// Ring spacing for the entry splash \u2014 smaller = tighter, more concentric rings.\r\n\t[Property, Group(\u0022Entry\u0022), Range(20.0f, 400.0f)] public float EntryWavelength { get; set; } = 120.0f;\r\n\t// Ring size for the entry splash \u2014 larger = a bigger, broader ripple.\r\n\t[Property, Group(\u0022Entry\u0022), Range(10.0f, 500.0f)] public float EntryRingWidth { get; set; } = 50.0f;\r\n\t// Minimum downward speed (units/s) needed to splash. Set to 0 to ripple on any crossing.\r\n\t[Property, Group(\u0022Entry\u0022)] public float MinImpactSpeed { get; set; } = 40.0f;\r\n\r\n\t[Property, Group(\u0022Wake\u0022)] public bool EmitWake { get; set; } = false;\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeStrength { get; set; } = 0.1f;\r\n\t// Ring spacing for wake ripples \u2014 smaller = tighter, more concentric rings.\r\n\t[Property, Group(\u0022Wake\u0022), Range(20.0f, 400.0f)] public float WakeWavelength { get; set; } = 120.0f;\r\n\t// Ring size for wake ripples \u2014 larger = a bigger, broader ripple.\r\n\t[Property, Group(\u0022Wake\u0022), Range(10.0f, 500.0f)] public float WakeRingWidth { get; set; } = 50.0f;\r\n\t// Minimum horizontal speed (units/s) before a moving object leaves a wake.\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeMinSpeed { get; set; } = 1.0f;\r\n\t[Property, Group(\u0022Wake\u0022)] public float WakeInterval { get; set; } = 0.0333f; // 30 fps\r\n\r\n\t// Local-space offset of the point tested against the surface (e.g. the feet).\r\n\t[Property, Group(\u0022General\u0022)] public Vector3 SampleOffset { get; set; } = Vector3.Zero;\r\n\r\n\tprivate bool m_Initialized;\r\n\tprivate bool m_WasBelowSurface;\r\n\tprivate Vector3 m_LastPosition;\r\n\tprivate float m_WakeTimer;\r\n\r\n\tprivate Vector3 SamplePosition =\u003E WorldPosition \u002B WorldRotation * SampleOffset;\r\n\r\n\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tm_LastPosition = SamplePosition;\r\n\t\tm_WasBelowSurface = false;\r\n\t\tm_Initialized = false;\r\n\t}\r\n\r\n\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// If this gameobject is parented to anything, we don\u0027t want to play water ripple effects\r\n\t\t// (e.g. A player inside a boat)\r\n\t\tif (GameObject.Parent != Scene)\r\n\t\t\treturn;\r\n\r\n\t\tVector3 samplePos = SamplePosition;\r\n\r\n\t\t// Velocity from position delta \u2014 no Rigidbody required\r\n\t\tVector3 velocity = Time.Delta \u003E 0.0f ? (samplePos - m_LastPosition) / Time.Delta : Vector3.Zero;\r\n\t\tm_LastPosition = samplePos;\r\n\r\n\t\tfloat waterHeight = WaterManager.GetWaterHeightAt(samplePos);\r\n\r\n\t\t// Not over any water surface\r\n\t\tif (waterHeight \u003C= float.MinValue)\r\n\t\t{\r\n\t\t\tm_WasBelowSurface = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool belowSurface = samplePos.z \u003C= waterHeight;\r\n\r\n\t\t// Skip the first valid frame so an object spawned already in water doesn\u0027t splash\r\n\t\tif (!m_Initialized)\r\n\t\t{\r\n\t\t\tm_WasBelowSurface = belowSurface;\r\n\t\t\tm_Initialized = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Entry splash on the above -\u003E below surface crossing\r\n\t\tif (EmitOnEntry \u0026\u0026 belowSurface \u0026\u0026 !m_WasBelowSurface)\r\n\t\t{\r\n\t\t\tfloat impactSpeed = float.Max(0.0f, -velocity.z);\r\n\r\n\t\t\tif (impactSpeed \u003E= MinImpactSpeed)\r\n\t\t\t{\r\n\t\t\t\tfloat strength = (impactSpeed / 150.0f).Clamp(0.3f, 2.5f) * EntryStrength;\r\n\r\n\t\t\t\tWaterManager.AddRipple(samplePos.WithZ(waterHeight), strength, EntryWavelength, EntryRingWidth);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tm_WasBelowSurface = belowSurface;\r\n\r\n\t\tfloat horizontalSpeed = velocity.WithZ(0.0f).Length;\r\n\r\n\t\t// Continuous wake while skimming/swimming through the surface\r\n\t\tif (EmitWake \u0026\u0026 belowSurface)\r\n\t\t{\r\n\t\t\tif (horizontalSpeed \u003E= WakeMinSpeed)\r\n\t\t\t{\r\n\t\t\t\tm_WakeTimer -= Time.Delta;\r\n\r\n\t\t\t\tif (m_WakeTimer \u003C= 0.0f)\r\n\t\t\t\t{\r\n\t\t\t\t\tWaterManager.AddRipple(samplePos.WithZ(waterHeight), WakeStrength, WakeWavelength, WakeRingWidth);\r\n\t\t\t\t\tm_WakeTimer = WakeInterval;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Code/Water/WaterWaveUtility.cs","FileName":"WaterWaveUtility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\npublic enum WaterBodyType\r\n{\r\n\tOcean,\r\n\tLake,\r\n\tRiver,\r\n\tPool,\r\n\tCustom\r\n}\r\n\r\npublic static class WaterWaveUtility\r\n{\r\n\tpublic static Vector3 ComputeDisplacementAt(Vector2 worldXY, WaterDefinition profile)\r\n\t{\r\n\t\tVector3 detail = ComputeGerstner(worldXY, profile.WavesScale, profile.WavesSpeed, profile.WavesDirection, profile.WavesOctaves, profile.WavesLacunarity, profile.WavesPersistence, profile.WavesSteepness) * profile.WavesIntensity;\r\n\t\tVector3 swell = ComputeGerstner(worldXY, profile.SwellScale, profile.SwellSpeed, profile.SwellDirection, profile.SwellOctaves, profile.SwellLacunarity, profile.SwellPersistence, profile.SwellSteepness) * profile.SwellIntensity;\r\n\t\treturn detail \u002B swell;\r\n\t}\r\n\r\n\tpublic static Vector3 ComputeVelocityAt(Vector2 worldXY, WaterDefinition profile)\r\n\t{\r\n\t\tVector3 detail = ComputeGerstnerVelocity(worldXY, profile.WavesScale, profile.WavesSpeed, profile.WavesDirection, profile.WavesOctaves, profile.WavesLacunarity, profile.WavesPersistence, profile.WavesSteepness) * profile.WavesIntensity;\r\n\t\tVector3 swell = ComputeGerstnerVelocity(worldXY, profile.SwellScale, profile.SwellSpeed, profile.SwellDirection, profile.SwellOctaves, profile.SwellLacunarity, profile.SwellPersistence, profile.SwellSteepness) * profile.SwellIntensity;\r\n\t\treturn detail \u002B swell;\r\n\t}\r\n\r\n\tprivate static Vector3 ComputeGerstner(Vector2 worldXY, float scale, float speed, Vector2 direction, int octaves, float lacunarity, float persistence, float steepness)\r\n\t{\r\n\t\tif (scale \u003C= 0.0f || speed \u003C= 0.0f || octaves \u003C= 0)\r\n\t\t\treturn Vector3.Zero;\r\n\r\n\t\tVector2 waveDirection = direction.Normal;\r\n\t\tfloat t = Time.Now * speed;\r\n\r\n\t\tVector3 displacement = Vector3.Zero;\r\n\t\tfloat amp = 1.0f;\r\n\t\tfloat freq = scale;\r\n\t\tfloat maxAmp = 0f;\r\n\r\n\t\tfor (int oct = 0; oct \u003C octaves; oct\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat angle = oct * 1.2f;\r\n\t\t\tVector2 octDir = new(\r\n\t\t\t\twaveDirection.x * MathF.Cos(angle) - waveDirection.y * MathF.Sin(angle),\r\n\t\t\t\twaveDirection.x * MathF.Sin(angle) \u002B waveDirection.y * MathF.Cos(angle)\r\n\t\t\t);\r\n\r\n\t\t\tfloat phase = freq * (octDir.x * worldXY.x \u002B octDir.y * worldXY.y) \u002B t * freq * 0.5f;\r\n\t\t\tdisplacement.x \u002B= steepness * amp * octDir.x * MathF.Cos(phase);\r\n\t\t\tdisplacement.y \u002B= steepness * amp * octDir.y * MathF.Cos(phase);\r\n\t\t\tdisplacement.z \u002B= amp * MathF.Sin(phase);\r\n\r\n\t\t\tmaxAmp \u002B= amp;\r\n\t\t\tamp *= persistence;\r\n\t\t\tfreq *= lacunarity;\r\n\t\t}\r\n\r\n\t\treturn maxAmp \u003E 0.0f ? displacement / maxAmp : Vector3.Zero;\r\n\t}\r\n\r\n\tprivate static Vector3 ComputeGerstnerVelocity(Vector2 worldXY, float scale, float speed, Vector2 direction, int octaves, float lacunarity, float persistence, float steepness)\r\n\t{\r\n\t\tif (scale \u003C= 0.0f || speed \u003C= 0.0f || octaves \u003C= 0)\r\n\t\t\treturn Vector3.Zero;\r\n\r\n\t\tVector2 waveDirection = direction.Normal;\r\n\t\tfloat t = Time.Now * speed;\r\n\r\n\t\tVector3 velocity = Vector3.Zero;\r\n\t\tfloat amp = 1.0f;\r\n\t\tfloat freq = scale;\r\n\t\tfloat maxAmp = 0f;\r\n\r\n\t\tfor (int oct = 0; oct \u003C octaves; oct\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat angle = oct * 1.2f;\r\n\t\t\tVector2 octDir = new(\r\n\t\t\t\twaveDirection.x * MathF.Cos(angle) - waveDirection.y * MathF.Sin(angle),\r\n\t\t\t\twaveDirection.x * MathF.Sin(angle) \u002B waveDirection.y * MathF.Cos(angle)\r\n\t\t\t);\r\n\r\n\t\t\tfloat phase = freq * (octDir.x * worldXY.x \u002B octDir.y * worldXY.y) \u002B t * freq * 0.5f;\r\n\t\t\tfloat angularVelocity = freq * speed * 0.5f;\r\n\r\n\t\t\tvelocity.x -= steepness * amp * octDir.x * angularVelocity * MathF.Sin(phase);\r\n\t\t\tvelocity.y -= steepness * amp * octDir.y * angularVelocity * MathF.Sin(phase);\r\n\t\t\tvelocity.z \u002B= amp * angularVelocity * MathF.Cos(phase);\r\n\r\n\t\t\tmaxAmp \u002B= amp;\r\n\t\t\tamp *= persistence;\r\n\t\t\tfreq *= lacunarity;\r\n\t\t}\r\n\r\n\t\treturn maxAmp \u003E 0.0f ? velocity / maxAmp : Vector3.Zero;\r\n\t}\r\n}\r\n"},{"Ident":"redsnail.watertool","Path":"Water/WaterBodyRenderer.cs","FileName":"WaterBodyRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":410705,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\nusing Sandbox.Rendering;\r\n\r\nnamespace RedSnail.WaterTool;\r\n\r\n[Icon(\u0022water\u0022), Group(\u0022Environment\u0022), Title(\u0022Water Body Renderer\u0022)]\r\npublic sealed class WaterBodyRenderer : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer\r\n{\r\n#pragma warning disable CS0649\r\n\r\n\tprivate struct WaterVertex\r\n\t{\r\n\t\t[VertexLayout.Position] public Vector3 Position;\r\n\t\t[VertexLayout.Normal] public Vector3 Normal;\r\n\t\t[VertexLayout.Tangent] public Vector4 Tangent;\r\n\t\t[VertexLayout.TexCoord] public Vector2 TexCoord;\r\n\t\t[VertexLayout.Color] public Color Color;\r\n\t}\r\n\r\n#pragma warning restore CS0649\r\n\r\n\tprivate const float BASE_TILE_SIZE = 100.0f;\r\n\r\n\tprivate const int MAX_RINGS = 8;\r\n\r\n\tprivate const int MAX_WATER_INCLUSION_VOLUMES = 1024;\r\n\tprivate const int WATER_INCLUSION_VOLUME_ROWS = 3;\r\n\r\n\tprivate const int MAX_WATER_EXCLUSION_VOLUMES = 512;\r\n\tprivate const int WATER_EXCLUSION_VOLUME_ROWS = 3;\r\n\r\n\tprivate const int MAX_HULL_EXCLUSION_VOLUMES = 8;\r\n\tprivate const int HULL_EXCLUSION_META_ROWS = 6;\r\n\tprivate const int HULL_EXCLUSION_META_SIZE = MAX_HULL_EXCLUSION_VOLUMES * HULL_EXCLUSION_META_ROWS;\r\n\tprivate const int MAX_HULL_EXCLUSION_TRIS = 16384;\r\n\r\n\tprivate GpuBuffer\u003CWaterVertex\u003E m_VertexBuffer;\r\n\tprivate GpuBuffer\u003Cuint\u003E m_IndexBuffer;\r\n\tprivate GpuBuffer\u003CVector4\u003E m_WaterInclusionVolumeBuffer;\r\n\tprivate GpuBuffer\u003CVector4\u003E m_WaterExclusionVolumeBuffer;\r\n\tprivate int m_TotalIndexCount;\r\n\tprivate readonly RenderAttributes m_DrawAttributes = new();\r\n\tprivate int m_LastConfigHash;\r\n\tprivate readonly Vector4[] m_WaterInclusionVolumeData = new Vector4[MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS];\r\n\tprivate readonly Vector4[] m_WaterExclusionVolumeData = new Vector4[MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS];\r\n\tprivate GpuBuffer\u003CVector4\u003E m_HullExclusionBuffer;\r\n\tprivate readonly Vector4[] m_HullExclusionData = new Vector4[HULL_EXCLUSION_META_SIZE \u002B MAX_HULL_EXCLUSION_TRIS * 3];\r\n\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public Material Material { get; set; }\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Width { get; set; } = 10000.0f;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Length { get; set; } = 10000.0f;\r\n\t[Property, Group(\u0022General\u0022), Order(0)] public float Depth { get; set; } = 300.0f;\r\n\t[Property(Title = \u0022Infinite Rendering\u0022), Group(\u0022General\u0022), Order(0)] public bool UseHybridInclusionBounds { get; set; } = true;\r\n\t[Property, Group(\u0022Clipmap\u0022), Order(1)] public float BaseCellSize { get; set; } = 8.0f;\r\n\t[Property, Group(\u0022Clipmap\u0022), Order(1), Range(16, 512)] public int CellsPerRing { get; set; } = 64;\r\n\t[Property(Title = \u0022Use Camera For Clipmap\u0022), Group(\u0022Clipmap\u0022), Order(1)] public bool FollowCameraForClipmap { get; set; } = true;\r\n\t[Property, Group(\u0022Texture\u0022), Order(2), Range(0.1f, 2.0f)] public float TextureTilingMultiplier { get; set; } = 1.0f;\r\n\r\n\tprivate int VerticesPerRing =\u003E (CellsPerRing \u002B 1) * (CellsPerRing \u002B 1);\r\n\tprivate float OuterExtent =\u003E CellsPerRing * BaseCellSize * (1 \u003C\u003C (ComputeRingCount() - 1));\r\n\r\n\tinternal bool ParticipatesInRendering =\u003E Active \u0026\u0026 Material.IsValid();\r\n\tinternal bool HasValidBuffers =\u003E m_VertexBuffer.IsValid() \u0026\u0026 m_IndexBuffer.IsValid();\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tif (!ParticipatesInRendering)\r\n\t\t\treturn;\r\n\r\n\t\tCreateBuffers();\r\n\r\n\t\tm_LastConfigHash = ComputeConfigHash();\r\n\r\n\t\tWaterManager.Current?.RefreshWaterBodyRenderersList();\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tWaterManager.Current?.RefreshWaterBodyRenderersList();\r\n\r\n\t\tm_VertexBuffer = default;\r\n\t\tm_IndexBuffer = default;\r\n\t\tm_WaterInclusionVolumeBuffer?.Dispose();\r\n\t\tm_WaterInclusionVolumeBuffer = null;\r\n\t\tm_WaterExclusionVolumeBuffer?.Dispose();\r\n\t\tm_WaterExclusionVolumeBuffer = null;\r\n\t\tm_HullExclusionBuffer?.Dispose();\r\n\t\tm_HullExclusionBuffer = null;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif (!ParticipatesInRendering)\r\n\t\t\treturn;\r\n\r\n\t\tint configHash = ComputeConfigHash();\r\n\t\tif (!HasValidBuffers || configHash != m_LastConfigHash)\r\n\t\t{\r\n\t\t\tCreateBuffers();\r\n\t\t\tm_LastConfigHash = configHash;\r\n\t\t}\r\n\r\n\t\tUpdateShaderAttributes();\r\n\t}\r\n\r\n\tinternal BBox GetWorldBounds2D()\r\n\t{\r\n\t\tVector3 right = WorldRotation.Right * (Length / 2.0f);\r\n\t\tVector3 forward = WorldRotation.Forward * (Width / 2.0f);\r\n\r\n\t\tVector3 c0 = WorldPosition \u002B right \u002B forward;\r\n\t\tVector3 c1 = WorldPosition - right \u002B forward;\r\n\t\tVector3 c2 = WorldPosition \u002B right - forward;\r\n\t\tVector3 c3 = WorldPosition - right - forward;\r\n\r\n\t\tfloat minX = MathF.Min(MathF.Min(c0.x, c1.x), MathF.Min(c2.x, c3.x));\r\n\t\tfloat maxX = MathF.Max(MathF.Max(c0.x, c1.x), MathF.Max(c2.x, c3.x));\r\n\t\tfloat minY = MathF.Min(MathF.Min(c0.y, c1.y), MathF.Min(c2.y, c3.y));\r\n\t\tfloat maxY = MathF.Max(MathF.Max(c0.y, c1.y), MathF.Max(c2.y, c3.y));\r\n\r\n\t\treturn new BBox(new Vector3(minX, minY, WorldPosition.z - Depth), new Vector3(maxX, maxY, WorldPosition.z));\r\n\t}\r\n\r\n\t// Records the clipmap compute dispatches into the command list as DEFERRED commands.\r\n\t// They run later, on the render thread, when the camera executes the list - so the\r\n\t// per-ring attributes are set through the command list (which writes Graphics.Attributes\r\n\t// at execute time, exactly what CommandList.DispatchCompute reads) rather than on the\r\n\t// shared shader instance.\r\n\tinternal void RecordCompute(CommandList commandList, ComputeShader shader, Vector3 cameraPosition)\r\n\t{\r\n\t\tif (!ParticipatesInRendering || !HasValidBuffers)\r\n\t\t\treturn;\r\n\r\n\t\tint ringCount = ComputeRingCount();\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\r\n\t\tvar localBounds = GetWorldBounds2D();\r\n\r\n\t\tfor (int ring = 0; ring \u003C ringCount; ring\u002B\u002B)\r\n\t\t{\r\n\t\t\tfloat cellSize = BaseCellSize * (1 \u003C\u003C ring);\r\n\t\t\tVector3 clipmapAnchor = FollowCameraForClipmap ? cameraPosition : WorldPosition;\r\n\t\t\tfloat snapX = MathF.Floor(clipmapAnchor.x / cellSize) * cellSize;\r\n\t\t\tfloat snapY = MathF.Floor(clipmapAnchor.y / cellSize) * cellSize;\r\n\r\n\t\t\tcommandList.Attributes.Set(\u0022VertexBuffer\u0022, m_VertexBuffer);\r\n\t\t\tcommandList.Attributes.Set(\u0022VertexOffset\u0022, ring * verticesPerRing);\r\n\t\t\tcommandList.Attributes.Set(\u0022GridWidth\u0022, CellsPerRing);\r\n\t\t\tcommandList.Attributes.Set(\u0022CellSize\u0022, cellSize);\r\n\t\t\tcommandList.Attributes.Set(\u0022SnapPosition\u0022, new Vector2(snapX, snapY));\r\n\t\t\tcommandList.Attributes.Set(\u0022WaterZ\u0022, WorldPosition.z);\r\n\t\t\tcommandList.Attributes.Set(\u0022TilingScale\u0022, 1.0f / OuterExtent);\r\n\t\t\tcommandList.Attributes.Set(\u0022ClampToBounds\u0022, false);\r\n\t\t\tcommandList.Attributes.Set(\u0022BoundsMin\u0022, new Vector2(localBounds.Mins.x, localBounds.Mins.y));\r\n\t\t\tcommandList.Attributes.Set(\u0022BoundsMax\u0022, new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));\r\n\t\t\tcommandList.DispatchCompute(shader, verticesPerRing, 1, 1);\r\n\t\t}\r\n\t}\r\n\r\n\tinternal void BarrierTransition(CommandList _CommandList)\r\n\t{\r\n\t\tif (m_VertexBuffer.IsValid())\r\n\t\t\t_CommandList?.ResourceBarrierTransition(m_VertexBuffer, ResourceState.UnorderedAccess, ResourceState.VertexOrIndexBuffer);\r\n\t}\r\n\r\n\tinternal void Draw(CommandList _CommandList)\r\n\t{\r\n\t\tif (!ParticipatesInRendering || !HasValidBuffers)\r\n\t\t\treturn;\r\n\r\n\t\t_CommandList?.DrawIndexed(m_VertexBuffer, m_IndexBuffer, Material, 0, m_TotalIndexCount, m_DrawAttributes);\r\n\t}\r\n\r\n\tprivate void UpdateShaderAttributes()\r\n\t{\r\n\t\tBBox localBounds = GetWorldBounds2D();\r\n\r\n\t\tm_DrawAttributes.Set(\u0022RequireWaterInclusionVolumes\u0022, UseHybridInclusionBounds);\r\n\t\tm_DrawAttributes.Set(\u0022UseHybridInclusionBounds\u0022, UseHybridInclusionBounds);\r\n\t\tm_DrawAttributes.Set(\u0022HybridInclusionBoundsMin\u0022, new Vector2(localBounds.Mins.x, localBounds.Mins.y));\r\n\t\tm_DrawAttributes.Set(\u0022HybridInclusionBoundsMax\u0022, new Vector2(localBounds.Maxs.x, localBounds.Maxs.y));\r\n\r\n\t\tWaterDefinition profile = WaterManager.GetWaveProfile(WaterType);\r\n\r\n\t\tif (profile.IsValid())\r\n\t\t\tprofile.ApplyTo(m_DrawAttributes);\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterTime\u0022, Time.Now);\r\n\t\tm_DrawAttributes.Set(\u0022DepthMax\u0022, Depth);\r\n\r\n\t\tfloat tilingScalar = (OuterExtent / BASE_TILE_SIZE) * TextureTilingMultiplier;\r\n\t\tm_DrawAttributes.Set(\u0022NormalTiling\u0022, new Vector2(tilingScalar, tilingScalar));\r\n\r\n\t\tWaterManager.Current?.ApplyRippleAttributes(m_DrawAttributes);\r\n\t\tWaterManager.Current?.ApplyCalmAttributes(m_DrawAttributes);\r\n\r\n\t\t// Band-limit the wave normal to the local clipmap vertex spacing (see shader)\r\n\t\tm_DrawAttributes.Set(\u0022WaveNormalEpsScale\u0022, 3.0f / CellsPerRing);\r\n\t\tm_DrawAttributes.Set(\u0022WaveNormalEpsMin\u0022, BaseCellSize);\r\n\r\n\t\tvar viewPosition = WaterManager.GetViewPosition(Scene, WorldPosition);\r\n\r\n\t\tSetWaterInclusionVolumes(viewPosition);\r\n\t\tSetWaterExclusionVolumes(viewPosition);\r\n\t\tSetHullExclusionVolumes();\r\n\t}\r\n\r\n\tprivate void SetWaterInclusionVolumes(Vector3 referencePosition)\r\n\t{\r\n\t\tEnsureWaterInclusionVolumeBuffer();\r\n\r\n\t\tvar volumes = WaterManager.Current.Bodies\r\n\t\t\t.Where(v =\u003E v.IsValid() \u0026\u0026 v.Active \u0026\u0026 v.WaterType == WaterType)\r\n\t\t\t.OrderBy(v =\u003E v.WorldPosition.DistanceSquared(referencePosition))\r\n\t\t\t.Take(MAX_WATER_INCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tfor (int i = 0; i \u003C volumes.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar (center, forward, up, half) = volumes[i].GetWorldOBB();\r\n\r\n\t\t\tint rowOffset = i * WATER_INCLUSION_VOLUME_ROWS;\r\n\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 0] = new Vector4(forward.x, forward.y, forward.z, half.x);\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 1] = new Vector4(up.x, up.y, up.z, half.y);\r\n\t\t\tm_WaterInclusionVolumeData[rowOffset \u002B 2] = new Vector4(center.x, center.y, center.z, half.z);\r\n\t\t}\r\n\r\n\t\tm_WaterInclusionVolumeBuffer.SetData(m_WaterInclusionVolumeData.AsSpan(0, volumes.Count * WATER_INCLUSION_VOLUME_ROWS));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterInclusionVolumeCount\u0022, volumes.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterInclusionVolumeRows\u0022, m_WaterInclusionVolumeBuffer);\r\n\t}\r\n\r\n\tprivate void SetWaterExclusionVolumes(Vector3 referencePosition)\r\n\t{\r\n\t\tEnsureWaterExclusionVolumeBuffer();\r\n\r\n\t\tvar volumes = WaterManager.Current.ExclusionVolumes\r\n\t\t\t.Where(v =\u003E v.IsValid() \u0026\u0026 v.Enabled \u0026\u0026 v.Active)\r\n\t\t\t.OrderBy(v =\u003E v.WorldPosition.DistanceSquared(referencePosition))\r\n\t\t\t.Take(MAX_WATER_EXCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tfor (int i = 0; i \u003C volumes.Count; i\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar (center, forward, up, half) = volumes[i].GetWorldOBB();\r\n\r\n\t\t\tint rowOffset = i * WATER_EXCLUSION_VOLUME_ROWS;\r\n\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 0] = new Vector4(forward.x, forward.y, forward.z, half.x);\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 1] = new Vector4(up.x, up.y, up.z, half.y);\r\n\t\t\tm_WaterExclusionVolumeData[rowOffset \u002B 2] = new Vector4(center.x, center.y, center.z, half.z);\r\n\t\t}\r\n\r\n\t\tm_WaterExclusionVolumeBuffer.SetData(m_WaterExclusionVolumeData.AsSpan(0, volumes.Count * WATER_EXCLUSION_VOLUME_ROWS));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterExclusionVolumeCount\u0022, volumes.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterExclusionVolumeRows\u0022, m_WaterExclusionVolumeBuffer);\r\n\t}\r\n\r\n\tprivate void EnsureWaterExclusionVolumeBuffer()\r\n\t{\r\n\t\tif (m_WaterExclusionVolumeBuffer.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tm_WaterExclusionVolumeBuffer = new GpuBuffer\u003CVector4\u003E(MAX_WATER_EXCLUSION_VOLUMES * WATER_EXCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void SetHullExclusionVolumes()\r\n\t{\r\n\t\tif (WaterManager.Current == null)\r\n\t\t\treturn;\r\n\r\n\t\tvar hulls = WaterManager.Current.HullExclusionVolumes\r\n\t\t\t.Where(h =\u003E h.IsValid() \u0026\u0026 h.Active \u0026\u0026 h.LocalTriangles.Length \u003E 0)\r\n\t\t\t.Take(MAX_HULL_EXCLUSION_VOLUMES)\r\n\t\t\t.ToList();\r\n\r\n\t\tif (hulls.Count == 0)\r\n\t\t{\r\n\t\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionCount\u0022, 0);\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureHullExclusionBuffers();\r\n\r\n\t\tint triWriteCursor = HULL_EXCLUSION_META_SIZE;\r\n\r\n\t\tfor (int h = 0; h \u003C hulls.Count; h\u002B\u002B)\r\n\t\t{\r\n\t\t\tvar hull = hulls[h];\r\n\t\t\tvar tris = hull.LocalTriangles;\r\n\t\t\tint triCount = tris.Length / 3;\r\n\r\n\t\t\tif (triWriteCursor \u002B tris.Length \u003E m_HullExclusionData.Length)\r\n\t\t\t\tbreak;\r\n\r\n\t\t\thull.GetWorldToLocalRows(out var r0, out var r1, out var r2, out var r3);\r\n\r\n\t\t\tint meta = h * HULL_EXCLUSION_META_ROWS;\r\n\t\t\tm_HullExclusionData[meta \u002B 0] = r0;\r\n\t\t\tm_HullExclusionData[meta \u002B 1] = r1;\r\n\t\t\tm_HullExclusionData[meta \u002B 2] = r2;\r\n\t\t\tm_HullExclusionData[meta \u002B 3] = r3;\r\n\r\n\t\t\tvar aabb = hull.LocalAABB;\r\n\t\t\tm_HullExclusionData[meta \u002B 4] = new Vector4(triWriteCursor, triCount, aabb.Mins.x, aabb.Mins.y);\r\n\t\t\tm_HullExclusionData[meta \u002B 5] = new Vector4(aabb.Mins.z, aabb.Maxs.x, aabb.Maxs.y, aabb.Maxs.z);\r\n\r\n\t\t\tfor (int i = 0; i \u003C tris.Length; i\u002B\u002B)\r\n\t\t\t\tm_HullExclusionData[triWriteCursor \u002B i] = new Vector4(tris[i].x, tris[i].y, tris[i].z, 0f);\r\n\r\n\t\t\ttriWriteCursor \u002B= tris.Length;\r\n\t\t}\r\n\r\n\t\tm_HullExclusionBuffer.SetData(m_HullExclusionData.AsSpan(0, triWriteCursor));\r\n\r\n\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionCount\u0022, hulls.Count);\r\n\t\tm_DrawAttributes.Set(\u0022WaterHullExclusionData\u0022, m_HullExclusionBuffer);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureHullExclusionBuffers()\r\n\t{\r\n\t\tif (!m_HullExclusionBuffer.IsValid())\r\n\t\t\tm_HullExclusionBuffer = new GpuBuffer\u003CVector4\u003E(HULL_EXCLUSION_META_SIZE \u002B MAX_HULL_EXCLUSION_TRIS * 3, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\r\n\r\n\tprivate void EnsureWaterInclusionVolumeBuffer()\r\n\t{\r\n\t\tif (m_WaterInclusionVolumeBuffer.IsValid())\r\n\t\t\treturn;\r\n\r\n\t\tm_WaterInclusionVolumeBuffer = new GpuBuffer\u003CVector4\u003E(MAX_WATER_INCLUSION_VOLUMES * WATER_INCLUSION_VOLUME_ROWS, GpuBuffer.UsageFlags.Structured);\r\n\t}\r\n\r\n\tprivate int ComputeConfigHash()\r\n\t{\r\n\t\treturn HashCode.Combine(Width, Length, BaseCellSize, CellsPerRing);\r\n\t}\r\n\r\n\tprivate int ComputeRingCount()\r\n\t{\r\n\t\treturn ComputeRingCount(Width, Length);\r\n\t}\r\n\r\n\tprivate int ComputeRingCount(float width, float length)\r\n\t{\r\n\t\tfloat maxDim = MathF.Max(length, width);\r\n\t\tfloat innerExtent = CellsPerRing * BaseCellSize;\r\n\t\tfloat requiredExtent = maxDim * 2.0f;\r\n\r\n\t\tif (requiredExtent \u003C= innerExtent)\r\n\t\t\treturn 1;\r\n\r\n\t\tint rings = (int)MathF.Ceiling(MathF.Log2(requiredExtent / innerExtent)) \u002B 1;\r\n\t\treturn Math.Clamp(rings, 1, MAX_RINGS);\r\n\t}\r\n\r\n\tprivate void CreateBuffers()\r\n\t{\r\n\t\tint ringCount = ComputeRingCount();\r\n\t\tint n = CellsPerRing;\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\r\n\t\tint innerStart = n / 4 \u002B 1;\r\n\t\tint innerEnd = n * 3 / 4 - 1;\r\n\t\tint innerBlockSize = innerEnd - innerStart;\r\n\t\tint filledCells = n * n;\r\n\t\tint hollowCells = filledCells - (innerBlockSize * innerBlockSize);\r\n\t\tint totalIndices = filledCells * 6;\r\n\t\ttotalIndices \u002B= (ringCount - 1) * hollowCells * 6;\r\n\r\n\t\tm_VertexBuffer = new GpuBuffer\u003CWaterVertex\u003E(ringCount * verticesPerRing, GpuBuffer.UsageFlags.Vertex | GpuBuffer.UsageFlags.Structured);\r\n\t\tm_IndexBuffer = new GpuBuffer\u003Cuint\u003E(totalIndices, GpuBuffer.UsageFlags.Index | GpuBuffer.UsageFlags.Structured);\r\n\t\tUploadIndexBuffer(ringCount);\r\n\t}\r\n\r\n\tprivate void UploadIndexBuffer(int ringCount)\r\n\t{\r\n\t\tint n = CellsPerRing;\r\n\t\tint verticesPerRing = VerticesPerRing;\r\n\t\tint innerStart = n / 4 \u002B 1;\r\n\t\tint innerEnd = n * 3 / 4 - 1;\r\n\r\n\t\tvar indices = new List\u003Cuint\u003E();\r\n\r\n\t\tfor (int ring = 0; ring \u003C ringCount; ring\u002B\u002B)\r\n\t\t{\r\n\t\t\tuint baseVertex = (uint)(ring * verticesPerRing);\r\n\r\n\t\t\tfor (int y = 0; y \u003C n; y\u002B\u002B)\r\n\t\t\t{\r\n\t\t\t\tfor (int x = 0; x \u003C n; x\u002B\u002B)\r\n\t\t\t\t{\r\n\t\t\t\t\tif (ring \u003E 0 \u0026\u0026 x \u003E= innerStart \u0026\u0026 x \u003C innerEnd \u0026\u0026 y \u003E= innerStart \u0026\u0026 y \u003C innerEnd)\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tuint i0 = baseVertex \u002B (uint)(y * (n \u002B 1) \u002B x);\r\n\t\t\t\t\tuint i1 = i0 \u002B 1;\r\n\t\t\t\t\tuint i2 = i0 \u002B (uint)(n \u002B 1);\r\n\t\t\t\t\tuint i3 = i2 \u002B 1;\r\n\r\n\t\t\t\t\tindices.Add(i0);\r\n\t\t\t\t\tindices.Add(i1);\r\n\t\t\t\t\tindices.Add(i2);\r\n\t\t\t\t\tindices.Add(i1);\r\n\t\t\t\t\tindices.Add(i3);\r\n\t\t\t\t\tindices.Add(i2);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tm_IndexBuffer.SetData(indices);\r\n\t\tm_TotalIndexCount = indices.Count;\r\n\t}\r\n}\r\n"}]}