{"TotalCount":8,"Files":[{"Ident":"redsnail.grasstool","Path":"Code/GrassDefinition.cs","FileName":"GrassDefinition.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using Sandbox;\nusing Sandbox.Rendering;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Look and behaviour of one grass layer. Shared by every \u003Csee cref=\u0022GrassRenderer\u0022/\u003E that\n/// references it, so a whole world can be retuned from a single asset.\n/// \u003C/summary\u003E\n[AssetType( Name = \u0022Grass Definition\u0022, Extension = \u0022grassdef\u0022, Category = \u0022Grass\u0022 )]\npublic sealed class GrassDefinition : GameResource\n{\n\t[Property, Group( \u0022Density\u0022 ), Range( 1, 32 )]\n\tpublic int BladesPerCell { get; set; } = 8;\n\n\t/// \u003Csummary\u003EBlades stop generating past this distance from the camera.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 500, 60000 )]\n\tpublic float MaxDistance { get; set; } = 12000.0f;\n\n\t/// \u003Csummary\u003EDistance at which blade count starts thinning out toward \u003Csee cref=\u0022MaxDistance\u0022/\u003E.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 0, 60000 )]\n\tpublic float FadeStart { get; set; } = 4000.0f;\n\n\t/// \u003Csummary\u003EMinimum ground normal Z. Steeper cells generate nothing, so cliffs stay bare.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 0, 1 )]\n\tpublic float SlopeLimit { get; set; } = 0.5f;\n\n\t[Property, Group( \u0022Blade\u0022 )]\n\tpublic RangedFloat Height { get; set; } = new( 16.0f, 30.0f );\n\n\t[Property, Group( \u0022Blade\u0022 )]\n\tpublic RangedFloat Width { get; set; } = new( 1.5f, 2.5f );\n\n\t/// \u003Csummary\u003EHow far the blade tip leans forward at rest, as a fraction of its height.\u003C/summary\u003E\n\t[Property, Group( \u0022Blade\u0022 ), Range( 0, 1 )]\n\tpublic float Curve { get; set; } = 0.25f;\n\n\t/// \u003Csummary\u003EBlends the blade\u0027s up axis from world up (0) toward the ground normal (1).\u003C/summary\u003E\n\t[Property, Group( \u0022Blade\u0022 ), Range( 0, 1 )]\n\tpublic float AlignToGround { get; set; } = 0.35f;\n\n\t[Property, Group( \u0022Color\u0022 )]\n\tpublic Color RootColor { get; set; } = new( 0.11f, 0.20f, 0.06f );\n\n\t[Property, Group( \u0022Color\u0022 )]\n\tpublic Color TipColor { get; set; } = new( 0.42f, 0.58f, 0.18f );\n\n\t/// \u003Csummary\u003EPer-blade brightness jitter, so a field doesn\u0027t read as one flat sheet.\u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float ColorVariation { get; set; } = 0.25f;\n\n\t/// \u003Csummary\u003EDarkening at the blade root, faking the occlusion of a dense canopy.\u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float RootOcclusion { get; set; } = 0.4f;\n\n\t/// \u003Csummary\u003E\n\t/// Light bleeding through the blade from a source behind it. This is most of what sells a\n\t/// field backlit by a low sun. Fades out toward the root, where the canopy is dense.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 2 )]\n\tpublic float Transmission { get; set; } = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Softens the lighting terminator. A blade is thin rather than solid, so a hard cutoff at\n\t/// grazing light turns a field into a mass of black edges.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float Wrap { get; set; } = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Pushes the lighting normal toward world up. Grass lit by its true geometric normal reads\n\t/// as a mass of hard black edges; biasing up keeps a field looking like a soft surface.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float NormalUpBias { get; set; } = 0.6f;\n\n\t[Property, Group( \u0022Wind\u0022 )]\n\tpublic Vector2 WindDirection { get; set; } = new( 1.0f, 0.35f );\n\n\t[Property, Group( \u0022Wind\u0022 ), Range( 0, 4 )]\n\tpublic float WindStrength { get; set; } = 0.35f;\n\n\t[Property, Group( \u0022Wind\u0022 ), Range( 0, 10 )]\n\tpublic float WindSpeed { get; set; } = 1.6f;\n\n\t/// \u003Csummary\u003ESpatial frequency of the travelling wind wave. Smaller values give broader gusts.\u003C/summary\u003E\n\t[Property, Group( \u0022Wind\u0022 )]\n\tpublic float WindWaveScale { get; set; } = 0.0016f;\n\n\tpublic void ApplyTo( CommandList.AttributeAccess attributes )\n\t{\n\t\tattributes.Set( \u0022GrassBladesPerCell\u0022, BladesPerCell );\n\t\tattributes.Set( \u0022GrassMaxDistance\u0022, MaxDistance );\n\t\tattributes.Set( \u0022GrassFadeStart\u0022, MathX.Clamp( FadeStart, 0.0f, MaxDistance - 1.0f ) );\n\t\tattributes.Set( \u0022GrassSlopeLimit\u0022, SlopeLimit );\n\n\t\tattributes.Set( \u0022GrassBladeHeight\u0022, new Vector2( Height.Min, Height.Max ) );\n\t\tattributes.Set( \u0022GrassBladeWidth\u0022, new Vector2( Width.Min, Width.Max ) );\n\t\tattributes.Set( \u0022GrassBladeCurve\u0022, Curve );\n\t\tattributes.Set( \u0022GrassAlignToGround\u0022, AlignToGround );\n\n\t\tattributes.Set( \u0022GrassRootColor\u0022, (Vector3)RootColor );\n\t\tattributes.Set( \u0022GrassTipColor\u0022, (Vector3)TipColor );\n\t\tattributes.Set( \u0022GrassColorVariation\u0022, ColorVariation );\n\t\tattributes.Set( \u0022GrassRootOcclusion\u0022, RootOcclusion );\n\t\tattributes.Set( \u0022GrassTransmission\u0022, Transmission );\n\t\tattributes.Set( \u0022GrassWrap\u0022, Wrap );\n\t\tattributes.Set( \u0022GrassNormalUpBias\u0022, NormalUpBias );\n\n\t\tattributes.Set( \u0022GrassWindDirection\u0022, WindDirection.Normal );\n\t\tattributes.Set( \u0022GrassWindStrength\u0022, WindStrength );\n\t\tattributes.Set( \u0022GrassWindSpeed\u0022, WindSpeed );\n\t\tattributes.Set( \u0022GrassWindWaveScale\u0022, WindWaveScale );\n\t}\n\t\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\n\t{\n\t\treturn CreateSimpleAssetTypeIcon(\u0022grass\u0022, _Width, _Height, \u0022#070f0a\u0022, \u0022#7cfba9\u0022);\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":"GrassDefinition.cs","FileName":"GrassDefinition.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using Sandbox;\nusing Sandbox.Rendering;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Look and behaviour of one grass layer. Shared by every \u003Csee cref=\u0022GrassRenderer\u0022/\u003E that\n/// references it, so a whole world can be retuned from a single asset.\n/// \u003C/summary\u003E\n[AssetType( Name = \u0022Grass Definition\u0022, Extension = \u0022grassdef\u0022, Category = \u0022Grass\u0022 )]\npublic sealed class GrassDefinition : GameResource\n{\n\t[Property, Group( \u0022Density\u0022 ), Range( 1, 32 )]\n\tpublic int BladesPerCell { get; set; } = 8;\n\n\t/// \u003Csummary\u003EBlades stop generating past this distance from the camera.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 500, 60000 )]\n\tpublic float MaxDistance { get; set; } = 12000.0f;\n\n\t/// \u003Csummary\u003EDistance at which blade count starts thinning out toward \u003Csee cref=\u0022MaxDistance\u0022/\u003E.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 0, 60000 )]\n\tpublic float FadeStart { get; set; } = 4000.0f;\n\n\t/// \u003Csummary\u003EMinimum ground normal Z. Steeper cells generate nothing, so cliffs stay bare.\u003C/summary\u003E\n\t[Property, Group( \u0022Density\u0022 ), Range( 0, 1 )]\n\tpublic float SlopeLimit { get; set; } = 0.5f;\n\n\t[Property, Group( \u0022Blade\u0022 )]\n\tpublic RangedFloat Height { get; set; } = new( 16.0f, 30.0f );\n\n\t[Property, Group( \u0022Blade\u0022 )]\n\tpublic RangedFloat Width { get; set; } = new( 1.5f, 2.5f );\n\n\t/// \u003Csummary\u003EHow far the blade tip leans forward at rest, as a fraction of its height.\u003C/summary\u003E\n\t[Property, Group( \u0022Blade\u0022 ), Range( 0, 1 )]\n\tpublic float Curve { get; set; } = 0.25f;\n\n\t/// \u003Csummary\u003EBlends the blade\u0027s up axis from world up (0) toward the ground normal (1).\u003C/summary\u003E\n\t[Property, Group( \u0022Blade\u0022 ), Range( 0, 1 )]\n\tpublic float AlignToGround { get; set; } = 0.35f;\n\n\t[Property, Group( \u0022Color\u0022 )]\n\tpublic Color RootColor { get; set; } = new( 0.11f, 0.20f, 0.06f );\n\n\t[Property, Group( \u0022Color\u0022 )]\n\tpublic Color TipColor { get; set; } = new( 0.42f, 0.58f, 0.18f );\n\n\t/// \u003Csummary\u003EPer-blade brightness jitter, so a field doesn\u0027t read as one flat sheet.\u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float ColorVariation { get; set; } = 0.25f;\n\n\t/// \u003Csummary\u003EDarkening at the blade root, faking the occlusion of a dense canopy.\u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float RootOcclusion { get; set; } = 0.4f;\n\n\t/// \u003Csummary\u003E\n\t/// Light bleeding through the blade from a source behind it. This is most of what sells a\n\t/// field backlit by a low sun. Fades out toward the root, where the canopy is dense.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 2 )]\n\tpublic float Transmission { get; set; } = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Softens the lighting terminator. A blade is thin rather than solid, so a hard cutoff at\n\t/// grazing light turns a field into a mass of black edges.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float Wrap { get; set; } = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Pushes the lighting normal toward world up. Grass lit by its true geometric normal reads\n\t/// as a mass of hard black edges; biasing up keeps a field looking like a soft surface.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022Color\u0022 ), Range( 0, 1 )]\n\tpublic float NormalUpBias { get; set; } = 0.6f;\n\n\t[Property, Group( \u0022Wind\u0022 )]\n\tpublic Vector2 WindDirection { get; set; } = new( 1.0f, 0.35f );\n\n\t[Property, Group( \u0022Wind\u0022 ), Range( 0, 4 )]\n\tpublic float WindStrength { get; set; } = 0.35f;\n\n\t[Property, Group( \u0022Wind\u0022 ), Range( 0, 10 )]\n\tpublic float WindSpeed { get; set; } = 1.6f;\n\n\t/// \u003Csummary\u003ESpatial frequency of the travelling wind wave. Smaller values give broader gusts.\u003C/summary\u003E\n\t[Property, Group( \u0022Wind\u0022 )]\n\tpublic float WindWaveScale { get; set; } = 0.0016f;\n\n\tpublic void ApplyTo( CommandList.AttributeAccess attributes )\n\t{\n\t\tattributes.Set( \u0022GrassBladesPerCell\u0022, BladesPerCell );\n\t\tattributes.Set( \u0022GrassMaxDistance\u0022, MaxDistance );\n\t\tattributes.Set( \u0022GrassFadeStart\u0022, MathX.Clamp( FadeStart, 0.0f, MaxDistance - 1.0f ) );\n\t\tattributes.Set( \u0022GrassSlopeLimit\u0022, SlopeLimit );\n\n\t\tattributes.Set( \u0022GrassBladeHeight\u0022, new Vector2( Height.Min, Height.Max ) );\n\t\tattributes.Set( \u0022GrassBladeWidth\u0022, new Vector2( Width.Min, Width.Max ) );\n\t\tattributes.Set( \u0022GrassBladeCurve\u0022, Curve );\n\t\tattributes.Set( \u0022GrassAlignToGround\u0022, AlignToGround );\n\n\t\tattributes.Set( \u0022GrassRootColor\u0022, (Vector3)RootColor );\n\t\tattributes.Set( \u0022GrassTipColor\u0022, (Vector3)TipColor );\n\t\tattributes.Set( \u0022GrassColorVariation\u0022, ColorVariation );\n\t\tattributes.Set( \u0022GrassRootOcclusion\u0022, RootOcclusion );\n\t\tattributes.Set( \u0022GrassTransmission\u0022, Transmission );\n\t\tattributes.Set( \u0022GrassWrap\u0022, Wrap );\n\t\tattributes.Set( \u0022GrassNormalUpBias\u0022, NormalUpBias );\n\n\t\tattributes.Set( \u0022GrassWindDirection\u0022, WindDirection.Normal );\n\t\tattributes.Set( \u0022GrassWindStrength\u0022, WindStrength );\n\t\tattributes.Set( \u0022GrassWindSpeed\u0022, WindSpeed );\n\t\tattributes.Set( \u0022GrassWindWaveScale\u0022, WindWaveScale );\n\t}\n\t\n\tprotected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)\n\t{\n\t\treturn CreateSimpleAssetTypeIcon(\u0022grass\u0022, _Width, _Height, \u0022#070f0a\u0022, \u0022#7cfba9\u0022);\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":"GrassStorage.cs","FileName":"GrassStorage.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing Sandbox;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Sparse painted grass coverage, stored as a chunked grid of density samples rather than\n/// individual blade transforms. Blades are generated procedurally on the GPU from this data,\n/// so a square kilometre of dense grass costs a few megabytes instead of hundreds.\n/// \u003C/summary\u003E\npublic sealed class GrassStorage : BlobData\n{\n\tpublic override int Version =\u003E 1;\n\n\t/// \u003Csummary\u003ECells along one edge of a chunk.\u003C/summary\u003E\n\tpublic const int ChunkResolution = 64;\n\n\t/// \u003Csummary\u003EWorld-space size of a single density cell, in source units.\u003C/summary\u003E\n\tpublic const float CellSize = 32.0f;\n\n\t/// \u003Csummary\u003EWorld-space size of a chunk edge, in source units.\u003C/summary\u003E\n\tpublic const float ChunkSize = ChunkResolution * CellSize;\n\n\tpublic const int CellsPerChunk = ChunkResolution * ChunkResolution;\n\n\t/// \u003Csummary\u003E\n\t/// One density sample. Height and normal are baked when painting so grass sits on any\n\t/// geometry, not just terrain. Matches the HLSL \u003Cc\u003EGrassCell\u003C/c\u003E struct exactly.\n\t/// \u003C/summary\u003E\n\t[StructLayout( LayoutKind.Sequential )]\n\tpublic struct Cell\n\t{\n\t\tpublic float Height;\n\n\t\t/// \u003Csummary\u003Edensity (0-7) | normal.x (8-15) | normal.y (16-23) | unused (24-31)\u003C/summary\u003E\n\t\tpublic uint Packed;\n\n\t\tpublic readonly float Density =\u003E (Packed \u0026 0xFF) / 255.0f;\n\n\t\tpublic readonly Vector3 Normal\n\t\t{\n\t\t\tget\n\t\t\t{\n\t\t\t\tvar x = ((Packed \u003E\u003E 8) \u0026 0xFF) / 127.5f - 1.0f;\n\t\t\t\tvar y = ((Packed \u003E\u003E 16) \u0026 0xFF) / 127.5f - 1.0f;\n\t\t\t\tvar z = MathF.Sqrt( Math.Clamp( 1.0f - x * x - y * y, 0.0f, 1.0f ) );\n\t\t\t\treturn new Vector3( x, y, z );\n\t\t\t}\n\t\t}\n\n\t\tpublic static uint Pack( float density, Vector3 normal )\n\t\t{\n\t\t\tvar d = (uint)Math.Clamp( density * 255.0f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\tvar nx = (uint)Math.Clamp( (normal.x \u002B 1.0f) * 127.5f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\tvar ny = (uint)Math.Clamp( (normal.y \u002B 1.0f) * 127.5f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\treturn d | (nx \u003C\u003C 8) | (ny \u003C\u003C 16);\n\t\t}\n\t}\n\n\tpublic readonly record struct ChunkCoord( int X, int Y );\n\n\tprivate readonly Dictionary\u003CChunkCoord, Cell[]\u003E _chunks = [];\n\n\t/// \u003Csummary\u003EBumped on every mutation so the renderer knows to re-upload its GPU buffers.\u003C/summary\u003E\n\tpublic int Revision { get; private set; }\n\n\tpublic int ChunkCount =\u003E _chunks.Count;\n\n\tpublic IReadOnlyDictionary\u003CChunkCoord, Cell[]\u003E Chunks =\u003E _chunks;\n\n\tpublic static ChunkCoord WorldToChunk( Vector3 world ) =\u003E new(\n\t\t(int)MathF.Floor( world.x / ChunkSize ),\n\t\t(int)MathF.Floor( world.y / ChunkSize ) );\n\n\tpublic static Vector2 ChunkOrigin( ChunkCoord coord ) =\u003E new( coord.X * ChunkSize, coord.Y * ChunkSize );\n\n\t/// \u003Csummary\u003EGlobal cell index on an axis. Negative world positions floor correctly.\u003C/summary\u003E\n\tprivate static int WorldToCell( float world ) =\u003E (int)MathF.Floor( world / CellSize );\n\n\tprivate static int FloorDiv( int a, int b ) =\u003E a \u003E= 0 ? a / b : ~(~a / b);\n\n\tprivate static int Mod( int a, int b )\n\t{\n\t\tvar r = a % b;\n\t\treturn r \u003C 0 ? r \u002B b : r;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Writes a density sample at a world position, baking the surface height and normal alongside it.\n\t/// Density of zero frees the sample.\n\t/// \u003C/summary\u003E\n\tpublic void SetCell( float worldX, float worldY, float density, float height, Vector3 normal )\n\t{\n\t\tvar cellX = WorldToCell( worldX );\n\t\tvar cellY = WorldToCell( worldY );\n\t\tvar coord = new ChunkCoord( FloorDiv( cellX, ChunkResolution ), FloorDiv( cellY, ChunkResolution ) );\n\n\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t{\n\t\t\tif ( density \u003C= 0.0f ) return;\n\n\t\t\tcells = new Cell[CellsPerChunk];\n\t\t\t_chunks[coord] = cells;\n\t\t}\n\n\t\tvar index = Mod( cellY, ChunkResolution ) * ChunkResolution \u002B Mod( cellX, ChunkResolution );\n\t\tcells[index] = new Cell { Height = height, Packed = Cell.Pack( density, normal ) };\n\t\tRevision\u002B\u002B;\n\t}\n\n\tpublic Cell GetCell( float worldX, float worldY )\n\t{\n\t\tvar cellX = WorldToCell( worldX );\n\t\tvar cellY = WorldToCell( worldY );\n\t\tvar coord = new ChunkCoord( FloorDiv( cellX, ChunkResolution ), FloorDiv( cellY, ChunkResolution ) );\n\n\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t\treturn default;\n\n\t\treturn cells[Mod( cellY, ChunkResolution ) * ChunkResolution \u002B Mod( cellX, ChunkResolution )];\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Reduces density in a radius, removing samples that reach zero.\n\t/// \u003C/summary\u003E\n\tpublic void Erase( Vector3 center, float radius, float strength )\n\t{\n\t\tvar radiusSq = radius * radius;\n\t\tvar minCellX = WorldToCell( center.x - radius );\n\t\tvar maxCellX = WorldToCell( center.x \u002B radius );\n\t\tvar minCellY = WorldToCell( center.y - radius );\n\t\tvar maxCellY = WorldToCell( center.y \u002B radius );\n\n\t\tfor ( var cy = minCellY; cy \u003C= maxCellY; cy\u002B\u002B )\n\t\t{\n\t\t\tfor ( var cx = minCellX; cx \u003C= maxCellX; cx\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar coord = new ChunkCoord( FloorDiv( cx, ChunkResolution ), FloorDiv( cy, ChunkResolution ) );\n\t\t\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar wx = (cx \u002B 0.5f) * CellSize;\n\t\t\t\tvar wy = (cy \u002B 0.5f) * CellSize;\n\t\t\t\tvar dx = wx - center.x;\n\t\t\t\tvar dy = wy - center.y;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E radiusSq )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar index = Mod( cy, ChunkResolution ) * ChunkResolution \u002B Mod( cx, ChunkResolution );\n\t\t\t\tref var cell = ref cells[index];\n\t\t\t\tif ( (cell.Packed \u0026 0xFF) == 0 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar density = Math.Max( cell.Density - strength, 0.0f );\n\t\t\t\tcell.Packed = density \u003C= 0.0f ? 0u : Cell.Pack( density, cell.Normal );\n\t\t\t\tRevision\u002B\u002B;\n\t\t\t}\n\t\t}\n\n\t\tPruneEmptyChunks();\n\t}\n\n\tpublic void ClearAll()\n\t{\n\t\tif ( _chunks.Count == 0 ) return;\n\n\t\t_chunks.Clear();\n\t\tRevision\u002B\u002B;\n\t}\n\n\tprivate void PruneEmptyChunks()\n\t{\n\t\tList\u003CChunkCoord\u003E empty = null;\n\n\t\tforeach ( var (coord, cells) in _chunks )\n\t\t{\n\t\t\tvar used = false;\n\t\t\tfor ( var i = 0; i \u003C cells.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( (cells[i].Packed \u0026 0xFF) != 0 ) { used = true; break; }\n\t\t\t}\n\n\t\t\tif ( !used )\n\t\t\t{\n\t\t\t\tempty ??= [];\n\t\t\t\tempty.Add( coord );\n\t\t\t}\n\t\t}\n\n\t\tif ( empty is null ) return;\n\n\t\tforeach ( var coord in empty )\n\t\t\t_chunks.Remove( coord );\n\t}\n\n\tpublic override void Serialize( ref Writer writer )\n\t{\n\t\twriter.Stream.Write( _chunks.Count );\n\n\t\tforeach ( var (coord, cells) in _chunks )\n\t\t{\n\t\t\twriter.Stream.Write( coord.X );\n\t\t\twriter.Stream.Write( coord.Y );\n\n\t\t\tfor ( var i = 0; i \u003C CellsPerChunk; i\u002B\u002B )\n\t\t\t{\n\t\t\t\twriter.Stream.Write( cells[i].Height );\n\t\t\t\twriter.Stream.Write( cells[i].Packed );\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic override void Deserialize( ref Reader reader )\n\t{\n\t\t_chunks.Clear();\n\n\t\tvar chunkCount = reader.Stream.Read\u003Cint\u003E();\n\n\t\tfor ( var c = 0; c \u003C chunkCount; c\u002B\u002B )\n\t\t{\n\t\t\tvar coord = new ChunkCoord( reader.Stream.Read\u003Cint\u003E(), reader.Stream.Read\u003Cint\u003E() );\n\t\t\tvar cells = new Cell[CellsPerChunk];\n\n\t\t\tfor ( var i = 0; i \u003C CellsPerChunk; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcells[i].Height = reader.Stream.Read\u003Cfloat\u003E();\n\t\t\t\tcells[i].Packed = reader.Stream.Read\u003Cuint\u003E();\n\t\t\t}\n\n\t\t\t_chunks[coord] = cells;\n\t\t}\n\n\t\tRevision\u002B\u002B;\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":"Code/GrassRenderer.cs","FileName":"GrassRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing Sandbox;\nusing Sandbox.Rendering;\nusing RenderStage = Sandbox.Rendering.Stage;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Renders a painted grass field. Each frame a compute pass expands the density map into blade\n/// instances for whatever is near the camera, then a single indirect draw renders them all.\n/// No blade ever exists on the CPU, and nothing is stored per-blade in the scene.\n/// \u003C/summary\u003E\n[Icon( \u0022grass\u0022 ), Group( \u0022Grass\u0022 ), Title( \u0022Grass Renderer\u0022 )]\npublic sealed class GrassRenderer : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer\n{\n\t[StructLayout( LayoutKind.Sequential )]\n\tprivate struct GpuChunk\n\t{\n\t\tpublic Vector2 Origin;\n\t\tpublic int CellOffset;\n\t\tpublic int Pad;\n\t}\n\n\t[StructLayout( LayoutKind.Sequential )]\n\tprivate struct GpuBlade\n\t{\n\t\tpublic Vector3 Position;\n\t\tpublic float Yaw;\n\t\tpublic Vector3 Normal;\n\t\tpublic float Height;\n\t\tpublic float Width;\n\t\tpublic float Tint;\n\t\tpublic float Phase;\n\t\tpublic float Pad; // keeps the struct at 48 bytes\n\t}\n\n\t// Must match GRASS_BLADE_* in grass_shared.fxc.\n\tprivate const int BladeStripVerts = 7;\n\tprivate const int BladeVertexCount = (BladeStripVerts - 2) * 3;\n\n\t// Byte offset of InstanceCount within IndirectDrawArguments. The struct is sequential\n\t// { uint VertexCount; uint InstanceCount; uint FirstVertex; uint FirstInstance; }, so this is\n\t// fixed at 4. Marshal.OffsetOf would express it directly but is outside the sandbox whitelist.\n\tprivate const int ArgsInstanceCountOffset = 4;\n\n\t[Property, Group( \u0022General\u0022 )]\n\tpublic GrassDefinition Definition { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Ceiling on blades alive at once. Each costs 48 bytes of GPU memory, so 500k is ~24 MB.\n\t/// Raise it if dense fields visibly clip out at the far edge of the render distance.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022General\u0022 ), Range( 50000, 4000000 )]\n\tpublic int MaxBlades { get; set; } = 500000;\n\n\t/// \u003Csummary\u003E\n\t/// How far outside the camera frustum blades are still generated, in world units. Culling is\n\t/// done against the frustum as it was when the frame started, so turning the camera brings in\n\t/// blades that were never generated. Raise this if they visibly stream in at the screen edges,\n\t/// which is most obvious at low or capped framerates.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022General\u0022 ), Range( 0, 4000 )]\n\tpublic float CullPadding { get; set; } = 1000.0f;\n\n\t/// \u003Csummary\u003EPainted coverage. Serialized as a binary blob, not JSON.\u003C/summary\u003E\n\t[Property, Hide]\n\tpublic GrassStorage Storage { get; set; } = new();\n\n\tprivate ComputeShader _generateShader;\n\tprivate Material _material;\n\tprivate CommandList _commandList;\n\n\tprivate GpuBuffer\u003CGpuChunk\u003E _chunkBuffer;\n\tprivate GpuBuffer\u003CGrassStorage.Cell\u003E _cellBuffer;\n\tprivate GpuBuffer\u003Cint\u003E _visibleChunkBuffer;\n\tprivate GpuBuffer\u003CVector4\u003E _frustumPlaneBuffer;\n\tprivate GpuBuffer\u003CGpuBlade\u003E _bladeBuffer;\n\tprivate GpuBuffer\u003CGpuBuffer.IndirectDrawArguments\u003E _argsBuffer;\n\n\tprivate readonly List\u003CVector2\u003E _chunkOrigins = [];\n\tprivate int[] _visibleScratch = [];\n\tprivate readonly Vector4[] _planeScratch = new Vector4[6];\n\n\tprivate CameraComponent _lastCamera;\n\tprivate int _chunkCount;\n\tprivate int _uploadedVersion = -1;\n\tprivate int _uploadedMaxBlades = -1;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tStorage ??= new GrassStorage();\n\n\t\t_generateShader = new ComputeShader(\u0022grass_generate_cs\u0022);\n\t\t_material = Material.FromShader(\u0022grass\u0022);\n\t\t_commandList = new CommandList(\u0022Grass Rendering\u0022);\n\n\t\t// Force a full re-upload: the buffers were released on disable, so a matching version\n\t\t// number here would otherwise leave the compute pass reading freed resources.\n\t\t_uploadedVersion = -1;\n\t\t_uploadedMaxBlades = -1;\n\n\t\tRefreshBuffers();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\t_lastCamera?.RemoveCommandList(_commandList);\n\t\t_lastCamera = null;\n\n\t\t_commandList?.Reset();\n\t\t_commandList = null;\n\n\t\tReleaseBuffers();\n\n\t\t_generateShader = null;\n\t\t_material = null;\n\n\t\t_uploadedVersion = -1;\n\t\t_uploadedMaxBlades = -1;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar renderCamera = GetRenderCamera();\n\n\t\t// Re-attach when the camera changes, and also when the one we attached to stopped being\n\t\t// valid. Leaving play mode destroys the play camera without the reference here changing,\n\t\t// so comparing references alone would leave us bound to a dead camera forever.\n\t\tif (renderCamera != _lastCamera || !_lastCamera.IsValid())\n\t\t{\n\t\t\tif (_lastCamera.IsValid())\n\t\t\t\t_lastCamera.RemoveCommandList(_commandList);\n\n\t\t\t_lastCamera = null;\n\n\t\t\tif (renderCamera.IsValid())\n\t\t\t{\n\t\t\t\trenderCamera.AddCommandList(_commandList, RenderStage.AfterOpaque);\n\t\t\t\t_lastCamera = renderCamera;\n\t\t\t}\n\t\t}\n\n\t\t// Nothing to draw through until a camera exists. State is cleared above, so we pick one\n\t\t// up as soon as one appears.\n\t\tif (!_lastCamera.IsValid())\n\t\t\treturn;\n\n\t\t// Culling follows whichever camera the viewport actually looks through, which is not\n\t\t// necessarily the one replaying the list.\n\t\tvar cullCamera = GetCullCamera();\n\t\t\n\t\tif (!cullCamera.IsValid())\n\t\t\treturn;\n\n\t\tRefreshBuffers();\n\t\tRecordCommandList(cullCamera);\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The camera whose command list actually replays. A scene camera does so in the editor\n\t/// viewport as well as in game, so it wins when one exists; with an empty scene the editor\n\t/// camera is the only thing left that will replay ours.\n\t/// \u003C/summary\u003E\n\tprivate CameraComponent GetRenderCamera()\n\t{\n\t\tif (Scene.Camera.IsValid())\n\t\t\treturn Scene.Camera;\n\n\t\tif (Scene.IsEditor)\n\t\t\treturn Application.Editor?.Camera;\n\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The camera the blades are generated for. While editing this is the viewport camera, or\n\t/// nothing outside the game camera\u0027s frustum would ever be generated.\n\t/// \u003C/summary\u003E\n\tprivate CameraComponent GetCullCamera()\n\t{\n\t\tif (Scene.IsEditor)\n\t\t{\n\t\t\tvar editorCamera = Application.Editor?.Camera;\n\t\t\t\n\t\t\tif (editorCamera.IsValid())\n\t\t\t\treturn editorCamera;\n\t\t}\n\n\t\treturn Scene.Camera;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Re-packs painted chunks into GPU buffers. Skipped entirely unless the painted data or the\n\t/// blade budget actually changed.\n\t/// \u003C/summary\u003E\n\tprivate void RefreshBuffers()\n\t{\n\t\tif ( Storage is null || Storage.ChunkCount == 0 )\n\t\t{\n\t\t\tif ( _chunkCount != 0 )\n\t\t\t{\n\t\t\t\tReleaseBuffers();\n\t\t\t\t_chunkCount = 0;\n\t\t\t}\n\n\t\t\t_uploadedVersion = Storage?.Revision ?? -1;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _uploadedVersion == Storage.Revision \u0026\u0026 _uploadedMaxBlades == MaxBlades \u0026\u0026 _chunkBuffer is not null )\n\t\t\treturn;\n\n\t\tReleaseBuffers();\n\n\t\t_chunkCount = Storage.ChunkCount;\n\t\t_uploadedVersion = Storage.Revision;\n\t\t_uploadedMaxBlades = MaxBlades;\n\n\t\tvar chunks = new GpuChunk[_chunkCount];\n\t\tvar cells = new GrassStorage.Cell[_chunkCount * GrassStorage.CellsPerChunk];\n\n\t\t_chunkOrigins.Clear();\n\n\t\tvar index = 0;\n\t\tforeach ( var (coord, cellData) in Storage.Chunks )\n\t\t{\n\t\t\tvar origin = GrassStorage.ChunkOrigin( coord );\n\t\t\tvar offset = index * GrassStorage.CellsPerChunk;\n\n\t\t\tchunks[index] = new GpuChunk { Origin = origin, CellOffset = offset };\n\t\t\tArray.Copy( cellData, 0, cells, offset, GrassStorage.CellsPerChunk );\n\t\t\t_chunkOrigins.Add( origin );\n\n\t\t\tindex\u002B\u002B;\n\t\t}\n\n\t\t_chunkBuffer = new GpuBuffer\u003CGpuChunk\u003E( _chunkCount, GpuBuffer.UsageFlags.Structured );\n\t\t_chunkBuffer.SetData( chunks );\n\n\t\t_cellBuffer = new GpuBuffer\u003CGrassStorage.Cell\u003E( cells.Length, GpuBuffer.UsageFlags.Structured );\n\t\t_cellBuffer.SetData( cells );\n\n\t\t_visibleChunkBuffer = new GpuBuffer\u003Cint\u003E( _chunkCount, GpuBuffer.UsageFlags.Structured );\n\t\t_visibleScratch = new int[_chunkCount];\n\n\t\t_frustumPlaneBuffer = new GpuBuffer\u003CVector4\u003E( 6, GpuBuffer.UsageFlags.Structured );\n\n\t\t_bladeBuffer = new GpuBuffer\u003CGpuBlade\u003E( MaxBlades, GpuBuffer.UsageFlags.Structured | GpuBuffer.UsageFlags.Append );\n\n\t\t_argsBuffer = new GpuBuffer\u003CGpuBuffer.IndirectDrawArguments\u003E( 1, GpuBuffer.UsageFlags.IndirectDrawArguments );\n\t\t_argsBuffer.SetData( new[]\n\t\t{\n\t\t\tnew GpuBuffer.IndirectDrawArguments { VertexCount = BladeVertexCount }\n\t\t} );\n\t}\n\n\tprivate void RecordCommandList( CameraComponent camera )\n\t{\n\t\t_commandList.Reset();\n\n\t\tif ( _chunkCount == 0 || _bladeBuffer is null )\n\t\t\treturn;\n\n\t\tvar definition = Definition;\n\t\tvar cameraPosition = camera.WorldPosition;\n\n\t\tvar visibleCount = CollectVisibleChunks( cameraPosition, definition?.MaxDistance ?? 12000.0f );\n\t\tif ( visibleCount == 0 )\n\t\t\treturn;\n\n\t\tUploadFrustumPlanes( camera );\n\n\t\t_commandList.Attributes.Set( \u0022GrassChunks\u0022, _chunkBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassCells\u0022, _cellBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassVisibleChunks\u0022, _visibleChunkBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassFrustumPlanes\u0022, _frustumPlaneBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassBlades\u0022, _bladeBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassVisibleChunkCount\u0022, visibleCount );\n\t\t_commandList.Attributes.Set( \u0022GrassCameraPos\u0022, cameraPosition );\n\t\t_commandList.Attributes.Set( \u0022GrassTime\u0022, RealTime.Now );\n\n\t\tdefinition?.ApplyTo( _commandList.Attributes );\n\n\t\t_commandList.ResourceBarrierTransition( _bladeBuffer, ResourceState.UnorderedAccess );\n\t\t_commandList.SetCounterValue( _bladeBuffer, 0 );\n\n\t\t_commandList.DispatchCompute( _generateShader, visibleCount * GrassStorage.CellsPerChunk, 1, 1 );\n\n\t\t// The appends must all land before the counter is read into the draw arguments.\n\t\t_commandList.UavBarrier( _bladeBuffer );\n\n\t\t_commandList.ResourceBarrierTransition( _argsBuffer, ResourceState.CopyDestination );\n\t\t_commandList.CopyStructureCount( _bladeBuffer, _argsBuffer, ArgsInstanceCountOffset );\n\n\t\t_commandList.ResourceBarrierTransition( _bladeBuffer, ResourceState.GenericRead );\n\t\t_commandList.ResourceBarrierTransition( _argsBuffer, ResourceState.IndirectArgument );\n\n\t\t_commandList.DrawInstancedIndirect( _material, _argsBuffer );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Narrows the dispatch to chunks near the camera. Per-blade frustum culling happens on the\n\t/// GPU; this only has to be conservative.\n\t/// \u003C/summary\u003E\n\tprivate int CollectVisibleChunks( Vector3 cameraPosition, float maxDistance )\n\t{\n\t\t// A chunk\u0027s near corner can be in range while its centre isn\u0027t, hence the circumradius.\n\t\tvar cullRadius = maxDistance \u002B GrassStorage.ChunkSize * 0.7072f;\n\t\tvar cullRadiusSq = cullRadius * cullRadius;\n\n\t\tvar halfChunk = GrassStorage.ChunkSize * 0.5f;\n\t\tvar count = 0;\n\n\t\tfor ( var i = 0; i \u003C _chunkOrigins.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar origin = _chunkOrigins[i];\n\t\t\tvar dx = origin.x \u002B halfChunk - cameraPosition.x;\n\t\t\tvar dy = origin.y \u002B halfChunk - cameraPosition.y;\n\n\t\t\tif ( dx * dx \u002B dy * dy \u003E cullRadiusSq )\n\t\t\t\tcontinue;\n\n\t\t\t_visibleScratch[count\u002B\u002B] = i;\n\t\t}\n\n\t\tif ( count \u003E 0 )\n\t\t\t_visibleChunkBuffer.SetData( _visibleScratch.AsSpan( 0, count ) );\n\n\t\treturn count;\n\t}\n\n\tprivate void UploadFrustumPlanes( CameraComponent camera )\n\t{\n\t\tvar frustum = camera.GetFrustum();\n\n\t\t// These planes are sampled once on the CPU but the blades they cull are not drawn until the\n\t\t// frame presents, by which point the camera has kept turning. Pushing every plane outward\n\t\t// gives the generation something to work with at the screen edges - without it, blades\n\t\t// rotating into view were culled before they were ever needed, which reads as them\n\t\t// streaming in from the sides. The slack that buys scales with the frame time, so a capped\n\t\t// or struggling framerate is exactly when it matters most.\n\t\tvar padding = MathF.Max( CullPadding, 0.0f );\n\n\t\t// Plane.GetDistance is dot( point, Normal ) - Distance, so w is negated to let the shader\n\t\t// use a plain dot( xyz, p ) \u002B w. Adding the padding there slides the plane outward.\n\t\t_planeScratch[0] = ToVector4( frustum.LeftPlane, padding );\n\t\t_planeScratch[1] = ToVector4( frustum.RightPlane, padding );\n\t\t_planeScratch[2] = ToVector4( frustum.TopPlane, padding );\n\t\t_planeScratch[3] = ToVector4( frustum.BottomPlane, padding );\n\t\t_planeScratch[4] = ToVector4( frustum.NearPlane, padding );\n\n\t\t// The far plane is left tight - MaxDistance already governs the far edge, and padding it\n\t\t// would only generate blades that fade out before they are ever visible.\n\t\t_planeScratch[5] = ToVector4( frustum.FarPlane, 0.0f );\n\n\t\t_frustumPlaneBuffer.SetData( _planeScratch );\n\n\t\tstatic Vector4 ToVector4( Plane plane, float padding ) =\u003E\n\t\t\tnew( plane.Normal.x, plane.Normal.y, plane.Normal.z, -plane.Distance \u002B padding );\n\t}\n\n\tprivate void ReleaseBuffers()\n\t{\n\t\t_chunkBuffer?.Dispose();\n\t\t_chunkBuffer = null;\n\n\t\t_cellBuffer?.Dispose();\n\t\t_cellBuffer = null;\n\n\t\t_visibleChunkBuffer?.Dispose();\n\t\t_visibleChunkBuffer = null;\n\n\t\t_frustumPlaneBuffer?.Dispose();\n\t\t_frustumPlaneBuffer = null;\n\n\t\t_bladeBuffer?.Dispose();\n\t\t_bladeBuffer = null;\n\n\t\t_argsBuffer?.Dispose();\n\t\t_argsBuffer = null;\n\n\t\t_chunkOrigins.Clear();\n\t\t_chunkCount = 0;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Called by the editor tool after painting, so the next frame re-uploads the density map.\n\t/// \u003C/summary\u003E\n\tpublic void MarkDirty() =\u003E _uploadedVersion = -1;\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tif ( !Gizmo.IsSelected || Storage is null || Storage.ChunkCount == 0 )\n\t\t\treturn;\n\n\t\tGizmo.Draw.Color = Color.Green.WithAlpha( 0.35f );\n\n\t\tforeach ( var (coord, _) in Storage.Chunks )\n\t\t{\n\t\t\tvar origin = GrassStorage.ChunkOrigin( coord );\n\t\t\tvar mins = new Vector3( origin.x, origin.y, 0 );\n\t\t\tvar maxs = new Vector3( origin.x \u002B GrassStorage.ChunkSize, origin.y \u002B GrassStorage.ChunkSize, 0 );\n\n\t\t\tGizmo.Draw.LineBBox( new BBox( WorldTransform.PointToLocal( mins ), WorldTransform.PointToLocal( maxs ) ) );\n\t\t}\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":"Code/GrassStorage.cs","FileName":"GrassStorage.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing Sandbox;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Sparse painted grass coverage, stored as a chunked grid of density samples rather than\n/// individual blade transforms. Blades are generated procedurally on the GPU from this data,\n/// so a square kilometre of dense grass costs a few megabytes instead of hundreds.\n/// \u003C/summary\u003E\npublic sealed class GrassStorage : BlobData\n{\n\tpublic override int Version =\u003E 1;\n\n\t/// \u003Csummary\u003ECells along one edge of a chunk.\u003C/summary\u003E\n\tpublic const int ChunkResolution = 64;\n\n\t/// \u003Csummary\u003EWorld-space size of a single density cell, in source units.\u003C/summary\u003E\n\tpublic const float CellSize = 32.0f;\n\n\t/// \u003Csummary\u003EWorld-space size of a chunk edge, in source units.\u003C/summary\u003E\n\tpublic const float ChunkSize = ChunkResolution * CellSize;\n\n\tpublic const int CellsPerChunk = ChunkResolution * ChunkResolution;\n\n\t/// \u003Csummary\u003E\n\t/// One density sample. Height and normal are baked when painting so grass sits on any\n\t/// geometry, not just terrain. Matches the HLSL \u003Cc\u003EGrassCell\u003C/c\u003E struct exactly.\n\t/// \u003C/summary\u003E\n\t[StructLayout( LayoutKind.Sequential )]\n\tpublic struct Cell\n\t{\n\t\tpublic float Height;\n\n\t\t/// \u003Csummary\u003Edensity (0-7) | normal.x (8-15) | normal.y (16-23) | unused (24-31)\u003C/summary\u003E\n\t\tpublic uint Packed;\n\n\t\tpublic readonly float Density =\u003E (Packed \u0026 0xFF) / 255.0f;\n\n\t\tpublic readonly Vector3 Normal\n\t\t{\n\t\t\tget\n\t\t\t{\n\t\t\t\tvar x = ((Packed \u003E\u003E 8) \u0026 0xFF) / 127.5f - 1.0f;\n\t\t\t\tvar y = ((Packed \u003E\u003E 16) \u0026 0xFF) / 127.5f - 1.0f;\n\t\t\t\tvar z = MathF.Sqrt( Math.Clamp( 1.0f - x * x - y * y, 0.0f, 1.0f ) );\n\t\t\t\treturn new Vector3( x, y, z );\n\t\t\t}\n\t\t}\n\n\t\tpublic static uint Pack( float density, Vector3 normal )\n\t\t{\n\t\t\tvar d = (uint)Math.Clamp( density * 255.0f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\tvar nx = (uint)Math.Clamp( (normal.x \u002B 1.0f) * 127.5f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\tvar ny = (uint)Math.Clamp( (normal.y \u002B 1.0f) * 127.5f \u002B 0.5f, 0.0f, 255.0f );\n\t\t\treturn d | (nx \u003C\u003C 8) | (ny \u003C\u003C 16);\n\t\t}\n\t}\n\n\tpublic readonly record struct ChunkCoord( int X, int Y );\n\n\tprivate readonly Dictionary\u003CChunkCoord, Cell[]\u003E _chunks = [];\n\n\t/// \u003Csummary\u003EBumped on every mutation so the renderer knows to re-upload its GPU buffers.\u003C/summary\u003E\n\tpublic int Revision { get; private set; }\n\n\tpublic int ChunkCount =\u003E _chunks.Count;\n\n\tpublic IReadOnlyDictionary\u003CChunkCoord, Cell[]\u003E Chunks =\u003E _chunks;\n\n\tpublic static ChunkCoord WorldToChunk( Vector3 world ) =\u003E new(\n\t\t(int)MathF.Floor( world.x / ChunkSize ),\n\t\t(int)MathF.Floor( world.y / ChunkSize ) );\n\n\tpublic static Vector2 ChunkOrigin( ChunkCoord coord ) =\u003E new( coord.X * ChunkSize, coord.Y * ChunkSize );\n\n\t/// \u003Csummary\u003EGlobal cell index on an axis. Negative world positions floor correctly.\u003C/summary\u003E\n\tprivate static int WorldToCell( float world ) =\u003E (int)MathF.Floor( world / CellSize );\n\n\tprivate static int FloorDiv( int a, int b ) =\u003E a \u003E= 0 ? a / b : ~(~a / b);\n\n\tprivate static int Mod( int a, int b )\n\t{\n\t\tvar r = a % b;\n\t\treturn r \u003C 0 ? r \u002B b : r;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Writes a density sample at a world position, baking the surface height and normal alongside it.\n\t/// Density of zero frees the sample.\n\t/// \u003C/summary\u003E\n\tpublic void SetCell( float worldX, float worldY, float density, float height, Vector3 normal )\n\t{\n\t\tvar cellX = WorldToCell( worldX );\n\t\tvar cellY = WorldToCell( worldY );\n\t\tvar coord = new ChunkCoord( FloorDiv( cellX, ChunkResolution ), FloorDiv( cellY, ChunkResolution ) );\n\n\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t{\n\t\t\tif ( density \u003C= 0.0f ) return;\n\n\t\t\tcells = new Cell[CellsPerChunk];\n\t\t\t_chunks[coord] = cells;\n\t\t}\n\n\t\tvar index = Mod( cellY, ChunkResolution ) * ChunkResolution \u002B Mod( cellX, ChunkResolution );\n\t\tcells[index] = new Cell { Height = height, Packed = Cell.Pack( density, normal ) };\n\t\tRevision\u002B\u002B;\n\t}\n\n\tpublic Cell GetCell( float worldX, float worldY )\n\t{\n\t\tvar cellX = WorldToCell( worldX );\n\t\tvar cellY = WorldToCell( worldY );\n\t\tvar coord = new ChunkCoord( FloorDiv( cellX, ChunkResolution ), FloorDiv( cellY, ChunkResolution ) );\n\n\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t\treturn default;\n\n\t\treturn cells[Mod( cellY, ChunkResolution ) * ChunkResolution \u002B Mod( cellX, ChunkResolution )];\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Reduces density in a radius, removing samples that reach zero.\n\t/// \u003C/summary\u003E\n\tpublic void Erase( Vector3 center, float radius, float strength )\n\t{\n\t\tvar radiusSq = radius * radius;\n\t\tvar minCellX = WorldToCell( center.x - radius );\n\t\tvar maxCellX = WorldToCell( center.x \u002B radius );\n\t\tvar minCellY = WorldToCell( center.y - radius );\n\t\tvar maxCellY = WorldToCell( center.y \u002B radius );\n\n\t\tfor ( var cy = minCellY; cy \u003C= maxCellY; cy\u002B\u002B )\n\t\t{\n\t\t\tfor ( var cx = minCellX; cx \u003C= maxCellX; cx\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar coord = new ChunkCoord( FloorDiv( cx, ChunkResolution ), FloorDiv( cy, ChunkResolution ) );\n\t\t\t\tif ( !_chunks.TryGetValue( coord, out var cells ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar wx = (cx \u002B 0.5f) * CellSize;\n\t\t\t\tvar wy = (cy \u002B 0.5f) * CellSize;\n\t\t\t\tvar dx = wx - center.x;\n\t\t\t\tvar dy = wy - center.y;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E radiusSq )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar index = Mod( cy, ChunkResolution ) * ChunkResolution \u002B Mod( cx, ChunkResolution );\n\t\t\t\tref var cell = ref cells[index];\n\t\t\t\tif ( (cell.Packed \u0026 0xFF) == 0 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar density = Math.Max( cell.Density - strength, 0.0f );\n\t\t\t\tcell.Packed = density \u003C= 0.0f ? 0u : Cell.Pack( density, cell.Normal );\n\t\t\t\tRevision\u002B\u002B;\n\t\t\t}\n\t\t}\n\n\t\tPruneEmptyChunks();\n\t}\n\n\tpublic void ClearAll()\n\t{\n\t\tif ( _chunks.Count == 0 ) return;\n\n\t\t_chunks.Clear();\n\t\tRevision\u002B\u002B;\n\t}\n\n\tprivate void PruneEmptyChunks()\n\t{\n\t\tList\u003CChunkCoord\u003E empty = null;\n\n\t\tforeach ( var (coord, cells) in _chunks )\n\t\t{\n\t\t\tvar used = false;\n\t\t\tfor ( var i = 0; i \u003C cells.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( (cells[i].Packed \u0026 0xFF) != 0 ) { used = true; break; }\n\t\t\t}\n\n\t\t\tif ( !used )\n\t\t\t{\n\t\t\t\tempty ??= [];\n\t\t\t\tempty.Add( coord );\n\t\t\t}\n\t\t}\n\n\t\tif ( empty is null ) return;\n\n\t\tforeach ( var coord in empty )\n\t\t\t_chunks.Remove( coord );\n\t}\n\n\tpublic override void Serialize( ref Writer writer )\n\t{\n\t\twriter.Stream.Write( _chunks.Count );\n\n\t\tforeach ( var (coord, cells) in _chunks )\n\t\t{\n\t\t\twriter.Stream.Write( coord.X );\n\t\t\twriter.Stream.Write( coord.Y );\n\n\t\t\tfor ( var i = 0; i \u003C CellsPerChunk; i\u002B\u002B )\n\t\t\t{\n\t\t\t\twriter.Stream.Write( cells[i].Height );\n\t\t\t\twriter.Stream.Write( cells[i].Packed );\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic override void Deserialize( ref Reader reader )\n\t{\n\t\t_chunks.Clear();\n\n\t\tvar chunkCount = reader.Stream.Read\u003Cint\u003E();\n\n\t\tfor ( var c = 0; c \u003C chunkCount; c\u002B\u002B )\n\t\t{\n\t\t\tvar coord = new ChunkCoord( reader.Stream.Read\u003Cint\u003E(), reader.Stream.Read\u003Cint\u003E() );\n\t\t\tvar cells = new Cell[CellsPerChunk];\n\n\t\t\tfor ( var i = 0; i \u003C CellsPerChunk; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcells[i].Height = reader.Stream.Read\u003Cfloat\u003E();\n\t\t\t\tcells[i].Packed = reader.Stream.Read\u003Cuint\u003E();\n\t\t\t}\n\n\t\t\t_chunks[coord] = cells;\n\t\t}\n\n\t\tRevision\u002B\u002B;\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":"GrassRenderer.cs","FileName":"GrassRenderer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\nusing Sandbox;\nusing Sandbox.Rendering;\nusing RenderStage = Sandbox.Rendering.Stage;\n\nnamespace RedSnail.GrassTool;\n\n/// \u003Csummary\u003E\n/// Renders a painted grass field. Each frame a compute pass expands the density map into blade\n/// instances for whatever is near the camera, then a single indirect draw renders them all.\n/// No blade ever exists on the CPU, and nothing is stored per-blade in the scene.\n/// \u003C/summary\u003E\n[Icon( \u0022grass\u0022 ), Group( \u0022Grass\u0022 ), Title( \u0022Grass Renderer\u0022 )]\npublic sealed class GrassRenderer : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer\n{\n\t[StructLayout( LayoutKind.Sequential )]\n\tprivate struct GpuChunk\n\t{\n\t\tpublic Vector2 Origin;\n\t\tpublic int CellOffset;\n\t\tpublic int Pad;\n\t}\n\n\t[StructLayout( LayoutKind.Sequential )]\n\tprivate struct GpuBlade\n\t{\n\t\tpublic Vector3 Position;\n\t\tpublic float Yaw;\n\t\tpublic Vector3 Normal;\n\t\tpublic float Height;\n\t\tpublic float Width;\n\t\tpublic float Tint;\n\t\tpublic float Phase;\n\t\tpublic float Pad; // keeps the struct at 48 bytes\n\t}\n\n\t// Must match GRASS_BLADE_* in grass_shared.fxc.\n\tprivate const int BladeStripVerts = 7;\n\tprivate const int BladeVertexCount = (BladeStripVerts - 2) * 3;\n\n\t// Byte offset of InstanceCount within IndirectDrawArguments. The struct is sequential\n\t// { uint VertexCount; uint InstanceCount; uint FirstVertex; uint FirstInstance; }, so this is\n\t// fixed at 4. Marshal.OffsetOf would express it directly but is outside the sandbox whitelist.\n\tprivate const int ArgsInstanceCountOffset = 4;\n\n\t[Property, Group( \u0022General\u0022 )]\n\tpublic GrassDefinition Definition { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Ceiling on blades alive at once. Each costs 48 bytes of GPU memory, so 500k is ~24 MB.\n\t/// Raise it if dense fields visibly clip out at the far edge of the render distance.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022General\u0022 ), Range( 50000, 4000000 )]\n\tpublic int MaxBlades { get; set; } = 500000;\n\n\t/// \u003Csummary\u003E\n\t/// How far outside the camera frustum blades are still generated, in world units. Culling is\n\t/// done against the frustum as it was when the frame started, so turning the camera brings in\n\t/// blades that were never generated. Raise this if they visibly stream in at the screen edges,\n\t/// which is most obvious at low or capped framerates.\n\t/// \u003C/summary\u003E\n\t[Property, Group( \u0022General\u0022 ), Range( 0, 4000 )]\n\tpublic float CullPadding { get; set; } = 1000.0f;\n\n\t/// \u003Csummary\u003EPainted coverage. Serialized as a binary blob, not JSON.\u003C/summary\u003E\n\t[Property, Hide]\n\tpublic GrassStorage Storage { get; set; } = new();\n\n\tprivate ComputeShader _generateShader;\n\tprivate Material _material;\n\tprivate CommandList _commandList;\n\n\tprivate GpuBuffer\u003CGpuChunk\u003E _chunkBuffer;\n\tprivate GpuBuffer\u003CGrassStorage.Cell\u003E _cellBuffer;\n\tprivate GpuBuffer\u003Cint\u003E _visibleChunkBuffer;\n\tprivate GpuBuffer\u003CVector4\u003E _frustumPlaneBuffer;\n\tprivate GpuBuffer\u003CGpuBlade\u003E _bladeBuffer;\n\tprivate GpuBuffer\u003CGpuBuffer.IndirectDrawArguments\u003E _argsBuffer;\n\n\tprivate readonly List\u003CVector2\u003E _chunkOrigins = [];\n\tprivate int[] _visibleScratch = [];\n\tprivate readonly Vector4[] _planeScratch = new Vector4[6];\n\n\tprivate CameraComponent _lastCamera;\n\tprivate int _chunkCount;\n\tprivate int _uploadedVersion = -1;\n\tprivate int _uploadedMaxBlades = -1;\n\n\tprotected override void OnEnabled()\n\t{\n\t\tStorage ??= new GrassStorage();\n\n\t\t_generateShader = new ComputeShader(\u0022grass_generate_cs\u0022);\n\t\t_material = Material.FromShader(\u0022grass\u0022);\n\t\t_commandList = new CommandList(\u0022Grass Rendering\u0022);\n\n\t\t// Force a full re-upload: the buffers were released on disable, so a matching version\n\t\t// number here would otherwise leave the compute pass reading freed resources.\n\t\t_uploadedVersion = -1;\n\t\t_uploadedMaxBlades = -1;\n\n\t\tRefreshBuffers();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\t_lastCamera?.RemoveCommandList(_commandList);\n\t\t_lastCamera = null;\n\n\t\t_commandList?.Reset();\n\t\t_commandList = null;\n\n\t\tReleaseBuffers();\n\n\t\t_generateShader = null;\n\t\t_material = null;\n\n\t\t_uploadedVersion = -1;\n\t\t_uploadedMaxBlades = -1;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tvar renderCamera = GetRenderCamera();\n\n\t\t// Re-attach when the camera changes, and also when the one we attached to stopped being\n\t\t// valid. Leaving play mode destroys the play camera without the reference here changing,\n\t\t// so comparing references alone would leave us bound to a dead camera forever.\n\t\tif (renderCamera != _lastCamera || !_lastCamera.IsValid())\n\t\t{\n\t\t\tif (_lastCamera.IsValid())\n\t\t\t\t_lastCamera.RemoveCommandList(_commandList);\n\n\t\t\t_lastCamera = null;\n\n\t\t\tif (renderCamera.IsValid())\n\t\t\t{\n\t\t\t\trenderCamera.AddCommandList(_commandList, RenderStage.AfterOpaque);\n\t\t\t\t_lastCamera = renderCamera;\n\t\t\t}\n\t\t}\n\n\t\t// Nothing to draw through until a camera exists. State is cleared above, so we pick one\n\t\t// up as soon as one appears.\n\t\tif (!_lastCamera.IsValid())\n\t\t\treturn;\n\n\t\t// Culling follows whichever camera the viewport actually looks through, which is not\n\t\t// necessarily the one replaying the list.\n\t\tvar cullCamera = GetCullCamera();\n\t\t\n\t\tif (!cullCamera.IsValid())\n\t\t\treturn;\n\n\t\tRefreshBuffers();\n\t\tRecordCommandList(cullCamera);\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The camera whose command list actually replays. A scene camera does so in the editor\n\t/// viewport as well as in game, so it wins when one exists; with an empty scene the editor\n\t/// camera is the only thing left that will replay ours.\n\t/// \u003C/summary\u003E\n\tprivate CameraComponent GetRenderCamera()\n\t{\n\t\tif (Scene.Camera.IsValid())\n\t\t\treturn Scene.Camera;\n\n\t\tif (Scene.IsEditor)\n\t\t\treturn Application.Editor?.Camera;\n\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The camera the blades are generated for. While editing this is the viewport camera, or\n\t/// nothing outside the game camera\u0027s frustum would ever be generated.\n\t/// \u003C/summary\u003E\n\tprivate CameraComponent GetCullCamera()\n\t{\n\t\tif (Scene.IsEditor)\n\t\t{\n\t\t\tvar editorCamera = Application.Editor?.Camera;\n\t\t\t\n\t\t\tif (editorCamera.IsValid())\n\t\t\t\treturn editorCamera;\n\t\t}\n\n\t\treturn Scene.Camera;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Re-packs painted chunks into GPU buffers. Skipped entirely unless the painted data or the\n\t/// blade budget actually changed.\n\t/// \u003C/summary\u003E\n\tprivate void RefreshBuffers()\n\t{\n\t\tif ( Storage is null || Storage.ChunkCount == 0 )\n\t\t{\n\t\t\tif ( _chunkCount != 0 )\n\t\t\t{\n\t\t\t\tReleaseBuffers();\n\t\t\t\t_chunkCount = 0;\n\t\t\t}\n\n\t\t\t_uploadedVersion = Storage?.Revision ?? -1;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _uploadedVersion == Storage.Revision \u0026\u0026 _uploadedMaxBlades == MaxBlades \u0026\u0026 _chunkBuffer is not null )\n\t\t\treturn;\n\n\t\tReleaseBuffers();\n\n\t\t_chunkCount = Storage.ChunkCount;\n\t\t_uploadedVersion = Storage.Revision;\n\t\t_uploadedMaxBlades = MaxBlades;\n\n\t\tvar chunks = new GpuChunk[_chunkCount];\n\t\tvar cells = new GrassStorage.Cell[_chunkCount * GrassStorage.CellsPerChunk];\n\n\t\t_chunkOrigins.Clear();\n\n\t\tvar index = 0;\n\t\tforeach ( var (coord, cellData) in Storage.Chunks )\n\t\t{\n\t\t\tvar origin = GrassStorage.ChunkOrigin( coord );\n\t\t\tvar offset = index * GrassStorage.CellsPerChunk;\n\n\t\t\tchunks[index] = new GpuChunk { Origin = origin, CellOffset = offset };\n\t\t\tArray.Copy( cellData, 0, cells, offset, GrassStorage.CellsPerChunk );\n\t\t\t_chunkOrigins.Add( origin );\n\n\t\t\tindex\u002B\u002B;\n\t\t}\n\n\t\t_chunkBuffer = new GpuBuffer\u003CGpuChunk\u003E( _chunkCount, GpuBuffer.UsageFlags.Structured );\n\t\t_chunkBuffer.SetData( chunks );\n\n\t\t_cellBuffer = new GpuBuffer\u003CGrassStorage.Cell\u003E( cells.Length, GpuBuffer.UsageFlags.Structured );\n\t\t_cellBuffer.SetData( cells );\n\n\t\t_visibleChunkBuffer = new GpuBuffer\u003Cint\u003E( _chunkCount, GpuBuffer.UsageFlags.Structured );\n\t\t_visibleScratch = new int[_chunkCount];\n\n\t\t_frustumPlaneBuffer = new GpuBuffer\u003CVector4\u003E( 6, GpuBuffer.UsageFlags.Structured );\n\n\t\t_bladeBuffer = new GpuBuffer\u003CGpuBlade\u003E( MaxBlades, GpuBuffer.UsageFlags.Structured | GpuBuffer.UsageFlags.Append );\n\n\t\t_argsBuffer = new GpuBuffer\u003CGpuBuffer.IndirectDrawArguments\u003E( 1, GpuBuffer.UsageFlags.IndirectDrawArguments );\n\t\t_argsBuffer.SetData( new[]\n\t\t{\n\t\t\tnew GpuBuffer.IndirectDrawArguments { VertexCount = BladeVertexCount }\n\t\t} );\n\t}\n\n\tprivate void RecordCommandList( CameraComponent camera )\n\t{\n\t\t_commandList.Reset();\n\n\t\tif ( _chunkCount == 0 || _bladeBuffer is null )\n\t\t\treturn;\n\n\t\tvar definition = Definition;\n\t\tvar cameraPosition = camera.WorldPosition;\n\n\t\tvar visibleCount = CollectVisibleChunks( cameraPosition, definition?.MaxDistance ?? 12000.0f );\n\t\tif ( visibleCount == 0 )\n\t\t\treturn;\n\n\t\tUploadFrustumPlanes( camera );\n\n\t\t_commandList.Attributes.Set( \u0022GrassChunks\u0022, _chunkBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassCells\u0022, _cellBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassVisibleChunks\u0022, _visibleChunkBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassFrustumPlanes\u0022, _frustumPlaneBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassBlades\u0022, _bladeBuffer );\n\t\t_commandList.Attributes.Set( \u0022GrassVisibleChunkCount\u0022, visibleCount );\n\t\t_commandList.Attributes.Set( \u0022GrassCameraPos\u0022, cameraPosition );\n\t\t_commandList.Attributes.Set( \u0022GrassTime\u0022, RealTime.Now );\n\n\t\tdefinition?.ApplyTo( _commandList.Attributes );\n\n\t\t_commandList.ResourceBarrierTransition( _bladeBuffer, ResourceState.UnorderedAccess );\n\t\t_commandList.SetCounterValue( _bladeBuffer, 0 );\n\n\t\t_commandList.DispatchCompute( _generateShader, visibleCount * GrassStorage.CellsPerChunk, 1, 1 );\n\n\t\t// The appends must all land before the counter is read into the draw arguments.\n\t\t_commandList.UavBarrier( _bladeBuffer );\n\n\t\t_commandList.ResourceBarrierTransition( _argsBuffer, ResourceState.CopyDestination );\n\t\t_commandList.CopyStructureCount( _bladeBuffer, _argsBuffer, ArgsInstanceCountOffset );\n\n\t\t_commandList.ResourceBarrierTransition( _bladeBuffer, ResourceState.GenericRead );\n\t\t_commandList.ResourceBarrierTransition( _argsBuffer, ResourceState.IndirectArgument );\n\n\t\t_commandList.DrawInstancedIndirect( _material, _argsBuffer );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Narrows the dispatch to chunks near the camera. Per-blade frustum culling happens on the\n\t/// GPU; this only has to be conservative.\n\t/// \u003C/summary\u003E\n\tprivate int CollectVisibleChunks( Vector3 cameraPosition, float maxDistance )\n\t{\n\t\t// A chunk\u0027s near corner can be in range while its centre isn\u0027t, hence the circumradius.\n\t\tvar cullRadius = maxDistance \u002B GrassStorage.ChunkSize * 0.7072f;\n\t\tvar cullRadiusSq = cullRadius * cullRadius;\n\n\t\tvar halfChunk = GrassStorage.ChunkSize * 0.5f;\n\t\tvar count = 0;\n\n\t\tfor ( var i = 0; i \u003C _chunkOrigins.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar origin = _chunkOrigins[i];\n\t\t\tvar dx = origin.x \u002B halfChunk - cameraPosition.x;\n\t\t\tvar dy = origin.y \u002B halfChunk - cameraPosition.y;\n\n\t\t\tif ( dx * dx \u002B dy * dy \u003E cullRadiusSq )\n\t\t\t\tcontinue;\n\n\t\t\t_visibleScratch[count\u002B\u002B] = i;\n\t\t}\n\n\t\tif ( count \u003E 0 )\n\t\t\t_visibleChunkBuffer.SetData( _visibleScratch.AsSpan( 0, count ) );\n\n\t\treturn count;\n\t}\n\n\tprivate void UploadFrustumPlanes( CameraComponent camera )\n\t{\n\t\tvar frustum = camera.GetFrustum();\n\n\t\t// These planes are sampled once on the CPU but the blades they cull are not drawn until the\n\t\t// frame presents, by which point the camera has kept turning. Pushing every plane outward\n\t\t// gives the generation something to work with at the screen edges - without it, blades\n\t\t// rotating into view were culled before they were ever needed, which reads as them\n\t\t// streaming in from the sides. The slack that buys scales with the frame time, so a capped\n\t\t// or struggling framerate is exactly when it matters most.\n\t\tvar padding = MathF.Max( CullPadding, 0.0f );\n\n\t\t// Plane.GetDistance is dot( point, Normal ) - Distance, so w is negated to let the shader\n\t\t// use a plain dot( xyz, p ) \u002B w. Adding the padding there slides the plane outward.\n\t\t_planeScratch[0] = ToVector4( frustum.LeftPlane, padding );\n\t\t_planeScratch[1] = ToVector4( frustum.RightPlane, padding );\n\t\t_planeScratch[2] = ToVector4( frustum.TopPlane, padding );\n\t\t_planeScratch[3] = ToVector4( frustum.BottomPlane, padding );\n\t\t_planeScratch[4] = ToVector4( frustum.NearPlane, padding );\n\n\t\t// The far plane is left tight - MaxDistance already governs the far edge, and padding it\n\t\t// would only generate blades that fade out before they are ever visible.\n\t\t_planeScratch[5] = ToVector4( frustum.FarPlane, 0.0f );\n\n\t\t_frustumPlaneBuffer.SetData( _planeScratch );\n\n\t\tstatic Vector4 ToVector4( Plane plane, float padding ) =\u003E\n\t\t\tnew( plane.Normal.x, plane.Normal.y, plane.Normal.z, -plane.Distance \u002B padding );\n\t}\n\n\tprivate void ReleaseBuffers()\n\t{\n\t\t_chunkBuffer?.Dispose();\n\t\t_chunkBuffer = null;\n\n\t\t_cellBuffer?.Dispose();\n\t\t_cellBuffer = null;\n\n\t\t_visibleChunkBuffer?.Dispose();\n\t\t_visibleChunkBuffer = null;\n\n\t\t_frustumPlaneBuffer?.Dispose();\n\t\t_frustumPlaneBuffer = null;\n\n\t\t_bladeBuffer?.Dispose();\n\t\t_bladeBuffer = null;\n\n\t\t_argsBuffer?.Dispose();\n\t\t_argsBuffer = null;\n\n\t\t_chunkOrigins.Clear();\n\t\t_chunkCount = 0;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Called by the editor tool after painting, so the next frame re-uploads the density map.\n\t/// \u003C/summary\u003E\n\tpublic void MarkDirty() =\u003E _uploadedVersion = -1;\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tif ( !Gizmo.IsSelected || Storage is null || Storage.ChunkCount == 0 )\n\t\t\treturn;\n\n\t\tGizmo.Draw.Color = Color.Green.WithAlpha( 0.35f );\n\n\t\tforeach ( var (coord, _) in Storage.Chunks )\n\t\t{\n\t\t\tvar origin = GrassStorage.ChunkOrigin( coord );\n\t\t\tvar mins = new Vector3( origin.x, origin.y, 0 );\n\t\t\tvar maxs = new Vector3( origin.x \u002B GrassStorage.ChunkSize, origin.y \u002B GrassStorage.ChunkSize, 0 );\n\n\t\t\tGizmo.Draw.LineBBox( new BBox( WorldTransform.PointToLocal( mins ), WorldTransform.PointToLocal( maxs ) ) );\n\t\t}\n\t}\n}\n"},{"Ident":"redsnail.grasstool","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":341332,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Grass Tool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022grasstool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022redsnail\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022redsnail.grasstool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002228\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-08-12T23:58:53.3546626Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.137.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.137.0\u0022)]"},{"Ident":"redsnail.grasstool","Path":"Editor/GrassTool.cs","FileName":"GrassTool.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":341332,"Code":"using System;\nusing System.Linq;\nusing Editor;\nusing Editor.TerrainEditor;\nusing Sandbox;\n\nnamespace RedSnail.GrassTool.Editor;\n\n/// \u003Csummary\u003E\n/// Paints grass coverage onto any surface. The brush writes density into the target\n/// \u003Csee cref=\u0022GrassRenderer\u0022/\u003E\u0027s density map, baking the surface height and normal under each\n/// cell so blades sit on whatever geometry is there. Hold Ctrl to erase.\n/// \u003C/summary\u003E\n[EditorTool( \u0022grass\u0022 )]\n[Title( \u0022Grass\u0022 )]\n[Icon( \u0022grass\u0022 )]\npublic sealed class GrassPaintTool : EditorTool\n{\n\tpublic BrushSettings BrushSettings { get; private set; } = new();\n\n\tprivate GrassRenderer _target;\n\tprivate bool _erasing;\n\tprivate bool _dragging;\n\tprivate bool _painted;\n\tprivate Vector3 _lastPaintPosition;\n\n\t// Repainting the same spot every frame just burns traces, so the brush has to travel a\n\t// fraction of its own radius before it deposits again.\n\tprivate float PaintStepDistance =\u003E BrushSettings.Size * 0.25f;\n\n\tpublic GrassPaintTool()\n\t{\n\t\tRebuildSidebarOnSelectionChange = false;\n\t}\n\n\tpublic override Widget CreateToolSidebar()\n\t{\n\t\tvar sidebar = new ToolSidebarWidget();\n\t\tsidebar.AddTitle( \u0022Grass Brush\u0022, \u0022brush\u0022 );\n\t\tsidebar.MinimumWidth = 300;\n\n\t\t{\n\t\t\tvar group = sidebar.AddGroup( \u0022Brush\u0022 );\n\t\t\tvar so = BrushSettings.GetSerialized();\n\t\t\tgroup.Add( ControlSheet.CreateRow( so.GetProperty( nameof( BrushSettings.Size ) ) ) );\n\t\t\tgroup.Add( ControlSheet.CreateRow( so.GetProperty( nameof( BrushSettings.Opacity ) ) ) );\n\t\t}\n\n\t\t{\n\t\t\tvar group = sidebar.AddGroup( \u0022Actions\u0022 );\n\n\t\t\tvar clear = new Button( \u0022Clear All Grass\u0022, \u0022delete_sweep\u0022 );\n\t\t\tclear.ToolTip = \u0022Remove every painted cell from the target Grass Renderer\u0022;\n\t\t\tclear.Clicked \u002B= () =\u003E\n\t\t\t{\n\t\t\t\tvar target = ResolveTarget();\n\t\t\t\tif ( !target.IsValid() || target.Storage is null )\n\t\t\t\t\treturn;\n\n\t\t\t\t// Wiping the density map throws away every stroke and there is no undo for it,\n\t\t\t\t// so this one gets a confirmation.\n\t\t\t\tDialog.AskConfirm(\n\t\t\t\t\t() =\u003E\n\t\t\t\t\t{\n\t\t\t\t\t\ttarget.Storage.ClearAll();\n\t\t\t\t\t\ttarget.MarkDirty();\n\t\t\t\t\t},\n\t\t\t\t\t\u0022Are you sure you want to delete all grass? This action cannot be undone.\u0022,\n\t\t\t\t\t\u0022Delete All Grass\u0022,\n\t\t\t\t\t\u0022Delete\u0022,\n\t\t\t\t\t\u0022Cancel\u0022 );\n\t\t\t};\n\t\t\tgroup.Add( clear );\n\t\t}\n\n\t\t// Soaks up the leftover height. Without it the column spreads the groups out to fill the\n\t\t// panel instead of stacking them at the top.\n\t\tsidebar.Layout.AddStretchCell();\n\n\t\treturn sidebar;\n\t}\n\n\tpublic override void OnUpdate()\n\t{\n\t\t_erasing = Gizmo.IsCtrlPressed;\n\n\t\tDrawBrushPreview();\n\n\t\tGizmo.Hitbox.BBox( BBox.FromPositionAndSize( Vector3.Zero, 999999 ) );\n\n\t\tif ( Gizmo.IsLeftMouseDown )\n\t\t{\n\t\t\tif ( !_dragging )\n\t\t\t{\n\t\t\t\t_dragging = true;\n\t\t\t\t_lastPaintPosition = Vector3.Zero;\n\t\t\t}\n\n\t\t\tOnPaintUpdate();\n\t\t}\n\t\telse if ( _dragging )\n\t\t{\n\t\t\t_dragging = false;\n\t\t\t_lastPaintPosition = Vector3.Zero;\n\n\t\t\tif ( _painted )\n\t\t\t{\n\t\t\t\tResolveTarget()?.MarkDirty();\n\t\t\t\t_painted = false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Uses the selected renderer if there is one, otherwise the only one in the scene. Creating\n\t/// it implicitly would leave stray components around every time someone opens the tool.\n\t/// \u003C/summary\u003E\n\tprivate GrassRenderer ResolveTarget()\n\t{\n\t\tvar selected = Selection\n\t\t\t.OfType\u003CGameObject\u003E()\n\t\t\t.Select( go =\u003E go.Components.Get\u003CGrassRenderer\u003E( FindMode.EnabledInSelfAndDescendants ) )\n\t\t\t.FirstOrDefault( r =\u003E r.IsValid() );\n\n\t\tif ( selected.IsValid() )\n\t\t{\n\t\t\t_target = selected;\n\t\t\treturn _target;\n\t\t}\n\n\t\tif ( _target.IsValid() )\n\t\t\treturn _target;\n\n\t\t_target = Scene.GetAllComponents\u003CGrassRenderer\u003E().FirstOrDefault();\n\t\treturn _target;\n\t}\n\n\tprivate void OnPaintUpdate()\n\t{\n\t\tvar target = ResolveTarget();\n\t\tif ( !target.IsValid() || target.Storage is null )\n\t\t\treturn;\n\n\t\tvar cursor = TraceCursor();\n\t\tif ( !cursor.Hit )\n\t\t\treturn;\n\n\t\tif ( _lastPaintPosition != Vector3.Zero \u0026\u0026\n\t\t\t Vector3.DistanceBetween( cursor.HitPosition, _lastPaintPosition ) \u003C PaintStepDistance )\n\t\t\treturn;\n\n\t\t_lastPaintPosition = cursor.HitPosition;\n\n\t\tvar radius = (float)BrushSettings.Size;\n\t\tvar strength = BrushSettings.Opacity;\n\n\t\tif ( _erasing )\n\t\t{\n\t\t\ttarget.Storage.Erase( cursor.HitPosition, radius, strength );\n\t\t\t_painted = true;\n\t\t\treturn;\n\t\t}\n\n\t\tPaintCells( target, cursor.HitPosition, radius, strength );\n\t\t_painted = true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Walks every density cell the brush touches and traces straight down onto the world to bake\n\t/// the surface height and normal. Blades then follow whatever they were painted onto.\n\t/// \u003C/summary\u003E\n\tprivate void PaintCells( GrassRenderer target, Vector3 center, float radius, float strength )\n\t{\n\t\tvar radiusSq = radius * radius;\n\n\t\tvar minX = (int)MathF.Floor( (center.x - radius) / GrassStorage.CellSize );\n\t\tvar maxX = (int)MathF.Floor( (center.x \u002B radius) / GrassStorage.CellSize );\n\t\tvar minY = (int)MathF.Floor( (center.y - radius) / GrassStorage.CellSize );\n\t\tvar maxY = (int)MathF.Floor( (center.y \u002B radius) / GrassStorage.CellSize );\n\n\t\t// Enough headroom to find the surface from above without punching through overhangs the\n\t\t// brush was never aimed at.\n\t\tvar traceHeight = radius \u002B 512.0f;\n\n\t\tfor ( var cy = minY; cy \u003C= maxY; cy\u002B\u002B )\n\t\t{\n\t\t\tfor ( var cx = minX; cx \u003C= maxX; cx\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar wx = (cx \u002B 0.5f) * GrassStorage.CellSize;\n\t\t\t\tvar wy = (cy \u002B 0.5f) * GrassStorage.CellSize;\n\n\t\t\t\tvar dx = wx - center.x;\n\t\t\t\tvar dy = wy - center.y;\n\t\t\t\tvar distSq = dx * dx \u002B dy * dy;\n\t\t\t\tif ( distSq \u003E radiusSq )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar from = new Vector3( wx, wy, center.z \u002B traceHeight );\n\t\t\t\tvar to = new Vector3( wx, wy, center.z - traceHeight );\n\n\t\t\t\tvar tr = Scene.Trace.Ray( from, to )\n\t\t\t\t\t.UseRenderMeshes( true )\n\t\t\t\t\t.WithTag( \u0022solid\u0022 )\n\t\t\t\t\t.Run();\n\n\t\t\t\tif ( !tr.Hit )\n\t\t\t\t\tcontinue;\n\n\t\t\t\t// Soft edge, so overlapping strokes build up smoothly instead of leaving a disc.\n\t\t\t\tvar falloff = 1.0f - MathF.Sqrt( distSq ) / radius;\n\t\t\t\tvar added = strength * MathF.Pow( falloff, 0.5f );\n\n\t\t\t\tvar existing = target.Storage.GetCell( wx, wy ).Density;\n\t\t\t\tvar density = Math.Clamp( existing \u002B added, 0.0f, 1.0f );\n\n\t\t\t\ttarget.Storage.SetCell( wx, wy, density, tr.HitPosition.z, tr.Normal );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate SceneTraceResult TraceCursor() =\u003E\n\t\tScene.Trace.Ray( Gizmo.CurrentRay, 100000 )\n\t\t\t.UseRenderMeshes( true )\n\t\t\t.WithTag( \u0022solid\u0022 )\n\t\t\t.Run();\n\n\tprivate void DrawBrushPreview()\n\t{\n\t\tvar tr = TraceCursor();\n\t\tif ( !tr.Hit )\n\t\t\treturn;\n\n\t\tusing ( Gizmo.Scope( \u0022GrassBrush\u0022 ) )\n\t\t{\n\t\t\tGizmo.Draw.Color = _erasing\n\t\t\t\t? Color.FromBytes( 250, 150, 150 )\n\t\t\t\t: Color.FromBytes( 150, 250, 160 );\n\n\t\t\tGizmo.Draw.LineCircle( tr.HitPosition \u002B tr.Normal * 1.0f, tr.Normal, BrushSettings.Size );\n\t\t\tGizmo.Draw.LineCircle( tr.HitPosition \u002B tr.Normal * 1.0f, tr.Normal, BrushSettings.Size * 0.5f );\n\t\t}\n\t}\n}\n"}]}