Skip to content

Commit 38f49aa

Browse files
committed
terrain: death stranding micro pebbles (real geometry, not parallax)
1 parent eff8524 commit 38f49aa

18 files changed

Lines changed: 755 additions & 255 deletions

data/shaders/common_resources_gpu_driven.hlsl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ RWStructuredBuffer<uint> restir_pairing : register(u57);
146146
// declared rw to match the engine pattern for per-pass structured buffers but treated as read-only
147147
RWStructuredBuffer<EmissiveTriangle> emissive_triangles : register(u49);
148148

149-
// gpu procedural grass, written each frame by grass_populate.hlsl and consumed by the grass raster vs
150-
// grass_instances is the transient ring buffer, partitioned into one section per lod via lod_base in the push constant
151-
// grass_count holds one atomic counter per lod, bumped by interlockedadd during the populate dispatch
152-
// grass_indirect_args holds one DrawIndexedIndirect args entry per lod, the args compute reads grass_count
149+
// gpu scatter, grass and micro detail, written each frame by grass_populate.hlsl and consumed by the scatter raster vs
150+
// grass_instances is the transient pool, partitioned into one section per slot per lod via lod_base in the push constant
151+
// grass_count holds one atomic counter per slot per lod, bumped by interlockedadd during the populate dispatch
152+
// grass_indirect_args holds one DrawIndexedIndirect args entry per slot per lod, the args compute reads grass_count
153153
// and bakes index_count / first_index / vertex_offset / first_instance from the per-lod constants
154154
RWStructuredBuffer<GrassInstance> grass_instances : register(u50);
155155
RWStructuredBuffer<uint> grass_count : register(u51);

data/shaders/grass_indirect_args.hlsl

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,24 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2727
//
2828
// reads the per-lod atomic counters bumped by grass_populate.hlsl and clamps them into the
2929
// instance_count field of the per-lod IndirectDrawArgs entries. the other fields (index_count,
30-
// first_index, vertex_offset, first_instance) were baked once on the cpu in EnableProceduralGrass
30+
// first_index, vertex_offset, first_instance) were baked once on the cpu in EnableGpuScatter
3131
// from the grass mesh's per-lod offsets in the global geometry buffer.
3232
//
33-
// the args buffer holds renderer_max_grass_lod_count entries laid out contiguously, one per lod,
34-
// the raster draw calls offset into it by lod_index * sizeof(IndirectDrawArgs) to fetch theirs.
33+
// the args buffer holds one entry per scatter slot per lod laid out slot major, the raster draw calls
34+
// offset into it by (slot * lod_count + lod) * sizeof(IndirectDrawArgs) to fetch theirs. this runs once
35+
// per slot, the cpu pushes the caps of that slot and the index of its first entry.
3536
//
3637
// push constant layout (PassBufferData.values):
3738
// values[0].xyz = per-lod max_instances cap, one float per lod (must match the populate shader's
3839
// per-lod cap that the cpu pushed into values[0].w of grass_populate.hlsl)
3940
// values[0].w = lod_count
41+
// draw_index = the first args entry of this slot
4042

4143
[numthreads(8, 1, 1)]
4244
void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
4345
{
4446
uint lod_count = (uint)buffer_pass.values[0].w;
47+
uint slot_base = buffer_pass.draw_index;
4548

4649
uint lod = dispatch_thread_id.x;
4750
if (lod >= lod_count)
@@ -51,9 +54,10 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID)
5154
float caps[3] = { buffer_pass.values[0].x, buffer_pass.values[0].y, buffer_pass.values[0].z };
5255
uint max_instances_per_lod = (uint)caps[lod];
5356

54-
// clamp the atomic counter so the raster never tries to draw more blades than the slot can hold
55-
uint instance_count = min(grass_count[lod], max_instances_per_lod);
57+
// clamp the atomic counter so the raster never tries to draw more instances than the range can hold
58+
uint entry = slot_base + lod;
59+
uint instance_count = min(grass_count[entry], max_instances_per_lod);
5660

5761
// only the instance_count is dynamic, the rest of the args stay frozen at the values the cpu wrote
58-
grass_indirect_args[lod].instance_count = instance_count;
62+
grass_indirect_args[entry].instance_count = instance_count;
5963
}

data/shaders/grass_populate.hlsl

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3031
static const float grass_cull_half_height = 0.5f;
3132
static 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

data/shaders/taau.hlsl

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ static const float box_widen_static = 0.5f;
3131
static const float box_pad_relative = 0.08f;
3232
static const float box_pad_absolute = 0.002f;
3333
static const float box_pad_hdr = 0.2f;
34-
// relative depth, same surface, jitter and precision only
34+
// relative depth tolerance for same surface
3535
static const float reuse_depth_tol = 0.02f;
3636

3737
float3 tonemap_for_taa(float3 c)
@@ -109,8 +109,7 @@ float3 clip_to_aabb(float3 box_min, float3 box_max, float3 history)
109109
return (ratio > 1.0f) ? (center + offset * rcp(ratio)) : history;
110110
}
111111

