@@ -24,13 +24,15 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2424#include "common_culling.hlsl"
2525//============================
2626
27- // conservative world-space bound used to cull a blade against the camera frustum and the occluder hi-z
28- // covers the tallest scaled blade plus wind sway and the intra-cell scatter, generous enough that no
29- // on-screen blade is ever wrongly rejected so visible density stays identical
27+ // conservative world-space bound used to cull an instance against the camera frustum and the occluder
28+ // hi-z, covers the tallest scaled instance plus wind sway and the intra-cell scatter, generous enough
29+ // that nothing on screen is ever wrongly rejected so visible density stays identical. both are scaled
30+ // by the slot's largest instance, a stone chip a few centimetres across does not need a metre of slack
3031static const float grass_cull_half_height = 0.5f ;
3132static const float grass_cull_radius = 1.5f ;
3233
33- // gpu procedural grass placement (ghost of tsushima style)
34+ // gpu scatter placement (ghost of tsushima style), slot 0 is grass and the higher slots are micro
35+ // detail, pebbles and chips, the same ring machinery with a different mesh and a tighter reach
3436//
3537// one thread per cell in a 2d ring around the camera, the ring lives entirely inside grass_instances
3638// each accepted cell is atomically committed into the per-lod section of the buffer and the per-lod
@@ -44,6 +46,12 @@ static const float grass_cull_radius = 1.5f;
4446// values[0] = (cell_size, ring_radius, lod_base_in_instances, max_instances_per_lod)
4547// values[1] = (height_min, height_max, max_slope_cos, inner_radius)
4648// values[2] = (map_origin_x, map_origin_z, map_inv_size_x, map_inv_size_z)
49+ // every float is taken, so the rest travels in the bits of draw_index:
50+ // bits 0-3 lod ring
51+ // bits 4-7 scatter slot
52+ // bits 8-11 prop mask channel, 0 grass, 1 trees, 2 rocks
53+ // bits 12-19 packed instance scale at the small end of the size range
54+ // bits 20-27 packed instance scale at the large end
4755// camera xz comes from buffer_frame
4856// terrain height is r32 local y bound to tex (t7), material_index bitcast is the entity y
4957// biome_min arrives asfloat(is_transparent), negative disables the gate
@@ -189,7 +197,22 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
189197 float height_max = buffer_pass.values[1 ].y;
190198 float max_slope_cos = buffer_pass.values[1 ].z;
191199 float inner_radius = buffer_pass.values[1 ].w;
192- uint lod_index = buffer_pass.draw_index;
200+
201+ uint packed_index = buffer_pass.draw_index;
202+ uint lod_index = packed_index & 0xFu;
203+ uint slot_index = (packed_index >> 4 ) & 0xFu;
204+ uint mask_channel = (packed_index >> 8 ) & 0xFu;
205+ float scale_01_min = float ((packed_index >> 12 ) & 0xFFu) / 255.0f ;
206+ float scale_01_max = float ((packed_index >> 20 ) & 0xFFu) / 255.0f ;
207+
208+ // the counters and the indirect args are laid out slot major, matches renderer_gpu_scatter_arg_index
209+ uint count_index = slot_index * 3u + lod_index;
210+
211+ // the cull sphere has to grow with the instance, compose_instance_transform maps the packed scale
212+ // logarithmically over 0.01 to 100 and the raster will unpack it exactly the same way
213+ float largest_scale = exp2 (lerp (-6.643856f , 6.643856f , scale_01_max));
214+ float cull_radius = grass_cull_radius * largest_scale;
215+ float cull_height = grass_cull_half_height * largest_scale;
193216
194217 float2 camera_xz_anchor = get_camera_position ().xz;
195218
@@ -226,8 +249,14 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
226249 // hash combines the cell coordinates, the blade index inside the cell, and the lod seed,
227250 // each (cell, blade) pair gets an independent stable hash, no two blades collide and there is
228251 // no per-cell pattern because blade_index decorrelates blades that share a cell
252+ // the slot is part of the seed, otherwise every slot would stack its instances on the exact same
253+ // points and a pebble would be born inside every blade of grass
229254 uint blade_index = dispatch_thread_id.z;
230- uint h0 = hash_u32 ((uint )world_cell_x, (uint )world_cell_z, lod_index * 2654435761u + blade_index * 0x9e3779b9u);
255+ uint h0 = hash_u32 (
256+ (uint )world_cell_x,
257+ (uint )world_cell_z,
258+ lod_index * 2654435761u + blade_index * 0x9e3779b9u + slot_index * 0x85ebca6bu
259+ );
231260 float jx = hash_unit (h0);
232261 float jz = hash_unit (h0 * 16807u + 1u);
233262 float ys = hash_unit (h0 * 48271u + 3u);
@@ -312,17 +341,17 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
312341 // visibility cull, frustum + occluder hi-z on a conservative blade sphere, only blades the camera
313342 // can actually see survive so on-screen density is unchanged while off-screen and occluded blades
314343 // are dropped before the expensive slope taps, vertex shading and the atomic allocate
315- float3 blade_center = float3 (world_xz.x, world_y + grass_cull_half_height , world_xz.y);
344+ float3 blade_center = float3 (world_xz.x, world_y + cull_height , world_xz.y);
316345 float4 plane_l, plane_r, plane_b, plane_t;
317346 get_frustum_side_planes (plane_l, plane_r, plane_b, plane_t);
318- if (!sphere_in_side_planes (blade_center, grass_cull_radius , plane_l, plane_r, plane_b, plane_t))
347+ if (!sphere_in_side_planes (blade_center, cull_radius , plane_l, plane_r, plane_b, plane_t))
319348 return ;
320349
321350 // hi-z arrives on tex2, derive the max mip from the texture so no extra push constant is needed
322351 // the sphere test also rejects blades behind the camera (the side planes alone do not)
323352 uint hiz_w, hiz_h, hiz_mips;
324353 tex2.GetDimensions (0 , hiz_w, hiz_h, hiz_mips);
325- if (!sphere_hiz_visible (tex2, blade_center, grass_cull_radius , float (hiz_mips - 1u)))
354+ if (!sphere_hiz_visible (tex2, blade_center, cull_radius , float (hiz_mips - 1u)))
326355 return ;
327356
328357 // slope reject, two forward taps reusing the centre height, paid only by visible blades
@@ -336,7 +365,7 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
336365 return ;
337366 }
338367
339- // biome grass mask, same uv as the heightfield, r channel is grass suitability
368+ // biome mask, same uv as the heightfield, the slot picks the channel it is gated on
340369 // is_transparent carries biome_min as float bits, negative disables the gate
341370 float biome_min = asfloat (buffer_pass.is_transparent);
342371 if (biome_min >= 0.0f )
@@ -350,7 +379,8 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
350379 terrain_world_to_normalized (world_xz),
351380 float2 (mask_w, mask_h)
352381 );
353- float grass_w = tex3.SampleLevel (samplers[sampler_bilinear_clamp], mask_uv, 0 ).r;
382+ float4 mask = tex3.SampleLevel (samplers[sampler_bilinear_clamp], mask_uv, 0 );
383+ float grass_w = mask_channel == 0u ? mask.r : (mask_channel == 1u ? mask.g : mask.b);
354384 float slope_fit = saturate ((surface_normal.y - max_slope_cos) / max (1.0f - max_slope_cos, 1e-4f ));
355385 grass_w *= slope_fit;
356386 if (grass_w < biome_min)
@@ -365,14 +395,15 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
365395 }
366396 }
367397
368- // keep the authored blade proportions within a natural range
369- float scale_01 = 0.5f + (sc - 0.5f ) * 0.05f ;
398+ // one roll inside the authored size range, the ends arrive already packed the way the raster
399+ // unpacks them so no size ever leaves the range the layer asked for
400+ float scale_01 = lerp (scale_01_min, scale_01_max, sc);
370401
371402 // atomic-allocate a slot inside this lod's section, bail out cleanly once full
372403 // blades_per_cell was sized from cells_in_ring with floor() so the upper bound on writes is
373404 // floor(cap / cells_in_ring) * cells_in_ring <= cap, the clamp here is just a defensive guard
374405 uint local_slot;
375- InterlockedAdd (grass_count[lod_index ], 1u, local_slot);
406+ InterlockedAdd (grass_count[count_index ], 1u, local_slot);
376407 if (local_slot >= max_instances_per_lod)
377408 return ;
378409
0 commit comments