112-
// sky has no gbuffer velocity, rebuild camera rotation at infinity so history
113-
// tracks the dome instead of smearing into curved ghost trails
112+
// sky has no gbuffer velocity, rebuild camera rotation at infinity
114113
float2 compute_sky_velocity(float2 uv)
115114
{
116115
matrix vp_curr = pass_is_right_eye() ?
@@ -133,9 +132,7 @@ float2 compute_sky_velocity(float2 uv)
133132
return curr_clip.xy / max(curr_clip.w, 1e-6f) - prev_clip.xy / max(prev_clip.w, 1e-6f);
134133
}
135134

136-
// expected previous depth of this surface versus what was actually stored,
137-
// sampled at the reprojected jittered uv so a still camera is not compared
138-
// against the wrong texel
135+
// compare expected previous depth of this surface against what was stored
139136
float compute_history_reuse(int2 px_render, float2 res_render, int2 px_render_max, float2 velocity_ndc)
140137
{
141138
float depth_raw = tex_depth[px_render].r;
@@ -163,8 +160,7 @@ float compute_history_reuse(int2 px_render, float2 res_render, int2 px_render_ma
163160
return 1.0f;
164161
}
165162

166-
// mismatch, keep only if a still neighbour owns that previous depth, jitter
167-
// across a static silhouette, a moving neighbour is an occluder that left
163+
// mismatch, keep only if a still neighbour owns that previous depth
168164
static const float still_px = 0.5f;
169165
static const int2 n4[4] =
170166
{
@@ -230,7 +226,7 @@ float3 taau(uint2 px_out, float2 res_out)
230226
int2 closest_px = center;
231227
float closest_depth = -1.0f;
232228

233-
// 3x3 neighborhood for reconstruct + near aabb, corners only for the wide clamp
229+
// corners widen the near aabb built from the 3x3
234230
static const int2 wide_corners[4] =
235231
{
236232
int2(-2, -2), int2( 2, -2),
@@ -297,7 +293,7 @@ float3 taau(uint2 px_out, float2 res_out)
297293
bool current_valid = weight_sum > 0.0f;
298294
current_rgb_tm = current_valid ? current_rgb_tm * rcp(weight_sum) : 0.0f.xxx;
299295

300-
// dilated velocity is for history uv only, the reuse test must use this pixel
296+
// dilated velocity is for the history uv only, the reuse test uses this pixel
301297
float center_depth = tex_depth[center].r;
302298
bool is_sky = center_depth < 1e-4f;
303299
float2 velocity_center = tex_velocity[center].xy;
@@ -315,7 +311,6 @@ float3 taau(uint2 px_out, float2 res_out)
315311

316312
if (!current_valid)
317313
{
318-
// no current signal, try history at this uv
319314
float3 fallback = tex.SampleLevel(samplers[sampler_bilinear_clamp], uv_out, 0.0f).rgb;
320315
return saturate_16(max(fallback, 0.0f.xxx));
321316
}
@@ -328,7 +323,7 @@ float3 taau(uint2 px_out, float2 res_out)
328323
float motion_px = length(velocity_uv * res_out);
329324
float motion = saturate(motion_px * rcp(motion_px_full));
330325

331-
// always test, a revealed sky pixel has no velocity so a motion gate would keep the ghost
326+
// never gate on motion, a revealed sky pixel has no velocity
332327
float reuse = compute_history_reuse(center, active_render_f, px_render_max, velocity_center);
333328
if (reuse < 1.0f)
334329
{
@@ -345,8 +340,7 @@ float3 taau(uint2 px_out, float2 res_out)
345340
float3 rgb_min = lerp(rgb_min_near, rgb_min_wide, widen);
346341
float3 rgb_max = lerp(rgb_max_near, rgb_max_wide, widen);
347342

348-
// the reinhard curve compresses highlights, so a fixed tolerance in tonemapped space is a
349-
// tiny tolerance in linear space, this restores a constant relative tolerance for bright taps
343+
// reinhard compresses highlights, restore a constant relative tolerance for bright taps
350344
float box_level = max3(rgb_max);
351345
float pad_hdr = box_pad_hdr * box_level * (1.0f - box_level);
352346
float3 box_pad = max((rgb_max - rgb_min) * box_pad_relative, max(box_pad_absolute, pad_hdr));
@@ -358,8 +352,7 @@ float3 taau(uint2 px_out, float2 res_out)
358352

359353
float blend_base = lerp(blend_static, blend_motion, motion);
360354

361-
// measured back in linear, a small wobble at the top of the reinhard curve is a large
362-
// brightness swing and that is what makes saturated highlights read as unstable
355+
// measure in linear, a small wobble at the top of the curve is a large brightness swing
363356
float curr_l = max3(tonemap_for_taa_inv(current_rgb_tm));
364357
float hist_l = max3(tonemap_for_taa_inv(history_clipped_tm));
365358
float disagree = abs(curr_l - hist_l) * rcp(max(max(curr_l, hist_l), 0.1f));

0 commit comments

Comments
 (0)