oriverse-engine 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"modules":[{"label":"shaders/bone_palette_compute_sampling.wgsl","code":"// bone_palette_compute_sampling.wgsl\n//\n// Step 2:\n// - Sample clip TRS on GPU from packed i16 data (per clip, per frame, per node)\n// - Interpolate between frames (smooth animation)\n// - Accumulate hierarchy in DFS order using a fixed-depth stack\n// - Write bone palette (mat4) compatible with existing VS/Shadow paths\n//\n\nstruct BoneComputeUniform {\n node_count: u32,\n joint_count: u32,\n instance_count: u32,\n clip_count: u32,\n // Mesh tail (inv(T(pos_shift)) * S(base_scale)) for the upper-body\n // aim overlay: aim_mat = tail_inv * R_glam * tail (applied to upper-body palettes).\n tail: mat4x4<f32>,\n tail_inv: mat4x4<f32>,\n // Sim frame id: playing-clip percent derives from it (see derive_percent), so steady\n // loopers advance with zero per-frame CPU/upload work.\n sim_frame_id: u32,\n // Max node depth + 1 for this skeleton (level-sweep loop bound; unused by the legacy\n // serial kernel).\n level_count: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n// Mirrors AnimationBaseComponent::animation_percent_progress (sim_base.rs): progress_data is\n// start_frame_id while playing, an Fp-raw percent (16 fractional bits) when paused/speed-0;\n// anim_data packs looping/paused/autoplay bits + Fp-raw speed in the low 28 bits. The phase\n// de-sync offset applies to looping+autoplay clips only, matching the CPU path it replaces.\nconst ANIM_LOOPING_BIT: u32 = 0x80000000u;\nconst ANIM_PAUSED_BIT: u32 = 0x40000000u;\nconst ANIM_AUTOPLAY_BIT: u32 = 0x10000000u;\nconst ANIM_SPEED_MASK: u32 = 0x0FFFFFFFu;\nconst FP_ONE: f32 = 65536.0;\n\nfn derive_percent(progress_data: u32, anim_data: u32, duration_frames: u32, phase: f32) -> f32 {\n let speed_raw = anim_data & ANIM_SPEED_MASK;\n let looping = (anim_data & ANIM_LOOPING_BIT) != 0u;\n let paused = (anim_data & ANIM_PAUSED_BIT) != 0u;\n var p: f32;\n if (paused || speed_raw == 0u) {\n p = f32(progress_data) / FP_ONE; // literal Fp-raw percent\n } else {\n let elapsed = f32(u.sim_frame_id - progress_data); // u32 wrap-diff like the CPU path\n let speed = f32(speed_raw) / FP_ONE;\n p = 100.0 * speed * elapsed / max(f32(duration_frames), 0.01);\n }\n if (looping) {\n p = p % 100.0;\n let autoplaying = (anim_data & ANIM_AUTOPLAY_BIT) != 0u;\n if (autoplaying) {\n p = (p + phase) % 100.0;\n if (p < 0.0) { p += 100.0; }\n }\n }\n return p;\n}\n\nstruct ClipInfo {\n base_u32: u32, // offset into clip_trs_u32 (u32 units)\n frame_count: u32, // number of frames in this clip (>=1)\n ready: u32, // 1 if data present\n _pad0: u32,\n trans_center: vec4<f32>,\n trans_extent: vec4<f32>,\n scale_center: vec4<f32>,\n scale_extent: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u: BoneComputeUniform;\n@group(0) @binding(1) var<storage, read> anim_misc: array<vec4<u32>>; // nine vec4s per instance\n\n@group(0) @binding(2) var<storage, read> rest_local: array<mat4x4<f32>>; // per node (DFS order)\n@group(0) @binding(3) var<storage, read> node_depth: array<u32>; // per node (DFS order)\n@group(0) @binding(4) var<storage, read> node_to_joint: array<u32>; // per node -> joint index or 0xffffffff\n@group(0) @binding(5) var<storage, read> inverse_bind: array<mat4x4<f32>>; // per joint\n\n@group(0) @binding(6) var<storage, read> clip_infos: array<ClipInfo>;\n@group(0) @binding(7) var<storage, read> clip_trs_u32: array<u32>; // packed i16 TRS\n\n@group(0) @binding(8) var<storage, read_write> palette: array<mat4x4<f32>>;\n\nconst I16_SCALE: f32 = 1.0 / 32767.0;\n// Low 8 bits = depth (census max 110 nodes, depth far under 256); bits 26..16 = parent's\n// DFS index (0x7ff sentinel = root). The old 16-bit depth mask shrank to make parent room;\n// flag bits 31..27 unchanged. LOCKSTEP with gltf_anim::pack_node_depth_flags.\nconst NODE_DEPTH_MASK: u32 = 0x000000ffu;\nconst NODE_PARENT_SHIFT: u32 = 16u;\nconst NODE_PARENT_MASK: u32 = 0x7ffu;\nconst NODE_PARENT_ROOT: u32 = 0x7ffu;\nconst NODE_UPPER_BODY_BIT: u32 = 0x80000000u;\nconst NODE_ARM_BIT: u32 = 0x40000000u;\nconst NODE_COLLAPSE_SIDE_BIT: u32 = 0x20000000u;\nconst NODE_ARM_ROOT_L_BIT: u32 = 0x10000000u;\nconst NODE_ARM_ROOT_R_BIT: u32 = 0x08000000u;\nfn i16_from_u16(x: u32) -> i32 {\n // Interpret lower 16 bits as signed i16 (manual sign-extend; WGSL has no i16/u16 here)\n let u = (x & 0xffffu) << 16u;\n return i32(u) >> 16;\n}\nfn unpack_i16x2(p: u32) -> vec2<i32> {\n let lo = i16_from_u16(p);\n let hi = i16_from_u16(p >> 16u);\n return vec2<i32>(lo, hi);\n}\n\nfn decode_vec3(center: vec4<f32>, extent: vec4<f32>, p0: u32, p1: u32) -> vec3<f32> {\n let a = unpack_i16x2(p0);\n let b = unpack_i16x2(p1);\n let nx = f32(a.x) * I16_SCALE;\n let ny = f32(a.y) * I16_SCALE;\n let nz = f32(b.x) * I16_SCALE;\n return center.xyz + extent.xyz * vec3<f32>(nx, ny, nz);\n}\n\nfn decode_quat(p0: u32, p1: u32) -> vec4<f32> {\n let a = unpack_i16x2(p0);\n let b = unpack_i16x2(p1);\n var q = vec4<f32>(\n f32(a.x) * I16_SCALE,\n f32(a.y) * I16_SCALE,\n f32(b.x) * I16_SCALE,\n f32(b.y) * I16_SCALE,\n );\n // Normalize to counter quantization drift\n let len2 = max(dot(q, q), 1e-8);\n q = q * inverseSqrt(len2);\n return q;\n}\n\nfn quat_nlerp(a: vec4<f32>, b_in: vec4<f32>, t: f32) -> vec4<f32> {\n var b = b_in;\n if (dot(a, b) < 0.0) { b = -b; }\n var q = mix(a, b, t);\n let len2 = max(dot(q, q), 1e-8);\n q = q * inverseSqrt(len2);\n return q;\n}\n\nfn quat_to_mat3(q: vec4<f32>) -> mat3x3<f32> {\n let x = q.x; let y = q.y; let z = q.z; let w = q.w;\n let x2 = x + x; let y2 = y + y; let z2 = z + z;\n let xx = x * x2; let yy = y * y2; let zz = z * z2;\n let xy = x * y2; let xz = x * z2; let yz = y * z2;\n let wx = w * x2; let wy = w * y2; let wz = w * z2;\n return mat3x3<f32>(\n vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),\n vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),\n vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy)),\n );\n}\n\nfn mat4_from_srt(s: vec3<f32>, q: vec4<f32>, t: vec3<f32>) -> mat4x4<f32> {\n let r = quat_to_mat3(q);\n return mat4x4<f32>(\n vec4<f32>(r[0] * s.x, 0.0),\n vec4<f32>(r[1] * s.y, 0.0),\n vec4<f32>(r[2] * s.z, 0.0),\n vec4<f32>(t, 1.0),\n );\n}\n\n// Replace the rotation of a local TRS matrix, keeping its sampled translation and scale\n// (left-hand IK override: CPU pre-blends animated->IK rotation, GPU swaps it in).\nfn override_local_rot(m: mat4x4<f32>, q: vec4<f32>) -> mat4x4<f32> {\n let s = vec3<f32>(length(m[0].xyz), length(m[1].xyz), length(m[2].xyz));\n let r = quat_to_mat3(q);\n return mat4x4<f32>(\n vec4<f32>(r[0] * s.x, 0.0),\n vec4<f32>(r[1] * s.y, 0.0),\n vec4<f32>(r[2] * s.z, 0.0),\n m[3],\n );\n}\n\n// Rotation quat from a (possibly scaled) column-major matrix: columns normalized first.\n// Inverse of quat_to_mat3 (standard trace method with the same index convention).\nfn mat3_rot_quat(m: mat4x4<f32>) -> vec4<f32> {\n let c0 = normalize(m[0].xyz);\n let c1 = normalize(m[1].xyz);\n let c2 = normalize(m[2].xyz);\n let tr = c0.x + c1.y + c2.z;\n var q: vec4<f32>;\n if (tr > 0.0) {\n let s = sqrt(tr + 1.0) * 2.0;\n q = vec4<f32>((c1.z - c2.y) / s, (c2.x - c0.z) / s, (c0.y - c1.x) / s, 0.25 * s);\n } else if (c0.x > c1.y && c0.x > c2.z) {\n let s = sqrt(1.0 + c0.x - c1.y - c2.z) * 2.0;\n q = vec4<f32>(0.25 * s, (c1.x + c0.y) / s, (c2.x + c0.z) / s, (c1.z - c2.y) / s);\n } else if (c1.y > c2.z) {\n let s = sqrt(1.0 + c1.y - c0.x - c2.z) * 2.0;\n q = vec4<f32>((c1.x + c0.y) / s, 0.25 * s, (c2.y + c1.z) / s, (c2.x - c0.z) / s);\n } else {\n let s = sqrt(1.0 + c2.z - c0.x - c1.y) * 2.0;\n q = vec4<f32>((c2.x + c0.z) / s, (c2.y + c1.z) / s, 0.25 * s, (c0.y - c1.x) / s);\n }\n let len2 = max(dot(q, q), 1e-8);\n return q * inverseSqrt(len2);\n}\n\nfn quat_axis_angle(axis: vec3<f32>, ang: f32) -> vec4<f32> {\n let h = ang * 0.5;\n return vec4<f32>(axis * sin(h), cos(h));\n}\n\n// Hamilton product a*b (applies b first, then a) \u2014 matches the CPU quat_mul convention.\nfn quat_mul_q(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {\n return vec4<f32>(\n a.w * b.xyz + b.w * a.xyz + cross(a.xyz, b.xyz),\n a.w * b.w - dot(a.xyz, b.xyz),\n );\n}\n\nstruct Trs {\n t: vec3<f32>,\n q: vec4<f32>,\n s: vec3<f32>,\n}\n\nfn sample_trs(ci: ClipInfo, node_i: u32, percent_in: f32) -> Trs {\n // Map percent [0,100] -> frame [0, frame_count-1] (smooth interpolation)\n let fc = f32(ci.frame_count);\n let max_f = max(fc - 1.0, 0.0);\n let percent = clamp(percent_in, 0.0, 100.0);\n let f = clamp(percent * max_f / 100.0, 0.0, max_f);\n let f0 = u32(floor(f));\n let f1 = min(f0 + 1u, ci.frame_count - 1u);\n let tt = fract(f);\n\n let stride_u32 = 6u;\n let idx0 = ci.base_u32 + (f0 * u.node_count + node_i) * stride_u32;\n let idx1 = ci.base_u32 + (f1 * u.node_count + node_i) * stride_u32;\n\n // Layout per sample: trans(2u32), rot(2u32), scale(2u32)\n let t0 = decode_vec3(ci.trans_center, ci.trans_extent, clip_trs_u32[idx0 + 0u], clip_trs_u32[idx0 + 1u]);\n let q0 = decode_quat(clip_trs_u32[idx0 + 2u], clip_trs_u32[idx0 + 3u]);\n let s0 = decode_vec3(ci.scale_center, ci.scale_extent, clip_trs_u32[idx0 + 4u], clip_trs_u32[idx0 + 5u]);\n\n let t1 = decode_vec3(ci.trans_center, ci.trans_extent, clip_trs_u32[idx1 + 0u], clip_trs_u32[idx1 + 1u]);\n let q1 = decode_quat(clip_trs_u32[idx1 + 2u], clip_trs_u32[idx1 + 3u]);\n let s1 = decode_vec3(ci.scale_center, ci.scale_extent, clip_trs_u32[idx1 + 4u], clip_trs_u32[idx1 + 5u]);\n\n let t_out = mix(t0, t1, tt);\n let s_out = mix(s0, s1, tt);\n let q_out = quat_nlerp(q0, q1, tt);\n return Trs(t_out, q_out, s_out);\n}\n\nfn sample_local_mat(ci: ClipInfo, node_i: u32, percent: f32) -> mat4x4<f32> {\n if (ci.ready == 0u || ci.frame_count == 0u) {\n return rest_local[node_i];\n }\n let trs = sample_trs(ci, node_i, percent);\n return mat4_from_srt(trs.s, trs.q, trs.t);\n}\n\nconst MAX_DEPTH: u32 = 96u;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let inst_i = gid.x;\n if (inst_i >= u.instance_count) { return; }\n\n // LOCKSTEP with render_base.rs ANIM_MISC_VEC4_PER_INSTANCE.\n let base = inst_i * 39u;\n let misc0 = anim_misc[base];\n let misc1 = anim_misc[base + 1u];\n let misc2 = anim_misc[base + 2u];\n // Left-hand IK: entry3 = [dfs_node_a, dfs_node_b, reserved, 0], entries 4..6 = local quats.\n let ik_nodes = anim_misc[base + 3u];\n let ik_q0 = anim_misc[base + 4u];\n let ik_q1 = anim_misc[base + 5u];\n // Ground-conform knee IK: entry34 = [thighL, shinL, thighR, shinR] DFS indices\n // (u32::MAX = off); entries 35..38 = CPU-blended local quats.\n let leg_nodes = anim_misc[base + 34u];\n // TPS orientation warp: entry7 = lower-body leg yaw quat bits (all-zero = off).\n let misc7 = anim_misc[base + 7u];\n // entry8: [animation_data, upper_animation_data, upper_progress_data, dur_pack(u16x2)]\n let misc8 = anim_misc[base + 8u];\n // entries 9..33: bone overrides (count + upper layer weight, DFS node indices,\n // payloads, packed params). LOCKSTEP with render_base build_entry.\n let misc9 = anim_misc[base + 9u];\n let bo_count = min(misc9.x, 16u);\n // SetUpperBodyAnimationWeight: eased on CPU; 1.0 = legacy full replace.\n let upper_w = clamp(bitcast<f32>(misc9.y), 0.0, 1.0);\n let clip_id = misc0.x;\n let phase = bitcast<f32>(misc1.z);\n let dur_main = misc8.w & 0xffffu;\n let dur_upper = misc8.w >> 16u;\n let percent = derive_percent(misc0.y, misc8.x, dur_main, phase);\n // Palette for this group is laid out densely by instance index.\n let palette_offset = inst_i * u.joint_count;\n let prev_clip_id = misc0.z;\n let prev_percent = bitcast<f32>(misc0.w);\n let prev_w = bitcast<f32>(misc1.x);\n let upper_clip_id = misc1.y;\n let upper_percent = derive_percent(misc8.z, misc8.y, dur_upper, 0.0);\n // flags: bit0 = crossfade active, bit1 = FPS arms carve (collapse non-arm joints).\n let cf_flags = misc1.w;\n let collapse_non_arm = (cf_flags & 2u) != 0u;\n\n // Upper-body aim overlay (TPS pitch): rigid rotation of upper-body joints about the\n // point mapping to the holder origin. All-zero quat bits = off.\n let has_aim = (misc2.x | misc2.y | misc2.z | misc2.w) != 0u;\n var aim_mat: mat4x4<f32>;\n if (has_aim) {\n let aq = vec4<f32>(\n bitcast<f32>(misc2.x), bitcast<f32>(misc2.y),\n bitcast<f32>(misc2.z), bitcast<f32>(misc2.w),\n );\n let r = quat_to_mat3(aq);\n let rot = mat4x4<f32>(\n vec4<f32>(r[0], 0.0), vec4<f32>(r[1], 0.0),\n vec4<f32>(r[2], 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n aim_mat = u.tail_inv * rot * u.tail;\n }\n\n // Lower-body leg yaw overlay (orientation warp): same conjugation as aim_mat, applied\n // to !is_upper joints so the legs face the move direction while the torso stays aimed.\n let has_leg = (misc7.x | misc7.y | misc7.z | misc7.w) != 0u;\n var leg_mat: mat4x4<f32>;\n if (has_leg) {\n let lq = vec4<f32>(\n bitcast<f32>(misc7.x), bitcast<f32>(misc7.y),\n bitcast<f32>(misc7.z), bitcast<f32>(misc7.w),\n );\n let lr = quat_to_mat3(lq);\n let lrot = mat4x4<f32>(\n vec4<f32>(lr[0], 0.0), vec4<f32>(lr[1], 0.0),\n vec4<f32>(lr[2], 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n leg_mat = u.tail_inv * lrot * u.tail;\n }\n\n var ci: ClipInfo;\n if (clip_id < u.clip_count) {\n ci = clip_infos[clip_id];\n } else {\n ci = clip_infos[0u];\n ci.ready = 0u;\n ci.frame_count = 0u;\n }\n\n var ci_prev: ClipInfo;\n var has_prev: bool = false;\n if ((cf_flags & 1u) != 0u && prev_w > 1e-6 && prev_clip_id < u.clip_count) {\n ci_prev = clip_infos[prev_clip_id];\n has_prev = (ci_prev.ready != 0u && ci_prev.frame_count != 0u);\n }\n\n var ci_upper: ClipInfo;\n var has_upper: bool = false;\n if (upper_clip_id < u.clip_count) {\n ci_upper = clip_infos[upper_clip_id];\n has_upper = (ci_upper.ready != 0u && ci_upper.frame_count != 0u);\n }\n\n // Per-instance DFS stack. Uses node_depth to index; assumes DFS ordering.\n var stack: array<mat4x4<f32>, 96>;\n\n // Arms-collapse pre-pass: capture the posed upper-arm root positions so non-arm\n // joints can collapse onto their side's shoulder (roots may come after other nodes\n // in DFS order). Crossfade/IK are irrelevant here: arms instances never crossfade\n // and IK only re-rotates the roots (their origins depend on ancestors only).\n var arm_root_l = vec3<f32>(0.0, 0.0, 0.0);\n var arm_root_r = vec3<f32>(0.0, 0.0, 0.0);\n if (collapse_non_arm) {\n for (var n = 0u; n < u.node_count; n = n + 1u) {\n let packed_depth = node_depth[n];\n let d = packed_depth & NODE_DEPTH_MASK;\n if (d >= MAX_DEPTH) { continue; }\n var local: mat4x4<f32>;\n let au = has_upper && (packed_depth & NODE_UPPER_BODY_BIT) != 0u;\n if (au && upper_w >= 0.999) {\n local = sample_local_mat(ci_upper, n, upper_percent);\n } else if (au && upper_w > 0.001) {\n // Weighted upper layer: TRS-blend the upper sample over the base local.\n let bl = sample_local_mat(ci, n, percent);\n let ut = sample_trs(ci_upper, n, upper_percent);\n let bq = mat3_rot_quat(bl);\n let bs = vec3<f32>(length(bl[0].xyz), length(bl[1].xyz), length(bl[2].xyz));\n local = mat4_from_srt(\n mix(bs, ut.s, upper_w),\n quat_nlerp(bq, ut.q, upper_w),\n mix(bl[3].xyz, ut.t, upper_w),\n );\n } else {\n local = sample_local_mat(ci, n, percent);\n }\n if (d == 0u) { stack[0] = local; } else { stack[d] = stack[d - 1u] * local; }\n if ((packed_depth & NODE_ARM_ROOT_L_BIT) != 0u) { arm_root_l = stack[d][3].xyz; }\n if ((packed_depth & NODE_ARM_ROOT_R_BIT) != 0u) { arm_root_r = stack[d][3].xyz; }\n }\n }\n\n for (var n = 0u; n < u.node_count; n = n + 1u) {\n let packed_depth = node_depth[n];\n let d = packed_depth & NODE_DEPTH_MASK;\n let is_upper = (packed_depth & NODE_UPPER_BODY_BIT) != 0u;\n if (d >= MAX_DEPTH) { continue; }\n var base_local: mat4x4<f32>;\n if (has_prev && ci.ready != 0u && ci.frame_count != 0u) {\n let t = clamp(1.0 - prev_w, 0.0, 1.0);\n let a = sample_trs(ci_prev, n, prev_percent);\n let b = sample_trs(ci, n, percent);\n let t_out = mix(a.t, b.t, t);\n let s_out = mix(a.s, b.s, t);\n let q_out = quat_nlerp(a.q, b.q, t);\n base_local = mat4_from_srt(s_out, q_out, t_out);\n } else {\n base_local = sample_local_mat(ci, n, percent);\n }\n\n var local: mat4x4<f32>;\n if (has_upper && is_upper && upper_w >= 0.999) {\n local = sample_local_mat(ci_upper, n, upper_percent);\n } else if (has_upper && is_upper && upper_w > 0.001) {\n // Weighted upper layer (SetUpperBodyAnimationWeight): TRS-blend the upper\n // sample over the (possibly crossfaded) base local.\n let ut = sample_trs(ci_upper, n, upper_percent);\n let bq = mat3_rot_quat(base_local);\n let bs = vec3<f32>(\n length(base_local[0].xyz), length(base_local[1].xyz), length(base_local[2].xyz),\n );\n local = mat4_from_srt(\n mix(bs, ut.s, upper_w),\n quat_nlerp(bq, ut.q, upper_w),\n mix(base_local[3].xyz, ut.t, upper_w),\n );\n } else {\n local = base_local;\n }\n if (n == ik_nodes.x) {\n local = override_local_rot(local, vec4<f32>(\n bitcast<f32>(ik_q0.x), bitcast<f32>(ik_q0.y),\n bitcast<f32>(ik_q0.z), bitcast<f32>(ik_q0.w),\n ));\n } else if (n == ik_nodes.y) {\n local = override_local_rot(local, vec4<f32>(\n bitcast<f32>(ik_q1.x), bitcast<f32>(ik_q1.y),\n bitcast<f32>(ik_q1.z), bitcast<f32>(ik_q1.w),\n ));\n }\n if (n == leg_nodes.x || n == leg_nodes.y || n == leg_nodes.z || n == leg_nodes.w) {\n var li = 0u;\n if (n == leg_nodes.y) { li = 1u; }\n else if (n == leg_nodes.z) { li = 2u; }\n else if (n == leg_nodes.w) { li = 3u; }\n let lq = anim_misc[base + 35u + li];\n local = override_local_rot(local, vec4<f32>(\n bitcast<f32>(lq.x), bitcast<f32>(lq.y), bitcast<f32>(lq.z), bitcast<f32>(lq.w),\n ));\n }\n // Bone-override slots (SetBoneRotation / Mix / BoneLookAt), applied after\n // clip/crossfade/upper blending and IK. Payload = eased local quat, or the\n // model-space look target (mode 3). Weight byte is pre-eased on CPU.\n for (var s = 0u; s < bo_count; s = s + 1u) {\n let bn = anim_misc[base + 10u + (s / 4u)][s % 4u];\n if (bn != n) { continue; }\n let payload = anim_misc[base + 14u + s];\n let q_or_t = vec4<f32>(\n bitcast<f32>(payload.x), bitcast<f32>(payload.y),\n bitcast<f32>(payload.z), bitcast<f32>(payload.w),\n );\n let pp = anim_misc[base + 30u + (s / 4u)][s % 4u];\n let w = f32(pp & 0xffu) / 255.0;\n let mode = (pp >> 8u) & 0xffu;\n if (mode == 3u) {\n // LookAt: aim the node's rest forward (gltf +Z = character +X) at the\n // model-space target, clamped to p0 degrees from rest, engaged by w.\n var parent = mat4x4<f32>(\n vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 1.0, 0.0, 0.0),\n vec4<f32>(0.0, 0.0, 1.0, 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n if (d > 0u) { parent = stack[d - 1u]; }\n let rest = rest_local[n];\n let node_pos = (parent * vec4<f32>(rest[3].xyz, 1.0)).xyz;\n let to_target = q_or_t.xyz - node_pos;\n if (dot(to_target, to_target) > 1e-8) {\n let dir = normalize(to_target);\n let fwd_rest = normalize((parent * vec4<f32>(normalize(rest[2].xyz), 0.0)).xyz);\n let c = clamp(dot(fwd_rest, dir), -1.0, 1.0);\n let max_r = radians(f32((pp >> 16u) & 0xffu));\n let ang = min(acos(c), max_r);\n var axis = cross(fwd_rest, dir);\n let al = length(axis);\n if (al > 1e-6 && ang > 1e-4) {\n axis = axis / al;\n // Model-space delta conjugated into parent-local space.\n let pq = mat3_rot_quat(parent);\n let dq_model = quat_axis_angle(axis, ang);\n let pq_inv = vec4<f32>(-pq.xyz, pq.w);\n let dq_local = quat_mul_q(quat_mul_q(pq_inv, dq_model), pq);\n let rest_q = mat3_rot_quat(rest);\n let aim_q = quat_nlerp(rest_q, quat_mul_q(dq_local, rest_q), w);\n local = override_local_rot(local, aim_q);\n }\n }\n } else if (mode == 2u) {\n // Additive: weighted offset composed onto the sampled rotation.\n let addq = quat_nlerp(vec4<f32>(0.0, 0.0, 0.0, 1.0), q_or_t, w);\n let cur = mat3_rot_quat(local);\n local = override_local_rot(local, quat_mul_q(cur, addq));\n } else {\n // Replace (legacy mode 0 arrives with w = 1): blend sampled -> target.\n let cur = mat3_rot_quat(local);\n local = override_local_rot(local, quat_nlerp(cur, q_or_t, w));\n }\n break;\n }\n if (d == 0u) {\n stack[0] = local;\n } else {\n stack[d] = stack[d - 1u] * local;\n }\n let j = node_to_joint[n];\n if (j != 0xffffffffu && j < u.joint_count) {\n var pal: mat4x4<f32>;\n if (collapse_non_arm && (packed_depth & NODE_ARM_BIT) == 0u) {\n // Bone-shrink hiding (FPS arms carve): send every vert weighted to this\n // non-arm joint to its side's posed upper-arm root, pinching the shoulder\n // boundary closed instead of leaving an open mesh cut. Epsilon (not zero)\n // linear part keeps blended normals finite for fully-collapsed verts.\n var collapse_to = arm_root_l;\n if ((packed_depth & NODE_COLLAPSE_SIDE_BIT) != 0u) { collapse_to = arm_root_r; }\n let e = 1e-4;\n pal = mat4x4<f32>(\n vec4<f32>(e, 0.0, 0.0, 0.0),\n vec4<f32>(0.0, e, 0.0, 0.0),\n vec4<f32>(0.0, 0.0, e, 0.0),\n vec4<f32>(collapse_to, 1.0),\n );\n } else {\n pal = stack[d] * inverse_bind[j];\n if (has_aim && is_upper) {\n pal = aim_mat * pal;\n }\n if (has_leg && !is_upper) {\n pal = leg_mat * pal;\n }\n }\n palette[palette_offset + j] = pal;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// LEVEL-SWEEP kernel (default path; render_base falls back to the serial `main`\n// above for >MAX_LEVEL_NODES rigs or under ORIVERSE_BONE_LEVEL=0).\n//\n// One WORKGROUP per instance, threads strided over nodes: locals sample in\n// parallel and the hierarchy composes level-by-level through workgroup-shared\n// world matrices \u2014 same association order (world[parent] * local) as the serial\n// DFS walk, so results match to float noise. The serial kernel's 6KB private\n// stack (one thread per instance = single-workgroup dispatches; 0.39ms for ONE\n// character on city_builder_stress) does not exist here \u2014 which also removes\n// the dynamically-indexed private array that dominated mobile shader-compile\n// time (the wasm freeze wave's \"cap depth\" follow-up is obsoleted by this).\n//\n// Two deliberate deltas vs the serial kernel, both strict improvements:\n// - Arms-collapse roots read the FINAL composed matrices (the serial pre-pass\n// re-walked clean clip samples; collapse targets now include IK/overrides \u2014\n// affects only the pinch point of hidden fully-collapsed verts).\n// - No MAX_DEPTH=96 skip: every node composes (the serial kernel left palettes\n// of deeper nodes stale).\n//\n// LAYOUT CONSTRAINT (Pixel Mali-G715, driver r44+): the per-instance header must\n// stay in PLAIN LOCALS and the per-node body INLINE in the entry \u2014 mirroring the\n// serial kernel. Routing them through a by-value context struct + helper fn made\n// the driver's compiler segfault deterministically (SEGV_MAPERR in\n// MaliScalarizerImpl::scalarize at vkCreateComputePipelines; probe-bisected\n// 28Jul26 \u2014 struct form crashes, this locals+inline form compiles).\n// ---------------------------------------------------------------------------\n\nconst MAX_LEVEL_NODES: u32 = 128u;\nconst LEVEL_WG: u32 = 64u;\n\nvar<workgroup> wg_world: array<mat4x4<f32>, 128>;\nvar<workgroup> wg_arm_root_l: vec3<f32>;\nvar<workgroup> wg_arm_root_r: vec3<f32>;\n\n@compute @workgroup_size(64)\nfn main_levels(\n @builtin(workgroup_id) wid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n) {\n // One workgroup per instance: the dispatch issues exactly instance_count groups,\n // so no early return exists and every workgroupBarrier sits in uniform control flow.\n let inst_i = wid.x;\n let tid = lid.x;\n let node_count = min(u.node_count, MAX_LEVEL_NODES);\n\n // Per-instance header decode, plain locals (identical field/bit layout to the serial\n // kernel's prologue; LOCKSTEP with render_base ANIM_MISC_VEC4_PER_INSTANCE).\n let base = inst_i * 39u;\n let misc0 = anim_misc[base];\n let misc1 = anim_misc[base + 1u];\n let misc2 = anim_misc[base + 2u];\n let ik_nodes = anim_misc[base + 3u];\n let ik_q0 = anim_misc[base + 4u];\n let ik_q1 = anim_misc[base + 5u];\n let leg_nodes = anim_misc[base + 34u];\n let misc7 = anim_misc[base + 7u];\n let misc8 = anim_misc[base + 8u];\n let misc9 = anim_misc[base + 9u];\n let bo_count = min(misc9.x, 16u);\n let upper_w = clamp(bitcast<f32>(misc9.y), 0.0, 1.0);\n let clip_id = misc0.x;\n let phase = bitcast<f32>(misc1.z);\n let dur_main = misc8.w & 0xffffu;\n let dur_upper = misc8.w >> 16u;\n let percent = derive_percent(misc0.y, misc8.x, dur_main, phase);\n let palette_offset = inst_i * u.joint_count;\n let prev_clip_id = misc0.z;\n let prev_percent = bitcast<f32>(misc0.w);\n let prev_w = bitcast<f32>(misc1.x);\n let upper_clip_id = misc1.y;\n let upper_percent = derive_percent(misc8.z, misc8.y, dur_upper, 0.0);\n let cf_flags = misc1.w;\n let collapse_non_arm = (cf_flags & 2u) != 0u;\n\n let has_aim = (misc2.x | misc2.y | misc2.z | misc2.w) != 0u;\n var aim_mat: mat4x4<f32>;\n if (has_aim) {\n let aq = vec4<f32>(\n bitcast<f32>(misc2.x), bitcast<f32>(misc2.y),\n bitcast<f32>(misc2.z), bitcast<f32>(misc2.w),\n );\n let r = quat_to_mat3(aq);\n let rot = mat4x4<f32>(\n vec4<f32>(r[0], 0.0), vec4<f32>(r[1], 0.0),\n vec4<f32>(r[2], 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n aim_mat = u.tail_inv * rot * u.tail;\n }\n let has_leg = (misc7.x | misc7.y | misc7.z | misc7.w) != 0u;\n var leg_mat: mat4x4<f32>;\n if (has_leg) {\n let lq = vec4<f32>(\n bitcast<f32>(misc7.x), bitcast<f32>(misc7.y),\n bitcast<f32>(misc7.z), bitcast<f32>(misc7.w),\n );\n let lr = quat_to_mat3(lq);\n let lrot = mat4x4<f32>(\n vec4<f32>(lr[0], 0.0), vec4<f32>(lr[1], 0.0),\n vec4<f32>(lr[2], 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n leg_mat = u.tail_inv * lrot * u.tail;\n }\n\n var ci: ClipInfo;\n if (clip_id < u.clip_count) {\n ci = clip_infos[clip_id];\n } else {\n ci = clip_infos[0u];\n ci.ready = 0u;\n ci.frame_count = 0u;\n }\n var ci_prev: ClipInfo;\n var has_prev: bool = false;\n if ((cf_flags & 1u) != 0u && prev_w > 1e-6 && prev_clip_id < u.clip_count) {\n ci_prev = clip_infos[prev_clip_id];\n has_prev = (ci_prev.ready != 0u && ci_prev.frame_count != 0u);\n }\n var ci_upper: ClipInfo;\n var has_upper: bool = false;\n if (upper_clip_id < u.clip_count) {\n ci_upper = clip_infos[upper_clip_id];\n has_upper = (ci_upper.ready != 0u && ci_upper.frame_count != 0u);\n }\n\n // Hierarchy composition, one depth level at a time (parents final before children).\n for (var lvl = 0u; lvl < u.level_count; lvl = lvl + 1u) {\n for (var n = tid; n < node_count; n = n + LEVEL_WG) {\n let packed_depth = node_depth[n];\n if ((packed_depth & NODE_DEPTH_MASK) == lvl) {\n var parent_world = mat4x4<f32>(\n vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 1.0, 0.0, 0.0),\n vec4<f32>(0.0, 0.0, 1.0, 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),\n );\n let p = (packed_depth >> NODE_PARENT_SHIFT) & NODE_PARENT_MASK;\n if (lvl > 0u && p != NODE_PARENT_ROOT && p < node_count) {\n parent_world = wg_world[p];\n }\n // Final LOCAL matrix for this node, inline (crossfade, weighted upper\n // layer, hand-IK, knee-IK, bone-override slots incl. mode-3 LookAt,\n // which reads the parent's WORLD matrix \u2014 final by level order here).\n let is_upper = (packed_depth & NODE_UPPER_BODY_BIT) != 0u;\n var base_local: mat4x4<f32>;\n if (has_prev && ci.ready != 0u && ci.frame_count != 0u) {\n let t = clamp(1.0 - prev_w, 0.0, 1.0);\n let a = sample_trs(ci_prev, n, prev_percent);\n let b = sample_trs(ci, n, percent);\n base_local = mat4_from_srt(mix(a.s, b.s, t), quat_nlerp(a.q, b.q, t), mix(a.t, b.t, t));\n } else {\n base_local = sample_local_mat(ci, n, percent);\n }\n\n var local: mat4x4<f32>;\n if (has_upper && is_upper && upper_w >= 0.999) {\n local = sample_local_mat(ci_upper, n, upper_percent);\n } else if (has_upper && is_upper && upper_w > 0.001) {\n let ut = sample_trs(ci_upper, n, upper_percent);\n let bq = mat3_rot_quat(base_local);\n let bs = vec3<f32>(\n length(base_local[0].xyz), length(base_local[1].xyz), length(base_local[2].xyz),\n );\n local = mat4_from_srt(\n mix(bs, ut.s, upper_w),\n quat_nlerp(bq, ut.q, upper_w),\n mix(base_local[3].xyz, ut.t, upper_w),\n );\n } else {\n local = base_local;\n }\n if (n == ik_nodes.x) {\n local = override_local_rot(local, vec4<f32>(bitcast<f32>(ik_q0.x), bitcast<f32>(ik_q0.y), bitcast<f32>(ik_q0.z), bitcast<f32>(ik_q0.w)));\n } else if (n == ik_nodes.y) {\n local = override_local_rot(local, vec4<f32>(bitcast<f32>(ik_q1.x), bitcast<f32>(ik_q1.y), bitcast<f32>(ik_q1.z), bitcast<f32>(ik_q1.w)));\n }\n if (n == leg_nodes.x || n == leg_nodes.y || n == leg_nodes.z || n == leg_nodes.w) {\n var li = 0u;\n if (n == leg_nodes.y) { li = 1u; }\n else if (n == leg_nodes.z) { li = 2u; }\n else if (n == leg_nodes.w) { li = 3u; }\n let lq = anim_misc[base + 35u + li];\n local = override_local_rot(local, vec4<f32>(\n bitcast<f32>(lq.x), bitcast<f32>(lq.y), bitcast<f32>(lq.z), bitcast<f32>(lq.w),\n ));\n }\n for (var s = 0u; s < bo_count; s = s + 1u) {\n let bn = anim_misc[base + 10u + (s / 4u)][s % 4u];\n if (bn != n) { continue; }\n let payload = anim_misc[base + 14u + s];\n let q_or_t = vec4<f32>(\n bitcast<f32>(payload.x), bitcast<f32>(payload.y),\n bitcast<f32>(payload.z), bitcast<f32>(payload.w),\n );\n let pp = anim_misc[base + 30u + (s / 4u)][s % 4u];\n let w = f32(pp & 0xffu) / 255.0;\n let mode = (pp >> 8u) & 0xffu;\n if (mode == 3u) {\n let rest = rest_local[n];\n let node_pos = (parent_world * vec4<f32>(rest[3].xyz, 1.0)).xyz;\n let to_target = q_or_t.xyz - node_pos;\n if (dot(to_target, to_target) > 1e-8) {\n let dir = normalize(to_target);\n let fwd_rest = normalize((parent_world * vec4<f32>(normalize(rest[2].xyz), 0.0)).xyz);\n let c = clamp(dot(fwd_rest, dir), -1.0, 1.0);\n let max_r = radians(f32((pp >> 16u) & 0xffu));\n let ang = min(acos(c), max_r);\n var axis = cross(fwd_rest, dir);\n let al = length(axis);\n if (al > 1e-6 && ang > 1e-4) {\n axis = axis / al;\n let pq = mat3_rot_quat(parent_world);\n let dq_model = quat_axis_angle(axis, ang);\n let pq_inv = vec4<f32>(-pq.xyz, pq.w);\n let dq_local = quat_mul_q(quat_mul_q(pq_inv, dq_model), pq);\n let rest_q = mat3_rot_quat(rest);\n let aim_q = quat_nlerp(rest_q, quat_mul_q(dq_local, rest_q), w);\n local = override_local_rot(local, aim_q);\n }\n }\n } else if (mode == 2u) {\n let addq = quat_nlerp(vec4<f32>(0.0, 0.0, 0.0, 1.0), q_or_t, w);\n let cur = mat3_rot_quat(local);\n local = override_local_rot(local, quat_mul_q(cur, addq));\n } else {\n let cur = mat3_rot_quat(local);\n local = override_local_rot(local, quat_nlerp(cur, q_or_t, w));\n }\n break;\n }\n wg_world[n] = parent_world * local;\n }\n }\n workgroupBarrier();\n }\n\n // Arms-collapse anchors from the FINAL composed matrices (see header note).\n for (var n = tid; n < node_count; n = n + LEVEL_WG) {\n let packed_depth = node_depth[n];\n if ((packed_depth & NODE_ARM_ROOT_L_BIT) != 0u) { wg_arm_root_l = wg_world[n][3].xyz; }\n if ((packed_depth & NODE_ARM_ROOT_R_BIT) != 0u) { wg_arm_root_r = wg_world[n][3].xyz; }\n }\n workgroupBarrier();\n\n // Palette writes \u2014 same math as the serial kernel's tail.\n for (var n = tid; n < node_count; n = n + LEVEL_WG) {\n let packed_depth = node_depth[n];\n let is_upper = (packed_depth & NODE_UPPER_BODY_BIT) != 0u;\n let j = node_to_joint[n];\n if (j != 0xffffffffu && j < u.joint_count) {\n var pal: mat4x4<f32>;\n if (collapse_non_arm && (packed_depth & NODE_ARM_BIT) == 0u) {\n var collapse_to = wg_arm_root_l;\n if ((packed_depth & NODE_COLLAPSE_SIDE_BIT) != 0u) { collapse_to = wg_arm_root_r; }\n let e = 1e-4;\n pal = mat4x4<f32>(\n vec4<f32>(e, 0.0, 0.0, 0.0),\n vec4<f32>(0.0, e, 0.0, 0.0),\n vec4<f32>(0.0, 0.0, e, 0.0),\n vec4<f32>(collapse_to, 1.0),\n );\n } else {\n pal = wg_world[n] * inverse_bind[j];\n if (has_aim && is_upper) {\n pal = aim_mat * pal;\n }\n if (has_leg && !is_upper) {\n pal = leg_mat * pal;\n }\n }\n palette[palette_offset + j] = pal;\n }\n }\n}\n\n"},{"label":"SkyGen Shader","code":"// sky_gen.wgsl - procedural sky + IBL generation (runs only on regen, not per frame).\n// Hillaire-2020 style atmosphere: transmittance LUT + multi-scattering LUT (both sun-independent,\n// generated once) feed a per-regen raymarch that renders the day cubemap, then irradiance\n// convolve + GGX prefilter derive the IBL cubes consumed by main_pass split-sum.\n//\n// Space conventions:\n// - \"world\" dirs are render-space Y-up. Day/night cubes are stored in \"skybox space\"\n// (z-flipped world, matching skybox.wgsl / water.wgsl sampling with flip=(1,1,-1)).\n// - Irradiance/prefiltered cubes are stored in WORLD space (main_pass samples N/reflect_dir\n// without a flip), so convolve passes flip z when sampling the day cube.\n\nstruct SkyGenGlobals {\n sun_dir_ws: vec3<f32>, // world-space (render Y-up) direction TOWARD the sun\n day_cube_res: f32,\n cloud_coverage: f32, // 0 = clear, 0.1 = default sparse wisps, 1 = overcast\n haze: f32, // Mie density multiplier, 1 = default (changing it requires T/MS LUT regen)\n cloud_detail: f32, // fractal detail: 0 = soft blobs, 1 = default, 2 = max wispy\n cloud_scale: f32, // pattern frequency: 0.5 = big clouds, 1 = default, 2 = many small\n // IBL calibration knobs (live-tunable via /sky, defaults in sky_gen.rs IBL_KNOBS):\n ibl_tint: vec3<f32>, // per-channel grade toward the artist-bake neutral\n ibl_exposure: f32, // ambient level match\n ibl_sun_diffuse: f32, // azimuthal sun lobe strength in irradiance\n ibl_sun_spec: f32, // GGX sun lobe strength in prefiltered mips\n // Ground bounce albedo (SkyboxGroundColor world attr; pre-divided by ibl_tint on CPU\n // so the graded bottom hue equals the world-set color). Scalars to keep std140 packing.\n ground_r: f32,\n ground_g: f32,\n ground_b: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n // World grade (SetSkyboxTint/Saturation/Exposure/Hue/Contrast): applied to the IBL\n // convolve outputs so lighting follows the graded sky (skybox.wgsl grades the display).\n grade_tint: vec3<f32>,\n grade_saturation: f32,\n grade_exposure: f32, // stops\n grade_hue: f32, // degrees\n grade_contrast: f32,\n _pad3: f32,\n};\nstruct SkyGenPerDraw {\n face: u32,\n mode: u32, // 0 = day sky, 1 = night sky\n roughness: f32,\n target_res: f32, // face resolution of the mip being rendered\n};\n@group(0) @binding(0) var<uniform> u_globals: SkyGenGlobals;\n@group(0) @binding(1) var<uniform> u_draw: SkyGenPerDraw;\n@group(0) @binding(2) var t_lut: texture_2d<f32>;\n@group(0) @binding(3) var ms_lut: texture_2d<f32>;\n@group(0) @binding(4) var src_cube: texture_cube<f32>;\n@group(0) @binding(5) var samp: sampler;\n\nconst PI: f32 = 3.14159265359;\n// Planet + atmosphere (meters), Hillaire 2020 coefficients\nconst R_BOTTOM: f32 = 6360e3;\nconst R_TOP: f32 = 6460e3;\nconst VIEW_H: f32 = 200.0; // viewer altitude above ground\nconst RAYLEIGH_SCATTER: vec3<f32> = vec3<f32>(5.802e-6, 13.558e-6, 33.1e-6);\nconst RAYLEIGH_H: f32 = 8000.0;\nconst MIE_SCATTER: f32 = 2.4e-6; // reduced from 3.996e-6: clearer horizon, less washed-out haze\nconst MIE_EXT: f32 = 2.67e-6;\nconst MIE_H: f32 = 1200.0;\nconst MIE_G: f32 = 0.8;\nconst OZONE_ABSORB: vec3<f32> = vec3<f32>(0.650e-6, 1.881e-6, 0.085e-6);\nconst SUN_ILLUM: vec3<f32> = vec3<f32>(20.0, 20.0, 20.0); // HDR scale, tuned vs old baked sky\nconst GROUND_ALBEDO: vec3<f32> = vec3<f32>(0.0); // Hillaire ref value; no ground tint in sky/IBL\n// Clouds\nconst CLOUD_H: f32 = 1500.0;\nconst CLOUD_UV_SCALE: f32 = 1.0 / 4500.0; // noise feature size ~4.5km\nconst CLOUD_DENSITY: f32 = 3.5;\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>, // texture space, (0,0) top-left\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VertexOutput {\n var pos: vec2<f32>;\n switch (vi) {\n case 0u: { pos = vec2<f32>(-1.0, -1.0); }\n case 1u: { pos = vec2<f32>( 3.0, -1.0); }\n default: { pos = vec2<f32>(-1.0, 3.0); }\n }\n var out: VertexOutput;\n out.position = vec4<f32>(pos, 0.0, 1.0);\n out.uv = vec2<f32>(pos.x * 0.5 + 0.5, 0.5 - pos.y * 0.5);\n return out;\n}\n\n// uv (texel center, [0,1]) -> cube direction, WebGPU/GL face order +X,-X,+Y,-Y,+Z,-Z\nfn cube_dir(face: u32, uv: vec2<f32>) -> vec3<f32> {\n let s = uv.x * 2.0 - 1.0;\n let t = uv.y * 2.0 - 1.0; // v is down\n var d: vec3<f32>;\n switch (face) {\n case 0u: { d = vec3<f32>( 1.0, -t, -s); }\n case 1u: { d = vec3<f32>(-1.0, -t, s); }\n case 2u: { d = vec3<f32>( s, 1.0, t); }\n case 3u: { d = vec3<f32>( s, -1.0, -t); }\n case 4u: { d = vec3<f32>( s, -t, 1.0); }\n default: { d = vec3<f32>( -s, -t, -1.0); }\n }\n return normalize(d);\n}\n\n// Nearest positive intersection with sphere of radius rad centered at origin (-1 if none)\nfn intersect_sphere(pos: vec3<f32>, dir: vec3<f32>, rad: f32) -> f32 {\n let b = dot(pos, dir);\n let c = dot(pos, pos) - rad * rad;\n let disc = b * b - c;\n if (disc < 0.0) { return -1.0; }\n let sq = sqrt(disc);\n let t0 = -b - sq;\n let t1 = -b + sq;\n if (t0 > 0.0) { return t0; }\n if (t1 > 0.0) { return t1; }\n return -1.0;\n}\n\nfn atmosphere_densities(h: f32) -> vec3<f32> { // (rayleigh, mie, ozone)\n let hc = max(h, 0.0);\n return vec3<f32>(\n exp(-hc / RAYLEIGH_H),\n exp(-hc / MIE_H) * u_globals.haze, // shared by T LUT, MS LUT and the sky raymarch\n max(0.0, 1.0 - abs(hc - 25000.0) / 15000.0),\n );\n}\nfn extinction_at(h: f32) -> vec3<f32> {\n let d = atmosphere_densities(h);\n return RAYLEIGH_SCATTER * d.x + vec3<f32>(MIE_EXT) * d.y + OZONE_ABSORB * d.z;\n}\n\n// Transmittance LUT parameterization (Hillaire / Bruneton distance-to-top mapping)\nfn t_lut_uv(r: f32, mu: f32) -> vec2<f32> {\n let h_atm = sqrt(R_TOP * R_TOP - R_BOTTOM * R_BOTTOM);\n let rho = sqrt(max(r * r - R_BOTTOM * R_BOTTOM, 0.0));\n let disc = r * r * (mu * mu - 1.0) + R_TOP * R_TOP;\n let d = max(0.0, -r * mu + sqrt(max(disc, 0.0)));\n let d_min = R_TOP - r;\n let d_max = rho + h_atm;\n return vec2<f32>((d - d_min) / max(d_max - d_min, 1e-6), rho / h_atm);\n}\nfn t_lut_params(uv: vec2<f32>) -> vec2<f32> { // returns (r, mu)\n let h_atm = sqrt(R_TOP * R_TOP - R_BOTTOM * R_BOTTOM);\n let rho = h_atm * uv.y;\n let r = sqrt(rho * rho + R_BOTTOM * R_BOTTOM);\n let d_min = R_TOP - r;\n let d_max = rho + h_atm;\n let d = d_min + uv.x * (d_max - d_min);\n var mu = 1.0;\n if (d > 1e-4) { mu = (h_atm * h_atm - rho * rho - d * d) / (2.0 * r * d); }\n return vec2<f32>(r, clamp(mu, -1.0, 1.0));\n}\n\n// Transmittance from radius r along cos-zenith mu to atmosphere top (LUT sample)\nfn transmittance_to_top(r: f32, mu: f32) -> vec3<f32> {\n return textureSampleLevel(t_lut, samp, t_lut_uv(r, mu), 0.0).rgb;\n}\n// Sun visibility incl. planet shadow\nfn sun_transmittance(pos: vec3<f32>, sun_dir: vec3<f32>) -> vec3<f32> {\n let r = length(pos);\n let mu_s = dot(pos / r, sun_dir);\n // planet occlusion\n if (intersect_sphere(pos, sun_dir, R_BOTTOM) > 0.0) { return vec3<f32>(0.0); }\n return transmittance_to_top(r, mu_s);\n}\n\nfn phase_rayleigh(c: f32) -> f32 { return 3.0 / (16.0 * PI) * (1.0 + c * c); }\nfn phase_hg(c: f32, g: f32) -> f32 {\n let g2 = g * g;\n return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * c, 1.5));\n}\n\nfn ms_lut_sample(r: f32, mu_s: f32) -> vec3<f32> {\n let uv = vec2<f32>(mu_s * 0.5 + 0.5, (r - R_BOTTOM) / (R_TOP - R_BOTTOM));\n return textureSampleLevel(ms_lut, samp, clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;\n}\n\n// ---------- Transmittance LUT ----------\n@fragment\nfn fs_transmittance_lut(in: VertexOutput) -> @location(0) vec4<f32> {\n let rm = t_lut_params(in.uv);\n let r = rm.x;\n let mu = rm.y;\n let pos = vec3<f32>(0.0, r, 0.0);\n let dir = vec3<f32>(sqrt(max(1.0 - mu * mu, 0.0)), mu, 0.0);\n let t_top = intersect_sphere(pos, dir, R_TOP);\n var tau = vec3<f32>(0.0);\n let steps = 40u;\n let dt = max(t_top, 0.0) / f32(steps);\n for (var i = 0u; i < steps; i++) {\n let p = pos + dir * ((f32(i) + 0.5) * dt);\n tau += extinction_at(length(p) - R_BOTTOM) * dt;\n }\n return vec4<f32>(exp(-tau), 1.0);\n}\n\n// ---------- Multi-scattering LUT (Hillaire: isotropic 2nd-order estimate) ----------\n@fragment\nfn fs_ms_lut(in: VertexOutput) -> @location(0) vec4<f32> {\n let mu_s = in.uv.x * 2.0 - 1.0;\n let r = mix(R_BOTTOM + 10.0, R_TOP - 10.0, in.uv.y);\n let sun_dir = vec3<f32>(sqrt(max(1.0 - mu_s * mu_s, 0.0)), mu_s, 0.0);\n let pos = vec3<f32>(0.0, r, 0.0);\n\n var l_2nd = vec3<f32>(0.0);\n var f_ms = vec3<f32>(0.0);\n let n_dirs = 64u;\n let golden = 2.399963229728653; // golden angle\n for (var i = 0u; i < n_dirs; i++) {\n // Fibonacci sphere direction\n let cos_th = 1.0 - 2.0 * (f32(i) + 0.5) / f32(n_dirs);\n let sin_th = sqrt(max(1.0 - cos_th * cos_th, 0.0));\n let phi = golden * f32(i);\n let dir = vec3<f32>(sin_th * cos(phi), cos_th, sin_th * sin(phi));\n\n let t_ground = intersect_sphere(pos, dir, R_BOTTOM);\n var t_max = intersect_sphere(pos, dir, R_TOP);\n if (t_ground > 0.0) { t_max = t_ground; }\n let steps = 20u;\n let dt = max(t_max, 0.0) / f32(steps);\n var through = vec3<f32>(1.0);\n for (var j = 0u; j < steps; j++) {\n let p = pos + dir * ((f32(j) + 0.5) * dt);\n let h = length(p) - R_BOTTOM;\n let dens = atmosphere_densities(h);\n let sc = RAYLEIGH_SCATTER * dens.x + vec3<f32>(MIE_SCATTER) * dens.y;\n let ext = max(extinction_at(h), vec3<f32>(1e-9));\n let t_step = exp(-ext * dt);\n let sun_t = sun_transmittance(p, sun_dir);\n let s = sun_t * sc * (1.0 / (4.0 * PI));\n l_2nd += through * (s - s * t_step) / ext;\n f_ms += through * (sc - sc * t_step) / ext;\n through *= t_step;\n }\n if (t_ground > 0.0) {\n let pg = pos + dir * t_ground;\n let sun_t = sun_transmittance(pg, sun_dir);\n l_2nd += through * GROUND_ALBEDO / PI * sun_t * max(dot(normalize(pg), sun_dir), 0.0);\n }\n }\n l_2nd /= f32(n_dirs);\n f_ms /= f32(n_dirs);\n let psi = l_2nd / max(vec3<f32>(1.0) - f_ms, vec3<f32>(1e-3));\n return vec4<f32>(psi, 1.0);\n}\n\n// ---------- Sky radiance (day) ----------\nfn sky_scatter(world_dir: vec3<f32>, sun_dir: vec3<f32>) -> vec3<f32> {\n let pos = vec3<f32>(0.0, R_BOTTOM + VIEW_H, 0.0);\n let t_ground = intersect_sphere(pos, world_dir, R_BOTTOM);\n var t_max = intersect_sphere(pos, world_dir, R_TOP);\n if (t_ground > 0.0) { t_max = t_ground; }\n let c = dot(world_dir, sun_dir);\n let ph_r = phase_rayleigh(c);\n let ph_m = phase_hg(c, MIE_G);\n\n var lum = vec3<f32>(0.0);\n var through = vec3<f32>(1.0);\n let steps = 32u;\n let dt = max(t_max, 0.0) / f32(steps);\n for (var i = 0u; i < steps; i++) {\n let p = pos + world_dir * ((f32(i) + 0.5) * dt);\n let rp = length(p);\n let h = rp - R_BOTTOM;\n let dens = atmosphere_densities(h);\n let sc_r = RAYLEIGH_SCATTER * dens.x;\n let sc_m = vec3<f32>(MIE_SCATTER) * dens.y;\n let ext = max(extinction_at(h), vec3<f32>(1e-9));\n let t_step = exp(-ext * dt);\n let sun_t = sun_transmittance(p, sun_dir);\n let psi = ms_lut_sample(rp, dot(p / rp, sun_dir));\n let s = sun_t * (sc_r * ph_r + sc_m * ph_m) + psi * (sc_r + sc_m);\n lum += through * (s - s * t_step) / ext;\n through *= t_step;\n }\n if (t_ground > 0.0) {\n let pg = pos + world_dir * t_ground;\n let up_g = normalize(pg);\n let sun_t = sun_transmittance(pg, sun_dir);\n lum += through * GROUND_ALBEDO / PI * sun_t * max(dot(up_g, sun_dir), 0.0);\n }\n return lum * SUN_ILLUM;\n}\n\n// ---------- Clouds (baked into day cube) ----------\nfn hash12(p: vec2<f32>) -> f32 {\n var p3 = fract(vec3<f32>(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn vnoise(p: vec2<f32>) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = hash12(i);\n let b = hash12(i + vec2<f32>(1.0, 0.0));\n let c = hash12(i + vec2<f32>(0.0, 1.0));\n let d = hash12(i + vec2<f32>(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfn fbm_o(p_in: vec2<f32>, octaves: u32) -> f32 {\n var p = p_in;\n var amp = 0.5;\n var sum = 0.0;\n var norm = 0.0;\n let rot = mat2x2<f32>(0.8, -0.6, 0.6, 0.8);\n for (var i = 0u; i < octaves; i++) {\n sum += amp * vnoise(p);\n norm += amp;\n p = rot * p * 2.02;\n amp *= 0.5;\n }\n return sum / norm;\n}\nfn fbm(p: vec2<f32>) -> f32 { return fbm_o(p, 5u); }\n\n// Gradient (Perlin) noise + Worley: smoother isotropic billows than value noise (clouds only;\n// the milky way keeps the value-noise fbm above).\nfn hash22(p: vec2<f32>) -> vec2<f32> {\n var p3 = fract(vec3<f32>(p.xyx) * vec3<f32>(0.1031, 0.1030, 0.0973));\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\nfn gnoise(p: vec2<f32>) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let va = dot(hash22(i) * 2.0 - 1.0, f);\n let vb = dot(hash22(i + vec2<f32>(1.0, 0.0)) * 2.0 - 1.0, f - vec2<f32>(1.0, 0.0));\n let vc = dot(hash22(i + vec2<f32>(0.0, 1.0)) * 2.0 - 1.0, f - vec2<f32>(0.0, 1.0));\n let vd = dot(hash22(i + vec2<f32>(1.0, 1.0)) * 2.0 - 1.0, f - vec2<f32>(1.0, 1.0));\n return mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y) * 0.7 + 0.5;\n}\nfn fbm_g(p_in: vec2<f32>, octaves: u32) -> f32 {\n var p = p_in;\n var amp = 0.5;\n var sum = 0.0;\n var norm = 0.0;\n let rot = mat2x2<f32>(0.8, -0.6, 0.6, 0.8);\n for (var i = 0u; i < octaves; i++) {\n sum += amp * gnoise(p);\n norm += amp;\n p = rot * p * 2.02;\n amp *= 0.5;\n }\n return sum / norm;\n}\nfn worley(p: vec2<f32>) -> f32 {\n let i = floor(p);\n let f = fract(p);\n var dmin = 8.0;\n for (var x = -1; x <= 1; x++) {\n for (var y = -1; y <= 1; y++) {\n let g = vec2<f32>(f32(x), f32(y));\n let d = g + hash22(i + g) - f;\n dmin = min(dmin, dot(d, d));\n }\n }\n return sqrt(dmin);\n}\n\n// Domain-warped Perlin fbm + Perlin-Worley billow (2D take on the TileableVolumeNoise recipe)\n// + high-frequency erosion; detail scales warp and erosion.\nfn cloud_field(uv: vec2<f32>, detail: f32) -> f32 {\n let q = vec2<f32>(fbm_g(uv, 4u), fbm_g(uv + vec2<f32>(5.2, 1.3), 4u));\n let p = uv + (q - vec2<f32>(0.5)) * (1.1 * detail);\n var n = fbm_g(p, 5u);\n let w = 1.0 - (worley(p * 1.7) * 0.625 + worley(p * 3.4) * 0.25 + worley(p * 6.8) * 0.125);\n n = clamp(n + (w - 0.45) * 0.3, 0.0, 1.0); // puffy cell cores, eroded canyons between\n let hf = fbm_g(p * 3.7 + vec2<f32>(11.7, 7.3), 3u);\n n -= (hf - 0.5) * 0.22 * detail;\n return n;\n}\n\nfn apply_clouds(base: vec3<f32>, world_dir: vec3<f32>, sun_dir: vec3<f32>) -> vec3<f32> {\n let coverage = clamp(u_globals.cloud_coverage, 0.0, 1.0);\n if (world_dir.y < 0.02 || coverage <= 0.001) { return base; }\n let detail = clamp(u_globals.cloud_detail, 0.0, 2.0);\n let scale = clamp(u_globals.cloud_scale, 0.25, 4.0);\n let cp = world_dir * (CLOUD_H / world_dir.y);\n let uv = cp.xz * (CLOUD_UV_SCALE * scale);\n let n = cloud_field(uv, detail);\n // coverage shifts the field remap window; default 0.1 = a few sparse wisps\n // (window tuned to the Perlin+Worley field range, which is narrower than value noise)\n let cov_lo = mix(0.72, 0.15, coverage);\n let cov = smoothstep(cov_lo, cov_lo + 0.28, n); // opacity (edge softness)\n if (cov <= 0.001) { return base; }\n let dens = smoothstep(cov_lo, cov_lo + 0.6, n); // optical thickness proxy (wider ramp)\n\n // UE5-style cloud lighting on the 2D field:\n // - sun optical depth from 3 field taps toward the sun (proxy for the shadow march)\n // - Wrenninge/Hillaire multi-scattering octaves (UE \"multi scattering approximation\"):\n // sum of b^i * phase(c^i * g) * exp(-a^i * tau); keeps thick cores luminous, not dead gray\n // - dual-lobe HG phase (forward silver lining + weak back-scatter glow)\n // - Decima powder term gated by sun angle (darkens only away from the sun)\n let mu = dot(world_dir, sun_dir);\n let sun_flat = sun_dir.xz / max(sun_dir.y, 0.25);\n var tau_sun = dens;\n for (var i = 1; i <= 3; i++) {\n let n_s = cloud_field(uv - sun_flat * (f32(i) * 170.0 * CLOUD_UV_SCALE * scale), detail);\n tau_sun += smoothstep(cov_lo, cov_lo + 0.6, n_s) * 0.6;\n }\n let cloud_pos = vec3<f32>(0.0, R_BOTTOM + CLOUD_H, 0.0);\n let sun_t = sun_transmittance(cloud_pos, sun_dir); // reddens at low sun automatically\n var scat = 0.0;\n var ms_a = 1.0; // extinction attenuation per octave\n var ms_b = 1.0; // contribution per octave\n var ms_c = 1.0; // phase eccentricity attenuation per octave\n for (var i = 0; i < 3; i++) {\n let ph = mix(phase_hg(mu, -0.15 * ms_c), phase_hg(mu, 0.6 * ms_c), 0.8);\n scat += ms_b * ph * exp(-tau_sun * 2.6 * ms_a);\n ms_a *= 0.35;\n ms_b *= 0.55;\n ms_c *= 0.5;\n }\n let powder = mix(1.0 - exp(-dens * 4.0), 1.0, 0.5 + 0.5 * mu);\n let direct = sun_t * SUN_ILLUM * scat * powder * 0.35;\n let ambient = (base * 1.6 + vec3<f32>(0.01)) * mix(0.52, 1.0, exp(-dens * 1.8));\n let cloud_col = ambient * 0.55 + direct;\n\n let trans = exp(-cov * CLOUD_DENSITY);\n let fade = smoothstep(0.02, 0.12, world_dir.y);\n return mix(base, cloud_col, (1.0 - trans) * fade);\n}\n\n// ---------- Night sky (static gradient + milky way; stars are a skybox.wgsl overlay) ----------\n// Band normal shared with skybox.wgsl MW_N (star clustering aligns with the baked glow).\nconst MW_N: vec3<f32> = vec3<f32>(0.4211, 0.3609, 0.8321);\n\nfn night_radiance(world_dir: vec3<f32>) -> vec3<f32> {\n let y = world_dir.y;\n let zenith = vec3<f32>(0.0010, 0.0016, 0.0038);\n let horizon = vec3<f32>(0.0115, 0.0105, 0.0150); // slight warm glow like the old night photo\n var col = mix(zenith, horizon, pow(1.0 - clamp(y, 0.0, 1.0), 3.0));\n if (y < 0.0) { col = mix(horizon * 0.55, vec3<f32>(0.0018, 0.0020, 0.0026), clamp(-y * 6.0, 0.0, 1.0)); }\n\n // Milky way: great-circle glow, fbm star-cloud patches, dark dust lane through the core\n let d_mw = dot(world_dir, MW_N);\n let t1 = normalize(cross(MW_N, vec3<f32>(0.0, 1.0, 0.0)));\n let t2 = cross(MW_N, t1);\n let along = atan2(dot(world_dir, t2), dot(world_dir, t1));\n let buv = vec2<f32>(along * 2.5, d_mw * 9.0);\n let clumps = fbm(buv * 2.0);\n let glow = exp(-d_mw * d_mw * 30.0);\n let wide = exp(-d_mw * d_mw * 8.0);\n let dust = smoothstep(0.42, 0.72, fbm(buv * vec2<f32>(3.1, 1.7) + vec2<f32>(7.3, 2.9))) * exp(-d_mw * d_mw * 70.0);\n var mw = wide * 0.30 + glow * (0.5 + 1.3 * clumps);\n mw *= 1.0 - 0.85 * dust;\n col += vec3<f32>(0.0270, 0.0285, 0.0410) * mw;\n col += vec3<f32>(0.0150, 0.0105, 0.0070) * glow * clumps * clumps; // warm dense core patches\n return col;\n}\n\n@fragment\nfn fs_sky_face(in: VertexOutput) -> @location(0) vec4<f32> {\n let cube_d = cube_dir(u_draw.face, in.uv);\n // day/night cubes are stored in skybox space; convert to world for the physics\n let world_dir = vec3<f32>(cube_d.x, cube_d.y, -cube_d.z);\n if (u_draw.mode == 1u) {\n return vec4<f32>(night_radiance(world_dir), 1.0);\n }\n let sun_dir = normalize(u_globals.sun_dir_ws);\n var col = sky_scatter(world_dir, sun_dir);\n // vivid-blue look: neutral exposure + gentle desat so AgX doesn't crush red to 0 at the\n // zenith. Channel-neutral, so low-sun scenes still redden naturally via transmittance.\n col *= 0.45;\n let luma = dot(col, vec3<f32>(0.2126, 0.7152, 0.0722));\n col = max(mix(vec3<f32>(luma), col, 0.85), vec3<f32>(0.0));\n // Mips are analytic re-renders (not downsampled), so tiny mips point-sample the cloud\n // field far below Nyquist -> blotchy aliasing that leaks into the IBL convolve. Clouds\n // contribute little to IBL at default coverage; skip them below 64px.\n if (u_draw.target_res >= 64.0) {\n col = apply_clouds(col, world_dir, sun_dir);\n }\n return vec4<f32>(col, 1.0);\n}\n\n// ---------- IBL: irradiance convolve ----------\nfn hammersley(i: u32, n: u32) -> vec2<f32> {\n var bits = i;\n bits = (bits << 16u) | (bits >> 16u);\n bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);\n bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);\n bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);\n bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);\n return vec2<f32>(f32(i) / f32(n), f32(bits) * 2.3283064365386963e-10);\n}\nfn tangent_basis(n: vec3<f32>) -> mat3x3<f32> {\n var up = vec3<f32>(0.0, 1.0, 0.0);\n if (abs(n.y) > 0.99) { up = vec3<f32>(1.0, 0.0, 0.0); }\n let t = normalize(cross(up, n));\n let b = cross(n, t);\n return mat3x3<f32>(t, b, n);\n}\n// day cube is skybox-space -> flip z to sample by world dir\nfn sample_day(world_dir: vec3<f32>, level: f32) -> vec3<f32> {\n return textureSampleLevel(src_cube, samp, vec3<f32>(world_dir.x, world_dir.y, -world_dir.z), level).rgb;\n}\n\n// IBL calibration vs the artist-baked cubes (assets/skybox fallback KTX2 averages:\n// irradiance all-faces RGB (0.30,0.38,0.46), -Y luma ~60% of +Y, sun-side faces ~1.8x opposite).\n// - virtual neutral ground bounce fills the lower hemisphere (sky itself keeps black ground)\n// - per-channel tint rebalances the vivid-blue sky toward the artist's near-neutral average\n// while keeping directional hue variation (blue above / neutral below)\n// - analytic sun lobes restore the artist bake's sun-side directionality: the day cube has no\n// sun disc (it is a skybox.wgsl overlay), so the convolved IBL would otherwise be isotropic\n// IBL calibration values come from u_globals (live-tunable via /sky; defaults in sky_gen.rs).\n\n// Env lookup for the IBL convolves: below-horizon returns a colored ground bounce.\n// Brightness comes from the mirrored sky's LUMA (stays sun/sunset-aware) but the hue\n// comes from the SkyboxGroundColor world attr \u2014 the artist bake's bottom is a dim\n// green-gray, not a mirror of the blue sky. Always heavily blurred; ground is matte.\nfn sample_env(world_dir: vec3<f32>, level: f32) -> vec3<f32> {\n if (world_dir.y < 0.0) {\n let m = vec3<f32>(world_dir.x, -world_dir.y, world_dir.z);\n let ground_mip = log2(u_globals.day_cube_res) - 4.0; // 16px face\n let sky = sample_day(m, max(level, ground_mip));\n let luma = dot(sky, vec3<f32>(0.2126, 0.7152, 0.0722));\n return luma * vec3<f32>(u_globals.ground_r, u_globals.ground_g, u_globals.ground_b);\n }\n return sample_day(world_dir, level);\n}\n// Calibration (tint/exposure knobs), then the world grade with the same math as\n// skybox.wgsl apply_skybox_grade. Tint/hue/sat/exposure are linear ops, so grading the\n// convolved output equals convolving the graded sky; contrast is a close approximation.\nfn ibl_grade(col: vec3<f32>) -> vec3<f32> {\n let cal = max(col * u_globals.ibl_tint * u_globals.ibl_exposure, vec3<f32>(0.0));\n let y = dot(cal, vec3<f32>(0.2126, 0.7152, 0.0722));\n var ypbpr = vec3<f32>(y,\n -0.114572 * cal.r - 0.385428 * cal.g + 0.5 * cal.b,\n 0.5 * cal.r - 0.454153 * cal.g - 0.045847 * cal.b);\n let hue = radians(u_globals.grade_hue);\n let hc = cos(hue);\n let hs = sin(hue);\n let sat = max(0.0, u_globals.grade_saturation);\n ypbpr = vec3<f32>(ypbpr.x, (ypbpr.y * hc - ypbpr.z * hs) * sat, (ypbpr.y * hs + ypbpr.z * hc) * sat);\n var rgb = vec3<f32>(ypbpr.x + 1.5748 * ypbpr.z,\n ypbpr.x - 0.187324 * ypbpr.y - 0.468124 * ypbpr.z,\n ypbpr.x + 1.8556 * ypbpr.y);\n rgb = (rgb - vec3<f32>(0.5)) * max(0.0, u_globals.grade_contrast) + vec3<f32>(0.5);\n return max(rgb * u_globals.grade_tint * exp2(u_globals.grade_exposure), vec3<f32>(0.0));\n}\n// Sun transmittance at the viewer (reddens/dims the lobes at low sun automatically)\nfn ibl_sun_t(sun_dir: vec3<f32>) -> vec3<f32> {\n return sun_transmittance(vec3<f32>(0.0, R_BOTTOM + VIEW_H, 0.0), sun_dir);\n}\n// Azimuthal sun-side factor: the artist bake's directionality is mostly horizontal (sun-side\n// faces ~1.8x opposite, +Y near average), and the real directional light already covers the\n// top-down sun energy - so boost only the horizontal normal component (floors stay dark).\nfn ibl_sun_azimuth(n: vec3<f32>, sun_dir: vec3<f32>) -> f32 {\n let sun_h = vec3<f32>(sun_dir.x, 0.0, sun_dir.z);\n return max(dot(n, normalize(sun_h + vec3<f32>(1e-5, 0.0, 0.0))), 0.0);\n}\n\n@fragment\nfn fs_irradiance(in: VertexOutput) -> @location(0) vec4<f32> {\n let n = cube_dir(u_draw.face, in.uv); // world space\n let tbn = tangent_basis(n);\n // Per-pixel golden-angle rotation of the fixed Hammersley set (interleaved gradient\n // noise, stable across regens): turns structured banding into sub-noise the 64px\n // bilinear lookup smooths away.\n let jitter = 2.0 * PI * fract(52.9829189 * fract(dot(in.position.xy, vec2<f32>(0.06711056, 0.00583715))));\n var sum = vec3<f32>(0.0);\n let n_samples = 1024u;\n // ~8px pre-blur: the cosine kernel spans the hemisphere so this barely biases the\n // integral, but it kills the sun-side variance that showed as per-texel blotch.\n let src_mip = log2(u_globals.day_cube_res) - 3.0;\n for (var i = 0u; i < n_samples; i++) {\n let xi = hammersley(i, n_samples);\n // cosine-weighted hemisphere\n let phi = 2.0 * PI * xi.x + jitter;\n let cos_th = sqrt(1.0 - xi.y);\n let sin_th = sqrt(xi.y);\n let l = tbn * vec3<f32>(sin_th * cos(phi), sin_th * sin(phi), cos_th);\n sum += sample_env(l, src_mip);\n }\n let sun_dir = normalize(u_globals.sun_dir_ws);\n let boost = vec3<f32>(1.0) + u_globals.ibl_sun_diffuse * ibl_sun_azimuth(n, sun_dir) * ibl_sun_t(sun_dir);\n return vec4<f32>(ibl_grade(sum / f32(n_samples) * boost), 1.0);\n}\n\n// ---------- IBL: GGX prefilter (split-sum; mip i baked at roughness=(i/(N-1))^5\n// to invert main_pass's `pow(roughness, 1/5) * (mip_count-1)` lookup) ----------\nfn importance_ggx(xi: vec2<f32>, roughness: f32, n: vec3<f32>, jitter: f32) -> vec3<f32> {\n let a = roughness * roughness;\n let phi = 2.0 * PI * xi.x + jitter;\n let cos_th = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));\n let sin_th = sqrt(1.0 - cos_th * cos_th);\n let h = vec3<f32>(sin_th * cos(phi), sin_th * sin(phi), cos_th);\n return tangent_basis(n) * h;\n}\n\n@fragment\nfn fs_prefilter(in: VertexOutput) -> @location(0) vec4<f32> {\n let n = cube_dir(u_draw.face, in.uv); // world space; N = V = R\n let roughness = u_draw.roughness;\n if (roughness < 0.001) {\n return vec4<f32>(ibl_grade(sample_env(n, 0.0)), 1.0);\n }\n let n_samples = 64u;\n let sa_texel = 4.0 * PI / (6.0 * u_globals.day_cube_res * u_globals.day_cube_res);\n // Same per-pixel golden-angle decorrelation as fs_irradiance (fixed sets band).\n let jitter = 2.0 * PI * fract(52.9829189 * fract(dot(in.position.xy, vec2<f32>(0.06711056, 0.00583715))));\n var sum = vec3<f32>(0.0);\n var weight = 0.0;\n for (var i = 0u; i < n_samples; i++) {\n let xi = hammersley(i, n_samples);\n let h = importance_ggx(xi, roughness, n, jitter);\n let l = normalize(2.0 * dot(n, h) * h - n);\n let ndl = dot(n, l);\n if (ndl > 0.0) {\n let ndh = max(dot(n, h), 0.0);\n let a = roughness * roughness;\n let d_ggx = a * a / (PI * pow(ndh * ndh * (a * a - 1.0) + 1.0, 2.0));\n let pdf = d_ggx / 4.0 + 1e-4; // N=V=R so VdotH == NdotH\n\n let sa_sample = 1.0 / (f32(n_samples) * pdf);\n let level = max(0.5 * log2(sa_sample / sa_texel), 0.0);\n sum += sample_env(l, level) * ndl;\n weight += ndl;\n }\n }\n // Analytic sun highlight (N = V = R): peak-normalized GGX lobe at h = half(n, sun),\n // blending to the diffuse cosine lobe as the mip goes fully rough.\n let sun_dir = normalize(u_globals.sun_dir_ws);\n let a_s = roughness * roughness;\n let a2_s = a_s * a_s;\n let ndh_s = max(dot(n, normalize(n + sun_dir)), 0.0);\n let lobe = pow(a2_s / (ndh_s * ndh_s * (a2_s - 1.0) + 1.0), 2.0);\n let lobe_mix = mix(lobe, ibl_sun_azimuth(n, sun_dir), a_s); // rough mips go azimuthal like irradiance\n let boost = vec3<f32>(1.0) + u_globals.ibl_sun_spec * lobe_mix * ibl_sun_t(sun_dir);\n return vec4<f32>(ibl_grade(sum / max(weight, 1e-4) * boost), 1.0);\n}\n\n// ---------- BRDF integration LUT (Karis split-sum scale/bias) ----------\n@fragment\nfn fs_brdf_lut(in: VertexOutput) -> @location(0) vec4<f32> {\n let ndv = max(in.uv.x, 1e-3);\n // main_pass samples at vec2(NdotV, roughness) with v-down texture coords -> roughness = uv.y\n let roughness = max(in.uv.y, 1e-3);\n let v = vec3<f32>(sqrt(1.0 - ndv * ndv), 0.0, ndv);\n let n = vec3<f32>(0.0, 0.0, 1.0);\n var scale = 0.0;\n var bias = 0.0;\n let n_samples = 512u;\n for (var i = 0u; i < n_samples; i++) {\n let xi = hammersley(i, n_samples);\n let h = importance_ggx(xi, roughness, n, 0.0);\n let l = normalize(2.0 * dot(v, h) * h - v);\n let ndl = l.z;\n if (ndl > 0.0) {\n let ndh = max(h.z, 0.0);\n let vdh = max(dot(v, h), 0.0);\n let k = roughness * roughness / 2.0;\n let g_v = ndv / (ndv * (1.0 - k) + k);\n let g_l = ndl / (ndl * (1.0 - k) + k);\n let g_vis = g_v * g_l * vdh / max(ndh * ndv, 1e-4);\n let fc = pow(1.0 - vdh, 5.0);\n scale += (1.0 - fc) * g_vis;\n bias += fc * g_vis;\n }\n }\n return vec4<f32>(scale / f32(n_samples), bias / f32(n_samples), 0.0, 1.0);\n}\n"},{"label":"Skybox Shader","code":"\n const DO_GAMMA: bool = false;\n // skybox.wgsl\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n@group(1) @binding(0) var skybox_texture: texture_cube<f32>;\n@group(1) @binding(1) var skybox_sampler: sampler;\n// Sky billboard slot (moon/galaxy/gate): textured quad at a sky direction.\nstruct SkyBillboard {\n dir_size: vec4<f32>, // xyz = dir_ws (true world space, normalized), w = tan(half angular size); <=0 = hidden\n tint_roll: vec4<f32>, // rgb = tint * intensity (HDR), w = roll (radians)\n misc: vec4<f32>, // x = additive flag (0 = alpha over sky, 1 = additive)\n};\nstruct SkyboxParams {\n // sun overlay\n sun_dir_ws: vec3<f32>,\n sun_radius: f32,\n sun_color: vec3<f32>,\n halo: f32,\n halo_falloff: f32,\n halo_intensity: f32,\n horizon_tilt_sin: f32,\n skybox_yaw: f32, // rotation around up axis (radians)\n skybox_tint: vec3<f32>,\n skybox_saturation: f32,\n skybox_exposure: f32,\n skybox_hue: f32,\n skybox_contrast: f32,\n star_intensity: f32, // >0 = night sky: render procedural star overlay\n // x = camera world Y (meters) for the folded background height fog; yzw spare.\n fog_misc: vec4<f32>,\n billboards: array<SkyBillboard, 4>,\n};\nstruct FogSettings {\n color: vec3<f32>,\n mode: u32,\n start: f32,\n end_: f32,\n density: f32,\n height_enabled: u32,\n height_weight: f32,\n height_bottom: f32,\n height_top: f32,\n height_softness: f32,\n sky_affect: f32, // 0..1: how much full-distance fog covers the sky background\n};\n@group(2) @binding(0) var<uniform> u_skybox: SkyboxParams;\n@group(2) @binding(1) var<uniform> u_fog: FogSettings;\n@group(3) @binding(0) var sky_billboard_tex0: texture_2d<f32>;\n@group(3) @binding(1) var sky_billboard_tex1: texture_2d<f32>;\n@group(3) @binding(2) var sky_billboard_tex2: texture_2d<f32>;\n@group(3) @binding(3) var sky_billboard_tex3: texture_2d<f32>;\n@group(3) @binding(4) var sky_billboard_sampler: sampler;\n\n// Projects the view ray onto the billboard's tangent plane; returns (u, v, inside-mask).\n// UV is always continuous/clamped so the unconditional textureSample keeps valid derivatives.\nfn sky_billboard_uvw(dir: vec3<f32>, bb: SkyBillboard) -> vec3<f32> {\n let fwd = bb.dir_size.xyz;\n let tan_half = bb.dir_size.w;\n let denom = dot(dir, fwd);\n // Basis around fwd (world-up reference, X fallback near zenith), then roll.\n var up_ref = vec3<f32>(0.0, 1.0, 0.0);\n if (abs(fwd.y) > 0.99) { up_ref = vec3<f32>(1.0, 0.0, 0.0); }\n let right0 = normalize(cross(up_ref, fwd));\n let up0 = cross(fwd, right0);\n let cr = cos(bb.tint_roll.w);\n let sr = sin(bb.tint_roll.w);\n let right = right0 * cr + up0 * sr;\n let up = up0 * cr - right0 * sr;\n let inv_denom = 1.0 / max(denom, 1e-3);\n let plane = vec2<f32>(dot(dir, right), -dot(dir, up)) * inv_denom;\n let uv = plane / max(tan_half, 1e-4) * 0.5 + vec2<f32>(0.5);\n let inside = all(uv >= vec2<f32>(0.0)) && all(uv <= vec2<f32>(1.0)) && denom > 0.0 && tan_half > 0.0;\n return vec3<f32>(clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)), select(0.0, 1.0, inside));\n}\nfn apply_sky_billboard(color: vec3<f32>, tex: vec4<f32>, bb: SkyBillboard, mask: f32) -> vec3<f32> {\n let rgb = tex.rgb * bb.tint_roll.rgb;\n let a = tex.a * mask;\n return mix(mix(color, rgb, a), color + rgb * a, bb.misc.x);\n}\n\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n return vec3<f32>(y, -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b, 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(ypbpr.x + 1.5748 * ypbpr.z, ypbpr.x - 0.187324 * ypbpr.y - 0.468124 * ypbpr.z, ypbpr.x + 1.8556 * ypbpr.y);\n}\n\nfn apply_skybox_grade(color: vec3<f32>) -> vec3<f32> {\n var ypbpr = rgb_to_ypbpr709(color);\n let hue = radians(u_skybox.skybox_hue);\n let c = cos(hue);\n let s = sin(hue);\n ypbpr = vec3<f32>(ypbpr.x, (ypbpr.y * c - ypbpr.z * s) * max(0.0, u_skybox.skybox_saturation), (ypbpr.y * s + ypbpr.z * c) * max(0.0, u_skybox.skybox_saturation));\n var rgb = ypbpr709_to_rgb(ypbpr);\n rgb = (rgb - vec3<f32>(0.5)) * max(0.0, u_skybox.skybox_contrast) + vec3<f32>(0.5);\n return rgb * u_skybox.skybox_tint * exp2(u_skybox.skybox_exposure);\n}\n\nfn hash33(p_in: vec3<f32>) -> vec3<f32> {\n var p = fract(p_in * vec3<f32>(0.1031, 0.1030, 0.0973));\n p += dot(p, p.yxz + 33.33);\n return fract((p.xxy + p.yxx) * p.zyx);\n}\n\n// One voronoi-style star layer: one candidate star per grid cell, host chance + brightness\n// from the cell hash. d is in cell units, so higher cell counts = smaller angular stars.\nfn star_layer(uv: vec2<f32>, face: f32, cells: f32, seed: f32, host: f32, mag_scale: f32, core_r: f32) -> vec3<f32> {\n let g = (uv * 0.5 + 0.5) * cells;\n let cell = floor(g);\n let h = hash33(vec3<f32>(cell, face * 17.0 + seed));\n if (h.z < host) { return vec3<f32>(0.0); }\n let star_pos = cell + vec2<f32>(0.1) + h.xy * 0.8;\n let d = length(g - star_pos);\n let t = (h.z - host) / (1.0 - host);\n let mag = (pow(t, 2.5) * 6.0 + 0.12) * mag_scale;\n let core = smoothstep(core_r, 0.0, d) + exp(-d * d * 32.0) * 0.05; // crisp core + tight glow\n let temp = fract(h.z * 7.31);\n let col = mix(vec3<f32>(1.0, 0.92, 0.82), vec3<f32>(0.72, 0.82, 1.0), temp);\n return col * (mag * core);\n}\n\n// Milky way band normal; must match MW_N in sky_gen.wgsl night_radiance (baked glow).\nconst MW_N: vec3<f32> = vec3<f32>(0.4211, 0.3609, 0.8321);\n\n// Procedural stars (night only). Kept as an overlay (like the sun disc) instead of baked into\n// the low-res night cube so they stay crisp at screen resolution. Three layers at different\n// frequencies give a fractal mix: rare bright stars, the main field, and fine faint dust.\nfn star_field(dir: vec3<f32>) -> vec3<f32> {\n // dominant-axis cube projection -> per-face cell grid\n let ad = abs(dir);\n var uv: vec2<f32>;\n var face = 0.0;\n if (ad.x >= ad.y && ad.x >= ad.z) {\n uv = dir.yz / ad.x;\n face = select(0.0, 1.0, dir.x < 0.0);\n } else if (ad.y >= ad.z) {\n uv = dir.xz / ad.y;\n face = select(2.0, 3.0, dir.y < 0.0);\n } else {\n uv = dir.xy / ad.z;\n face = select(4.0, 5.0, dir.z < 0.0);\n }\n // cube coord -> world dir is a z-flip (matches night cube bake space in sky_gen.wgsl)\n let mw = exp(-pow(dot(vec3<f32>(dir.x, dir.y, -dir.z), MW_N), 2.0) * 30.0);\n var s = star_layer(uv, face, 34.0, 3.0, 0.90, 0.65, 0.12);\n s += star_layer(uv, face, 70.0, 5.0, 0.70 - 0.10 * mw, 0.45, 0.09);\n s += star_layer(uv, face, 150.0, 9.0, 0.60 - 0.30 * mw, 0.28, 0.08);\n return s;\n}\n\nstruct VertexInput {\n @location(0) position: vec3<f32>\n};\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) pos: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vertex_index : u32) -> VertexOutput {\n var pos: vec2<f32>;\n switch (vertex_index) {\n case 0u: { pos = vec2<f32>(-1.0, -1.0); }\n case 1u: { pos = vec2<f32>( 3.0, -1.0); }\n default: { pos = vec2<f32>(-1.0, 3.0); }\n }\n var vs_out: VertexOutput;\n vs_out.position = vec4<f32>(pos, 1.0, 1.0);\n vs_out.pos = vs_out.position;\n return vs_out;\n}\n@fragment\nfn fs_main(vs_out: VertexOutput) -> @location(0) vec4<f32> {\n let flip = vec3<f32>(1.0, 1.0, -1.0);\n\n let t = u_camera.inverse_view_proj * vs_out.pos;\n // World-space view direction (after projection inversion and cubemap Z-flip)\n let dir_world = normalize((t.xyz / t.w) * flip);\n\n // Build sampling direction for the cubemap:\n // apply yaw around world up (Y), then a global horizon tilt.\n let c = cos(u_skybox.skybox_yaw);\n let s = sin(u_skybox.skybox_yaw);\n let rot_x = dir_world.x * c - dir_world.z * s;\n let rot_z = dir_world.x * s + dir_world.z * c;\n let dir_y = dir_world.y + u_skybox.horizon_tilt_sin;\n let dir_sample = normalize(vec3<f32>(rot_x, dir_y, rot_z));\n\n // Note: fog term may flood the lower hemisphere\n var color = textureSample(skybox_texture, skybox_sampler, dir_sample);\n color = vec4<f32>(apply_skybox_grade(color.rgb), color.a);\n\n // Sun overlay (HDR); skipped when the sun is hidden (night / SetSunVisible off).\n // Note: sun_dir is world-space; apply the same Z flip used for the cubemap.\n if (u_skybox.halo_intensity > 0.0) {\n let sun_dir = normalize(u_skybox.sun_dir_ws * flip);\n let r = max(u_skybox.sun_radius, 1e-4);\n let fall = max(u_skybox.halo_falloff, 0.5);\n\n // Compute sun overlay using the unrotated world-space view direction\n let cosA = clamp(dot(dir_world, sun_dir), -1.0, 1.0);\n let ang = acos(cosA);\n let core_edge = max(0.15 * r, 0.001); // soft core edge in [r, r + 0.15*r]\n let core = 1.0 - smoothstep(r, r + core_edge, ang);\n\n // broader halo extending out to ~r + halo, with extra falloff\n let halo_outer = max(r + u_skybox.halo, r + core_edge);\n let halo_raw = 1.0 - smoothstep(halo_outer, halo_outer * (1.0 + 0.35), ang);\n let halo = pow(max(halo_raw, 0.0), fall);\n\n let sun_rgb = (core + halo) * u_skybox.halo_intensity * u_skybox.sun_color;\n color = vec4<f32>(color.rgb + sun_rgb, color.a);\n }\n\n if (u_skybox.star_intensity > 0.0) {\n let horizon_fade = smoothstep(-0.02, 0.06, dir_sample.y);\n color = vec4<f32>(color.rgb + star_field(dir_sample) * (u_skybox.star_intensity * horizon_fade), color.a);\n }\n\n // Sky billboards (moon/galaxy/gate), composited last so alpha slots occlude sun/stars.\n // Math runs in true world space (unflip), independent of skybox yaw / cubemap Z-flip.\n // Each slot is skipped entirely when hidden (dir_size.w <= 0); the conditions are uniform\n // (uniform-buffer loads), so textureSample stays in uniform control flow.\n let dir_true = dir_world * flip;\n var bb_rgb = color.rgb;\n if (u_skybox.billboards[0].dir_size.w > 0.0) {\n let uvw = sky_billboard_uvw(dir_true, u_skybox.billboards[0]);\n let tex = textureSample(sky_billboard_tex0, sky_billboard_sampler, uvw.xy);\n bb_rgb = apply_sky_billboard(bb_rgb, tex, u_skybox.billboards[0], uvw.z);\n }\n if (u_skybox.billboards[1].dir_size.w > 0.0) {\n let uvw = sky_billboard_uvw(dir_true, u_skybox.billboards[1]);\n let tex = textureSample(sky_billboard_tex1, sky_billboard_sampler, uvw.xy);\n bb_rgb = apply_sky_billboard(bb_rgb, tex, u_skybox.billboards[1], uvw.z);\n }\n if (u_skybox.billboards[2].dir_size.w > 0.0) {\n let uvw = sky_billboard_uvw(dir_true, u_skybox.billboards[2]);\n let tex = textureSample(sky_billboard_tex2, sky_billboard_sampler, uvw.xy);\n bb_rgb = apply_sky_billboard(bb_rgb, tex, u_skybox.billboards[2], uvw.z);\n }\n if (u_skybox.billboards[3].dir_size.w > 0.0) {\n let uvw = sky_billboard_uvw(dir_true, u_skybox.billboards[3]);\n let tex = textureSample(sky_billboard_tex3, sky_billboard_sampler, uvw.xy);\n bb_rgb = apply_sky_billboard(bb_rgb, tex, u_skybox.billboards[3], uvw.z);\n }\n color = vec4<f32>(bb_rgb, color.a);\n\n // Background height fog (folded from the retired standalone fog pass; applied after all\n // sky content so sun/stars/billboards fog too, matching the old post-sky fullscreen pass).\n var sky_fog = 0.0;\n if (u_fog.height_enabled != 0u) {\n let cam_y = u_skybox.fog_misc.x;\n let sample_dist = 1000.0; // representative far distance\n let y_far = cam_y + dir_world.y * sample_dist;\n let lo = min(u_fog.height_bottom, u_fog.height_top);\n let hi = max(u_fog.height_bottom, u_fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(u_fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, y_far);\n let w = clamp(u_fog.height_weight, 0.0, 1.0);\n sky_fog = clamp(w * h, 0.0, 1.0);\n }\n // Distance fog on the sky background: at sky depth every fog mode saturates, so the\n // contribution is just sky_affect (0 = classic clear sky, 1 = fully fogged; the\n // underwater murk grade drives this to 1 while submerged). Combine multiplicatively\n // with the height band so neither term double-fogs.\n sky_fog = 1.0 - (1.0 - sky_fog) * (1.0 - clamp(u_fog.sky_affect, 0.0, 1.0));\n if (sky_fog > 0.0) {\n color = vec4<f32>(mix(color.rgb, u_fog.color, sky_fog), color.a);\n }\n\n if (DO_GAMMA) {\n color = pow(color, vec4<f32>(1.0/2.2));\n }\n return color;\n}\n "},{"label":"shaders/light_cluster_build.wgsl","code":"// light_cluster_build.wgsl\nstruct PointLight {\n position_radius: vec4<f32>, // xyz, r\n color_intensity: vec4<f32>, // rgb, I (unused in this pass)\n diffuse_spec_scale: vec4<f32>, // x = diffuse, y = specular, z = fade width, w = diffuse-only flag\n};\nstruct SpotLight {\n position_range: vec4<f32>, // xyz, range\n color_intensity: vec4<f32>,\n dir_cos_inner: vec4<f32>, // xyz = dir (world), w = cos(inner)\n cos_outer_pad: vec4<f32>, // x = cos(outer); y/z scales unused here\n};\nstruct AreaLight {\n center_radius: vec4<f32>, // xyz, range\n color_intensity: vec4<f32>,\n axis_u_half: vec4<f32>, // xyz=U (unit), w=halfWidth\n axis_v_half: vec4<f32>, // xyz=V (unit), w=halfHeight\n diffuse_spec_scale: vec4<f32>,\n};\nstruct ClusterLightUniform {\n view: mat4x4<f32>,\n view_proj: mat4x4<f32>,\n proj_scale: vec2<f32>,\n z_params: vec2<f32>,\n tile_counts: vec4<u32>, // x,y,z,maxLightsPerCluster\n inv_screen_size: vec2<f32>,\n num_point_lights: u32,\n num_spot_lights: u32,\n num_area_lights: u32,\n};\n\n@group(0) @binding(0) var<storage, read> point_lights: array<PointLight>;\n@group(0) @binding(1) var<storage, read> spot_lights: array<SpotLight>;\n@group(0) @binding(2) var<storage, read> area_lights: array<AreaLight>;\n@group(0) @binding(3) var<uniform> consts: ClusterLightUniform;\n\n@group(1) @binding(0) var<storage, read_write> clusterCounter : array<atomic<u32>>;\n@group(1) @binding(1) var<storage, read_write> cluster_light_indices : array<u32>;\n\nconst WG_SIZE : u32 = 64;\nconst SPOT_TYPE_BIT : u32 = 0x80000000u;\nconst AREA_TYPE_BIT : u32 = 0x40000000u;\n// Must match `main_pass.wgsl` so cluster influence matches shading.\n// special case we want reflecting point light on smooth floor (scifi)\n// - SPEC_RADIUS_SCALE 2.5, band 0.8, artist_squeeze = 0.2\nconst SPEC_RADIUS_SCALE : f32 = 1.25;\n\n// Shared function: cull a sphere (center, radius) and append `typedIndex`\nfn add_sphere_light(pos_world: vec3<f32>, radius: f32, typedIndex: u32) {\n var pos_view = (consts.view * vec4<f32>(pos_world, 1.0)).xyz;\n\n let depth = -pos_view.z;\n if (depth + radius <= 0.0) { return; }\n\n let near = consts.z_params.x;\n let far = consts.z_params.y;\n\n if (depth - radius > far || depth + radius < near) {\n return;\n }\n\n // z-slice range (log)\n let logDiv = log2(far / near);\n let zMin = clamp(\n u32(floor(log2(max(depth - radius, near) / near) * f32(consts.tile_counts.z) / logDiv)),\n 0u, consts.tile_counts.z - 1u);\n let zMax = clamp(\n u32(floor(log2(min(depth + radius, far ) / near) * f32(consts.tile_counts.z) / logDiv)),\n 0u, consts.tile_counts.z - 1u);\n\n // Conservative screen-space prefilter (cheap; keeps normal-case tile loop tight).\n // Degenerates to full-screen when the sphere straddles the near plane; the per-tile\n // sphere-vs-frustum test below (E443 fix) does the real culling in that case.\n let clip = (consts.view_proj * vec4<f32>(pos_world, 1.0));\n let w_abs = max(abs(clip.w), 1e-4);\n let ndc_xy = clip.xy / w_abs;\n let tilesX = f32(consts.tile_counts.x);\n let tilesY = f32(consts.tile_counts.y);\n let centre_tx = (ndc_xy.x * 0.5 + 0.5) * tilesX;\n let centre_ty = (0.5 - ndc_xy.y * 0.5) * tilesY;\n let denom = max(max(depth - radius, near), 1e-4);\n let screen_r_tiles_x = abs(radius * consts.proj_scale.x / denom) * 0.5 * tilesX;\n let screen_r_tiles_y = abs(radius * consts.proj_scale.y / denom) * 0.5 * tilesY;\n let xMin = clamp(u32(max(centre_tx - screen_r_tiles_x, 0.0)), 0u, consts.tile_counts.x - 1u);\n let xMax = clamp(u32(min(centre_tx + screen_r_tiles_x, tilesX - 1.0)), 0u, consts.tile_counts.x - 1u);\n let yMin = clamp(u32(max(centre_ty - screen_r_tiles_y, 0.0)), 0u, consts.tile_counts.y - 1u);\n let yMax = clamp(u32(min(centre_ty + screen_r_tiles_y, tilesY - 1.0)), 0u, consts.tile_counts.y - 1u);\n\n let invTilesX = 1.0 / tilesX;\n let invTilesY = 1.0 / tilesY;\n // Exact per-cluster sphere-vs-frustum rejection (E443 fix):\n // when the sphere straddles the near plane the prefilter degenerates to full-screen;\n // these 6 plane-vs-sphere tests cull the ~90% of lights that don't truly touch each tile.\n // View-space convention: camera at origin, forward = -Z (pos_view.z < 0 in front).\n let C = pos_view;\n let logRatio = log2(far / near) / f32(consts.tile_counts.z);\n let zSliceRatio = exp2(logRatio);\n\n for (var z = zMin; z <= zMax; z = z + 1u) {\n // Z-slab in view space (both bounds are <= 0; z_far more negative).\n let z_near_slice = -near * exp2(f32(z) * logRatio);\n let z_far_slice = z_near_slice * zSliceRatio;\n if (C.z - radius > z_near_slice) { continue; }\n if (C.z + radius < z_far_slice) { continue; }\n\n for (var y = yMin; y <= yMax; y = y + 1u) {\n let ndc_y_max = 1.0 - 2.0 * f32(y) * invTilesY; // tile top edge (uv.y smaller \u2192 ndc.y larger)\n let ndc_y_min = ndc_y_max - 2.0 * invTilesY; // tile bottom edge\n let tan_b = ndc_y_min / consts.proj_scale.y;\n let tan_t = ndc_y_max / consts.proj_scale.y;\n // Bottom plane: interior has C.y + tan_b*C.z >= 0. Reject if sphere fully below.\n if (C.y + tan_b * C.z < -radius * sqrt(1.0 + tan_b*tan_b)) { continue; }\n // Top plane: interior has C.y + tan_t*C.z <= 0. Reject if sphere fully above.\n if (C.y + tan_t * C.z > radius * sqrt(1.0 + tan_t*tan_t)) { continue; }\n\n for (var x = xMin; x <= xMax; x = x + 1u) {\n let ndc_x_min = 2.0 * f32(x) * invTilesX - 1.0; // tile left edge\n let ndc_x_max = ndc_x_min + 2.0 * invTilesX; // tile right edge\n let tan_l = ndc_x_min / consts.proj_scale.x;\n let tan_r = ndc_x_max / consts.proj_scale.x;\n if (C.x + tan_l * C.z < -radius * sqrt(1.0 + tan_l*tan_l)) { continue; }\n if (C.x + tan_r * C.z > radius * sqrt(1.0 + tan_r*tan_r)) { continue; }\n\n let clusterId = (z * consts.tile_counts.y + y) * consts.tile_counts.x + x;\n let idx = atomicAdd(&clusterCounter[clusterId], 1u);\n if (idx < consts.tile_counts.w) {\n cluster_light_indices[clusterId * consts.tile_counts.w + idx] = typedIndex;\n }\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// GATHER path (default; ORIVERSE_CLUSTER_GATHER=0 falls back to the scatter\n// entries below). One thread per CLUSTER looping the light arrays: 3,456\n// coherent threads instead of one long divergent loop per light (the scatter\n// ran a few hundred threads TOTAL \u2014 single-digit occupancy \u2014 and cost\n// 2.6-3.8ms on ms_japan1; see the [gpu_spike] harvest). Same 6-plane + z-slab\n// tests as the scatter's inner loops (sqrt factors hoisted per cluster), no\n// atomics, list order type-major like the scatter's sequential passes but now\n// deterministic within a type (index order); raw over-cap count semantics kept.\n// ---------------------------------------------------------------------------\n\nstruct ClusterFrustum {\n tan_l: f32, tan_r: f32, tan_b: f32, tan_t: f32,\n inv_len_l: f32, inv_len_r: f32, inv_len_b: f32, inv_len_t: f32,\n z_near_slice: f32, z_far_slice: f32,\n};\n\nfn cluster_frustum(x: u32, y: u32, z: u32) -> ClusterFrustum {\n let near = consts.z_params.x;\n let far = consts.z_params.y;\n let logRatio = log2(far / near) / f32(consts.tile_counts.z);\n let z_near_slice = -near * exp2(f32(z) * logRatio);\n let z_far_slice = z_near_slice * exp2(logRatio);\n let invTilesX = 1.0 / f32(consts.tile_counts.x);\n let invTilesY = 1.0 / f32(consts.tile_counts.y);\n let ndc_x_min = 2.0 * f32(x) * invTilesX - 1.0;\n let ndc_x_max = ndc_x_min + 2.0 * invTilesX;\n let ndc_y_max = 1.0 - 2.0 * f32(y) * invTilesY;\n let ndc_y_min = ndc_y_max - 2.0 * invTilesY;\n let tan_l = ndc_x_min / consts.proj_scale.x;\n let tan_r = ndc_x_max / consts.proj_scale.x;\n let tan_b = ndc_y_min / consts.proj_scale.y;\n let tan_t = ndc_y_max / consts.proj_scale.y;\n return ClusterFrustum(\n tan_l, tan_r, tan_b, tan_t,\n sqrt(1.0 + tan_l * tan_l), sqrt(1.0 + tan_r * tan_r),\n sqrt(1.0 + tan_b * tan_b), sqrt(1.0 + tan_t * tan_t),\n z_near_slice, z_far_slice,\n );\n}\n\n// Identical tests to the scatter's inner loops (E443 semantics preserved).\nfn sphere_touches_cluster(C: vec3<f32>, radius: f32, f: ClusterFrustum) -> bool {\n if (C.z - radius > f.z_near_slice) { return false; }\n if (C.z + radius < f.z_far_slice) { return false; }\n if (C.y + f.tan_b * C.z < -radius * f.inv_len_b) { return false; }\n if (C.y + f.tan_t * C.z > radius * f.inv_len_t) { return false; }\n if (C.x + f.tan_l * C.z < -radius * f.inv_len_l) { return false; }\n if (C.x + f.tan_r * C.z > radius * f.inv_len_r) { return false; }\n return true;\n}\n\n@compute @workgroup_size(WG_SIZE)\nfn cs_gather(@builtin(global_invocation_id) gid: vec3<u32>) {\n let cid = gid.x;\n let n_clusters = consts.tile_counts.x * consts.tile_counts.y * consts.tile_counts.z;\n if (cid >= n_clusters) { return; }\n // Same linearization as the scatter append: cid = (z*tilesY + y)*tilesX + x.\n let x = cid % consts.tile_counts.x;\n let y = (cid / consts.tile_counts.x) % consts.tile_counts.y;\n let z = cid / (consts.tile_counts.x * consts.tile_counts.y);\n let f = cluster_frustum(x, y, z);\n\n let cap = consts.tile_counts.w;\n let base = cid * cap;\n var count: u32 = 0u;\n\n for (var i = 0u; i < consts.num_point_lights; i = i + 1u) {\n let pl = point_lights[i];\n let radius = select(pl.position_radius.w * SPEC_RADIUS_SCALE, pl.position_radius.w, pl.diffuse_spec_scale.w > 0.5);\n let C = (consts.view * vec4<f32>(pl.position_radius.xyz, 1.0)).xyz;\n if (-C.z + radius <= 0.0) { continue; }\n if (sphere_touches_cluster(C, radius, f)) {\n if (count < cap) { cluster_light_indices[base + count] = i; }\n count = count + 1u;\n }\n }\n for (var i = 0u; i < consts.num_spot_lights; i = i + 1u) {\n let sl = spot_lights[i];\n let radius = sl.position_range.w * SPEC_RADIUS_SCALE;\n let C = (consts.view * vec4<f32>(sl.position_range.xyz, 1.0)).xyz;\n if (-C.z + radius <= 0.0) { continue; }\n if (sphere_touches_cluster(C, radius, f)) {\n if (count < cap) { cluster_light_indices[base + count] = (i | SPOT_TYPE_BIT); }\n count = count + 1u;\n }\n }\n for (var i = 0u; i < consts.num_area_lights; i = i + 1u) {\n let al = area_lights[i];\n let radius = max(al.center_radius.w * SPEC_RADIUS_SCALE, 1e-4);\n let C = (consts.view * vec4<f32>(al.center_radius.xyz, 1.0)).xyz;\n if (-C.z + radius <= 0.0) { continue; }\n if (sphere_touches_cluster(C, radius, f)) {\n if (count < cap) { cluster_light_indices[base + count] = (i | AREA_TYPE_BIT); }\n count = count + 1u;\n }\n }\n atomicStore(&clusterCounter[cid], count);\n}\n\n@compute @workgroup_size(WG_SIZE)\nfn cs_points(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= consts.num_point_lights) { return; }\n let pl = point_lights[i];\n let cluster_radius = select(pl.position_radius.w * SPEC_RADIUS_SCALE, pl.position_radius.w, pl.diffuse_spec_scale.w > 0.5);\n add_sphere_light(pl.position_radius.xyz, cluster_radius, /*typedIndex*/ i);\n}\n\n@compute @workgroup_size(WG_SIZE)\nfn cs_spots(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= consts.num_spot_lights) { return; }\n let sl = spot_lights[i];\n\n // Conservative culling: sphere centered at apex with radius = range\n // (Phase 2 later: tighter cone bounds)\n let typedIndex = (i | SPOT_TYPE_BIT);\n add_sphere_light(sl.position_range.xyz, sl.position_range.w * SPEC_RADIUS_SCALE, typedIndex);\n}\n\n@compute @workgroup_size(WG_SIZE)\nfn cs_areas(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= consts.num_area_lights) { return; }\n let al = area_lights[i];\n let typedIndex = (i | AREA_TYPE_BIT);\n let cluster_r = max(al.center_radius.w * SPEC_RADIUS_SCALE, 1e-4);\n add_sphere_light(al.center_radius.xyz, cluster_r, typedIndex);\n}"},{"label":"shaders/shadow_directional.wgsl","code":"// shadow_directional.wgsl\nstruct LightCamera {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> light_camera: LightCamera;\n\n@vertex\nfn vs_main(\n @location(0) position: vec3<f32>,\n @location(4) model_mat_0: vec4<f32>,\n @location(5) model_mat_1: vec4<f32>,\n @location(6) model_mat_2: vec4<f32>,\n @location(7) model_mat_3: vec4<f32>,\n @interpolate(flat) @location(10) inst_flags: u32,\n) -> @builtin(position) vec4<f32> {\n let model = mat4x4<f32>(\n model_mat_0, model_mat_1, model_mat_2, model_mat_3\n );\n // bit0: no_shadow; bit9: death-dissolve (fading mesh particles stop casting whole,\n // the same convention as the skinned shadow shaders).\n if ((inst_flags & 1u) == 1u || (inst_flags & (1u << 9u)) != 0u) {\n // Fully clip this instance by placing it outside clip-space\n // Ensures zero rasterization on all hardware/drivers\n return vec4<f32>(2.0, 2.0, 2.0, 1.0);\n }\n var pos = light_camera.view_proj * (model * vec4<f32>(position, 1.0));\n // Shadow pancaking: casters in front of the light near plane clamp onto it\n // instead of being clipped (tall casters must still shadow this cascade).\n pos.z = max(pos.z, 0.0);\n return pos;\n}"},{"label":"shaders/shadow_directional_skinned.wgsl","code":"// shadow_directional_skinned.wgsl\nstruct CascadeData { \n lightViewProj : mat4x4<f32>, \n splitDepth : f32, \n _pad : vec3<f32>,\n};\n@group(0) @binding(0) var<uniform> uCascade: CascadeData;\n@group(1) @binding(0) var<storage, read> palette: array<mat4x4<f32>>;\n\nstruct VSOut { @builtin(position) pos: vec4<f32> };\n\n@vertex\nfn vs_main( // SkinnedVertex\n @location(0) pos: vec3<f32>,\n @location(4) joint_indices: vec4<u32>,\n @location(5) joint_weights: vec4<f32>,\n\n // per\u2011instance\n @location(6) m0: vec4<f32>,\n @location(7) m1: vec4<f32>,\n @location(8) m2: vec4<f32>,\n @location(9) m3: vec4<f32>,\n\n @location(12) anim_misc: vec4<u32>,\n) -> VSOut {\n const DEATH_DISSOLVE_BIT: u32 = 1u << 9u;\n // bit0: no_shadow\n if ((anim_misc.x & 1u) == 1u || (anim_misc.x & DEATH_DISSOLVE_BIT) != 0u) {\n var o : VSOut;\n // Fully clip this instance by placing it outside clip-space\n o.pos = vec4<f32>(2.0, 2.0, 2.0, 1.0);\n return o;\n }\n // fetch skin matrices\n let paletteOffset = anim_misc.w;\n let base = paletteOffset + joint_indices;\n\n // Normalise the weight vector\n let weight_sum = max(joint_weights.x + joint_weights.y + joint_weights.z + joint_weights.w, 1e-5);\n let weights = joint_weights / weight_sum;\n\n // linear\u2011blend\u2011skin\n var skinned = vec4<f32>(pos, 1.0);\n skinned = palette[base.x] * skinned * weights.x\n + palette[base.y] * skinned * weights.y\n + palette[base.z] * skinned * weights.z\n + palette[base.w] * skinned * weights.w;\n\n // model \u2192 world\n let model = mat4x4<f32>(m0, m1, m2, m3);\n let world = model * skinned;\n\n var o : VSOut;\n o.pos = uCascade.lightViewProj * world;\n o.pos.z = max(o.pos.z, 0.0); // shadow pancaking\n return o;\n}\n"},{"label":"shaders/shadow_local_clear.wgsl","code":"// shadow_local_clear.wgsl\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {\n var pos: vec2<f32>;\n switch (vi) {\n case 0u: { pos = vec2<f32>(-1.0, -1.0); }\n case 1u: { pos = vec2<f32>( 3.0, -1.0); }\n default: { pos = vec2<f32>(-1.0, 3.0); }\n }\n return vec4<f32>(pos, 1.0, 1.0);\n}\n@fragment\nfn fs_depth1() -> @builtin(frag_depth) f32 {\n return 1.0;\n}\n"},{"label":"shaders/static_lod_debug.wgsl","code":"// Debug-only static LOD classifier. It does not affect rendering.\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nstruct StaticLodDebugParams {\n instance_count: u32,\n group_slot: u32,\n lod_mask: u32,\n target_lod: u32,\n lod1_screen_size: f32,\n lod2_screen_size: f32,\n lod3_screen_size: f32,\n max_distance_m: f32,\n impostor_screen_size: f32, // mesh -> impostor handoff, 0 = disabled\n _pad_a: f32,\n _pad_b: f32,\n _pad_c: f32,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\nstruct InstanceAABB {\n min: vec3<f32>,\n occlusion_depth_margin: f32,\n max: vec3<f32>,\n _pad1: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera_data: CameraUniform;\n@group(0) @binding(1) var<uniform> params: StaticLodDebugParams;\n@group(0) @binding(2) var<storage, read> bounds_data: array<InstanceAABB>;\n@group(0) @binding(3) var<storage, read_write> stats: array<atomic<u32>>;\n\nconst STAT_COUNT: u32 = 12u;\nconst STAT_TOTAL: u32 = 0u;\nconst STAT_VISIBLE: u32 = 1u;\nconst STAT_LOD0: u32 = 2u;\nconst STAT_LOD1: u32 = 3u;\nconst STAT_LOD2: u32 = 4u;\nconst STAT_LOD3: u32 = 5u;\nconst STAT_FALLBACK: u32 = 6u;\nconst STAT_RESERVED: u32 = 7u;\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < 0.0;\n}\n\nfn aabb_visible(bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let m = camera_data.view_proj;\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2),\n );\n for (var i = 0u; i < 6u; i = i + 1u) {\n if (aabb_outside_plane(planes[i], bmin, bmax)) {\n return false;\n }\n }\n return true;\n}\n\nfn selected_lod(screen_size: f32) -> u32 {\n if (screen_size <= params.lod3_screen_size) {\n return 3u;\n }\n if (screen_size <= params.lod2_screen_size) {\n return 2u;\n }\n if (screen_size <= params.lod1_screen_size) {\n return 1u;\n }\n return 0u;\n}\n\nfn resolve_lod(lod: u32) -> u32 {\n var l = lod;\n loop {\n if ((params.lod_mask & (1u << l)) != 0u) {\n return l;\n }\n if (l == 0u) {\n break;\n }\n l = l - 1u;\n }\n for (var up = lod + 1u; up < 4u; up = up + 1u) {\n if ((params.lod_mask & (1u << up)) != 0u) {\n return up;\n }\n }\n return 0xFFFFFFFFu;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.instance_count) {\n return;\n }\n\n let base = params.group_slot * STAT_COUNT;\n atomicAdd(&stats[base + STAT_TOTAL], 1u);\n\n let bb = bounds_data[idx];\n let bmin = bb.min;\n let bmax = bb.max;\n if (!aabb_visible(bmin, bmax)) {\n return;\n }\n\n let center = (bmin + bmax) * 0.5;\n let distance_m = distance(camera_data.camera_position, center);\n if (params.max_distance_m > 0.0 && distance_m > params.max_distance_m) {\n return;\n }\n // occlusion_depth_margin carries the CPU-precomputed sphere screen size on this path.\n if (params.impostor_screen_size > 0.0\n && bounds_data[idx].occlusion_depth_margin < params.impostor_screen_size) {\n return;\n }\n atomicAdd(&stats[base + STAT_VISIBLE], 1u);\n\n let wanted_lod = selected_lod(bounds_data[idx].occlusion_depth_margin);\n let lod = resolve_lod(wanted_lod);\n if (lod == 0xFFFFFFFFu) {\n atomicAdd(&stats[base + STAT_FALLBACK], 1u);\n return;\n }\n if (lod != wanted_lod) {\n atomicAdd(&stats[base + STAT_FALLBACK], 1u);\n }\n switch lod {\n case 0u: { atomicAdd(&stats[base + STAT_LOD0], 1u); }\n case 1u: { atomicAdd(&stats[base + STAT_LOD1], 1u); }\n case 2u: { atomicAdd(&stats[base + STAT_LOD2], 1u); }\n default: { atomicAdd(&stats[base + STAT_LOD3], 1u); }\n }\n}\n"},{"label":"static_lod_global_prefix_classify","code":"// Shared frustum + Hi-Z occlusion helpers, prepended via include_str! concat to consumer\n// shaders (static_lod_global_prefix_classify.wgsl, skinned_hiz_cull.wgsl). Consumers must\n// declare the module-scope bindings `camera_data: CameraUniform` and\n// `depth_pyramid: texture_2d<f32>` (WGSL module-scope declarations are order-independent).\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < 0.0;\n}\n\n// Frustum test against an arbitrary view_proj (perspective or ortho \u2014 e.g. a shadow\n// cascade's light volume); plane extraction is form-agnostic.\nfn aabb_visible_vp(m: mat4x4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2),\n );\n for (var i = 0u; i < 6u; i = i + 1u) {\n if (aabb_outside_plane(planes[i], bmin, bmax)) { return false; }\n }\n return true;\n}\n\nfn aabb_visible(bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n return aabb_visible_vp(camera_data.view_proj, bmin, bmax);\n}\n\n// Hi-Z occlusion test against the last-frame MAX-depth pyramid ([0,1] depth, 0 = near, 1 = far).\n// Every coarse texel over-estimates occluder depth and `margin_m` pulls the instance toward the\n// camera before comparing, so all uncertainty (one frame of motion, TAA jitter, near-plane or\n// behind-camera corners, ortho cameras) resolves to \"visible\".\nfn hiz_visible(bmin: vec3<f32>, bmax: vec3<f32>, margin_m: f32) -> bool {\n // Margin math below assumes a perspective projection (clip.w = view depth).\n if (camera_data.proj[3][3] != 0.0) { return true; }\n var min_ndc = vec2<f32>(1e10, 1e10);\n var max_ndc = vec2<f32>(-1e10, -1e10);\n var nearest_z = 1e10;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let corner = vec3<f32>(\n select(bmin.x, bmax.x, (i & 1u) != 0u),\n select(bmin.y, bmax.y, (i & 2u) != 0u),\n select(bmin.z, bmax.z, (i & 4u) != 0u),\n );\n let clip = camera_data.view_proj * vec4<f32>(corner, 1.0);\n // Corner at/behind the camera plane, or within `margin_m` of it: depth is meaningless\n // there, keep visible (this also force-passes near-plane intersections).\n let w_adj = clip.w - margin_m;\n if (w_adj <= 1e-4) { return true; }\n // Pull the corner margin_m meters toward the camera along view depth:\n // clip.z = -c22*w + c32 => z(w - m) = clip.z + c22*m.\n let z_adj = clip.z + camera_data.proj[2][2] * margin_m;\n nearest_z = min(nearest_z, z_adj / w_adj);\n let ndc = clip.xy / clip.w;\n min_ndc = min(min_ndc, ndc);\n max_ndc = max(max_ndc, ndc);\n }\n if (nearest_z <= 0.0) { return true; }\n\n // NDC -> UV: y flips, so the y min/max swap sides (max_ndc.y becomes min_uv.y).\n var min_uv = vec2<f32>(min_ndc.x * 0.5 + 0.5, 0.5 - max_ndc.y * 0.5);\n var max_uv = vec2<f32>(max_ndc.x * 0.5 + 0.5, 0.5 - min_ndc.y * 0.5);\n min_uv = clamp(min_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n max_uv = clamp(max_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n\n // Pyramid mip0 is half the scene resolution (first reduction happens while reading live\n // depth); all footprint math below is in pyramid-texel space, so that only shifts every\n // selection one level coarser in screen terms (minimum granularity 2 screen px).\n let pyr_res = textureDimensions(depth_pyramid);\n let size_uv = max_uv - min_uv;\n let size_px = max(size_uv.x * f32(pyr_res.x), size_uv.y * f32(pyr_res.y));\n let num_mips = textureNumLevels(depth_pyramid);\n // Pick the mip where the footprint spans <= 2 texels per axis so the 2x2 gather covers it;\n // bump once if misalignment still crosses a third texel (guaranteed enough at half size).\n var mip = u32(clamp(ceil(log2((size_px + 1e-6) / 2.0)), 0.0, f32(num_mips - 1u)));\n var mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n var px00 = vec2<i32>(min_uv * mip_size);\n var px11 = vec2<i32>(max_uv * mip_size);\n if (px11.x > px00.x + 1 || px11.y > px00.y + 1) {\n mip = min(mip + 1u, num_mips - 1u);\n mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n px00 = vec2<i32>(min_uv * mip_size);\n px11 = vec2<i32>(max_uv * mip_size);\n }\n let d00 = textureLoad(depth_pyramid, px00, i32(mip)).r;\n let d01 = textureLoad(depth_pyramid, vec2<i32>(px00.x, px11.y), i32(mip)).r;\n let d10 = textureLoad(depth_pyramid, vec2<i32>(px11.x, px00.y), i32(mip)).r;\n let d11 = textureLoad(depth_pyramid, px11, i32(mip)).r;\n let occluder_depth = max(max(d00, d01), max(d10, d11));\n return nearest_z <= occluder_depth;\n}\n\n// Global static LOD classify for segmented prefix compaction.\n// Loaded with shaders/hiz_shared.wgsl prepended (CameraUniform, aabb_visible, hiz_visible).\n\nstruct PrefixParams {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n lod1_screen_size: f32,\n lod2_screen_size: f32,\n lod3_screen_size: f32,\n // Reflection view extras (main camera passes neutral values: -1e30 / 0).\n // Instances fully below this world-space Y are culled (mirror clips them anyway).\n cull_below_y: f32,\n // Instances projecting smaller than this (sphere_screen_size units) are culled.\n min_screen_size: f32,\n // Hi-Z occlusion test against depth_pyramid (main camera only; reflection binds a dummy).\n occlusion_enabled: u32,\n // Also write the pre-occlusion LOD to camera_selections_pre (shadow classify input).\n write_pre_selections: u32,\n _pad2: u32,\n};\n\nstruct BoundsMeta {\n min: vec3<f32>,\n sphere_radius: f32,\n max: vec3<f32>,\n group_slot: u32,\n sphere_center: vec3<f32>,\n // Hi-Z depth margin in meters: keep visible unless at least this far behind the occluder.\n occlusion_margin_m: f32,\n};\n\nstruct GroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32, // per-group render cutoff, 0 = unlimited (tree impostor ceiling)\n corr_identity_bits: u32, // bit lod set = lod_correction[lod] is identity (draw fast path)\n // Mesh -> impostor handoff: cut below this sphere screen size (0 = disabled).\n impostor_screen_size: f32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\nstruct ChunkMeta {\n group_slot: u32,\n local_chunk: u32,\n _pad0: vec2<u32>,\n};\n\nstruct Selection {\n lod: u32,\n local_rank: u32,\n};\n\n@group(0) @binding(0) var<uniform> camera_data: CameraUniform;\n@group(0) @binding(1) var<uniform> params: PrefixParams;\n@group(0) @binding(2) var<storage, read> bounds_data: array<BoundsMeta>;\n@group(0) @binding(3) var<storage, read> group_infos: array<GroupInfo>;\n@group(0) @binding(4) var<storage, read> chunk_meta: array<ChunkMeta>;\n@group(0) @binding(5) var<storage, read_write> selections: array<Selection>;\n// Cleared to zero by the CPU before this pass; each visible lane atomicAdd's its (chunk, lod)\n// entry, which yields both the per-segment count and this lane's within-chunk rank.\n@group(0) @binding(6) var<storage, read_write> chunk_counts: array<atomic<u32>>;\n// Last-frame MAX-depth (farthest) Hi-Z pyramid; 1x1 far-depth dummy when occlusion is off.\n@group(0) @binding(7) var depth_pyramid: texture_2d<f32>;\n// Pre-occlusion camera LODs for shadow classify (written only when write_pre_selections == 1).\n@group(0) @binding(8) var<storage, read_write> camera_selections_pre: array<Selection>;\n\nconst INVALID_LOD: u32 = 0xFFFFFFFFu;\nconst LOD_BUCKET_COUNT: u32 = 4u;\nconst WG_SIZE: u32 = 64u;\n\nfn selected_lod(screen_size: f32) -> u32 {\n if (screen_size <= params.lod3_screen_size) { return 3u; }\n if (screen_size <= params.lod2_screen_size) { return 2u; }\n if (screen_size <= params.lod1_screen_size) { return 1u; }\n return 0u;\n}\n\nfn sphere_screen_size(center: vec3<f32>, radius: f32) -> f32 {\n let projection_scale = max(0.5 * camera_data.proj[0][0], 0.5 * camera_data.proj[1][1]);\n let distance_m = max(distance(camera_data.camera_position, center), 1.0);\n return 2.0 * projection_scale * radius / distance_m;\n}\n\nfn resolve_lod(lod: u32, lod_mask: u32) -> u32 {\n var l = lod;\n loop {\n if ((lod_mask & (1u << l)) != 0u) { return l; }\n if (l == 0u) { break; }\n l = l - 1u;\n }\n for (var up = lod + 1u; up < 4u; up = up + 1u) {\n if ((lod_mask & (1u << up)) != 0u) { return up; }\n }\n return INVALID_LOD;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wid: vec3<u32>) {\n let chunk_id = wid.x;\n let lane = lid.x;\n var lod = INVALID_LOD;\n var global_idx = 0u;\n var local_chunk = 0u;\n var in_group = false;\n // Only the scalar GroupInfo fields we need: copying the whole struct (4 mat4 corrections)\n // into function space is heavy per-thread register/private-memory traffic.\n var chunk_entry_start = 0u;\n var group_chunk_count = 0u;\n\n if (chunk_id < params.chunk_count) {\n let cm = chunk_meta[chunk_id];\n let slot = cm.group_slot;\n let lod_mask = group_infos[slot].lod_mask;\n let group_max_distance_m = group_infos[slot].max_distance_m;\n let group_impostor_screen_size = group_infos[slot].impostor_screen_size;\n chunk_entry_start = group_infos[slot].chunk_entry_start;\n group_chunk_count = group_infos[slot].chunk_count;\n local_chunk = cm.local_chunk;\n let local_i = local_chunk * WG_SIZE + lane;\n in_group = local_i < group_infos[slot].source_count;\n global_idx = group_infos[slot].source_start + local_i;\n if (in_group && global_idx < params.instance_count) {\n let bb = bounds_data[global_idx];\n // GBuffer visibility culls by BoundsMeta min/max; LOD choice below uses only the group sphere.\n if (bb.max.y >= params.cull_below_y && aabb_visible(bb.min, bb.max)) {\n let screen_size = sphere_screen_size(bb.sphere_center, bb.sphere_radius);\n // Below the group's impostor threshold the billboard takes over (tree handoff).\n if (screen_size >= params.min_screen_size\n && (group_impostor_screen_size <= 0.0 || screen_size >= group_impostor_screen_size)) {\n let distance_m = distance(camera_data.camera_position, bb.sphere_center);\n if (group_max_distance_m <= 0.0 || distance_m <= group_max_distance_m) {\n lod = resolve_lod(selected_lod(screen_size), lod_mask);\n }\n }\n }\n // Pre-occlusion LOD (shadow classify input; shadow casters ignore occlusion).\n if (params.write_pre_selections == 1u) {\n camera_selections_pre[global_idx] = Selection(lod, 0u);\n }\n if (lod != INVALID_LOD && params.occlusion_enabled == 1u\n && !hiz_visible(bb.min, bb.max, bb.occlusion_margin_m)) {\n lod = INVALID_LOD;\n }\n }\n }\n\n // Per-segment counts + within-chunk ranks via one atomicAdd per visible lane. This replaces\n // a workgroup-shared wg_lods array + barrier + lane-0 counting loop: that pattern miscompiled\n // on native Vulkan (lane 0 observed phantom LOD0 entries for lanes that wrote INVALID_LOD,\n // inflating per-segment counts ~32x on single-instance groups and corrupting the packed\n // compaction layout scene-wide). chunk_counts is cleared by the CPU before this pass, so\n // empty segments stay zero. Ranks come out in atomic arrival order, which is fine: the\n // scatter only needs rank uniqueness within a (chunk, lod) segment.\n if (in_group && global_idx < params.instance_count) {\n var rank = 0u;\n if (lod != INVALID_LOD) {\n let entry = chunk_entry_start + local_chunk + lod * group_chunk_count;\n rank = atomicAdd(&chunk_counts[entry], 1u);\n }\n selections[global_idx] = Selection(lod, rank);\n }\n}\n"},{"label":"shaders/static_lod_global_prefix_write_counts.wgsl","code":"// Convert scanned chunk counts into per-group/per-LOD bucket counts.\n\nstruct PrefixParams {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n};\n\nstruct GroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32, // per-group render cutoff, 0 = unlimited (checked at classify)\n _pad0a: u32,\n _pad0b: u32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\n@group(0) @binding(0) var<uniform> params: PrefixParams;\n@group(0) @binding(1) var<storage, read> group_infos: array<GroupInfo>;\n@group(0) @binding(2) var<storage, read> chunk_counts: array<u32>;\n@group(0) @binding(3) var<storage, read_write> bucket_counts: array<u32>;\n// Packed segment start (first_instance) per (group, lod) into the single compact_instances buffer.\n@group(0) @binding(4) var<storage, read_write> segment_offsets: array<u32>;\n\nconst LOD_BUCKET_COUNT: u32 = 4u;\n\nfn prefix_before(index: u32) -> u32 {\n if (index == 0u) { return 0u; }\n return chunk_counts[index - 1u];\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n let group_slot = idx / LOD_BUCKET_COUNT;\n let lod = idx % LOD_BUCKET_COUNT;\n if (group_slot >= params.group_count) { return; }\n\n let bucket = group_slot * LOD_BUCKET_COUNT + lod;\n let group = group_infos[group_slot];\n if (group.chunk_count == 0u) {\n bucket_counts[bucket] = 0u;\n segment_offsets[bucket] = 0u;\n return;\n }\n\n let segment_start = group.chunk_entry_start + lod * group.chunk_count;\n let segment_last = segment_start + group.chunk_count - 1u;\n let base = prefix_before(segment_start);\n bucket_counts[bucket] = chunk_counts[segment_last] - base;\n segment_offsets[bucket] = base;\n}\n"},{"label":"shaders/static_lod_global_prefix_scatter_packed.wgsl","code":"// Packed scatter: write the SOURCE INDEX of every visible static-LOD instance into one\n// compact_indices buffer using the globally-scanned chunk_counts. dst is the true packed offset\n// (prefix_before(count_index) + local_rank), so each (group, lod) segment is contiguous and\n// capacity is the visible instance count. Draws fetch the 112B InstanceData through the index\n// (and apply lod_correction from group_infos there), so this pass writes 4B per survivor instead\n// of a 112B corrected copy. 5 storage buffers \u2014 well under the S22\n// max_storage_buffers_per_shader_stage = 8.\n\nstruct PrefixParams {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n};\n\nstruct GroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32, // per-group render cutoff, 0 = unlimited (checked at classify)\n corr_identity_bits: u32, // bit lod set = lod_correction[lod] is identity (draw fast path)\n _pad0b: u32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\nstruct ChunkMeta {\n group_slot: u32,\n local_chunk: u32,\n _pad0: vec2<u32>,\n};\n\nstruct Selection {\n lod: u32,\n local_rank: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: PrefixParams;\n@group(0) @binding(2) var<storage, read> group_infos: array<GroupInfo>;\n@group(0) @binding(3) var<storage, read> chunk_meta: array<ChunkMeta>;\n@group(0) @binding(4) var<storage, read> selections: array<Selection>;\n@group(0) @binding(5) var<storage, read> chunk_counts: array<u32>;\n@group(0) @binding(6) var<storage, read_write> compact_indices: array<u32>;\n\nconst INVALID_LOD: u32 = 0xFFFFFFFFu;\nconst WG_SIZE: u32 = 64u;\n\nfn prefix_before(index: u32) -> u32 {\n if (index == 0u) { return 0u; }\n return chunk_counts[index - 1u];\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wid: vec3<u32>) {\n let chunk_id = wid.x;\n if (chunk_id >= params.chunk_count) { return; }\n\n // Read scalar fields directly from the storage binding. Do NOT copy the whole GroupInfo (it embeds\n // array<mat4x4<f32>,4> = 256B) into a local: that value copy is heavy register/private pressure and\n // is the prime suspect for Dawn's Adreno backend failing to realize this pipeline (GPUInternalError).\n let group_slot = chunk_meta[chunk_id].group_slot;\n let local_chunk = chunk_meta[chunk_id].local_chunk;\n\n let local_i = local_chunk * WG_SIZE + lid.x;\n if (local_i >= group_infos[group_slot].source_count) { return; }\n\n let global_idx = group_infos[group_slot].source_start + local_i;\n if (global_idx >= params.instance_count) { return; }\n\n let selection = selections[global_idx];\n let lod = selection.lod;\n if (lod == INVALID_LOD || lod >= 4u) { return; }\n\n let segment_start = group_infos[group_slot].chunk_entry_start + lod * group_infos[group_slot].chunk_count;\n let count_index = segment_start + local_chunk;\n // chunk_counts is already globally scanned, so prefix_before(count_index) is the packed global\n // base for this chunk; adding the per-chunk rank gives the final packed slot.\n let dst = prefix_before(count_index) + selection.local_rank;\n\n compact_indices[dst] = global_idx;\n}\n"},{"label":"shaders/static_lod_write_indirect_counts.wgsl","code":"// Write per-LOD bucket counts into the one global (per-view) indirect draw-arg buffer.\n// C2b: every (group, lod) bucket owns a contiguous region of that buffer; params.lod_baseN is\n// the region base in u32 units (0xFFFFFFFF = bucket absent, nothing to write).\n\nstruct Params {\n group_slot: u32,\n submesh_count0: u32,\n submesh_count1: u32,\n submesh_count2: u32,\n submesh_count3: u32,\n lod_base0: u32,\n lod_base1: u32,\n lod_base2: u32,\n lod_base3: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n@group(0) @binding(0) var<storage, read> bucket_counts: array<u32>;\n@group(0) @binding(1) var<uniform> params: Params;\n@group(0) @binding(2) var<storage, read_write> indirect_global: array<u32>;\n\nconst LOD_BUCKET_COUNT: u32 = 4u;\nconst STRIDE_U32: u32 = 5u;\nconst ABSENT_BASE: u32 = 0xFFFFFFFFu;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let sid = gid.x;\n let base_count = params.group_slot * LOD_BUCKET_COUNT;\n\n if (sid < params.submesh_count0 && params.lod_base0 != ABSENT_BASE) {\n indirect_global[params.lod_base0 + sid * STRIDE_U32 + 1u] = bucket_counts[base_count + 0u];\n }\n if (sid < params.submesh_count1 && params.lod_base1 != ABSENT_BASE) {\n indirect_global[params.lod_base1 + sid * STRIDE_U32 + 1u] = bucket_counts[base_count + 1u];\n }\n if (sid < params.submesh_count2 && params.lod_base2 != ABSENT_BASE) {\n indirect_global[params.lod_base2 + sid * STRIDE_U32 + 1u] = bucket_counts[base_count + 2u];\n }\n if (sid < params.submesh_count3 && params.lod_base3 != ABSENT_BASE) {\n indirect_global[params.lod_base3 + sid * STRIDE_U32 + 1u] = bucket_counts[base_count + 3u];\n }\n}\n"},{"label":"shaders/static_lod_shadow_classify.wgsl","code":"struct Params {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n cascade_count: u32,\n draw_count: u32,\n lod1_texel_diameter: f32,\n lod2_texel_diameter: f32,\n lod3_texel_diameter: f32,\n shadow_map_size: f32,\n cascade2_shadow_only_skip_texels: f32,\n cascade3_shadow_only_skip_texels: f32,\n lod0_exception_texels: f32,\n};\n\nstruct CascadeData {\n light_view_proj: mat4x4<f32>,\n split_depth: f32,\n _pad: array<f32, 7>,\n};\n\nstruct CascadeCullData {\n light_view: mat4x4<f32>,\n min: vec4<f32>,\n max: vec4<f32>,\n};\n\nstruct BoundsMeta {\n min: vec3<f32>,\n sphere_radius: f32,\n max: vec3<f32>,\n group_slot: u32,\n sphere_center: vec3<f32>,\n _pad1: u32,\n};\n\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\n\nstruct GroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n _pad0: array<u32, 3>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\nstruct ChunkMeta {\n cascade_index: u32,\n group_slot: u32,\n local_chunk: u32,\n _pad: u32,\n};\n\nstruct Selection {\n lod: u32,\n local_rank: u32,\n};\n\n// Must match MAX_SHADOW_CASCADE_COUNT (shadow_directional.rs). cascade_culls is a uniform (not a\n// storage buffer) so this stage stays at 8 storage buffers and fits the S22 limit of 8.\nconst MAX_CASCADE_COUNT: u32 = 4u;\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> cascades: array<CascadeData>;\n@group(0) @binding(2) var<storage, read> group_infos: array<GroupInfo>;\n@group(0) @binding(3) var<storage, read> chunk_meta: array<ChunkMeta>;\n@group(0) @binding(4) var<storage, read> bounds_data: array<BoundsMeta>;\n@group(0) @binding(5) var<storage, read> source_instances: array<InstanceData>;\n@group(0) @binding(6) var<storage, read_write> selections: array<Selection>;\n// Cleared to zero by the CPU before this pass; each shadow-visible lane atomicAdd's its\n// (chunk, lod) entry (count + within-chunk rank in one op, see the camera classify).\n@group(0) @binding(7) var<storage, read_write> chunk_counts: array<atomic<u32>>;\n@group(0) @binding(8) var<uniform> cascade_culls: array<CascadeCullData, MAX_CASCADE_COUNT>;\n@group(0) @binding(9) var<storage, read> camera_selections: array<Selection>;\n\nconst INVALID_LOD: u32 = 0xFFFFFFFFu;\nconst LOD_BUCKET_COUNT: u32 = 4u;\nconst WG_SIZE: u32 = 64u;\n\nfn light_aabb_overlap(cull: CascadeCullData, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let corners = array<vec3<f32>, 8>(\n vec3<f32>(bmin.x, bmin.y, bmin.z), vec3<f32>(bmax.x, bmin.y, bmin.z),\n vec3<f32>(bmin.x, bmax.y, bmin.z), vec3<f32>(bmax.x, bmax.y, bmin.z),\n vec3<f32>(bmin.x, bmin.y, bmax.z), vec3<f32>(bmax.x, bmin.y, bmax.z),\n vec3<f32>(bmin.x, bmax.y, bmax.z), vec3<f32>(bmax.x, bmax.y, bmax.z)\n );\n var ls_min = vec3<f32>(1e20);\n var ls_max = vec3<f32>(-1e20);\n for (var i = 0u; i < 8u; i = i + 1u) {\n let p = (cull.light_view * vec4<f32>(corners[i], 1.0)).xyz;\n ls_min = min(ls_min, p);\n ls_max = max(ls_max, p);\n }\n return ls_max.x >= cull.min.x && ls_min.x <= cull.max.x &&\n ls_max.y >= cull.min.y && ls_min.y <= cull.max.y &&\n ls_max.z >= cull.min.z && ls_min.z <= cull.max.z;\n}\n\nfn shadow_texel_diameter(m: mat4x4<f32>, center: vec3<f32>, radius: f32) -> f32 {\n let c = m * vec4<f32>(center, 1.0);\n let x = m * vec4<f32>(center + vec3<f32>(radius, 0.0, 0.0), 1.0);\n let y = m * vec4<f32>(center + vec3<f32>(0.0, radius, 0.0), 1.0);\n if (abs(c.w) <= 1e-6 || abs(x.w) <= 1e-6 || abs(y.w) <= 1e-6) { return 0.0; }\n let cn = c.xy / c.w;\n return max(length((x.xy / x.w) - cn), length((y.xy / y.w) - cn)) * params.shadow_map_size;\n}\n\nfn selected_lod(texel_diameter: f32) -> u32 {\n var lod = 0u;\n if (texel_diameter <= params.lod3_texel_diameter) {\n lod = 3u;\n } else if (texel_diameter <= params.lod2_texel_diameter) {\n lod = 2u;\n } else if (texel_diameter <= params.lod1_texel_diameter) {\n lod = 1u;\n }\n return lod;\n}\n\nfn min_shadow_lod_for_cascade(cascade_index: u32) -> u32 {\n if (cascade_index <= 1u) {\n return 1u;\n }\n return 2u;\n}\n\nfn should_skip_far_shadow(cascade_index: u32, camera_visible: bool, texel_diameter: f32) -> bool {\n if (camera_visible) {\n return false;\n }\n return false;\n // TODO: right now seems bugged\n // if (cascade_index == 2u) {\n // return texel_diameter < params.cascade2_shadow_only_skip_texels;\n // }\n // if (cascade_index >= 3u) {\n // return texel_diameter < params.cascade3_shadow_only_skip_texels;\n // }\n // return false;\n}\n\nfn resolve_shadow_lod(lod: u32, mask: u32, min_lod: u32, allow_lod0: bool) -> u32 {\n var start = max(lod, min_lod);\n if (allow_lod0 && lod == 0u && (mask & 1u) != 0u) {\n return 0u;\n }\n for (var up = start; up < 4u; up = up + 1u) {\n if ((mask & (1u << up)) != 0u) { return up; }\n }\n for (var down = start; down > 0u; down = down - 1u) {\n let candidate = down - 1u;\n if ((mask & (1u << candidate)) != 0u) { return candidate; }\n }\n return INVALID_LOD;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wid: vec3<u32>) {\n let chunk_id = wid.x;\n let lane = lid.x;\n var lod = INVALID_LOD;\n var global_idx = 0u;\n var group = GroupInfo(0u, 0u, 0u, 0u, 0u, array<u32, 3>(0u, 0u, 0u), array<mat4x4<f32>, 4>());\n var cm = ChunkMeta(0u, 0u, 0u, 0u);\n var in_group = false;\n if (chunk_id < params.chunk_count) {\n cm = chunk_meta[chunk_id];\n group = group_infos[cm.group_slot];\n let local_i = cm.local_chunk * WG_SIZE + lane;\n in_group = local_i < group.source_count;\n global_idx = group.source_start + local_i;\n if (in_group && global_idx < params.instance_count && cm.cascade_index < params.cascade_count) {\n let bb = bounds_data[global_idx];\n let inst = source_instances[global_idx];\n let m = cascades[cm.cascade_index].light_view_proj;\n if ((inst.flags & 1u) == 0u &&\n light_aabb_overlap(cascade_culls[cm.cascade_index], bb.min, bb.max)) {\n let texel_diameter = shadow_texel_diameter(m, bb.sphere_center, bb.sphere_radius);\n let camera_lod = camera_selections[global_idx].lod;\n let camera_visible = camera_lod != INVALID_LOD;\n if (!should_skip_far_shadow(cm.cascade_index, camera_visible, texel_diameter)) {\n var requested_lod = selected_lod(texel_diameter);\n var min_lod = min_shadow_lod_for_cascade(cm.cascade_index);\n if (!camera_visible) {\n min_lod = max(min_lod, 1u);\n } else {\n requested_lod = max(requested_lod, camera_lod);\n }\n let allow_lod0 = cm.cascade_index == 0u &&\n camera_visible &&\n camera_lod == 0u &&\n texel_diameter > params.lod0_exception_texels;\n lod = resolve_shadow_lod(requested_lod, group.lod_mask, min_lod, allow_lod0);\n }\n }\n }\n }\n // Count + rank via one atomicAdd per shadow-visible lane (see the camera classify for why the\n // former wg_lods/barrier/lane-0 counting formulation was replaced: it miscompiled on native\n // Vulkan and produced phantom counts). chunk_counts is CPU-cleared before this pass.\n if (in_group && global_idx < params.instance_count) {\n var rank = 0u;\n if (lod != INVALID_LOD) {\n let entry = group.chunk_entry_start + cm.local_chunk + lod * group.chunk_count;\n rank = atomicAdd(&chunk_counts[entry], 1u);\n }\n selections[cm.cascade_index * params.instance_count + global_idx] = Selection(lod, rank);\n }\n}\n"},{"label":"shaders/static_lod_shadow_scatter.wgsl","code":"// static_lod_shadow_scatter.wgsl\n// Writes u32 SOURCE INDICES (not InstanceData copies): shadow draws fetch the payload through\n// the index and apply lod_correction there. No source_instances binding needed \u2014 the shadow\n// CLASSIFY already excludes no-shadow (flags&1) instances, so they arrive here as INVALID_LOD.\n// 7 storage buffers (was 8 = the S22 max_storage_buffers_per_shader_stage limit).\nstruct Params {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n cascade_count: u32,\n draw_count: u32,\n _pad: vec2<u32>,\n};\n\nstruct GroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n _pad0: array<u32, 3>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\nstruct ChunkMeta {\n cascade_index: u32,\n group_slot: u32,\n local_chunk: u32,\n _pad: u32,\n};\n\nstruct Selection {\n lod: u32,\n local_rank: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(2) var<storage, read> group_infos: array<GroupInfo>;\n@group(0) @binding(3) var<storage, read> chunk_meta: array<ChunkMeta>;\n@group(0) @binding(4) var<storage, read> selections: array<Selection>;\n@group(0) @binding(5) var<storage, read> chunk_counts: array<u32>;\n@group(0) @binding(6) var<storage, read_write> compact_indices: array<u32>;\n@group(0) @binding(7) var<storage, read_write> segment_offsets: array<u32>;\n@group(0) @binding(8) var<storage, read_write> segment_counts: array<u32>;\n\nconst INVALID_LOD: u32 = 0xFFFFFFFFu;\nconst LOD_BUCKET_COUNT: u32 = 4u;\nconst WG_SIZE: u32 = 64u;\n\nfn prefix_before(index: u32) -> u32 {\n if (index == 0u) { return 0u; }\n return chunk_counts[index - 1u];\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(local_invocation_id) lid: vec3<u32>, @builtin(workgroup_id) wid: vec3<u32>) {\n let chunk_id = wid.x;\n if (chunk_id >= params.chunk_count) { return; }\n // Read scalar fields directly from the storage binding; do NOT copy the whole GroupInfo (it embeds\n // array<mat4x4<f32>,4> = 256B) into a local \u2014 that value copy is the prime suspect for Dawn's Adreno\n // backend failing to realize this pipeline (GPUInternalError).\n let cascade_index = chunk_meta[chunk_id].cascade_index;\n let group_slot = chunk_meta[chunk_id].group_slot;\n let local_chunk = chunk_meta[chunk_id].local_chunk;\n let chunk_entry_start = group_infos[group_slot].chunk_entry_start;\n let chunk_count = group_infos[group_slot].chunk_count;\n if (local_chunk == 0u && lid.x == 0u) {\n for (var l = 0u; l < LOD_BUCKET_COUNT; l = l + 1u) {\n let seg_start = chunk_entry_start + l * chunk_count;\n let seg_last = seg_start + chunk_count - 1u;\n let seg_base = prefix_before(seg_start);\n let segment_index = group_slot * LOD_BUCKET_COUNT + l;\n segment_offsets[segment_index] = seg_base;\n segment_counts[segment_index] = chunk_counts[seg_last] - seg_base;\n }\n }\n let local_i = local_chunk * WG_SIZE + lid.x;\n if (local_i >= group_infos[group_slot].source_count) { return; }\n let global_idx = group_infos[group_slot].source_start + local_i;\n if (global_idx >= params.instance_count || cascade_index >= params.cascade_count) { return; }\n let selection = selections[cascade_index * params.instance_count + global_idx];\n let lod = selection.lod;\n if (lod == INVALID_LOD || lod >= 4u) { return; }\n\n let segment_start = chunk_entry_start + lod * chunk_count;\n let count_index = segment_start + local_chunk;\n let segment_base = prefix_before(segment_start);\n let chunk_base = prefix_before(count_index) - segment_base;\n let dst = segment_base + chunk_base + selection.local_rank;\n\n compact_indices[dst] = global_idx;\n}\n"},{"label":"shaders/static_lod_shadow_write_indirect.wgsl","code":"struct Params {\n instance_count: u32,\n group_count: u32,\n chunk_count: u32,\n chunk_entry_count: u32,\n cascade_count: u32,\n draw_count: u32,\n _pad: vec2<u32>,\n};\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(2) var<storage, read_write> segment_counts: array<u32>;\n@group(0) @binding(4) var<storage, read_write> indirect_args: array<u32>;\n\nconst LOD_BUCKET_COUNT: u32 = 4u;\nconst STRIDE_U32: u32 = 5u;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let draw_id = gid.x;\n if (draw_id >= params.draw_count) { return; }\n let segment = indirect_args[draw_id * STRIDE_U32 + 4u];\n let count = segment_counts[segment];\n indirect_args[draw_id * STRIDE_U32 + 1u] = count;\n // first_instance carried the segment index as a CPU scratch channel; reset to 0 before the\n // draw (nonzero would make wgpu discard the draw without INDIRECT_FIRST_INSTANCE).\n indirect_args[draw_id * STRIDE_U32 + 4u] = 0u;\n}\n"},{"label":"shaders/static_lod_shadow.wgsl","code":"// Index fetch: the shadow compact buffer holds u32 SOURCE INDICES; lod_correction is applied\n// here (identity fast path via corr_identity_bits), mirroring gbuffer.wgsl vs_main_lod.\n// segment_index = shadow_group_slot*4 + lod; shadow group slots are per-(cascade,group).\nstruct LightCamera {\n view_proj: mat4x4<f32>,\n};\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\nstruct DrawId {\n draw_index: u32,\n segment_index: u32,\n _pad1: u32,\n _pad2: u32,\n};\nstruct ShadowGroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n corr_identity_bits: u32,\n _pad0: array<u32, 2>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\n@group(0) @binding(0) var<uniform> light_camera: LightCamera;\n@group(1) @binding(0) var<storage, read> instances: array<InstanceData>;\n@group(1) @binding(1) var<storage, read> segment_offsets: array<u32>;\n@group(1) @binding(2) var<storage, read> compact_indices: array<u32>;\n@group(1) @binding(3) var<uniform> draw_id: DrawId;\n@group(1) @binding(4) var<storage, read> group_infos: array<ShadowGroupInfo>;\n\n// Switch-select (not dynamic array<mat4x4> indexing): the Adreno-safe pattern from the scatters.\nfn lod_correction_for(slot: u32, lod: u32) -> mat4x4<f32> {\n switch lod {\n case 0u: { return group_infos[slot].lod_correction[0]; }\n case 1u: { return group_infos[slot].lod_correction[1]; }\n case 2u: { return group_infos[slot].lod_correction[2]; }\n default: { return group_infos[slot].lod_correction[3]; }\n }\n}\n\nfn lod_fetch_model_matrix_seg(inst_model: mat4x4<f32>, segment_index: u32) -> mat4x4<f32> {\n let slot = segment_index / 4u;\n let lod = segment_index % 4u;\n if (((group_infos[slot].corr_identity_bits >> lod) & 1u) == 0u) {\n return inst_model * lod_correction_for(slot, lod);\n }\n return inst_model;\n}\nfn lod_fetch_model_matrix(inst_model: mat4x4<f32>) -> mat4x4<f32> {\n return lod_fetch_model_matrix_seg(inst_model, draw_id.segment_index);\n}\n\n@vertex\nfn vs_main(\n @builtin(instance_index) instance_index: u32,\n @location(0) position: vec3<f32>,\n) -> @builtin(position) vec4<f32> {\n let inst = instances[compact_indices[segment_offsets[draw_id.segment_index] + instance_index]];\n let model_matrix = lod_fetch_model_matrix(inst.model_matrix);\n var pos = light_camera.view_proj * (model_matrix * vec4<f32>(position, 1.0));\n pos.z = max(pos.z, 0.0); // shadow pancaking\n return pos;\n}\n"},{"label":"shaders/static_lod_shadow_alpha.wgsl","code":"struct LightCamera {\n view_proj: mat4x4<f32>,\n};\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\nstruct DrawId {\n draw_index: u32,\n segment_index: u32,\n _pad1: u32,\n _pad2: u32,\n};\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n};\n\nstruct ShadowGroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n corr_identity_bits: u32,\n _pad0: array<u32, 2>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n\n@group(0) @binding(0) var<uniform> light_camera: LightCamera;\n@group(1) @binding(0) var<storage, read> instances: array<InstanceData>;\n@group(1) @binding(1) var<storage, read> segment_offsets: array<u32>;\n@group(1) @binding(2) var<storage, read> compact_indices: array<u32>;\n@group(1) @binding(3) var<uniform> draw_id: DrawId;\n@group(1) @binding(4) var<storage, read> group_infos: array<ShadowGroupInfo>;\n@group(2) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(2) @binding(1) var normal_texture: texture_2d<f32>;\n@group(2) @binding(2) var orm_texture: texture_2d<f32>;\n@group(2) @binding(3) var material_sampler: sampler;\n\n// Switch-select (not dynamic array<mat4x4> indexing): the Adreno-safe pattern from the scatters.\nfn lod_correction_for(slot: u32, lod: u32) -> mat4x4<f32> {\n switch lod {\n case 0u: { return group_infos[slot].lod_correction[0]; }\n case 1u: { return group_infos[slot].lod_correction[1]; }\n case 2u: { return group_infos[slot].lod_correction[2]; }\n default: { return group_infos[slot].lod_correction[3]; }\n }\n}\n\nfn lod_fetch_model_matrix_seg(inst_model: mat4x4<f32>, segment_index: u32) -> mat4x4<f32> {\n let slot = segment_index / 4u;\n let lod = segment_index % 4u;\n if (((group_infos[slot].corr_identity_bits >> lod) & 1u) == 0u) {\n return inst_model * lod_correction_for(slot, lod);\n }\n return inst_model;\n}\nfn lod_fetch_model_matrix(inst_model: mat4x4<f32>) -> mat4x4<f32> {\n return lod_fetch_model_matrix_seg(inst_model, draw_id.segment_index);\n}\n\n@vertex\nfn vs_main(\n @builtin(instance_index) instance_index: u32,\n @location(0) position: vec3<f32>,\n @location(2) uv: vec2<f32>,\n) -> VSOut {\n let inst = instances[compact_indices[segment_offsets[draw_id.segment_index] + instance_index]];\n let model_matrix = lod_fetch_model_matrix(inst.model_matrix);\n var o: VSOut;\n o.clip_position = light_camera.view_proj * (model_matrix * vec4<f32>(position, 1.0));\n o.clip_position.z = max(o.clip_position.z, 0.0); // shadow pancaking\n o.uv = uv;\n o.mesh_color = inst.mesh_color;\n return o;\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2<f32>, @location(1) mesh_color: vec4<f32>) {\n let base_color = textureSample(base_color_texture, material_sampler, uv);\n if ((base_color * mesh_color).a < 0.5) { discard; }\n}\n"},{"label":"shaders/depth_prepass_lod.wgsl","code":"// Depth-only prepass over the static-LOD packed bucket draws (PBR opaque only).\n// The gbuffer LOD opaque pipeline then runs depth Equal with writes off, shading each\n// pixel exactly once. Position math MUST stay bit-identical to gbuffer.wgsl vs_main_lod:\n// both entry points mark @builtin(position) @invariant and share the same expressions,\n// camera uniform, and instance fetch \u2014 including the tree-wind displacement (a mismatch\n// there makes swaying trees fail the Equal test and vanish).\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n // [render dir x, render dir z, strength 0..1, gustiness 0..1]\n wind: vec4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst INSTANCE_FLAG_TREE_WIND: u32 = 1u << 2u;\n\n// Verbatim copy of gbuffer.wgsl tree_wind_offset (see the invariance note above).\nfn tree_wind_offset(world_pos: vec3<f32>, tree_root: vec3<f32>, wind_data: vec4<f32>) -> vec3<f32> {\n let strength = u_camera.wind.z;\n let wdir = vec3<f32>(u_camera.wind.x, 0.0, u_camera.wind.y);\n let t = u_camera.time_seconds;\n let tree_phase = dot(tree_root, wdir) * 0.15;\n let gust = 0.5 + 0.5 * sin(t * 0.9 - tree_phase) * u_camera.wind.w;\n let trunk = wind_data.z * strength * (0.10 + 0.22 * gust + 0.05 * sin(t * 1.3 - tree_phase));\n let bs = sin(t * (1.8 + 1.2 * strength) + wind_data.x * 6.2831853 - tree_phase);\n let branch = wind_data.y * strength * (0.35 + 0.65 * gust) * 0.18;\n return wdir * (trunk + bs * branch) + vec3<f32>(0.0, 1.0, 0.0) * (bs * branch * -0.3);\n}\n\n// Packed index fetch, mirroring gbuffer.wgsl vs_main_lod's group 2 (bound at group 1 here):\n// compact buffer holds u32 source indices; lod_correction applied here (identity fast path),\n// with the SAME expressions as gbuffer.wgsl so @invariant positions stay bit-identical.\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\nstruct DrawId {\n draw_index: u32,\n segment_index: u32,\n _pad1: u32,\n _pad2: u32,\n};\nstruct LodGroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32,\n corr_identity_bits: u32,\n _pad0b: u32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n@group(1) @binding(0) var<storage, read> lod_instances: array<InstanceData>;\n@group(1) @binding(1) var<storage, read> lod_segment_offsets: array<u32>;\n@group(1) @binding(2) var<storage, read> lod_compact_indices: array<u32>;\n@group(1) @binding(3) var<uniform> lod_draw_id: DrawId;\n@group(1) @binding(4) var<storage, read> lod_group_infos: array<LodGroupInfo>;\n\n// Switch-select (not dynamic array<mat4x4> indexing): the Adreno-safe pattern from the scatters.\nfn lod_correction_for(slot: u32, lod: u32) -> mat4x4<f32> {\n switch lod {\n case 0u: { return lod_group_infos[slot].lod_correction[0]; }\n case 1u: { return lod_group_infos[slot].lod_correction[1]; }\n case 2u: { return lod_group_infos[slot].lod_correction[2]; }\n default: { return lod_group_infos[slot].lod_correction[3]; }\n }\n}\n\n// Verbatim copy of gbuffer.wgsl lod_fetch_model_matrix_seg (see the invariance note above).\nfn lod_fetch_model_matrix_seg(inst_model: mat4x4<f32>, segment_index: u32) -> mat4x4<f32> {\n let slot = segment_index / 4u;\n let lod = segment_index % 4u;\n if (((lod_group_infos[slot].corr_identity_bits >> lod) & 1u) == 0u) {\n return inst_model * lod_correction_for(slot, lod);\n }\n return inst_model;\n}\nfn lod_fetch_model_matrix(inst_model: mat4x4<f32>) -> mat4x4<f32> {\n return lod_fetch_model_matrix_seg(inst_model, lod_draw_id.segment_index);\n}\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n // TANGENT carries wind data for INSTANCE_FLAG_TREE_WIND instances (see gbuffer.wgsl).\n @location(3) tangent: vec4<f32>,\n};\n\n@vertex\nfn vs_main(\n @builtin(instance_index) instance_index: u32,\n input: VertexInput,\n) -> @builtin(position) @invariant vec4<f32> {\n let inst = lod_instances[lod_compact_indices[lod_segment_offsets[lod_draw_id.segment_index] + instance_index]];\n let model_matrix = lod_fetch_model_matrix(inst.model_matrix);\n var world_position = model_matrix * vec4<f32>(input.position, 1.0);\n if ((inst.flags & INSTANCE_FLAG_TREE_WIND) != 0u) {\n world_position = vec4<f32>(\n world_position.xyz + tree_wind_offset(world_position.xyz, model_matrix[3].xyz, input.tangent),\n 1.0,\n );\n }\n return u_camera.view_proj * world_position;\n}\n"},{"label":"shaders/prefix_sum.wgsl","code":"// prefix_sum.wgsl\nstruct PrefixSumUniform {\n instance_count: u32,\n submesh_id: u32,\n chunk_count_0: u32,\n chunk_count_1: u32,\n chunk_count_2: u32,\n chunk_count_3: u32,\n};\n\n@group(0) @binding(0) var<storage, read_write> visible_flags: array<u32>;\n@group(0) @binding(1) var<storage, read_write> partial_sums_1: array<u32>;\n@group(0) @binding(2) var<storage, read_write> partial_sums_2: array<u32>;\n@group(0) @binding(3) var<storage, read_write> partial_sums_3: array<u32>;\n@group(0) @binding(4) var<storage, read_write> partial_sums_4: array<u32>;\n\n@group(0) @binding(5) var<uniform> params: PrefixSumUniform;\n@group(0) @binding(6) var<storage, read_write> debug_buffer: array<u32>;\n\nconst GROUP_SIZE: u32 = 32u;\nvar<workgroup> temp: array<u32, GROUP_SIZE>; // Note: beyond 32, some hardware/driver bugs is causing wrong calculations?\n\nfn assert(clause: bool, code: u32, idx: u32, value: u32) {\n if !clause {\n debug_buffer[0] = code;\n debug_buffer[1] = idx;\n debug_buffer[2] = value;\n debug_buffer[3] = params.submesh_id;\n }\n}\n\n// convert temp into a prefix sums array\nfn blelloch_scan(local_idx: u32) -> u32 {\n // Up-sweep\n var offset = 1u;\n while (offset < GROUP_SIZE) {\n let i = ((local_idx + 1u) * offset * 2u) - 1u;\n\n if (i < GROUP_SIZE) {\n temp[i] = temp[i] + temp[i - offset];\n }\n workgroupBarrier();\n offset *= 2u;\n }\n\n // Last element contains the total sum of this chunk\n var chunk_sum = 0u;\n if (local_idx == 0u) {\n chunk_sum = temp[GROUP_SIZE - 1u];\n temp[GROUP_SIZE - 1u] = 0u;\n }\n workgroupBarrier();\n\n // Down-sweep\n var offset2 = GROUP_SIZE / 2u;\n while (offset2 > 0u) {\n let i = ((local_idx + 1u) * offset2 * 2u) - 1u;\n if (i < GROUP_SIZE) {\n var t = temp[i];\n temp[i] += temp[i - offset2];\n temp[i - offset2] = t;\n }\n workgroupBarrier();\n offset2 /= 2u;\n }\n\n return chunk_sum;\n}\n\n@compute @workgroup_size(32)\nfn local_prefix_sum(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let group_idx = wid.x;\n let local_idx = lid.x;\n let idx = gid.x;\n\n if (idx < params.instance_count) {\n // assert(visible_flags[idx] == 1u, 2005u, idx, visible_flags[idx]);\n assert(idx < arrayLength(&visible_flags), 2006u, idx, arrayLength(&visible_flags));\n }\n // assert(params.submesh_id != 0, 3005u, idx, params.instance_count);\n // assert(false, 3005u, idx, visible_flags[idx]);\n\n temp[local_idx] = select(0u, visible_flags[idx], idx < params.instance_count);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(local_idx);\n\n // Write back the prefix sums for this chunk\n if (idx < params.instance_count) {\n // for testing mode, where all flags are 1\n // assert(visible_flags[idx] == 1u, 2008u, idx, visible_flags[idx]);\n // assert(temp[local_idx] == local_idx, 2007u, idx, temp[local_idx]);\n // assert(idx != 2u, 3005u, idx, visible_flags[idx]);\n // assert(params.submesh_id != 0, 3005u, idx, visible_flags[idx]);\n\n visible_flags[idx] += temp[local_idx]; // += makes it inclusive scan\n\n // Write partial sum (the total sum of this chunk)\n if (local_idx == 0u) {\n // if group_idx < params.chunk_count_0 - 1u {\n // assert(chunk_sum == 32u, 2010u, group_idx, chunk_sum);\n // }\n partial_sums_1[group_idx] = chunk_sum;\n }\n }\n}\n\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_1(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_0;\n temp[lid.x] = select(0u, partial_sums_1[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_1[idx] = temp[lid.x]; // partial_sums[i] will now hold the exclusive prefix sum\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_1 - 1u {\n // assert(chunk_sum == 32u * 32u, 2012u, group_idx, chunk_sum);\n // }\n partial_sums_2[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_2(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_1;\n temp[lid.x] = select(0u, partial_sums_2[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_2[idx] = temp[lid.x];\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_2 - 1u {\n // assert(chunk_sum == 32u * 32u * 32u, 2013u, group_idx, chunk_sum);\n // }\n partial_sums_3[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_3(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_2;\n temp[lid.x] = select(0u, partial_sums_3[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_3[idx] = temp[lid.x];\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_3 - 1u {\n // assert(chunk_sum == 32u * 32u * 32u * 32u, 2014u, group_idx, chunk_sum);\n // }\n partial_sums_4[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_4(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_3;\n temp[lid.x] = select(0u, partial_sums_4[idx], in_bounds);\n workgroupBarrier();\n\n blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_4[idx] = temp[lid.x];\n }\n}\n\n// Use prefix sums to add an offset to each chunk of visible_flags.\n// start with visible_flags that are local_index in workgroup, end with global_index.\n@compute @workgroup_size(32)\nfn add_partial_sums(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let chunk_id_1 = wid.x;\n if (idx >= params.instance_count) {\n return;\n }\n var offset = 0u;\n if (chunk_id_1 > 0u) {\n offset += partial_sums_1[chunk_id_1]; \n }\n let chunk_id_2 = chunk_id_1 >> 5u; // div 32\n if (chunk_id_2 > 0u) {\n offset += partial_sums_2[chunk_id_2]; \n }\n let chunk_id_3 = chunk_id_2 >> 5u;\n if (chunk_id_3 > 0u) {\n offset += partial_sums_3[chunk_id_3]; \n }\n let chunk_id_4 = chunk_id_3 >> 5u;\n if (chunk_id_4 > 0u) {\n offset += partial_sums_4[chunk_id_4]; \n }\n visible_flags[idx] += offset;\n\n // assert(visible_flags[idx] == (idx % 32u) + 1u, 2011u, idx, visible_flags[idx]);\n}\n\n"},{"label":"shaders/prefix_sum.wgsl","code":"// prefix_sum.wgsl\nstruct PrefixSumUniform {\n instance_count: u32,\n submesh_id: u32,\n chunk_count_0: u32,\n chunk_count_1: u32,\n chunk_count_2: u32,\n chunk_count_3: u32,\n};\n\n@group(0) @binding(0) var<storage, read_write> visible_flags: array<u32>;\n@group(0) @binding(1) var<storage, read_write> partial_sums_1: array<u32>;\n@group(0) @binding(2) var<storage, read_write> partial_sums_2: array<u32>;\n@group(0) @binding(3) var<storage, read_write> partial_sums_3: array<u32>;\n@group(0) @binding(4) var<storage, read_write> partial_sums_4: array<u32>;\n\n@group(0) @binding(5) var<uniform> params: PrefixSumUniform;\n@group(0) @binding(6) var<storage, read_write> debug_buffer: array<u32>;\n\nconst GROUP_SIZE: u32 = 32u;\nvar<workgroup> temp: array<u32, GROUP_SIZE>; // Note: beyond 32, some hardware/driver bugs is causing wrong calculations?\n\nfn assert(clause: bool, code: u32, idx: u32, value: u32) {\n if !clause {\n debug_buffer[0] = code;\n debug_buffer[1] = idx;\n debug_buffer[2] = value;\n debug_buffer[3] = params.submesh_id;\n }\n}\n\n// convert temp into a prefix sums array\nfn blelloch_scan(local_idx: u32) -> u32 {\n // Up-sweep\n var offset = 1u;\n while (offset < GROUP_SIZE) {\n let i = ((local_idx + 1u) * offset * 2u) - 1u;\n\n if (i < GROUP_SIZE) {\n temp[i] = temp[i] + temp[i - offset];\n }\n workgroupBarrier();\n offset *= 2u;\n }\n\n // Last element contains the total sum of this chunk\n var chunk_sum = 0u;\n if (local_idx == 0u) {\n chunk_sum = temp[GROUP_SIZE - 1u];\n temp[GROUP_SIZE - 1u] = 0u;\n }\n workgroupBarrier();\n\n // Down-sweep\n var offset2 = GROUP_SIZE / 2u;\n while (offset2 > 0u) {\n let i = ((local_idx + 1u) * offset2 * 2u) - 1u;\n if (i < GROUP_SIZE) {\n var t = temp[i];\n temp[i] += temp[i - offset2];\n temp[i - offset2] = t;\n }\n workgroupBarrier();\n offset2 /= 2u;\n }\n\n return chunk_sum;\n}\n\n@compute @workgroup_size(32)\nfn local_prefix_sum(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let group_idx = wid.x;\n let local_idx = lid.x;\n let idx = gid.x;\n\n if (idx < params.instance_count) {\n // assert(visible_flags[idx] == 1u, 2005u, idx, visible_flags[idx]);\n assert(idx < arrayLength(&visible_flags), 2006u, idx, arrayLength(&visible_flags));\n }\n // assert(params.submesh_id != 0, 3005u, idx, params.instance_count);\n // assert(false, 3005u, idx, visible_flags[idx]);\n\n temp[local_idx] = select(0u, visible_flags[idx], idx < params.instance_count);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(local_idx);\n\n // Write back the prefix sums for this chunk\n if (idx < params.instance_count) {\n // for testing mode, where all flags are 1\n // assert(visible_flags[idx] == 1u, 2008u, idx, visible_flags[idx]);\n // assert(temp[local_idx] == local_idx, 2007u, idx, temp[local_idx]);\n // assert(idx != 2u, 3005u, idx, visible_flags[idx]);\n // assert(params.submesh_id != 0, 3005u, idx, visible_flags[idx]);\n\n visible_flags[idx] += temp[local_idx]; // += makes it inclusive scan\n\n // Write partial sum (the total sum of this chunk)\n if (local_idx == 0u) {\n // if group_idx < params.chunk_count_0 - 1u {\n // assert(chunk_sum == 32u, 2010u, group_idx, chunk_sum);\n // }\n partial_sums_1[group_idx] = chunk_sum;\n }\n }\n}\n\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_1(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_0;\n temp[lid.x] = select(0u, partial_sums_1[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_1[idx] = temp[lid.x]; // partial_sums[i] will now hold the exclusive prefix sum\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_1 - 1u {\n // assert(chunk_sum == 32u * 32u, 2012u, group_idx, chunk_sum);\n // }\n partial_sums_2[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_2(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_1;\n temp[lid.x] = select(0u, partial_sums_2[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_2[idx] = temp[lid.x];\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_2 - 1u {\n // assert(chunk_sum == 32u * 32u * 32u, 2013u, group_idx, chunk_sum);\n // }\n partial_sums_3[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_3(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_2;\n temp[lid.x] = select(0u, partial_sums_3[idx], in_bounds);\n workgroupBarrier();\n\n let chunk_sum = blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_3[idx] = temp[lid.x];\n\n if (lid.x == 0u) {\n // if group_idx < params.chunk_count_3 - 1u {\n // assert(chunk_sum == 32u * 32u * 32u * 32u, 2014u, group_idx, chunk_sum);\n // }\n partial_sums_4[group_idx] = chunk_sum;\n }\n }\n}\n@compute @workgroup_size(32)\nfn prefix_sum_partial_sums_4(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let group_idx = wid.x;\n let in_bounds = idx < params.chunk_count_3;\n temp[lid.x] = select(0u, partial_sums_4[idx], in_bounds);\n workgroupBarrier();\n\n blelloch_scan(lid.x);\n\n if (in_bounds) {\n partial_sums_4[idx] = temp[lid.x];\n }\n}\n\n// Use prefix sums to add an offset to each chunk of visible_flags.\n// start with visible_flags that are local_index in workgroup, end with global_index.\n@compute @workgroup_size(32)\nfn add_partial_sums(@builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_id) lid: vec3<u32>,\n @builtin(workgroup_id) wid: vec3<u32>) {\n let idx = gid.x;\n let chunk_id_1 = wid.x;\n if (idx >= params.instance_count) {\n return;\n }\n var offset = 0u;\n if (chunk_id_1 > 0u) {\n offset += partial_sums_1[chunk_id_1]; \n }\n let chunk_id_2 = chunk_id_1 >> 5u; // div 32\n if (chunk_id_2 > 0u) {\n offset += partial_sums_2[chunk_id_2]; \n }\n let chunk_id_3 = chunk_id_2 >> 5u;\n if (chunk_id_3 > 0u) {\n offset += partial_sums_3[chunk_id_3]; \n }\n let chunk_id_4 = chunk_id_3 >> 5u;\n if (chunk_id_4 > 0u) {\n offset += partial_sums_4[chunk_id_4]; \n }\n visible_flags[idx] += offset;\n\n // assert(visible_flags[idx] == (idx % 32u) + 1u, 2011u, idx, visible_flags[idx]);\n}\n\n"},{"label":"shaders/shadow_directional.wgsl","code":"// shadow_directional.wgsl\nstruct LightCamera {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> light_camera: LightCamera;\n\n@vertex\nfn vs_main(\n @location(0) position: vec3<f32>,\n @location(4) model_mat_0: vec4<f32>,\n @location(5) model_mat_1: vec4<f32>,\n @location(6) model_mat_2: vec4<f32>,\n @location(7) model_mat_3: vec4<f32>,\n @interpolate(flat) @location(10) inst_flags: u32,\n) -> @builtin(position) vec4<f32> {\n let model = mat4x4<f32>(\n model_mat_0, model_mat_1, model_mat_2, model_mat_3\n );\n // bit0: no_shadow; bit9: death-dissolve (fading mesh particles stop casting whole,\n // the same convention as the skinned shadow shaders).\n if ((inst_flags & 1u) == 1u || (inst_flags & (1u << 9u)) != 0u) {\n // Fully clip this instance by placing it outside clip-space\n // Ensures zero rasterization on all hardware/drivers\n return vec4<f32>(2.0, 2.0, 2.0, 1.0);\n }\n var pos = light_camera.view_proj * (model * vec4<f32>(position, 1.0));\n // Shadow pancaking: casters in front of the light near plane clamp onto it\n // instead of being clipped (tall casters must still shadow this cascade).\n pos.z = max(pos.z, 0.0);\n return pos;\n}"},{"label":"shaders/shadow_directional_alpha.wgsl","code":"// shadow_directional_alpha.wgsl\n// Depth-only shadow caster with alpha cutoff using base color texture\n\nstruct LightCamera {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> light_camera: LightCamera;\n\n// PBR (only base color and sampler are used)\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>; // unused\n@group(1) @binding(2) var orm_texture: texture_2d<f32>; // unused\n@group(1) @binding(3) var material_sampler: sampler;\n\nconst ALPHA_CUTOFF: f32 = 0.5;\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(\n @location(0) position: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(4) model_mat_0: vec4<f32>,\n @location(5) model_mat_1: vec4<f32>,\n @location(6) model_mat_2: vec4<f32>,\n @location(7) model_mat_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @interpolate(flat) @location(10) inst_flags: u32,\n) -> VSOut {\n var o: VSOut;\n // bit0: no_shadow; bit9: death-dissolve (fading mesh particles stop casting whole,\n // the same convention as the skinned shadow shaders).\n if ((inst_flags & 1u) == 1u || (inst_flags & (1u << 9u)) != 0u) {\n // Fully clip this instance by placing it outside clip-space\n o.clip_position = vec4<f32>(2.0, 2.0, 2.0, 1.0);\n o.uv = uv;\n o.mesh_color = mesh_color;\n return o;\n }\n let model = mat4x4<f32>(model_mat_0, model_mat_1, model_mat_2, model_mat_3);\n let world_pos = model * vec4<f32>(position, 1.0);\n o.clip_position = light_camera.view_proj * world_pos;\n o.clip_position.z = max(o.clip_position.z, 0.0); // shadow pancaking\n o.uv = uv;\n o.mesh_color = mesh_color;\n return o;\n}\n\n@fragment\nfn fs_main(\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n) {\n let base_color = textureSample(base_color_texture, material_sampler, uv);\n let shaded = base_color * mesh_color;\n if (shaded.a < ALPHA_CUTOFF) { discard; }\n}\n\n\n"},{"label":"shaders/shadow_directional_skinned.wgsl","code":"// shadow_directional_skinned.wgsl\nstruct CascadeData { \n lightViewProj : mat4x4<f32>, \n splitDepth : f32, \n _pad : vec3<f32>,\n};\n@group(0) @binding(0) var<uniform> uCascade: CascadeData;\n@group(1) @binding(0) var<storage, read> palette: array<mat4x4<f32>>;\n\nstruct VSOut { @builtin(position) pos: vec4<f32> };\n\n@vertex\nfn vs_main( // SkinnedVertex\n @location(0) pos: vec3<f32>,\n @location(4) joint_indices: vec4<u32>,\n @location(5) joint_weights: vec4<f32>,\n\n // per\u2011instance\n @location(6) m0: vec4<f32>,\n @location(7) m1: vec4<f32>,\n @location(8) m2: vec4<f32>,\n @location(9) m3: vec4<f32>,\n\n @location(12) anim_misc: vec4<u32>,\n) -> VSOut {\n const DEATH_DISSOLVE_BIT: u32 = 1u << 9u;\n // bit0: no_shadow\n if ((anim_misc.x & 1u) == 1u || (anim_misc.x & DEATH_DISSOLVE_BIT) != 0u) {\n var o : VSOut;\n // Fully clip this instance by placing it outside clip-space\n o.pos = vec4<f32>(2.0, 2.0, 2.0, 1.0);\n return o;\n }\n // fetch skin matrices\n let paletteOffset = anim_misc.w;\n let base = paletteOffset + joint_indices;\n\n // Normalise the weight vector\n let weight_sum = max(joint_weights.x + joint_weights.y + joint_weights.z + joint_weights.w, 1e-5);\n let weights = joint_weights / weight_sum;\n\n // linear\u2011blend\u2011skin\n var skinned = vec4<f32>(pos, 1.0);\n skinned = palette[base.x] * skinned * weights.x\n + palette[base.y] * skinned * weights.y\n + palette[base.z] * skinned * weights.z\n + palette[base.w] * skinned * weights.w;\n\n // model \u2192 world\n let model = mat4x4<f32>(m0, m1, m2, m3);\n let world = model * skinned;\n\n var o : VSOut;\n o.pos = uCascade.lightViewProj * world;\n o.pos.z = max(o.pos.z, 0.0); // shadow pancaking\n return o;\n}\n"},{"label":"shaders/shadow_directional_skinned_alpha.wgsl","code":"// shadow_directional_skinned_alpha.wgsl\n// Skinned depth-only shadow caster with alpha cutoff using base color texture\n\nstruct CascadeData { \n lightViewProj : mat4x4<f32>, \n splitDepth : f32, \n _pad : vec3<f32>,\n};\n@group(0) @binding(0) var<uniform> uCascade: CascadeData;\n@group(1) @binding(0) var<storage, read> palette: array<mat4x4<f32>>;\n\n// PBR (only base color used)\n@group(2) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(2) @binding(1) var normal_texture: texture_2d<f32>; // unused\n@group(2) @binding(2) var orm_texture: texture_2d<f32>; // unused\n@group(2) @binding(3) var material_sampler: sampler;\n\nconst ALPHA_CUTOFF: f32 = 0.5;\n\nfn hash12(p: vec2<f32>) -> f32 {\n return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);\n}\n\nstruct VSOut { \n @builtin(position) pos: vec4<f32>, \n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n @interpolate(flat) @location(2) flags: u32,\n};\n\n@vertex\nfn vs_main( // SkinnedVertex\n @location(0) in_pos: vec3<f32>,\n @location(2) in_uv: vec2<f32>,\n @location(4) joint_indices: vec4<u32>,\n @location(5) joint_weights: vec4<f32>,\n\n // per\u2011instance\n @location(6) m0: vec4<f32>,\n @location(7) m1: vec4<f32>,\n @location(8) m2: vec4<f32>,\n @location(9) m3: vec4<f32>,\n @location(10) mesh_color: vec4<f32>,\n @location(12) anim_misc: vec4<u32>,\n) -> VSOut {\n var o : VSOut;\n // bit0: no_shadow\n if ((anim_misc.x & 1u) == 1u) {\n o.pos = vec4<f32>(2.0, 2.0, 2.0, 1.0);\n o.uv = in_uv;\n o.mesh_color = mesh_color;\n o.flags = anim_misc.x;\n return o;\n }\n\n // fetch skin matrices\n let paletteOffset = anim_misc.w;\n let base = paletteOffset + joint_indices;\n\n // Normalise the weight vector\n let weight_sum = max(joint_weights.x + joint_weights.y + joint_weights.z + joint_weights.w, 1e-5);\n let weights = joint_weights / weight_sum;\n\n // linear\u2011blend\u2011skin\n var skinned = vec4<f32>(in_pos, 1.0);\n skinned = palette[base.x] * skinned * weights.x\n + palette[base.y] * skinned * weights.y\n + palette[base.z] * skinned * weights.z\n + palette[base.w] * skinned * weights.w;\n\n // model \u2192 world\n let model = mat4x4<f32>(m0, m1, m2, m3);\n let world = model * skinned;\n\n o.pos = uCascade.lightViewProj * world;\n o.pos.z = max(o.pos.z, 0.0); // shadow pancaking\n o.uv = in_uv;\n o.mesh_color = mesh_color;\n o.flags = anim_misc.x;\n return o;\n}\n\n@fragment\nfn fs_main(\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n @interpolate(flat) @location(2) flags: u32,\n) {\n const DEATH_DISSOLVE_BIT: u32 = 1u << 9u;\n let base_color = textureSample(base_color_texture, material_sampler, uv);\n let shaded = base_color * mesh_color;\n if ((flags & DEATH_DISSOLVE_BIT) != 0u) {\n let visibility = clamp(mesh_color.a, 0.0, 1.0);\n let dissolve_noise = hash12(uv * 17.0);\n if (dissolve_noise > visibility) { discard; }\n }\n if (shaded.a < ALPHA_CUTOFF) { discard; }\n}\n\n\n"},{"label":"GBuffer Shader","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nconst USE_ALPHA_CUTOFF: bool = false;\nconst ALPHA_CUTOFF: f32 = 0.5 ;\n// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// gbuffer.wgsl\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) emission_rgbi: vec4<f32>,\n @location(10) flags: u32,\n};\nstruct VertexOutput {\n // @invariant: depth_prepass_lod.wgsl re-runs the same position math depth-only and the\n // opaque LOD gbuffer pipeline compares Equal against it \u2014 positions must match bit-exact.\n @builtin(position) @invariant clip_position: vec4<f32>,\n @location(0) normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) tangent: vec3<f32>,\n @location(3) bitangent: vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos: vec3<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n // [render dir x, render dir z, strength 0..1, gustiness 0..1]\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst INSTANCE_FLAG_TREE_WIND: u32 = 1u << 2u;\nconst INSTANCE_FLAG_EDGE_BEVEL: u32 = 1u << 3u;\n\n// Hierarchical tree sway (tree meshes only, gated by INSTANCE_FLAG_TREE_WIND).\n// `wind_data` rides the TANGENT slot: x = per-branch phase 0..1, y = branch flex,\n// z = trunk bend weight (height^2). Returns a world-space offset in meters.\nfn tree_wind_offset(world_pos: vec3<f32>, tree_root: vec3<f32>, wind_data: vec4<f32>) -> vec3<f32> {\n let strength = u_camera.wind.z;\n let wdir = vec3<f32>(u_camera.wind.x, 0.0, u_camera.wind.y);\n let t = u_camera.time_seconds;\n // Gust wave travels along the wind direction; per-tree phase keys off root position so a\n // forest ripples instead of pumping in unison.\n let tree_phase = dot(tree_root, wdir) * 0.15;\n let gust = 0.5 + 0.5 * sin(t * 0.9 - tree_phase) * u_camera.wind.w;\n // Trunk: slow lean into the wind + gentle oscillation, growing with height^2.\n let trunk = wind_data.z * strength * (0.10 + 0.22 * gust + 0.05 * sin(t * 1.3 - tree_phase));\n // Branch: per-branch phase, faster oscillation, flex ramps toward branch tips.\n let bs = sin(t * (1.8 + 1.2 * strength) + wind_data.x * 6.2831853 - tree_phase);\n let branch = wind_data.y * strength * (0.35 + 0.65 * gust) * 0.18;\n return wdir * (trunk + bs * branch) + vec3<f32>(0.0, 1.0, 0.0) * (bs * branch * -0.3);\n}\n\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>;\n@group(1) @binding(2) var orm_texture: texture_2d<f32>;\n@group(1) @binding(3) var material_sampler: sampler;\n\nconst DEBUG_METALLIC_SHIFT: u32 = 10u;\nconst DEBUG_ROUGHNESS_SHIFT: u32 = 18u;\nconst DEBUG_METALLIC_ENABLED: u32 = 1u << 26u;\nconst DEBUG_ROUGHNESS_ENABLED: u32 = 1u << 27u;\nconst WORLD_GRID_MATERIAL_FLAG: u32 = 1u << 28u;\n\n@vertex\nfn vs_main(@builtin(instance_index) instance_id: u32, input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n let model_matrix = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n var world_position = model_matrix * vec4<f32>(input.position, 1.0);\n\n let normal_matrix = mat3x3<f32>(\n model_matrix[0].xyz,\n model_matrix[1].xyz,\n model_matrix[2].xyz\n );\n let world_normal = normalize(normal_matrix * ori_oct_decode_normal(input.normal_oct));\n var world_tangent = normalize(normal_matrix * input.tangent.xyz);\n var handedness = input.tangent.w;\n if ((input.flags & INSTANCE_FLAG_TREE_WIND) != 0u) {\n world_position = vec4<f32>(\n world_position.xyz + tree_wind_offset(world_position.xyz, model_matrix[3].xyz, input.tangent),\n 1.0,\n );\n // TANGENT carries wind data, not geometry: rebuild an orthonormal tangent so normal\n // mapping stays sane if a normal-mapped material is ever assigned to a tree.\n world_tangent = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), world_normal) + vec3<f32>(0.001, 0.0, 0.0));\n handedness = 1.0;\n }\n let world_bitangent = normalize(cross(world_normal, world_tangent) * handedness);\n\n output.clip_position = u_camera.view_proj * world_position;\n output.normal = world_normal;\n output.uv = input.uv;\n output.tangent = world_tangent;\n output.bitangent = world_bitangent;\n output.mesh_color = input.mesh_color;\n output.emission_rgbi = input.emission_rgbi;\n output.flags = input.flags;\n output.world_pos = world_position.xyz;\n output.cur_clip = u_camera.unjittered_view_proj * world_position;\n output.prev_clip = u_camera.prev_unjittered_view_proj * world_position;\n return output;\n}\n\n// Static-LOD storage-fetch variant: the compact buffer holds u32 SOURCE INDICES; per-instance\n// data is fetched from the shared source instances buffer through the index, mirroring\n// static_lod_shadow.wgsl. lod_correction (identity for nearly all content \u2014 corr_identity_bits\n// fast path) is applied here instead of being baked by the scatter: model_matrix * corr is the\n// exact expression the scatter used, so draw output is unchanged. The pipeline using this entry\n// point binds no instance vertex buffer and adds the instance-fetch bind group at group 2.\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\nstruct DrawId {\n draw_index: u32,\n segment_index: u32,\n _pad1: u32,\n _pad2: u32,\n};\nstruct LodGroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32,\n corr_identity_bits: u32,\n _pad0b: u32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n@group(2) @binding(0) var<storage, read> lod_instances: array<InstanceData>;\n@group(2) @binding(1) var<storage, read> lod_segment_offsets: array<u32>;\n@group(2) @binding(2) var<storage, read> lod_compact_indices: array<u32>;\n@group(2) @binding(3) var<uniform> lod_draw_id: DrawId;\n@group(2) @binding(4) var<storage, read> lod_group_infos: array<LodGroupInfo>;\n\n// Switch-select (not dynamic array<mat4x4> indexing): the Adreno-safe pattern from the scatters.\nfn lod_correction_for(slot: u32, lod: u32) -> mat4x4<f32> {\n switch lod {\n case 0u: { return lod_group_infos[slot].lod_correction[0]; }\n case 1u: { return lod_group_infos[slot].lod_correction[1]; }\n case 2u: { return lod_group_infos[slot].lod_correction[2]; }\n default: { return lod_group_infos[slot].lod_correction[3]; }\n }\n}\n\n// Fetch + correction shared by every LOD draw entry point. segment_index = group_slot*4 + lod.\nfn lod_fetch_model_matrix_seg(inst_model: mat4x4<f32>, segment_index: u32) -> mat4x4<f32> {\n let slot = segment_index / 4u;\n let lod = segment_index % 4u;\n if (((lod_group_infos[slot].corr_identity_bits >> lod) & 1u) == 0u) {\n return inst_model * lod_correction_for(slot, lod);\n }\n return inst_model;\n}\nfn lod_fetch_model_matrix(inst_model: mat4x4<f32>) -> mat4x4<f32> {\n return lod_fetch_model_matrix_seg(inst_model, lod_draw_id.segment_index);\n}\n\nstruct VertexInputLod {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n};\n\n@vertex\nfn vs_main_lod(@builtin(instance_index) instance_index: u32, input: VertexInputLod) -> VertexOutput {\n var output: VertexOutput;\n let inst = lod_instances[lod_compact_indices[lod_segment_offsets[lod_draw_id.segment_index] + instance_index]];\n let model_matrix = lod_fetch_model_matrix(inst.model_matrix);\n var world_position = model_matrix * vec4<f32>(input.position, 1.0);\n let normal_matrix = mat3x3<f32>(\n model_matrix[0].xyz,\n model_matrix[1].xyz,\n model_matrix[2].xyz\n );\n let world_normal = normalize(normal_matrix * ori_oct_decode_normal(input.normal_oct));\n var world_tangent = normalize(normal_matrix * input.tangent.xyz);\n var handedness = input.tangent.w;\n if ((inst.flags & INSTANCE_FLAG_TREE_WIND) != 0u) {\n world_position = vec4<f32>(\n world_position.xyz + tree_wind_offset(world_position.xyz, model_matrix[3].xyz, input.tangent),\n 1.0,\n );\n world_tangent = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), world_normal) + vec3<f32>(0.001, 0.0, 0.0));\n handedness = 1.0;\n }\n let world_bitangent = normalize(cross(world_normal, world_tangent) * handedness);\n\n output.clip_position = u_camera.view_proj * world_position;\n output.normal = world_normal;\n output.uv = input.uv;\n output.tangent = world_tangent;\n output.bitangent = world_bitangent;\n output.mesh_color = inst.mesh_color;\n output.emission_rgbi = inst.emission_color_and_intensity;\n output.flags = inst.flags;\n output.world_pos = world_position.xyz;\n output.cur_clip = u_camera.unjittered_view_proj * world_position;\n output.prev_clip = u_camera.prev_unjittered_view_proj * world_position;\n return output;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>,\n @location(1) normal: vec4<f32>,\n @location(2) orm: vec4<f32>,\n @location(3) velocity: vec2<f32>,\n};\n\n// Velocity gbuffer term: `prev_uv - cur_uv` in UV units from UNJITTERED clip positions (same\n// convention as the upscaler inputs). w<=0 guard doubles as the secondary-camera off-switch\n// (their camera uniforms leave the unjittered matrices zeroed).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nstruct GridTriplanarCoords {\n uvx: vec2<f32>,\n uvy: vec2<f32>,\n uvz: vec2<f32>,\n uvx_dx: vec2<f32>,\n uvx_dy: vec2<f32>,\n uvy_dx: vec2<f32>,\n uvy_dy: vec2<f32>,\n uvz_dx: vec2<f32>,\n uvz_dy: vec2<f32>,\n weights: vec3<f32>,\n};\n\nconst GRID_MINOR_SPACING_METERS: f32 = 1.0;\nconst GRID_MAJOR_SPACING_METERS: f32 = 5.0;\nconst GRID_MINOR_PHASE_METERS: f32 = 0.0;\nconst GRID_MAJOR_PHASE_METERS: f32 = 0.0;\nconst GRID_MINOR_WIDTH_METERS: f32 = 0.00732421875;\nconst GRID_MAJOR_WIDTH_METERS: f32 = 0.0244140625;\nconst GRID_BACKGROUND_LEVEL: f32 = 0.04;\nconst GRID_MINOR_LEVEL: f32 = 0.07;\nconst GRID_MAJOR_LEVEL: f32 = 0.10;\n\nfn grid_triplanar_coords(\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos_dx: vec3<f32>,\n world_pos_dy: vec3<f32>,\n) -> GridTriplanarCoords {\n let w0 = pow(abs(world_normal), vec3<f32>(1.0, 1.0, 1.0));\n let wsum = max(w0.x + w0.y + w0.z, 0.000001);\n var coords: GridTriplanarCoords;\n coords.uvx = world_pos.zy;\n coords.uvy = world_pos.xz;\n coords.uvz = world_pos.xy;\n coords.uvx_dx = world_pos_dx.zy;\n coords.uvx_dy = world_pos_dy.zy;\n coords.uvy_dx = world_pos_dx.xz;\n coords.uvy_dy = world_pos_dy.xz;\n coords.uvz_dx = world_pos_dx.xy;\n coords.uvz_dy = world_pos_dy.xy;\n coords.weights = w0 / wsum;\n return coords;\n}\n\nfn grid_periodic_pulse_integral(t: f32, half_width: f32) -> f32 {\n let h = clamp(half_width, 0.0, 0.5);\n let whole = floor(t);\n let f = fract(t);\n var partial = h;\n if (f < h) {\n partial = f;\n } else if (f > 1.0 - h) {\n partial = h + f - (1.0 - h);\n }\n return whole * (2.0 * h) + partial;\n}\n\nfn grid_filtered_periodic_pulse(\n coord: f32,\n period: f32,\n line_width: f32,\n phase: f32,\n filter_width: f32,\n) -> f32 {\n let safe_period = max(period, 0.000001);\n let safe_filter = max(filter_width, 0.000001);\n let half_filter = safe_filter * 0.5;\n let a = (coord - phase - half_filter) / safe_period;\n let b = (coord - phase + half_filter) / safe_period;\n let half_width = clamp((line_width * 0.5) / safe_period, 0.0, 0.5);\n let coverage = (\n grid_periodic_pulse_integral(b, half_width) -\n grid_periodic_pulse_integral(a, half_width)\n ) / max(b - a, 0.000001);\n return clamp(coverage, 0.0, 1.0);\n}\n\nfn grid_line_mask(\n uv: vec2<f32>,\n uv_dx: vec2<f32>,\n uv_dy: vec2<f32>,\n period: f32,\n line_width: f32,\n phase: f32,\n) -> f32 {\n let x_filter = abs(uv_dx.x) + abs(uv_dy.x);\n let y_filter = abs(uv_dx.y) + abs(uv_dy.y);\n let x_mask = grid_filtered_periodic_pulse(uv.x, period, line_width, phase, x_filter);\n let y_mask = grid_filtered_periodic_pulse(uv.y, period, line_width, phase, y_filter);\n return max(x_mask, y_mask);\n}\n\nfn grid_projected_color(uv: vec2<f32>, uv_dx: vec2<f32>, uv_dy: vec2<f32>) -> vec3<f32> {\n let minor_raw = grid_line_mask(\n uv,\n uv_dx,\n uv_dy,\n GRID_MINOR_SPACING_METERS,\n GRID_MINOR_WIDTH_METERS,\n GRID_MINOR_PHASE_METERS,\n );\n let major = grid_line_mask(\n uv,\n uv_dx,\n uv_dy,\n GRID_MAJOR_SPACING_METERS,\n GRID_MAJOR_WIDTH_METERS,\n GRID_MAJOR_PHASE_METERS,\n );\n let minor = clamp(minor_raw - major, 0.0, 1.0);\n let line_hole = max(minor_raw, major);\n let value = GRID_BACKGROUND_LEVEL * (1.0 - line_hole)\n + GRID_MINOR_LEVEL * minor\n + GRID_MAJOR_LEVEL * major;\n return vec3<f32>(value);\n}\n\nfn world_grid_base_color(\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos_dx: vec3<f32>,\n world_pos_dy: vec3<f32>,\n) -> vec3<f32> {\n let coords = grid_triplanar_coords(world_pos, world_normal, world_pos_dx, world_pos_dy);\n let sx = grid_projected_color(coords.uvx, coords.uvx_dx, coords.uvx_dy);\n let sy = grid_projected_color(coords.uvy, coords.uvy_dx, coords.uvy_dy);\n let sz = grid_projected_color(coords.uvz, coords.uvz_dx, coords.uvz_dy);\n return sx * coords.weights.x + sy * coords.weights.y + sz * coords.weights.z;\n}\n\n// Mesh-particle dissolve (DISPLAY_FLAG_DEATH_DISSOLVE bit, static-side reuse).\nconst MESH_PARTICLE_FADE_BIT: u32 = 1u << 9u;\nfn bayer4(p: vec2<u32>) -> f32 {\n var m = array<f32, 16>(\n 0.03125, 0.53125, 0.15625, 0.65625,\n 0.78125, 0.28125, 0.90625, 0.40625,\n 0.21875, 0.71875, 0.09375, 0.59375,\n 0.96875, 0.46875, 0.84375, 0.34375,\n );\n return m[(p.y % 4u) * 4u + (p.x % 4u)];\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @location(0) normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) tangent: vec3<f32>,\n @location(3) bitangent: vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos: vec3<f32>,\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n @builtin(front_facing) front_facing: bool,\n) -> GBufferOutput {\n var output: GBufferOutput;\n output.velocity = gbuffer_velocity(cur_clip, prev_clip);\n if (u_camera.reflection_clip_enabled > 0.5 && world_pos.y < u_camera.reflection_clip_y) {\n discard;\n }\n\n let world_pos_dx = dpdx(world_pos);\n let world_pos_dy = dpdy(world_pos);\n let mat_mip_bias = u_camera.mip_bias + mesh_color_tuning.sample_params.x;\n let base_color_sampled = textureSampleBias(base_color_texture, material_sampler, uv, mat_mip_bias);\n let texture_normal = textureSampleBias(normal_texture, material_sampler, uv, mat_mip_bias).rgb;\n let orm_sample = textureSampleBias(orm_texture, material_sampler, uv, mat_mip_bias);\n let ao_multiplier = mesh_color_tuning.surface_params.w;\n let metallic_multiplier = mesh_color_tuning.surface_params.z;\n let min_roughness = mesh_color_tuning.surface_params.x;\n let do_roughness_smoothstep = mesh_color_tuning.misc_params.y > 0.5;\n let roughness_after_curve = select(orm_sample.g, smoothstep(0.0, 1.0, orm_sample.g), do_roughness_smoothstep);\n var orm = vec3<f32>(\n clamp(orm_sample.r * ao_multiplier, 0.0, 1.0),\n clamp(max(roughness_after_curve, min_roughness), 0.0, 1.0),\n clamp(orm_sample.b * metallic_multiplier, 0.0, 1.0)\n );\n if ((flags & DEBUG_METALLIC_ENABLED) != 0u) {\n orm.b = f32((flags >> DEBUG_METALLIC_SHIFT) & 255u) / 255.0;\n }\n if ((flags & DEBUG_ROUGHNESS_ENABLED) != 0u) {\n orm.g = f32((flags >> DEBUG_ROUGHNESS_SHIFT) & 255u) / 255.0;\n }\n\n // Read .rg only and force z=-1: matches BC5 (which decodes b=0) so behavior is identical on\n // BC and ASTC (ASTC keeps a non-zero blue we must ignore).\n var tangent_space_normal = apply_normal_strength(vec3<f32>(texture_normal.rg * 2.0 - vec2<f32>(1.0), -1.0));\n let world_normal_unbent = normalize(\n tangent_space_normal.x * tangent +\n tangent_space_normal.y * bitangent +\n -tangent_space_normal.z * normal\n );\n let world_normal = normalize(mix(world_normal_unbent, vec3<f32>(0.0, 1.0, 0.0), mesh_color_tuning.misc_params.z));\n\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let base_color = vec4<f32>(adjusted_rgb, base_color_sampled.a);\n var shaded_color = base_color * mesh_color;\n var final_normal = world_normal;\n if ((flags & WORLD_GRID_MATERIAL_FLAG) != 0u) {\n let grid_color = world_grid_base_color(world_pos, normal, world_pos_dx, world_pos_dy);\n shaded_color = vec4<f32>(grid_color, 1.0);\n final_normal = normalize(normal);\n orm = vec3<f32>(1.0, 1.0, 0.0);\n }\n if (USE_ALPHA_CUTOFF) {\n if (shaded_color.a < ALPHA_CUTOFF) { discard; }\n }\n // Mesh-particle fade: flag-gated screen-space Bayer dissolve driven by mesh_color.a\n // (deferred output has no alpha; static instances never set this bit otherwise).\n if ((flags & MESH_PARTICLE_FADE_BIT) != 0u) {\n if (bayer4(vec2<u32>(clip_position.xy)) > mesh_color.a) { discard; }\n }\n // Two-sided (cull-None) pipelines: backfaces shade with the flipped FINAL normal (flipping\n // the input normal would invert the synthetic tree-wind TBN handedness). Culled pipelines\n // never rasterize backfaces, so this is a no-op for them.\n if (!front_facing) {\n final_normal = -final_normal;\n }\n // normal.a class: 1.0 = static, 0.875 = static + screen-space edge bevel (#mesh assets).\n output.normal = vec4<f32>(final_normal, select(1.0, 0.875, (flags & INSTANCE_FLAG_EDGE_BEVEL) != 0u));\n // SetEmissionColor tint: the light pass tints the emissive add by base_color, so pull the\n // written albedo toward the emission color as glow strengthens (full by ~I=8). Near-black\n // emission color (never set) keeps the albedo-tinted glow; trailer mode reuses the rgb slots.\n let E = clamp(emission_rgbi.a + mesh_color_tuning.misc_params.w, 0.0, 1.0);\n let em_lin = exp2(E * 10.0) - 1.0;\n const TRAILER_MODE_BIT: u32 = 1u << 8u;\n if (em_lin > 0.0 && max(emission_rgbi.r, max(emission_rgbi.g, emission_rgbi.b)) > 0.003 && (flags & TRAILER_MODE_BIT) == 0u) {\n shaded_color = vec4<f32>(mix(shaded_color.rgb, emission_rgbi.rgb, clamp(em_lin * 0.125, 0.0, 1.0)), shaded_color.a);\n }\n output.base_color = shaded_color;\n output.base_color.a = mesh_color_tuning.misc_params.x;\n\n output.orm = vec4<f32>(orm, E);\n\n let ndc_z = clip_position.z / clip_position.w;\n return output;\n}\n"},{"label":"GBuffer Masked Shader","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nconst USE_ALPHA_CUTOFF: bool = true;\nconst ALPHA_CUTOFF: f32 = 0.5 ;\n// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// gbuffer.wgsl\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) emission_rgbi: vec4<f32>,\n @location(10) flags: u32,\n};\nstruct VertexOutput {\n // @invariant: depth_prepass_lod.wgsl re-runs the same position math depth-only and the\n // opaque LOD gbuffer pipeline compares Equal against it \u2014 positions must match bit-exact.\n @builtin(position) @invariant clip_position: vec4<f32>,\n @location(0) normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) tangent: vec3<f32>,\n @location(3) bitangent: vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos: vec3<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n // [render dir x, render dir z, strength 0..1, gustiness 0..1]\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst INSTANCE_FLAG_TREE_WIND: u32 = 1u << 2u;\nconst INSTANCE_FLAG_EDGE_BEVEL: u32 = 1u << 3u;\n\n// Hierarchical tree sway (tree meshes only, gated by INSTANCE_FLAG_TREE_WIND).\n// `wind_data` rides the TANGENT slot: x = per-branch phase 0..1, y = branch flex,\n// z = trunk bend weight (height^2). Returns a world-space offset in meters.\nfn tree_wind_offset(world_pos: vec3<f32>, tree_root: vec3<f32>, wind_data: vec4<f32>) -> vec3<f32> {\n let strength = u_camera.wind.z;\n let wdir = vec3<f32>(u_camera.wind.x, 0.0, u_camera.wind.y);\n let t = u_camera.time_seconds;\n // Gust wave travels along the wind direction; per-tree phase keys off root position so a\n // forest ripples instead of pumping in unison.\n let tree_phase = dot(tree_root, wdir) * 0.15;\n let gust = 0.5 + 0.5 * sin(t * 0.9 - tree_phase) * u_camera.wind.w;\n // Trunk: slow lean into the wind + gentle oscillation, growing with height^2.\n let trunk = wind_data.z * strength * (0.10 + 0.22 * gust + 0.05 * sin(t * 1.3 - tree_phase));\n // Branch: per-branch phase, faster oscillation, flex ramps toward branch tips.\n let bs = sin(t * (1.8 + 1.2 * strength) + wind_data.x * 6.2831853 - tree_phase);\n let branch = wind_data.y * strength * (0.35 + 0.65 * gust) * 0.18;\n return wdir * (trunk + bs * branch) + vec3<f32>(0.0, 1.0, 0.0) * (bs * branch * -0.3);\n}\n\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>;\n@group(1) @binding(2) var orm_texture: texture_2d<f32>;\n@group(1) @binding(3) var material_sampler: sampler;\n\nconst DEBUG_METALLIC_SHIFT: u32 = 10u;\nconst DEBUG_ROUGHNESS_SHIFT: u32 = 18u;\nconst DEBUG_METALLIC_ENABLED: u32 = 1u << 26u;\nconst DEBUG_ROUGHNESS_ENABLED: u32 = 1u << 27u;\nconst WORLD_GRID_MATERIAL_FLAG: u32 = 1u << 28u;\n\n@vertex\nfn vs_main(@builtin(instance_index) instance_id: u32, input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n let model_matrix = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n var world_position = model_matrix * vec4<f32>(input.position, 1.0);\n\n let normal_matrix = mat3x3<f32>(\n model_matrix[0].xyz,\n model_matrix[1].xyz,\n model_matrix[2].xyz\n );\n let world_normal = normalize(normal_matrix * ori_oct_decode_normal(input.normal_oct));\n var world_tangent = normalize(normal_matrix * input.tangent.xyz);\n var handedness = input.tangent.w;\n if ((input.flags & INSTANCE_FLAG_TREE_WIND) != 0u) {\n world_position = vec4<f32>(\n world_position.xyz + tree_wind_offset(world_position.xyz, model_matrix[3].xyz, input.tangent),\n 1.0,\n );\n // TANGENT carries wind data, not geometry: rebuild an orthonormal tangent so normal\n // mapping stays sane if a normal-mapped material is ever assigned to a tree.\n world_tangent = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), world_normal) + vec3<f32>(0.001, 0.0, 0.0));\n handedness = 1.0;\n }\n let world_bitangent = normalize(cross(world_normal, world_tangent) * handedness);\n\n output.clip_position = u_camera.view_proj * world_position;\n output.normal = world_normal;\n output.uv = input.uv;\n output.tangent = world_tangent;\n output.bitangent = world_bitangent;\n output.mesh_color = input.mesh_color;\n output.emission_rgbi = input.emission_rgbi;\n output.flags = input.flags;\n output.world_pos = world_position.xyz;\n output.cur_clip = u_camera.unjittered_view_proj * world_position;\n output.prev_clip = u_camera.prev_unjittered_view_proj * world_position;\n return output;\n}\n\n// Static-LOD storage-fetch variant: the compact buffer holds u32 SOURCE INDICES; per-instance\n// data is fetched from the shared source instances buffer through the index, mirroring\n// static_lod_shadow.wgsl. lod_correction (identity for nearly all content \u2014 corr_identity_bits\n// fast path) is applied here instead of being baked by the scatter: model_matrix * corr is the\n// exact expression the scatter used, so draw output is unchanged. The pipeline using this entry\n// point binds no instance vertex buffer and adds the instance-fetch bind group at group 2.\nstruct InstanceData {\n model_matrix: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission_color_and_intensity: vec4<f32>,\n flags: u32,\n _pad: array<u32, 3>,\n};\nstruct DrawId {\n draw_index: u32,\n segment_index: u32,\n _pad1: u32,\n _pad2: u32,\n};\nstruct LodGroupInfo {\n lod_mask: u32,\n source_start: u32,\n source_count: u32,\n chunk_entry_start: u32,\n chunk_count: u32,\n max_distance_m: f32,\n corr_identity_bits: u32,\n _pad0b: u32,\n lod_offsets: vec4<u32>,\n lod_correction: array<mat4x4<f32>, 4>,\n};\n@group(2) @binding(0) var<storage, read> lod_instances: array<InstanceData>;\n@group(2) @binding(1) var<storage, read> lod_segment_offsets: array<u32>;\n@group(2) @binding(2) var<storage, read> lod_compact_indices: array<u32>;\n@group(2) @binding(3) var<uniform> lod_draw_id: DrawId;\n@group(2) @binding(4) var<storage, read> lod_group_infos: array<LodGroupInfo>;\n\n// Switch-select (not dynamic array<mat4x4> indexing): the Adreno-safe pattern from the scatters.\nfn lod_correction_for(slot: u32, lod: u32) -> mat4x4<f32> {\n switch lod {\n case 0u: { return lod_group_infos[slot].lod_correction[0]; }\n case 1u: { return lod_group_infos[slot].lod_correction[1]; }\n case 2u: { return lod_group_infos[slot].lod_correction[2]; }\n default: { return lod_group_infos[slot].lod_correction[3]; }\n }\n}\n\n// Fetch + correction shared by every LOD draw entry point. segment_index = group_slot*4 + lod.\nfn lod_fetch_model_matrix_seg(inst_model: mat4x4<f32>, segment_index: u32) -> mat4x4<f32> {\n let slot = segment_index / 4u;\n let lod = segment_index % 4u;\n if (((lod_group_infos[slot].corr_identity_bits >> lod) & 1u) == 0u) {\n return inst_model * lod_correction_for(slot, lod);\n }\n return inst_model;\n}\nfn lod_fetch_model_matrix(inst_model: mat4x4<f32>) -> mat4x4<f32> {\n return lod_fetch_model_matrix_seg(inst_model, lod_draw_id.segment_index);\n}\n\nstruct VertexInputLod {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n};\n\n@vertex\nfn vs_main_lod(@builtin(instance_index) instance_index: u32, input: VertexInputLod) -> VertexOutput {\n var output: VertexOutput;\n let inst = lod_instances[lod_compact_indices[lod_segment_offsets[lod_draw_id.segment_index] + instance_index]];\n let model_matrix = lod_fetch_model_matrix(inst.model_matrix);\n var world_position = model_matrix * vec4<f32>(input.position, 1.0);\n let normal_matrix = mat3x3<f32>(\n model_matrix[0].xyz,\n model_matrix[1].xyz,\n model_matrix[2].xyz\n );\n let world_normal = normalize(normal_matrix * ori_oct_decode_normal(input.normal_oct));\n var world_tangent = normalize(normal_matrix * input.tangent.xyz);\n var handedness = input.tangent.w;\n if ((inst.flags & INSTANCE_FLAG_TREE_WIND) != 0u) {\n world_position = vec4<f32>(\n world_position.xyz + tree_wind_offset(world_position.xyz, model_matrix[3].xyz, input.tangent),\n 1.0,\n );\n world_tangent = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), world_normal) + vec3<f32>(0.001, 0.0, 0.0));\n handedness = 1.0;\n }\n let world_bitangent = normalize(cross(world_normal, world_tangent) * handedness);\n\n output.clip_position = u_camera.view_proj * world_position;\n output.normal = world_normal;\n output.uv = input.uv;\n output.tangent = world_tangent;\n output.bitangent = world_bitangent;\n output.mesh_color = inst.mesh_color;\n output.emission_rgbi = inst.emission_color_and_intensity;\n output.flags = inst.flags;\n output.world_pos = world_position.xyz;\n output.cur_clip = u_camera.unjittered_view_proj * world_position;\n output.prev_clip = u_camera.prev_unjittered_view_proj * world_position;\n return output;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>,\n @location(1) normal: vec4<f32>,\n @location(2) orm: vec4<f32>,\n @location(3) velocity: vec2<f32>,\n};\n\n// Velocity gbuffer term: `prev_uv - cur_uv` in UV units from UNJITTERED clip positions (same\n// convention as the upscaler inputs). w<=0 guard doubles as the secondary-camera off-switch\n// (their camera uniforms leave the unjittered matrices zeroed).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nstruct GridTriplanarCoords {\n uvx: vec2<f32>,\n uvy: vec2<f32>,\n uvz: vec2<f32>,\n uvx_dx: vec2<f32>,\n uvx_dy: vec2<f32>,\n uvy_dx: vec2<f32>,\n uvy_dy: vec2<f32>,\n uvz_dx: vec2<f32>,\n uvz_dy: vec2<f32>,\n weights: vec3<f32>,\n};\n\nconst GRID_MINOR_SPACING_METERS: f32 = 1.0;\nconst GRID_MAJOR_SPACING_METERS: f32 = 5.0;\nconst GRID_MINOR_PHASE_METERS: f32 = 0.0;\nconst GRID_MAJOR_PHASE_METERS: f32 = 0.0;\nconst GRID_MINOR_WIDTH_METERS: f32 = 0.00732421875;\nconst GRID_MAJOR_WIDTH_METERS: f32 = 0.0244140625;\nconst GRID_BACKGROUND_LEVEL: f32 = 0.04;\nconst GRID_MINOR_LEVEL: f32 = 0.07;\nconst GRID_MAJOR_LEVEL: f32 = 0.10;\n\nfn grid_triplanar_coords(\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos_dx: vec3<f32>,\n world_pos_dy: vec3<f32>,\n) -> GridTriplanarCoords {\n let w0 = pow(abs(world_normal), vec3<f32>(1.0, 1.0, 1.0));\n let wsum = max(w0.x + w0.y + w0.z, 0.000001);\n var coords: GridTriplanarCoords;\n coords.uvx = world_pos.zy;\n coords.uvy = world_pos.xz;\n coords.uvz = world_pos.xy;\n coords.uvx_dx = world_pos_dx.zy;\n coords.uvx_dy = world_pos_dy.zy;\n coords.uvy_dx = world_pos_dx.xz;\n coords.uvy_dy = world_pos_dy.xz;\n coords.uvz_dx = world_pos_dx.xy;\n coords.uvz_dy = world_pos_dy.xy;\n coords.weights = w0 / wsum;\n return coords;\n}\n\nfn grid_periodic_pulse_integral(t: f32, half_width: f32) -> f32 {\n let h = clamp(half_width, 0.0, 0.5);\n let whole = floor(t);\n let f = fract(t);\n var partial = h;\n if (f < h) {\n partial = f;\n } else if (f > 1.0 - h) {\n partial = h + f - (1.0 - h);\n }\n return whole * (2.0 * h) + partial;\n}\n\nfn grid_filtered_periodic_pulse(\n coord: f32,\n period: f32,\n line_width: f32,\n phase: f32,\n filter_width: f32,\n) -> f32 {\n let safe_period = max(period, 0.000001);\n let safe_filter = max(filter_width, 0.000001);\n let half_filter = safe_filter * 0.5;\n let a = (coord - phase - half_filter) / safe_period;\n let b = (coord - phase + half_filter) / safe_period;\n let half_width = clamp((line_width * 0.5) / safe_period, 0.0, 0.5);\n let coverage = (\n grid_periodic_pulse_integral(b, half_width) -\n grid_periodic_pulse_integral(a, half_width)\n ) / max(b - a, 0.000001);\n return clamp(coverage, 0.0, 1.0);\n}\n\nfn grid_line_mask(\n uv: vec2<f32>,\n uv_dx: vec2<f32>,\n uv_dy: vec2<f32>,\n period: f32,\n line_width: f32,\n phase: f32,\n) -> f32 {\n let x_filter = abs(uv_dx.x) + abs(uv_dy.x);\n let y_filter = abs(uv_dx.y) + abs(uv_dy.y);\n let x_mask = grid_filtered_periodic_pulse(uv.x, period, line_width, phase, x_filter);\n let y_mask = grid_filtered_periodic_pulse(uv.y, period, line_width, phase, y_filter);\n return max(x_mask, y_mask);\n}\n\nfn grid_projected_color(uv: vec2<f32>, uv_dx: vec2<f32>, uv_dy: vec2<f32>) -> vec3<f32> {\n let minor_raw = grid_line_mask(\n uv,\n uv_dx,\n uv_dy,\n GRID_MINOR_SPACING_METERS,\n GRID_MINOR_WIDTH_METERS,\n GRID_MINOR_PHASE_METERS,\n );\n let major = grid_line_mask(\n uv,\n uv_dx,\n uv_dy,\n GRID_MAJOR_SPACING_METERS,\n GRID_MAJOR_WIDTH_METERS,\n GRID_MAJOR_PHASE_METERS,\n );\n let minor = clamp(minor_raw - major, 0.0, 1.0);\n let line_hole = max(minor_raw, major);\n let value = GRID_BACKGROUND_LEVEL * (1.0 - line_hole)\n + GRID_MINOR_LEVEL * minor\n + GRID_MAJOR_LEVEL * major;\n return vec3<f32>(value);\n}\n\nfn world_grid_base_color(\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos_dx: vec3<f32>,\n world_pos_dy: vec3<f32>,\n) -> vec3<f32> {\n let coords = grid_triplanar_coords(world_pos, world_normal, world_pos_dx, world_pos_dy);\n let sx = grid_projected_color(coords.uvx, coords.uvx_dx, coords.uvx_dy);\n let sy = grid_projected_color(coords.uvy, coords.uvy_dx, coords.uvy_dy);\n let sz = grid_projected_color(coords.uvz, coords.uvz_dx, coords.uvz_dy);\n return sx * coords.weights.x + sy * coords.weights.y + sz * coords.weights.z;\n}\n\n// Mesh-particle dissolve (DISPLAY_FLAG_DEATH_DISSOLVE bit, static-side reuse).\nconst MESH_PARTICLE_FADE_BIT: u32 = 1u << 9u;\nfn bayer4(p: vec2<u32>) -> f32 {\n var m = array<f32, 16>(\n 0.03125, 0.53125, 0.15625, 0.65625,\n 0.78125, 0.28125, 0.90625, 0.40625,\n 0.21875, 0.71875, 0.09375, 0.59375,\n 0.96875, 0.46875, 0.84375, 0.34375,\n );\n return m[(p.y % 4u) * 4u + (p.x % 4u)];\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @location(0) normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) tangent: vec3<f32>,\n @location(3) bitangent: vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos: vec3<f32>,\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n @builtin(front_facing) front_facing: bool,\n) -> GBufferOutput {\n var output: GBufferOutput;\n output.velocity = gbuffer_velocity(cur_clip, prev_clip);\n if (u_camera.reflection_clip_enabled > 0.5 && world_pos.y < u_camera.reflection_clip_y) {\n discard;\n }\n\n let world_pos_dx = dpdx(world_pos);\n let world_pos_dy = dpdy(world_pos);\n let mat_mip_bias = u_camera.mip_bias + mesh_color_tuning.sample_params.x;\n let base_color_sampled = textureSampleBias(base_color_texture, material_sampler, uv, mat_mip_bias);\n let texture_normal = textureSampleBias(normal_texture, material_sampler, uv, mat_mip_bias).rgb;\n let orm_sample = textureSampleBias(orm_texture, material_sampler, uv, mat_mip_bias);\n let ao_multiplier = mesh_color_tuning.surface_params.w;\n let metallic_multiplier = mesh_color_tuning.surface_params.z;\n let min_roughness = mesh_color_tuning.surface_params.x;\n let do_roughness_smoothstep = mesh_color_tuning.misc_params.y > 0.5;\n let roughness_after_curve = select(orm_sample.g, smoothstep(0.0, 1.0, orm_sample.g), do_roughness_smoothstep);\n var orm = vec3<f32>(\n clamp(orm_sample.r * ao_multiplier, 0.0, 1.0),\n clamp(max(roughness_after_curve, min_roughness), 0.0, 1.0),\n clamp(orm_sample.b * metallic_multiplier, 0.0, 1.0)\n );\n if ((flags & DEBUG_METALLIC_ENABLED) != 0u) {\n orm.b = f32((flags >> DEBUG_METALLIC_SHIFT) & 255u) / 255.0;\n }\n if ((flags & DEBUG_ROUGHNESS_ENABLED) != 0u) {\n orm.g = f32((flags >> DEBUG_ROUGHNESS_SHIFT) & 255u) / 255.0;\n }\n\n // Read .rg only and force z=-1: matches BC5 (which decodes b=0) so behavior is identical on\n // BC and ASTC (ASTC keeps a non-zero blue we must ignore).\n var tangent_space_normal = apply_normal_strength(vec3<f32>(texture_normal.rg * 2.0 - vec2<f32>(1.0), -1.0));\n let world_normal_unbent = normalize(\n tangent_space_normal.x * tangent +\n tangent_space_normal.y * bitangent +\n -tangent_space_normal.z * normal\n );\n let world_normal = normalize(mix(world_normal_unbent, vec3<f32>(0.0, 1.0, 0.0), mesh_color_tuning.misc_params.z));\n\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let base_color = vec4<f32>(adjusted_rgb, base_color_sampled.a);\n var shaded_color = base_color * mesh_color;\n var final_normal = world_normal;\n if ((flags & WORLD_GRID_MATERIAL_FLAG) != 0u) {\n let grid_color = world_grid_base_color(world_pos, normal, world_pos_dx, world_pos_dy);\n shaded_color = vec4<f32>(grid_color, 1.0);\n final_normal = normalize(normal);\n orm = vec3<f32>(1.0, 1.0, 0.0);\n }\n if (USE_ALPHA_CUTOFF) {\n if (shaded_color.a < ALPHA_CUTOFF) { discard; }\n }\n // Mesh-particle fade: flag-gated screen-space Bayer dissolve driven by mesh_color.a\n // (deferred output has no alpha; static instances never set this bit otherwise).\n if ((flags & MESH_PARTICLE_FADE_BIT) != 0u) {\n if (bayer4(vec2<u32>(clip_position.xy)) > mesh_color.a) { discard; }\n }\n // Two-sided (cull-None) pipelines: backfaces shade with the flipped FINAL normal (flipping\n // the input normal would invert the synthetic tree-wind TBN handedness). Culled pipelines\n // never rasterize backfaces, so this is a no-op for them.\n if (!front_facing) {\n final_normal = -final_normal;\n }\n // normal.a class: 1.0 = static, 0.875 = static + screen-space edge bevel (#mesh assets).\n output.normal = vec4<f32>(final_normal, select(1.0, 0.875, (flags & INSTANCE_FLAG_EDGE_BEVEL) != 0u));\n // SetEmissionColor tint: the light pass tints the emissive add by base_color, so pull the\n // written albedo toward the emission color as glow strengthens (full by ~I=8). Near-black\n // emission color (never set) keeps the albedo-tinted glow; trailer mode reuses the rgb slots.\n let E = clamp(emission_rgbi.a + mesh_color_tuning.misc_params.w, 0.0, 1.0);\n let em_lin = exp2(E * 10.0) - 1.0;\n const TRAILER_MODE_BIT: u32 = 1u << 8u;\n if (em_lin > 0.0 && max(emission_rgbi.r, max(emission_rgbi.g, emission_rgbi.b)) > 0.003 && (flags & TRAILER_MODE_BIT) == 0u) {\n shaded_color = vec4<f32>(mix(shaded_color.rgb, emission_rgbi.rgb, clamp(em_lin * 0.125, 0.0, 1.0)), shaded_color.a);\n }\n output.base_color = shaded_color;\n output.base_color.a = mesh_color_tuning.misc_params.x;\n\n output.orm = vec4<f32>(orm, E);\n\n let ndc_z = clip_position.z / clip_position.w;\n return output;\n}\n"},{"label":"GBuffer Skinned Shader","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nconst USE_ALPHA_CUTOFF: bool = false;\nconst ALPHA_CUTOFF: f32 = 0.5 ;\n// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// gbuffer_skinned.wgsl\nstruct SkinnedVertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n @location(4) joints: vec4<u32>,\n @location(5) weights: vec4<f32>,\n\n @location(6) model_0: vec4<f32>,\n @location(7) model_1: vec4<f32>,\n @location(8) model_2: vec4<f32>,\n @location(9) model_3: vec4<f32>,\n @location(10) mesh_color: vec4<f32>,\n @location(11) emission_rgbi: vec4<f32>,\n @location(12) anim_misc: vec4<u32>,\n @location(13) percent_progress: f32,\n // Own index into the full-list instance/prev-snapshot buffers (compacted records carry\n // the original index here, stamped by the cull) - the prev-record fetch key for velocity.\n @location(14) src_index: u32,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera : CameraUniform;\n\n@group(1) @binding(0) var base_color_texture : texture_2d<f32>;\n@group(1) @binding(1) var normal_texture : texture_2d<f32>;\n@group(1) @binding(2) var orm_texture : texture_2d<f32>;\n@group(1) @binding(3) var material_sampler : sampler;\n\nconst DEBUG_METALLIC_SHIFT: u32 = 10u;\nconst DEBUG_ROUGHNESS_SHIFT: u32 = 18u;\nconst DEBUG_METALLIC_ENABLED: u32 = 1u << 26u;\nconst DEBUG_ROUGHNESS_ENABLED: u32 = 1u << 27u;\n\n@group(2) @binding(0) var<storage, read> u_palettes : array<mat4x4<f32>>;\n// Prev-frame snapshots for the velocity output (bindings aliased to the CURRENT buffers on\n// stale-snapshot frames => prev == cur => camera-term velocity; see gbuffer_palette_bg).\n@group(2) @binding(1) var<storage, read> u_prev_palettes : array<mat4x4<f32>>;\n// Mirrors render_base::SkinnedInstanceData (144 bytes).\nstruct SkinnedInst {\n model: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission: vec4<f32>,\n misc0: vec4<u32>, // flags, clip_id, skin_id, palette_offset\n misc1: vec4<u32>, // percent(f32 bits), upper_clip, upper_percent(f32 bits), src_index\n aim_pitch_quat: vec4<f32>,\n};\n@group(2) @binding(2) var<storage, read> u_prev_instances : array<SkinnedInst>;\n\nstruct VertexOutput {\n @builtin(position) clip_position : vec4<f32>,\n @location(0) normal : vec3<f32>,\n @location(1) uv : vec2<f32>,\n @location(2) tangent : vec3<f32>,\n @location(3) bitangent : vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags : u32,\n @location(7) world_pos : vec3<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip : vec4<f32>,\n @location(9) prev_clip : vec4<f32>,\n};\n\n@vertex\nfn vs_main(in : SkinnedVertexInput) -> VertexOutput {\n let weight_sum = max(in.weights.x + in.weights.y + in.weights.z + in.weights.w, 1e-5);\n let weights = in.weights / weight_sum;\n var skinned_pos = vec4<f32>(0.0);\n var skinned_norm = vec3<f32>(0.0);\n var skinned_tan = vec3<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n let joint_index = in.joints[i] + in.anim_misc.w;\n let joint_mat = u_palettes[joint_index];\n skinned_pos += joint_mat * vec4<f32>(in.position , 1.0) * weights[i];\n skinned_norm += (joint_mat * vec4<f32>(in.normal , 0.0)).xyz * weights[i];\n skinned_tan += (joint_mat * vec4<f32>(in.tangent.xyz, 0.0)).xyz * weights[i];\n }\n // Prev pose (position only) for the velocity output: record + palettes from the prev\n // snapshot, fetched by src_index.\n let prev = u_prev_instances[in.src_index];\n var prev_skinned_pos = vec4<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n prev_skinned_pos +=\n u_prev_palettes[in.joints[i] + prev.misc0.w] * vec4<f32>(in.position, 1.0) * weights[i];\n }\n let model = mat4x4<f32>(in.model_0, in.model_1, in.model_2, in.model_3);\n let world_pos: vec4<f32> = model * skinned_pos;\n let prev_world_pos: vec4<f32> = prev.model * prev_skinned_pos;\n let normal_mtx = mat3x3<f32>(model[0].xyz, model[1].xyz, model[2].xyz);\n let world_normal : vec3<f32> = normalize(normal_mtx * skinned_norm);\n let world_tangent : vec3<f32> = normalize(normal_mtx * skinned_tan);\n let world_bitangent : vec3<f32> = normalize(cross(world_normal, world_tangent) * in.tangent.w);\n var out : VertexOutput;\n out.clip_position = u_camera.view_proj * world_pos;\n out.normal = world_normal;\n out.uv = in.uv;\n out.tangent = world_tangent;\n out.bitangent = world_bitangent;\n out.mesh_color = in.mesh_color;\n out.emission_rgbi = in.emission_rgbi;\n out.flags = in.anim_misc.x;\n out.world_pos = world_pos.xyz;\n out.cur_clip = u_camera.unjittered_view_proj * world_pos;\n out.prev_clip = u_camera.prev_unjittered_view_proj * prev_world_pos;\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color : vec4<f32>,\n @location(1) normal : vec4<f32>,\n @location(2) orm : vec4<f32>,\n @location(3) velocity : vec2<f32>,\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nfn hash12(p: vec2<f32>) -> f32 {\n return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position : vec4<f32>,\n @location(0) normal : vec3<f32>,\n @location(1) uv : vec2<f32>,\n @location(2) tangent : vec3<f32>,\n @location(3) bitangent : vec3<f32>,\n @location(4) mesh_color : vec4<f32>,\n @location(5) emission_rgbi : vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos : vec3<f32>,\n @location(8) cur_clip : vec4<f32>,\n @location(9) prev_clip : vec4<f32>,\n) -> GBufferOutput {\n const DEATH_DISSOLVE_BIT: u32 = 1u << 9u;\n var out : GBufferOutput;\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n if (u_camera.reflection_clip_enabled > 0.5 && world_pos.y < u_camera.reflection_clip_y) {\n discard;\n }\n let mat_mip_bias = u_camera.mip_bias + mesh_color_tuning.sample_params.x;\n let base_color_sampled = textureSampleBias(base_color_texture, material_sampler, uv, mat_mip_bias);\n let tex_normal_rgb = textureSampleBias(normal_texture , material_sampler, uv, mat_mip_bias).rgb;\n let orm_sample = textureSampleBias(orm_texture, material_sampler, uv, mat_mip_bias);\n let ao_multiplier = mesh_color_tuning.surface_params.w;\n let metallic_multiplier = mesh_color_tuning.surface_params.z;\n let min_roughness = mesh_color_tuning.surface_params.x;\n let do_roughness_smoothstep = mesh_color_tuning.misc_params.y > 0.5;\n let roughness_after_curve = select(orm_sample.g, smoothstep(0.0, 1.0, orm_sample.g), do_roughness_smoothstep);\n var orm = vec3<f32>(\n clamp(orm_sample.r * ao_multiplier, 0.0, 1.0),\n clamp(max(roughness_after_curve, min_roughness), 0.0, 1.0),\n clamp(orm_sample.b * metallic_multiplier, 0.0, 1.0)\n );\n if ((flags & DEBUG_METALLIC_ENABLED) != 0u) {\n orm.b = f32((flags >> DEBUG_METALLIC_SHIFT) & 255u) / 255.0;\n }\n if ((flags & DEBUG_ROUGHNESS_ENABLED) != 0u) {\n orm.g = f32((flags >> DEBUG_ROUGHNESS_SHIFT) & 255u) / 255.0;\n }\n // .rg-only + z=-1 to match BC5 (b=0) so BC and ASTC decode identically.\n let ts_normal : vec3<f32> = apply_normal_strength(vec3<f32>(tex_normal_rgb.rg * 2.0 - vec2<f32>(1.0), -1.0));\n let world_n_unbent : vec3<f32> = normalize(\n ts_normal.x * tangent\n + ts_normal.y * bitangent\n - ts_normal.z * normal);\n let world_n = normalize(mix(world_n_unbent, vec3<f32>(0.0, 1.0, 0.0), mesh_color_tuning.misc_params.z));\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let base_color = vec4<f32>(adjusted_rgb, base_color_sampled.a);\n let shaded_color = base_color * mesh_color;\n if ((flags & DEATH_DISSOLVE_BIT) != 0u) {\n let visibility = clamp(mesh_color.a, 0.0, 1.0);\n let dissolve_noise = hash12(uv * 17.0);\n if (dissolve_noise > visibility) { discard; }\n }\n if (USE_ALPHA_CUTOFF) {\n if (shaded_color.a < ALPHA_CUTOFF) { discard; }\n }\n let E = clamp(emission_rgbi.a + mesh_color_tuning.misc_params.w, 0.0, 1.0);\n // SetEmissionColor tint: the light pass tints the emissive add by base_color, so pull the\n // written albedo toward the emission color as glow strengthens (full by ~I=8). Near-black\n // emission color (never set) keeps the albedo-tinted glow; trailer mode reuses the rgb slots.\n const TRAILER_MODE_BIT: u32 = 1u << 8u;\n let em_lin = exp2(E * 10.0) - 1.0;\n var final_color = shaded_color;\n if (em_lin > 0.0 && max(emission_rgbi.r, max(emission_rgbi.g, emission_rgbi.b)) > 0.003 && (flags & TRAILER_MODE_BIT) == 0u) {\n final_color = vec4<f32>(mix(shaded_color.rgb, emission_rgbi.rgb, clamp(em_lin * 0.125, 0.0, 1.0)), shaded_color.a);\n }\n out.base_color = final_color;\n out.base_color.a = mesh_color_tuning.misc_params.x;\n out.normal = vec4<f32>(world_n, 0.0);\n out.orm = vec4<f32>(orm, E);\n let ndc_z = clip_position.z / clip_position.w;\n return out;\n}\n"},{"label":"GBuffer Skinned Masked Shader","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nconst USE_ALPHA_CUTOFF: bool = true;\nconst ALPHA_CUTOFF: f32 = 0.5 ;\n// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// gbuffer_skinned.wgsl\nstruct SkinnedVertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n @location(4) joints: vec4<u32>,\n @location(5) weights: vec4<f32>,\n\n @location(6) model_0: vec4<f32>,\n @location(7) model_1: vec4<f32>,\n @location(8) model_2: vec4<f32>,\n @location(9) model_3: vec4<f32>,\n @location(10) mesh_color: vec4<f32>,\n @location(11) emission_rgbi: vec4<f32>,\n @location(12) anim_misc: vec4<u32>,\n @location(13) percent_progress: f32,\n // Own index into the full-list instance/prev-snapshot buffers (compacted records carry\n // the original index here, stamped by the cull) - the prev-record fetch key for velocity.\n @location(14) src_index: u32,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera : CameraUniform;\n\n@group(1) @binding(0) var base_color_texture : texture_2d<f32>;\n@group(1) @binding(1) var normal_texture : texture_2d<f32>;\n@group(1) @binding(2) var orm_texture : texture_2d<f32>;\n@group(1) @binding(3) var material_sampler : sampler;\n\nconst DEBUG_METALLIC_SHIFT: u32 = 10u;\nconst DEBUG_ROUGHNESS_SHIFT: u32 = 18u;\nconst DEBUG_METALLIC_ENABLED: u32 = 1u << 26u;\nconst DEBUG_ROUGHNESS_ENABLED: u32 = 1u << 27u;\n\n@group(2) @binding(0) var<storage, read> u_palettes : array<mat4x4<f32>>;\n// Prev-frame snapshots for the velocity output (bindings aliased to the CURRENT buffers on\n// stale-snapshot frames => prev == cur => camera-term velocity; see gbuffer_palette_bg).\n@group(2) @binding(1) var<storage, read> u_prev_palettes : array<mat4x4<f32>>;\n// Mirrors render_base::SkinnedInstanceData (144 bytes).\nstruct SkinnedInst {\n model: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission: vec4<f32>,\n misc0: vec4<u32>, // flags, clip_id, skin_id, palette_offset\n misc1: vec4<u32>, // percent(f32 bits), upper_clip, upper_percent(f32 bits), src_index\n aim_pitch_quat: vec4<f32>,\n};\n@group(2) @binding(2) var<storage, read> u_prev_instances : array<SkinnedInst>;\n\nstruct VertexOutput {\n @builtin(position) clip_position : vec4<f32>,\n @location(0) normal : vec3<f32>,\n @location(1) uv : vec2<f32>,\n @location(2) tangent : vec3<f32>,\n @location(3) bitangent : vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) flags : u32,\n @location(7) world_pos : vec3<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip : vec4<f32>,\n @location(9) prev_clip : vec4<f32>,\n};\n\n@vertex\nfn vs_main(in : SkinnedVertexInput) -> VertexOutput {\n let weight_sum = max(in.weights.x + in.weights.y + in.weights.z + in.weights.w, 1e-5);\n let weights = in.weights / weight_sum;\n var skinned_pos = vec4<f32>(0.0);\n var skinned_norm = vec3<f32>(0.0);\n var skinned_tan = vec3<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n let joint_index = in.joints[i] + in.anim_misc.w;\n let joint_mat = u_palettes[joint_index];\n skinned_pos += joint_mat * vec4<f32>(in.position , 1.0) * weights[i];\n skinned_norm += (joint_mat * vec4<f32>(in.normal , 0.0)).xyz * weights[i];\n skinned_tan += (joint_mat * vec4<f32>(in.tangent.xyz, 0.0)).xyz * weights[i];\n }\n // Prev pose (position only) for the velocity output: record + palettes from the prev\n // snapshot, fetched by src_index.\n let prev = u_prev_instances[in.src_index];\n var prev_skinned_pos = vec4<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n prev_skinned_pos +=\n u_prev_palettes[in.joints[i] + prev.misc0.w] * vec4<f32>(in.position, 1.0) * weights[i];\n }\n let model = mat4x4<f32>(in.model_0, in.model_1, in.model_2, in.model_3);\n let world_pos: vec4<f32> = model * skinned_pos;\n let prev_world_pos: vec4<f32> = prev.model * prev_skinned_pos;\n let normal_mtx = mat3x3<f32>(model[0].xyz, model[1].xyz, model[2].xyz);\n let world_normal : vec3<f32> = normalize(normal_mtx * skinned_norm);\n let world_tangent : vec3<f32> = normalize(normal_mtx * skinned_tan);\n let world_bitangent : vec3<f32> = normalize(cross(world_normal, world_tangent) * in.tangent.w);\n var out : VertexOutput;\n out.clip_position = u_camera.view_proj * world_pos;\n out.normal = world_normal;\n out.uv = in.uv;\n out.tangent = world_tangent;\n out.bitangent = world_bitangent;\n out.mesh_color = in.mesh_color;\n out.emission_rgbi = in.emission_rgbi;\n out.flags = in.anim_misc.x;\n out.world_pos = world_pos.xyz;\n out.cur_clip = u_camera.unjittered_view_proj * world_pos;\n out.prev_clip = u_camera.prev_unjittered_view_proj * prev_world_pos;\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color : vec4<f32>,\n @location(1) normal : vec4<f32>,\n @location(2) orm : vec4<f32>,\n @location(3) velocity : vec2<f32>,\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nfn hash12(p: vec2<f32>) -> f32 {\n return fract(sin(dot(p, vec2<f32>(12.9898, 78.233))) * 43758.5453);\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position : vec4<f32>,\n @location(0) normal : vec3<f32>,\n @location(1) uv : vec2<f32>,\n @location(2) tangent : vec3<f32>,\n @location(3) bitangent : vec3<f32>,\n @location(4) mesh_color : vec4<f32>,\n @location(5) emission_rgbi : vec4<f32>,\n @interpolate(flat) @location(6) flags: u32,\n @location(7) world_pos : vec3<f32>,\n @location(8) cur_clip : vec4<f32>,\n @location(9) prev_clip : vec4<f32>,\n) -> GBufferOutput {\n const DEATH_DISSOLVE_BIT: u32 = 1u << 9u;\n var out : GBufferOutput;\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n if (u_camera.reflection_clip_enabled > 0.5 && world_pos.y < u_camera.reflection_clip_y) {\n discard;\n }\n let mat_mip_bias = u_camera.mip_bias + mesh_color_tuning.sample_params.x;\n let base_color_sampled = textureSampleBias(base_color_texture, material_sampler, uv, mat_mip_bias);\n let tex_normal_rgb = textureSampleBias(normal_texture , material_sampler, uv, mat_mip_bias).rgb;\n let orm_sample = textureSampleBias(orm_texture, material_sampler, uv, mat_mip_bias);\n let ao_multiplier = mesh_color_tuning.surface_params.w;\n let metallic_multiplier = mesh_color_tuning.surface_params.z;\n let min_roughness = mesh_color_tuning.surface_params.x;\n let do_roughness_smoothstep = mesh_color_tuning.misc_params.y > 0.5;\n let roughness_after_curve = select(orm_sample.g, smoothstep(0.0, 1.0, orm_sample.g), do_roughness_smoothstep);\n var orm = vec3<f32>(\n clamp(orm_sample.r * ao_multiplier, 0.0, 1.0),\n clamp(max(roughness_after_curve, min_roughness), 0.0, 1.0),\n clamp(orm_sample.b * metallic_multiplier, 0.0, 1.0)\n );\n if ((flags & DEBUG_METALLIC_ENABLED) != 0u) {\n orm.b = f32((flags >> DEBUG_METALLIC_SHIFT) & 255u) / 255.0;\n }\n if ((flags & DEBUG_ROUGHNESS_ENABLED) != 0u) {\n orm.g = f32((flags >> DEBUG_ROUGHNESS_SHIFT) & 255u) / 255.0;\n }\n // .rg-only + z=-1 to match BC5 (b=0) so BC and ASTC decode identically.\n let ts_normal : vec3<f32> = apply_normal_strength(vec3<f32>(tex_normal_rgb.rg * 2.0 - vec2<f32>(1.0), -1.0));\n let world_n_unbent : vec3<f32> = normalize(\n ts_normal.x * tangent\n + ts_normal.y * bitangent\n - ts_normal.z * normal);\n let world_n = normalize(mix(world_n_unbent, vec3<f32>(0.0, 1.0, 0.0), mesh_color_tuning.misc_params.z));\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let base_color = vec4<f32>(adjusted_rgb, base_color_sampled.a);\n let shaded_color = base_color * mesh_color;\n if ((flags & DEATH_DISSOLVE_BIT) != 0u) {\n let visibility = clamp(mesh_color.a, 0.0, 1.0);\n let dissolve_noise = hash12(uv * 17.0);\n if (dissolve_noise > visibility) { discard; }\n }\n if (USE_ALPHA_CUTOFF) {\n if (shaded_color.a < ALPHA_CUTOFF) { discard; }\n }\n let E = clamp(emission_rgbi.a + mesh_color_tuning.misc_params.w, 0.0, 1.0);\n // SetEmissionColor tint: the light pass tints the emissive add by base_color, so pull the\n // written albedo toward the emission color as glow strengthens (full by ~I=8). Near-black\n // emission color (never set) keeps the albedo-tinted glow; trailer mode reuses the rgb slots.\n const TRAILER_MODE_BIT: u32 = 1u << 8u;\n let em_lin = exp2(E * 10.0) - 1.0;\n var final_color = shaded_color;\n if (em_lin > 0.0 && max(emission_rgbi.r, max(emission_rgbi.g, emission_rgbi.b)) > 0.003 && (flags & TRAILER_MODE_BIT) == 0u) {\n final_color = vec4<f32>(mix(shaded_color.rgb, emission_rgbi.rgb, clamp(em_lin * 0.125, 0.0, 1.0)), shaded_color.a);\n }\n out.base_color = final_color;\n out.base_color.a = mesh_color_tuning.misc_params.x;\n out.normal = vec4<f32>(world_n, 0.0);\n out.orm = vec4<f32>(orm, E);\n let ndc_z = clip_position.z / clip_position.w;\n return out;\n}\n"},{"label":"Forward Unlit Blend Shader","code":"// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// forward_unlit_blend.wgsl\n// Unlit forward alpha blend (premultiplied) for static meshes.\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>, // unused\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>, // unused\n\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) emission_rgbi: vec4<f32>, // unused\n @location(10) flags: u32, // unused\n};\n\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>; // unused\n@group(1) @binding(2) var orm_texture: texture_2d<f32>; // unused\n@group(1) @binding(3) var material_sampler: sampler;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n let model = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n let world_pos = model * vec4<f32>(input.position, 1.0);\n\n var out: VertexOutput;\n out.clip_position = u_camera.view_proj * world_pos;\n out.uv = input.uv;\n out.mesh_color = input.mesh_color;\n return out;\n}\n\n@fragment\nfn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {\n let base_color_sampled = textureSample(base_color_texture, material_sampler, in.uv);\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let shaded = vec4<f32>(adjusted_rgb, base_color_sampled.a) * in.mesh_color;\n\n let a = clamp(shaded.a, 0.0, 1.0);\n return vec4<f32>(shaded.rgb * a, a);\n}\n"},{"label":"Forward Unlit Blend (Skinned) Shader","code":"// common/color_tuning.wgsl\n// Shared HSB controls for material (group 1)\n\nstruct MeshColorTuning {\n hue_degrees: f32,\n saturation: f32,\n brightness: f32,\n contrast: f32,\n tint: vec4<f32>,\n surface_params: vec4<f32>,\n misc_params: vec4<f32>,\n sample_params: vec4<f32>, // x = texture mip bias added to camera mip bias\n};\n@group(1) @binding(4) var<uniform> mesh_color_tuning: MeshColorTuning;\n\n// Rec.709 luma in *linear* light\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\n// RGB <-> YPbPr (BT.709) in *linear* light\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n let pb = -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b;\n let pr = 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b;\n return vec3<f32>(y, pb, pr);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n let y = ypbpr.x;\n let pb = ypbpr.y;\n let pr = ypbpr.z;\n let r = y + 1.5748 * pr;\n let g = y - 0.187324 * pb - 0.468124 * pr;\n let b = y + 1.8556 * pb;\n return vec3<f32>(r, g, b);\n}\n\nfn apply_hsb(rgb_in: vec3<f32>) -> vec3<f32> {\n // Convert to YPbPr (linear 709)\n var ypbpr = rgb_to_ypbpr709(rgb_in);\n\n // Hue: rotate Pb/Pr\n // (Ideally compute cos/sin on CPU and pass in as uniforms.)\n let a = radians(mesh_color_tuning.hue_degrees);\n let c = cos(a);\n let s = sin(a);\n let pb2 = ypbpr.y * c - ypbpr.z * s;\n let pr2 = ypbpr.y * s + ypbpr.z * c;\n\n // Saturation: scale chroma directly (keeps Y constant exactly)\n let sat = mesh_color_tuning.saturation;\n ypbpr = vec3<f32>(ypbpr.x, pb2 * sat, pr2 * sat);\n\n // Back to RGB\n var rgb = ypbpr709_to_rgb(ypbpr);\n\n // Brightness: simple gain (keeps hue constant)\n rgb *= mesh_color_tuning.brightness;\n\n // Contrast multiplier around mid-gray 0.5 (per-material, pre-tonemap; 1 = neutral)\n let c_mul = max(0.0, mesh_color_tuning.contrast);\n rgb = (rgb - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n\n // Apply tint after HSB\n return rgb * mesh_color_tuning.tint.rgb;\n}\n\nfn apply_normal_strength(ts_normal_in: vec3<f32>) -> vec3<f32> {\n let strength = mesh_color_tuning.surface_params.y;\n let xy = ts_normal_in.xy * strength;\n return normalize(vec3<f32>(xy, ts_normal_in.z));\n}\n// forward_unlit_blend_skinned.wgsl\n// Unlit forward alpha blend (premultiplied) for skinned meshes.\n\nstruct SkinnedVertexInput {\n /* per-vertex attributes */\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>, // unused\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>, // unused\n @location(4) joints: vec4<u32>,\n @location(5) weights: vec4<f32>,\n\n /* per-instance attributes */\n @location(6) model_0: vec4<f32>,\n @location(7) model_1: vec4<f32>,\n @location(8) model_2: vec4<f32>,\n @location(9) model_3: vec4<f32>,\n @location(10) mesh_color: vec4<f32>,\n @location(11) emission_rgbi: vec4<f32>, // unused\n @location(12) anim_misc: vec4<u32>, // [flags, clip_id, skin_id, palette_offset]\n @location(13) percent_progress: f32, // unused\n};\n\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>; // unused\n@group(1) @binding(2) var orm_texture: texture_2d<f32>; // unused\n@group(1) @binding(3) var material_sampler: sampler;\n\n// Palette buffer (matches skinned GBuffer/shadow paths)\n@group(2) @binding(0) var<storage, read> u_palettes: array<mat4x4<f32>>;\n\n@vertex\nfn vs_main(in: SkinnedVertexInput) -> VertexOutput {\n let weight_sum = max(in.weights.x + in.weights.y + in.weights.z + in.weights.w, 1e-5);\n let weights = in.weights / weight_sum;\n\n var skinned_pos = vec4<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n let joint_index = in.joints[i] + in.anim_misc.w;\n let joint_mat = u_palettes[joint_index];\n skinned_pos += joint_mat * vec4<f32>(in.position, 1.0) * weights[i];\n }\n\n let model = mat4x4<f32>(in.model_0, in.model_1, in.model_2, in.model_3);\n let world_pos = model * skinned_pos;\n\n var out: VertexOutput;\n out.clip_position = u_camera.view_proj * world_pos;\n out.uv = in.uv;\n out.mesh_color = in.mesh_color;\n return out;\n}\n\n@fragment\nfn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {\n let base_color_sampled = textureSample(base_color_texture, material_sampler, in.uv);\n let adjusted_rgb = apply_hsb(base_color_sampled.rgb);\n let shaded = vec4<f32>(adjusted_rgb, base_color_sampled.a) * in.mesh_color;\n\n let a = clamp(shaded.a, 0.0, 1.0);\n return vec4<f32>(shaded.rgb * a, a);\n}\n"},{"label":"shaders/smaa_edges.wgsl","code":"// smaa_edges.wgsl\n// SMAA luma edge detection with local contrast adaptation.\n\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let p = vec2<f32>(px, py);\n o.pos = vec4<f32>(p * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n return o;\n}\n\nstruct EdgesParams {\n inv_size: vec2<f32>,\n luma_threshold: f32,\n depth_rel_threshold: f32,\n};\n\n@group(0) @binding(0) var t_color: texture_2d<f32>;\n@group(0) @binding(1) var t_linear_depth: texture_2d<f32>;\n@group(0) @binding(2) var<uniform> u_edges: EdgesParams;\n\nfn luma(rgb: vec3<f32>) -> f32 {\n return dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\nfn luma_at(p: vec2<i32>) -> f32 {\n return luma(textureLoad(t_color, p, 0).rgb);\n}\n\nfn depth_rel_delta(z0: f32, z1: f32) -> f32 {\n let denom = max(1e-4, max(z0, z1));\n return abs(z0 - z1) / denom;\n}\n\n@fragment\nfn fs_edges(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {\n let dimc = textureDimensions(t_color);\n let dimd = textureDimensions(t_linear_depth);\n let xyi = clamp(vec2<i32>(pos.xy), vec2<i32>(0, 0), vec2<i32>(i32(dimc.x) - 1, i32(dimc.y) - 1));\n let xy = xyi;\n let x0 = max(xy.x - 1, 0);\n let y0 = max(xy.y - 1, 0);\n let x1 = min(xy.x + 1, i32(dimc.x) - 1);\n let y1 = min(xy.y + 1, i32(dimc.y) - 1);\n let x00 = max(xy.x - 2, 0);\n let y00 = max(xy.y - 2, 0);\n\n let l = luma_at(xy);\n let ll = luma_at(vec2<i32>(x0, xy.y));\n let lt = luma_at(vec2<i32>(xy.x, y0));\n\n let delta_xy = abs(l - vec2<f32>(ll, lt));\n var edges = step(vec2<f32>(u_edges.luma_threshold), delta_xy);\n if (dot(edges, vec2<f32>(1.0)) == 0.0) {\n discard;\n }\n\n let lr = luma_at(vec2<i32>(x1, xy.y));\n let lb = luma_at(vec2<i32>(xy.x, y1));\n let delta_rb = abs(l - vec2<f32>(lr, lb));\n\n var max_delta = max(delta_xy, delta_rb);\n\n let lll = luma_at(vec2<i32>(x00, xy.y));\n let ltt = luma_at(vec2<i32>(xy.x, y00));\n let delta_ll_tt = abs(vec2<f32>(ll, lt) - vec2<f32>(lll, ltt));\n max_delta = max(max_delta, delta_ll_tt);\n\n let final_delta = max(max_delta.x, max_delta.y);\n edges *= step(vec2<f32>(final_delta), 2.0 * delta_xy);\n\n // Optional depth predication: when depth is flat, require stronger luma evidence.\n let predication_enabled = (dimc.x == dimd.x) && (dimc.y == dimd.y);\n if (predication_enabled) {\n let NO_DEPTH_EDGE_SCALE: f32 = 4.0;\n // t_linear_depth is the gbuffer depth (sky = 1.0); remap to the retired R32F\n // attachment's cleared-0 convention so sky silhouettes stay strong depth edges.\n let zr = textureLoad(t_linear_depth, xy, 0).x;\n let zlr = textureLoad(t_linear_depth, vec2<i32>(x0, xy.y), 0).x;\n let ztr = textureLoad(t_linear_depth, vec2<i32>(xy.x, y0), 0).x;\n let z = select(zr, 0.0, zr >= 1.0);\n let zl = select(zlr, 0.0, zlr >= 1.0);\n let zt = select(ztr, 0.0, ztr >= 1.0);\n let depth_edge_l = depth_rel_delta(z, zl) >= u_edges.depth_rel_threshold;\n let depth_edge_t = depth_rel_delta(z, zt) >= u_edges.depth_rel_threshold;\n let strong_luma = delta_xy >= vec2<f32>(u_edges.luma_threshold * NO_DEPTH_EDGE_SCALE);\n let depth_gate = vec2<f32>(\n select(0.0, 1.0, depth_edge_l || strong_luma.x),\n select(0.0, 1.0, depth_edge_t || strong_luma.y),\n );\n edges *= depth_gate;\n }\n\n return vec4<f32>(edges, 0.0, 1.0);\n}\n\n"},{"label":"shaders/smaa_weights.wgsl","code":"// smaa_weights.wgsl\n// SMAA blending weight calculation (WGSL), ported from glsl-smaa (iryoku reference).\n// High-quality 1x path: diagonal, horizontal/vertical, and corner patterns.\n\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let p = vec2<f32>(px, py);\n o.pos = vec4<f32>(p * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n return o;\n}\n\nstruct SmaaParams {\n viewport_size: vec2<f32>,\n texel_size: vec2<f32>,\n};\n\n// Tunables (matching glsl-smaa defaults/preset-ish)\nconst SMAA_MAX_SEARCH_STEPS: i32 = 16;\nconst SMAA_MAX_SEARCH_STEPS_DIAG: i32 = 8;\nconst SMAA_CORNER_ROUNDING_NORM: f32 = 0.25;\nconst SMAA_AREATEX_MAX_DISTANCE: f32 = 16.0;\nconst SMAA_AREATEX_MAX_DISTANCE_DIAG: f32 = 20.0;\nconst SMAA_AREATEX_PIXEL_SIZE: vec2<f32> = vec2<f32>(1.0 / 160.0, 1.0 / 560.0);\nconst SMAA_AREATEX_SUBTEX_SIZE: f32 = 1.0 / 7.0;\nconst SMAA_SEARCHTEX_SIZE: vec2<f32> = vec2<f32>(66.0, 33.0);\nconst SMAA_SEARCHTEX_PACKED_SIZE: vec2<f32> = vec2<f32>(64.0, 16.0);\n\n@group(0) @binding(0) var s_linear: sampler;\n@group(0) @binding(1) var<uniform> u: SmaaParams;\n@group(0) @binding(2) var t_edges: texture_2d<f32>;\n@group(0) @binding(3) var t_area: texture_2d<f32>;\n@group(0) @binding(4) var t_search: texture_2d<f32>;\n\nfn mad(a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> vec2<f32> { return a * b + c; }\nfn mad4(a: vec4<f32>, b: vec4<f32>, c: vec4<f32>) -> vec4<f32> { return a * b + c; }\nfn saturate2(v: vec2<f32>) -> vec2<f32> { return clamp(v, vec2<f32>(0.0), vec2<f32>(1.0)); }\nfn round2(v: vec2<f32>) -> vec2<f32> { return floor(v + vec2<f32>(0.5)); }\nfn round4(v: vec4<f32>) -> vec4<f32> { return floor(v + vec4<f32>(0.5)); }\n\nstruct DiagSearch {\n d: vec2<f32>,\n e: vec2<f32>,\n};\n\nfn sample_edges(coord: vec2<f32>) -> vec2<f32> {\n return textureSampleLevel(t_edges, s_linear, coord, 0.0).rg;\n}\nfn sample_edges_offset(coord: vec2<f32>, offset_px: vec2<f32>) -> vec2<f32> {\n return textureSampleLevel(t_edges, s_linear, coord + offset_px * u.texel_size, 0.0).rg;\n}\n\nfn decode_diag2(e_in: vec2<f32>) -> vec2<f32> {\n var e = e_in;\n e.x = e.x * abs(5.0 * e.x - 5.0 * 0.75);\n return round2(e);\n}\n\nfn decode_diag4(e_in: vec4<f32>) -> vec4<f32> {\n var e = e_in;\n e.x = e.x * abs(5.0 * e.x - 5.0 * 0.75);\n e.z = e.z * abs(5.0 * e.z - 5.0 * 0.75);\n return round4(e);\n}\n\nfn search_diag1(texcoord_in: vec2<f32>, dir: vec2<f32>) -> DiagSearch {\n var coord = vec4<f32>(texcoord_in, -1.0, 1.0);\n var e = vec2<f32>(0.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(coord.z < f32(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && coord.w > 0.9)) { break; }\n coord = coord + vec4<f32>(dir * u.texel_size, 1.0, 0.0);\n e = sample_edges(coord.xy);\n coord.w = dot(e, vec2<f32>(0.5));\n i += 1;\n }\n return DiagSearch(coord.zw, e);\n}\n\nfn search_diag2(texcoord_in: vec2<f32>, dir: vec2<f32>) -> DiagSearch {\n var coord = vec4<f32>(texcoord_in + vec2<f32>(0.25 * u.texel_size.x, 0.0), -1.0, 1.0);\n var e = vec2<f32>(0.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(coord.z < f32(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && coord.w > 0.9)) { break; }\n coord = coord + vec4<f32>(dir * u.texel_size, 1.0, 0.0);\n e = decode_diag2(sample_edges(coord.xy));\n coord.w = dot(e, vec2<f32>(0.5));\n i += 1;\n }\n return DiagSearch(coord.zw, e);\n}\n\nfn search_length(e: vec2<f32>, offset: f32) -> f32 {\n // glsl-smaa SMAASearchLength\n var scale = SMAA_SEARCHTEX_SIZE * vec2<f32>(0.5, -1.0);\n var bias = SMAA_SEARCHTEX_SIZE * vec2<f32>(offset, 1.0);\n scale = scale + vec2<f32>(-1.0, 1.0);\n bias = bias + vec2<f32>( 0.5, -0.5);\n scale = scale / SMAA_SEARCHTEX_PACKED_SIZE;\n bias = bias / SMAA_SEARCHTEX_PACKED_SIZE;\n return textureSampleLevel(t_search, s_linear, mad(scale, e, bias), 0.0).r;\n}\n\nfn search_x_left(texcoord_in: vec2<f32>, end_x: f32) -> f32 {\n var texcoord = texcoord_in;\n var e = vec2<f32>(0.0, 1.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(texcoord.x > end_x && e.y > 0.8281 && e.x == 0.0)) { break; }\n e = sample_edges(texcoord);\n texcoord = mad(vec2<f32>(-2.0, 0.0), u.texel_size, texcoord);\n i += 1;\n }\n let offset = mad(vec2<f32>(-(255.0 / 127.0)), vec2<f32>(search_length(e, 0.0)), vec2<f32>(3.25)).x;\n return mad(vec2<f32>(u.texel_size.x), vec2<f32>(offset), vec2<f32>(texcoord.x)).x;\n}\n\nfn search_x_right(texcoord_in: vec2<f32>, end_x: f32) -> f32 {\n var texcoord = texcoord_in;\n var e = vec2<f32>(0.0, 1.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(texcoord.x < end_x && e.y > 0.8281 && e.x == 0.0)) { break; }\n e = sample_edges(texcoord);\n texcoord = mad(vec2<f32>(2.0, 0.0), u.texel_size, texcoord);\n i += 1;\n }\n let offset = mad(vec2<f32>(-(255.0 / 127.0)), vec2<f32>(search_length(e, 0.5)), vec2<f32>(3.25)).x;\n return mad(vec2<f32>(-u.texel_size.x), vec2<f32>(offset), vec2<f32>(texcoord.x)).x;\n}\n\nfn search_y_up(texcoord_in: vec2<f32>, end_y: f32) -> f32 {\n var texcoord = texcoord_in;\n var e = vec2<f32>(1.0, 0.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(texcoord.y > end_y && e.x > 0.8281 && e.y == 0.0)) { break; }\n e = sample_edges(texcoord);\n texcoord = mad(vec2<f32>(0.0, -2.0), u.texel_size, texcoord);\n i += 1;\n }\n let offset = mad(vec2<f32>(-(255.0 / 127.0)), vec2<f32>(search_length(e.yx, 0.0)), vec2<f32>(3.25)).x;\n return mad(vec2<f32>(u.texel_size.y), vec2<f32>(offset), vec2<f32>(texcoord.y)).x;\n}\n\nfn search_y_down(texcoord_in: vec2<f32>, end_y: f32) -> f32 {\n var texcoord = texcoord_in;\n var e = vec2<f32>(1.0, 0.0);\n var i: i32 = 0;\n loop {\n if (i >= SMAA_MAX_SEARCH_STEPS) { break; }\n if (!(texcoord.y < end_y && e.x > 0.8281 && e.y == 0.0)) { break; }\n e = sample_edges(texcoord);\n texcoord = mad(vec2<f32>(0.0, 2.0), u.texel_size, texcoord);\n i += 1;\n }\n let offset = mad(vec2<f32>(-(255.0 / 127.0)), vec2<f32>(search_length(e.yx, 0.5)), vec2<f32>(3.25)).x;\n return mad(vec2<f32>(-u.texel_size.y), vec2<f32>(offset), vec2<f32>(texcoord.y)).x;\n}\n\nfn area(dist: vec2<f32>, e1: f32, e2: f32, offset: f32) -> vec2<f32> {\n // glsl-smaa SMAAArea (areaTex in RG)\n let texcoord_px = mad(vec2<f32>(SMAA_AREATEX_MAX_DISTANCE), round2(4.0 * vec2<f32>(e1, e2)), dist);\n var tc = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord_px, 0.5 * SMAA_AREATEX_PIXEL_SIZE);\n tc.y = SMAA_AREATEX_SUBTEX_SIZE * offset + tc.y;\n return textureSampleLevel(t_area, s_linear, tc, 0.0).rg;\n}\n\nfn area_diag(dist: vec2<f32>, e: vec2<f32>, offset: f32) -> vec2<f32> {\n var tc = mad(vec2<f32>(SMAA_AREATEX_MAX_DISTANCE_DIAG), e, dist);\n tc = mad(SMAA_AREATEX_PIXEL_SIZE, tc, 0.5 * SMAA_AREATEX_PIXEL_SIZE);\n tc.x += 0.5;\n tc.y += SMAA_AREATEX_SUBTEX_SIZE * offset;\n return textureSampleLevel(t_area, s_linear, tc, 0.0).rg;\n}\n\nfn calculate_diag_weights(texcoord: vec2<f32>, e: vec2<f32>) -> vec2<f32> {\n var weights = vec2<f32>(0.0);\n var d = vec4<f32>(0.0);\n\n if (e.x > 0.0) {\n let s = search_diag1(texcoord, vec2<f32>(-1.0, 1.0));\n d.x = s.d.x + select(0.0, 1.0, s.e.y > 0.9);\n d.z = s.d.y;\n }\n let s1 = search_diag1(texcoord, vec2<f32>(1.0, -1.0));\n d.y = s1.d.x;\n d.w = s1.d.y;\n\n if (d.x + d.y > 2.0) {\n let coords = mad4(\n vec4<f32>(-d.x + 0.25, d.x, d.y, -d.y - 0.25),\n vec4<f32>(u.texel_size, u.texel_size),\n texcoord.xyxy,\n );\n var c = vec4<f32>(\n sample_edges_offset(coords.xy, vec2<f32>(-1.0, 0.0)),\n sample_edges_offset(coords.zw, vec2<f32>(1.0, 0.0)),\n );\n let dec = decode_diag4(c);\n c = vec4<f32>(dec.y, dec.x, dec.w, dec.z);\n var cc = 2.0 * c.xz + c.yw;\n cc = select(cc, vec2<f32>(0.0), d.zw >= vec2<f32>(0.9));\n weights += area_diag(d.xy, cc, 0.0);\n }\n\n let s2 = search_diag2(texcoord, vec2<f32>(-1.0, -1.0));\n d.x = s2.d.x;\n d.z = s2.d.y;\n if (sample_edges_offset(texcoord, vec2<f32>(1.0, 0.0)).x > 0.0) {\n let s3 = search_diag2(texcoord, vec2<f32>(1.0, 1.0));\n d.y = s3.d.x + select(0.0, 1.0, s3.e.y > 0.9);\n d.w = s3.d.y;\n } else {\n d.y = 0.0;\n d.w = 0.0;\n }\n\n if (d.x + d.y > 2.0) {\n let coords = mad4(\n vec4<f32>(-d.x, -d.x, d.y, d.y),\n vec4<f32>(u.texel_size, u.texel_size),\n texcoord.xyxy,\n );\n let c = vec4<f32>(\n sample_edges_offset(coords.xy, vec2<f32>(-1.0, 0.0)).y,\n sample_edges_offset(coords.xy, vec2<f32>(0.0, -1.0)).x,\n sample_edges_offset(coords.zw, vec2<f32>(1.0, 0.0)).yx,\n );\n var cc = 2.0 * c.xz + c.yw;\n cc = select(cc, vec2<f32>(0.0), d.zw >= vec2<f32>(0.9));\n weights += area_diag(d.xy, cc, 0.0).yx;\n }\n\n return weights;\n}\n\nfn detect_horizontal_corner_pattern(weights_in: vec2<f32>, texcoord: vec4<f32>, d: vec2<f32>) -> vec2<f32> {\n let left_right = step(d.xy, d.yx);\n var rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * left_right;\n rounding = rounding / max(1e-5, left_right.x + left_right.y);\n\n var factor = vec2<f32>(1.0);\n factor.x -= rounding.x * sample_edges_offset(texcoord.xy, vec2<f32>(0.0, 1.0)).x;\n factor.x -= rounding.y * sample_edges_offset(texcoord.zw, vec2<f32>(1.0, 1.0)).x;\n factor.y -= rounding.x * sample_edges_offset(texcoord.xy, vec2<f32>(0.0, -2.0)).x;\n factor.y -= rounding.y * sample_edges_offset(texcoord.zw, vec2<f32>(1.0, -2.0)).x;\n return weights_in * saturate2(factor);\n}\n\nfn detect_vertical_corner_pattern(weights_in: vec2<f32>, texcoord: vec4<f32>, d: vec2<f32>) -> vec2<f32> {\n let left_right = step(d.xy, d.yx);\n var rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * left_right;\n rounding = rounding / max(1e-5, left_right.x + left_right.y);\n\n var factor = vec2<f32>(1.0);\n factor.x -= rounding.x * sample_edges_offset(texcoord.xy, vec2<f32>(1.0, 0.0)).y;\n factor.x -= rounding.y * sample_edges_offset(texcoord.zw, vec2<f32>(1.0, 1.0)).y;\n factor.y -= rounding.x * sample_edges_offset(texcoord.xy, vec2<f32>(-2.0, 0.0)).y;\n factor.y -= rounding.y * sample_edges_offset(texcoord.zw, vec2<f32>(-2.0, 1.0)).y;\n return weights_in * saturate2(factor);\n}\n\n@fragment\nfn fs_weights(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {\n // Derive texcoords at pixel centers to match SMAA sampling assumptions.\n let dimu = textureDimensions(t_edges);\n let xyi = clamp(vec2<i32>(pos.xy), vec2<i32>(0, 0), vec2<i32>(i32(dimu.x) - 1, i32(dimu.y) - 1));\n let texcoord = (vec2<f32>(xyi) + vec2<f32>(0.5, 0.5)) * u.texel_size;\n let pixcoord = texcoord * u.viewport_size;\n\n // Offsets from weights.vert (PSEUDO_GATHER4)\n let off0 = mad4(vec4<f32>(u.texel_size, u.texel_size), vec4<f32>(-0.25, -0.125, 1.25, -0.125), texcoord.xyxy);\n let off1 = mad4(vec4<f32>(u.texel_size, u.texel_size), vec4<f32>(-0.125, -0.25, -0.125, 1.25), texcoord.xyxy);\n let off2 = mad4(\n vec4<f32>(u.texel_size.x, u.texel_size.x, u.texel_size.y, u.texel_size.y),\n vec4<f32>(-2.0, 2.0, -2.0, 2.0) * f32(SMAA_MAX_SEARCH_STEPS),\n vec4<f32>(off0.x, off0.z, off1.y, off1.w),\n );\n\n var weights = vec4<f32>(0.0);\n var e = sample_edges(texcoord);\n\n // Horizontal (north edge)\n if (e.y > 0.0) {\n let diag_weights = calculate_diag_weights(texcoord, e);\n if (dot(diag_weights, vec2<f32>(1.0)) <= 1e-5) {\n var coords = vec3<f32>(0.0);\n coords.x = search_x_left(off0.xy, off2.x);\n coords.y = off1.y;\n coords.z = search_x_right(off0.zw, off2.y);\n\n let d = abs(round2(mad(u.viewport_size.xx, vec2<f32>(coords.x, coords.z), -pixcoord.xx)));\n let sqrt_d = sqrt(d);\n\n let e1 = sample_edges(vec2<f32>(coords.x, coords.y)).x;\n let e2 = sample_edges_offset(vec2<f32>(coords.z, coords.y), vec2<f32>(1.0, 0.0)).x;\n\n var h_weights = area(sqrt_d, e1, e2, 0.0);\n coords.y = texcoord.y;\n h_weights = detect_horizontal_corner_pattern(h_weights, coords.xyzy, d);\n weights = vec4<f32>(h_weights.x, h_weights.y, weights.z, weights.w);\n } else {\n weights = vec4<f32>(diag_weights.x, diag_weights.y, weights.z, weights.w);\n e.x = 0.0;\n }\n }\n\n // Vertical (west edge)\n if (e.x > 0.0) {\n var coords = vec3<f32>(0.0);\n coords.y = search_y_up(off1.xy, off2.z);\n coords.x = off0.x;\n coords.z = search_y_down(off1.zw, off2.w);\n\n let d = abs(round2(mad(u.viewport_size.yy, vec2<f32>(coords.y, coords.z), -pixcoord.yy)));\n let sqrt_d = sqrt(d);\n\n let e1 = sample_edges(vec2<f32>(coords.x, coords.y)).y;\n let e2 = sample_edges_offset(vec2<f32>(coords.x, coords.z), vec2<f32>(0.0, 1.0)).y;\n\n var v_weights = area(sqrt_d, e1, e2, 0.0);\n coords.x = texcoord.x;\n v_weights = detect_vertical_corner_pattern(v_weights, coords.xyxz, d);\n weights = vec4<f32>(weights.x, weights.y, v_weights.x, v_weights.y);\n }\n\n // Output layout matches glsl-smaa:\n // .r/.g used for horizontal weights, .b/.a for vertical weights.\n return weights;\n}\n\n"},{"label":"shaders/smaa_neighborhood.wgsl","code":"// smaa_neighborhood.wgsl\n// SMAA neighborhood blending (WGSL), ported from glsl-smaa.\n\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let p = vec2<f32>(px, py);\n o.pos = vec4<f32>(p * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n return o;\n}\n\nstruct NbParams {\n texel_size: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var s_linear: sampler;\n@group(0) @binding(1) var<uniform> u: NbParams;\n@group(0) @binding(2) var t_color: texture_2d<f32>;\n@group(0) @binding(3) var t_blend: texture_2d<f32>;\n\nfn mad4(a: vec4<f32>, b: vec4<f32>, c: vec4<f32>) -> vec4<f32> { return a * b + c; }\n\n@fragment\nfn fs_neighborhood(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {\n let dimu = textureDimensions(t_color);\n let xyi = clamp(vec2<i32>(pos.xy), vec2<i32>(0, 0), vec2<i32>(i32(dimu.x) - 1, i32(dimu.y) - 1));\n let texcoord = (vec2<f32>(xyi) + vec2<f32>(0.5, 0.5)) * u.texel_size;\n\n // Equivalent of smaa-blend.vert vOffset.\n let v_offset = mad4(vec4<f32>(u.texel_size, u.texel_size), vec4<f32>(1.0, 0.0, 0.0, 1.0), texcoord.xyxy);\n\n // Fetch weights (layout matches glsl-smaa blend.frag)\n var a: vec4<f32>;\n a.x = textureSampleLevel(t_blend, s_linear, v_offset.xy, 0.0).a; // right\n a.y = textureSampleLevel(t_blend, s_linear, v_offset.zw, 0.0).g; // top\n let cur = textureSampleLevel(t_blend, s_linear, texcoord, 0.0);\n a.w = cur.x; // bottom\n a.z = cur.z; // left\n\n if (dot(a, vec4<f32>(1.0)) <= 1e-5) {\n return textureSampleLevel(t_color, s_linear, texcoord, 0.0);\n }\n\n let h = max(a.x, a.z) > max(a.y, a.w);\n\n var blending_offset = vec4<f32>(0.0, a.y, 0.0, a.w);\n var blending_weight = a.yw;\n if (h) {\n blending_offset = vec4<f32>(a.x, 0.0, a.z, 0.0);\n blending_weight = a.xz;\n }\n blending_weight = blending_weight / max(1e-5, dot(blending_weight, vec2<f32>(1.0)));\n\n let blending_coord = mad4(blending_offset, vec4<f32>(u.texel_size, -u.texel_size), texcoord.xyxy);\n var color = blending_weight.x * textureSampleLevel(t_color, s_linear, blending_coord.xy, 0.0);\n color += blending_weight.y * textureSampleLevel(t_color, s_linear, blending_coord.zw, 0.0);\n return color;\n}\n\n"},{"label":"shaders/smaa_debug_readback.wgsl","code":"// smaa_debug_readback.wgsl\n// Tiny readback helper: scan a small grid from edges+blend and write packed stats.\n\nstruct DebugParams {\n size: vec2<u32>,\n _pad: vec2<u32>,\n};\n\n@group(0) @binding(0) var t_edges: texture_2d<f32>;\n@group(0) @binding(1) var t_blend: texture_2d<f32>;\n@group(0) @binding(2) var t_color: texture_2d<f32>;\n@group(0) @binding(3) var<uniform> u_dbg: DebugParams;\n@group(0) @binding(4) var<storage, read_write> out_u32: array<u32, 13>;\n\nfn pack_rg8(v: vec2<f32>) -> u32 {\n let r = u32(round(clamp(v.x, 0.0, 1.0) * 255.0));\n let g = u32(round(clamp(v.y, 0.0, 1.0) * 255.0));\n return (r & 255u) | ((g & 255u) << 8u);\n}\n\nfn pack_rgba8(v: vec4<f32>) -> u32 {\n let r = u32(round(clamp(v.x, 0.0, 1.0) * 255.0));\n let g = u32(round(clamp(v.y, 0.0, 1.0) * 255.0));\n let b = u32(round(clamp(v.z, 0.0, 1.0) * 255.0));\n let a = u32(round(clamp(v.w, 0.0, 1.0) * 255.0));\n return (r & 255u) | ((g & 255u) << 8u) | ((b & 255u) << 16u) | ((a & 255u) << 24u);\n}\n\nfn luma(rgb: vec3<f32>) -> f32 {\n return dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\n@compute @workgroup_size(1, 1, 1)\nfn cs_main() {\n let w = max(1u, u_dbg.size.x);\n let h = max(1u, u_dbg.size.y);\n\n // Grid scan across the frame (dense enough to hit typical edges).\n let gx: u32 = 32u;\n let gy: u32 = 18u;\n var edges_cnt: u32 = 0u;\n var blend_cnt: u32 = 0u;\n\n var e0: u32 = 0u;\n var e1: u32 = 0u;\n var e2: u32 = 0u;\n var b0: u32 = 0u;\n var b1: u32 = 0u;\n var b2: u32 = 0u;\n var c0: u32 = 0u;\n\n var lmin: f32 = 1e9;\n var lmax: f32 = -1e9;\n var lref: f32 = 0.0;\n var lvar_cnt: u32 = 0u;\n var edge_like_cnt: u32 = 0u;\n\n for (var iy: u32 = 0u; iy < gy; iy += 1u) {\n for (var ix: u32 = 0u; ix < gx; ix += 1u) {\n let x = (w - 1u) * ix / max(1u, gx - 1u);\n let y = (h - 1u) * iy / max(1u, gy - 1u);\n let p = vec2<i32>(i32(x), i32(y));\n let idx = iy * gx + ix;\n\n let e = textureLoad(t_edges, p, 0).rg;\n edges_cnt += select(0u, 1u, (e.x + e.y) > 0.0);\n\n let b = textureLoad(t_blend, p, 0);\n blend_cnt += select(0u, 1u, (b.x + b.y + b.z + b.w) > 0.0);\n\n let c = textureLoad(t_color, p, 0);\n let lum = luma(c.rgb);\n let pl = vec2<i32>(max(i32(x) - 1, 0), i32(y));\n let pt = vec2<i32>(i32(x), max(i32(y) - 1, 0));\n let lum_l = luma(textureLoad(t_color, pl, 0).rgb);\n let lum_t = luma(textureLoad(t_color, pt, 0).rgb);\n edge_like_cnt += select(0u, 1u, max(abs(lum - lum_l), abs(lum - lum_t)) > 0.01);\n if (idx == 0u) {\n lref = lum;\n c0 = pack_rgba8(c);\n } else {\n lvar_cnt += select(0u, 1u, abs(lum - lref) > 1e-4);\n }\n lmin = min(lmin, lum);\n lmax = max(lmax, lum);\n\n // Store a few reference samples (start, mid, end) for debugging.\n if (idx == 0u) {\n e0 = pack_rg8(e);\n b0 = pack_rgba8(b);\n } else if (idx == (gx * gy) / 2u) {\n e1 = pack_rg8(e);\n b1 = pack_rgba8(b);\n } else if (idx == gx * gy - 1u) {\n e2 = pack_rg8(e);\n b2 = pack_rgba8(b);\n }\n }\n }\n\n let lmin_u16 = u32(round(clamp(lmin, 0.0, 1.0) * 65535.0));\n let lmax_u16 = u32(round(clamp(lmax, 0.0, 1.0) * 65535.0));\n\n out_u32[0] = edges_cnt;\n out_u32[1] = blend_cnt;\n out_u32[2] = lmin_u16;\n out_u32[3] = lmax_u16;\n out_u32[4] = lvar_cnt;\n out_u32[5] = e0;\n out_u32[6] = e1;\n out_u32[7] = e2;\n out_u32[8] = b0;\n out_u32[9] = b1;\n out_u32[10] = b2;\n out_u32[11] = c0;\n out_u32[12] = edge_like_cnt;\n}\n\n"},{"label":"shaders/upscale.wgsl","code":"// upscale.wgsl\n// Final scene -> surface composite. Samples the scene-resolution SMAA-resolved color with a linear\n// sampler and writes it to the swapchain. At scene == surface this is a 1:1 passthrough (sampling at\n// exact texel centers); when scene < surface the linear filter upscales.\n\nstruct VsOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n o.uv = vec2<f32>(px, py);\n o.pos = vec4<f32>(o.uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n return o;\n}\n\n@group(0) @binding(0) var s_linear: sampler;\n@group(0) @binding(1) var t_src: texture_2d<f32>;\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec4<f32> {\n return textureSampleLevel(t_src, s_linear, in.uv, 0.0);\n}\n"},{"label":"shaders/tonemap.wgsl","code":"// tonemap.wgsl\nstruct TonemapSettings {\n contrast: f32,\n saturation:f32,\n exposure: f32,\n vignette_strength: f32,\n vignette_radius: f32,\n vignette_softness: f32,\n light_debug_mode: u32,\n dst_is_srgb: u32,\n tonemap_mode: u32,\n shadow_lift_threshold: f32,\n _pad1: u32,\n shadow_lift: f32,\n agx_params: vec4<f32>,\n vignette_color: vec4<f32>,\n // x = bloom intensity (0 = off this frame), y = bloom clamp (0 = no clamp).\n bloom_params: vec4<f32>,\n};\n@group(0) @binding(0) var t_hdr: texture_2d<f32>;\n@group(0) @binding(1) var<uniform> u_tone: TonemapSettings;\n// Half-res bloom top (up[0]); bilinear sample here replaces the former full-res\n// BloomUpToFull + BloomComposite passes.\n@group(0) @binding(2) var s_bloom: sampler;\n@group(0) @binding(3) var t_bloom: texture_2d<f32>;\n\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let p = vec2<f32>(px, py);\n\n // Fullscreen triangle with Y flip so uv.y=0 is the top row\n o.pos = vec4<f32>(p * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n o.uv = p; // Fullscreen triangle: verts use {0,2}; so no *0.5 needed\n return o;\n}\n\n// ACES filmic (RRT+ODT fit)\nconst ACES_INPUT_MAT : mat3x3<f32> = mat3x3<f32>(\n vec3<f32>(0.59719, 0.07600, 0.02840), // column 0\n vec3<f32>(0.35458, 0.90834, 0.13383), // column 1\n vec3<f32>(0.04823, 0.01566, 0.83777) // column 2\n);\nconst ACES_OUTPUT_MAT : mat3x3<f32> = mat3x3<f32>(\n vec3<f32>( 1.60475, -0.10208, -0.00327), // column 0\n vec3<f32>(-0.53108, 1.10813, -0.07276), // column 1\n vec3<f32>(-0.07367, -0.00605, 1.07602) // column 2\n);\nconst AGX_INSET_MAT : mat3x3<f32> = mat3x3<f32>(\n vec3<f32>(0.544814746488245, 0.140416948464053, 0.0888104196149096),\n vec3<f32>(0.373787398372697, 0.754137554567394, 0.178871756420858),\n vec3<f32>(0.0813978551390581, 0.105445496968552, 0.732317823964232)\n);\nconst AGX_OUTSET_MAT : mat3x3<f32> = mat3x3<f32>(\n vec3<f32>(1.96488741169489, -0.299313364904742, -0.164352742528393),\n vec3<f32>(-0.855988495690215, 1.32639796461980, -0.238183969428088),\n vec3<f32>(-0.108898916004672, -0.0270845997150571, 1.40253671195648)\n);\nconst TONEMAP_WHITE : f32 = 11.2;\nconst AWP_CROSSOVER_POINT : f32 = 0.18;\nconst AWP_SHOULDER_MAX : f32 = 1.0 - AWP_CROSSOVER_POINT;\nfn rrt_odt_fit(v: vec3<f32>) -> vec3<f32> {\n let a = v * (v + vec3<f32>(0.0245786)) - vec3<f32>(0.000090537);\n let b = v * (vec3<f32>(0.983729) * v + vec3<f32>(0.4329510)) + vec3<f32>(0.238081);\n return a / b;\n}\nfn allenwp_curve(x: vec3<f32>) -> vec3<f32> {\n let awp_contrast = u_tone.agx_params.x;\n let awp_toe_a = u_tone.agx_params.y;\n let awp_slope = u_tone.agx_params.z;\n let awp_w = u_tone.agx_params.w;\n var shoulder = x - vec3<f32>(AWP_CROSSOVER_POINT);\n let slope_s = awp_slope * shoulder;\n shoulder = slope_s * (vec3<f32>(1.0) + shoulder / vec3<f32>(awp_w))\n / (vec3<f32>(1.0) + slope_s / vec3<f32>(AWP_SHOULDER_MAX));\n shoulder += vec3<f32>(AWP_CROSSOVER_POINT);\n var toe = pow(x, vec3<f32>(awp_contrast));\n toe = toe / (toe + vec3<f32>(awp_toe_a));\n return select(shoulder, toe, x < vec3<f32>(AWP_CROSSOVER_POINT));\n}\n\nfn apply_shadow_lift(color: vec3<f32>) -> vec3<f32> {\n let shadow_lift = clamp(u_tone.shadow_lift, 0.0, 3.0);\n let shadow_lift_threshold = clamp(u_tone.shadow_lift_threshold, 0.02, 0.3);\n let luma = dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n if (shadow_lift <= 0.0 || luma <= 0.0 || luma >= shadow_lift_threshold) {\n return color;\n }\n let u = luma / shadow_lift_threshold;\n let lifted_luma = luma + shadow_lift * shadow_lift_threshold * u * (1.0 - u) * (1.0 - u);\n let scale = lifted_luma / max(luma, 1e-6);\n return clamp(color * vec3<f32>(scale), vec3<f32>(0.0), vec3<f32>(1.0));\n}\n\nfn rounded_rect_sdf(p: vec2<f32>, half_size: vec2<f32>, radius: f32) -> f32 {\n let q = abs(p) - half_size + vec2<f32>(radius);\n return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - radius;\n}\n\n// Approximate Godot/Blender AgX path using the same inset/outset matrices and allenwp curve.\nfn tonemap_agx(color: vec3<f32>) -> vec3<f32> {\n var c = AGX_INSET_MAT * color;\n c = allenwp_curve(c);\n c = min(vec3<f32>(1.0), c);\n return AGX_OUTSET_MAT * c;\n}\n\n// Scene-linear (sRGB primaries) -> filmic -> linear sRGB in [0,1].\nfn tonemap_aces_filmic(color: vec3<f32>) -> vec3<f32> {\n var c = color;\n c = ACES_INPUT_MAT * c; // sRGB primaries -> ACES fitted domain\n c = rrt_odt_fit(c); // RRT + ODT \"filmic\" curve\n c = ACES_OUTPUT_MAT * c; // Back to sRGB primaries\n\n // Clamp to display range; keep linear values (swapchain is *sRGB*)\n return clamp(c, vec3<f32>(0.0), vec3<f32>(1.0));\n}\n\n// Hable filmic curve with exposure bias and white point normalization.\n// WGSL note: vectorize scalar additions (no vec + scalar) and keep per-channel ops.\nfn tonemap_godot_filmic(color: vec3<f32>, p_white: f32) -> vec3<f32> {\n // Taken from godot source code\n // exposure bias: input scale (color *= bias, white *= bias)\n // has no effect on the curve's general shape or visual properties\n let exposure_bias = 2.0;\n let A = 0.22 * exposure_bias * exposure_bias;\n let B = 0.30 * exposure_bias;\n let C = 0.10;\n let D = 0.20;\n let E = 0.01;\n let F = 0.30;\n\n // ((color*(A*color + C*B) + D*E) / (color*(A*color + B) + D*F)) - E/F\n let num = color * (color * vec3<f32>(A) + vec3<f32>(C * B)) + vec3<f32>(D * E);\n let den = color * (color * vec3<f32>(A) + vec3<f32>(B)) + vec3<f32>(D * F);\n let color_tonemapped = num / den - vec3<f32>(E / F);\n\n let pw_num = p_white * (A * p_white + C * B) + D * E;\n let pw_den = p_white * (A * p_white + B) + D * F;\n let p_white_tonemapped = pw_num / pw_den - E / F;\n\n return color_tonemapped / vec3<f32>(p_white_tonemapped);\n}\n\n@fragment\nfn fs_tonemap(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let dim = vec2<f32>(textureDimensions(t_hdr));\n // Clamp to inside [0 .. dim-1] if uv hits exactly 1.0 at the edge.\n let xy = min(uv * dim, dim - vec2<f32>(1.0, 1.0));\n let exposure = exp2(u_tone.exposure);\n // Bloom folded in pre-exposure (bit-equivalent to the former additive composite into\n // hdr_scene): half-res up[0] bilinear tap * intensity, optional clamp.\n var bloom = textureSampleLevel(t_bloom, s_bloom, uv, 0.0).rgb * u_tone.bloom_params.x;\n if (u_tone.bloom_params.y > 0.0) {\n bloom = min(bloom, vec3<f32>(u_tone.bloom_params.y));\n }\n let hdr = max((textureLoad(t_hdr, vec2<i32>(xy), 0).rgb + bloom) * vec3<f32>(exposure), vec3<f32>(0.0));\n\n var ldr: vec3<f32>;\n if (u_tone.tonemap_mode == 2u) {\n ldr = tonemap_aces_filmic(hdr);\n } else if (u_tone.tonemap_mode == 1u) {\n ldr = tonemap_godot_filmic(hdr, TONEMAP_WHITE);\n } else {\n ldr = tonemap_agx(hdr);\n }\n\n // Saturation multiplier (1 = neutral, 0 = grayscale)\n let lum = dot(ldr, vec3<f32>(0.2126, 0.7152, 0.0722));\n let sat = max(0.0, u_tone.saturation);\n ldr = mix(vec3<f32>(lum, lum, lum), ldr, sat);\n\n // Contrast multiplier around mid-gray 0.5 (1 = neutral, 0 = flat gray)\n let c_mul = max(0.0, u_tone.contrast);\n ldr = (ldr - vec3<f32>(0.5)) * c_mul + vec3<f32>(0.5);\n ldr = clamp(ldr, vec3<f32>(0.0), vec3<f32>(1.0));\n ldr = apply_shadow_lift(ldr);\n\n // Vignette (screen-space rounded-square mask, matching Blender's compositor-style shape).\n let aspect = dim.x / max(1.0, dim.y);\n let p = (uv - vec2<f32>(0.5, 0.5)) * vec2<f32>(aspect, 1.0);\n let vig_radius = clamp(u_tone.vignette_radius, 0.0, 1.0);\n let vig_softness = clamp(u_tone.vignette_softness, 0.0, 1.0);\n let scale = mix(vec2<f32>(0.25, 0.25), vec2<f32>(1.0, 1.0), vig_radius);\n let half_size = 0.5 * scale * vec2<f32>(aspect, 1.0);\n let corner_radius = min(1.0, min(half_size.x, half_size.y));\n let dist = rounded_rect_sdf(p, half_size, corner_radius);\n let center_mask = select(\n select(1.0, 0.0, dist > 0.0),\n 1.0 - smoothstep(-0.3 * vig_softness, vig_softness / (2.0 - vig_softness), dist),\n vig_softness > 1e-6\n );\n let v = clamp(1.0 - center_mask, 0.0, 1.0);\n let vig_strength = clamp(u_tone.vignette_strength, 0.0, 1.0);\n let t = vig_strength * v;\n let vig_rgb = clamp(u_tone.vignette_color.rgb, vec3<f32>(0.0), vec3<f32>(1.0));\n ldr = mix(ldr, vig_rgb, t);\n\n // Encode to sRGB only if the destination surface format is NOT sRGB\n // On desktop we normally present to an sRGB swapchain (hardware converts),\n // but on web the swapchain can be UNORM, so we must encode here.\n var out_rgb = ldr;\n if (u_tone.dst_is_srgb == 0u) {\n let a = 0.055;\n let threshold = 0.0031308;\n let t = vec3<f32>(threshold, threshold, threshold);\n let is_high = step(t, out_rgb);\n let low = out_rgb * 12.92;\n let high = (1.0 + a) * pow(out_rgb, vec3<f32>(1.0 / 2.4)) - a;\n out_rgb = mix(low, high, is_high);\n }\n\n // 3x3 composition grid overlay (screen-space thin lines)\n if (u_tone.light_debug_mode == 11u) {\n let texel = vec2<f32>(1.0, 1.0) / dim;\n let t_x = texel.x * 1.5; // ~1-2 px visually\n let t_y = texel.y * 1.5;\n let c1 = 1.0 - smoothstep(0.0, t_x, abs(uv.x - (1.0 / 3.0)));\n let c2 = 1.0 - smoothstep(0.0, t_x, abs(uv.x - (2.0 / 3.0)));\n let c3 = 1.0 - smoothstep(0.0, t_y, abs(uv.y - (1.0 / 3.0)));\n let c4 = 1.0 - smoothstep(0.0, t_y, abs(uv.y - (2.0 / 3.0)));\n let coverage = max(max(c1, c2), max(c3, c4));\n let grid_color = vec3<f32>(1.0, 1.0, 1.0);\n let alpha = clamp(coverage, 0.0, 1.0);\n ldr = mix(ldr, grid_color, alpha);\n }\n\n return vec4<f32>(out_rgb, 1.0);\n}\n"},{"label":"shaders/selection_outline.wgsl","code":"// selection_outline.wgsl\n// Constant-width silhouette outlines for selected/hovered editor objects.\n// Edge-detects two independent coverage masks (selected-only / hovered-only, occlusion-free,\n// 0 = background) and draws a ~thickness px halo just outside each silhouette. Each ring is\n// computed from its own mask, so both are complete loops and may overlap/cross (Roblox-style);\n// a ring also draws over the OTHER object's body, only never over its own.\n// Rendered into the SMAA INPUT (post-tonemap, before SMAA) so the exact stencil color is kept\n// AND SMAA anti-aliases the outline for free -> crisp, not jagged, at no extra pass cost. The\n// edge test is a cheap binary kernel since SMAA does the smoothing.\n\nstruct OutlineParams {\n selected_color: vec4<f32>, // rgb + a=opacity\n hovered_color: vec4<f32>, // rgb + a=opacity\n thickness_px: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n@group(0) @binding(0) var sel_tex: texture_2d<u32>;\n@group(0) @binding(1) var hov_tex: texture_2d<u32>;\n@group(0) @binding(2) var<uniform> params: OutlineParams;\n\n@vertex\nfn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {\n // Fullscreen triangle.\n let uv = vec2<f32>(f32((vid << 1u) & 2u), f32(vid & 2u));\n return vec4<f32>(uv * 2.0 - vec2<f32>(1.0), 0.0, 1.0);\n}\n\n// Globals read directly (textures as fn params can trip naga version quirks).\nfn covered_sel(p: vec2<i32>, d: vec2<i32>) -> bool {\n return textureLoad(sel_tex, clamp(p, vec2<i32>(0), d - vec2<i32>(1)), 0).r != 0u;\n}\nfn covered_hov(p: vec2<i32>, d: vec2<i32>) -> bool {\n return textureLoad(hov_tex, clamp(p, vec2<i32>(0), d - vec2<i32>(1)), 0).r != 0u;\n}\n\n@fragment\nfn fs_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {\n let d = vec2<i32>(textureDimensions(sel_tex));\n let p0 = vec2<i32>(frag.xy);\n let sel_self = covered_sel(p0, d);\n let hov_self = covered_hov(p0, d);\n if (sel_self && hov_self) { discard; } // inside both -> neither ring can draw\n\n // Binary edge: ring if that mask's silhouette is within `thickness` px and the pixel is\n // outside it. Circular kernel for uniform width; SMAA AAs the edges.\n let r = max(i32(round(max(params.thickness_px, 1.0))), 1);\n let r2 = r * r;\n var sel_near = false;\n var hov_near = false;\n for (var dy = -r; dy <= r; dy = dy + 1) {\n for (var dx = -r; dx <= r; dx = dx + 1) {\n if ((dx == 0 && dy == 0) || (dx * dx + dy * dy > r2)) { continue; }\n let pp = p0 + vec2<i32>(dx, dy);\n if (!sel_near && covered_sel(pp, d)) { sel_near = true; }\n if (!hov_near && covered_hov(pp, d)) { hov_near = true; }\n }\n }\n if (!sel_self && sel_near) { // selected ring wins where the rings coincide\n return vec4<f32>(params.selected_color.rgb, params.selected_color.a);\n }\n if (!hov_self && hov_near) {\n return vec4<f32>(params.hovered_color.rgb, params.hovered_color.a);\n }\n // No ring: alpha-0 is an exact no-op under this pass's blend (and avoids `discard` followed\n // by `return`, which naga rejects as instructions-after-terminator while Tint requires the return).\n return vec4<f32>(0.0);\n}\n"},{"label":"shaders/highlight_outline.wgsl","code":"// highlight_outline.wgsl\n// Per-object-styled silhouette outlines for Weave ShowOutlineHighlight (game mode).\n// The mask (R32Uint, occlusion-free, 0 = background) stores each highlighted object's pick id;\n// per-pixel we find covered taps within the max radius and ring the pixel if any tap belongs to\n// an entry whose thickness reaches this pixel. One fullscreen pass handles arbitrary per-object\n// color/thickness (palette in a uniform), no extra mask textures.\n\nstruct HighlightEntry {\n color: vec4<f32>, // rgb + a=opacity\n id: u32, // CompactOriObjectId::to_u32 (never 0)\n thickness_px: f32,\n _pad0: f32,\n _pad1: f32,\n};\nstruct HighlightParams {\n count: u32,\n max_radius: f32,\n _pad0: f32,\n _pad1: f32,\n entries: array<HighlightEntry, 64>,\n};\n@group(0) @binding(0) var mask_tex: texture_2d<u32>;\n@group(0) @binding(1) var<uniform> params: HighlightParams;\n\n@vertex\nfn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {\n // Fullscreen triangle.\n let uv = vec2<f32>(f32((vid << 1u) & 2u), f32(vid & 2u));\n return vec4<f32>(uv * 2.0 - vec2<f32>(1.0), 0.0, 1.0);\n}\n\nfn mask_id(p: vec2<i32>, d: vec2<i32>) -> u32 {\n return textureLoad(mask_tex, clamp(p, vec2<i32>(0), d - vec2<i32>(1)), 0).r;\n}\n\nfn entry_index(id: u32) -> i32 {\n for (var i = 0u; i < params.count; i = i + 1u) {\n if (params.entries[i].id == id) { return i32(i); }\n }\n return -1;\n}\n\n@fragment\nfn fs_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {\n let d = vec2<i32>(textureDimensions(mask_tex));\n let p0 = vec2<i32>(frag.xy);\n if (mask_id(p0, d) != 0u) { // inside a highlighted silhouette: never ring over itself\n return vec4<f32>(0.0);\n }\n\n // Nearest covered tap within the max radius decides the ring (so adjoining objects keep\n // clean borders); the ring only draws if that entry's own thickness reaches this pixel.\n let r = max(i32(round(max(params.max_radius, 1.0))), 1);\n let r2 = r * r;\n var best_d2 = r2 + 1;\n var best_id = 0u;\n for (var dy = -r; dy <= r; dy = dy + 1) {\n for (var dx = -r; dx <= r; dx = dx + 1) {\n let d2 = dx * dx + dy * dy;\n if (d2 == 0 || d2 > r2 || d2 >= best_d2) { continue; }\n let id = mask_id(p0 + vec2<i32>(dx, dy), d);\n if (id != 0u) {\n best_d2 = d2;\n best_id = id;\n }\n }\n }\n if (best_id != 0u) {\n let ei = entry_index(best_id);\n if (ei >= 0) {\n let e = params.entries[u32(ei)];\n if (f32(best_d2) <= e.thickness_px * e.thickness_px) {\n return e.color;\n }\n }\n }\n // Alpha-0 is an exact no-op under this pass's blend.\n return vec4<f32>(0.0);\n}\n"},{"label":"shaders/decal_pass_global.wgsl","code":"struct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nstruct DecalDrawParams {\n bucket_index: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\nstruct DecalBucketRange {\n visible_start: u32,\n visible_count: u32,\n pad: vec2<u32>,\n};\n\nstruct GpuDecal {\n center: vec4<f32>,\n half_size: vec4<f32>,\n axis_x: vec4<f32>,\n axis_y: vec4<f32>,\n axis_z: vec4<f32>,\n color: vec4<f32>,\n normal: vec4<f32>,\n orm: vec4<f32>,\n};\n\nstruct DecalOutputs {\n @location(0) base_color: vec4<f32>,\n @location(1) normal: vec4<f32>,\n @location(2) orm: vec4<f32>,\n};\n\n@group(0) @binding(0) var depth_texture: texture_depth_2d;\n@group(0) @binding(1) var normal_texture: texture_2d<f32>;\n@group(0) @binding(2) var decal_base_color_texture: texture_2d<f32>;\n@group(0) @binding(3) var decal_normal_texture: texture_2d<f32>;\n@group(0) @binding(4) var decal_orm_texture: texture_2d<f32>;\n@group(0) @binding(5) var decal_sampler: sampler;\n@group(0) @binding(6) var<uniform> u_draw: DecalDrawParams;\n@group(0) @binding(7) var<storage, read> source_decals: array<GpuDecal>;\n@group(0) @binding(8) var receiver_base_color_texture: texture_2d<f32>;\n@group(0) @binding(9) var<storage, read> visible_ids: array<u32>;\n@group(0) @binding(10) var<storage, read> bucket_ranges: array<DecalBucketRange>;\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {\n var out: VertexOutput;\n let x = f32(vertex_index & 1u) * 4.0 - 1.0;\n let y = f32(vertex_index & 2u) * 2.0 - 1.0;\n out.clip_position = vec4<f32>(x, y, 0.0, 1.0);\n return out;\n}\n\nfn saturate(v: f32) -> f32 {\n return clamp(v, 0.0, 1.0);\n}\n\nfn receive_decal_from_flags(flags: f32) -> bool {\n return flags > 0.9 || (flags > 0.2 && flags < 0.5);\n}\n\nfn decode_tangent_space_normal(sampled_rgb: vec3<f32>) -> vec3<f32> {\n // .rg-only + z=-1 to match BC5 (b=0); identical decode on BC and ASTC.\n return normalize(vec3<f32>(sampled_rgb.rg * 2.0 - vec2<f32>(1.0), -1.0));\n}\n\nfn procedural_alpha_test_base_color(uv: vec2<f32>, repeat_count: f32) -> vec4<f32> {\n let stripe = step(0.5, fract(uv.x * repeat_count));\n let checker = step(0.5, fract(uv.y * 2.0));\n let tint = mix(vec3<f32>(0.15, 0.85, 1.0), vec3<f32>(1.0, 0.25, 0.7), checker);\n return vec4<f32>(tint, stripe);\n}\n\nfn procedural_directional_test_normal(uv: vec2<f32>, slope_strength: f32, repeat_count: f32) -> vec3<f32> {\n let wave = sin(uv.x * repeat_count * 6.28318530718);\n return normalize(vec3<f32>(wave * slope_strength, 0.0, 1.0));\n}\n\nfn procedural_directional_test_orm(uv: vec2<f32>, rough_min: f32, rough_max: f32, repeat_count: f32) -> vec3<f32> {\n let wave = 0.5 + 0.5 * sin(uv.x * repeat_count * 6.28318530718);\n let roughness = mix(rough_min, rough_max, wave);\n return vec3<f32>(1.0, roughness, 0.0);\n}\n\n@fragment\nfn fs_decal(@builtin(position) clip_position: vec4<f32>) -> DecalOutputs {\n let bucket = bucket_ranges[u_draw.bucket_index];\n if bucket.visible_count == 0u {\n discard;\n }\n let size_u = textureDimensions(depth_texture);\n let size = vec2<f32>(size_u);\n let pixel_coords = vec2<i32>(clip_position.xy);\n let depth = textureLoad(depth_texture, pixel_coords, 0);\n if depth >= 1.0 {\n discard;\n }\n if (!receive_decal_from_flags(textureLoad(receiver_base_color_texture, pixel_coords, 0).a)) {\n discard;\n }\n let world_normal_raw = textureLoad(normal_texture, pixel_coords, 0).xyz;\n let world_normal_len = length(world_normal_raw);\n if world_normal_len <= 0.0001 {\n discard;\n }\n let world_normal = world_normal_raw / world_normal_len;\n\n let uv = clip_position.xy / size;\n let flipped_uv = vec2<f32>(uv.x, 1.0 - uv.y);\n let ndc = vec4<f32>(flipped_uv * 2.0 - 1.0, depth, 1.0);\n let world_pos_h = u_camera.inverse_view_proj * ndc;\n let world_pos = world_pos_h.xyz / world_pos_h.w;\n\n var out_color = vec4<f32>(0.0);\n var out_normal = vec3<f32>(0.0);\n var out_normal_alpha = 0.0;\n var out_orm = vec3<f32>(0.0);\n var out_orm_alpha = 0.0;\n for (var j = 0u; j < bucket.visible_count; j = j + 1u) {\n let decal = source_decals[visible_ids[bucket.visible_start + j]];\n let delta = world_pos - decal.center.xyz;\n let local_pos = vec3<f32>(\n dot(delta, decal.axis_x.xyz) / decal.half_size.x,\n dot(delta, decal.axis_y.xyz) / decal.half_size.y,\n dot(delta, decal.axis_z.xyz) / decal.half_size.z\n );\n let inside = max(max(abs(local_pos.x), abs(local_pos.y)), abs(local_pos.z));\n if inside > 1.0 {\n continue;\n }\n\n let edge_dist = max(abs(local_pos.x), abs(local_pos.y));\n let edge_fade = 1.0 - smoothstep(0.86, 1.0, edge_dist);\n if edge_fade <= 0.0 {\n continue;\n }\n let decal_uv = vec2<f32>(local_pos.x * 0.5 + 0.5, 0.5 - local_pos.y * 0.5);\n let texel = select(\n textureSampleLevel(decal_base_color_texture, decal_sampler, decal_uv, 0.0),\n procedural_alpha_test_base_color(decal_uv, decal.half_size.w),\n decal.center.w > 0.5\n );\n let decal_normal_sample = textureSampleLevel(decal_normal_texture, decal_sampler, decal_uv, 0.0).rgb;\n let decal_orm_sample = textureSampleLevel(decal_orm_texture, decal_sampler, decal_uv, 0.0).rgb;\n\n let facing = smoothstep(0.12, 0.45, dot(world_normal, decal.axis_z.xyz));\n if facing <= 0.0 {\n continue;\n }\n let opening_fade = 1.0 - smoothstep(0.86, 1.0, local_pos.z);\n let far_fade = smoothstep(-1.0, -0.72, local_pos.z);\n let depth_fade = opening_fade * far_fade;\n let alpha = saturate(decal.color.a * texel.a * edge_fade * depth_fade * facing);\n if alpha <= 0.0 {\n continue;\n }\n\n let weight = (1.0 - out_color.a) * alpha;\n let tangent_space_normal = select(\n decode_tangent_space_normal(decal_normal_sample),\n procedural_directional_test_normal(decal_uv, decal.normal.y, decal.normal.z),\n decal.normal.x > 0.5\n );\n let decal_normal = normalize(\n tangent_space_normal.x * decal.axis_x.xyz +\n tangent_space_normal.y * -decal.axis_y.xyz +\n tangent_space_normal.z * decal.axis_z.xyz\n );\n let decal_orm = select(\n decal_orm_sample,\n procedural_directional_test_orm(decal_uv, -decal.orm.x, decal.orm.y, decal.orm.z),\n decal.orm.x < 0.0\n );\n let normal_weight = (1.0 - out_normal_alpha) * alpha * decal.normal.w;\n let orm_weight = (1.0 - out_orm_alpha) * alpha * decal.orm.a;\n out_color = vec4<f32>(out_color.rgb + decal.color.rgb * texel.rgb * weight, out_color.a);\n out_normal += decal_normal * normal_weight;\n out_orm += decal_orm * orm_weight;\n out_color.a += (1.0 - out_color.a) * alpha;\n out_normal_alpha += normal_weight;\n out_orm_alpha += orm_weight;\n }\n\n if out_color.a <= 0.0 {\n discard;\n }\n var outputs: DecalOutputs;\n outputs.base_color = out_color;\n outputs.normal = vec4<f32>(out_normal, out_normal_alpha);\n outputs.orm = vec4<f32>(out_orm, out_orm_alpha);\n return outputs;\n}\n"},{"label":"shaders/decal_cull_flags.wgsl","code":"struct DecalGlobalCullParams {\n decal_count: u32,\n enable_frustum: u32,\n enable_distance: u32,\n _pad0: u32,\n max_distance_m: f32,\n frustum_margin_m: f32,\n _pad1: vec2<f32>,\n camera_position: vec4<f32>,\n frustum_planes: array<vec4<f32>, 6>,\n};\n\nstruct GpuDecal {\n center: vec4<f32>,\n half_size: vec4<f32>,\n axis_x: vec4<f32>,\n axis_y: vec4<f32>,\n axis_z: vec4<f32>,\n color: vec4<f32>,\n normal: vec4<f32>,\n orm: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> params: DecalGlobalCullParams;\n@group(0) @binding(1) var<storage, read> source_decals: array<GpuDecal>;\n@group(0) @binding(2) var<storage, read_write> visible_flags: array<u32>;\n@group(0) @binding(3) var<storage, read_write> debug_buffer: array<u32>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= params.decal_count) {\n return;\n }\n let decal = source_decals[i];\n let center = decal.center.xyz;\n let extent = abs(decal.axis_x.xyz) * decal.half_size.x +\n abs(decal.axis_y.xyz) * decal.half_size.y +\n abs(decal.axis_z.xyz) * decal.half_size.z;\n var visible = 1u;\n if (max(max(extent.x, extent.y), extent.z) <= 0.0) {\n visible = 0u;\n }\n if (visible != 0u && params.enable_distance != 0u) {\n let max_d = params.max_distance_m + length(extent);\n let delta = params.camera_position.xyz - center;\n if (dot(delta, delta) > max_d * max_d) {\n visible = 0u;\n }\n }\n if (visible != 0u && params.enable_frustum != 0u) {\n for (var p = 0u; p < 6u; p = p + 1u) {\n let plane = params.frustum_planes[p];\n let margin = dot(abs(plane.xyz), extent) + params.frustum_margin_m;\n if (dot(plane.xyz, center) + plane.w < -margin) {\n visible = 0u;\n }\n }\n }\n visible_flags[i] = visible;\n}\n"},{"label":"shaders/decal_bucket_ranges.wgsl","code":"struct DecalBucketMeta {\n source_start: u32,\n source_count: u32,\n kind: u32,\n _pad: u32,\n};\n\nstruct DecalBucketRange {\n visible_start: u32,\n visible_count: u32,\n pad: vec2<u32>,\n};\n\nstruct DecalBucketRangeParams {\n bucket_count: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: DecalBucketRangeParams;\n@group(0) @binding(1) var<storage, read> bucket_meta: array<DecalBucketMeta>;\n@group(0) @binding(2) var<storage, read> visible_flags: array<u32>;\n@group(0) @binding(3) var<storage, read_write> bucket_ranges: array<DecalBucketRange>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let bucket_i = gid.x;\n if (bucket_i >= params.bucket_count) {\n return;\n }\n let bucket = bucket_meta[bucket_i];\n if (bucket.source_count == 0u) {\n bucket_ranges[bucket_i].visible_start = 0u;\n bucket_ranges[bucket_i].visible_count = 0u;\n bucket_ranges[bucket_i].pad = vec2<u32>(0u, 0u);\n return;\n }\n var before = 0u;\n if (bucket.source_start > 0u) {\n before = visible_flags[bucket.source_start - 1u];\n }\n let last = bucket.source_start + bucket.source_count - 1u;\n let end_count = visible_flags[last];\n bucket_ranges[bucket_i].visible_start = before;\n bucket_ranges[bucket_i].visible_count = end_count - before;\n bucket_ranges[bucket_i].pad = vec2<u32>(0u, 0u);\n}\n"},{"label":"terrain_vertex","code":"// terrain_vertex.wgsl - instanced quadtree tile vertex stage.\n// Each instance is one 32x32-quad tile (33x33 samples + 4 skirt strips) selected by\n// terrain_tiles.rs; heights come from the shared rg32float tile atlas baked by\n// terrain_tile_bake.wgsl (R = display height, G = parent-LOD height for geomorphing).\n// Vertex ids: [0, 33*33) = grid samples, then 4*33 skirt copies of the edge rows/cols\n// (same XZ, height - skirt_depth) hiding cracks between neighboring LODs.\n\nstruct CameraUniform {\n // Must match `oriverse_wgpu/src/shaders/gbuffer.wgsl` CameraUniform layout exactly.\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nstruct TerrainUniform {\n samples_x: u32,\n samples_y: u32,\n cell_size: f32,\n min_z: f32,\n max_z: f32,\n // Recipe surface-input ring-stack camera-follow center in grid cells.\n clip_center_x: f32,\n clip_center_y: f32,\n debug_lod_view: f32, // 1 = tint fragments by tile LOD tier (/lodview)\n origin: vec3<f32>,\n // Instance-index offset into the tile buffer (Hi-Z culled frames draw 0..count over the\n // compacted mirror; the VS adds the terrain's range start). 0 on direct frames.\n tile_base: f32,\n // Ring-stack clip (terrain_recipe_rings.rs; array at group(1) binding(8)):\n // (level_count [0 = off], min_valid_level, terminal texel_cells, spare).\n clip2: vec4<f32>,\n // Per level (base_cell_x, base_cell_y, texel_cells, spare); ring bases 512-texel-aligned.\n clip_levels: array<vec4<f32>, 8>,\n};\n\n// Must match TileInstanceGpu in terrain_tiles.rs (48 bytes).\nstruct TileInstance {\n corner: vec2<f32>, // world XZ of tile-local sample (0,0)\n quad_step: f32, // world meters per tile quad at this LOD\n _pad0: f32,\n atlas_base: vec2<f32>, // atlas texel coords of tile-local sample (0,0)\n atlas_step: f32, // texels per local sample (1, or 2^-d on ancestor fallback)\n morph_start: f32,\n morph_end: f32,\n skirt_depth: f32,\n clamp_max: vec2<f32>, // grid boundary in tile-local quads (partial edge tiles collapse)\n};\n\n@group(1) @binding(1) var<uniform> terrain: TerrainUniform;\n@group(1) @binding(5) var<storage, read> tiles: array<TileInstance>;\n@group(1) @binding(6) var tile_atlas: texture_2d<f32>;\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @location(0) world_pos: vec3<f32>,\n @location(1) world_n: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) ndc_z: f32,\n // Custom terrain shader inputs (optional for fragment to consume):\n @location(4) local_pos: vec3<f32>,\n @location(5) local_n: vec3<f32>,\n @location(6) world_t: vec3<f32>,\n @location(7) world_b: vec3<f32>,\n // 1 = vertex normal carries sub-sim-cell detail octaves (finest LODs); 0 = fragment should\n // use the per-pixel sim-resolution normal map instead. Fades with the geomorph factor.\n @location(9) detail_w: f32,\n // World meters per tile quad (the tile's LOD); /lodview tier tint input.\n @location(10) quad_step: f32,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(11) cur_clip: vec4<f32>,\n @location(12) prev_clip: vec4<f32>,\n};\n\nconst ORI_TILE_SAMPLES: u32 = 33u;\nconst ORI_TILE_GRID_VERTS: u32 = ORI_TILE_SAMPLES * ORI_TILE_SAMPLES;\n\n// Tile-local vertex position in sample units + skirt flag. Edge order must match\n// build_tile_indices() in terrain_tiles.rs (0=row0, 1=row32, 2=col0, 3=col32).\nfn ori_tile_local(vi: u32) -> vec3<f32> {\n if (vi < ORI_TILE_GRID_VERTS) {\n return vec3<f32>(f32(vi % ORI_TILE_SAMPLES), f32(vi / ORI_TILE_SAMPLES), 0.0);\n }\n let s = vi - ORI_TILE_GRID_VERTS;\n let edge = s / ORI_TILE_SAMPLES;\n let i = f32(s % ORI_TILE_SAMPLES);\n let last = f32(ORI_TILE_SAMPLES - 1u);\n switch edge {\n case 0u: { return vec3<f32>(i, 0.0, 1.0); }\n case 1u: { return vec3<f32>(i, last, 1.0); }\n case 2u: { return vec3<f32>(0.0, i, 1.0); }\n default: { return vec3<f32>(last, i, 1.0); }\n }\n}\n\n// Manual bilinear (rg32float is not filterable in base WebGPU). `l` in tile-local sample\n// units; the 1-texel apron baked around each tile keeps l in [-1, 33] valid.\nfn ori_tile_h(t: TileInstance, l: vec2<f32>) -> vec2<f32> {\n let a = t.atlas_base + l * t.atlas_step;\n let f = floor(a);\n let fr = a - f;\n let p = vec2<i32>(f);\n let h00 = textureLoad(tile_atlas, p, 0).rg;\n let h10 = textureLoad(tile_atlas, p + vec2<i32>(1, 0), 0).rg;\n let h01 = textureLoad(tile_atlas, p + vec2<i32>(0, 1), 0).rg;\n let h11 = textureLoad(tile_atlas, p + vec2<i32>(1, 1), 0).rg;\n return mix(mix(h00, h10, fr.x), mix(h01, h11, fr.x), fr.y);\n}\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vi: u32,\n @builtin(instance_index) ii: u32,\n) -> VsOut {\n let t = tiles[ii + u32(terrain.tile_base)];\n let lv = ori_tile_local(vi);\n let is_skirt = lv.z > 0.5;\n let l = min(lv.xy, t.clamp_max);\n\n // Geomorph: odd vertices slide toward the even (parent-grid) position while the height\n // blends R -> G (parent LOD's value), so LOD switches are pop-free.\n let h_pre = ori_tile_h(t, l);\n let pre_pos = vec3<f32>(\n t.corner.x + l.x * t.quad_step,\n terrain.origin.y + h_pre.x,\n t.corner.y - l.y * t.quad_step,\n );\n let d = distance(camera.camera_position, pre_pos);\n let m = clamp((d - t.morph_start) / max(t.morph_end - t.morph_start, 1e-3), 0.0, 1.0);\n var lm = l;\n var h = h_pre.x;\n if (m > 0.0) {\n lm = min(l - fract(l * 0.5) * 2.0 * m, t.clamp_max);\n let hm = ori_tile_h(t, lm);\n h = mix(hm.x, hm.y, m);\n }\n\n let world_x = t.corner.x + lm.x * t.quad_step;\n // Sim +Y maps to -glam Z: rows walk toward -Z from the tile corner (grid \"top\").\n let world_z = t.corner.y - lm.y * t.quad_step;\n var world_y = terrain.origin.y + h;\n if (is_skirt) { world_y -= t.skirt_depth; }\n\n // Normal: central differences on the atlas R channel at the nearest texel (apron covers\n // the +-1 reads). One atlas texel spans quad_step/atlas_step world meters.\n let p = vec2<i32>(round(t.atlas_base + lm * t.atlas_step));\n let hx0 = textureLoad(tile_atlas, p - vec2<i32>(1, 0), 0).r;\n let hx1 = textureLoad(tile_atlas, p + vec2<i32>(1, 0), 0).r;\n let hy0 = textureLoad(tile_atlas, p - vec2<i32>(0, 1), 0).r;\n let hy1 = textureLoad(tile_atlas, p + vec2<i32>(0, 1), 0).r;\n let ws = t.quad_step / max(t.atlas_step, 1e-6);\n let dx = vec3<f32>(2.0 * ws, hx1 - hx0, 0.0);\n let dz = vec3<f32>(0.0, hy1 - hy0, -2.0 * ws);\n // Keep CCW winding + +Y normals on a flat plane.\n let n = normalize(cross(dx, dz));\n // Orthonormal TBN (world-space), consistent with `n`.\n var tan = dx - n * dot(n, dx);\n tan = tan / max(length(tan), 1e-6);\n var b = cross(n, tan);\n b = b / max(length(b), 1e-6);\n // Keep bitangent roughly aligned with dz (stabilizes handedness).\n if (dot(b, dz) < 0.0) { b = -b; }\n // Re-orthonormalize tangent in case we flipped b.\n tan = cross(b, n);\n\n // Whole-terrain uv (grass weightmap / analysis / splat weightmap addressing).\n let size_x = f32(max(1u, terrain.samples_x - 1u)) * terrain.cell_size;\n let size_z = f32(max(1u, terrain.samples_y - 1u)) * terrain.cell_size;\n let uv = vec2<f32>(\n (world_x - terrain.origin.x) / size_x + 0.5,\n 0.5 - (world_z - terrain.origin.z) / size_z,\n );\n\n var out: VsOut;\n out.world_pos = vec3<f32>(world_x, world_y, world_z);\n out.world_n = n;\n out.uv = uv;\n out.local_pos = out.world_pos - terrain.origin;\n out.local_n = out.world_n;\n out.world_t = tan;\n out.world_b = b;\n // Detail octaves count at this LOD = log2(cell/quad_step); the geomorph factor m blends\n // heights toward one octave less, so subtracting m fades detail_w in lockstep (pop-free).\n out.detail_w = clamp(log2(max(terrain.cell_size / t.quad_step, 1e-6)) - m, 0.0, 1.0);\n out.quad_step = t.quad_step;\n out.clip_pos = camera.view_proj * vec4<f32>(out.world_pos, 1.0);\n out.ndc_z = out.clip_pos.z / out.clip_pos.w;\n out.cur_clip = camera.unjittered_view_proj * vec4<f32>(out.world_pos, 1.0);\n out.prev_clip = camera.prev_unjittered_view_proj * vec4<f32>(out.world_pos, 1.0);\n return out;\n}\n"},{"label":"terrain_fragment_default","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nstruct CameraUniform {\n // Must match `oriverse_wgpu/src/shaders/gbuffer.wgsl` CameraUniform layout exactly.\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nstruct TerrainUniform {\n samples_x: u32,\n samples_y: u32,\n cell_size: f32,\n min_z: f32,\n max_z: f32,\n // Recipe surface-input ring-stack camera-follow center in grid cells.\n clip_center_x: f32,\n clip_center_y: f32,\n debug_lod_view: f32, // 1 = tint fragments by tile LOD tier (/lodview)\n origin: vec3<f32>,\n // Instance-index offset into the tile buffer (Hi-Z culled frames draw 0..count over the\n // compacted mirror; the VS adds the terrain's range start). 0 on direct frames.\n tile_base: f32,\n // Ring-stack clip (terrain_recipe_rings.rs; array at group(1) binding(8)):\n // (level_count [0 = off], min_valid_level, terminal texel_cells, spare).\n clip2: vec4<f32>,\n // Per level (base_cell_x, base_cell_y, texel_cells, spare); ring bases 512-texel-aligned.\n clip_levels: array<vec4<f32>, 8>,\n};\n@group(1) @binding(1) var<uniform> terrain: TerrainUniform;\n// Sim-resolution world-space normal map (RGBA8 n*0.5+0.5, mipped); per-pixel shading normals\n// so coarse-LOD tiles don't facet. Blended with the vertex normal by detail_w (finest LODs\n// carry sub-cell detail octaves the map doesn't have).\n@group(1) @binding(7) var terrain_normal_tex: texture_2d<f32>;\n\nstruct MaterialUniform { params4: array<vec4<f32>, 8>, };\n@group(2) @binding(0) var tex0: texture_2d<f32>;\n@group(2) @binding(1) var tex1: texture_2d<f32>;\n@group(2) @binding(2) var tex2: texture_2d<f32>;\n@group(2) @binding(3) var tex3: texture_2d<f32>;\n@group(2) @binding(4) var tex4: texture_2d<f32>;\n@group(2) @binding(5) var tex5: texture_2d<f32>;\n@group(2) @binding(6) var tex6: texture_2d<f32>;\n@group(2) @binding(7) var tex7: texture_2d<f32>;\n@group(2) @binding(8) var tex8: texture_2d<f32>;\n@group(2) @binding(9) var tex9: texture_2d<f32>;\n@group(2) @binding(10) var tex10: texture_2d<f32>;\n@group(2) @binding(11) var tex11: texture_2d<f32>;\n@group(2) @binding(12) var tex12: texture_2d<f32>;\n@group(2) @binding(13) var tex13: texture_2d<f32>;\n@group(2) @binding(14) var tex14: texture_2d<f32>;\n@group(2) @binding(15) var tex15: texture_2d<f32>;\n@group(2) @binding(16) var tex_sampler: sampler;\n@group(2) @binding(17) var<uniform> mat: MaterialUniform;\n\nstruct VsOut {\n @location(0) world_pos: vec3<f32>,\n @location(1) world_n: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) ndc_z: f32,\n @location(4) local_pos: vec3<f32>,\n @location(5) local_n: vec3<f32>,\n @location(6) world_t: vec3<f32>,\n @location(7) world_b: vec3<f32>,\n @location(9) detail_w: f32,\n // World meters per tile quad (the tile's LOD); /lodview tier tint input.\n @location(10) quad_step: f32,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(11) cur_clip: vec4<f32>,\n @location(12) prev_clip: vec4<f32>,\n};\n\n// /lodview tier palette: tile quad size relative to the sim cell, red = finest\n// (cell/4 sub-cell detail) through magenta/white = coarsest far rings.\nfn ori_lod_tier_color(quad_step: f32) -> vec3<f32> {\n let tier = i32(round(log2(max(quad_step, 1e-4) / max(terrain.cell_size, 1e-4)))) + 2;\n switch tier {\n case 0: { return vec3<f32>(1.0, 0.15, 0.15); } // cell/4\n case 1: { return vec3<f32>(1.0, 0.55, 0.10); } // cell/2\n case 2: { return vec3<f32>(1.0, 1.00, 0.15); } // cell\n case 3: { return vec3<f32>(0.20, 0.90, 0.20); } // 2x\n case 4: { return vec3<f32>(0.15, 0.90, 0.90); } // 4x\n case 5: { return vec3<f32>(0.25, 0.40, 1.00); } // 8x\n case 6: { return vec3<f32>(0.90, 0.25, 0.90); } // 16x\n default: { return vec3<f32>(1.0, 1.0, 1.0); } // coarser\n }\n}\n\n// Per-pixel geometric normal: sim-res normal map at far/coarse LODs, vertex normal (which\n// carries detail octaves) near the camera. uv remaps to texel centers (grid spans n-1 cells\n// but the texture has n texels), then blends by detail_w.\n// The map only covers the sim grid: display-extension fragments (uv outside [0,1]) must not\n// sample it (tex_sampler repeats = the sim region's normals tiled across the whole\n// extension). Clamp the sample and fade to the vertex normal over ~2 cells outside.\n\n// ---- Recipe surface-input ring stack (terrain_recipe_rings.rs; twin comments in\n// terrain_material16.wgsl).\n@group(1) @binding(8) var terrain_clip_stack: texture_2d_array<f32>;\n\nfn ori_clip_stack_level(cells: vec2<f32>) -> f32 {\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n let d = max(abs(cells.x - terrain.clip_center_x), abs(cells.y - terrain.clip_center_y));\n let lf_min = log2(max(minify, 2.0)) - 1.0;\n let lf_dist = log2(max(d * (1.0 / 112.0), 1.0));\n return clamp(max(lf_min, lf_dist), terrain.clip2.y, terrain.clip2.x - 1.0);\n}\n\nfn ori_clip_stack_layer(cells: vec2<f32>, lv: i32) -> vec4<f32> {\n let p = terrain.clip_levels[lv];\n var rel = (cells - p.xy) / max(p.z, 1e-3);\n // Terminal level: no wrap - clamp to the first/last texel center (old L1 edge clamp).\n if (lv == i32(terrain.clip2.x) - 1) { rel = clamp(rel, vec2<f32>(0.0), vec2<f32>(511.0)); }\n return textureSampleLevel(terrain_clip_stack, tex_sampler, (rel + 0.5) / 512.0, lv, 0.0);\n}\n\nfn ori_clip_stack_sample(cells: vec2<f32>) -> vec4<f32> {\n let lf = ori_clip_stack_level(cells);\n let li = i32(lf);\n let lj = min(li + 1, i32(terrain.clip2.x) - 1);\n return mix(ori_clip_stack_layer(cells, li), ori_clip_stack_layer(cells, lj), fract(lf));\n}\n\nfn ori_clip_stack_vertex_fade(cells: vec2<f32>) -> f32 {\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n return smoothstep(2.0, 4.0, minify / max(terrain.clip2.z, 1.0));\n}\n\n// Debug switch: false = geometric normal maps (grid map + recipe clipmap) off, pure mesh\n// vertex normals (LOD budget reviews). ctx.user stays live for paint.\nconst ORI_TERRAIN_NORMAL_MAPS: bool = true;\n\nfn ori_terrain_geo_normal(uv: vec2<f32>, world_n: vec3<f32>, detail_w: f32) -> vec3<f32> {\n if (!ORI_TERRAIN_NORMAL_MAPS) { return normalize(world_n); }\n let n_tex = vec2<f32>(f32(terrain.samples_x), f32(terrain.samples_y));\n let cells = uv * (n_tex - vec2<f32>(1.0));\n if (terrain.clip2.x > 0.5) {\n let m = ori_clip_stack_sample(cells);\n let map_n = normalize(m.rgb * 2.0 - vec3<f32>(1.0));\n let w = max(detail_w, ori_clip_stack_vertex_fade(cells));\n return normalize(mix(map_n, normalize(world_n), w));\n }\n let over = max(max(-uv, uv - vec2<f32>(1.0)), vec2<f32>(0.0)) * (n_tex - vec2<f32>(1.0));\n let ext_w = smoothstep(0.0, 2.0, max(over.x, over.y));\n let uv_c = clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0));\n let uv_n = (uv_c * (n_tex - vec2<f32>(1.0)) + vec2<f32>(0.5)) / n_tex;\n let map_n = normalize(textureSample(terrain_normal_tex, tex_sampler, uv_n).rgb * 2.0 - vec3<f32>(1.0));\n return normalize(mix(map_n, normalize(world_n), max(detail_w, ext_w)));\n}\n\nstruct FsOut {\n @location(0) out_base: vec4<f32>,\n @location(1) out_normal: vec4<f32>,\n @location(2) out_orm: vec4<f32>,\n @location(3) out_velocity: vec2<f32>,\n};\n\nfn decode_normal_bc5(sample: vec4<f32>) -> vec3<f32> {\n // BC5 stores X,Y in RG (linear). Reconstruct Z (positive hemisphere).\n let xy = sample.rg * 2.0 - vec2<f32>(1.0, 1.0);\n let z2 = max(1.0 - dot(xy, xy), 0.0);\n return vec3<f32>(xy.x, xy.y, sqrt(z2));\n}\n\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(@builtin(position) clip_position: vec4<f32>, in: VsOut) -> FsOut {\n // Planar reflection: clip below-plane terrain (same rule as gbuffer.wgsl) so the mirrored\n // camera never sees underground tiles/skirts.\n if (camera.reflection_clip_enabled > 0.5 && in.world_pos.y < camera.reflection_clip_y) {\n discard;\n }\n // Params:\n // - param0..3 (params4[0]): base intensity (RGBA multiplier)\n // - param4..7 (params4[1]): normal intensity:\n // x,y = scale tangent normal XY, z = blend (0..1, 1=full), w unused\n // - param8..11 (params4[2]): x = uv_scale (preferred), y = normal preview flag (0/1)\n //\n // Texture slots (material tex0..15):\n // - tex0: splat/weights (RGBA, linear). NOTE: sampled with unscaled `uv_splat`.\n // - tex1..4: base color layers (sRGB), sampled with `uv` (scaled).\n // - tex5..8: tangent-space normal layers (BC5, linear), sampled with `uv`.\n // - tex9..12: ORM layers (AO, Roughness, Metal in RGB, linear). Alpha optionally stores height for height-blend.\n //\n let p0 = mat.params4[0];\n let p1 = mat.params4[1];\n let p2 = mat.params4[2];\n let legacy = (p2.x == 0.0) && all(p0.yzw == vec3<f32>(0.0)) && all(p1 == vec4<f32>(0.0));\n let uv_scale = select(select(p2.x, p0.x, legacy), 1.0, (p2.x == 0.0) && !legacy);\n let uv_splat = in.uv;\n let uv = in.uv * uv_scale;\n\n // If user only sets param0, treat it as a scalar multiplier (not red-only).\n let base_rgb = select(vec3<f32>(p0.x), p0.xyz, (p0.y != 0.0) || (p0.z != 0.0));\n let base_a = select(1.0, p0.w, p0.w != 0.0);\n let base_mul = select(vec4<f32>(base_rgb, base_a), vec4<f32>(1.0), legacy);\n\n // If user only sets param4, treat it as a scalar normal XY scale (and keep blend=1).\n let n_xy = select(vec2<f32>(p1.x), p1.xy, p1.y != 0.0);\n let n_blend = select(1.0, p1.z, p1.z != 0.0);\n let normal_int = select(vec4<f32>(n_xy.x, n_xy.y, n_blend, 0.0), vec4<f32>(1.0, 1.0, 1.0, 0.0), legacy);\n\n let normal_only = p2.y > 0.5;\n\n // Base + tangent normal\n var base: vec4<f32>;\n var n_ts: vec3<f32>;\n var orm_rgb: vec3<f32>;\n var heights: vec4<f32>;\n if normal_only {\n base = vec4<f32>(0.5, 0.5, 0.5, 1.0);\n let n5 = decode_normal_bc5(textureSampleBias(tex5, tex_sampler, uv, camera.mip_bias));\n let len2 = dot(n5, n5);\n n_ts = select(vec3<f32>(0.0, 0.0, 1.0), n5 * inverseSqrt(len2), len2 > 1e-10);\n orm_rgb = vec3<f32>(1.0, 0.9, 0.0);\n heights = vec4<f32>(0.0);\n } else {\n // Terrain is always opaque (no alpha-cutoff path for terrain).\n // In the main renderer, emissive is encoded in ORM alpha (`orm.a`), not basecolor alpha.\n // Some marketplace basecolor textures can come through with alpha=0 / 1-bit alpha artifacts\n // (especially BC formats). If we pass that through, terrain can look \"invisible\" even though\n // it's being drawn. Force alpha=1.0 to keep terrain contributing to the gbuffer reliably.\n // Splatmap should not be affected by uv_scale (only layer textures tile).\n let tex0_sample = textureSample(tex0, tex_sampler, uv_splat); // splat weights: no mip bias\n let tex1_sample = textureSampleBias(tex1, tex_sampler, uv, camera.mip_bias);\n let tex2_sample = textureSampleBias(tex2, tex_sampler, uv, camera.mip_bias);\n let tex3_sample = textureSampleBias(tex3, tex_sampler, uv, camera.mip_bias);\n let tex4_sample = textureSampleBias(tex4, tex_sampler, uv, camera.mip_bias);\n // Per-layer ORM (tex9-tex12): rgb = (AO, Roughness, Metal), alpha optionally contains height for height-blend.\n let orm1 = textureSampleBias(tex9, tex_sampler, uv, camera.mip_bias);\n let orm2 = textureSampleBias(tex10, tex_sampler, uv, camera.mip_bias);\n let orm3 = textureSampleBias(tex11, tex_sampler, uv, camera.mip_bias);\n let orm4 = textureSampleBias(tex12, tex_sampler, uv, camera.mip_bias);\n heights = vec4<f32>(orm1.a, orm2.a, orm3.a, orm4.a);\n\n // Height-based blend:\n // Use the alpha channel as a relative height signal; if all heights are equal it reduces to plain splat weights.\n var w0 = vec4<f32>(tex0_sample.rgb, 0.0);\n w0 = w0 / max(dot(w0, vec4<f32>(1.0)), 1e-6);\n // Height blend strength: lower reduces \"winner-take-all\" striping from noisy height/alpha.\n let height_str = 1.5;\n let hmax = max(max(heights.x, heights.y), max(heights.z, heights.w));\n let w_h = max(w0 + (heights - vec4<f32>(hmax)) * height_str, vec4<f32>(0.0));\n let wsum_h = dot(w_h, vec4<f32>(1.0));\n let w = select(w0 / max(dot(w0, vec4<f32>(1.0)), 1e-6), w_h / max(wsum_h, 1e-6), wsum_h > 1e-6);\n\n base = vec4<f32>(w.x * tex1_sample.rgb, 1.0)\n + vec4<f32>(w.y * tex2_sample.rgb, 1.0)\n + vec4<f32>(w.z * tex3_sample.rgb, 1.0)\n + vec4<f32>(w.w * tex4_sample.rgb, 1.0);\n\n // Tangent-space normals (tex5-tex8), blended by splat weights (tex0).\n let n5 = decode_normal_bc5(textureSampleBias(tex5, tex_sampler, uv, camera.mip_bias));\n let n6 = decode_normal_bc5(textureSampleBias(tex6, tex_sampler, uv, camera.mip_bias));\n let n7 = decode_normal_bc5(textureSampleBias(tex7, tex_sampler, uv, camera.mip_bias));\n let n8 = decode_normal_bc5(textureSampleBias(tex8, tex_sampler, uv, camera.mip_bias));\n let n_ts_raw = (w.x * n5 + w.y * n6 + w.z * n7 + w.w * n8);\n let n_ts_len2 = dot(n_ts_raw, n_ts_raw);\n n_ts = select(vec3<f32>(0.0, 0.0, 1.0), n_ts_raw * inverseSqrt(n_ts_len2), n_ts_len2 > 1e-10);\n\n // Blend ORM the same way as base/normal.\n orm_rgb = w.x * orm1.rgb + w.y * orm2.rgb + w.z * orm3.rgb + w.w * orm4.rgb;\n }\n\n // Apply base intensity after either base path.\n base *= base_mul;\n\n // Apply normal intensity in tangent space: scale XY and blend back to flat (0,0,1).\n {\n let xy = n_ts.xy * normal_int.xy;\n let z = sqrt(max(1.0 - dot(xy, xy), 0.0));\n let n_scaled = vec3<f32>(xy, z);\n n_ts = normalize(mix(vec3<f32>(0.0, 0.0, 1.0), n_scaled, clamp(normal_int.z, 0.0, 1.0)));\n }\n\n // Match gbuffer convention: store world normal directly.\n let n_geo = ori_terrain_geo_normal(in.uv, in.world_n, in.detail_w);\n // Re-orthogonalize the interpolated tangent frame against the per-pixel normal (same\n // handedness rule as the VS: keep b aligned with the interpolated bitangent).\n var t = normalize(in.world_t - n_geo * dot(n_geo, in.world_t));\n var b = cross(n_geo, t);\n if (dot(b, in.world_b) < 0.0) { b = -b; t = cross(b, n_geo); }\n let tbn = mat3x3<f32>(t, b, n_geo);\n let n = normalize(tbn * n_ts);\n\n // ORM: AO/Rough/Metal; ORM alpha is reserved for emissive in this renderer, so keep it 0 for terrain.\n let orm = orm_rgb;\n\n var out: FsOut;\n out.out_base = base;\n if (terrain.debug_lod_view > 0.5) {\n out.out_base = vec4<f32>(\n mix(out.out_base.rgb, ori_lod_tier_color(in.quad_step), 0.65), out.out_base.a);\n }\n out.out_normal = vec4<f32>(n, 1.0);\n // IMPORTANT: in this renderer, ORM alpha is used as emissive intensity (see `shaders/gbuffer.wgsl`).\n // Terrain should not be emissive by default.\n out.out_orm = vec4<f32>(orm, 0.0);\n out.out_velocity = gbuffer_velocity(in.cur_clip, in.prev_clip);\n // Linear depth (fog/SSAO input):\n // - Must match `shaders/gbuffer.wgsl` encoding: `clip_position.z / clip_position.w`.\n return out;\n}\n"},{"label":"terrain_fragment_pick","code":"// terrain_pick.wgsl\n// Fragment shader for terrain ID picking. Uses the same terrain vertex output as `terrain_vertex.wgsl`.\n\nstruct PickUniform {\n pick_id: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n@group(2) @binding(0) var<uniform> pick: PickUniform;\n\nstruct VsOut {\n @location(0) world_pos: vec3<f32>,\n @location(1) world_n: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) ndc_z: f32,\n @location(4) local_pos: vec3<f32>,\n @location(5) local_n: vec3<f32>,\n @location(6) world_t: vec3<f32>,\n @location(7) world_b: vec3<f32>,\n};\n\n@fragment\nfn fs_main(@builtin(position) clip_position: vec4<f32>, in: VsOut) -> @location(0) u32 {\n return pick.pick_id;\n}\n"},{"label":"terrain_tile_bake","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// terrain_tile_bake.wgsl - bakes display-height tiles into the shared rg32float atlas.\n// R = full display height, G = parent-LOD-equivalent height (one less detail octave), both\n// terrain-local meters. One workgroup-z layer per queued tile (batched per terrain asset).\n// The display-height function comes from terrain_display_height.wgsl (concatenated at pipeline\n// build) and is shared with grass spawn so blades sit exactly on the rendered surface.\n\nstruct BakeUniform {\n min_z: f32,\n max_z: f32,\n cell_m: f32,\n tile_count: u32,\n // SetTerrainErosionDetailPercent / 100; must match the grass-spawn value (shared surface).\n erosion_detail: f32,\n slots_dim: f32, // atlas slots per row (tile_minmax addressing)\n _pad1: f32,\n _pad2: f32,\n // Display-extension generator (radius_cells 0 = off); see ori_ext_terrain.wgsl.\n ext: OriExtParams,\n};\nstruct BakeTile {\n tile_q: vec2<i32>, // level-0 quad coords of tile sample (0,0)\n level: u32, // texel step = 2^level level-0 quads\n atlas_xy: u32, // packed atlas texel origin (x | y<<16) of texel (0,0) = sample (-1,-1)\n};\n@group(0) @binding(0) var height_tex: texture_2d<u32>;\n@group(0) @binding(1) var<uniform> bake: BakeUniform;\n@group(0) @binding(2) var<storage, read> bake_tiles: array<BakeTile>;\n@group(0) @binding(3) var atlas: texture_storage_2d<rg32float, write>;\n// Per-slot height bounds for the Hi-Z terrain cull: [slot*2] = min, [slot*2+1] = max, both\n// in the ORDERABLE u32 encoding below (terrain-local meters, R and G channels enveloped).\n// The CPU resets a slot's pair to the empty accumulator when queueing its bake.\n@group(0) @binding(4) var<storage, read_write> tile_minmax: array<atomic<u32>>;\n\n// Order-preserving f32 -> u32 mapping (works for both signs): monotone under u32 compare.\nfn ori_orderable_f32(h: f32) -> u32 {\n let b = bitcast<u32>(h);\n return select(b ^ 0x80000000u, ~b, (b & 0x80000000u) != 0u);\n}\n\n// Keep in sync with terrain_tiles.rs (TILE_TEXELS / SUBDIV).\nconst TDH_TILE_TEXELS: u32 = 35u; // 33 samples + 1 apron texel on each side (VS normals/morph)\nconst TDH_SUBDIV: f32 = 4.0; // level-0 quads per sim cell\nconst TDH_SUBDIV_LOG2: i32 = 2; // level 2 == sim grid resolution\n\n@compute @workgroup_size(8, 8, 1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.z >= bake.tile_count || gid.x >= TDH_TILE_TEXELS || gid.y >= TDH_TILE_TEXELS) {\n return;\n }\n let t = bake_tiles[gid.z];\n let texel = vec2<i32>(gid.xy) - vec2<i32>(1, 1); // -1 = apron\n let q = vec2<f32>(t.tile_q + (texel << vec2<u32>(t.level, t.level)));\n let s = q / TDH_SUBDIV; // sim-grid coords; the display fn clamps to the grid (edge apron)\n let octaves = max(0, TDH_SUBDIV_LOG2 - i32(t.level));\n let step_cells = f32(1u << t.level) / TDH_SUBDIV;\n let hs = ori_terrain_display_height(height_tex, s, bake.min_z, bake.max_z, bake.cell_m, octaves, bake.erosion_detail, bake.ext, step_cells);\n let a = vec2<i32>(i32(t.atlas_xy & 0xffffu), i32(t.atlas_xy >> 16u)) + vec2<i32>(gid.xy);\n textureStore(atlas, a, vec4<f32>(hs.h, hs.h_coarse, 0.0, 0.0));\n // Envelope both channels: geomorph blends the mesh between them at runtime.\n let slot = 2u * ((t.atlas_xy >> 16u) / TDH_TILE_TEXELS * u32(bake.slots_dim)\n + (t.atlas_xy & 0xffffu) / TDH_TILE_TEXELS);\n atomicMin(&tile_minmax[slot], ori_orderable_f32(min(hs.h, hs.h_coarse)));\n atomicMax(&tile_minmax[slot + 1u], ori_orderable_f32(max(hs.h, hs.h_coarse)));\n}\n\n// ORI_TDH_V1 - shared display-surface height for terrain tile bake + grass spawn.\n// Display surface = Catmull-Rom upsample of the sim heightfield (correction clamped to\n// +-ORI_TDH_CR_CLAMP_M vs bilinear = the sim collision surface) + relief-scaled band-limited\n// erosion-filter detail (slope-aligned gullies; ori_erosion_filter.wgsl, appended at module\n// build). Pure function of (heights texture, sim-grid position): every display consumer must\n// sample through this or grass floats/sinks vs the rendered ground.\n// Requires the ori noise prelude (preprocess_wgsl injects it).\nconst ORI_TDH_CR_CLAMP_M: f32 = 0.25;\nconst ORI_TDH_DETAIL_SEED: u32 = 31337u;\n// Erosion detail parameters (reference defaults from the Advanced Terrain Erosion Filter).\n// Octave o has frequency ~(1/SCALE_CELLS)*2^o in sim-cell units; octave count is picked per LOD\n// so baked octaves stay band-limited, finer LODs add finer octaves.\nconst ORI_TDH_EROSION_SCALE_CELLS: f32 = 1.1; // first-octave gully features span ~1 sim cell\nconst ORI_TDH_EROSION_STRENGTH: f32 = 0.22;\nconst ORI_TDH_EROSION_GULLY_WEIGHT: f32 = 0.5; // 0 = sharp peaks, no gullies; 1 = full gullies\nconst ORI_TDH_EROSION_DETAIL: f32 = 1.5; // lower = fine gullies only on steep slopes\nconst ORI_TDH_EROSION_CELL_SCALE: f32 = 0.7;\nconst ORI_TDH_EROSION_NORMALIZATION: f32 = 0.5;\nconst ORI_TDH_EROSION_ROUNDING: vec4<f32> = vec4<f32>(0.1, 0.0, 0.1, 2.0); // ridge/crease/in/oct\nconst ORI_TDH_EROSION_ONSET: vec4<f32> = vec4<f32>(1.25, 1.25, 2.8, 1.5);\nconst ORI_TDH_EROSION_ASSUMED_SLOPE: vec2<f32> = vec2<f32>(0.7, 1.0);\n\nstruct OriTdhSample {\n h: f32, // terrain-local display height (meters; same space as decode_height)\n h_coarse: f32, // same with one less detail octave = the parent LOD's value (geomorph target)\n grad: vec2<f32>, // d(h)/d(sim cell x/y) of the bilinear base (no detail; good enough for tilt)\n};\n\nfn ori_tdh_decode(v: u32, min_z: f32, max_z: f32) -> f32 {\n return min_z + (f32(v & 65535u) / 65535.0) * (max_z - min_z);\n}\n\nfn ori_tdh_cr_weights(t: f32) -> vec4<f32> {\n let t2 = t * t;\n let t3 = t2 * t;\n return vec4<f32>(\n 0.5 * (-t3 + 2.0 * t2 - t),\n 0.5 * (3.0 * t3 - 5.0 * t2 + 2.0),\n 0.5 * (-3.0 * t3 + 4.0 * t2 + t),\n 0.5 * (t3 - t2),\n );\n}\n\n// s_in: sample position in sim-grid coords (0..sx-1, 0..sy-1), clamped inside.\n// detail_octaves: 0 = pure Catmull-Rom (sim-res and coarser LODs), up to 2 at the finest LODs\n// and for grass placement. Octave freqs/amps depend only on the sim grid (not the LOD), so a\n// parent tile's value at a shared point equals the child's h_coarse there (pop-free morphing).\n// Display-extension blend band (cells outside the grid over which the procedural height\n// eases in from the edge-clamped real height, hiding f32-vs-Fp generator mismatch).\nconst ORI_TDH_EXT_BLEND_CELLS: f32 = 16.0;\n\n// detail_strength: SetTerrainErosionDetailPercent / 100 (1.0 = reference defaults, 0 = smooth).\n// Every consumer (tile bake, grass spawn) must pass the same value or grass floats/sinks.\n// ext: display-extension generator (OriExtParams from ori_ext_terrain.wgsl, appended\n// alongside this file); radius_cells 0 = disabled. Samples outside the sim grid evaluate the\n// procedural generator, eased from the clamped edge height over ORI_TDH_EXT_BLEND_CELLS.\n// sample_step_cells: sim-cell spacing between consecutive samples of the CALLER (tile texel\n// step for bakes, spawn spacing for grass) - band-limits the extension generator per LOD.\nfn ori_terrain_display_height(\n ht: texture_2d<u32>, s_in: vec2<f32>, min_z: f32, max_z: f32, cell_m: f32, detail_octaves: i32,\n detail_strength: f32, ext: OriExtParams, sample_step_cells: f32,\n) -> OriTdhSample {\n let dims = vec2<i32>(textureDimensions(ht));\n let s = clamp(s_in, vec2<f32>(0.0), vec2<f32>(f32(dims.x - 1), f32(dims.y - 1)));\n let x0 = i32(floor(s.x));\n let y0 = i32(floor(s.y));\n let fx = s.x - f32(x0);\n let fy = s.y - f32(y0);\n\n // 4x4 clamped patch around the bilinear cell (16 taps; also feeds the relief estimate).\n var p: array<vec4<f32>, 4>;\n var pmin = 1e30;\n var pmax = -1e30;\n for (var j = 0; j < 4; j = j + 1) {\n let yy = clamp(y0 - 1 + j, 0, dims.y - 1);\n var row = vec4<f32>(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let xx = clamp(x0 - 1 + i, 0, dims.x - 1);\n let h = ori_tdh_decode(textureLoad(ht, vec2<i32>(xx, yy), 0).r, min_z, max_z);\n row[i] = h;\n pmin = min(pmin, h);\n pmax = max(pmax, h);\n }\n p[j] = row;\n }\n\n // Bilinear base = the sim collision surface.\n let h00 = p[1].y;\n let h10 = p[1].z;\n let h01 = p[2].y;\n let h11 = p[2].z;\n let bil = mix(mix(h00, h10, fx), mix(h01, h11, fx), fy);\n var out: OriTdhSample;\n out.grad = vec2<f32>(\n mix(h10 - h00, h11 - h01, fy),\n mix(h01 - h00, h11 - h10, fx),\n );\n\n // Recipe display mode: the #terrain generator is the display content EVERYWHERE - no\n // Catmull-Rom, no detail octaves, no blend band, no grid boundary. Only enabled after the\n // divergence check verified sim heights == generator, so `grad` (grass tilt only) can stay\n // the sim-patch bilinear gradient above.\n if (ext.recipe_mode > 0.5) {\n let eh = ori_ext_height(s_in, ext, sample_step_cells);\n out.h = eh.x;\n out.h_coarse = eh.y;\n return out;\n }\n\n // Separable Catmull-Rom; clamp the correction so the display surface never leaves the\n // collision surface by more than ORI_TDH_CR_CLAMP_M on extreme terrain.\n let wx = ori_tdh_cr_weights(fx);\n var wy = ori_tdh_cr_weights(fy); // var: naga rejects dynamic indexing of let vectors\n var cr = 0.0;\n for (var j = 0; j < 4; j = j + 1) { cr = cr + wy[j] * dot(p[j], wx); }\n let base = bil + clamp(cr - bil, -ORI_TDH_CR_CLAMP_M, ORI_TDH_CR_CLAMP_M);\n out.h = base;\n out.h_coarse = base;\n\n // Relief-scaled erosion detail: flats stay flat, amplitude follows the local 4x4 height\n // range. Amplitude/frequency/fade depend only on sim-grid position, never on the LOD level,\n // and the filter's (octaves-1) prefix equals the parent LOD's value (pop-free morphing).\n if (detail_octaves > 0 && detail_strength > 0.0) {\n let range = pmax - pmin;\n let amp = min(range * 0.10, cell_m * 0.35) * smoothstep(0.02, 0.25, range) * detail_strength;\n if (amp > 0.0) {\n // Cell units on both axes (slope = rise/run per cell) so the reference defaults apply.\n let slope_cells = out.grad / max(cell_m, 1e-6);\n // Altitude fade from the local patch: carve toward -1 near the patch low, sharpen\n // toward +1 near the patch high (the filter's V-valley / crisp-ridge ingredient).\n let fade = ((bil - pmin) / max(range, 1e-6)) * 2.0 - 1.0;\n let e = ori_erosion_filter(\n s, vec3<f32>(0.0, slope_cells), fade,\n ORI_TDH_EROSION_STRENGTH, ORI_TDH_EROSION_GULLY_WEIGHT, ORI_TDH_EROSION_DETAIL,\n ORI_TDH_EROSION_ROUNDING, ORI_TDH_EROSION_ONSET, ORI_TDH_EROSION_ASSUMED_SLOPE,\n ORI_TDH_EROSION_SCALE_CELLS, min(detail_octaves, 2), 2.0,\n 0.5, ORI_TDH_EROSION_CELL_SCALE, ORI_TDH_EROSION_NORMALIZATION, ORI_TDH_DETAIL_SEED,\n );\n // Normalize deltas to ~[-1,1] and scale by the relief amplitude. magnitude_coarse is 0\n // when only one octave runs: h_coarse then stays the pure-CR base = the parent's value.\n out.h = base + amp * (e.delta / max(e.magnitude, 1e-6));\n out.h_coarse = base + amp * (e.delta_coarse / max(e.magnitude_coarse, 1e-6));\n }\n }\n\n // Display extension: outside the sim grid, ease into the procedural generator. Everything\n // above evaluated at the CLAMPED edge sample (and the detail amp decays to 0 there since\n // the clamped 4x4 patch flattens), so C0 continuity at the boundary is by construction.\n if (ext.radius_cells > 0.0) {\n let over = max(vec2<f32>(0.0) - s_in, s_in - vec2<f32>(f32(dims.x - 1), f32(dims.y - 1)));\n let out_d = max(max(over.x, over.y), 0.0);\n if (out_d > 0.0) {\n // The block renders its FULL-res sim content at every tile LOD (Catmull-Rom of the\n // heights texture), but the generator band-limits per LOD - at the grid boundary a\n // coarse ext tile would be missing the finest erosion/base octaves the adjacent block\n // tile still shows (= a shelf running along the grid edge). Force full detail near the\n // grid and fade to the true per-LOD band limit deep into the extension; the band\n // weights are smooth in the step, so content stays continuous along the fade.\n let step_eff = mix(min(sample_step_cells, 0.25), sample_step_cells,\n smoothstep(ORI_TDH_EXT_BLEND_CELLS, 272.0, out_d));\n let eh = ori_ext_height(s_in, ext, step_eff);\n let t = smoothstep(0.0, ORI_TDH_EXT_BLEND_CELLS, out_d);\n out.h = mix(out.h, eh.x, t);\n out.h_coarse = mix(out.h_coarse, eh.y, t);\n }\n }\n return out;\n}\n\n// ORI_EROSION_FILTER_V1 - stateless erosion-style noise: stripe \"gullies\" aligned to the local\n// slope, stacked over octaves with fade targets so valleys carve V-shaped and peaks sharpen.\n// Port of \"Phacelle Noise\" + \"Advanced Terrain Erosion Filter\" by Rune Skovbo Johansen\n// (https://www.shadertoy.com/view/wXcfWn, technique lineage: Clay John, Fewes). Deviations from\n// the reference: the float-fract hash is replaced with the engine lattice hash (ori_hash2_cell,\n// noise prelude) plus a seed, and the octave loop also returns the one-less-octave result\n// (the loop is prefix-stable) for LOD geomorph targets. Sim-side Fp mirror: fp_erosion.rs.\n// Appended after terrain_display_height.wgsl at module build; requires the ori noise prelude.\n//\n// Phacelle Noise and Advanced Terrain Erosion Filter copyright (c) 2025 Rune Skovbo Johansen.\n// This Source Code Form is subject to the terms of the Mozilla Public\n// License, v. 2.0. If a copy of the MPL was not distributed with this\n// file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nconst ORI_EROSION_TAU: f32 = 6.28318530717959;\n\n// Two [-1,1] channels from one engine lattice hash (replaces the reference's float-fract hash).\nfn ori_erosion_hash2(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h & 0xffffu), f32(h >> 16u)) * (2.0 / 65536.0) - 1.0;\n}\n\nfn ori_pow_inv(t: f32, power: f32) -> f32 {\n return 1.0 - pow(1.0 - clamp(t, 0.0, 1.0), power);\n}\nfn ori_ease_out(t: f32) -> f32 {\n let v = 1.0 - clamp(t, 0.0, 1.0);\n return 1.0 - v * v;\n}\nfn ori_smooth_start(t: f32, smoothing: f32) -> f32 {\n if (t >= smoothing) { return t - 0.5 * smoothing; }\n return 0.5 * t * t / max(smoothing, 1e-6);\n}\nfn ori_safe_normalize2(n: vec2<f32>) -> vec2<f32> {\n let l = length(n);\n if (l > 1e-10) { return n / l; }\n return n;\n}\n\n// Phacelle (\"phase cell\") noise: a stripe pattern aligned with norm_dir, blended from cosine and\n// sine waves anchored at 4x4 jittered cell points (Worley-style) so stripes stay continuous\n// across cells. norm_dir must be normalized; freq is stripes per cell (keep near 1); offset01 is\n// the phase offset in cycles; normalization (0..1) limits how much small magnitudes get boosted.\n// Returns xy = normalized cos/sin waves, zw = side dir (multiply onto sin for cos derivatives).\nfn ori_phacelle_noise(p: vec2<f32>, norm_dir: vec2<f32>, freq: f32, offset01: f32,\n normalization: f32, seed: u32) -> vec4<f32> {\n let side_dir = vec2<f32>(-norm_dir.y, norm_dir.x) * freq * ORI_EROSION_TAU;\n let offset = offset01 * ORI_EROSION_TAU;\n let p_int = floor(p);\n let p_frac = p - p_int;\n var phase_dir = vec2<f32>(0.0);\n var weight_sum = 0.0;\n for (var i = -1; i <= 2; i = i + 1) {\n for (var j = -1; j <= 2; j = j + 1) {\n let grid_offset = vec2<f32>(f32(i), f32(j));\n let cell = vec2<i32>(p_int + grid_offset);\n let random_offset = ori_erosion_hash2(cell, seed) * 0.5;\n let from_cell_point = p_frac - grid_offset - random_offset;\n let sqr_dist = dot(from_cell_point, from_cell_point);\n // Bell weight: 1 at dist 0; the -0.01111 makes it exactly 0 at 1.5 (the farthest any\n // contributing cell point can be), avoiding subtle grid-line artifacts.\n let weight = max(0.0, exp(-sqr_dist * 2.0) - 0.01111);\n weight_sum += weight;\n let wave_input = dot(from_cell_point, side_dir) + offset;\n phase_dir += vec2<f32>(cos(wave_input), sin(wave_input)) * weight;\n }\n }\n let interpolated = phase_dir / max(weight_sum, 1e-6);\n let magnitude = max(1.0 - normalization, length(interpolated));\n return vec4<f32>(interpolated / magnitude, side_dir);\n}\n\nstruct OriErosionResult {\n delta: f32, // height delta after all octaves (same height units as the input slope)\n delta_coarse: f32, // same with one less octave = the parent LOD's value (geomorph target)\n ridge: f32, // ridge map: -1 on creases .. 1 on ridges (drainage/splat masks)\n magnitude: f32, // sum of octave strengths; delta/magnitude is a ~[-1,1] value\n magnitude_coarse: f32, // 0 when octaves == 1 (parent is the unmodified base)\n};\n\n// Faithful port of ErosionFilter (see file header for parameter docs in the reference).\n// p and the height axis must share units: height_and_slope = (height, d(height)/d(p)).\n// scale/strength are in those units; scale must not vary per pixel.\nfn ori_erosion_filter(\n p: vec2<f32>, height_and_slope: vec3<f32>, fade_target_in: f32,\n strength_in: f32, gully_weight: f32, detail: f32,\n rounding: vec4<f32>, onset: vec4<f32>, assumed_slope: vec2<f32>,\n scale: f32, octaves: i32, lacunarity: f32,\n gain: f32, cell_scale: f32, normalization: f32, seed: u32,\n) -> OriErosionResult {\n var strength = strength_in * scale;\n var fade_target = clamp(fade_target_in, -1.0, 1.0);\n var hs = height_and_slope;\n var freq = 1.0 / (scale * cell_scale);\n let slope_len = max(length(hs.yz), 1e-10);\n var out: OriErosionResult;\n out.delta_coarse = 0.0;\n out.magnitude = 0.0;\n out.magnitude_coarse = 0.0;\n var rounding_mult = 1.0;\n let rounding_for_input =\n mix(rounding.y, rounding.x, clamp(fade_target + 0.5, 0.0, 1.0)) * rounding.z;\n // Accumulating mask: initial slope first, then the slope of each octave too.\n var combi_mask = ori_ease_out(ori_smooth_start(slope_len * onset.x, rounding_for_input * onset.x));\n var ridge_combi_mask = ori_ease_out(slope_len * onset.z);\n var ridge_fade_target = fade_target;\n // Gully direction source: actual slope mixed toward an assumed slope magnitude.\n var gully_slope = mix(hs.yz, hs.yz / slope_len * assumed_slope.x, assumed_slope.y);\n\n for (var i = 0; i < octaves; i = i + 1) {\n if (i == octaves - 1) {\n // The loop is prefix-stable (octave i only reads state from octaves < i), so the\n // accumulation so far IS the (octaves-1) result = the parent LOD's value.\n out.delta_coarse = hs.x - height_and_slope.x;\n out.magnitude_coarse = out.magnitude;\n }\n var phacelle = ori_phacelle_noise(\n p * freq, ori_safe_normalize2(gully_slope), cell_scale, 0.25, normalization,\n seed ^ (u32(i) * 0x9e3779b9u));\n // p was multiplied by freq; negate since slope directions point downhill.\n phacelle = vec4<f32>(phacelle.xy, phacelle.zw * -freq);\n let sloping = abs(phacelle.y);\n // Add non-masked normalized slope for subsequent octave directions (steepest wave part).\n gully_slope += sign(phacelle.y) * phacelle.zw * strength * gully_weight;\n // Gullies: height offset (-1..1) in x, derivative in yz; fade toward fade_target by mask.\n let gullies = vec3<f32>(phacelle.x, phacelle.y * phacelle.zw);\n let faded_gullies = mix(vec3<f32>(fade_target, 0.0, 0.0), gullies * gully_weight, combi_mask);\n hs += faded_gullies * strength;\n out.magnitude += strength;\n fade_target = faded_gullies.x;\n // Fold the new octave into the mask (and the ridge-map variants).\n let rounding_for_octave =\n mix(rounding.y, rounding.x, clamp(phacelle.x + 0.5, 0.0, 1.0)) * rounding_mult;\n let new_mask = ori_ease_out(ori_smooth_start(sloping * onset.y, rounding_for_octave * onset.y));\n combi_mask = ori_pow_inv(combi_mask, detail) * new_mask;\n ridge_fade_target = mix(ridge_fade_target, gullies.x, ridge_combi_mask);\n ridge_combi_mask = ridge_combi_mask * ori_ease_out(sloping * onset.w);\n strength *= gain;\n freq *= lacunarity;\n rounding_mult *= rounding.w;\n }\n out.ridge = ridge_fade_target * (1.0 - ridge_combi_mask);\n out.delta = hs.x - height_and_slope.x;\n return out;\n}\n\n// ============================================================================================\n// Terrain display-extension generator: GPU twin of the #terrain recipe composition\n// (gradient-fbm base -> height mask -> OriErosion directional erosion). Used by the terrain\n// DISPLAY EXTENSION to continue a sim heightfield procedurally beyond the collision grid\n// (render-only, no sim state), and as the whole display surface in recipe mode.\n// Erosion kernel spec: docs/ori_erosion_spec.md; Fp sim twin: fp_ori_erosion.rs (keep in\n// sync). Requires the ori noise prelude (ori_hash2_cell).\n//\n// EXACTNESS CONTRACT: for a block built with TerrainHeightsNoise mode 4 (+ MaskFromHeight\n// + the OriErosion op / TerrainHeightsFromTerrain), this generator reproduces the sim field\n// bit-approximately (f32 vs Fp rounding only): same base fbm (seed + octave, gain, /total\n// normalization), same erosion dir recovery (central diff at +-1 cell,\n// n_dd = dh * base_scale_cells / W), same octave seed stream (seed ^ 0x9e3779b9 * (i+1)),\n// same jitter/gradient lattice hashes, same polynomial window.\n// ============================================================================================\n\nconst ORI_EXT_TAU: f32 = 6.28318530717959;\n\n// Gradient vector in [-1,1)^2 from one lattice hash (top/bottom 16 bits) - matches\n// fp_noise::gradient_at (Fp) at every lattice cell.\nfn ori_ext_gradient_at(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 32768.0 - 1.0;\n}\n\n// Gradient (Perlin) noise, zero at integer lattice points, quintic fade - the WGSL twin of\n// fp_noise::gradient2 (same hash family; f32 vs Fp rounding differs at sub-cm level).\nfn ori_ext_gradient2(p: vec2<f32>, seed: u32) -> f32 {\n let i = floor(p);\n let f = p - i;\n let c = vec2<i32>(i);\n let ga = ori_ext_gradient_at(c, seed);\n let gb = ori_ext_gradient_at(c + vec2<i32>(1, 0), seed);\n let gc = ori_ext_gradient_at(c + vec2<i32>(0, 1), seed);\n let gd = ori_ext_gradient_at(c + vec2<i32>(1, 1), seed);\n let va = dot(ga, f);\n let vb = dot(gb, f - vec2<f32>(1.0, 0.0));\n let vc = dot(gc, f - vec2<f32>(0.0, 1.0));\n let vd = dot(gd, f - vec2<f32>(1.0, 1.0));\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let a = va + (vb - va) * u.x;\n let b = vc + (vd - vc) * u.x;\n return a + (b - a) * u.y;\n}\n\n// OriErosion feature point offset inside a cell: 0.5 + jitter per axis, jitter in\n// [-0.45, 0.45) - twin of fp_ori_erosion::feature_offset.\nfn ori_ext_feature_offset(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(0.5) + (vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 65536.0 - 0.5) * 0.9;\n}\n\n// One OriErosion directional wave sample (value, ddx, ddy) - twin of\n// fp_ori_erosion::ori_erosion_sample (spec section 3): 5x5 jittered feature lattice,\n// polynomial window (1 - d2/4)^8 with EXACT zero support at R = 2 (the d2 >= 4 skip\n// drops only exact zeros - no truncation seams; the bell matches the reference\n// exp(-2 d2) including its tail, so grooves stay correlated across neighboring cells\n// and flow smoothly into each other), 1/tau-convention gradient along dir.\nfn ori_ext_erosion_sample(p: vec2<f32>, dir: vec2<f32>, seed: u32) -> vec3<f32> {\n let ip = floor(p);\n let fp_ = p - ip;\n let ci = vec2<i32>(ip);\n var v = 0.0;\n var g = vec2<f32>(0.0);\n var wt = 0.0;\n for (var i = -2; i <= 2; i = i + 1) {\n for (var k = -2; k <= 2; k = k + 1) {\n let q = ori_ext_feature_offset(ci + vec2<i32>(i, k), seed);\n let r = fp_ - vec2<f32>(f32(i), f32(k)) - q;\n let d2 = dot(r, r);\n if (d2 >= 4.0) { continue; }\n let t = 1.0 - d2 * 0.25;\n // Same x64-scaled form as the Fp twin (w = (8 t^4)^2 = 64 t^8); the scale cancels\n // in the weighted average, and matching the arithmetic shape keeps rounding aligned.\n let t2 = t * t;\n let t4s = t2 * t2 * 8.0;\n let w = t4s * t4s;\n wt = wt + w;\n // Phase wrapped to one period before the tau multiply (exact: cos period 1) - keeps\n // the trig argument in [0, tau) on both twins, bounding f32-vs-Fp drift (see the Fp\n // twin in fp_ori_erosion.rs).\n // Phase snapped to the exact 1/1024 grid (floor), twin of fp_ori_erosion: both\n // sides evaluate the SAME cos bucket, so residual positional drift cannot reach\n // the height output.\n let phase = floor(fract(dot(r, dir)) * 1024.0) / 1024.0 * ORI_EXT_TAU;\n v = v + cos(phase) * w;\n // 1/tau gradient convention, twin of fp_ori_erosion (branch_strength absorbs it;\n // keeps feedback magnitudes O(1) so positional Fp-vs-f32 drift stays bounded).\n g = g - sin(phase) * w * dir;\n }\n }\n // wt > 0 by construction: the own-cell feature is within sqrt(1.805) < R = 2 of any\n // sample (locked by fp_ori_erosion support tests).\n return vec3<f32>(v, g) / wt;\n}\n\n// Display-extension generator params (zeroed radius = disabled; see BakeUniform).\n// 20 f32 = 80 bytes (uniform address space rounds nested struct sizes to 16). Every Rust\n// fill site is a [f32; 20] in OriExtParams field order - keep them in lockstep.\nstruct OriExtParams {\n radius_cells: f32, // extension radius in sim cells (0 = off)\n base_scale_cells: f32, // base-noise feature size in cells\n base_amp_m: f32, // base amplitude in meters (heights = (fbm*0.5+0.5)*amp)\n base_octaves: f32, // gradient fbm octaves\n base_gain: f32, // per-octave amplitude factor (0.1 near-mono smooth; classic 0.5)\n erosion_scale_cells: f32,\n erosion_octaves: f32,\n erosion_strength_m: f32,\n slope_strength: f32,\n branch_strength: f32,\n mask_start_m: f32, // erosion amplitude mask: smoothstep(start, end, base) like\n mask_end_m: f32, // TerrainHeightsMaskFromHeight on the pre-erosion base\n base_seed: f32,\n erosion_seed: f32, // sim op seed (the scene may use a different stream than the base)\n domain_w_m: f32, // noise unit-domain width in meters (samples_x * cell_m); dir scale\n // > 0.5 = recipe display mode: the generator IS the display content everywhere (inside the\n // sim grid too - no Catmull-Rom, no boundary). Set per asset after the divergence check\n // verifies the sim heights match this recipe (see TerrainPass::ensure_terrain_asset).\n // Keep recipe_mode at flat index 15: terrain_tiles injects it as ext[15] per asset.\n recipe_mode: f32,\n erosion_gain: f32, // per-octave erosion amplitude factor (reference 0.5)\n erosion_lacunarity: f32, // per-octave erosion frequency factor (reference 2)\n _pad_a: f32,\n _pad_b: f32,\n};\n\n// Band-limit weight for one octave: 1 when the octave wavelength is well above the sample\n// spacing, fading to 0 approaching Nyquist. Baked per tile LOD; without this, octaves finer\n// than a coarse ring's spacing alias into DIFFERENT content per ring = straight seams at\n// ring boundaries (and the geomorph can't hide them if h_coarse == h).\nfn ori_ext_band(wavelength_cells: f32, step_cells: f32) -> f32 {\n return smoothstep(2.0, 4.0, wavelength_cells / max(step_cells, 1e-3));\n}\n\n// Base heights (meters, terrain-local like decode_height output). EXACT twin of\n// fp_noise::gradient_fbm2 (freq x2, amp x gain, per-octave seed = seed + i, sum / total) so\n// a sim block built with TerrainHeightsNoise mode 4 continues bit-consistently (f32 vs Fp\n// rounding aside) into the infinite field.\n// Returns (h_m, h_coarse_m): the coarse variant band-limits at 2x the sample spacing = the\n// parent LOD's value, restoring pop-free geomorph for extension tiles. Normalization uses\n// the UNfaded total so amplitude stays consistent with the sim block at any LOD (faded\n// octaves contribute their zero mean).\nfn ori_ext_base(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n let seed = u32(e.base_seed);\n // Division, not s * (1/scale): twin of terrain_gen::noise_heights (the reciprocal's\n // truncation grows with |s| and diverges from the Fp sim on steep recipes).\n let p = s / max(e.base_scale_cells, 1.0);\n var n = 0.0;\n var nc = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var f = 1.0;\n let n_oct = i32(clamp(e.base_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let wl = e.base_scale_cells / f;\n let bf = ori_ext_band(wl, step_cells);\n let bc = ori_ext_band(wl, step_cells * 2.0);\n total = total + amp;\n if (bf > 0.0) {\n let v = ori_ext_gradient2(p * f, seed + u32(i));\n n = n + v * amp * bf;\n nc = nc + v * amp * bc;\n }\n amp = amp * e.base_gain;\n f = f * 2.0;\n }\n n = n / total;\n nc = nc / total;\n return vec2<f32>((n * 0.5 + 0.5) * e.base_amp_m, (nc * 0.5 + 0.5) * e.base_amp_m);\n}\n\n// One band-limited OriErosion accumulation (the octave loop of spec section 4) with its own\n// mask, dir and branch-feedback chain. `step_cells` selects the band weights: evaluating at\n// 2x the step reproduces the PARENT LOD's chain exactly, which is what makes the geomorph\n// identity (child h_coarse == parent h) hold bit-for-bit.\n//\n// The erosion octave is NOT zero-mean: its local mean is ~= the windowed cos average,\n// dc(t) ~= max(0, exp(-K t^2)) with t = |dir| and K = ORI_EROSION_DC_K (numeric fit locked\n// by fp_ori_erosion::tests::dc_fit_matches_constant) - near 1 on flats, ~0 on steep slopes.\n// Band-limited octaves must fade toward that DC, not toward 0, or coarse LOD rings sink by\n// up to 0.5*strength on flats = curved cliff shelves at ring boundaries. Full-detail output\n// (band weights 1) is unchanged by dc.\nfn ori_ext_erosion_chain(pe: vec2<f32>, dir: vec2<f32>, m: f32, e: OriExtParams, step_cells: f32) -> f32 {\n var hx = 0.0;\n var hd = vec2<f32>(0.0);\n var a = 0.5 * m;\n var a_total = 0.0;\n var f = 1.0;\n let seed = u32(e.erosion_seed);\n let n_oct = i32(clamp(e.erosion_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let bf = ori_ext_band(e.erosion_scale_cells / f, step_cells);\n // Direction snapped to the exact 1/256 grid (floor), twin of fp_ori_erosion: the\n // branch-feedback recursion re-synchronizes with the Fp sim every octave instead of\n // compounding f32-vs-Fp drift. Exact: |bd|*256 < 2^20 stays integer-exact in f32.\n let bd = floor((dir + vec2<f32>(hd.y, -hd.x) * e.branch_strength) * 256.0) / 256.0;\n let dc = exp(-4.26 * dot(bd, bd)); // ORI_EROSION_DC_K\n if (bf > 0.0) {\n // Twin of the sim octave seed stream: seed ^ (0x9e3779b9 * (1 + octave)), wrapping.\n let oseed = seed ^ (0x9e3779b9u * (1u + u32(i)));\n let v = ori_ext_erosion_sample(pe * f, bd, oseed);\n hx = hx + mix(dc, v.x, bf) * a;\n hd = hd + v.yz * a * f * bf;\n } else {\n hx = hx + dc * a;\n }\n a_total = a_total + a;\n // Spectrum from the recipe keys (reference 0.5 / 2); clamps twin fp_ori_erosion.\n a = a * clamp(e.erosion_gain, 0.03125, 1.0);\n f = f * clamp(e.erosion_lacunarity, 1.125, 4.0);\n }\n // Amplitude-sum normalization (twin of fp_ori_erosion): output spans [-m, m] at any\n // octave schedule, so erosion_strength means the same carve depth everywhere.\n if (a_total <= 1e-4) { return 0.0; }\n return hx * m / a_total;\n}\n\nfn ori_ext_height(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n return ori_ext_height_user(s, e, step_cells).xy;\n}\n\n// (h, h_coarse, user01): user01 = the erosion accumulation hx * 0.5 + 0.5 = the recipe's\n// erosion/ridge map (the sim op's ErosionMapOut / ctx.user), full-detail chain.\nfn ori_ext_height_user(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec3<f32> {\n let base = ori_ext_base(s, e, step_cells);\n // dir = curl of the recovered slope * slope_strength (dir = (ddy, -ddx) * s), twin of\n // ori_erosion_apply_with_base: dd_scale = base_scale_cells / W (heights and W in meters).\n // .x diffs feed the fine chain; .y (parent-band) diffs feed the coarse chain, exactly\n // like the parent LOD would compute them.\n let dd_scale = e.base_scale_cells / max(e.domain_w_m, 1e-6);\n let be = ori_ext_base(s + vec2<f32>(1.0, 0.0), e, step_cells);\n let bw = ori_ext_base(s - vec2<f32>(1.0, 0.0), e, step_cells);\n let bn = ori_ext_base(s + vec2<f32>(0.0, 1.0), e, step_cells);\n let bs = ori_ext_base(s - vec2<f32>(0.0, 1.0), e, step_cells);\n let dir_f = vec2<f32>((bn.x - bs.x), -(be.x - bw.x)) * dd_scale * e.slope_strength;\n // Division, twin of ori_erosion_apply_with_base (see the coordinate-drift note there).\n let pe = s / max(e.erosion_scale_cells, 1.0);\n // Mask on the pre-erosion base (MaskFromHeight twin; start < end enforced at the fill site).\n let m_f = smoothstep(e.mask_start_m, e.mask_end_m, base.x);\n let hx = ori_ext_erosion_chain(pe, dir_f, m_f, e, step_cells);\n // Coarse chain (geomorph target = the parent LOD's value): only distinct when 2x the\n // step actually band-limits something the fine step does not.\n let n_oct = clamp(e.erosion_octaves, 1.0, 8.0);\n let wl_min_ero = e.erosion_scale_cells / pow(clamp(e.erosion_lacunarity, 1.125, 4.0), n_oct - 1.0);\n let wl_min_base = e.base_scale_cells / exp2(clamp(e.base_octaves, 1.0, 8.0) - 1.0);\n var hxc = hx;\n var base_c = base.x;\n if (ori_ext_band(min(wl_min_ero, wl_min_base), step_cells * 2.0) < 1.0) {\n let dir_c = vec2<f32>((bn.y - bs.y), -(be.y - bw.y)) * dd_scale * e.slope_strength;\n let m_c = smoothstep(e.mask_start_m, e.mask_end_m, base.y);\n hxc = ori_ext_erosion_chain(pe, dir_c, m_c, e, step_cells * 2.0);\n base_c = base.y;\n }\n // The -0.5 carve bias is NOT masked - masked zones sink uniformly (the sim op does the\n // same; the water datum depends on it).\n return vec3<f32>(\n base.x + (hx - 0.5) * e.erosion_strength_m,\n base_c + (hxc - 0.5) * e.erosion_strength_m,\n clamp(hx * 0.5 + 0.5, 0.0, 1.0),\n );\n}\n"},{"label":"terrain_fragment_splat_layers","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n // Parallax occlusion quality 0..1 (0 = off); scales march steps and fade distance.\n parallax_quality: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n// Shared material bodies sample through this; no per-submesh tuning bound in this template.\nfn ori_material_mip_bias() -> f32 { return u_camera.mip_bias; }\n\nstruct TerrainUniform {\n samples_x: u32,\n samples_y: u32,\n cell_size: f32,\n min_z: f32,\n max_z: f32,\n // Recipe surface-input ring-stack camera-follow center in grid cells.\n clip_center_x: f32,\n clip_center_y: f32,\n debug_lod_view: f32, // 1 = tint fragments by tile LOD tier (/lodview)\n origin: vec3<f32>,\n // Instance-index offset into the tile buffer (Hi-Z culled frames draw 0..count over the\n // compacted mirror; the VS adds the terrain's range start). 0 on direct frames.\n tile_base: f32,\n // Ring-stack clip (terrain_recipe_rings.rs; array at group(1) binding(8)):\n // (level_count [0 = off], min_valid_level, terminal texel_cells, spare).\n clip2: vec4<f32>,\n // Per level (base_cell_x, base_cell_y, texel_cells, spare); ring bases 512-texel-aligned.\n clip_levels: array<vec4<f32>, 8>,\n};\n// Must match TileInstanceGpu in terrain_tiles.rs (48 bytes).\nstruct TileInstance {\n corner: vec2<f32>, // world XZ of tile-local sample (0,0)\n quad_step: f32, // world meters per tile quad at this LOD\n _pad0: f32,\n atlas_base: vec2<f32>, // atlas texel coords of tile-local sample (0,0)\n atlas_step: f32, // texels per local sample\n morph_start: f32,\n morph_end: f32,\n skirt_depth: f32,\n clamp_max: vec2<f32>, // grid boundary in tile-local quads\n};\n\n@group(1) @binding(0) var height_tex: texture_2d<u32>;\n@group(1) @binding(1) var<uniform> terrain: TerrainUniform;\n// Heightfield analysis (R=convexity 0.5-neutral, G=AO, B=moisture/cavity), grid-uv addressed.\n@group(1) @binding(2) var analysis_tex: texture_2d<f32>;\n@group(1) @binding(5) var<storage, read> tiles: array<TileInstance>;\n@group(1) @binding(6) var tile_atlas: texture_2d<f32>;\n// Sim-resolution world-space normal map (RGBA8 n*0.5+0.5, mipped); per-pixel shading normals\n// so coarse-LOD tiles don't facet (blended with the vertex normal by detail_w).\n@group(1) @binding(7) var terrain_normal_tex: texture_2d<f32>;\n\n// ---- Recipe surface-input ring stack (terrain_recipe_rings.rs): K camera-following\n// toroidal clip levels, each one 512^2 array layer (RGB = generator normal, A = erosion /\n// ctx.user). clip2 = (level_count [0 = off], min_valid_level, terminal texel_cells,\n// spare); the last layer is the static terminal level (edge-clamped, never scrolls).\n// Rings store layer texel = world texel mod 512; tex_sampler's Repeat addressing makes\n// hardware bilinear seam-free inside a valid window (texture texels 511 and 0 always\n// hold world-adjacent data).\n@group(1) @binding(8) var terrain_clip_stack: texture_2d_array<f32>;\n\n// Continuous level pick: pixel footprint (level k fades out over minify 2*2^k..4*2^k -\n// the old smoothstep(2,4) handoff generalized) vs camera distance (level k fully used to\n// 112*2^k cells, fully handed to k+1 by 224*2^k; window edge is ~256*2^k, so the blend\n// completes >= 31 level-texels before data runs out). The octave-wide distance\n// cross-fade is what kills the old moving L0<->L1 detail ring.\nfn ori_clip_stack_level(cells: vec2<f32>) -> f32 {\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n let d = max(abs(cells.x - terrain.clip_center_x), abs(cells.y - terrain.clip_center_y));\n let lf_min = log2(max(minify, 2.0)) - 1.0;\n let lf_dist = log2(max(d * (1.0 / 112.0), 1.0));\n return clamp(max(lf_min, lf_dist), terrain.clip2.y, terrain.clip2.x - 1.0);\n}\n\nfn ori_clip_stack_layer(cells: vec2<f32>, lv: i32) -> vec4<f32> {\n let p = terrain.clip_levels[lv];\n var rel = (cells - p.xy) / max(p.z, 1e-3);\n // Terminal level: no wrap - clamp to the first/last texel center (the far-field edge\n // clamp the old L1 provided).\n if (lv == i32(terrain.clip2.x) - 1) { rel = clamp(rel, vec2<f32>(0.0), vec2<f32>(511.0)); }\n return textureSampleLevel(terrain_clip_stack, tex_sampler, (rel + 0.5) / 512.0, lv, 0.0);\n}\n\n// Ring-stack sample (normal rgb + erosion alpha): blend of the two footprint-adjacent\n// layers - coarser rings ARE the mip chain.\nfn ori_clip_stack_sample(cells: vec2<f32>) -> vec4<f32> {\n let lf = ori_clip_stack_level(cells);\n let li = i32(lf);\n let lj = min(li + 1, i32(terrain.clip2.x) - 1);\n return mix(ori_clip_stack_layer(cells, li), ori_clip_stack_layer(cells, lj), fract(lf));\n}\n\n// Final fallback: vertex normals once even the terminal level minifies past ~2 of ITS\n// texels per pixel.\nfn ori_clip_stack_vertex_fade(cells: vec2<f32>) -> f32 {\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n return smoothstep(2.0, 4.0, minify / max(terrain.clip2.z, 1.0));\n}\n\n// Debug switch (terrain_default's twin): false = geometric normal maps off, pure mesh\n// vertex normals (LOD budget reviews). ctx.user stays live for paint.\nconst ORI_TERRAIN_NORMAL_MAPS: bool = true;\n\n\nstruct MaterialUniform { params4: array<vec4<f32>, 8>, };\n@group(2) @binding(0) var tex0: texture_2d<f32>;\n@group(2) @binding(1) var tex1: texture_2d<f32>;\n@group(2) @binding(2) var tex2: texture_2d<f32>;\n@group(2) @binding(3) var tex3: texture_2d<f32>;\n@group(2) @binding(4) var tex4: texture_2d<f32>;\n@group(2) @binding(5) var tex5: texture_2d<f32>;\n@group(2) @binding(6) var tex6: texture_2d<f32>;\n@group(2) @binding(7) var tex7: texture_2d<f32>;\n@group(2) @binding(8) var tex8: texture_2d<f32>;\n@group(2) @binding(9) var tex9: texture_2d<f32>;\n@group(2) @binding(10) var tex10: texture_2d<f32>;\n@group(2) @binding(11) var tex11: texture_2d<f32>;\n@group(2) @binding(12) var tex12: texture_2d<f32>;\n@group(2) @binding(13) var tex13: texture_2d<f32>;\n@group(2) @binding(14) var tex14: texture_2d<f32>;\n@group(2) @binding(15) var tex15: texture_2d<f32>;\n@group(2) @binding(16) var tex_sampler: sampler;\n@group(2) @binding(17) var<uniform> mat: MaterialUniform;\n\nstruct TerrainVertexCtx {\n uv: vec2<f32>,\n normal: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos: vec3<f32>,\n local_pos: vec3<f32>,\n tangent: vec3<f32>,\n bitangent: vec3<f32>,\n};\n\nstruct TerrainMaterialCtx {\n uv: vec2<f32>,\n normal: vec3<f32>,\n world_normal: vec3<f32>,\n world_pos: vec3<f32>,\n local_pos: vec3<f32>,\n local_n: vec3<f32>,\n tangent: vec3<f32>,\n bitangent: vec3<f32>,\n front_facing: bool,\n instance_pos_xz: vec2<f32>,\n seed: f32,\n // SetTerrainSplatMask / #terrain erosion-map input (analysis alpha), 0..1. In recipe\n // display mode it covers the display extension too (input_span domain).\n user: f32,\n base_color: vec4<f32>,\n orm: vec4<f32>,\n // Parity with the static material16 ctx so shared bodies (e.g. default_pbr_triplanar) compile.\n mesh_color: vec4<f32>,\n emission_rgbi: vec4<f32>,\n};\n\nstruct TerrainMaterialOut {\n base_color: vec4<f32>,\n world_normal: vec3<f32>,\n orm: vec4<f32>,\n};\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @location(0) world_pos: vec3<f32>,\n @location(1) world_n: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) ndc_z: f32,\n @location(4) local_pos: vec3<f32>,\n @location(5) local_n: vec3<f32>,\n @location(6) world_t: vec3<f32>,\n @location(7) world_b: vec3<f32>,\n @location(9) detail_w: f32,\n // World meters per tile quad (the tile's LOD); /lodview tier tint input.\n @location(10) quad_step: f32,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(11) cur_clip: vec4<f32>,\n @location(12) prev_clip: vec4<f32>,\n};\n\n// /lodview tier palette: tile quad size relative to the sim cell, red = finest\n// (cell/4 sub-cell detail) through magenta/white = coarsest far rings.\nfn ori_lod_tier_color(quad_step: f32) -> vec3<f32> {\n let tier = i32(round(log2(max(quad_step, 1e-4) / max(terrain.cell_size, 1e-4)))) + 2;\n switch tier {\n case 0: { return vec3<f32>(1.0, 0.15, 0.15); } // cell/4\n case 1: { return vec3<f32>(1.0, 0.55, 0.10); } // cell/2\n case 2: { return vec3<f32>(1.0, 1.00, 0.15); } // cell\n case 3: { return vec3<f32>(0.20, 0.90, 0.20); } // 2x\n case 4: { return vec3<f32>(0.15, 0.90, 0.90); } // 4x\n case 5: { return vec3<f32>(0.25, 0.40, 1.00); } // 8x\n case 6: { return vec3<f32>(0.90, 0.25, 0.90); } // 16x\n default: { return vec3<f32>(1.0, 1.0, 1.0); } // coarser\n }\n}\n\nstruct FsOut {\n @location(0) out_base: vec4<f32>,\n @location(1) out_normal: vec4<f32>,\n @location(2) out_orm: vec4<f32>,\n @location(3) out_velocity: vec2<f32>,\n};\n\nfn decode_height(u: u32) -> f32 {\n let q: f32 = f32(u & 65535u) / 65535.0;\n return terrain.min_z + q * (terrain.max_z - terrain.min_z);\n}\n\nfn height_at(ix: i32, iy: i32) -> f32 {\n let sx: i32 = i32(terrain.samples_x);\n let sy: i32 = i32(terrain.samples_y);\n let x = clamp(ix, 0, sx - 1);\n let y = clamp(iy, 0, sy - 1);\n let v = textureLoad(height_tex, vec2<i32>(x, y), 0).r;\n return decode_height(v);\n}\n\nfn ori_seed_from_world_pos(world_pos: vec3<f32>) -> f32 {\n return fract(sin(dot(world_pos.xz, vec2<f32>(12.9898, 78.233))) * 43758.5453);\n}\n\nfn ori_pick_normal(default_n: vec3<f32>, a: vec3<f32>, b: vec3<f32>) -> vec3<f32> {\n if (any(b != default_n)) { return normalize(b); }\n if (any(a != default_n)) { return normalize(a); }\n return normalize(default_n);\n}\n\nfn ori_pick_vec4(default_v: vec4<f32>, a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {\n if (any(b != default_v)) { return b; }\n if (any(a != default_v)) { return a; }\n return default_v;\n}\n\n// Engine injects optional module-scope WGSL here.\n// Use `@module { ... }` in terrain material snippets.\n// Module-scope helpers for the built-in terrain layered splat (injected via __ORI_CUSTOM_FN__\n// only for the layered pipeline). All texture access uses textureSampleGrad with analytically\n// derived gradients, so calls are legal in non-uniform control flow: zero-weight layers and\n// near-zero triplanar planes are branched out entirely (big win vs the old always-36-samples).\n\n// Noise comes from the engine prelude (ori_value_noise2 / ori_fbm2, injected by preprocess_wgsl).\n\n// Stochastic anti-tiling (texture-variation): two hashed UV offsets blended by a low-frequency\n// noise ramp; offsets are constant within a variation band so the mix is seam-free.\nstruct OriTsTaps {\n off_a: vec2<f32>,\n off_b: vec2<f32>,\n blend: f32,\n}\n\nfn ori_ts_hash_off(n: f32) -> vec2<f32> {\n return fract(sin(vec2<f32>(n * 12.9898, n * 78.233)) * 43758.5453);\n}\n\nfn ori_ts_taps(uv: vec2<f32>, anti_on: f32) -> OriTsTaps {\n var t: OriTsTaps;\n let e = step(0.5, anti_on);\n // uv is in tile units; variation bands span a few tiles.\n let l = ori_value_noise2(uv.x * 0.09, uv.y * 0.09, 1337u) * 8.0;\n let ia = floor(l);\n t.off_a = ori_ts_hash_off(ia) * e;\n t.off_b = ori_ts_hash_off(ia + 1.0) * e;\n t.blend = smoothstep(0.3, 0.7, fract(l)) * e;\n return t;\n}\n\nfn ori_ts_sample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>, gx: vec2<f32>, gy: vec2<f32>, taps: OriTsTaps) -> vec4<f32> {\n if (taps.blend < 0.004) {\n return textureSampleGrad(t, s, uv + taps.off_a, gx, gy);\n }\n if (taps.blend > 0.996) {\n return textureSampleGrad(t, s, uv + taps.off_b, gx, gy);\n }\n let a = textureSampleGrad(t, s, uv + taps.off_a, gx, gy);\n let b = textureSampleGrad(t, s, uv + taps.off_b, gx, gy);\n return mix(a, b, taps.blend);\n}\n\nstruct OriTsLayerOut {\n col: vec3<f32>,\n orm: vec4<f32>, // rgb = AO/rough/metal, a = height (ORM-alpha convention; 0 when unset)\n n: vec3<f32>, // world-space (whiteout triplanar blend)\n}\n\n// One material layer at one tile scale: triplanar color/normal/orm with per-plane branching\n// and anti-tiling. Same rg-only normal convention as default_pbr_triplanar.\nfn ori_ts_layer_at(\n col_t: texture_2d<f32>, nrm_t: texture_2d<f32>, orm_t: texture_2d<f32>, s: sampler,\n wp: vec3<f32>, wn: vec3<f32>, tw: vec3<f32>, tile: f32,\n dpx: vec3<f32>, dpy: vec3<f32>, anti: f32,\n) -> OriTsLayerOut {\n let p = wp / tile;\n var col = vec4<f32>(0.0);\n var orm = vec4<f32>(0.0);\n var n = vec3<f32>(0.0);\n var wsum = 0.0;\n if (tw.x > 0.02) {\n let uv = p.zy;\n let gx = dpx.zy / tile;\n let gy = dpy.zy / tile;\n let taps = ori_ts_taps(uv, anti);\n col += ori_ts_sample(col_t, s, uv, gx, gy, taps) * tw.x;\n orm += ori_ts_sample(orm_t, s, uv, gx, gy, taps) * tw.x;\n let tn = ori_ts_sample(nrm_t, s, uv, gx, gy, taps).rg * 2.0 - vec2<f32>(1.0);\n n += vec3<f32>(tn + wn.zy, wn.x).zyx * tw.x;\n wsum += tw.x;\n }\n if (tw.y > 0.02) {\n let uv = p.xz;\n let gx = dpx.xz / tile;\n let gy = dpy.xz / tile;\n let taps = ori_ts_taps(uv, anti);\n col += ori_ts_sample(col_t, s, uv, gx, gy, taps) * tw.y;\n orm += ori_ts_sample(orm_t, s, uv, gx, gy, taps) * tw.y;\n let tn = ori_ts_sample(nrm_t, s, uv, gx, gy, taps).rg * 2.0 - vec2<f32>(1.0);\n n += vec3<f32>(tn + wn.xz, wn.y).xzy * tw.y;\n wsum += tw.y;\n }\n if (tw.z > 0.02) {\n let uv = p.xy;\n let gx = dpx.xy / tile;\n let gy = dpy.xy / tile;\n let taps = ori_ts_taps(uv, anti);\n col += ori_ts_sample(col_t, s, uv, gx, gy, taps) * tw.z;\n orm += ori_ts_sample(orm_t, s, uv, gx, gy, taps) * tw.z;\n let tn = ori_ts_sample(nrm_t, s, uv, gx, gy, taps).rg * 2.0 - vec2<f32>(1.0);\n n += vec3<f32>(tn + wn.xy, wn.z) * tw.z;\n wsum += tw.z;\n }\n var out: OriTsLayerOut;\n let inv = 1.0 / max(wsum, 1e-4);\n out.col = col.rgb * inv;\n out.orm = orm * inv;\n out.n = normalize(n);\n return out;\n}\n\n// Layer with near/far tile-scale blend (far_f from camera distance; far_mul=1 disables).\nfn ori_ts_layer(\n col_t: texture_2d<f32>, nrm_t: texture_2d<f32>, orm_t: texture_2d<f32>, s: sampler,\n wp: vec3<f32>, wn: vec3<f32>, tw: vec3<f32>, tile: f32, far_mul: f32, far_f: f32,\n dpx: vec3<f32>, dpy: vec3<f32>, anti: f32,\n) -> OriTsLayerOut {\n if (far_f < 0.01) {\n return ori_ts_layer_at(col_t, nrm_t, orm_t, s, wp, wn, tw, tile, dpx, dpy, anti);\n }\n let far = ori_ts_layer_at(col_t, nrm_t, orm_t, s, wp, wn, tw, tile * far_mul, dpx, dpy, anti);\n if (far_f > 0.99) {\n return far;\n }\n let near = ori_ts_layer_at(col_t, nrm_t, orm_t, s, wp, wn, tw, tile, dpx, dpy, anti);\n var out: OriTsLayerOut;\n out.col = mix(near.col, far.col, far_f);\n out.orm = mix(near.orm, far.orm, far_f);\n out.n = normalize(mix(near.n, far.n, far_f));\n return out;\n}\n\n\nconst ORI_TILE_SAMPLES: u32 = 33u;\nconst ORI_TILE_GRID_VERTS: u32 = ORI_TILE_SAMPLES * ORI_TILE_SAMPLES;\n\n// Tile-local vertex position in sample units + skirt flag (see terrain_vertex.wgsl).\nfn ori_tile_local(vi: u32) -> vec3<f32> {\n if (vi < ORI_TILE_GRID_VERTS) {\n return vec3<f32>(f32(vi % ORI_TILE_SAMPLES), f32(vi / ORI_TILE_SAMPLES), 0.0);\n }\n let s = vi - ORI_TILE_GRID_VERTS;\n let edge = s / ORI_TILE_SAMPLES;\n let i = f32(s % ORI_TILE_SAMPLES);\n let last = f32(ORI_TILE_SAMPLES - 1u);\n switch edge {\n case 0u: { return vec3<f32>(i, 0.0, 1.0); }\n case 1u: { return vec3<f32>(i, last, 1.0); }\n case 2u: { return vec3<f32>(0.0, i, 1.0); }\n default: { return vec3<f32>(last, i, 1.0); }\n }\n}\n\n// Manual bilinear on the tile atlas (rg32float is not filterable in base WebGPU).\nfn ori_tile_h(t: TileInstance, l: vec2<f32>) -> vec2<f32> {\n let a = t.atlas_base + l * t.atlas_step;\n let f = floor(a);\n let fr = a - f;\n let p = vec2<i32>(f);\n let h00 = textureLoad(tile_atlas, p, 0).rg;\n let h10 = textureLoad(tile_atlas, p + vec2<i32>(1, 0), 0).rg;\n let h01 = textureLoad(tile_atlas, p + vec2<i32>(0, 1), 0).rg;\n let h11 = textureLoad(tile_atlas, p + vec2<i32>(1, 1), 0).rg;\n return mix(mix(h00, h10, fr.x), mix(h01, h11, fr.x), fr.y);\n}\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vi: u32,\n @builtin(instance_index) ii: u32,\n) -> VsOut {\n let tile = tiles[ii + u32(terrain.tile_base)];\n let lv = ori_tile_local(vi);\n let is_skirt = lv.z > 0.5;\n let l = min(lv.xy, tile.clamp_max);\n\n // Geomorph: odd vertices slide toward the even (parent-grid) position while the height\n // blends R -> G (parent LOD's value); see terrain_vertex.wgsl.\n let h_pre = ori_tile_h(tile, l);\n let pre_pos = vec3<f32>(\n tile.corner.x + l.x * tile.quad_step,\n terrain.origin.y + h_pre.x,\n tile.corner.y - l.y * tile.quad_step,\n );\n let dist_pre = distance(u_camera.camera_position, pre_pos);\n let m = clamp((dist_pre - tile.morph_start) / max(tile.morph_end - tile.morph_start, 1e-3), 0.0, 1.0);\n var lm = l;\n var h = h_pre.x;\n if (m > 0.0) {\n lm = min(l - fract(l * 0.5) * 2.0 * m, tile.clamp_max);\n let hm = ori_tile_h(tile, lm);\n h = mix(hm.x, hm.y, m);\n }\n\n let world_x = tile.corner.x + lm.x * tile.quad_step;\n let world_z = tile.corner.y - lm.y * tile.quad_step;\n var world_y = terrain.origin.y + h;\n if (is_skirt) { world_y -= tile.skirt_depth; }\n\n // Normal: central differences on the atlas R channel at the nearest texel.\n let p = vec2<i32>(round(tile.atlas_base + lm * tile.atlas_step));\n let hx0 = textureLoad(tile_atlas, p - vec2<i32>(1, 0), 0).r;\n let hx1 = textureLoad(tile_atlas, p + vec2<i32>(1, 0), 0).r;\n let hy0 = textureLoad(tile_atlas, p - vec2<i32>(0, 1), 0).r;\n let hy1 = textureLoad(tile_atlas, p + vec2<i32>(0, 1), 0).r;\n let ws = tile.quad_step / max(tile.atlas_step, 1e-6);\n let dx = vec3<f32>(2.0 * ws, hx1 - hx0, 0.0);\n let dz = vec3<f32>(0.0, hy1 - hy0, -2.0 * ws);\n let n = normalize(cross(dx, dz));\n var t = dx - n * dot(n, dx);\n t = t / max(length(t), 1e-6);\n var b = cross(n, t);\n b = b / max(length(b), 1e-6);\n if (dot(b, dz) < 0.0) { b = -b; }\n t = cross(b, n);\n\n let size_x = f32(max(1u, terrain.samples_x - 1u)) * terrain.cell_size;\n let size_z = f32(max(1u, terrain.samples_y - 1u)) * terrain.cell_size;\n\n let default_world_pos = vec3<f32>(world_x, world_y, world_z);\n let default_world_n = n;\n let default_local_pos = default_world_pos - terrain.origin;\n let default_t = t;\n let default_b = b;\n\n var ctx: TerrainVertexCtx;\n ctx.uv = vec2<f32>(\n (world_x - terrain.origin.x) / size_x + 0.5,\n 0.5 - (world_z - terrain.origin.z) / size_z,\n );\n ctx.normal = default_world_n;\n ctx.world_normal = default_world_n;\n ctx.world_pos = default_world_pos;\n ctx.local_pos = default_local_pos;\n ctx.tangent = default_t;\n ctx.bitangent = default_b;\n {\n \n }\n\n var out: VsOut;\n out.world_pos = ctx.world_pos;\n out.world_n = ori_pick_normal(default_world_n, ctx.normal, ctx.world_normal);\n out.uv = ctx.uv;\n out.local_pos = ctx.local_pos;\n out.local_n = out.world_n;\n out.world_t = normalize(ctx.tangent);\n out.world_b = normalize(ctx.bitangent);\n // Detail octaves at this LOD = log2(cell/quad_step); geomorph m fades toward one octave\n // less, so subtracting m fades detail_w in lockstep (see terrain_vertex.wgsl).\n out.detail_w = clamp(log2(max(terrain.cell_size / tile.quad_step, 1e-6)) - m, 0.0, 1.0);\n out.quad_step = tile.quad_step;\n out.clip_pos = u_camera.view_proj * vec4<f32>(out.world_pos, 1.0);\n out.ndc_z = out.clip_pos.z / out.clip_pos.w;\n out.cur_clip = u_camera.unjittered_view_proj * vec4<f32>(out.world_pos, 1.0);\n out.prev_clip = u_camera.prev_unjittered_view_proj * vec4<f32>(out.world_pos, 1.0);\n return out;\n}\n\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(\n @builtin(front_facing) front_facing: bool,\n in: VsOut,\n) -> FsOut {\n // Planar reflection: clip below-plane terrain (same rule as gbuffer.wgsl) so the mirrored\n // camera never sees underground tiles/skirts.\n if (u_camera.reflection_clip_enabled > 0.5 && in.world_pos.y < u_camera.reflection_clip_y) {\n discard;\n }\n // Per-pixel geometric normal: sim-res normal map at far/coarse LODs, vertex normal (which\n // carries sub-cell detail octaves) near the camera. uv remaps to texel centers (the grid\n // spans n-1 cells but the texture has n texels). Everything downstream (triplanar weights,\n // slope bands, layer normal basis) inherits the per-pixel normal via ctx.normal.\n // The map only covers the sim grid: display-extension fragments (uv outside [0,1]) must\n // not sample it (tex_sampler repeats = the sim region's normals tiled across the whole\n // extension). Clamp the sample and fade to the vertex normal over ~2 cells outside.\n let ori_n_tex = vec2<f32>(f32(terrain.samples_x), f32(terrain.samples_y));\n let ori_cells = in.uv * (ori_n_tex - vec2<f32>(1.0));\n let ori_clip = terrain.clip2.x > 0.5;\n // Ring-stack sample (uniform flow; used when clip on): the two footprint-adjacent\n // levels blended, valid-suffix clamped during initial fill / teleports.\n let ori_clip_sample = ori_clip_stack_sample(ori_cells);\n let ori_over = max(max(-in.uv, in.uv - vec2<f32>(1.0)), vec2<f32>(0.0))\n * (ori_n_tex - vec2<f32>(1.0));\n let ori_ext_w = smoothstep(0.0, 2.0, max(ori_over.x, ori_over.y));\n let ori_uv_c = clamp(in.uv, vec2<f32>(0.0), vec2<f32>(1.0));\n let ori_uv_n = (ori_uv_c * (ori_n_tex - vec2<f32>(1.0)) + vec2<f32>(0.5)) / ori_n_tex;\n let ori_grid_n = normalize(\n textureSample(terrain_normal_tex, tex_sampler, ori_uv_n).rgb * 2.0 - vec3<f32>(1.0));\n let ori_map_n = select(ori_grid_n, normalize(ori_clip_sample.rgb * 2.0 - vec3<f32>(1.0)), ori_clip);\n let ori_map_w = select(1.0,\n select(max(in.detail_w, ori_ext_w),\n max(in.detail_w, ori_clip_stack_vertex_fade(ori_cells)), ori_clip),\n ORI_TERRAIN_NORMAL_MAPS);\n let default_world_n = normalize(mix(ori_map_n, normalize(in.world_n), ori_map_w))\n * select(-1.0, 1.0, front_facing);\n let default_base = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n let default_orm = vec4<f32>(1.0, 1.0, 0.0, 0.0);\n\n var ctx: TerrainMaterialCtx;\n ctx.uv = in.uv;\n ctx.normal = default_world_n;\n ctx.world_normal = default_world_n;\n ctx.world_pos = in.world_pos;\n ctx.local_pos = in.local_pos;\n ctx.local_n = normalize(in.local_n);\n ctx.tangent = normalize(in.world_t);\n ctx.bitangent = normalize(in.world_b);\n ctx.front_facing = front_facing;\n ctx.instance_pos_xz = in.world_pos.xz;\n ctx.seed = ori_seed_from_world_pos(in.world_pos);\n // ctx.user: recipe terrains read the clipmap's erosion map (covers the extension);\n // data terrains read the SetTerrainSplatMask baked into the analysis alpha (grid only).\n // Half-texel clamp (tex_sampler repeats; analysis is sim-grid resolution, so a fixed\n // 0.001 margin bilinearly wraps the outer row to the opposite edge).\n let ori_ana_h = vec2<f32>(0.5) / vec2<f32>(textureDimensions(analysis_tex));\n let ori_ana_user = textureSampleLevel(\n analysis_tex, tex_sampler, clamp(in.uv, ori_ana_h, vec2<f32>(1.0) - ori_ana_h), 0.0).a;\n ctx.user = select(ori_ana_user, ori_clip_sample.a, ori_clip);\n ctx.base_color = default_base;\n ctx.orm = default_orm;\n ctx.mesh_color = vec4<f32>(1.0);\n ctx.emission_rgbi = vec4<f32>(0.0);\n\n var out: TerrainMaterialOut;\n out.base_color = default_base;\n out.world_normal = default_world_n;\n out.orm = default_orm;\n\n {\n \n// Built-in 4-layer slope/height splat over triplanar #texture layers (SetTerrainLayers).\n// Slots: base=tex0/1/2, steep=tex3/4/5, high=tex6/7/8, low=tex9/10/11 (color/normal/orm each).\n// params4[0]=tile_m per layer (base,steep,high,low);\n// params4[1]=(slope_start_deg, slope_end_deg, high_start_m, high_end_m);\n// params4[2]=(low_start_m, low_end_m, has_steep, has_high);\n// params4[3]=(has_low, band_noise_amp, anti_tile_enable, far_tile_mul; 1=off);\n// params4[4]=(macro_tint_amp, macro_rough_amp, height_blend_str, far_blend_start_m);\n// params4[5]=(far_blend_end_m, analysis_strength, splat_mode).\n// splat_mode / tex12: 0 = no weightmap; 1 = baked #terrain_splat weightmap, RGB =\n// steep/high/low weights; 2 = colormap mode (out.albedo): tex12 RGB is FINAL albedo (mip\n// chain baked), base material supplies normal/ORM, band jitter/height-sharpen/macro skipped.\n// CPU resolves \"auto\" defaults before packing, so the shader sees final values.\n// Sampling uses textureSampleGrad (helpers in terrain_splat_layers_fns.wgsl), so zero-weight\n// layers/planes are branched out; a typical pixel samples 1-2 layers instead of all 4.\nlet tiles = max(mat.params4[0], vec4<f32>(0.001));\nlet pb1 = mat.params4[1];\nlet pb2 = mat.params4[2];\nlet pb3 = mat.params4[3];\nlet pb4 = mat.params4[4];\nlet pb5 = mat.params4[5];\n\nlet wn = normalize(ctx.normal);\nvar tw = pow(abs(wn), vec3<f32>(4.0));\ntw = tw / (tw.x + tw.y + tw.z);\n\n// World-pos gradients once, in uniform control flow; per-plane UV grads derive analytically.\n// exp2(mip_bias) scales the grads = upscaler mip bias (textureSampleGrad has no bias arg).\nlet ts_mip_scale = exp2(u_camera.mip_bias);\nlet ts_dpx = dpdx(ctx.world_pos) * ts_mip_scale;\nlet ts_dpy = dpdy(ctx.world_pos) * ts_mip_scale;\n// UV grads for the colormap-mode albedo sample; derivatives must stay in uniform control flow.\nlet ts_uv_cm = ctx.uv;\nlet ts_duvx = dpdx(ctx.uv) * ts_mip_scale;\nlet ts_duvy = dpdy(ctx.uv) * ts_mip_scale;\n\n// Heightfield analysis (R=convexity 0.5-neutral, G=AO, B=moisture, A=data ctx.user).\n// Half-texel clamp: tex_sampler repeats, and analysis is sim-grid resolution - a fixed\n// 0.001 margin lets the outer row's bilinear footprint wrap to the opposite edge.\nlet ana_h = vec2<f32>(0.5) / vec2<f32>(textureDimensions(analysis_tex));\nlet ana = textureSampleLevel(analysis_tex, tex_sampler, clamp(ctx.uv, ana_h, vec2<f32>(1.0) - ana_h), 0.0);\nlet ana_str = pb5.y;\nlet convex = (ana.r * 2.0 - 1.0) * ana_str;\n\n// Layer weights: base -> high -> low -> steep (cliff layer wins last).\n// Band edges are perturbed by shared low-frequency fbm so they stop reading as contour lines;\n// convex ridges read rockier, moist hollows pull the low layer up.\nlet band_n = (ori_fbm2(ctx.world_pos.x * 0.13, ctx.world_pos.z * 0.13, 3, 41u) - 0.5) * 2.0 * pb3.y; // ~ -amp..amp\nlet slope_deg = degrees(acos(clamp(wn.y, -1.0, 1.0))) + band_n * 14.0 + convex * 8.0;\nlet h_high = ctx.world_pos.y + band_n * max(pb1.w - pb1.z, 0.5);\nlet h_low = ctx.world_pos.y + band_n * max(pb2.x - pb2.y, 0.5);\nlet w_steep = smoothstep(pb1.x, pb1.y, slope_deg) * pb2.z;\nlet w_high = smoothstep(pb1.z, pb1.w, h_high) * pb2.w;\nlet w_low = clamp((1.0 - smoothstep(pb2.y, pb2.x, h_low)) + ana.b * 0.5 * ana_str, 0.0, 1.0) * pb3.x;\nvar lw = vec4<f32>(1.0, 0.0, 0.0, 0.0);\nlw = mix(lw, vec4<f32>(0.0, 0.0, 1.0, 0.0), w_high);\nlw = mix(lw, vec4<f32>(0.0, 0.0, 0.0, 1.0), w_low);\nlw = mix(lw, vec4<f32>(0.0, 1.0, 0.0, 0.0), w_steep);\n\n// #terrain_splat weightmap override: DSL-authored weights replace the procedural bands\n// (height-blend sharpening below still applies). Missing layers keep their weight in base.\n// Colormap mode (2) samples only the base layer (for normal/ORM); its color is replaced below.\nif (pb5.z > 1.5) {\n lw = vec4<f32>(1.0, 0.0, 0.0, 0.0);\n} else if (pb5.z > 0.5) {\n let sw_h = vec2<f32>(0.5) / vec2<f32>(textureDimensions(tex12));\n let sw = textureSampleLevel(tex12, tex_sampler, clamp(ts_uv_cm, sw_h, vec2<f32>(1.0) - sw_h), 0.0).rgb\n * vec3<f32>(pb2.z, pb2.w, pb3.x);\n let ssum = sw.x + sw.y + sw.z;\n let sn = sw / max(ssum, 1.0);\n lw = vec4<f32>(1.0 - min(ssum, 1.0), sn.x, sn.y, sn.z);\n}\n\n// Far-distance tile-scale blend (fights distant tiling shimmer). far_mul<=1 disables.\nlet far_mul = max(pb3.w, 1.0);\nlet cam_d = length(ctx.world_pos - u_camera.camera_position);\nlet far_f = smoothstep(pb4.w, max(pb5.x, pb4.w + 1.0), cam_d) * step(1.5, far_mul);\nlet anti = pb3.z;\n\nvar col0 = vec3<f32>(0.0); var col1 = vec3<f32>(0.0); var col2 = vec3<f32>(0.0); var col3 = vec3<f32>(0.0);\nvar orm0 = vec4<f32>(0.0); var orm1 = vec4<f32>(0.0); var orm2 = vec4<f32>(0.0); var orm3 = vec4<f32>(0.0);\nvar n0 = wn; var n1 = wn; var n2 = wn; var n3 = wn;\nif (lw.x > 0.004) {\n let r = ori_ts_layer(tex0, tex1, tex2, tex_sampler, ctx.world_pos, wn, tw, tiles.x, far_mul, far_f, ts_dpx, ts_dpy, anti);\n col0 = r.col; orm0 = r.orm; n0 = r.n;\n}\nif (lw.y > 0.004) {\n let r = ori_ts_layer(tex3, tex4, tex5, tex_sampler, ctx.world_pos, wn, tw, tiles.y, far_mul, far_f, ts_dpx, ts_dpy, anti);\n col1 = r.col; orm1 = r.orm; n1 = r.n;\n}\nif (lw.z > 0.004) {\n let r = ori_ts_layer(tex6, tex7, tex8, tex_sampler, ctx.world_pos, wn, tw, tiles.z, far_mul, far_f, ts_dpx, ts_dpy, anti);\n col2 = r.col; orm2 = r.orm; n2 = r.n;\n}\nif (lw.w > 0.004) {\n let r = ori_ts_layer(tex9, tex10, tex11, tex_sampler, ctx.world_pos, wn, tw, tiles.w, far_mul, far_f, ts_dpx, ts_dpy, anti);\n col3 = r.col; orm3 = r.orm; n3 = r.n;\n}\n\n// Height-based blend sharpening (ORM alpha = height; all-zero heights degrade to plain weights).\nlet hts = vec4<f32>(orm0.a, orm1.a, orm2.a, orm3.a);\nlet hmax = max(max(hts.x, hts.y), max(hts.z, hts.w));\nlet w_h = max(lw + (hts - vec4<f32>(hmax)) * pb4.z * step(0.001, hmax), vec4<f32>(0.0));\nlet wsum_h = dot(w_h, vec4<f32>(1.0));\nlet w = select(lw, w_h / max(wsum_h, 1e-5), wsum_h > 1e-5);\n\nvar out_col = col0 * w.x + col1 * w.y + col2 * w.z + col3 * w.w;\nlet orm_mix = orm0 * w.x + orm1 * w.y + orm2 * w.z + orm3 * w.w;\nvar out_rough = orm_mix.g;\n\nif (pb5.z > 1.5) {\n // Colormap mode: tex12 RGB is the authored final albedo (mip chain baked at upload).\n // Macro tint / cavity darkening are skipped - exactness is the whole point of the mode.\n // Mip-aware half-texel clamp (Repeat sampler): higher mips need a wider margin or the\n // box-mip edge texels blend the opposite border at distance.\n let cm_dims = vec2<f32>(textureDimensions(tex12));\n let cm_lod = clamp(\n log2(max(max(length(ts_duvx * cm_dims), length(ts_duvy * cm_dims)), 1e-6)),\n 0.0, f32(textureNumLevels(tex12) - 1u));\n let cm_h = min(vec2<f32>(0.5 * exp2(ceil(cm_lod))) / cm_dims, vec2<f32>(0.5));\n let uv_c = clamp(ts_uv_cm, cm_h, vec2<f32>(1.0) - cm_h);\n out_col = textureSampleGrad(tex12, tex_sampler, uv_c, ts_duvx, ts_duvy).rgb;\n} else {\n // Macro variation: low-frequency worldspace tint + roughness modulation; cavity (1-AO) darkening.\n let macro_n = (ori_fbm2(ctx.world_pos.x * 0.021, ctx.world_pos.z * 0.021, 3, 42u) - 0.5) * 2.0;\n out_col *= (1.0 + macro_n * pb4.x) * (1.0 - (1.0 - ana.g) * 0.25 * ana_str);\n out_rough = clamp(out_rough * (1.0 + macro_n * pb4.y), 0.02, 1.0);\n}\n\n// Display-extension pixels (uv outside the sim grid; pb5.w = flag, pb6.x = water world-y,\n// pb6.y = W_m): reference altitude/flatness bands in linear color, since neither the\n// colormap nor the weightmap covers the procedural continuation. Recipe worlds wanting\n// real paint out here use a live terrain #shader instead (ctx.user carries the recipe's\n// erosion map across the whole span).\nif (pb5.w > 0.5\n && (ctx.uv.x < -0.001 || ctx.uv.y < -0.001 || ctx.uv.x > 1.001 || ctx.uv.y > 1.001)) {\n let pb6 = mat.params4[6];\n let u01 = 0.45 + (ctx.world_pos.y - pb6.x) / max(pb6.y, 1.0);\n var c = vec3<f32>(0.22, 0.2, 0.2) * smoothstep(0.4, 0.52, u01);\n let grassw = smoothstep(0.5, 0.47, u01) * smoothstep(0.8, 1.0, wn.y);\n c = mix(c, vec3<f32>(0.2, 0.36, 0.13), grassw);\n c = mix(c, vec3<f32>(1.0), smoothstep(0.53, 0.6, u01));\n c = mix(c, vec3<f32>(0.8, 0.7, 0.6), smoothstep(0.455, 0.45, u01));\n out_col = c;\n}\n\nout.base_color = vec4<f32>(out_col, 1.0) * ctx.mesh_color;\nout.world_normal = normalize(n0 * w.x + n1 * w.y + n2 * w.z + n3 * w.w);\nout.orm = vec4<f32>(orm_mix.r, out_rough, orm_mix.b, ctx.emission_rgbi.a);\n\n\n }\n\n var base = ori_pick_vec4(default_base, ctx.base_color, out.base_color);\n let orm = ori_pick_vec4(default_orm, ctx.orm, out.orm);\n let world_n = ori_pick_normal(default_world_n, ctx.world_normal, out.world_normal);\n\n var fs_out: FsOut;\n fs_out.out_base = base;\n if (terrain.debug_lod_view > 0.5) {\n fs_out.out_base = vec4<f32>(\n mix(fs_out.out_base.rgb, ori_lod_tier_color(in.quad_step), 0.65), fs_out.out_base.a);\n }\n fs_out.out_normal = vec4<f32>(world_n, 1.0);\n fs_out.out_orm = orm;\n fs_out.out_velocity = gbuffer_velocity(in.cur_clip, in.prev_clip);\n return fs_out;\n}\n"},{"label":"terrain_recipe_check","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// ============================================================================================\n// Terrain display-extension generator: GPU twin of the #terrain recipe composition\n// (gradient-fbm base -> height mask -> OriErosion directional erosion). Used by the terrain\n// DISPLAY EXTENSION to continue a sim heightfield procedurally beyond the collision grid\n// (render-only, no sim state), and as the whole display surface in recipe mode.\n// Erosion kernel spec: docs/ori_erosion_spec.md; Fp sim twin: fp_ori_erosion.rs (keep in\n// sync). Requires the ori noise prelude (ori_hash2_cell).\n//\n// EXACTNESS CONTRACT: for a block built with TerrainHeightsNoise mode 4 (+ MaskFromHeight\n// + the OriErosion op / TerrainHeightsFromTerrain), this generator reproduces the sim field\n// bit-approximately (f32 vs Fp rounding only): same base fbm (seed + octave, gain, /total\n// normalization), same erosion dir recovery (central diff at +-1 cell,\n// n_dd = dh * base_scale_cells / W), same octave seed stream (seed ^ 0x9e3779b9 * (i+1)),\n// same jitter/gradient lattice hashes, same polynomial window.\n// ============================================================================================\n\nconst ORI_EXT_TAU: f32 = 6.28318530717959;\n\n// Gradient vector in [-1,1)^2 from one lattice hash (top/bottom 16 bits) - matches\n// fp_noise::gradient_at (Fp) at every lattice cell.\nfn ori_ext_gradient_at(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 32768.0 - 1.0;\n}\n\n// Gradient (Perlin) noise, zero at integer lattice points, quintic fade - the WGSL twin of\n// fp_noise::gradient2 (same hash family; f32 vs Fp rounding differs at sub-cm level).\nfn ori_ext_gradient2(p: vec2<f32>, seed: u32) -> f32 {\n let i = floor(p);\n let f = p - i;\n let c = vec2<i32>(i);\n let ga = ori_ext_gradient_at(c, seed);\n let gb = ori_ext_gradient_at(c + vec2<i32>(1, 0), seed);\n let gc = ori_ext_gradient_at(c + vec2<i32>(0, 1), seed);\n let gd = ori_ext_gradient_at(c + vec2<i32>(1, 1), seed);\n let va = dot(ga, f);\n let vb = dot(gb, f - vec2<f32>(1.0, 0.0));\n let vc = dot(gc, f - vec2<f32>(0.0, 1.0));\n let vd = dot(gd, f - vec2<f32>(1.0, 1.0));\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let a = va + (vb - va) * u.x;\n let b = vc + (vd - vc) * u.x;\n return a + (b - a) * u.y;\n}\n\n// OriErosion feature point offset inside a cell: 0.5 + jitter per axis, jitter in\n// [-0.45, 0.45) - twin of fp_ori_erosion::feature_offset.\nfn ori_ext_feature_offset(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(0.5) + (vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 65536.0 - 0.5) * 0.9;\n}\n\n// One OriErosion directional wave sample (value, ddx, ddy) - twin of\n// fp_ori_erosion::ori_erosion_sample (spec section 3): 5x5 jittered feature lattice,\n// polynomial window (1 - d2/4)^8 with EXACT zero support at R = 2 (the d2 >= 4 skip\n// drops only exact zeros - no truncation seams; the bell matches the reference\n// exp(-2 d2) including its tail, so grooves stay correlated across neighboring cells\n// and flow smoothly into each other), 1/tau-convention gradient along dir.\nfn ori_ext_erosion_sample(p: vec2<f32>, dir: vec2<f32>, seed: u32) -> vec3<f32> {\n let ip = floor(p);\n let fp_ = p - ip;\n let ci = vec2<i32>(ip);\n var v = 0.0;\n var g = vec2<f32>(0.0);\n var wt = 0.0;\n for (var i = -2; i <= 2; i = i + 1) {\n for (var k = -2; k <= 2; k = k + 1) {\n let q = ori_ext_feature_offset(ci + vec2<i32>(i, k), seed);\n let r = fp_ - vec2<f32>(f32(i), f32(k)) - q;\n let d2 = dot(r, r);\n if (d2 >= 4.0) { continue; }\n let t = 1.0 - d2 * 0.25;\n // Same x64-scaled form as the Fp twin (w = (8 t^4)^2 = 64 t^8); the scale cancels\n // in the weighted average, and matching the arithmetic shape keeps rounding aligned.\n let t2 = t * t;\n let t4s = t2 * t2 * 8.0;\n let w = t4s * t4s;\n wt = wt + w;\n // Phase wrapped to one period before the tau multiply (exact: cos period 1) - keeps\n // the trig argument in [0, tau) on both twins, bounding f32-vs-Fp drift (see the Fp\n // twin in fp_ori_erosion.rs).\n // Phase snapped to the exact 1/1024 grid (floor), twin of fp_ori_erosion: both\n // sides evaluate the SAME cos bucket, so residual positional drift cannot reach\n // the height output.\n let phase = floor(fract(dot(r, dir)) * 1024.0) / 1024.0 * ORI_EXT_TAU;\n v = v + cos(phase) * w;\n // 1/tau gradient convention, twin of fp_ori_erosion (branch_strength absorbs it;\n // keeps feedback magnitudes O(1) so positional Fp-vs-f32 drift stays bounded).\n g = g - sin(phase) * w * dir;\n }\n }\n // wt > 0 by construction: the own-cell feature is within sqrt(1.805) < R = 2 of any\n // sample (locked by fp_ori_erosion support tests).\n return vec3<f32>(v, g) / wt;\n}\n\n// Display-extension generator params (zeroed radius = disabled; see BakeUniform).\n// 20 f32 = 80 bytes (uniform address space rounds nested struct sizes to 16). Every Rust\n// fill site is a [f32; 20] in OriExtParams field order - keep them in lockstep.\nstruct OriExtParams {\n radius_cells: f32, // extension radius in sim cells (0 = off)\n base_scale_cells: f32, // base-noise feature size in cells\n base_amp_m: f32, // base amplitude in meters (heights = (fbm*0.5+0.5)*amp)\n base_octaves: f32, // gradient fbm octaves\n base_gain: f32, // per-octave amplitude factor (0.1 near-mono smooth; classic 0.5)\n erosion_scale_cells: f32,\n erosion_octaves: f32,\n erosion_strength_m: f32,\n slope_strength: f32,\n branch_strength: f32,\n mask_start_m: f32, // erosion amplitude mask: smoothstep(start, end, base) like\n mask_end_m: f32, // TerrainHeightsMaskFromHeight on the pre-erosion base\n base_seed: f32,\n erosion_seed: f32, // sim op seed (the scene may use a different stream than the base)\n domain_w_m: f32, // noise unit-domain width in meters (samples_x * cell_m); dir scale\n // > 0.5 = recipe display mode: the generator IS the display content everywhere (inside the\n // sim grid too - no Catmull-Rom, no boundary). Set per asset after the divergence check\n // verifies the sim heights match this recipe (see TerrainPass::ensure_terrain_asset).\n // Keep recipe_mode at flat index 15: terrain_tiles injects it as ext[15] per asset.\n recipe_mode: f32,\n erosion_gain: f32, // per-octave erosion amplitude factor (reference 0.5)\n erosion_lacunarity: f32, // per-octave erosion frequency factor (reference 2)\n _pad_a: f32,\n _pad_b: f32,\n};\n\n// Band-limit weight for one octave: 1 when the octave wavelength is well above the sample\n// spacing, fading to 0 approaching Nyquist. Baked per tile LOD; without this, octaves finer\n// than a coarse ring's spacing alias into DIFFERENT content per ring = straight seams at\n// ring boundaries (and the geomorph can't hide them if h_coarse == h).\nfn ori_ext_band(wavelength_cells: f32, step_cells: f32) -> f32 {\n return smoothstep(2.0, 4.0, wavelength_cells / max(step_cells, 1e-3));\n}\n\n// Base heights (meters, terrain-local like decode_height output). EXACT twin of\n// fp_noise::gradient_fbm2 (freq x2, amp x gain, per-octave seed = seed + i, sum / total) so\n// a sim block built with TerrainHeightsNoise mode 4 continues bit-consistently (f32 vs Fp\n// rounding aside) into the infinite field.\n// Returns (h_m, h_coarse_m): the coarse variant band-limits at 2x the sample spacing = the\n// parent LOD's value, restoring pop-free geomorph for extension tiles. Normalization uses\n// the UNfaded total so amplitude stays consistent with the sim block at any LOD (faded\n// octaves contribute their zero mean).\nfn ori_ext_base(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n let seed = u32(e.base_seed);\n // Division, not s * (1/scale): twin of terrain_gen::noise_heights (the reciprocal's\n // truncation grows with |s| and diverges from the Fp sim on steep recipes).\n let p = s / max(e.base_scale_cells, 1.0);\n var n = 0.0;\n var nc = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var f = 1.0;\n let n_oct = i32(clamp(e.base_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let wl = e.base_scale_cells / f;\n let bf = ori_ext_band(wl, step_cells);\n let bc = ori_ext_band(wl, step_cells * 2.0);\n total = total + amp;\n if (bf > 0.0) {\n let v = ori_ext_gradient2(p * f, seed + u32(i));\n n = n + v * amp * bf;\n nc = nc + v * amp * bc;\n }\n amp = amp * e.base_gain;\n f = f * 2.0;\n }\n n = n / total;\n nc = nc / total;\n return vec2<f32>((n * 0.5 + 0.5) * e.base_amp_m, (nc * 0.5 + 0.5) * e.base_amp_m);\n}\n\n// One band-limited OriErosion accumulation (the octave loop of spec section 4) with its own\n// mask, dir and branch-feedback chain. `step_cells` selects the band weights: evaluating at\n// 2x the step reproduces the PARENT LOD's chain exactly, which is what makes the geomorph\n// identity (child h_coarse == parent h) hold bit-for-bit.\n//\n// The erosion octave is NOT zero-mean: its local mean is ~= the windowed cos average,\n// dc(t) ~= max(0, exp(-K t^2)) with t = |dir| and K = ORI_EROSION_DC_K (numeric fit locked\n// by fp_ori_erosion::tests::dc_fit_matches_constant) - near 1 on flats, ~0 on steep slopes.\n// Band-limited octaves must fade toward that DC, not toward 0, or coarse LOD rings sink by\n// up to 0.5*strength on flats = curved cliff shelves at ring boundaries. Full-detail output\n// (band weights 1) is unchanged by dc.\nfn ori_ext_erosion_chain(pe: vec2<f32>, dir: vec2<f32>, m: f32, e: OriExtParams, step_cells: f32) -> f32 {\n var hx = 0.0;\n var hd = vec2<f32>(0.0);\n var a = 0.5 * m;\n var a_total = 0.0;\n var f = 1.0;\n let seed = u32(e.erosion_seed);\n let n_oct = i32(clamp(e.erosion_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let bf = ori_ext_band(e.erosion_scale_cells / f, step_cells);\n // Direction snapped to the exact 1/256 grid (floor), twin of fp_ori_erosion: the\n // branch-feedback recursion re-synchronizes with the Fp sim every octave instead of\n // compounding f32-vs-Fp drift. Exact: |bd|*256 < 2^20 stays integer-exact in f32.\n let bd = floor((dir + vec2<f32>(hd.y, -hd.x) * e.branch_strength) * 256.0) / 256.0;\n let dc = exp(-4.26 * dot(bd, bd)); // ORI_EROSION_DC_K\n if (bf > 0.0) {\n // Twin of the sim octave seed stream: seed ^ (0x9e3779b9 * (1 + octave)), wrapping.\n let oseed = seed ^ (0x9e3779b9u * (1u + u32(i)));\n let v = ori_ext_erosion_sample(pe * f, bd, oseed);\n hx = hx + mix(dc, v.x, bf) * a;\n hd = hd + v.yz * a * f * bf;\n } else {\n hx = hx + dc * a;\n }\n a_total = a_total + a;\n // Spectrum from the recipe keys (reference 0.5 / 2); clamps twin fp_ori_erosion.\n a = a * clamp(e.erosion_gain, 0.03125, 1.0);\n f = f * clamp(e.erosion_lacunarity, 1.125, 4.0);\n }\n // Amplitude-sum normalization (twin of fp_ori_erosion): output spans [-m, m] at any\n // octave schedule, so erosion_strength means the same carve depth everywhere.\n if (a_total <= 1e-4) { return 0.0; }\n return hx * m / a_total;\n}\n\nfn ori_ext_height(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n return ori_ext_height_user(s, e, step_cells).xy;\n}\n\n// (h, h_coarse, user01): user01 = the erosion accumulation hx * 0.5 + 0.5 = the recipe's\n// erosion/ridge map (the sim op's ErosionMapOut / ctx.user), full-detail chain.\nfn ori_ext_height_user(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec3<f32> {\n let base = ori_ext_base(s, e, step_cells);\n // dir = curl of the recovered slope * slope_strength (dir = (ddy, -ddx) * s), twin of\n // ori_erosion_apply_with_base: dd_scale = base_scale_cells / W (heights and W in meters).\n // .x diffs feed the fine chain; .y (parent-band) diffs feed the coarse chain, exactly\n // like the parent LOD would compute them.\n let dd_scale = e.base_scale_cells / max(e.domain_w_m, 1e-6);\n let be = ori_ext_base(s + vec2<f32>(1.0, 0.0), e, step_cells);\n let bw = ori_ext_base(s - vec2<f32>(1.0, 0.0), e, step_cells);\n let bn = ori_ext_base(s + vec2<f32>(0.0, 1.0), e, step_cells);\n let bs = ori_ext_base(s - vec2<f32>(0.0, 1.0), e, step_cells);\n let dir_f = vec2<f32>((bn.x - bs.x), -(be.x - bw.x)) * dd_scale * e.slope_strength;\n // Division, twin of ori_erosion_apply_with_base (see the coordinate-drift note there).\n let pe = s / max(e.erosion_scale_cells, 1.0);\n // Mask on the pre-erosion base (MaskFromHeight twin; start < end enforced at the fill site).\n let m_f = smoothstep(e.mask_start_m, e.mask_end_m, base.x);\n let hx = ori_ext_erosion_chain(pe, dir_f, m_f, e, step_cells);\n // Coarse chain (geomorph target = the parent LOD's value): only distinct when 2x the\n // step actually band-limits something the fine step does not.\n let n_oct = clamp(e.erosion_octaves, 1.0, 8.0);\n let wl_min_ero = e.erosion_scale_cells / pow(clamp(e.erosion_lacunarity, 1.125, 4.0), n_oct - 1.0);\n let wl_min_base = e.base_scale_cells / exp2(clamp(e.base_octaves, 1.0, 8.0) - 1.0);\n var hxc = hx;\n var base_c = base.x;\n if (ori_ext_band(min(wl_min_ero, wl_min_base), step_cells * 2.0) < 1.0) {\n let dir_c = vec2<f32>((bn.y - bs.y), -(be.y - bw.y)) * dd_scale * e.slope_strength;\n let m_c = smoothstep(e.mask_start_m, e.mask_end_m, base.y);\n hxc = ori_ext_erosion_chain(pe, dir_c, m_c, e, step_cells * 2.0);\n base_c = base.y;\n }\n // The -0.5 carve bias is NOT masked - masked zones sink uniformly (the sim op does the\n // same; the water datum depends on it).\n return vec3<f32>(\n base.x + (hx - 0.5) * e.erosion_strength_m,\n base_c + (hxc - 0.5) * e.erosion_strength_m,\n clamp(hx * 0.5 + 0.5, 0.0, 1.0),\n );\n}\n\n// Recipe display-mode divergence check: max |decoded sim heights - #terrain generator| over\n// the whole grid. Runs once per heights upload (see TerrainPass::schedule_recipe_check); the\n// result decides whether the display renders the generator everywhere (recipe mode) or falls\n// back to Catmull-Rom of the sim samples (data mode, post-edited heights).\n// abs() keeps the value non-negative, and non-negative f32 bit patterns are monotonic as u32,\n// so atomicMax on the bits yields the true float max.\n// Requires ori_ext_terrain.wgsl (OriExtParams + ori_ext_height) + the noise prelude.\n\nstruct RecipeCheckUniform {\n min_z: f32,\n max_z: f32,\n sx: u32,\n sy: u32,\n ext: OriExtParams,\n};\n@group(0) @binding(0) var chk_height_tex: texture_2d<u32>;\n@group(0) @binding(1) var<uniform> chk: RecipeCheckUniform;\n@group(0) @binding(2) var<storage, read_write> chk_max_bits: atomic<u32>;\n\n@compute @workgroup_size(8, 8, 1)\nfn recipe_check_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n // All cells, border ring included: ori_recipe_compose takes function-based dir\n // taps beyond the grid, so the sim heights match the infinite generator everywhere.\n if (gid.x >= chk.sx || gid.y >= chk.sy) { return; }\n let v = textureLoad(chk_height_tex, vec2<i32>(gid.xy), 0).r;\n let h_sim = chk.min_z + (f32(v & 65535u) / 65535.0) * (chk.max_z - chk.min_z);\n // step 0.25 = finest bake LOD: every recipe octave at full weight (band factors 1).\n let h_gen = ori_ext_height(vec2<f32>(f32(gid.x), f32(gid.y)), chk.ext, 0.25).x;\n atomicMax(&chk_max_bits, bitcast<u32>(abs(h_sim - h_gen)));\n}\n"},{"label":"terrain_recipe_rings","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// ============================================================================================\n// Terrain display-extension generator: GPU twin of the #terrain recipe composition\n// (gradient-fbm base -> height mask -> OriErosion directional erosion). Used by the terrain\n// DISPLAY EXTENSION to continue a sim heightfield procedurally beyond the collision grid\n// (render-only, no sim state), and as the whole display surface in recipe mode.\n// Erosion kernel spec: docs/ori_erosion_spec.md; Fp sim twin: fp_ori_erosion.rs (keep in\n// sync). Requires the ori noise prelude (ori_hash2_cell).\n//\n// EXACTNESS CONTRACT: for a block built with TerrainHeightsNoise mode 4 (+ MaskFromHeight\n// + the OriErosion op / TerrainHeightsFromTerrain), this generator reproduces the sim field\n// bit-approximately (f32 vs Fp rounding only): same base fbm (seed + octave, gain, /total\n// normalization), same erosion dir recovery (central diff at +-1 cell,\n// n_dd = dh * base_scale_cells / W), same octave seed stream (seed ^ 0x9e3779b9 * (i+1)),\n// same jitter/gradient lattice hashes, same polynomial window.\n// ============================================================================================\n\nconst ORI_EXT_TAU: f32 = 6.28318530717959;\n\n// Gradient vector in [-1,1)^2 from one lattice hash (top/bottom 16 bits) - matches\n// fp_noise::gradient_at (Fp) at every lattice cell.\nfn ori_ext_gradient_at(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 32768.0 - 1.0;\n}\n\n// Gradient (Perlin) noise, zero at integer lattice points, quintic fade - the WGSL twin of\n// fp_noise::gradient2 (same hash family; f32 vs Fp rounding differs at sub-cm level).\nfn ori_ext_gradient2(p: vec2<f32>, seed: u32) -> f32 {\n let i = floor(p);\n let f = p - i;\n let c = vec2<i32>(i);\n let ga = ori_ext_gradient_at(c, seed);\n let gb = ori_ext_gradient_at(c + vec2<i32>(1, 0), seed);\n let gc = ori_ext_gradient_at(c + vec2<i32>(0, 1), seed);\n let gd = ori_ext_gradient_at(c + vec2<i32>(1, 1), seed);\n let va = dot(ga, f);\n let vb = dot(gb, f - vec2<f32>(1.0, 0.0));\n let vc = dot(gc, f - vec2<f32>(0.0, 1.0));\n let vd = dot(gd, f - vec2<f32>(1.0, 1.0));\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let a = va + (vb - va) * u.x;\n let b = vc + (vd - vc) * u.x;\n return a + (b - a) * u.y;\n}\n\n// OriErosion feature point offset inside a cell: 0.5 + jitter per axis, jitter in\n// [-0.45, 0.45) - twin of fp_ori_erosion::feature_offset.\nfn ori_ext_feature_offset(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(0.5) + (vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 65536.0 - 0.5) * 0.9;\n}\n\n// One OriErosion directional wave sample (value, ddx, ddy) - twin of\n// fp_ori_erosion::ori_erosion_sample (spec section 3): 5x5 jittered feature lattice,\n// polynomial window (1 - d2/4)^8 with EXACT zero support at R = 2 (the d2 >= 4 skip\n// drops only exact zeros - no truncation seams; the bell matches the reference\n// exp(-2 d2) including its tail, so grooves stay correlated across neighboring cells\n// and flow smoothly into each other), 1/tau-convention gradient along dir.\nfn ori_ext_erosion_sample(p: vec2<f32>, dir: vec2<f32>, seed: u32) -> vec3<f32> {\n let ip = floor(p);\n let fp_ = p - ip;\n let ci = vec2<i32>(ip);\n var v = 0.0;\n var g = vec2<f32>(0.0);\n var wt = 0.0;\n for (var i = -2; i <= 2; i = i + 1) {\n for (var k = -2; k <= 2; k = k + 1) {\n let q = ori_ext_feature_offset(ci + vec2<i32>(i, k), seed);\n let r = fp_ - vec2<f32>(f32(i), f32(k)) - q;\n let d2 = dot(r, r);\n if (d2 >= 4.0) { continue; }\n let t = 1.0 - d2 * 0.25;\n // Same x64-scaled form as the Fp twin (w = (8 t^4)^2 = 64 t^8); the scale cancels\n // in the weighted average, and matching the arithmetic shape keeps rounding aligned.\n let t2 = t * t;\n let t4s = t2 * t2 * 8.0;\n let w = t4s * t4s;\n wt = wt + w;\n // Phase wrapped to one period before the tau multiply (exact: cos period 1) - keeps\n // the trig argument in [0, tau) on both twins, bounding f32-vs-Fp drift (see the Fp\n // twin in fp_ori_erosion.rs).\n // Phase snapped to the exact 1/1024 grid (floor), twin of fp_ori_erosion: both\n // sides evaluate the SAME cos bucket, so residual positional drift cannot reach\n // the height output.\n let phase = floor(fract(dot(r, dir)) * 1024.0) / 1024.0 * ORI_EXT_TAU;\n v = v + cos(phase) * w;\n // 1/tau gradient convention, twin of fp_ori_erosion (branch_strength absorbs it;\n // keeps feedback magnitudes O(1) so positional Fp-vs-f32 drift stays bounded).\n g = g - sin(phase) * w * dir;\n }\n }\n // wt > 0 by construction: the own-cell feature is within sqrt(1.805) < R = 2 of any\n // sample (locked by fp_ori_erosion support tests).\n return vec3<f32>(v, g) / wt;\n}\n\n// Display-extension generator params (zeroed radius = disabled; see BakeUniform).\n// 20 f32 = 80 bytes (uniform address space rounds nested struct sizes to 16). Every Rust\n// fill site is a [f32; 20] in OriExtParams field order - keep them in lockstep.\nstruct OriExtParams {\n radius_cells: f32, // extension radius in sim cells (0 = off)\n base_scale_cells: f32, // base-noise feature size in cells\n base_amp_m: f32, // base amplitude in meters (heights = (fbm*0.5+0.5)*amp)\n base_octaves: f32, // gradient fbm octaves\n base_gain: f32, // per-octave amplitude factor (0.1 near-mono smooth; classic 0.5)\n erosion_scale_cells: f32,\n erosion_octaves: f32,\n erosion_strength_m: f32,\n slope_strength: f32,\n branch_strength: f32,\n mask_start_m: f32, // erosion amplitude mask: smoothstep(start, end, base) like\n mask_end_m: f32, // TerrainHeightsMaskFromHeight on the pre-erosion base\n base_seed: f32,\n erosion_seed: f32, // sim op seed (the scene may use a different stream than the base)\n domain_w_m: f32, // noise unit-domain width in meters (samples_x * cell_m); dir scale\n // > 0.5 = recipe display mode: the generator IS the display content everywhere (inside the\n // sim grid too - no Catmull-Rom, no boundary). Set per asset after the divergence check\n // verifies the sim heights match this recipe (see TerrainPass::ensure_terrain_asset).\n // Keep recipe_mode at flat index 15: terrain_tiles injects it as ext[15] per asset.\n recipe_mode: f32,\n erosion_gain: f32, // per-octave erosion amplitude factor (reference 0.5)\n erosion_lacunarity: f32, // per-octave erosion frequency factor (reference 2)\n _pad_a: f32,\n _pad_b: f32,\n};\n\n// Band-limit weight for one octave: 1 when the octave wavelength is well above the sample\n// spacing, fading to 0 approaching Nyquist. Baked per tile LOD; without this, octaves finer\n// than a coarse ring's spacing alias into DIFFERENT content per ring = straight seams at\n// ring boundaries (and the geomorph can't hide them if h_coarse == h).\nfn ori_ext_band(wavelength_cells: f32, step_cells: f32) -> f32 {\n return smoothstep(2.0, 4.0, wavelength_cells / max(step_cells, 1e-3));\n}\n\n// Base heights (meters, terrain-local like decode_height output). EXACT twin of\n// fp_noise::gradient_fbm2 (freq x2, amp x gain, per-octave seed = seed + i, sum / total) so\n// a sim block built with TerrainHeightsNoise mode 4 continues bit-consistently (f32 vs Fp\n// rounding aside) into the infinite field.\n// Returns (h_m, h_coarse_m): the coarse variant band-limits at 2x the sample spacing = the\n// parent LOD's value, restoring pop-free geomorph for extension tiles. Normalization uses\n// the UNfaded total so amplitude stays consistent with the sim block at any LOD (faded\n// octaves contribute their zero mean).\nfn ori_ext_base(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n let seed = u32(e.base_seed);\n // Division, not s * (1/scale): twin of terrain_gen::noise_heights (the reciprocal's\n // truncation grows with |s| and diverges from the Fp sim on steep recipes).\n let p = s / max(e.base_scale_cells, 1.0);\n var n = 0.0;\n var nc = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var f = 1.0;\n let n_oct = i32(clamp(e.base_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let wl = e.base_scale_cells / f;\n let bf = ori_ext_band(wl, step_cells);\n let bc = ori_ext_band(wl, step_cells * 2.0);\n total = total + amp;\n if (bf > 0.0) {\n let v = ori_ext_gradient2(p * f, seed + u32(i));\n n = n + v * amp * bf;\n nc = nc + v * amp * bc;\n }\n amp = amp * e.base_gain;\n f = f * 2.0;\n }\n n = n / total;\n nc = nc / total;\n return vec2<f32>((n * 0.5 + 0.5) * e.base_amp_m, (nc * 0.5 + 0.5) * e.base_amp_m);\n}\n\n// One band-limited OriErosion accumulation (the octave loop of spec section 4) with its own\n// mask, dir and branch-feedback chain. `step_cells` selects the band weights: evaluating at\n// 2x the step reproduces the PARENT LOD's chain exactly, which is what makes the geomorph\n// identity (child h_coarse == parent h) hold bit-for-bit.\n//\n// The erosion octave is NOT zero-mean: its local mean is ~= the windowed cos average,\n// dc(t) ~= max(0, exp(-K t^2)) with t = |dir| and K = ORI_EROSION_DC_K (numeric fit locked\n// by fp_ori_erosion::tests::dc_fit_matches_constant) - near 1 on flats, ~0 on steep slopes.\n// Band-limited octaves must fade toward that DC, not toward 0, or coarse LOD rings sink by\n// up to 0.5*strength on flats = curved cliff shelves at ring boundaries. Full-detail output\n// (band weights 1) is unchanged by dc.\nfn ori_ext_erosion_chain(pe: vec2<f32>, dir: vec2<f32>, m: f32, e: OriExtParams, step_cells: f32) -> f32 {\n var hx = 0.0;\n var hd = vec2<f32>(0.0);\n var a = 0.5 * m;\n var a_total = 0.0;\n var f = 1.0;\n let seed = u32(e.erosion_seed);\n let n_oct = i32(clamp(e.erosion_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let bf = ori_ext_band(e.erosion_scale_cells / f, step_cells);\n // Direction snapped to the exact 1/256 grid (floor), twin of fp_ori_erosion: the\n // branch-feedback recursion re-synchronizes with the Fp sim every octave instead of\n // compounding f32-vs-Fp drift. Exact: |bd|*256 < 2^20 stays integer-exact in f32.\n let bd = floor((dir + vec2<f32>(hd.y, -hd.x) * e.branch_strength) * 256.0) / 256.0;\n let dc = exp(-4.26 * dot(bd, bd)); // ORI_EROSION_DC_K\n if (bf > 0.0) {\n // Twin of the sim octave seed stream: seed ^ (0x9e3779b9 * (1 + octave)), wrapping.\n let oseed = seed ^ (0x9e3779b9u * (1u + u32(i)));\n let v = ori_ext_erosion_sample(pe * f, bd, oseed);\n hx = hx + mix(dc, v.x, bf) * a;\n hd = hd + v.yz * a * f * bf;\n } else {\n hx = hx + dc * a;\n }\n a_total = a_total + a;\n // Spectrum from the recipe keys (reference 0.5 / 2); clamps twin fp_ori_erosion.\n a = a * clamp(e.erosion_gain, 0.03125, 1.0);\n f = f * clamp(e.erosion_lacunarity, 1.125, 4.0);\n }\n // Amplitude-sum normalization (twin of fp_ori_erosion): output spans [-m, m] at any\n // octave schedule, so erosion_strength means the same carve depth everywhere.\n if (a_total <= 1e-4) { return 0.0; }\n return hx * m / a_total;\n}\n\nfn ori_ext_height(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n return ori_ext_height_user(s, e, step_cells).xy;\n}\n\n// (h, h_coarse, user01): user01 = the erosion accumulation hx * 0.5 + 0.5 = the recipe's\n// erosion/ridge map (the sim op's ErosionMapOut / ctx.user), full-detail chain.\nfn ori_ext_height_user(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec3<f32> {\n let base = ori_ext_base(s, e, step_cells);\n // dir = curl of the recovered slope * slope_strength (dir = (ddy, -ddx) * s), twin of\n // ori_erosion_apply_with_base: dd_scale = base_scale_cells / W (heights and W in meters).\n // .x diffs feed the fine chain; .y (parent-band) diffs feed the coarse chain, exactly\n // like the parent LOD would compute them.\n let dd_scale = e.base_scale_cells / max(e.domain_w_m, 1e-6);\n let be = ori_ext_base(s + vec2<f32>(1.0, 0.0), e, step_cells);\n let bw = ori_ext_base(s - vec2<f32>(1.0, 0.0), e, step_cells);\n let bn = ori_ext_base(s + vec2<f32>(0.0, 1.0), e, step_cells);\n let bs = ori_ext_base(s - vec2<f32>(0.0, 1.0), e, step_cells);\n let dir_f = vec2<f32>((bn.x - bs.x), -(be.x - bw.x)) * dd_scale * e.slope_strength;\n // Division, twin of ori_erosion_apply_with_base (see the coordinate-drift note there).\n let pe = s / max(e.erosion_scale_cells, 1.0);\n // Mask on the pre-erosion base (MaskFromHeight twin; start < end enforced at the fill site).\n let m_f = smoothstep(e.mask_start_m, e.mask_end_m, base.x);\n let hx = ori_ext_erosion_chain(pe, dir_f, m_f, e, step_cells);\n // Coarse chain (geomorph target = the parent LOD's value): only distinct when 2x the\n // step actually band-limits something the fine step does not.\n let n_oct = clamp(e.erosion_octaves, 1.0, 8.0);\n let wl_min_ero = e.erosion_scale_cells / pow(clamp(e.erosion_lacunarity, 1.125, 4.0), n_oct - 1.0);\n let wl_min_base = e.base_scale_cells / exp2(clamp(e.base_octaves, 1.0, 8.0) - 1.0);\n var hxc = hx;\n var base_c = base.x;\n if (ori_ext_band(min(wl_min_ero, wl_min_base), step_cells * 2.0) < 1.0) {\n let dir_c = vec2<f32>((bn.y - bs.y), -(be.y - bw.y)) * dd_scale * e.slope_strength;\n let m_c = smoothstep(e.mask_start_m, e.mask_end_m, base.y);\n hxc = ori_ext_erosion_chain(pe, dir_c, m_c, e, step_cells * 2.0);\n base_c = base.y;\n }\n // The -0.5 carve bias is NOT masked - masked zones sink uniformly (the sim op does the\n // same; the water datum depends on it).\n return vec3<f32>(\n base.x + (hx - 0.5) * e.erosion_strength_m,\n base_c + (hxc - 0.5) * e.erosion_strength_m,\n clamp(hx * 0.5 + 0.5, 0.0, 1.0),\n );\n}\n\n// Recipe surface-input ring stack: K camera-following toroidal clipmap rings, each 2x\n// coarser (texel_cells = 2^k), each one 512x512 layer of an rgba8 texture array - RGB =\n// world-space geometric normals, A = the recipe erosion map (ctx.user). The last layer is\n// the STATIC TERMINAL level (today's L1 semantics): texel_cells = max(2^(K-1), span/512),\n// origin fixed on the grid center, filled once per param change, edge-clamped at sample\n// time, never scrolls. Coarser rings ARE the mip chain - consumers pick a level by pixel\n// footprint + camera distance and blend the two adjacent layers trilinear-style.\n//\n// Rings store texel t of level k at layer texel (t mod 512) (per-axis). Camera movement\n// refills only the newly exposed edge-strip rects; a strip routinely crosses the wrapped\n// 512 boundary, so the destination wrap is per-texel, never per-rect. Sampling relies on\n// Repeat-addressing samplers: inside a fully valid window, texture texels 511 and 0 always\n// hold world-adjacent data, so hardware bilinear is seam-free without aprons.\n//\n// Pass 1 (ring_heights): one ori_ext_height_user eval per texel over the rect's +-1-texel\n// halo into an rg32float scratch (rect-local coords; halo texels evaluate the generator\n// directly - no neighbor dependency). step_cells band-limits the generator per level.\n// Pass 2 (ring_normals): central diffs of scratch -> world-space normal (the\n// bake_terrain_normals formula, normalize(-dhx, 2*texel_cells*cell_m, dhy)) packed\n// n*0.5+0.5 with erosion in alpha, stored to the layer at the wrapped destination.\n// Requires ori_ext_terrain.wgsl (+ noise prelude).\n\nstruct RingFillUniform {\n ext: OriExtParams,\n rect_texel_x: f32, // world level-texel coords of the rect origin (not wrapped)\n rect_texel_y: f32,\n rect_w: f32,\n rect_h: f32,\n texel_cells: f32, // cells per texel of this level (rings 2^k, terminal may be fractional)\n cell_m: f32,\n step_cells: f32, // generator band-limit step (ring 0 = 0.25 full detail, else texel_cells)\n layer: f32, // destination array layer\n dst_wrap: f32, // 1 = toroidal ring (dst = wt mod 512), 0 = terminal (dst = gid, full rect)\n samples_x: f32, // sim grid samples (surface-weights pass: uv + world mapping)\n origin_world_x: f32, // terrain world origin (render meters; surface-weights pass)\n origin_world_y: f32,\n origin_world_z: f32,\n user_detail01: f32, // SetTerrainErosionDetailPercent/100 clamped 0..1: scales the erosion\n // MAP deviation so splat/grass coloring follows the displayed depth\n _pad1: f32,\n _pad2: f32,\n};\n@group(0) @binding(0) var<uniform> ring: RingFillUniform;\n@group(0) @binding(1) var ring_scratch_out: texture_storage_2d<rg32float, write>;\n\n@compute @workgroup_size(8, 8, 1)\nfn ring_heights(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= u32(ring.rect_w) + 2u || gid.y >= u32(ring.rect_h) + 2u) { return; }\n // Scratch texel (0,0) = rect texel (-1,-1): the +-1 ring the normal diffs need.\n let cell = (vec2<f32>(ring.rect_texel_x, ring.rect_texel_y) + vec2<f32>(gid.xy) - 1.0)\n * ring.texel_cells;\n let hu = ori_ext_height_user(cell, ring.ext, ring.step_cells);\n // Erosion-map deviation follows the live detail knob (geometry is scaled via ext\n // erosion_strength at the fill site): at 0 the coloring goes neutral with the surface.\n let user = 0.5 + (hu.z - 0.5) * ring.user_detail01;\n textureStore(ring_scratch_out, vec2<i32>(gid.xy), vec4<f32>(hu.x, user, 0.0, 0.0));\n}\n\n@group(0) @binding(2) var ring_scratch_in: texture_2d<f32>;\n@group(0) @binding(3) var ring_atlas_out: texture_storage_2d_array<rgba8unorm, write>;\n\n/// World texel + wrapped destination for the rect texel at `gid`.\nfn ring_dst(gid: vec2<u32>) -> vec2<i32> {\n let wt = vec2<i32>(i32(ring.rect_texel_x), i32(ring.rect_texel_y)) + vec2<i32>(gid);\n if (ring.dst_wrap < 0.5) { return vec2<i32>(gid); }\n // Two's-complement & 511 == rem_euclid(512) for negatives (512 divides 2^32).\n return wt & vec2<i32>(511);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn ring_normals(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= u32(ring.rect_w) || gid.y >= u32(ring.rect_h)) { return; }\n let sp = vec2<i32>(gid.xy) + 1; // scratch coords (rect texel 0 = scratch texel 1)\n let hu = textureLoad(ring_scratch_in, sp, 0);\n let dhx = textureLoad(ring_scratch_in, sp + vec2<i32>(1, 0), 0).r\n - textureLoad(ring_scratch_in, sp - vec2<i32>(1, 0), 0).r;\n let dhy = textureLoad(ring_scratch_in, sp + vec2<i32>(0, 1), 0).r\n - textureLoad(ring_scratch_in, sp - vec2<i32>(0, 1), 0).r;\n // bake_terrain_normals twin at this level's sample spacing: the central diff spans\n // 2*texel_cells cells, so the vertical term is 2*texel_cells*cell_m.\n let n = normalize(vec3<f32>(\n -dhx, 2.0 * ring.texel_cells * max(ring.cell_m, 1e-3), dhy));\n textureStore(ring_atlas_out, ring_dst(gid.xy), i32(ring.layer), vec4<f32>(n * 0.5 + 0.5, hu.g));\n}\n"},{"label":"terrain_hiz_cull","code":"// Shared frustum + Hi-Z occlusion helpers, prepended via include_str! concat to consumer\n// shaders (static_lod_global_prefix_classify.wgsl, skinned_hiz_cull.wgsl). Consumers must\n// declare the module-scope bindings `camera_data: CameraUniform` and\n// `depth_pyramid: texture_2d<f32>` (WGSL module-scope declarations are order-independent).\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < 0.0;\n}\n\n// Frustum test against an arbitrary view_proj (perspective or ortho \u2014 e.g. a shadow\n// cascade's light volume); plane extraction is form-agnostic.\nfn aabb_visible_vp(m: mat4x4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2),\n );\n for (var i = 0u; i < 6u; i = i + 1u) {\n if (aabb_outside_plane(planes[i], bmin, bmax)) { return false; }\n }\n return true;\n}\n\nfn aabb_visible(bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n return aabb_visible_vp(camera_data.view_proj, bmin, bmax);\n}\n\n// Hi-Z occlusion test against the last-frame MAX-depth pyramid ([0,1] depth, 0 = near, 1 = far).\n// Every coarse texel over-estimates occluder depth and `margin_m` pulls the instance toward the\n// camera before comparing, so all uncertainty (one frame of motion, TAA jitter, near-plane or\n// behind-camera corners, ortho cameras) resolves to \"visible\".\nfn hiz_visible(bmin: vec3<f32>, bmax: vec3<f32>, margin_m: f32) -> bool {\n // Margin math below assumes a perspective projection (clip.w = view depth).\n if (camera_data.proj[3][3] != 0.0) { return true; }\n var min_ndc = vec2<f32>(1e10, 1e10);\n var max_ndc = vec2<f32>(-1e10, -1e10);\n var nearest_z = 1e10;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let corner = vec3<f32>(\n select(bmin.x, bmax.x, (i & 1u) != 0u),\n select(bmin.y, bmax.y, (i & 2u) != 0u),\n select(bmin.z, bmax.z, (i & 4u) != 0u),\n );\n let clip = camera_data.view_proj * vec4<f32>(corner, 1.0);\n // Corner at/behind the camera plane, or within `margin_m` of it: depth is meaningless\n // there, keep visible (this also force-passes near-plane intersections).\n let w_adj = clip.w - margin_m;\n if (w_adj <= 1e-4) { return true; }\n // Pull the corner margin_m meters toward the camera along view depth:\n // clip.z = -c22*w + c32 => z(w - m) = clip.z + c22*m.\n let z_adj = clip.z + camera_data.proj[2][2] * margin_m;\n nearest_z = min(nearest_z, z_adj / w_adj);\n let ndc = clip.xy / clip.w;\n min_ndc = min(min_ndc, ndc);\n max_ndc = max(max_ndc, ndc);\n }\n if (nearest_z <= 0.0) { return true; }\n\n // NDC -> UV: y flips, so the y min/max swap sides (max_ndc.y becomes min_uv.y).\n var min_uv = vec2<f32>(min_ndc.x * 0.5 + 0.5, 0.5 - max_ndc.y * 0.5);\n var max_uv = vec2<f32>(max_ndc.x * 0.5 + 0.5, 0.5 - min_ndc.y * 0.5);\n min_uv = clamp(min_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n max_uv = clamp(max_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n\n // Pyramid mip0 is half the scene resolution (first reduction happens while reading live\n // depth); all footprint math below is in pyramid-texel space, so that only shifts every\n // selection one level coarser in screen terms (minimum granularity 2 screen px).\n let pyr_res = textureDimensions(depth_pyramid);\n let size_uv = max_uv - min_uv;\n let size_px = max(size_uv.x * f32(pyr_res.x), size_uv.y * f32(pyr_res.y));\n let num_mips = textureNumLevels(depth_pyramid);\n // Pick the mip where the footprint spans <= 2 texels per axis so the 2x2 gather covers it;\n // bump once if misalignment still crosses a third texel (guaranteed enough at half size).\n var mip = u32(clamp(ceil(log2((size_px + 1e-6) / 2.0)), 0.0, f32(num_mips - 1u)));\n var mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n var px00 = vec2<i32>(min_uv * mip_size);\n var px11 = vec2<i32>(max_uv * mip_size);\n if (px11.x > px00.x + 1 || px11.y > px00.y + 1) {\n mip = min(mip + 1u, num_mips - 1u);\n mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n px00 = vec2<i32>(min_uv * mip_size);\n px11 = vec2<i32>(max_uv * mip_size);\n }\n let d00 = textureLoad(depth_pyramid, px00, i32(mip)).r;\n let d01 = textureLoad(depth_pyramid, vec2<i32>(px00.x, px11.y), i32(mip)).r;\n let d10 = textureLoad(depth_pyramid, vec2<i32>(px11.x, px00.y), i32(mip)).r;\n let d11 = textureLoad(depth_pyramid, px11, i32(mip)).r;\n let occluder_depth = max(max(d00, d01), max(d10, d11));\n return nearest_z <= occluder_depth;\n}\n\n// Terrain tile Hi-Z occlusion cull (loaded with shaders/hiz_shared.wgsl prepended, which\n// supplies CameraUniform + hiz_visible against the last-frame MAX-depth pyramid).\n//\n// One dispatch per terrain object over its tile range in the shared instance buffer. The\n// CPU walk already frustum-culled the selection (25 m margin), so only the Hi-Z test runs\n// here. Tile AABBs: XZ from corner/quad_step/clamp_max, Y from the per-atlas-slot height\n// bounds the bake pass maintains (terrain-local meters, orderable-u32 encoded; ancestor\n// fallback tiles read the ancestor slot = a superset envelope) with the skirt below.\n// Survivors compact into `culled[start + n]` and bump the terrain's indirect instance\n// count; the gbuffer VS indexes `tiles[instance_index + terrain.tile_base]` with\n// tile_base = start, so first_instance stays 0 (no INDIRECT_FIRST_INSTANCE dependency).\n\n// Mirrors terrain_tiles::TileInstanceGpu (48 bytes).\nstruct TileInst {\n corner: vec2<f32>,\n quad_step: f32,\n _pad0: f32,\n atlas_base: vec2<f32>,\n atlas_step: f32,\n morph_start: f32,\n morph_end: f32,\n skirt_depth: f32,\n clamp_max: vec2<f32>,\n};\n\nstruct TerrainCullParams {\n start: u32, // tile range in the shared instance buffer\n count: u32,\n slots_dim: u32, // atlas slots per row (minmax addressing)\n tile_texels: u32, // atlas texels per slot edge\n origin_y: f32, // terrain world origin Y (heights are terrain-local meters)\n margin_m: f32, // hiz_visible margin (TAA jitter + one frame of motion)\n _pad0: f32,\n _pad1: f32,\n};\n\nstruct DrawIndexedIndirect {\n index_count: u32,\n instance_count: atomic<u32>,\n first_index: u32,\n base_vertex: i32,\n first_instance: u32,\n};\n\n@group(0) @binding(0) var<uniform> camera_data: CameraUniform;\n@group(0) @binding(1) var<uniform> params: TerrainCullParams;\n@group(0) @binding(2) var<storage, read> tiles: array<TileInst>;\n@group(0) @binding(3) var<storage, read> tile_minmax: array<u32>;\n@group(0) @binding(4) var<storage, read_write> culled: array<TileInst>;\n@group(0) @binding(5) var<storage, read_write> indirect: DrawIndexedIndirect;\n@group(0) @binding(6) var depth_pyramid: texture_2d<f32>;\n\nfn ori_orderable_decode(u: u32) -> f32 {\n return select(bitcast<f32>(~u), bitcast<f32>(u ^ 0x80000000u), (u & 0x80000000u) != 0u);\n}\n\n@compute @workgroup_size(64)\nfn terrain_hiz_cull(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.count) { return; }\n let t = tiles[params.start + gid.x];\n // atlas_base = slot origin + 1 (apron); integer division recovers the slot even for\n // ancestor-fallback tiles whose base points inside the ancestor's region.\n let sb = vec2<u32>(t.atlas_base - vec2<f32>(1.0));\n let slot = 2u * (sb.y / params.tile_texels * params.slots_dim + sb.x / params.tile_texels);\n let h_min = ori_orderable_decode(tile_minmax[slot]);\n let h_max = ori_orderable_decode(tile_minmax[slot + 1u]);\n // Rows walk toward -Z from the tile corner; clamp_max collapses partial edge tiles.\n let bmin = vec3<f32>(\n t.corner.x,\n params.origin_y + h_min - t.skirt_depth,\n t.corner.y - t.clamp_max.y * t.quad_step);\n let bmax = vec3<f32>(\n t.corner.x + t.clamp_max.x * t.quad_step,\n params.origin_y + h_max,\n t.corner.y);\n if (hiz_visible(bmin, bmax, params.margin_m)) {\n let n = atomicAdd(&indirect.instance_count, 1u);\n culled[params.start + n] = t;\n }\n}\n"},{"label":"surface_array_blit","code":"// Fullscreen blit for terrain surface-array assembly: resizes arbitrary source images into\n// texture-array layers and walks each layer's mip chain (render target = one layer+mip view).\n// Filtering is the sampler's bilinear; sRGB conversion rides the view formats (sRGB source\n// decode on sample, sRGB target encode on write). Writing vec4 to an rg8unorm target keeps\n// just .rg (normal array).\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var src_sampler: sampler;\n\nstruct VsOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {\n // Fullscreen triangle.\n let uv = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));\n var out: VsOut;\n out.pos = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);\n out.uv = vec2<f32>(uv.x, 1.0 - uv.y);\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec4<f32> {\n return textureSampleLevel(src_tex, src_sampler, in.uv, 0.0);\n}\n"},{"label":"terrain_surface_blend","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// Built-in surface-array blend fragment for terrains with #terrain_surfaces + a weights\n// material and NO custom SetTerrainMaterial. Weights come from the clipmap-following\n// weight planes (two rgba8 = 8 surfaces, same slot/apron addressing + L0->L1 fade as the\n// normal atlas); surfaces sample from the albedo (srgb) + normal (rg) texture arrays with\n// per-surface world-XZ tiling. Top-3 weights blend (normalized; all-zero falls back to\n// surface 0). Pairs with the shared terrain_vertex.wgsl VS.\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nstruct TerrainUniform {\n samples_x: u32,\n samples_y: u32,\n cell_size: f32,\n min_z: f32,\n max_z: f32,\n clip_center_x: f32,\n clip_center_y: f32,\n debug_lod_view: f32,\n origin: vec3<f32>,\n tile_base: f32,\n // Ring-stack clip (terrain_recipe_rings.rs; array at group(1) binding(8)):\n // (level_count [0 = off], min_valid_level, terminal texel_cells, spare).\n clip2: vec4<f32>,\n // Per level (base_cell_x, base_cell_y, texel_cells, spare); ring bases 512-texel-aligned.\n clip_levels: array<vec4<f32>, 8>,\n};\n@group(1) @binding(1) var<uniform> terrain: TerrainUniform;\n@group(1) @binding(7) var terrain_normal_tex: texture_2d<f32>;\n// Recipe surface-input ring stack (normals rgb + erosion alpha; terrain_recipe_rings.rs).\n@group(1) @binding(8) var terrain_clip_stack: texture_2d_array<f32>;\n\nstruct SurfaceBlendUniform {\n tilings0: vec4<f32>, // meters per tile, surfaces 0..3\n tilings1: vec4<f32>, // surfaces 4..7\n count: f32,\n normal_strength: f32, // SetTerrainNormalStrengthPercent / 100 (0 = flat, 1 = authored)\n _pad1: f32,\n _pad2: f32,\n // Shading-detail knobs (erosion-showcase-derived defaults; weights alone can only choose\n // colors, so these luminance/normal terms live here):\n // knobs0 = (breakup_uv_freq, breakup_lum_amp, occlusion_strength, detail_normal_amp)\n // knobs1 = (breakup_seed, per_pixel_weights_enable, breakup_gain, height_blend_strength)\n knobs0: vec4<f32>,\n knobs1: vec4<f32>,\n};\n@group(2) @binding(0) var surf_albedo: texture_2d_array<f32>;\n@group(2) @binding(1) var surf_normal: texture_2d_array<f32>;\n// Ring-stack weight planes: one 512^2 layer per clip level, same toroidal addressing as\n// the clip stack (terrain_recipe_rings.rs).\n@group(2) @binding(2) var surf_weights_a: texture_2d_array<f32>;\n@group(2) @binding(3) var surf_weights_b: texture_2d_array<f32>;\n@group(2) @binding(4) var surf_sampler: sampler;\n@group(2) @binding(5) var<uniform> surf: SurfaceBlendUniform;\n// ORM+height per surface (RGB = AO/rough/metal, A = height for blend sharpening).\n@group(2) @binding(6) var surf_orm: texture_2d_array<f32>;\n\nstruct VsOut {\n @location(0) world_pos: vec3<f32>,\n @location(1) world_n: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) ndc_z: f32,\n @location(4) local_pos: vec3<f32>,\n @location(5) local_n: vec3<f32>,\n @location(6) world_t: vec3<f32>,\n @location(7) world_b: vec3<f32>,\n @location(9) detail_w: f32,\n @location(10) quad_step: f32,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(11) cur_clip: vec4<f32>,\n @location(12) prev_clip: vec4<f32>,\n};\n\nstruct FsOut {\n @location(0) out_base: vec4<f32>,\n @location(1) out_normal: vec4<f32>,\n @location(2) out_orm: vec4<f32>,\n @location(3) out_velocity: vec2<f32>,\n};\n\n// Per-pixel surface weights: the user's `ori_surface_weights` fn (the same source the\n// terrain_surface_weights compute harness runs per weight-plane texel) is inlined here by\n// the Rust side and evaluated per FRAGMENT, erasing the weight-plane resolution limit near\n// the camera. The baked planes remain the minification/far-field fallback. The static\n// (no-user-shader) pipeline variant injects a stub + ORI_PER_PIXEL_WEIGHTS = false.\nstruct OriSurfaceWeightsCtx {\n world_pos: vec3<f32>,\n height_m: f32,\n slope01: f32,\n erosion: f32,\n cell_uv: vec2<f32>,\n};\nstruct OriSurfaceWeights {\n w0: vec4<f32>, // surfaces 0..3\n w1: vec4<f32>, // surfaces 4..7\n};\nconst ORI_PER_PIXEL_WEIGHTS: bool = false;\nfn ori_surface_weights(ctx: OriSurfaceWeightsCtx) -> OriSurfaceWeights {\n var w: OriSurfaceWeights;\n return w;\n}\n\n// ---- Recipe surface-input ring stack (terrain_recipe_rings.rs; math twins the\n// terrain_material16.wgsl stack helpers, read through surf_sampler - also Repeat, so\n// hardware bilinear wraps seam-free inside a valid ring window). One pick per fragment\n// drives the clip sample AND both weight planes.\nstruct OriStackPick { l0: i32, l1: i32, f: f32 };\n\nfn ori_sb_stack_pick(cells: vec2<f32>) -> OriStackPick {\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n let d = max(abs(cells.x - terrain.clip_center_x), abs(cells.y - terrain.clip_center_y));\n let lf_min = log2(max(minify, 2.0)) - 1.0;\n let lf_dist = log2(max(d * (1.0 / 112.0), 1.0));\n let lf = clamp(max(lf_min, lf_dist), terrain.clip2.y, terrain.clip2.x - 1.0);\n var p: OriStackPick;\n p.l0 = i32(lf);\n p.l1 = min(p.l0 + 1, i32(terrain.clip2.x) - 1);\n p.f = fract(lf);\n return p;\n}\n\nfn ori_stack_uv(cells: vec2<f32>, lv: i32) -> vec2<f32> {\n let pl = terrain.clip_levels[lv];\n var rel = (cells - pl.xy) / max(pl.z, 1e-3);\n // Terminal level: no wrap - clamp to the first/last texel center (old L1 edge clamp).\n if (lv == i32(terrain.clip2.x) - 1) { rel = clamp(rel, vec2<f32>(0.0), vec2<f32>(511.0)); }\n return (rel + 0.5) / 512.0;\n}\n\nfn ori_sb_clip_stack(cells: vec2<f32>, p: OriStackPick) -> vec4<f32> {\n return mix(\n textureSampleLevel(terrain_clip_stack, surf_sampler, ori_stack_uv(cells, p.l0), p.l0, 0.0),\n textureSampleLevel(terrain_clip_stack, surf_sampler, ori_stack_uv(cells, p.l1), p.l1, 0.0),\n p.f);\n}\n\nfn ori_weights_stack(tex: texture_2d_array<f32>, cells: vec2<f32>, p: OriStackPick) -> vec4<f32> {\n return mix(\n textureSampleLevel(tex, surf_sampler, ori_stack_uv(cells, p.l0), p.l0, 0.0),\n textureSampleLevel(tex, surf_sampler, ori_stack_uv(cells, p.l1), p.l1, 0.0),\n p.f);\n}\n\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(@builtin(position) clip_position: vec4<f32>, in: VsOut) -> FsOut {\n // Planar reflection: clip below-plane terrain (same rule as gbuffer.wgsl) so the mirrored\n // camera never sees underground tiles/skirts.\n if (camera.reflection_clip_enabled > 0.5 && in.world_pos.y < camera.reflection_clip_y) {\n discard;\n }\n let n_tex = vec2<f32>(f32(terrain.samples_x), f32(terrain.samples_y));\n let cells = in.uv * (n_tex - vec2<f32>(1.0));\n // World-XZ gradients once, in uniform control flow: the top-3 surface indices flip between\n // adjacent pixels, so implicit derivatives of the divided UVs are garbage along material\n // edges (wrong mips = shimmer seams). Per-pick grads derive by the pick's tiling below.\n // exp2(mip_bias) folds in the upscaler mip bias (textureSampleGrad has no bias arg).\n let g_scale = exp2(camera.mip_bias);\n let gpx = dpdx(in.world_pos.xz) * g_scale;\n let gpy = dpdy(in.world_pos.xz) * g_scale;\n let ori_pick = ori_sb_stack_pick(cells);\n let wa = ori_weights_stack(surf_weights_a, cells, ori_pick);\n let wb = ori_weights_stack(surf_weights_b, cells, ori_pick);\n var w = array<f32, 8>(wa.x, wa.y, wa.z, wa.w, wb.x, wb.y, wb.z, wb.w);\n // Ring-stack sample (normal rgb + erosion alpha) feeds both the per-pixel weights ctx\n // and the shading-detail terms below. Uniform control flow throughout.\n let clip_hu = ori_sb_clip_stack(cells, ori_pick);\n if (ORI_PER_PIXEL_WEIGHTS && surf.knobs1.y > 0.5) {\n // Analytic weights need no L0 valid radius (they are exact everywhere, including the\n // display extension); only minification hands off to the prefiltered baked planes.\n var ctx: OriSurfaceWeightsCtx;\n ctx.world_pos = in.world_pos;\n ctx.height_m = in.world_pos.y - terrain.origin.y;\n // slope01 from the SMOOTHED terminal-level normal (the old L1's exact texel scale\n // and fill semantics): the weights compute derives slope from coarse-texel central\n // diffs, so authored slope thresholds are calibrated to cell-scale slope, not the\n // erosion-sharpened per-pixel normal (which reads steep in every micro-gully).\n let ori_term = i32(terrain.clip2.x) - 1;\n let cn = normalize(textureSampleLevel(terrain_clip_stack, surf_sampler,\n ori_stack_uv(cells, ori_term), ori_term, 0.0).rgb * 2.0 - vec3<f32>(1.0));\n let ny = clamp(cn.y, 1e-3, 1.0);\n ctx.slope01 = clamp(sqrt(max(1.0 - ny * ny, 0.0)) / ny, 0.0, 4.0) * 0.25;\n ctx.erosion = clip_hu.a;\n ctx.cell_uv = in.uv;\n let pw = ori_surface_weights(ctx);\n let ppx = array<f32, 8>(pw.w0.x, pw.w0.y, pw.w0.z, pw.w0.w, pw.w1.x, pw.w1.y, pw.w1.z, pw.w1.w);\n let minify = length(vec4<f32>(dpdx(cells), dpdy(cells)));\n let pf = smoothstep(2.0, 4.0, minify);\n for (var i = 0u; i < 8u; i = i + 1u) {\n w[i] = mix(clamp(ppx[i], 0.0, 1.0), w[i], pf);\n }\n }\n let count = u32(clamp(surf.count, 1.0, 8.0));\n var total = 0.0;\n for (var i = 0u; i < 8u; i = i + 1u) {\n if (i >= count) { w[i] = 0.0; }\n total = total + w[i];\n }\n if (total < 1e-4) {\n w[0] = 1.0;\n total = 1.0;\n }\n // Top-3 selection (indices of the largest weights; ties resolve to lower index).\n var i0 = 0u; var i1 = 0u; var i2 = 0u;\n var v0 = -1.0; var v1 = -1.0; var v2 = -1.0;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let v = w[i];\n if (v > v0) { i2 = i1; v2 = v1; i1 = i0; v1 = v0; i0 = i; v0 = v; }\n else if (v > v1) { i2 = i1; v2 = v1; i1 = i; v1 = v; }\n else if (v > v2) { i2 = i; v2 = v; }\n }\n let wsum = max(v0 + max(v1, 0.0) + max(v2, 0.0), 1e-5);\n let k0 = v0 / wsum;\n let k1 = max(v1, 0.0) / wsum;\n let k2 = max(v2, 0.0) / wsum;\n\n var tilings = array<f32, 8>(\n surf.tilings0.x, surf.tilings0.y, surf.tilings0.z, surf.tilings0.w,\n surf.tilings1.x, surf.tilings1.y, surf.tilings1.z, surf.tilings1.w);\n let t0 = max(tilings[i0], 0.1);\n let t1 = max(tilings[i1], 0.1);\n let t2 = max(tilings[i2], 0.1);\n let uv0 = in.world_pos.xz / t0;\n let uv1 = in.world_pos.xz / t1;\n let uv2 = in.world_pos.xz / t2;\n // Height-blend sharpening (splat-layers convention: ORM alpha = height, all-zero\n // heights degrade to plain weights): the material that \"sticks up\" wins the boundary,\n // snapping soft weight gradients into texture-shaped interlocks.\n let orm_s0 = textureSampleGrad(surf_orm, surf_sampler, uv0, i0, gpx / t0, gpy / t0);\n let orm_s1 = textureSampleGrad(surf_orm, surf_sampler, uv1, i1, gpx / t1, gpy / t1);\n let orm_s2 = textureSampleGrad(surf_orm, surf_sampler, uv2, i2, gpx / t2, gpy / t2);\n // Mask heights by weight presence: the top-3 pick pads with zero-weight layers when\n // fewer than 3 surfaces are active, and an unweighted layer's height must not push the\n // real layers out (all-zero result = black). Fallback to plain weights if ks collapses.\n let kmask = step(vec3<f32>(1e-4), vec3<f32>(k0, k1, k2));\n let hts = vec3<f32>(orm_s0.a, orm_s1.a, orm_s2.a) * kmask;\n let hmax = max(hts.x, max(hts.y, hts.z));\n let ks = max(\n vec3<f32>(k0, k1, k2) + (hts - vec3<f32>(hmax)) * surf.knobs1.w * step(0.001, hmax),\n vec3<f32>(0.0));\n let ksum = ks.x + ks.y + ks.z;\n let use_h = ksum > 1e-5;\n let s0 = select(k0, ks.x / max(ksum, 1e-5), use_h);\n let s1 = select(k1, ks.y / max(ksum, 1e-5), use_h);\n let s2 = select(k2, ks.z / max(ksum, 1e-5), use_h);\n var alb = textureSampleGrad(surf_albedo, surf_sampler, uv0, i0, gpx / t0, gpy / t0) * s0\n + textureSampleGrad(surf_albedo, surf_sampler, uv1, i1, gpx / t1, gpy / t1) * s1\n + textureSampleGrad(surf_albedo, surf_sampler, uv2, i2, gpx / t2, gpy / t2) * s2;\n let nrm_rg = textureSampleGrad(surf_normal, surf_sampler, uv0, i0, gpx / t0, gpy / t0).rg * s0\n + textureSampleGrad(surf_normal, surf_sampler, uv1, i1, gpx / t1, gpy / t1).rg * s1\n + textureSampleGrad(surf_normal, surf_sampler, uv2, i2, gpx / t2, gpy / t2).rg * s2;\n let orm_mix = orm_s0.rgb * s0 + orm_s1.rgb * s1 + orm_s2.rgb * s2;\n\n // Shading detail (slope breakup + occlusion terms; constants match the erosion-showcase live shader):\n // erosion occlusion sq(sat(ero+0.5)) -> AO, band-limited luminance breakup, and a\n // 4-tap fbm detail normal. Weights can only pick colors - these live here.\n let ero = clip_hu.a * 2.0 - 1.0;\n let occ01 = clamp(ero + 0.5, 0.0, 1.0);\n let out_ao = mix(1.0, occ01 * occ01, clamp(surf.knobs0.z, 0.0, 1.0));\n let bp = in.uv * surf.knobs0.x;\n let bstep = length(vec4<f32>(dpdx(bp), dpdy(bp)));\n let breakup = (ori_fbm2_bl(bp.x, bp.y, 8, u32(surf.knobs1.x), surf.knobs1.z, bstep) * 2.0 - 1.0) * 3.3658;\n alb = vec4<f32>(alb.rgb * max(1.0 + breakup * surf.knobs0.y, 0.0), alb.a);\n var n_detail = vec3<f32>(0.0);\n if (surf.knobs0.w > 0.0) {\n let dpn = in.world_pos.xz * 0.5;\n let dstep = length(vec4<f32>(dpdx(dpn), dpdy(dpn)));\n let e = 0.35;\n let d0 = ori_fbm2_bl(dpn.x - e, dpn.y, 6, 9u, 0.95, dstep);\n let d1 = ori_fbm2_bl(dpn.x + e, dpn.y, 6, 9u, 0.95, dstep);\n let d2 = ori_fbm2_bl(dpn.x, dpn.y - e, 6, 9u, 0.95, dstep);\n let d3 = ori_fbm2_bl(dpn.x, dpn.y + e, 6, 9u, 0.95, dstep);\n n_detail = vec3<f32>(d0 - d1, 0.0, d2 - d3) * surf.knobs0.w;\n }\n\n // Geometric normal = vertex normal (recipe display meshes carry full detail); surface\n // detail normal applies in the tangent frame like terrain_default.\n let n_geo = normalize(in.world_n);\n var t = normalize(in.world_t - n_geo * dot(n_geo, in.world_t));\n var b = cross(n_geo, t);\n if (dot(b, in.world_b) < 0.0) { b = -b; t = cross(b, n_geo); }\n let xy = (nrm_rg * 2.0 - vec2<f32>(1.0)) * surf.normal_strength;\n let z = sqrt(max(1.0 - dot(xy, xy), 0.0));\n let n = normalize(normalize(mat3x3<f32>(t, b, n_geo) * vec3<f32>(xy.x, xy.y, z)) + n_detail);\n\n var base_rgb = alb.rgb;\n\n var out: FsOut;\n out.out_base = vec4<f32>(base_rgb, 1.0);\n if (terrain.debug_lod_view > 0.5) {\n // /terrainlod tint (tier palette lives in terrain_default; approximate here by hue).\n let tier = clamp(log2(max(in.quad_step / max(terrain.cell_size, 1e-4), 1e-4)) + 2.0, 0.0, 7.0);\n out.out_base = vec4<f32>(mix(out.out_base.rgb, vec3<f32>(fract(tier * 0.35), 0.6, 1.0 - tier / 7.0), 0.65), 1.0);\n }\n out.out_normal = vec4<f32>(n, 1.0);\n // Sampled ORM (default layers = AO 1 / rough 0.9 / metal 0); erosion occlusion folds\n // into the AO channel.\n out.out_orm = vec4<f32>(orm_mix.r * out_ao, clamp(orm_mix.g, 0.02, 1.0), orm_mix.b, 0.0);\n out.out_velocity = gbuffer_velocity(in.cur_clip, in.prev_clip);\n return out;\n}\n"},{"label":"grass_gbuffer_default_injected","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// grass_gbuffer.wgsl\n// Minimal grass GBuffer shader (debug shading, no textures).\n\nstruct VertexIn {\n // Must match `Vertex` layout (static meshes)\n @location(0) position: vec3<f32>,\n @location(1) vnormal_oct: vec2<f32>, // packed vertex: octahedral snorm16 normal\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Grass instance (lean)\n @location(4) pos_scale: vec4<f32>, // xyz = world pos (meters), w = uniform scale\n @location(5) yaw_seed: vec2<f32>, // x = yaw radians, y = seed\n @location(6) normal_oct: vec2<f32>, // oct-encoded terrain normal (XZ)\n\n // GrassExtra (engine enrich pass; packed u8 params, see grass_enrich.wgsl)\n @location(7) extra: vec2<u32>,\n};\n\nfn unpack_extra_u8(v: u32, shift: u32) -> f32 {\n return f32((v >> shift) & 0xFFu) * (1.0 / 255.0);\n}\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera_bound: CameraUniform;\n// Injected bodies read `u_camera` by name; assigned from the binding per evaluation\n// (the prev velocity eval overrides time_seconds).\nvar<private> u_camera: CameraUniform;\n\n// Grass interaction (group 1): player benders (disks).\nconst GRASS_MAX_BENDERS: u32 = 16u;\nstruct GrassInteractionUniform {\n bender_count: u32,\n wind_amp_m: f32, // tip sway amplitude in meters at default height (SetGrassWindStrengthCm)\n variation_scale: f32, // global brightness variation multiplier (1.0 = default)\n wave_amp: f32, // meadow-wave relative height swing: (max-min)/(max+min), ~0.286 default\n // Each entry: (x_m, z_m, radius_m, strength01)\n // NOTE: keep as explicit fields (not an array) to avoid wgpu treating it as a runtime array.\n b0: vec4<f32>, b1: vec4<f32>, b2: vec4<f32>, b3: vec4<f32>,\n b4: vec4<f32>, b5: vec4<f32>, b6: vec4<f32>, b7: vec4<f32>,\n b8: vec4<f32>, b9: vec4<f32>, b10: vec4<f32>, b11: vec4<f32>,\n b12: vec4<f32>, b13: vec4<f32>, b14: vec4<f32>, b15: vec4<f32>,\n tint: vec4<f32>, // rgb = root color (linear, SetGrassRootColor), w = size variation multiplier\n look2: vec4<f32>, // x = meadow-wave freq multiplier (700cm/period), yzw = tip color (linear, SetGrassTipColor)\n // x = height mid multiplier ((min+max)/2 / 28cm), y = wind clock seconds (speed-scaled),\n // z = width knob multiplier (SetGrassBladeWidthPercent), w = reserved.\n look3: vec4<f32>,\n};\n\n// Meadow waves (UE-style \"perlin height\" trick): one shared low-frequency noise field drives\n// grass height, grass brightness, and the terrain ground mottle so they stay in phase.\n// Returns -1..1; ~7m features at freq_mult 1. Salt/base freq must match the terrain\n// shaders' ground tint noise (terrain scales p by the same freq multiplier).\nfn grass_meadow_wave(p_xz: vec2<f32>, freq_mult: f32) -> f32 {\n let p = p_xz * freq_mult;\n return (ori_value_noise2(p.x * 0.15, p.y * 0.15, 101u) - 0.5) * 2.0;\n}\n\n@group(1) @binding(0) var<uniform> u_grass_interaction_bound: GrassInteractionUniform;\n// Injected bodies read `u_grass_interaction` by name; the prev velocity eval overrides it\n// with u_grass_vel.prev_interaction.\nvar<private> u_grass_interaction: GrassInteractionUniform;\n// Prev-frame animation inputs for the velocity dual-eval. prev_time_pad sits BEFORE the\n// nested struct: wgpu sizes GrassInteractionUniform bindings with one extra vec4, so\n// nothing may rely on offsets after it.\nstruct GrassVelUniform {\n prev_time_pad: vec4<f32>, // x = previous frame time_seconds\n prev_interaction: GrassInteractionUniform,\n};\n@group(1) @binding(1) var<uniform> u_grass_vel: GrassVelUniform;\n\nfn grass_get_bender(i: u32) -> vec4<f32> {\n switch(i) {\n case 0u: { return u_grass_interaction.b0; }\n case 1u: { return u_grass_interaction.b1; }\n case 2u: { return u_grass_interaction.b2; }\n case 3u: { return u_grass_interaction.b3; }\n case 4u: { return u_grass_interaction.b4; }\n case 5u: { return u_grass_interaction.b5; }\n case 6u: { return u_grass_interaction.b6; }\n case 7u: { return u_grass_interaction.b7; }\n case 8u: { return u_grass_interaction.b8; }\n case 9u: { return u_grass_interaction.b9; }\n case 10u: { return u_grass_interaction.b10; }\n case 11u: { return u_grass_interaction.b11; }\n case 12u: { return u_grass_interaction.b12; }\n case 13u: { return u_grass_interaction.b13; }\n case 14u: { return u_grass_interaction.b14; }\n case 15u: { return u_grass_interaction.b15; }\n default: { return vec4<f32>(0.0); }\n }\n}\n\nfn grass_bender_offset_xz(p_xz: vec2<f32>, tip_t: f32) -> vec2<f32> {\n if (tip_t <= 0.0 || u_grass_interaction.bender_count == 0u) { return vec2<f32>(0.0, 0.0); }\n let strength_m: f32 = 0.65;\n var off: vec2<f32> = vec2<f32>(0.0, 0.0);\n for (var i: u32 = 0u; i < GRASS_MAX_BENDERS; i = i + 1u) {\n if (i >= u_grass_interaction.bender_count) { break; }\n let b = grass_get_bender(i);\n let r = b.z;\n if (r <= 1e-6) { continue; }\n let d = p_xz - b.xy;\n let d2 = dot(d, d);\n if (d2 >= r * r || d2 <= 1e-10) { continue; }\n let dist = sqrt(d2);\n let t = clamp(1.0 - dist / r, 0.0, 1.0);\n let dir = d / dist;\n off += dir * ((t * t) * (strength_m * b.w));\n }\n return off * tip_t;\n}\n\n// Material16 bindings (same layout as terrain/mesh materials).\n// Grass materials can be injected into this shader and use these bindings.\nstruct MaterialUniform { params4: array<vec4<f32>, 8>, };\n@group(2) @binding(0) var tex0: texture_2d<f32>;\n@group(2) @binding(1) var tex1: texture_2d<f32>;\n@group(2) @binding(2) var tex2: texture_2d<f32>;\n@group(2) @binding(3) var tex3: texture_2d<f32>;\n@group(2) @binding(4) var tex4: texture_2d<f32>;\n@group(2) @binding(5) var tex5: texture_2d<f32>;\n@group(2) @binding(6) var tex6: texture_2d<f32>;\n@group(2) @binding(7) var tex7: texture_2d<f32>;\n@group(2) @binding(8) var tex8: texture_2d<f32>;\n@group(2) @binding(9) var tex9: texture_2d<f32>;\n@group(2) @binding(10) var tex10: texture_2d<f32>;\n@group(2) @binding(11) var tex11: texture_2d<f32>;\n@group(2) @binding(12) var tex12: texture_2d<f32>;\n@group(2) @binding(13) var tex13: texture_2d<f32>;\n@group(2) @binding(14) var tex14: texture_2d<f32>;\n@group(2) @binding(15) var tex15: texture_2d<f32>;\n@group(2) @binding(16) var tex_sampler: sampler;\n@group(2) @binding(17) var<uniform> mat: MaterialUniform;\n\n// Engine injects optional module-scope WGSL here (helper fns/consts/structs).\n// Use `@module { ... }` in your local grass material snippet.\n\n\n\n\n// Vertex-stage context for injected `@vertex { ... }` snippets.\n// Snippet may mutate `world_pos` and/or `world_normal`.\nstruct GrassVertexCtx {\n instance_pos: vec3<f32>,\n scale: f32,\n yaw: f32,\n seed: f32,\n uv: vec2<f32>,\n rot_y: mat3x3<f32>,\n rot: mat3x3<f32>,\n up_world: vec3<f32>,\n // Raw mesh data (unscaled).\n in_pos: vec3<f32>,\n in_normal: vec3<f32>,\n in_tangent: vec4<f32>,\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n // GrassExtra clump/blade params (engine enrich pass; all 0..1).\n clump_hue: f32, // per-clump random (brightness variation)\n clump_facing: f32, // per-clump facing angle / tau\n clump_height: f32, // per-clump height multiplier random\n clump_tilt: f32, // per-clump lean amount random\n clump_edge: f32, // 0 = clump center, 1 = clump edge\n blade_hash: f32, // per-blade random (decorrelated from seed)\n blade_bend: f32, // per-blade bend random\n // Written by vertex body: fraction along blade height (drives fragment root->tip gradient).\n height01: f32,\n};\n\n// Fragment-stage context for injected `@fragment { ... }` snippets.\n// Snippet may mutate `base_color`, `orm`, and optionally `world_normal`.\nstruct GrassFragCtx {\n uv: vec2<f32>,\n seed: f32,\n front_facing: bool,\n instance_pos_xz: vec2<f32>,\n world_normal: vec3<f32>,\n base_color: vec4<f32>,\n orm: vec4<f32>,\n // From vertex stage: (height01, clump_hue, blade_hash, clump_edge).\n height01: f32,\n clump_hue: f32,\n blade_hash: f32,\n clump_edge: f32,\n};\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>, // (height01, clump_hue, blade_hash, clump_edge)\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n};\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\nfn rand01_cell(cell: vec2<i32>, salt: u32) -> f32 {\n let ux = bitcast<u32>(cell.x);\n let uy = bitcast<u32>(cell.y);\n let h = hash_u32(ux ^ (uy * 0x9E3779B9u) ^ (salt * 0x85EBCA6Bu));\n return f32(h & 0x00FFFFFFu) * (1.0 / 16777216.0);\n}\n\nfn value_noise_2d(p: vec2<f32>, salt: u32) -> f32 {\n let ip = vec2<i32>(floor(p));\n let fp = fract(p);\n let u = fp * fp * (vec2<f32>(3.0) - 2.0 * fp);\n let a = rand01_cell(ip + vec2<i32>(0, 0), salt);\n let b = rand01_cell(ip + vec2<i32>(1, 0), salt);\n let c = rand01_cell(ip + vec2<i32>(0, 1), salt);\n let d = rand01_cell(ip + vec2<i32>(1, 1), salt);\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn sign_nonzero(x: f32) -> f32 { return select(-1.0, 1.0, x >= 0.0); }\n\nfn oct_decode(p_in: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(p_in.x, 1.0 - abs(p_in.x) - abs(p_in.y), p_in.y);\n if (v.y < 0.0) {\n let x = (1.0 - abs(v.z)) * sign_nonzero(v.x);\n let z = (1.0 - abs(v.x)) * sign_nonzero(v.z);\n v = vec3<f32>(x, -v.y, z);\n }\n return normalize(v);\n}\n\nfn make_basis_from_up(up: vec3<f32>) -> mat3x3<f32> {\n let n = normalize(up);\n let a = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(n.y) > 0.999);\n let t = normalize(cross(a, n));\n let b = cross(n, t);\n // Columns: local X,Y,Z in world\n return mat3x3<f32>(t, n, b);\n}\n\n// Shared vertex prologue for both velocity evaluations (cur + prev frame state). Injected\n// bodies read/mutate `ctx` and the private `u_camera` / `u_grass_interaction` copies.\nfn grass_build_ctx(input: VertexIn) -> GrassVertexCtx {\n let yaw = input.yaw_seed.x;\n let c = cos(yaw);\n let s = sin(yaw);\n let R_y = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n\n let pos = input.pos_scale.xyz;\n let scale = input.pos_scale.w;\n let seed = input.yaw_seed.y;\n\n // Terrain normal from spawn (oct-encoded). Blend with world up for stability on steep slopes.\n let n_terrain = oct_decode(input.normal_oct);\n var up_world = normalize(mix(vec3<f32>(0.0, 1.0, 0.0), n_terrain, 0.85));\n up_world = normalize(vec3<f32>(up_world.x, max(up_world.y, 0.35), up_world.z));\n let R_align = make_basis_from_up(up_world);\n let R_yaw_local = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n let R = R_align * R_yaw_local;\n\n var ctx: GrassVertexCtx;\n ctx.instance_pos = pos;\n ctx.scale = scale;\n ctx.yaw = yaw;\n ctx.seed = seed;\n ctx.uv = input.uv;\n ctx.rot_y = R_y;\n ctx.rot = R;\n ctx.up_world = up_world;\n ctx.in_pos = input.position;\n ctx.in_normal = normalize(oct_decode(input.vnormal_oct));\n ctx.in_tangent = input.tangent;\n ctx.world_pos = pos + (ctx.rot * (ctx.in_pos * scale));\n ctx.world_normal = normalize(ctx.rot * ctx.in_normal);\n ctx.clump_hue = unpack_extra_u8(input.extra.x, 0u);\n ctx.clump_facing = unpack_extra_u8(input.extra.x, 8u);\n ctx.clump_height = unpack_extra_u8(input.extra.x, 16u);\n ctx.clump_tilt = unpack_extra_u8(input.extra.x, 24u);\n ctx.blade_hash = unpack_extra_u8(input.extra.y, 0u);\n ctx.blade_bend = unpack_extra_u8(input.extra.y, 8u);\n ctx.clump_edge = unpack_extra_u8(input.extra.y, 16u);\n ctx.height01 = clamp(input.uv.y, 0.0, 1.0);\n return ctx;\n}\n\n// Beyond this camera distance grass sway is ~a pixel of motion and the camera term (prev ==\n// cur world position) is accurate enough, so the second wind-body evaluation is skipped.\nconst GRASS_VEL_MAX_DIST_M: f32 = 40.0;\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n\n // Current-frame evaluation. Engine injects the optional vertex body below; snippets can\n // read/mutate `ctx` (movement, offsets, procedural deformation, etc.).\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var world_pos: vec3<f32>;\n var world_n: vec3<f32>;\n {\n var ctx = grass_build_ctx(input);\n {\n \n// Default injected grass vertex body.\n// Runs inside `vs_main` with `var ctx: GrassVertexCtx` in scope.\n\n// Default knobs (edit these, or copy this body into your custom @vertex snippet).\nlet height_scale: f32 = 1.75;\nlet width_scale: f32 = 0.55;\nlet base_lift_m: f32 = 0.04;\nlet tau: f32 = 6.28318530718;\nlet local_up = vec3<f32>(0.0, 1.0, 0.0);\n\n// Base blade transform (includes scale shaping).\nlet local_pos = vec3<f32>(\n ctx.in_pos.x * width_scale,\n ctx.in_pos.y * height_scale,\n ctx.in_pos.z * width_scale\n);\nlet local_n = normalize(vec3<f32>(\n ctx.in_normal.x / width_scale,\n ctx.in_normal.y / height_scale,\n ctx.in_normal.z / width_scale\n));\nctx.world_pos = ctx.instance_pos + (ctx.rot * (local_pos * ctx.scale));\nctx.world_normal = normalize(ctx.rot * local_n);\n\nlet r = fract(ctx.seed * 13.37);\nlet up_world = normalize(ctx.up_world);\n// Pin/bend factor should be based on actual blade height, not UVs (UVs vary across assets).\nlet height_local = dot(local_pos, local_up);\nlet tip = clamp(height_local / max(1e-6, height_scale), 0.0, 1.0);\nctx.height01 = tip;\nlet bend_t = pow(tip, 1.35);\nlet flutter_t = tip * tip * tip;\n\n// Coherent wind field (nearby blades bend together) via cheap value-noise.\n// Time = speed-scaled wind clock (look3.y); amplitude = SetGrassWindStrengthCm (wind_amp_m).\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec2<f32>(cos(angle), sin(angle));\n\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 2.0\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0;\nlet wave = sin(phase) + 0.45 * sin(phase * 1.9 + 1.7);\n\n// Permanent slight lean + animated bend.\nlet rest_dir = normalize(vec2<f32>(cos(r * 71.0), sin(r * 91.0)));\nlet rest = rest_dir * ((r - 0.5) * 0.06 * ctx.scale) * bend_t;\nlet wind_hf = ctx.scale * 6.6667; // custom meshes: sway follows spawn scale (calib 0.15)\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.36 + 0.88 * gust) * wind_hf * (0.85 + 0.30 * r);\nlet bend = wind_dir * (wave * bend_amp) * bend_t + rest;\nctx.world_pos.x += bend.x;\nctx.world_pos.z += bend.y;\n\n// Tip flutter (small, high frequency).\nlet flutter = wind_dir * sin(phase * 5.2 + r * 40.0) * (0.25 * u_grass_interaction.wind_amp_m * wind_hf) * flutter_t;\nctx.world_pos.x += flutter.x;\nctx.world_pos.z += flutter.y;\n\n// Player bending (disks). Use instance root so bending is anchored at the feet.\n// Use nonzero tip_t so stepping affects the whole blade (not just the tip).\nlet step_t = 0.35 + 0.65 * bend_t;\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, step_t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\n// Lift slightly so the blade base doesn't sink into terrain.\nctx.world_pos += up_world * (base_lift_m * ctx.scale);\n// Approximate length preservation: sideways bend causes slight droop.\nlet bend2 = dot(bend, bend);\nctx.world_pos -= up_world * (bend2 * (0.35 / max(0.15, height_scale * ctx.scale)));\n\n// Normal tilt based on bend direction.\nlet bend_dir3 = vec3<f32>(-bend.x, 0.0, -bend.y);\nlet bend_dir3_n = bend_dir3 * inverseSqrt(max(dot(bend_dir3, bend_dir3), 1e-8));\nlet tilt = clamp(length(bend) / max(0.18 * ctx.scale, 1e-6), 0.0, 0.65);\nctx.world_normal = normalize(ctx.world_normal + bend_dir3_n * tilt);\n\n\n\n }\n world_pos = ctx.world_pos;\n world_n = normalize(ctx.world_normal);\n out.grass_misc = vec4<f32>(ctx.height01, ctx.clump_hue, ctx.blade_hash, ctx.clump_edge);\n }\n\n // Previous-frame evaluation for the velocity output: same body, prev time + prev\n // bender/interaction state. Far grass keeps prev == cur (pure camera term).\n var world_pos_prev = world_pos;\n let cam_delta = input.pos_scale.xyz - u_camera_bound.camera_position;\n if (dot(cam_delta, cam_delta) <= GRASS_VEL_MAX_DIST_M * GRASS_VEL_MAX_DIST_M) {\n u_camera.time_seconds = u_grass_vel.prev_time_pad.x;\n u_grass_interaction = u_grass_vel.prev_interaction;\n var ctx = grass_build_ctx(input);\n {\n \n// Default injected grass vertex body.\n// Runs inside `vs_main` with `var ctx: GrassVertexCtx` in scope.\n\n// Default knobs (edit these, or copy this body into your custom @vertex snippet).\nlet height_scale: f32 = 1.75;\nlet width_scale: f32 = 0.55;\nlet base_lift_m: f32 = 0.04;\nlet tau: f32 = 6.28318530718;\nlet local_up = vec3<f32>(0.0, 1.0, 0.0);\n\n// Base blade transform (includes scale shaping).\nlet local_pos = vec3<f32>(\n ctx.in_pos.x * width_scale,\n ctx.in_pos.y * height_scale,\n ctx.in_pos.z * width_scale\n);\nlet local_n = normalize(vec3<f32>(\n ctx.in_normal.x / width_scale,\n ctx.in_normal.y / height_scale,\n ctx.in_normal.z / width_scale\n));\nctx.world_pos = ctx.instance_pos + (ctx.rot * (local_pos * ctx.scale));\nctx.world_normal = normalize(ctx.rot * local_n);\n\nlet r = fract(ctx.seed * 13.37);\nlet up_world = normalize(ctx.up_world);\n// Pin/bend factor should be based on actual blade height, not UVs (UVs vary across assets).\nlet height_local = dot(local_pos, local_up);\nlet tip = clamp(height_local / max(1e-6, height_scale), 0.0, 1.0);\nctx.height01 = tip;\nlet bend_t = pow(tip, 1.35);\nlet flutter_t = tip * tip * tip;\n\n// Coherent wind field (nearby blades bend together) via cheap value-noise.\n// Time = speed-scaled wind clock (look3.y); amplitude = SetGrassWindStrengthCm (wind_amp_m).\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec2<f32>(cos(angle), sin(angle));\n\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 2.0\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0;\nlet wave = sin(phase) + 0.45 * sin(phase * 1.9 + 1.7);\n\n// Permanent slight lean + animated bend.\nlet rest_dir = normalize(vec2<f32>(cos(r * 71.0), sin(r * 91.0)));\nlet rest = rest_dir * ((r - 0.5) * 0.06 * ctx.scale) * bend_t;\nlet wind_hf = ctx.scale * 6.6667; // custom meshes: sway follows spawn scale (calib 0.15)\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.36 + 0.88 * gust) * wind_hf * (0.85 + 0.30 * r);\nlet bend = wind_dir * (wave * bend_amp) * bend_t + rest;\nctx.world_pos.x += bend.x;\nctx.world_pos.z += bend.y;\n\n// Tip flutter (small, high frequency).\nlet flutter = wind_dir * sin(phase * 5.2 + r * 40.0) * (0.25 * u_grass_interaction.wind_amp_m * wind_hf) * flutter_t;\nctx.world_pos.x += flutter.x;\nctx.world_pos.z += flutter.y;\n\n// Player bending (disks). Use instance root so bending is anchored at the feet.\n// Use nonzero tip_t so stepping affects the whole blade (not just the tip).\nlet step_t = 0.35 + 0.65 * bend_t;\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, step_t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\n// Lift slightly so the blade base doesn't sink into terrain.\nctx.world_pos += up_world * (base_lift_m * ctx.scale);\n// Approximate length preservation: sideways bend causes slight droop.\nlet bend2 = dot(bend, bend);\nctx.world_pos -= up_world * (bend2 * (0.35 / max(0.15, height_scale * ctx.scale)));\n\n// Normal tilt based on bend direction.\nlet bend_dir3 = vec3<f32>(-bend.x, 0.0, -bend.y);\nlet bend_dir3_n = bend_dir3 * inverseSqrt(max(dot(bend_dir3, bend_dir3), 1e-8));\nlet tilt = clamp(length(bend) / max(0.18 * ctx.scale, 1e-6), 0.0, 0.65);\nctx.world_normal = normalize(ctx.world_normal + bend_dir3_n * tilt);\n\n\n\n }\n world_pos_prev = ctx.world_pos;\n }\n\n out.clip_position = u_camera_bound.view_proj * vec4<f32>(world_pos, 1.0);\n out.world_normal = world_n;\n out.uv = input.uv;\n out.seed = input.yaw_seed.y;\n out.instance_pos_xz = input.pos_scale.xz;\n out.cur_clip = u_camera_bound.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera_bound.prev_unjittered_view_proj * vec4<f32>(world_pos_prev, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>, // Rgba8Unorm\n @location(1) normal: vec4<f32>, // Rgba16Float\n @location(2) orm: vec4<f32>, // Rgba8Unorm\n @location(3) velocity: vec2<f32>, // Rg16Float\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @builtin(front_facing) front_facing: bool,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>,\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var out: GBufferOutput;\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n let n = normalize(world_normal) * select(-1.0, 1.0, front_facing);\n\n var ctx: GrassFragCtx;\n ctx.uv = uv;\n ctx.seed = seed;\n ctx.front_facing = front_facing;\n ctx.instance_pos_xz = instance_pos_xz;\n ctx.world_normal = n;\n ctx.base_color = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n ctx.orm = vec4<f32>(1.0, 1.0, 0.0, 0.0);\n ctx.height01 = grass_misc.x;\n ctx.clump_hue = grass_misc.y;\n ctx.blade_hash = grass_misc.z;\n ctx.clump_edge = grass_misc.w;\n\n // Engine injects the fragment body here.\n // Snippet can read tex0..tex15, mat.params4, and mutate `ctx`.\n {\n \n// Default injected grass fragment body (built-in proc blades + custom meshes\n// without an @fragment snippet).\n//\n// Color model (keep in sync with grass_fragment_card_body.wgsl):\n// albedo = mix(root, tip, gradient) * brightness-variation * meadow-wave ripple\n// Nothing else touches color: no hue shifts, no AO/roughness gradients, so the\n// SetGrassRootColor / SetGrassTipColor knobs stay WYSIWYG.\n\n// Root->tip gradient from the color knobs; per-blade jitter de-bands the gradient coordinate.\nlet grad_t = clamp(clamp(ctx.height01, 0.0, 1.0) + (ctx.blade_hash - 0.5) * 0.25, 0.0, 1.0);\nvar col = mix(u_grass_interaction.tint.rgb, u_grass_interaction.look2.yzw, grad_t);\n// Brightness Variation knob: +- brightness per blade and per ~0.5m clump (never hue).\nlet bright = (ctx.blade_hash - 0.5) * 0.30 + (ctx.clump_hue - 0.5) * 0.34;\ncol *= clamp(1.0 + bright * u_grass_interaction.variation_scale, 0.40, 1.85);\n// Meadow waves: brightness rides the same field as blade height (crests lighter, troughs\n// darker). Coupling derives from the Min/Max Height swing (wave_amp).\nlet meadow = grass_meadow_wave(ctx.instance_pos_xz, u_grass_interaction.look2.x);\ncol *= 1.0 + 0.5 * u_grass_interaction.wave_amp * meadow;\nctx.base_color = vec4<f32>(col, 1.0);\n// Flat AO (root darkening lives in the root color knob) and a single roughness.\nctx.orm = vec4<f32>(1.0, 0.75, 0.0, 0.0);\n\n\n }\n out.base_color = ctx.base_color;\n out.normal = vec4<f32>(normalize(ctx.world_normal), 1.0);\n out.orm = ctx.orm;\n\n // Match existing gbuffer convention.\n return out;\n}\n\n"},{"label":"grass_gbuffer_proc_injected","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// grass_gbuffer.wgsl\n// Minimal grass GBuffer shader (debug shading, no textures).\n\nstruct VertexIn {\n // Must match `Vertex` layout (static meshes)\n @location(0) position: vec3<f32>,\n @location(1) vnormal_oct: vec2<f32>, // packed vertex: octahedral snorm16 normal\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Grass instance (lean)\n @location(4) pos_scale: vec4<f32>, // xyz = world pos (meters), w = uniform scale\n @location(5) yaw_seed: vec2<f32>, // x = yaw radians, y = seed\n @location(6) normal_oct: vec2<f32>, // oct-encoded terrain normal (XZ)\n\n // GrassExtra (engine enrich pass; packed u8 params, see grass_enrich.wgsl)\n @location(7) extra: vec2<u32>,\n};\n\nfn unpack_extra_u8(v: u32, shift: u32) -> f32 {\n return f32((v >> shift) & 0xFFu) * (1.0 / 255.0);\n}\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera_bound: CameraUniform;\n// Injected bodies read `u_camera` by name; assigned from the binding per evaluation\n// (the prev velocity eval overrides time_seconds).\nvar<private> u_camera: CameraUniform;\n\n// Grass interaction (group 1): player benders (disks).\nconst GRASS_MAX_BENDERS: u32 = 16u;\nstruct GrassInteractionUniform {\n bender_count: u32,\n wind_amp_m: f32, // tip sway amplitude in meters at default height (SetGrassWindStrengthCm)\n variation_scale: f32, // global brightness variation multiplier (1.0 = default)\n wave_amp: f32, // meadow-wave relative height swing: (max-min)/(max+min), ~0.286 default\n // Each entry: (x_m, z_m, radius_m, strength01)\n // NOTE: keep as explicit fields (not an array) to avoid wgpu treating it as a runtime array.\n b0: vec4<f32>, b1: vec4<f32>, b2: vec4<f32>, b3: vec4<f32>,\n b4: vec4<f32>, b5: vec4<f32>, b6: vec4<f32>, b7: vec4<f32>,\n b8: vec4<f32>, b9: vec4<f32>, b10: vec4<f32>, b11: vec4<f32>,\n b12: vec4<f32>, b13: vec4<f32>, b14: vec4<f32>, b15: vec4<f32>,\n tint: vec4<f32>, // rgb = root color (linear, SetGrassRootColor), w = size variation multiplier\n look2: vec4<f32>, // x = meadow-wave freq multiplier (700cm/period), yzw = tip color (linear, SetGrassTipColor)\n // x = height mid multiplier ((min+max)/2 / 28cm), y = wind clock seconds (speed-scaled),\n // z = width knob multiplier (SetGrassBladeWidthPercent), w = reserved.\n look3: vec4<f32>,\n};\n\n// Meadow waves (UE-style \"perlin height\" trick): one shared low-frequency noise field drives\n// grass height, grass brightness, and the terrain ground mottle so they stay in phase.\n// Returns -1..1; ~7m features at freq_mult 1. Salt/base freq must match the terrain\n// shaders' ground tint noise (terrain scales p by the same freq multiplier).\nfn grass_meadow_wave(p_xz: vec2<f32>, freq_mult: f32) -> f32 {\n let p = p_xz * freq_mult;\n return (ori_value_noise2(p.x * 0.15, p.y * 0.15, 101u) - 0.5) * 2.0;\n}\n\n@group(1) @binding(0) var<uniform> u_grass_interaction_bound: GrassInteractionUniform;\n// Injected bodies read `u_grass_interaction` by name; the prev velocity eval overrides it\n// with u_grass_vel.prev_interaction.\nvar<private> u_grass_interaction: GrassInteractionUniform;\n// Prev-frame animation inputs for the velocity dual-eval. prev_time_pad sits BEFORE the\n// nested struct: wgpu sizes GrassInteractionUniform bindings with one extra vec4, so\n// nothing may rely on offsets after it.\nstruct GrassVelUniform {\n prev_time_pad: vec4<f32>, // x = previous frame time_seconds\n prev_interaction: GrassInteractionUniform,\n};\n@group(1) @binding(1) var<uniform> u_grass_vel: GrassVelUniform;\n\nfn grass_get_bender(i: u32) -> vec4<f32> {\n switch(i) {\n case 0u: { return u_grass_interaction.b0; }\n case 1u: { return u_grass_interaction.b1; }\n case 2u: { return u_grass_interaction.b2; }\n case 3u: { return u_grass_interaction.b3; }\n case 4u: { return u_grass_interaction.b4; }\n case 5u: { return u_grass_interaction.b5; }\n case 6u: { return u_grass_interaction.b6; }\n case 7u: { return u_grass_interaction.b7; }\n case 8u: { return u_grass_interaction.b8; }\n case 9u: { return u_grass_interaction.b9; }\n case 10u: { return u_grass_interaction.b10; }\n case 11u: { return u_grass_interaction.b11; }\n case 12u: { return u_grass_interaction.b12; }\n case 13u: { return u_grass_interaction.b13; }\n case 14u: { return u_grass_interaction.b14; }\n case 15u: { return u_grass_interaction.b15; }\n default: { return vec4<f32>(0.0); }\n }\n}\n\nfn grass_bender_offset_xz(p_xz: vec2<f32>, tip_t: f32) -> vec2<f32> {\n if (tip_t <= 0.0 || u_grass_interaction.bender_count == 0u) { return vec2<f32>(0.0, 0.0); }\n let strength_m: f32 = 0.65;\n var off: vec2<f32> = vec2<f32>(0.0, 0.0);\n for (var i: u32 = 0u; i < GRASS_MAX_BENDERS; i = i + 1u) {\n if (i >= u_grass_interaction.bender_count) { break; }\n let b = grass_get_bender(i);\n let r = b.z;\n if (r <= 1e-6) { continue; }\n let d = p_xz - b.xy;\n let d2 = dot(d, d);\n if (d2 >= r * r || d2 <= 1e-10) { continue; }\n let dist = sqrt(d2);\n let t = clamp(1.0 - dist / r, 0.0, 1.0);\n let dir = d / dist;\n off += dir * ((t * t) * (strength_m * b.w));\n }\n return off * tip_t;\n}\n\n// Material16 bindings (same layout as terrain/mesh materials).\n// Grass materials can be injected into this shader and use these bindings.\nstruct MaterialUniform { params4: array<vec4<f32>, 8>, };\n@group(2) @binding(0) var tex0: texture_2d<f32>;\n@group(2) @binding(1) var tex1: texture_2d<f32>;\n@group(2) @binding(2) var tex2: texture_2d<f32>;\n@group(2) @binding(3) var tex3: texture_2d<f32>;\n@group(2) @binding(4) var tex4: texture_2d<f32>;\n@group(2) @binding(5) var tex5: texture_2d<f32>;\n@group(2) @binding(6) var tex6: texture_2d<f32>;\n@group(2) @binding(7) var tex7: texture_2d<f32>;\n@group(2) @binding(8) var tex8: texture_2d<f32>;\n@group(2) @binding(9) var tex9: texture_2d<f32>;\n@group(2) @binding(10) var tex10: texture_2d<f32>;\n@group(2) @binding(11) var tex11: texture_2d<f32>;\n@group(2) @binding(12) var tex12: texture_2d<f32>;\n@group(2) @binding(13) var tex13: texture_2d<f32>;\n@group(2) @binding(14) var tex14: texture_2d<f32>;\n@group(2) @binding(15) var tex15: texture_2d<f32>;\n@group(2) @binding(16) var tex_sampler: sampler;\n@group(2) @binding(17) var<uniform> mat: MaterialUniform;\n\n// Engine injects optional module-scope WGSL here (helper fns/consts/structs).\n// Use `@module { ... }` in your local grass material snippet.\n\n\n\n\n// Vertex-stage context for injected `@vertex { ... }` snippets.\n// Snippet may mutate `world_pos` and/or `world_normal`.\nstruct GrassVertexCtx {\n instance_pos: vec3<f32>,\n scale: f32,\n yaw: f32,\n seed: f32,\n uv: vec2<f32>,\n rot_y: mat3x3<f32>,\n rot: mat3x3<f32>,\n up_world: vec3<f32>,\n // Raw mesh data (unscaled).\n in_pos: vec3<f32>,\n in_normal: vec3<f32>,\n in_tangent: vec4<f32>,\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n // GrassExtra clump/blade params (engine enrich pass; all 0..1).\n clump_hue: f32, // per-clump random (brightness variation)\n clump_facing: f32, // per-clump facing angle / tau\n clump_height: f32, // per-clump height multiplier random\n clump_tilt: f32, // per-clump lean amount random\n clump_edge: f32, // 0 = clump center, 1 = clump edge\n blade_hash: f32, // per-blade random (decorrelated from seed)\n blade_bend: f32, // per-blade bend random\n // Written by vertex body: fraction along blade height (drives fragment root->tip gradient).\n height01: f32,\n};\n\n// Fragment-stage context for injected `@fragment { ... }` snippets.\n// Snippet may mutate `base_color`, `orm`, and optionally `world_normal`.\nstruct GrassFragCtx {\n uv: vec2<f32>,\n seed: f32,\n front_facing: bool,\n instance_pos_xz: vec2<f32>,\n world_normal: vec3<f32>,\n base_color: vec4<f32>,\n orm: vec4<f32>,\n // From vertex stage: (height01, clump_hue, blade_hash, clump_edge).\n height01: f32,\n clump_hue: f32,\n blade_hash: f32,\n clump_edge: f32,\n};\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>, // (height01, clump_hue, blade_hash, clump_edge)\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n};\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\nfn rand01_cell(cell: vec2<i32>, salt: u32) -> f32 {\n let ux = bitcast<u32>(cell.x);\n let uy = bitcast<u32>(cell.y);\n let h = hash_u32(ux ^ (uy * 0x9E3779B9u) ^ (salt * 0x85EBCA6Bu));\n return f32(h & 0x00FFFFFFu) * (1.0 / 16777216.0);\n}\n\nfn value_noise_2d(p: vec2<f32>, salt: u32) -> f32 {\n let ip = vec2<i32>(floor(p));\n let fp = fract(p);\n let u = fp * fp * (vec2<f32>(3.0) - 2.0 * fp);\n let a = rand01_cell(ip + vec2<i32>(0, 0), salt);\n let b = rand01_cell(ip + vec2<i32>(1, 0), salt);\n let c = rand01_cell(ip + vec2<i32>(0, 1), salt);\n let d = rand01_cell(ip + vec2<i32>(1, 1), salt);\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn sign_nonzero(x: f32) -> f32 { return select(-1.0, 1.0, x >= 0.0); }\n\nfn oct_decode(p_in: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(p_in.x, 1.0 - abs(p_in.x) - abs(p_in.y), p_in.y);\n if (v.y < 0.0) {\n let x = (1.0 - abs(v.z)) * sign_nonzero(v.x);\n let z = (1.0 - abs(v.x)) * sign_nonzero(v.z);\n v = vec3<f32>(x, -v.y, z);\n }\n return normalize(v);\n}\n\nfn make_basis_from_up(up: vec3<f32>) -> mat3x3<f32> {\n let n = normalize(up);\n let a = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(n.y) > 0.999);\n let t = normalize(cross(a, n));\n let b = cross(n, t);\n // Columns: local X,Y,Z in world\n return mat3x3<f32>(t, n, b);\n}\n\n// Shared vertex prologue for both velocity evaluations (cur + prev frame state). Injected\n// bodies read/mutate `ctx` and the private `u_camera` / `u_grass_interaction` copies.\nfn grass_build_ctx(input: VertexIn) -> GrassVertexCtx {\n let yaw = input.yaw_seed.x;\n let c = cos(yaw);\n let s = sin(yaw);\n let R_y = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n\n let pos = input.pos_scale.xyz;\n let scale = input.pos_scale.w;\n let seed = input.yaw_seed.y;\n\n // Terrain normal from spawn (oct-encoded). Blend with world up for stability on steep slopes.\n let n_terrain = oct_decode(input.normal_oct);\n var up_world = normalize(mix(vec3<f32>(0.0, 1.0, 0.0), n_terrain, 0.85));\n up_world = normalize(vec3<f32>(up_world.x, max(up_world.y, 0.35), up_world.z));\n let R_align = make_basis_from_up(up_world);\n let R_yaw_local = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n let R = R_align * R_yaw_local;\n\n var ctx: GrassVertexCtx;\n ctx.instance_pos = pos;\n ctx.scale = scale;\n ctx.yaw = yaw;\n ctx.seed = seed;\n ctx.uv = input.uv;\n ctx.rot_y = R_y;\n ctx.rot = R;\n ctx.up_world = up_world;\n ctx.in_pos = input.position;\n ctx.in_normal = normalize(oct_decode(input.vnormal_oct));\n ctx.in_tangent = input.tangent;\n ctx.world_pos = pos + (ctx.rot * (ctx.in_pos * scale));\n ctx.world_normal = normalize(ctx.rot * ctx.in_normal);\n ctx.clump_hue = unpack_extra_u8(input.extra.x, 0u);\n ctx.clump_facing = unpack_extra_u8(input.extra.x, 8u);\n ctx.clump_height = unpack_extra_u8(input.extra.x, 16u);\n ctx.clump_tilt = unpack_extra_u8(input.extra.x, 24u);\n ctx.blade_hash = unpack_extra_u8(input.extra.y, 0u);\n ctx.blade_bend = unpack_extra_u8(input.extra.y, 8u);\n ctx.clump_edge = unpack_extra_u8(input.extra.y, 16u);\n ctx.height01 = clamp(input.uv.y, 0.0, 1.0);\n return ctx;\n}\n\n// Beyond this camera distance grass sway is ~a pixel of motion and the camera term (prev ==\n// cur world position) is accurate enough, so the second wind-body evaluation is skipped.\nconst GRASS_VEL_MAX_DIST_M: f32 = 40.0;\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n\n // Current-frame evaluation. Engine injects the optional vertex body below; snippets can\n // read/mutate `ctx` (movement, offsets, procedural deformation, etc.).\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var world_pos: vec3<f32>;\n var world_n: vec3<f32>;\n {\n var ctx = grass_build_ctx(input);\n {\n \n// Bezier proc-blade vertex body (procedural ribbons only; market meshes keep\n// grass_vertex_default_body.wgsl). Reshapes the segmented ribbon into a quadratic\n// bezier blade with clump-driven facing/height/tilt (GoT-style), coherent wind,\n// and distance width compensation. Runs inside `vs_main` with `ctx` in scope.\n//\n// Size model (keep in sync with grass_vertex_card_body.wgsl):\n// height = HEIGHT_CAL * scale * mid(cm knobs) * size_jit * edge spread * meadow wave\n// width = 0.92 * knob^0.8 * wave^0.5 * BladeWidth% (auto-follows height)\n// size_jit also scales blade width; every spread is mean-preserving and gated by\n// SetGrassSizeVariationPercent (tint.w); 0 => uniform carpet. Normals are forced to\n// terrain-up so the field lights as one soft surface (GoT-style).\n\nlet tau: f32 = 6.28318530718;\n// 1.8667 * spawn scale_base 0.15 = 0.28 m: default blade length = GRASS_DEFAULT_HEIGHT_CM.\nlet HEIGHT_CAL: f32 = 1.8667;\nlet base_lift_m: f32 = 0.02;\n\n// Proc ribbon: y is the 0..1 spine parameter, width axis = mesh tangent.\nlet t = clamp(ctx.in_pos.y, 0.0, 1.0);\nctx.height01 = t;\n\n// Facing: clump facing + per-blade deviation; crossed ribbon B gets +90 deg.\nvar facing = ctx.clump_facing * tau + (ctx.blade_hash - 0.5) * 1.9;\nfacing += select(0.0, 1.5707963, abs(ctx.in_tangent.z) > 0.5);\nlet g_up = normalize(ctx.up_world);\nlet face_flat = vec3<f32>(cos(facing), 0.0, sin(facing));\nlet face3 = normalize(face_flat - g_up * dot(face_flat, g_up));\nlet width_dir = normalize(cross(g_up, face3));\n\n// Height: mid from the cm knobs (look3.x), knob-gated spreads, meadow waves.\nlet meadow = grass_meadow_wave(ctx.instance_pos.xz, u_grass_interaction.look2.x);\nlet size_s = min(u_grass_interaction.tint.w, 2.0);\nlet size_jit = (1.0 + (ctx.clump_height - 0.5) * 0.45 * size_s)\n * (1.0 + (ctx.blade_hash - 0.5) * 0.20 * size_s);\nlet height_knob = max(u_grass_interaction.look3.x, 0.05);\nlet wave_h = max(1.0 + u_grass_interaction.wave_amp * meadow, 0.05);\nlet height = HEIGHT_CAL * ctx.scale * height_knob * size_jit\n * (1.0 - (ctx.clump_edge - 0.5) * 0.18 * size_s)\n * wave_h;\n// Width follows height (^0.8 knob, ^0.5 wave) + Blade Width knob; see card body.\nlet width_scale: f32 = 0.92 * pow(height_knob, 0.8) * sqrt(wave_h) * u_grass_interaction.look3.z;\n\n// Coherent wind field (same conventions as the card body; wind clock = look3.y).\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet w_angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec3<f32>(cos(w_angle), 0.0, sin(w_angle));\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 2.0\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0\n + ctx.blade_hash * 0.9;\nlet wave = sin(phase) + 0.45 * sin(phase * 1.9 + 1.7);\n// wind_amp_m = knob cm at default height, proportional to blade height (see card body).\nlet wind_hf = height_knob * wave_h * size_jit * (ctx.scale * 6.6667);\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.36 + 0.88 * gust) * wind_hf * (0.85 + 0.30 * ctx.blade_hash);\n\n// Quadratic bezier control points (root at origin). Tilt leans the tip along facing.\nlet tilt = (0.10 + 0.65 * ctx.clump_tilt) * (0.75 + 0.50 * ctx.blade_bend);\nvar p2 = (g_up * (1.0 - 0.35 * tilt * tilt) + face3 * tilt) * height;\nvar p1 = g_up * (height * 0.55) + face3 * (tilt * height * 0.22);\np2 += wind_dir * (wave * bend_amp);\np1 += wind_dir * (wave * bend_amp * 0.35);\n// Tip flutter (small, high frequency).\np2 += wind_dir * (sin(phase * 5.2 + ctx.blade_hash * 40.0) * (0.25 * u_grass_interaction.wind_amp_m * wind_hf));\n// Approximate length preservation: bending shouldn't stretch the blade.\np2 = normalize(p2) * height;\n\n// Bezier position + derivative (p0 = origin).\nlet omt = 1.0 - t;\nlet spine = p1 * (2.0 * omt * t) + p2 * (t * t);\nlet d_spine = p1 * (2.0 * (1.0 - 2.0 * t)) + p2 * (2.0 * t);\n\n// Width from mesh taper; boost far blades toward a constant on-screen width.\nlet width_coord = dot(ctx.in_pos, ctx.in_tangent.xyz);\nlet cam_dist = length(ctx.instance_pos - u_camera.camera_position);\nlet half_w0 = abs(width_coord) * width_scale * ctx.scale * size_jit;\nlet half_w = max(half_w0, cam_dist * 0.0014 * clamp(abs(width_coord) / 0.03, 0.0, 1.0));\nlet w_signed = sign(width_coord) * half_w;\n\nctx.world_pos = ctx.instance_pos + spine + width_dir * w_signed + g_up * (base_lift_m * ctx.scale);\n\n// Player benders displace the blade progressively (anchored at the root).\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, 0.35 + 0.65 * t * t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\nctx.world_normal = g_up;\n\n\n }\n world_pos = ctx.world_pos;\n world_n = normalize(ctx.world_normal);\n out.grass_misc = vec4<f32>(ctx.height01, ctx.clump_hue, ctx.blade_hash, ctx.clump_edge);\n }\n\n // Previous-frame evaluation for the velocity output: same body, prev time + prev\n // bender/interaction state. Far grass keeps prev == cur (pure camera term).\n var world_pos_prev = world_pos;\n let cam_delta = input.pos_scale.xyz - u_camera_bound.camera_position;\n if (dot(cam_delta, cam_delta) <= GRASS_VEL_MAX_DIST_M * GRASS_VEL_MAX_DIST_M) {\n u_camera.time_seconds = u_grass_vel.prev_time_pad.x;\n u_grass_interaction = u_grass_vel.prev_interaction;\n var ctx = grass_build_ctx(input);\n {\n \n// Bezier proc-blade vertex body (procedural ribbons only; market meshes keep\n// grass_vertex_default_body.wgsl). Reshapes the segmented ribbon into a quadratic\n// bezier blade with clump-driven facing/height/tilt (GoT-style), coherent wind,\n// and distance width compensation. Runs inside `vs_main` with `ctx` in scope.\n//\n// Size model (keep in sync with grass_vertex_card_body.wgsl):\n// height = HEIGHT_CAL * scale * mid(cm knobs) * size_jit * edge spread * meadow wave\n// width = 0.92 * knob^0.8 * wave^0.5 * BladeWidth% (auto-follows height)\n// size_jit also scales blade width; every spread is mean-preserving and gated by\n// SetGrassSizeVariationPercent (tint.w); 0 => uniform carpet. Normals are forced to\n// terrain-up so the field lights as one soft surface (GoT-style).\n\nlet tau: f32 = 6.28318530718;\n// 1.8667 * spawn scale_base 0.15 = 0.28 m: default blade length = GRASS_DEFAULT_HEIGHT_CM.\nlet HEIGHT_CAL: f32 = 1.8667;\nlet base_lift_m: f32 = 0.02;\n\n// Proc ribbon: y is the 0..1 spine parameter, width axis = mesh tangent.\nlet t = clamp(ctx.in_pos.y, 0.0, 1.0);\nctx.height01 = t;\n\n// Facing: clump facing + per-blade deviation; crossed ribbon B gets +90 deg.\nvar facing = ctx.clump_facing * tau + (ctx.blade_hash - 0.5) * 1.9;\nfacing += select(0.0, 1.5707963, abs(ctx.in_tangent.z) > 0.5);\nlet g_up = normalize(ctx.up_world);\nlet face_flat = vec3<f32>(cos(facing), 0.0, sin(facing));\nlet face3 = normalize(face_flat - g_up * dot(face_flat, g_up));\nlet width_dir = normalize(cross(g_up, face3));\n\n// Height: mid from the cm knobs (look3.x), knob-gated spreads, meadow waves.\nlet meadow = grass_meadow_wave(ctx.instance_pos.xz, u_grass_interaction.look2.x);\nlet size_s = min(u_grass_interaction.tint.w, 2.0);\nlet size_jit = (1.0 + (ctx.clump_height - 0.5) * 0.45 * size_s)\n * (1.0 + (ctx.blade_hash - 0.5) * 0.20 * size_s);\nlet height_knob = max(u_grass_interaction.look3.x, 0.05);\nlet wave_h = max(1.0 + u_grass_interaction.wave_amp * meadow, 0.05);\nlet height = HEIGHT_CAL * ctx.scale * height_knob * size_jit\n * (1.0 - (ctx.clump_edge - 0.5) * 0.18 * size_s)\n * wave_h;\n// Width follows height (^0.8 knob, ^0.5 wave) + Blade Width knob; see card body.\nlet width_scale: f32 = 0.92 * pow(height_knob, 0.8) * sqrt(wave_h) * u_grass_interaction.look3.z;\n\n// Coherent wind field (same conventions as the card body; wind clock = look3.y).\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet w_angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec3<f32>(cos(w_angle), 0.0, sin(w_angle));\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 2.0\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0\n + ctx.blade_hash * 0.9;\nlet wave = sin(phase) + 0.45 * sin(phase * 1.9 + 1.7);\n// wind_amp_m = knob cm at default height, proportional to blade height (see card body).\nlet wind_hf = height_knob * wave_h * size_jit * (ctx.scale * 6.6667);\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.36 + 0.88 * gust) * wind_hf * (0.85 + 0.30 * ctx.blade_hash);\n\n// Quadratic bezier control points (root at origin). Tilt leans the tip along facing.\nlet tilt = (0.10 + 0.65 * ctx.clump_tilt) * (0.75 + 0.50 * ctx.blade_bend);\nvar p2 = (g_up * (1.0 - 0.35 * tilt * tilt) + face3 * tilt) * height;\nvar p1 = g_up * (height * 0.55) + face3 * (tilt * height * 0.22);\np2 += wind_dir * (wave * bend_amp);\np1 += wind_dir * (wave * bend_amp * 0.35);\n// Tip flutter (small, high frequency).\np2 += wind_dir * (sin(phase * 5.2 + ctx.blade_hash * 40.0) * (0.25 * u_grass_interaction.wind_amp_m * wind_hf));\n// Approximate length preservation: bending shouldn't stretch the blade.\np2 = normalize(p2) * height;\n\n// Bezier position + derivative (p0 = origin).\nlet omt = 1.0 - t;\nlet spine = p1 * (2.0 * omt * t) + p2 * (t * t);\nlet d_spine = p1 * (2.0 * (1.0 - 2.0 * t)) + p2 * (2.0 * t);\n\n// Width from mesh taper; boost far blades toward a constant on-screen width.\nlet width_coord = dot(ctx.in_pos, ctx.in_tangent.xyz);\nlet cam_dist = length(ctx.instance_pos - u_camera.camera_position);\nlet half_w0 = abs(width_coord) * width_scale * ctx.scale * size_jit;\nlet half_w = max(half_w0, cam_dist * 0.0014 * clamp(abs(width_coord) / 0.03, 0.0, 1.0));\nlet w_signed = sign(width_coord) * half_w;\n\nctx.world_pos = ctx.instance_pos + spine + width_dir * w_signed + g_up * (base_lift_m * ctx.scale);\n\n// Player benders displace the blade progressively (anchored at the root).\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, 0.35 + 0.65 * t * t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\nctx.world_normal = g_up;\n\n\n }\n world_pos_prev = ctx.world_pos;\n }\n\n out.clip_position = u_camera_bound.view_proj * vec4<f32>(world_pos, 1.0);\n out.world_normal = world_n;\n out.uv = input.uv;\n out.seed = input.yaw_seed.y;\n out.instance_pos_xz = input.pos_scale.xz;\n out.cur_clip = u_camera_bound.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera_bound.prev_unjittered_view_proj * vec4<f32>(world_pos_prev, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>, // Rgba8Unorm\n @location(1) normal: vec4<f32>, // Rgba16Float\n @location(2) orm: vec4<f32>, // Rgba8Unorm\n @location(3) velocity: vec2<f32>, // Rg16Float\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @builtin(front_facing) front_facing: bool,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>,\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var out: GBufferOutput;\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n let n = normalize(world_normal) * select(-1.0, 1.0, front_facing);\n\n var ctx: GrassFragCtx;\n ctx.uv = uv;\n ctx.seed = seed;\n ctx.front_facing = front_facing;\n ctx.instance_pos_xz = instance_pos_xz;\n ctx.world_normal = n;\n ctx.base_color = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n ctx.orm = vec4<f32>(1.0, 1.0, 0.0, 0.0);\n ctx.height01 = grass_misc.x;\n ctx.clump_hue = grass_misc.y;\n ctx.blade_hash = grass_misc.z;\n ctx.clump_edge = grass_misc.w;\n\n // Engine injects the fragment body here.\n // Snippet can read tex0..tex15, mat.params4, and mutate `ctx`.\n {\n \n// Default injected grass fragment body (built-in proc blades + custom meshes\n// without an @fragment snippet).\n//\n// Color model (keep in sync with grass_fragment_card_body.wgsl):\n// albedo = mix(root, tip, gradient) * brightness-variation * meadow-wave ripple\n// Nothing else touches color: no hue shifts, no AO/roughness gradients, so the\n// SetGrassRootColor / SetGrassTipColor knobs stay WYSIWYG.\n\n// Root->tip gradient from the color knobs; per-blade jitter de-bands the gradient coordinate.\nlet grad_t = clamp(clamp(ctx.height01, 0.0, 1.0) + (ctx.blade_hash - 0.5) * 0.25, 0.0, 1.0);\nvar col = mix(u_grass_interaction.tint.rgb, u_grass_interaction.look2.yzw, grad_t);\n// Brightness Variation knob: +- brightness per blade and per ~0.5m clump (never hue).\nlet bright = (ctx.blade_hash - 0.5) * 0.30 + (ctx.clump_hue - 0.5) * 0.34;\ncol *= clamp(1.0 + bright * u_grass_interaction.variation_scale, 0.40, 1.85);\n// Meadow waves: brightness rides the same field as blade height (crests lighter, troughs\n// darker). Coupling derives from the Min/Max Height swing (wave_amp).\nlet meadow = grass_meadow_wave(ctx.instance_pos_xz, u_grass_interaction.look2.x);\ncol *= 1.0 + 0.5 * u_grass_interaction.wave_amp * meadow;\nctx.base_color = vec4<f32>(col, 1.0);\n// Flat AO (root darkening lives in the root color knob) and a single roughness.\nctx.orm = vec4<f32>(1.0, 0.75, 0.0, 0.0);\n\nctx.world_normal = normalize(world_normal);\n\n\n }\n out.base_color = ctx.base_color;\n out.normal = vec4<f32>(normalize(ctx.world_normal), 1.0);\n out.orm = ctx.orm;\n\n // Match existing gbuffer convention.\n return out;\n}\n\n"},{"label":"grass_gbuffer_card_injected","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// grass_gbuffer.wgsl\n// Minimal grass GBuffer shader (debug shading, no textures).\n\nstruct VertexIn {\n // Must match `Vertex` layout (static meshes)\n @location(0) position: vec3<f32>,\n @location(1) vnormal_oct: vec2<f32>, // packed vertex: octahedral snorm16 normal\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Grass instance (lean)\n @location(4) pos_scale: vec4<f32>, // xyz = world pos (meters), w = uniform scale\n @location(5) yaw_seed: vec2<f32>, // x = yaw radians, y = seed\n @location(6) normal_oct: vec2<f32>, // oct-encoded terrain normal (XZ)\n\n // GrassExtra (engine enrich pass; packed u8 params, see grass_enrich.wgsl)\n @location(7) extra: vec2<u32>,\n};\n\nfn unpack_extra_u8(v: u32, shift: u32) -> f32 {\n return f32((v >> shift) & 0xFFu) * (1.0 / 255.0);\n}\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera_bound: CameraUniform;\n// Injected bodies read `u_camera` by name; assigned from the binding per evaluation\n// (the prev velocity eval overrides time_seconds).\nvar<private> u_camera: CameraUniform;\n\n// Grass interaction (group 1): player benders (disks).\nconst GRASS_MAX_BENDERS: u32 = 16u;\nstruct GrassInteractionUniform {\n bender_count: u32,\n wind_amp_m: f32, // tip sway amplitude in meters at default height (SetGrassWindStrengthCm)\n variation_scale: f32, // global brightness variation multiplier (1.0 = default)\n wave_amp: f32, // meadow-wave relative height swing: (max-min)/(max+min), ~0.286 default\n // Each entry: (x_m, z_m, radius_m, strength01)\n // NOTE: keep as explicit fields (not an array) to avoid wgpu treating it as a runtime array.\n b0: vec4<f32>, b1: vec4<f32>, b2: vec4<f32>, b3: vec4<f32>,\n b4: vec4<f32>, b5: vec4<f32>, b6: vec4<f32>, b7: vec4<f32>,\n b8: vec4<f32>, b9: vec4<f32>, b10: vec4<f32>, b11: vec4<f32>,\n b12: vec4<f32>, b13: vec4<f32>, b14: vec4<f32>, b15: vec4<f32>,\n tint: vec4<f32>, // rgb = root color (linear, SetGrassRootColor), w = size variation multiplier\n look2: vec4<f32>, // x = meadow-wave freq multiplier (700cm/period), yzw = tip color (linear, SetGrassTipColor)\n // x = height mid multiplier ((min+max)/2 / 28cm), y = wind clock seconds (speed-scaled),\n // z = width knob multiplier (SetGrassBladeWidthPercent), w = reserved.\n look3: vec4<f32>,\n};\n\n// Meadow waves (UE-style \"perlin height\" trick): one shared low-frequency noise field drives\n// grass height, grass brightness, and the terrain ground mottle so they stay in phase.\n// Returns -1..1; ~7m features at freq_mult 1. Salt/base freq must match the terrain\n// shaders' ground tint noise (terrain scales p by the same freq multiplier).\nfn grass_meadow_wave(p_xz: vec2<f32>, freq_mult: f32) -> f32 {\n let p = p_xz * freq_mult;\n return (ori_value_noise2(p.x * 0.15, p.y * 0.15, 101u) - 0.5) * 2.0;\n}\n\n@group(1) @binding(0) var<uniform> u_grass_interaction_bound: GrassInteractionUniform;\n// Injected bodies read `u_grass_interaction` by name; the prev velocity eval overrides it\n// with u_grass_vel.prev_interaction.\nvar<private> u_grass_interaction: GrassInteractionUniform;\n// Prev-frame animation inputs for the velocity dual-eval. prev_time_pad sits BEFORE the\n// nested struct: wgpu sizes GrassInteractionUniform bindings with one extra vec4, so\n// nothing may rely on offsets after it.\nstruct GrassVelUniform {\n prev_time_pad: vec4<f32>, // x = previous frame time_seconds\n prev_interaction: GrassInteractionUniform,\n};\n@group(1) @binding(1) var<uniform> u_grass_vel: GrassVelUniform;\n\nfn grass_get_bender(i: u32) -> vec4<f32> {\n switch(i) {\n case 0u: { return u_grass_interaction.b0; }\n case 1u: { return u_grass_interaction.b1; }\n case 2u: { return u_grass_interaction.b2; }\n case 3u: { return u_grass_interaction.b3; }\n case 4u: { return u_grass_interaction.b4; }\n case 5u: { return u_grass_interaction.b5; }\n case 6u: { return u_grass_interaction.b6; }\n case 7u: { return u_grass_interaction.b7; }\n case 8u: { return u_grass_interaction.b8; }\n case 9u: { return u_grass_interaction.b9; }\n case 10u: { return u_grass_interaction.b10; }\n case 11u: { return u_grass_interaction.b11; }\n case 12u: { return u_grass_interaction.b12; }\n case 13u: { return u_grass_interaction.b13; }\n case 14u: { return u_grass_interaction.b14; }\n case 15u: { return u_grass_interaction.b15; }\n default: { return vec4<f32>(0.0); }\n }\n}\n\nfn grass_bender_offset_xz(p_xz: vec2<f32>, tip_t: f32) -> vec2<f32> {\n if (tip_t <= 0.0 || u_grass_interaction.bender_count == 0u) { return vec2<f32>(0.0, 0.0); }\n let strength_m: f32 = 0.65;\n var off: vec2<f32> = vec2<f32>(0.0, 0.0);\n for (var i: u32 = 0u; i < GRASS_MAX_BENDERS; i = i + 1u) {\n if (i >= u_grass_interaction.bender_count) { break; }\n let b = grass_get_bender(i);\n let r = b.z;\n if (r <= 1e-6) { continue; }\n let d = p_xz - b.xy;\n let d2 = dot(d, d);\n if (d2 >= r * r || d2 <= 1e-10) { continue; }\n let dist = sqrt(d2);\n let t = clamp(1.0 - dist / r, 0.0, 1.0);\n let dir = d / dist;\n off += dir * ((t * t) * (strength_m * b.w));\n }\n return off * tip_t;\n}\n\n// Material16 bindings (same layout as terrain/mesh materials).\n// Grass materials can be injected into this shader and use these bindings.\nstruct MaterialUniform { params4: array<vec4<f32>, 8>, };\n@group(2) @binding(0) var tex0: texture_2d<f32>;\n@group(2) @binding(1) var tex1: texture_2d<f32>;\n@group(2) @binding(2) var tex2: texture_2d<f32>;\n@group(2) @binding(3) var tex3: texture_2d<f32>;\n@group(2) @binding(4) var tex4: texture_2d<f32>;\n@group(2) @binding(5) var tex5: texture_2d<f32>;\n@group(2) @binding(6) var tex6: texture_2d<f32>;\n@group(2) @binding(7) var tex7: texture_2d<f32>;\n@group(2) @binding(8) var tex8: texture_2d<f32>;\n@group(2) @binding(9) var tex9: texture_2d<f32>;\n@group(2) @binding(10) var tex10: texture_2d<f32>;\n@group(2) @binding(11) var tex11: texture_2d<f32>;\n@group(2) @binding(12) var tex12: texture_2d<f32>;\n@group(2) @binding(13) var tex13: texture_2d<f32>;\n@group(2) @binding(14) var tex14: texture_2d<f32>;\n@group(2) @binding(15) var tex15: texture_2d<f32>;\n@group(2) @binding(16) var tex_sampler: sampler;\n@group(2) @binding(17) var<uniform> mat: MaterialUniform;\n\n// Engine injects optional module-scope WGSL here (helper fns/consts/structs).\n// Use `@module { ... }` in your local grass material snippet.\n\n\n\n\n// Vertex-stage context for injected `@vertex { ... }` snippets.\n// Snippet may mutate `world_pos` and/or `world_normal`.\nstruct GrassVertexCtx {\n instance_pos: vec3<f32>,\n scale: f32,\n yaw: f32,\n seed: f32,\n uv: vec2<f32>,\n rot_y: mat3x3<f32>,\n rot: mat3x3<f32>,\n up_world: vec3<f32>,\n // Raw mesh data (unscaled).\n in_pos: vec3<f32>,\n in_normal: vec3<f32>,\n in_tangent: vec4<f32>,\n world_pos: vec3<f32>,\n world_normal: vec3<f32>,\n // GrassExtra clump/blade params (engine enrich pass; all 0..1).\n clump_hue: f32, // per-clump random (brightness variation)\n clump_facing: f32, // per-clump facing angle / tau\n clump_height: f32, // per-clump height multiplier random\n clump_tilt: f32, // per-clump lean amount random\n clump_edge: f32, // 0 = clump center, 1 = clump edge\n blade_hash: f32, // per-blade random (decorrelated from seed)\n blade_bend: f32, // per-blade bend random\n // Written by vertex body: fraction along blade height (drives fragment root->tip gradient).\n height01: f32,\n};\n\n// Fragment-stage context for injected `@fragment { ... }` snippets.\n// Snippet may mutate `base_color`, `orm`, and optionally `world_normal`.\nstruct GrassFragCtx {\n uv: vec2<f32>,\n seed: f32,\n front_facing: bool,\n instance_pos_xz: vec2<f32>,\n world_normal: vec3<f32>,\n base_color: vec4<f32>,\n orm: vec4<f32>,\n // From vertex stage: (height01, clump_hue, blade_hash, clump_edge).\n height01: f32,\n clump_hue: f32,\n blade_hash: f32,\n clump_edge: f32,\n};\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>, // (height01, clump_hue, blade_hash, clump_edge)\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n};\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\nfn rand01_cell(cell: vec2<i32>, salt: u32) -> f32 {\n let ux = bitcast<u32>(cell.x);\n let uy = bitcast<u32>(cell.y);\n let h = hash_u32(ux ^ (uy * 0x9E3779B9u) ^ (salt * 0x85EBCA6Bu));\n return f32(h & 0x00FFFFFFu) * (1.0 / 16777216.0);\n}\n\nfn value_noise_2d(p: vec2<f32>, salt: u32) -> f32 {\n let ip = vec2<i32>(floor(p));\n let fp = fract(p);\n let u = fp * fp * (vec2<f32>(3.0) - 2.0 * fp);\n let a = rand01_cell(ip + vec2<i32>(0, 0), salt);\n let b = rand01_cell(ip + vec2<i32>(1, 0), salt);\n let c = rand01_cell(ip + vec2<i32>(0, 1), salt);\n let d = rand01_cell(ip + vec2<i32>(1, 1), salt);\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn sign_nonzero(x: f32) -> f32 { return select(-1.0, 1.0, x >= 0.0); }\n\nfn oct_decode(p_in: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(p_in.x, 1.0 - abs(p_in.x) - abs(p_in.y), p_in.y);\n if (v.y < 0.0) {\n let x = (1.0 - abs(v.z)) * sign_nonzero(v.x);\n let z = (1.0 - abs(v.x)) * sign_nonzero(v.z);\n v = vec3<f32>(x, -v.y, z);\n }\n return normalize(v);\n}\n\nfn make_basis_from_up(up: vec3<f32>) -> mat3x3<f32> {\n let n = normalize(up);\n let a = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(n.y) > 0.999);\n let t = normalize(cross(a, n));\n let b = cross(n, t);\n // Columns: local X,Y,Z in world\n return mat3x3<f32>(t, n, b);\n}\n\n// Shared vertex prologue for both velocity evaluations (cur + prev frame state). Injected\n// bodies read/mutate `ctx` and the private `u_camera` / `u_grass_interaction` copies.\nfn grass_build_ctx(input: VertexIn) -> GrassVertexCtx {\n let yaw = input.yaw_seed.x;\n let c = cos(yaw);\n let s = sin(yaw);\n let R_y = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n\n let pos = input.pos_scale.xyz;\n let scale = input.pos_scale.w;\n let seed = input.yaw_seed.y;\n\n // Terrain normal from spawn (oct-encoded). Blend with world up for stability on steep slopes.\n let n_terrain = oct_decode(input.normal_oct);\n var up_world = normalize(mix(vec3<f32>(0.0, 1.0, 0.0), n_terrain, 0.85));\n up_world = normalize(vec3<f32>(up_world.x, max(up_world.y, 0.35), up_world.z));\n let R_align = make_basis_from_up(up_world);\n let R_yaw_local = mat3x3<f32>(\n vec3<f32>( c, 0.0, -s),\n vec3<f32>(0.0, 1.0, 0.0),\n vec3<f32>( s, 0.0, c)\n );\n let R = R_align * R_yaw_local;\n\n var ctx: GrassVertexCtx;\n ctx.instance_pos = pos;\n ctx.scale = scale;\n ctx.yaw = yaw;\n ctx.seed = seed;\n ctx.uv = input.uv;\n ctx.rot_y = R_y;\n ctx.rot = R;\n ctx.up_world = up_world;\n ctx.in_pos = input.position;\n ctx.in_normal = normalize(oct_decode(input.vnormal_oct));\n ctx.in_tangent = input.tangent;\n ctx.world_pos = pos + (ctx.rot * (ctx.in_pos * scale));\n ctx.world_normal = normalize(ctx.rot * ctx.in_normal);\n ctx.clump_hue = unpack_extra_u8(input.extra.x, 0u);\n ctx.clump_facing = unpack_extra_u8(input.extra.x, 8u);\n ctx.clump_height = unpack_extra_u8(input.extra.x, 16u);\n ctx.clump_tilt = unpack_extra_u8(input.extra.x, 24u);\n ctx.blade_hash = unpack_extra_u8(input.extra.y, 0u);\n ctx.blade_bend = unpack_extra_u8(input.extra.y, 8u);\n ctx.clump_edge = unpack_extra_u8(input.extra.y, 16u);\n ctx.height01 = clamp(input.uv.y, 0.0, 1.0);\n return ctx;\n}\n\n// Beyond this camera distance grass sway is ~a pixel of motion and the camera term (prev ==\n// cur world position) is accurate enough, so the second wind-body evaluation is skipped.\nconst GRASS_VEL_MAX_DIST_M: f32 = 40.0;\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n\n // Current-frame evaluation. Engine injects the optional vertex body below; snippets can\n // read/mutate `ctx` (movement, offsets, procedural deformation, etc.).\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var world_pos: vec3<f32>;\n var world_n: vec3<f32>;\n {\n var ctx = grass_build_ctx(input);\n {\n \n// Injected vertex body for built-in alpha-cut tuft cards (grass_style = cards).\n// Card mesh: crossed unit quads, y-up, base at y=0. Shares the default body's wind\n// field / benders; tufts are wider and stiffer than single blades.\n//\n// Size model (keep in sync with grass_vertex_proc_body.wgsl):\n// height = HEIGHT_CAL * scale * mid(cm knobs) * size_jit * meadow wave\n// width = 2.8 * knob^0.8 * wave^0.5 * BladeWidth% (auto-follows height: no texture stretch)\n// size_jit scales the whole tuft (width + height) and is mean-preserving, gated by\n// SetGrassSizeVariationPercent (tint.w); 0 => uniform carpet. Normals are forced to\n// terrain-up so the field lights as one soft surface (GoT-style).\n\n// 1.8667 * spawn scale_base 0.15 = 0.28 m: default tuft height = GRASS_DEFAULT_HEIGHT_CM.\nlet HEIGHT_CAL: f32 = 1.8667;\nlet meadow = grass_meadow_wave(ctx.instance_pos.xz, u_grass_interaction.look2.x);\nlet size_s = min(u_grass_interaction.tint.w, 2.0);\nlet size_jit = (1.0 + (ctx.clump_height - 0.5) * 0.45 * size_s)\n * (1.0 + (ctx.blade_hash - 0.5) * 0.20 * size_s);\nlet height_knob = max(u_grass_interaction.look3.x, 0.05);\nlet wave_h = max(1.0 + u_grass_interaction.wave_amp * meadow, 0.05);\nlet height_scale: f32 = HEIGHT_CAL * height_knob * wave_h;\n// Width follows height so the atlas never stretches: near-aspect (^0.8) on the height knob,\n// gentler (^0.5) on the meadow wave (waves should read as height swells, not size patches).\n// look3.z = Blade Width knob (wispy-vs-lush override).\nlet width_scale: f32 = 2.8 * pow(height_knob, 0.8) * sqrt(wave_h) * u_grass_interaction.look3.z;\nlet base_lift_m: f32 = 0.02;\nlet tau: f32 = 6.28318530718;\n\nlet local_pos = vec3<f32>(\n ctx.in_pos.x * width_scale,\n ctx.in_pos.y * height_scale,\n ctx.in_pos.z * width_scale\n) * size_jit;\nctx.world_pos = ctx.instance_pos + (ctx.rot * (local_pos * ctx.scale));\n\nlet r = fract(ctx.seed * 13.37);\nlet up_world = normalize(ctx.up_world);\nctx.world_normal = up_world;\nlet tip = clamp(ctx.in_pos.y, 0.0, 1.0);\nctx.height01 = tip;\nlet bend_t = tip * tip;\n\n// Coherent wind field (same as grass_vertex_default_body). Time = speed-scaled wind clock\n// (look3.y) so the Wind Speed knob stretches time smoothly without phase pops.\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec2<f32>(cos(angle), sin(angle));\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\n// Slower phase + weaker secondary harmonic than the blade body: broad serene swells.\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 1.4\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0;\nlet wave = sin(phase) + 0.32 * sin(phase * 1.9 + 1.7);\n// wind_amp_m = knob cm at default height; taller grass sways proportionally more.\n// (0.34 + 0.91*gust) keeps the old gust response with mean 1.0; ctx.scale/0.15 = spawn calib.\nlet wind_hf = height_knob * wave_h * size_jit * (ctx.scale * 6.6667);\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.34 + 0.91 * gust) * wind_hf * (0.85 + 0.30 * r);\nlet bend = wind_dir * (wave * bend_amp) * bend_t;\nctx.world_pos.x += bend.x;\nctx.world_pos.z += bend.y;\n\n// Player bending (disks), anchored at the tuft root.\nlet step_t = 0.35 + 0.65 * bend_t;\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, step_t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\n// Lift slightly so the card base doesn't sink into terrain.\nctx.world_pos += up_world * (base_lift_m * ctx.scale);\n\n\n }\n world_pos = ctx.world_pos;\n world_n = normalize(ctx.world_normal);\n out.grass_misc = vec4<f32>(ctx.height01, ctx.clump_hue, ctx.blade_hash, ctx.clump_edge);\n }\n\n // Previous-frame evaluation for the velocity output: same body, prev time + prev\n // bender/interaction state. Far grass keeps prev == cur (pure camera term).\n var world_pos_prev = world_pos;\n let cam_delta = input.pos_scale.xyz - u_camera_bound.camera_position;\n if (dot(cam_delta, cam_delta) <= GRASS_VEL_MAX_DIST_M * GRASS_VEL_MAX_DIST_M) {\n u_camera.time_seconds = u_grass_vel.prev_time_pad.x;\n u_grass_interaction = u_grass_vel.prev_interaction;\n var ctx = grass_build_ctx(input);\n {\n \n// Injected vertex body for built-in alpha-cut tuft cards (grass_style = cards).\n// Card mesh: crossed unit quads, y-up, base at y=0. Shares the default body's wind\n// field / benders; tufts are wider and stiffer than single blades.\n//\n// Size model (keep in sync with grass_vertex_proc_body.wgsl):\n// height = HEIGHT_CAL * scale * mid(cm knobs) * size_jit * meadow wave\n// width = 2.8 * knob^0.8 * wave^0.5 * BladeWidth% (auto-follows height: no texture stretch)\n// size_jit scales the whole tuft (width + height) and is mean-preserving, gated by\n// SetGrassSizeVariationPercent (tint.w); 0 => uniform carpet. Normals are forced to\n// terrain-up so the field lights as one soft surface (GoT-style).\n\n// 1.8667 * spawn scale_base 0.15 = 0.28 m: default tuft height = GRASS_DEFAULT_HEIGHT_CM.\nlet HEIGHT_CAL: f32 = 1.8667;\nlet meadow = grass_meadow_wave(ctx.instance_pos.xz, u_grass_interaction.look2.x);\nlet size_s = min(u_grass_interaction.tint.w, 2.0);\nlet size_jit = (1.0 + (ctx.clump_height - 0.5) * 0.45 * size_s)\n * (1.0 + (ctx.blade_hash - 0.5) * 0.20 * size_s);\nlet height_knob = max(u_grass_interaction.look3.x, 0.05);\nlet wave_h = max(1.0 + u_grass_interaction.wave_amp * meadow, 0.05);\nlet height_scale: f32 = HEIGHT_CAL * height_knob * wave_h;\n// Width follows height so the atlas never stretches: near-aspect (^0.8) on the height knob,\n// gentler (^0.5) on the meadow wave (waves should read as height swells, not size patches).\n// look3.z = Blade Width knob (wispy-vs-lush override).\nlet width_scale: f32 = 2.8 * pow(height_knob, 0.8) * sqrt(wave_h) * u_grass_interaction.look3.z;\nlet base_lift_m: f32 = 0.02;\nlet tau: f32 = 6.28318530718;\n\nlet local_pos = vec3<f32>(\n ctx.in_pos.x * width_scale,\n ctx.in_pos.y * height_scale,\n ctx.in_pos.z * width_scale\n) * size_jit;\nctx.world_pos = ctx.instance_pos + (ctx.rot * (local_pos * ctx.scale));\n\nlet r = fract(ctx.seed * 13.37);\nlet up_world = normalize(ctx.up_world);\nctx.world_normal = up_world;\nlet tip = clamp(ctx.in_pos.y, 0.0, 1.0);\nctx.height01 = tip;\nlet bend_t = tip * tip;\n\n// Coherent wind field (same as grass_vertex_default_body). Time = speed-scaled wind clock\n// (look3.y) so the Wind Speed knob stretches time smoothly without phase pops.\nlet wind_p = vec2<f32>(ctx.instance_pos.x, ctx.instance_pos.z) * 0.12;\nlet wind_t = u_grass_interaction.look3.y;\nlet t0 = wind_t * 0.10;\nlet t1 = wind_t * 0.22;\nlet angle = value_noise_2d(wind_p + vec2<f32>(t0, -t0 * 0.7), 0u) * tau;\nlet wind_dir = vec2<f32>(cos(angle), sin(angle));\nlet gust = 0.45 + 0.55 * value_noise_2d(wind_p + vec2<f32>(13.1, -9.7) + vec2<f32>(t1, t1 * 0.6), 1u);\n// Slower phase + weaker secondary harmonic than the blade body: broad serene swells.\nlet phase = (ctx.instance_pos.x * 0.07 + ctx.instance_pos.z * 0.06)\n + wind_t * 1.4\n + value_noise_2d(wind_p + vec2<f32>(3.7, 5.1), 2u) * 4.0;\nlet wave = sin(phase) + 0.32 * sin(phase * 1.9 + 1.7);\n// wind_amp_m = knob cm at default height; taller grass sways proportionally more.\n// (0.34 + 0.91*gust) keeps the old gust response with mean 1.0; ctx.scale/0.15 = spawn calib.\nlet wind_hf = height_knob * wave_h * size_jit * (ctx.scale * 6.6667);\nlet bend_amp = u_grass_interaction.wind_amp_m * (0.34 + 0.91 * gust) * wind_hf * (0.85 + 0.30 * r);\nlet bend = wind_dir * (wave * bend_amp) * bend_t;\nctx.world_pos.x += bend.x;\nctx.world_pos.z += bend.y;\n\n// Player bending (disks), anchored at the tuft root.\nlet step_t = 0.35 + 0.65 * bend_t;\nlet step_off = grass_bender_offset_xz(ctx.instance_pos.xz, step_t);\nctx.world_pos.x += step_off.x;\nctx.world_pos.z += step_off.y;\n\n// Lift slightly so the card base doesn't sink into terrain.\nctx.world_pos += up_world * (base_lift_m * ctx.scale);\n\n\n }\n world_pos_prev = ctx.world_pos;\n }\n\n out.clip_position = u_camera_bound.view_proj * vec4<f32>(world_pos, 1.0);\n out.world_normal = world_n;\n out.uv = input.uv;\n out.seed = input.yaw_seed.y;\n out.instance_pos_xz = input.pos_scale.xz;\n out.cur_clip = u_camera_bound.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera_bound.prev_unjittered_view_proj * vec4<f32>(world_pos_prev, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>, // Rgba8Unorm\n @location(1) normal: vec4<f32>, // Rgba16Float\n @location(2) orm: vec4<f32>, // Rgba8Unorm\n @location(3) velocity: vec2<f32>, // Rg16Float\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @builtin(front_facing) front_facing: bool,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) instance_pos_xz: vec2<f32>,\n @location(4) grass_misc: vec4<f32>,\n @location(5) cur_clip: vec4<f32>,\n @location(6) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n u_camera = u_camera_bound;\n u_grass_interaction = u_grass_interaction_bound;\n var out: GBufferOutput;\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n let n = normalize(world_normal) * select(-1.0, 1.0, front_facing);\n\n var ctx: GrassFragCtx;\n ctx.uv = uv;\n ctx.seed = seed;\n ctx.front_facing = front_facing;\n ctx.instance_pos_xz = instance_pos_xz;\n ctx.world_normal = n;\n ctx.base_color = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n ctx.orm = vec4<f32>(1.0, 1.0, 0.0, 0.0);\n ctx.height01 = grass_misc.x;\n ctx.clump_hue = grass_misc.y;\n ctx.blade_hash = grass_misc.z;\n ctx.clump_edge = grass_misc.w;\n\n // Engine injects the fragment body here.\n // Snippet can read tex0..tex15, mat.params4, and mutate `ctx`.\n {\n \n// Injected fragment body for built-in alpha-cut tuft cards (grass_style = cards).\n// tex0 = built-in tuft atlas (2x2 greyscale variants, alpha cutout; see GRASS_CARD_ATLAS_DSL).\n//\n// Color model (keep in sync with grass_fragment_default_body.wgsl):\n// albedo = mix(root, tip, gradient) * brightness-variation * meadow-wave ripple\n// Nothing else touches color: no hue shifts, no AO/roughness gradients, so the\n// SetGrassRootColor / SetGrassTipColor knobs stay WYSIWYG.\n\nlet variant = floor(clamp(ctx.blade_hash, 0.0, 0.999) * 4.0);\n// Mirror half the tufts for extra variety.\nlet u = select(ctx.uv.x, 1.0 - ctx.uv.x, fract(ctx.seed * 7.31) > 0.5);\nlet cell = vec2<f32>(variant % 2.0, floor(variant * 0.5));\nlet atlas_uv = (cell + vec2<f32>(clamp(u, 0.0, 1.0), 1.0 - clamp(ctx.uv.y, 0.0, 1.0))) * 0.5;\nlet texel = textureSampleBias(tex0, tex_sampler, atlas_uv, u_camera.mip_bias);\nif (texel.a < 0.5) { discard; }\n\n// Atlas luminance = root->tip gradient coordinate (sqrt ~undoes the bake's sRGB-ish curve).\nlet grad_t = clamp(sqrt(dot(texel.rgb, vec3<f32>(0.299, 0.587, 0.114))), 0.0, 1.0);\nvar col = mix(u_grass_interaction.tint.rgb, u_grass_interaction.look2.yzw, grad_t);\n// Brightness Variation knob: +- brightness per tuft and per ~0.5m clump (never hue).\nlet bright = (ctx.blade_hash - 0.5) * 0.30 + (ctx.clump_hue - 0.5) * 0.34;\ncol *= clamp(1.0 + bright * u_grass_interaction.variation_scale, 0.40, 1.85);\n// Meadow waves: brightness rides the same field as tuft height (crests lighter, troughs\n// darker). Coupling derives from the Min/Max Height swing (wave_amp).\nlet meadow = grass_meadow_wave(ctx.instance_pos_xz, u_grass_interaction.look2.x);\ncol *= 1.0 + 0.5 * u_grass_interaction.wave_amp * meadow;\nctx.base_color = vec4<f32>(col, 1.0);\n// Flat AO (root darkening lives in the root color knob) and a single roughness.\nctx.orm = vec4<f32>(1.0, 0.75, 0.0, 0.0);\n// The vertex stage writes terrain-up normals; undo the template's two-sided flip so\n// back-facing card pixels don't light from below.\nctx.world_normal = normalize(world_normal);\n\n\n }\n out.base_color = ctx.base_color;\n out.normal = vec4<f32>(normalize(ctx.world_normal), 1.0);\n out.orm = ctx.orm;\n\n // Match existing gbuffer convention.\n return out;\n}\n\n"},{"label":"grass_interaction_debug","code":"// grass_interaction_debug.wgsl\n// GPU->CPU readback helper: copy u_grass_interaction.b0 into a storage buffer.\n\nstruct GrassInteractionUniform {\n bender_count: u32,\n wind_amp_m: f32,\n variation_scale: f32,\n wave_amp: f32,\n // Each entry: (x_m, z_m, radius_m, strength01)\n b0: vec4<f32>, b1: vec4<f32>, b2: vec4<f32>, b3: vec4<f32>,\n b4: vec4<f32>, b5: vec4<f32>, b6: vec4<f32>, b7: vec4<f32>,\n b8: vec4<f32>, b9: vec4<f32>, b10: vec4<f32>, b11: vec4<f32>,\n b12: vec4<f32>, b13: vec4<f32>, b14: vec4<f32>, b15: vec4<f32>,\n tint: vec4<f32>,\n};\n\nstruct InteractionDbgOut {\n bender_count: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n b0: vec4<f32>,\n _pad_end: vec4<u32>,\n};\n\n@group(0) @binding(0) var<uniform> u_grass_interaction: GrassInteractionUniform;\n@group(0) @binding(1) var<storage, read_write> out_dbg: InteractionDbgOut;\n\n@compute @workgroup_size(1, 1, 1)\nfn main() {\n out_dbg.bender_count = u_grass_interaction.bender_count;\n out_dbg.b0 = u_grass_interaction.b0;\n}\n\n"},{"label":"grass_spawn","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// grass_spawn.wgsl\n// One grass instance per terrain height sample (debug spawn).\n\nstruct TerrainUniform {\n spawn_samples_x: u32,\n spawn_samples_y: u32,\n _unused_span: f32,\n _pad0: u32,\n terrain_cell_size: f32,\n spawn_cell_size: f32,\n min_z: f32,\n max_z: f32,\n scale_base: f32,\n // SetTerrainErosionDetailPercent / 100; must match the tile-bake value (shared surface).\n erosion_detail: f32,\n _pad1: vec3<f32>,\n origin: vec3<f32>,\n _pad2: f32,\n // (min_x, max_z, size_x, size_z); max_z is the terrain \"top\" edge.\n terrain_xz: vec4<f32>,\n // (global_ix0, global_iz0, _, _) for the camera-centered spawn window.\n window_cell0: vec4<i32>,\n // Display-extension/recipe generator params; must match the tile bake (same surface) or\n // blades float/sink in recipe display mode. radius > 0 on recipe-display worlds with an\n // extension (grass spawns beyond the grid there); recipe_mode carries the per-asset verdict.\n ext: OriExtParams,\n // Ring-stack surface-weight clip (terrain_recipe_rings.rs): (level_count,\n // min_valid_level, terminal texel_cells, enable [0 = surface gating off]).\n clip2: vec4<f32>,\n // (cam_cell_x, cam_cell_y, surface_count, spare).\n clip_center: vec4<f32>,\n // Per level (base_cell_x, base_cell_y, texel_cells, spare); same contract as the\n // terrain blend (terrain_surface_blend.wgsl).\n clip_levels: array<vec4<f32>, 8>,\n // Per-surface grass affinity (spawn-density factor), surfaces 0..3 / 4..7.\n surf_grass0: vec4<f32>,\n surf_grass1: vec4<f32>,\n};\n\n@group(0) @binding(0) var height_tex: texture_2d<u32>;\n@group(0) @binding(1) var weight_tex: texture_2d<f32>;\n@group(0) @binding(2) var weight_sampler: sampler;\n@group(0) @binding(3) var<uniform> terrain: TerrainUniform;\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(4) var<storage, read_write> out_instances: array<GrassInstance>;\n\n// DEFAULT spawn only: recipe ring-stack surface-weight planes (8 weights = 2x rgba8\n// arrays, one 512^2 layer per clip level; terrain_recipe_rings.rs). Dummy 1x1 arrays are\n// bound while inactive (clip2.w = 0). CUSTOM grass-spawn compute shaders (a terrain's\n// `grass_spawn_material` #shader) get a DIFFERENT group(1): the spawn-material group.\n// IMPORTANT (WebGPU/wasm): compute stage commonly has a max of 16 sampled textures. This\n// pass already samples 2 terrain textures (height_tex + weight_tex), so the custom\n// spawn-material group is capped at 14: tex0..tex13 + tex_sampler + a\n// `MaterialUniform { params4: array<vec4<f32>, 8> }` uniform at bindings 14/15.\n@group(1) @binding(0) var surf_weights_a: texture_2d_array<f32>;\n@group(1) @binding(1) var surf_weights_b: texture_2d_array<f32>;\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\nfn hash_cell(ix: i32, iz: i32, salt: u32) -> u32 {\n return hash_u32(bitcast<u32>(ix) ^ (bitcast<u32>(iz) * 0x9E3779B9u) ^ salt);\n}\n\nstruct HeightGrad {\n h: f32,\n dhdx: f32,\n dhdz: f32,\n};\n\n// Display-surface height + gradient (world XZ, meters). Samples through\n// ori_terrain_display_height (terrain_display_height.wgsl, appended at module build) with the\n// finest-LOD detail octave count so blades sit exactly on the rendered tile surface (the sim\n// collision surface is plain bilinear and can deviate by the clamped CR + detail offset).\nfn sample_height_and_grad(u_in: f32, v_in: f32, hx_dim: u32, hy_dim: u32) -> HeightGrad {\n let w = f32(max(1u, hx_dim - 1u));\n let h = f32(max(1u, hy_dim - 1u));\n // Recipe-display worlds with an extension spawn beyond the grid: the display-height\n // helper evaluates the recipe generator for out-of-grid coords. Without an extension\n // (data worlds), clamp to the grid as before.\n var uv = vec2<f32>(u_in, v_in);\n if (terrain.ext.radius_cells <= 0.0) {\n uv = clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0));\n }\n let s = uv * vec2<f32>(w, h);\n let hs = ori_terrain_display_height(\n height_tex, s, terrain.min_z, terrain.max_z, terrain.terrain_cell_size, 2,\n terrain.erosion_detail, terrain.ext, 0.25,\n );\n var out: HeightGrad;\n out.h = hs.h;\n let inv_cell = 1.0 / max(terrain.terrain_cell_size, 1e-6);\n out.dhdx = hs.grad.x * inv_cell;\n // Note: v increases as world_z decreases (see UV convention), so d/dz has a negative.\n out.dhdz = -hs.grad.y * inv_cell;\n return out;\n}\n\n// Surface-aware spawn density: grass affinity of the #terrain_surfaces blend at a grid\n// position (cells = uv * (samples - 1), same space as the terrain blend). Nearest-texel\n// load from the ring-stack weight planes at the finest valid level for the spawn\n// distance (the blend's distance term; no screen derivatives in compute), toroidal\n// wrap via `mod 512` (terminal level clamps instead - no wrap).\nfn ori_grass_surface_affinity(cells: vec2<f32>) -> f32 {\n let d = max(abs(cells.x - terrain.clip_center.x), abs(cells.y - terrain.clip_center.y));\n let lf = clamp(log2(max(d * (1.0 / 112.0), 1.0)), terrain.clip2.y, terrain.clip2.x - 1.0);\n let lv = i32(lf);\n let pl = terrain.clip_levels[lv];\n var rel = (cells - pl.xy) / max(pl.z, 1e-3);\n if (lv == i32(terrain.clip2.x) - 1) { rel = clamp(rel, vec2<f32>(0.0), vec2<f32>(511.0)); }\n let texel = vec2<i32>(floor(rel + vec2<f32>(0.5))) & vec2<i32>(511);\n let wa = textureLoad(surf_weights_a, texel, lv, 0);\n let wb = textureLoad(surf_weights_b, texel, lv, 0);\n let w = array<f32, 8>(wa.x, wa.y, wa.z, wa.w, wb.x, wb.y, wb.z, wb.w);\n let g = array<f32, 8>(\n terrain.surf_grass0.x, terrain.surf_grass0.y, terrain.surf_grass0.z, terrain.surf_grass0.w,\n terrain.surf_grass1.x, terrain.surf_grass1.y, terrain.surf_grass1.z, terrain.surf_grass1.w);\n let count = u32(clamp(terrain.clip_center.z, 1.0, 8.0));\n var total = 0.0;\n var acc = 0.0;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let wi = select(0.0, w[i], i < count);\n total = total + wi;\n acc = acc + wi * g[i];\n }\n // All-zero weights fall back to surface 0 (same rule as the terrain blend).\n return select(acc / max(total, 1e-4), g[0], total < 1e-4);\n}\n\nfn sign_nonzero(x: f32) -> f32 { return select(-1.0, 1.0, x >= 0.0); }\n\n// Octahedral encoding for unit vectors where +Y is \"up\". Returns [-1,1] range.\nfn oct_encode(n_in: vec3<f32>) -> vec2<f32> {\n var n = n_in / (abs(n_in.x) + abs(n_in.y) + abs(n_in.z) + 1e-12);\n var p = n.xz;\n if (n.y < 0.0) {\n p = (vec2<f32>(1.0, 1.0) - abs(p.yx)) * vec2<f32>(sign_nonzero(p.x), sign_nonzero(p.y));\n }\n return p;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let x = gid.x;\n let y = gid.y;\n if (x >= terrain.spawn_samples_x || y >= terrain.spawn_samples_y) { return; }\n\n let sx = terrain.spawn_samples_x;\n let idx = y * sx + x;\n\n let cell_ix = terrain.window_cell0.x + i32(x);\n let cell_iz = terrain.window_cell0.y + i32(y);\n\n let hdim = textureDimensions(height_tex);\n let hx_dim = max(1u, hdim.x);\n let hy_dim = max(1u, hdim.y);\n let terrain_cell: f32 = terrain.terrain_cell_size;\n let spawn_cell: f32 = terrain.spawn_cell_size;\n let size_x: f32 = f32(max(1u, hx_dim - 1u)) * terrain_cell;\n let size_z: f32 = f32(max(1u, hy_dim - 1u)) * terrain_cell;\n let terrain_min_x: f32 = terrain.terrain_xz.x;\n let terrain_max_z: f32 = terrain.terrain_xz.y;\n\n let inv_size_x = 1.0 / max(1e-6, size_x);\n let inv_size_z = 1.0 / max(1e-6, size_z);\n\n // Deterministic per-cell pseudo-random\n let h0 = hash_cell(cell_ix, cell_iz, 0xA5A5A5A5u);\n let r01 = f32(h0 & 65535u) / 65535.0;\n let r02 = f32(hash_u32(h0 ^ 0xA2C2A2C2u) & 65535u) / 65535.0;\n let r03 = f32(hash_u32(h0 ^ 0x19B9C5F1u) & 65535u) / 65535.0;\n\n // More blades: boost spawn probability and add per-instance position jitter.\n let jitter = (vec2<f32>(r02, r03) - vec2<f32>(0.5, 0.5)) * (spawn_cell * 0.85);\n\n // Sample at the actual spawn point (after jitter) to avoid \"stepping\" on slopes.\n // UV convention matches terrain vertex: u increases +X, v increases -Z.\n let world_x = f32(cell_ix) * spawn_cell;\n let world_z = f32(cell_iz) * spawn_cell;\n // uv unclamped when a recipe extension exists (radius > 0): blades beyond the grid get\n // their height from the recipe generator in sample_height_and_grad. Data worlds clamp\n // as before (edge heights are all there is).\n var u = (world_x + jitter.x - terrain_min_x) * inv_size_x;\n var v = (terrain_max_z - (world_z + jitter.y)) * inv_size_z;\n if (terrain.ext.radius_cells <= 0.0) {\n u = clamp(u, 0.0, 1.0);\n v = clamp(v, 0.0, 1.0);\n }\n let hg = sample_height_and_grad(u, v, hx_dim, hy_dim);\n let h: f32 = hg.h;\n\n let world_y = terrain.origin.y + h;\n\n // Weightmap uses the same uv convention as terrain vertex (clamp sampler: beyond-grid\n // blades take the edge density mask; the surface affinity below stays live out there).\n let uv_w = clamp(vec2<f32>(u, v), vec2<f32>(0.0), vec2<f32>(1.0));\n let w = textureSampleLevel(weight_tex, weight_sampler, uv_w, 0.0);\n // Simplified: use R only as density mask (single grass type).\n var density: f32 = w.x;\n // Surface-aware gating: scale by the #terrain_surfaces grass affinity of the blend\n // weights at this cell (rock/snow/sand surfaces suppress spawn).\n if (terrain.clip2.w > 0.5) {\n let cells = vec2<f32>(u, v) * vec2<f32>(f32(max(1u, hx_dim - 1u)), f32(max(1u, hy_dim - 1u)));\n density = density * ori_grass_surface_affinity(cells);\n }\n\n let density_boost = clamp(density * 2.5, 0.0, 1.0);\n let do_spawn = select(0.0, 1.0, r01 < density_boost);\n // Uniform scale: all size randomness lives in the vertex bodies, gated by the\n // SetGrassSizeVariationPercent knob (so 0 => truly even carpet).\n let scale = terrain.scale_base * do_spawn;\n let yaw = (f32(hash_u32(h0)) / 4294967295.0) * 6.28318530718;\n\n out_instances[idx].pos_scale = vec4<f32>(world_x + jitter.x, world_y, world_z + jitter.y, scale);\n // Keep a fractional component for variation (floor() will be 0 => type 0).\n out_instances[idx].yaw_seed = vec2<f32>(yaw, r01);\n let n = normalize(vec3<f32>(-hg.dhdx, 1.0, -hg.dhdz));\n out_instances[idx]._pad = oct_encode(n);\n}\n\n\n// ORI_TDH_V1 - shared display-surface height for terrain tile bake + grass spawn.\n// Display surface = Catmull-Rom upsample of the sim heightfield (correction clamped to\n// +-ORI_TDH_CR_CLAMP_M vs bilinear = the sim collision surface) + relief-scaled band-limited\n// erosion-filter detail (slope-aligned gullies; ori_erosion_filter.wgsl, appended at module\n// build). Pure function of (heights texture, sim-grid position): every display consumer must\n// sample through this or grass floats/sinks vs the rendered ground.\n// Requires the ori noise prelude (preprocess_wgsl injects it).\nconst ORI_TDH_CR_CLAMP_M: f32 = 0.25;\nconst ORI_TDH_DETAIL_SEED: u32 = 31337u;\n// Erosion detail parameters (reference defaults from the Advanced Terrain Erosion Filter).\n// Octave o has frequency ~(1/SCALE_CELLS)*2^o in sim-cell units; octave count is picked per LOD\n// so baked octaves stay band-limited, finer LODs add finer octaves.\nconst ORI_TDH_EROSION_SCALE_CELLS: f32 = 1.1; // first-octave gully features span ~1 sim cell\nconst ORI_TDH_EROSION_STRENGTH: f32 = 0.22;\nconst ORI_TDH_EROSION_GULLY_WEIGHT: f32 = 0.5; // 0 = sharp peaks, no gullies; 1 = full gullies\nconst ORI_TDH_EROSION_DETAIL: f32 = 1.5; // lower = fine gullies only on steep slopes\nconst ORI_TDH_EROSION_CELL_SCALE: f32 = 0.7;\nconst ORI_TDH_EROSION_NORMALIZATION: f32 = 0.5;\nconst ORI_TDH_EROSION_ROUNDING: vec4<f32> = vec4<f32>(0.1, 0.0, 0.1, 2.0); // ridge/crease/in/oct\nconst ORI_TDH_EROSION_ONSET: vec4<f32> = vec4<f32>(1.25, 1.25, 2.8, 1.5);\nconst ORI_TDH_EROSION_ASSUMED_SLOPE: vec2<f32> = vec2<f32>(0.7, 1.0);\n\nstruct OriTdhSample {\n h: f32, // terrain-local display height (meters; same space as decode_height)\n h_coarse: f32, // same with one less detail octave = the parent LOD's value (geomorph target)\n grad: vec2<f32>, // d(h)/d(sim cell x/y) of the bilinear base (no detail; good enough for tilt)\n};\n\nfn ori_tdh_decode(v: u32, min_z: f32, max_z: f32) -> f32 {\n return min_z + (f32(v & 65535u) / 65535.0) * (max_z - min_z);\n}\n\nfn ori_tdh_cr_weights(t: f32) -> vec4<f32> {\n let t2 = t * t;\n let t3 = t2 * t;\n return vec4<f32>(\n 0.5 * (-t3 + 2.0 * t2 - t),\n 0.5 * (3.0 * t3 - 5.0 * t2 + 2.0),\n 0.5 * (-3.0 * t3 + 4.0 * t2 + t),\n 0.5 * (t3 - t2),\n );\n}\n\n// s_in: sample position in sim-grid coords (0..sx-1, 0..sy-1), clamped inside.\n// detail_octaves: 0 = pure Catmull-Rom (sim-res and coarser LODs), up to 2 at the finest LODs\n// and for grass placement. Octave freqs/amps depend only on the sim grid (not the LOD), so a\n// parent tile's value at a shared point equals the child's h_coarse there (pop-free morphing).\n// Display-extension blend band (cells outside the grid over which the procedural height\n// eases in from the edge-clamped real height, hiding f32-vs-Fp generator mismatch).\nconst ORI_TDH_EXT_BLEND_CELLS: f32 = 16.0;\n\n// detail_strength: SetTerrainErosionDetailPercent / 100 (1.0 = reference defaults, 0 = smooth).\n// Every consumer (tile bake, grass spawn) must pass the same value or grass floats/sinks.\n// ext: display-extension generator (OriExtParams from ori_ext_terrain.wgsl, appended\n// alongside this file); radius_cells 0 = disabled. Samples outside the sim grid evaluate the\n// procedural generator, eased from the clamped edge height over ORI_TDH_EXT_BLEND_CELLS.\n// sample_step_cells: sim-cell spacing between consecutive samples of the CALLER (tile texel\n// step for bakes, spawn spacing for grass) - band-limits the extension generator per LOD.\nfn ori_terrain_display_height(\n ht: texture_2d<u32>, s_in: vec2<f32>, min_z: f32, max_z: f32, cell_m: f32, detail_octaves: i32,\n detail_strength: f32, ext: OriExtParams, sample_step_cells: f32,\n) -> OriTdhSample {\n let dims = vec2<i32>(textureDimensions(ht));\n let s = clamp(s_in, vec2<f32>(0.0), vec2<f32>(f32(dims.x - 1), f32(dims.y - 1)));\n let x0 = i32(floor(s.x));\n let y0 = i32(floor(s.y));\n let fx = s.x - f32(x0);\n let fy = s.y - f32(y0);\n\n // 4x4 clamped patch around the bilinear cell (16 taps; also feeds the relief estimate).\n var p: array<vec4<f32>, 4>;\n var pmin = 1e30;\n var pmax = -1e30;\n for (var j = 0; j < 4; j = j + 1) {\n let yy = clamp(y0 - 1 + j, 0, dims.y - 1);\n var row = vec4<f32>(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let xx = clamp(x0 - 1 + i, 0, dims.x - 1);\n let h = ori_tdh_decode(textureLoad(ht, vec2<i32>(xx, yy), 0).r, min_z, max_z);\n row[i] = h;\n pmin = min(pmin, h);\n pmax = max(pmax, h);\n }\n p[j] = row;\n }\n\n // Bilinear base = the sim collision surface.\n let h00 = p[1].y;\n let h10 = p[1].z;\n let h01 = p[2].y;\n let h11 = p[2].z;\n let bil = mix(mix(h00, h10, fx), mix(h01, h11, fx), fy);\n var out: OriTdhSample;\n out.grad = vec2<f32>(\n mix(h10 - h00, h11 - h01, fy),\n mix(h01 - h00, h11 - h10, fx),\n );\n\n // Recipe display mode: the #terrain generator is the display content EVERYWHERE - no\n // Catmull-Rom, no detail octaves, no blend band, no grid boundary. Only enabled after the\n // divergence check verified sim heights == generator, so `grad` (grass tilt only) can stay\n // the sim-patch bilinear gradient above.\n if (ext.recipe_mode > 0.5) {\n let eh = ori_ext_height(s_in, ext, sample_step_cells);\n out.h = eh.x;\n out.h_coarse = eh.y;\n return out;\n }\n\n // Separable Catmull-Rom; clamp the correction so the display surface never leaves the\n // collision surface by more than ORI_TDH_CR_CLAMP_M on extreme terrain.\n let wx = ori_tdh_cr_weights(fx);\n var wy = ori_tdh_cr_weights(fy); // var: naga rejects dynamic indexing of let vectors\n var cr = 0.0;\n for (var j = 0; j < 4; j = j + 1) { cr = cr + wy[j] * dot(p[j], wx); }\n let base = bil + clamp(cr - bil, -ORI_TDH_CR_CLAMP_M, ORI_TDH_CR_CLAMP_M);\n out.h = base;\n out.h_coarse = base;\n\n // Relief-scaled erosion detail: flats stay flat, amplitude follows the local 4x4 height\n // range. Amplitude/frequency/fade depend only on sim-grid position, never on the LOD level,\n // and the filter's (octaves-1) prefix equals the parent LOD's value (pop-free morphing).\n if (detail_octaves > 0 && detail_strength > 0.0) {\n let range = pmax - pmin;\n let amp = min(range * 0.10, cell_m * 0.35) * smoothstep(0.02, 0.25, range) * detail_strength;\n if (amp > 0.0) {\n // Cell units on both axes (slope = rise/run per cell) so the reference defaults apply.\n let slope_cells = out.grad / max(cell_m, 1e-6);\n // Altitude fade from the local patch: carve toward -1 near the patch low, sharpen\n // toward +1 near the patch high (the filter's V-valley / crisp-ridge ingredient).\n let fade = ((bil - pmin) / max(range, 1e-6)) * 2.0 - 1.0;\n let e = ori_erosion_filter(\n s, vec3<f32>(0.0, slope_cells), fade,\n ORI_TDH_EROSION_STRENGTH, ORI_TDH_EROSION_GULLY_WEIGHT, ORI_TDH_EROSION_DETAIL,\n ORI_TDH_EROSION_ROUNDING, ORI_TDH_EROSION_ONSET, ORI_TDH_EROSION_ASSUMED_SLOPE,\n ORI_TDH_EROSION_SCALE_CELLS, min(detail_octaves, 2), 2.0,\n 0.5, ORI_TDH_EROSION_CELL_SCALE, ORI_TDH_EROSION_NORMALIZATION, ORI_TDH_DETAIL_SEED,\n );\n // Normalize deltas to ~[-1,1] and scale by the relief amplitude. magnitude_coarse is 0\n // when only one octave runs: h_coarse then stays the pure-CR base = the parent's value.\n out.h = base + amp * (e.delta / max(e.magnitude, 1e-6));\n out.h_coarse = base + amp * (e.delta_coarse / max(e.magnitude_coarse, 1e-6));\n }\n }\n\n // Display extension: outside the sim grid, ease into the procedural generator. Everything\n // above evaluated at the CLAMPED edge sample (and the detail amp decays to 0 there since\n // the clamped 4x4 patch flattens), so C0 continuity at the boundary is by construction.\n if (ext.radius_cells > 0.0) {\n let over = max(vec2<f32>(0.0) - s_in, s_in - vec2<f32>(f32(dims.x - 1), f32(dims.y - 1)));\n let out_d = max(max(over.x, over.y), 0.0);\n if (out_d > 0.0) {\n // The block renders its FULL-res sim content at every tile LOD (Catmull-Rom of the\n // heights texture), but the generator band-limits per LOD - at the grid boundary a\n // coarse ext tile would be missing the finest erosion/base octaves the adjacent block\n // tile still shows (= a shelf running along the grid edge). Force full detail near the\n // grid and fade to the true per-LOD band limit deep into the extension; the band\n // weights are smooth in the step, so content stays continuous along the fade.\n let step_eff = mix(min(sample_step_cells, 0.25), sample_step_cells,\n smoothstep(ORI_TDH_EXT_BLEND_CELLS, 272.0, out_d));\n let eh = ori_ext_height(s_in, ext, step_eff);\n let t = smoothstep(0.0, ORI_TDH_EXT_BLEND_CELLS, out_d);\n out.h = mix(out.h, eh.x, t);\n out.h_coarse = mix(out.h_coarse, eh.y, t);\n }\n }\n return out;\n}\n\n// ORI_EROSION_FILTER_V1 - stateless erosion-style noise: stripe \"gullies\" aligned to the local\n// slope, stacked over octaves with fade targets so valleys carve V-shaped and peaks sharpen.\n// Port of \"Phacelle Noise\" + \"Advanced Terrain Erosion Filter\" by Rune Skovbo Johansen\n// (https://www.shadertoy.com/view/wXcfWn, technique lineage: Clay John, Fewes). Deviations from\n// the reference: the float-fract hash is replaced with the engine lattice hash (ori_hash2_cell,\n// noise prelude) plus a seed, and the octave loop also returns the one-less-octave result\n// (the loop is prefix-stable) for LOD geomorph targets. Sim-side Fp mirror: fp_erosion.rs.\n// Appended after terrain_display_height.wgsl at module build; requires the ori noise prelude.\n//\n// Phacelle Noise and Advanced Terrain Erosion Filter copyright (c) 2025 Rune Skovbo Johansen.\n// This Source Code Form is subject to the terms of the Mozilla Public\n// License, v. 2.0. If a copy of the MPL was not distributed with this\n// file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nconst ORI_EROSION_TAU: f32 = 6.28318530717959;\n\n// Two [-1,1] channels from one engine lattice hash (replaces the reference's float-fract hash).\nfn ori_erosion_hash2(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h & 0xffffu), f32(h >> 16u)) * (2.0 / 65536.0) - 1.0;\n}\n\nfn ori_pow_inv(t: f32, power: f32) -> f32 {\n return 1.0 - pow(1.0 - clamp(t, 0.0, 1.0), power);\n}\nfn ori_ease_out(t: f32) -> f32 {\n let v = 1.0 - clamp(t, 0.0, 1.0);\n return 1.0 - v * v;\n}\nfn ori_smooth_start(t: f32, smoothing: f32) -> f32 {\n if (t >= smoothing) { return t - 0.5 * smoothing; }\n return 0.5 * t * t / max(smoothing, 1e-6);\n}\nfn ori_safe_normalize2(n: vec2<f32>) -> vec2<f32> {\n let l = length(n);\n if (l > 1e-10) { return n / l; }\n return n;\n}\n\n// Phacelle (\"phase cell\") noise: a stripe pattern aligned with norm_dir, blended from cosine and\n// sine waves anchored at 4x4 jittered cell points (Worley-style) so stripes stay continuous\n// across cells. norm_dir must be normalized; freq is stripes per cell (keep near 1); offset01 is\n// the phase offset in cycles; normalization (0..1) limits how much small magnitudes get boosted.\n// Returns xy = normalized cos/sin waves, zw = side dir (multiply onto sin for cos derivatives).\nfn ori_phacelle_noise(p: vec2<f32>, norm_dir: vec2<f32>, freq: f32, offset01: f32,\n normalization: f32, seed: u32) -> vec4<f32> {\n let side_dir = vec2<f32>(-norm_dir.y, norm_dir.x) * freq * ORI_EROSION_TAU;\n let offset = offset01 * ORI_EROSION_TAU;\n let p_int = floor(p);\n let p_frac = p - p_int;\n var phase_dir = vec2<f32>(0.0);\n var weight_sum = 0.0;\n for (var i = -1; i <= 2; i = i + 1) {\n for (var j = -1; j <= 2; j = j + 1) {\n let grid_offset = vec2<f32>(f32(i), f32(j));\n let cell = vec2<i32>(p_int + grid_offset);\n let random_offset = ori_erosion_hash2(cell, seed) * 0.5;\n let from_cell_point = p_frac - grid_offset - random_offset;\n let sqr_dist = dot(from_cell_point, from_cell_point);\n // Bell weight: 1 at dist 0; the -0.01111 makes it exactly 0 at 1.5 (the farthest any\n // contributing cell point can be), avoiding subtle grid-line artifacts.\n let weight = max(0.0, exp(-sqr_dist * 2.0) - 0.01111);\n weight_sum += weight;\n let wave_input = dot(from_cell_point, side_dir) + offset;\n phase_dir += vec2<f32>(cos(wave_input), sin(wave_input)) * weight;\n }\n }\n let interpolated = phase_dir / max(weight_sum, 1e-6);\n let magnitude = max(1.0 - normalization, length(interpolated));\n return vec4<f32>(interpolated / magnitude, side_dir);\n}\n\nstruct OriErosionResult {\n delta: f32, // height delta after all octaves (same height units as the input slope)\n delta_coarse: f32, // same with one less octave = the parent LOD's value (geomorph target)\n ridge: f32, // ridge map: -1 on creases .. 1 on ridges (drainage/splat masks)\n magnitude: f32, // sum of octave strengths; delta/magnitude is a ~[-1,1] value\n magnitude_coarse: f32, // 0 when octaves == 1 (parent is the unmodified base)\n};\n\n// Faithful port of ErosionFilter (see file header for parameter docs in the reference).\n// p and the height axis must share units: height_and_slope = (height, d(height)/d(p)).\n// scale/strength are in those units; scale must not vary per pixel.\nfn ori_erosion_filter(\n p: vec2<f32>, height_and_slope: vec3<f32>, fade_target_in: f32,\n strength_in: f32, gully_weight: f32, detail: f32,\n rounding: vec4<f32>, onset: vec4<f32>, assumed_slope: vec2<f32>,\n scale: f32, octaves: i32, lacunarity: f32,\n gain: f32, cell_scale: f32, normalization: f32, seed: u32,\n) -> OriErosionResult {\n var strength = strength_in * scale;\n var fade_target = clamp(fade_target_in, -1.0, 1.0);\n var hs = height_and_slope;\n var freq = 1.0 / (scale * cell_scale);\n let slope_len = max(length(hs.yz), 1e-10);\n var out: OriErosionResult;\n out.delta_coarse = 0.0;\n out.magnitude = 0.0;\n out.magnitude_coarse = 0.0;\n var rounding_mult = 1.0;\n let rounding_for_input =\n mix(rounding.y, rounding.x, clamp(fade_target + 0.5, 0.0, 1.0)) * rounding.z;\n // Accumulating mask: initial slope first, then the slope of each octave too.\n var combi_mask = ori_ease_out(ori_smooth_start(slope_len * onset.x, rounding_for_input * onset.x));\n var ridge_combi_mask = ori_ease_out(slope_len * onset.z);\n var ridge_fade_target = fade_target;\n // Gully direction source: actual slope mixed toward an assumed slope magnitude.\n var gully_slope = mix(hs.yz, hs.yz / slope_len * assumed_slope.x, assumed_slope.y);\n\n for (var i = 0; i < octaves; i = i + 1) {\n if (i == octaves - 1) {\n // The loop is prefix-stable (octave i only reads state from octaves < i), so the\n // accumulation so far IS the (octaves-1) result = the parent LOD's value.\n out.delta_coarse = hs.x - height_and_slope.x;\n out.magnitude_coarse = out.magnitude;\n }\n var phacelle = ori_phacelle_noise(\n p * freq, ori_safe_normalize2(gully_slope), cell_scale, 0.25, normalization,\n seed ^ (u32(i) * 0x9e3779b9u));\n // p was multiplied by freq; negate since slope directions point downhill.\n phacelle = vec4<f32>(phacelle.xy, phacelle.zw * -freq);\n let sloping = abs(phacelle.y);\n // Add non-masked normalized slope for subsequent octave directions (steepest wave part).\n gully_slope += sign(phacelle.y) * phacelle.zw * strength * gully_weight;\n // Gullies: height offset (-1..1) in x, derivative in yz; fade toward fade_target by mask.\n let gullies = vec3<f32>(phacelle.x, phacelle.y * phacelle.zw);\n let faded_gullies = mix(vec3<f32>(fade_target, 0.0, 0.0), gullies * gully_weight, combi_mask);\n hs += faded_gullies * strength;\n out.magnitude += strength;\n fade_target = faded_gullies.x;\n // Fold the new octave into the mask (and the ridge-map variants).\n let rounding_for_octave =\n mix(rounding.y, rounding.x, clamp(phacelle.x + 0.5, 0.0, 1.0)) * rounding_mult;\n let new_mask = ori_ease_out(ori_smooth_start(sloping * onset.y, rounding_for_octave * onset.y));\n combi_mask = ori_pow_inv(combi_mask, detail) * new_mask;\n ridge_fade_target = mix(ridge_fade_target, gullies.x, ridge_combi_mask);\n ridge_combi_mask = ridge_combi_mask * ori_ease_out(sloping * onset.w);\n strength *= gain;\n freq *= lacunarity;\n rounding_mult *= rounding.w;\n }\n out.ridge = ridge_fade_target * (1.0 - ridge_combi_mask);\n out.delta = hs.x - height_and_slope.x;\n return out;\n}\n\n// ============================================================================================\n// Terrain display-extension generator: GPU twin of the #terrain recipe composition\n// (gradient-fbm base -> height mask -> OriErosion directional erosion). Used by the terrain\n// DISPLAY EXTENSION to continue a sim heightfield procedurally beyond the collision grid\n// (render-only, no sim state), and as the whole display surface in recipe mode.\n// Erosion kernel spec: docs/ori_erosion_spec.md; Fp sim twin: fp_ori_erosion.rs (keep in\n// sync). Requires the ori noise prelude (ori_hash2_cell).\n//\n// EXACTNESS CONTRACT: for a block built with TerrainHeightsNoise mode 4 (+ MaskFromHeight\n// + the OriErosion op / TerrainHeightsFromTerrain), this generator reproduces the sim field\n// bit-approximately (f32 vs Fp rounding only): same base fbm (seed + octave, gain, /total\n// normalization), same erosion dir recovery (central diff at +-1 cell,\n// n_dd = dh * base_scale_cells / W), same octave seed stream (seed ^ 0x9e3779b9 * (i+1)),\n// same jitter/gradient lattice hashes, same polynomial window.\n// ============================================================================================\n\nconst ORI_EXT_TAU: f32 = 6.28318530717959;\n\n// Gradient vector in [-1,1)^2 from one lattice hash (top/bottom 16 bits) - matches\n// fp_noise::gradient_at (Fp) at every lattice cell.\nfn ori_ext_gradient_at(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 32768.0 - 1.0;\n}\n\n// Gradient (Perlin) noise, zero at integer lattice points, quintic fade - the WGSL twin of\n// fp_noise::gradient2 (same hash family; f32 vs Fp rounding differs at sub-cm level).\nfn ori_ext_gradient2(p: vec2<f32>, seed: u32) -> f32 {\n let i = floor(p);\n let f = p - i;\n let c = vec2<i32>(i);\n let ga = ori_ext_gradient_at(c, seed);\n let gb = ori_ext_gradient_at(c + vec2<i32>(1, 0), seed);\n let gc = ori_ext_gradient_at(c + vec2<i32>(0, 1), seed);\n let gd = ori_ext_gradient_at(c + vec2<i32>(1, 1), seed);\n let va = dot(ga, f);\n let vb = dot(gb, f - vec2<f32>(1.0, 0.0));\n let vc = dot(gc, f - vec2<f32>(0.0, 1.0));\n let vd = dot(gd, f - vec2<f32>(1.0, 1.0));\n let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n let a = va + (vb - va) * u.x;\n let b = vc + (vd - vc) * u.x;\n return a + (b - a) * u.y;\n}\n\n// OriErosion feature point offset inside a cell: 0.5 + jitter per axis, jitter in\n// [-0.45, 0.45) - twin of fp_ori_erosion::feature_offset.\nfn ori_ext_feature_offset(c: vec2<i32>, seed: u32) -> vec2<f32> {\n let h = ori_hash2_cell(c.x, c.y, seed);\n return vec2<f32>(0.5) + (vec2<f32>(f32(h >> 16u), f32(h & 0xffffu)) / 65536.0 - 0.5) * 0.9;\n}\n\n// One OriErosion directional wave sample (value, ddx, ddy) - twin of\n// fp_ori_erosion::ori_erosion_sample (spec section 3): 5x5 jittered feature lattice,\n// polynomial window (1 - d2/4)^8 with EXACT zero support at R = 2 (the d2 >= 4 skip\n// drops only exact zeros - no truncation seams; the bell matches the reference\n// exp(-2 d2) including its tail, so grooves stay correlated across neighboring cells\n// and flow smoothly into each other), 1/tau-convention gradient along dir.\nfn ori_ext_erosion_sample(p: vec2<f32>, dir: vec2<f32>, seed: u32) -> vec3<f32> {\n let ip = floor(p);\n let fp_ = p - ip;\n let ci = vec2<i32>(ip);\n var v = 0.0;\n var g = vec2<f32>(0.0);\n var wt = 0.0;\n for (var i = -2; i <= 2; i = i + 1) {\n for (var k = -2; k <= 2; k = k + 1) {\n let q = ori_ext_feature_offset(ci + vec2<i32>(i, k), seed);\n let r = fp_ - vec2<f32>(f32(i), f32(k)) - q;\n let d2 = dot(r, r);\n if (d2 >= 4.0) { continue; }\n let t = 1.0 - d2 * 0.25;\n // Same x64-scaled form as the Fp twin (w = (8 t^4)^2 = 64 t^8); the scale cancels\n // in the weighted average, and matching the arithmetic shape keeps rounding aligned.\n let t2 = t * t;\n let t4s = t2 * t2 * 8.0;\n let w = t4s * t4s;\n wt = wt + w;\n // Phase wrapped to one period before the tau multiply (exact: cos period 1) - keeps\n // the trig argument in [0, tau) on both twins, bounding f32-vs-Fp drift (see the Fp\n // twin in fp_ori_erosion.rs).\n // Phase snapped to the exact 1/1024 grid (floor), twin of fp_ori_erosion: both\n // sides evaluate the SAME cos bucket, so residual positional drift cannot reach\n // the height output.\n let phase = floor(fract(dot(r, dir)) * 1024.0) / 1024.0 * ORI_EXT_TAU;\n v = v + cos(phase) * w;\n // 1/tau gradient convention, twin of fp_ori_erosion (branch_strength absorbs it;\n // keeps feedback magnitudes O(1) so positional Fp-vs-f32 drift stays bounded).\n g = g - sin(phase) * w * dir;\n }\n }\n // wt > 0 by construction: the own-cell feature is within sqrt(1.805) < R = 2 of any\n // sample (locked by fp_ori_erosion support tests).\n return vec3<f32>(v, g) / wt;\n}\n\n// Display-extension generator params (zeroed radius = disabled; see BakeUniform).\n// 20 f32 = 80 bytes (uniform address space rounds nested struct sizes to 16). Every Rust\n// fill site is a [f32; 20] in OriExtParams field order - keep them in lockstep.\nstruct OriExtParams {\n radius_cells: f32, // extension radius in sim cells (0 = off)\n base_scale_cells: f32, // base-noise feature size in cells\n base_amp_m: f32, // base amplitude in meters (heights = (fbm*0.5+0.5)*amp)\n base_octaves: f32, // gradient fbm octaves\n base_gain: f32, // per-octave amplitude factor (0.1 near-mono smooth; classic 0.5)\n erosion_scale_cells: f32,\n erosion_octaves: f32,\n erosion_strength_m: f32,\n slope_strength: f32,\n branch_strength: f32,\n mask_start_m: f32, // erosion amplitude mask: smoothstep(start, end, base) like\n mask_end_m: f32, // TerrainHeightsMaskFromHeight on the pre-erosion base\n base_seed: f32,\n erosion_seed: f32, // sim op seed (the scene may use a different stream than the base)\n domain_w_m: f32, // noise unit-domain width in meters (samples_x * cell_m); dir scale\n // > 0.5 = recipe display mode: the generator IS the display content everywhere (inside the\n // sim grid too - no Catmull-Rom, no boundary). Set per asset after the divergence check\n // verifies the sim heights match this recipe (see TerrainPass::ensure_terrain_asset).\n // Keep recipe_mode at flat index 15: terrain_tiles injects it as ext[15] per asset.\n recipe_mode: f32,\n erosion_gain: f32, // per-octave erosion amplitude factor (reference 0.5)\n erosion_lacunarity: f32, // per-octave erosion frequency factor (reference 2)\n _pad_a: f32,\n _pad_b: f32,\n};\n\n// Band-limit weight for one octave: 1 when the octave wavelength is well above the sample\n// spacing, fading to 0 approaching Nyquist. Baked per tile LOD; without this, octaves finer\n// than a coarse ring's spacing alias into DIFFERENT content per ring = straight seams at\n// ring boundaries (and the geomorph can't hide them if h_coarse == h).\nfn ori_ext_band(wavelength_cells: f32, step_cells: f32) -> f32 {\n return smoothstep(2.0, 4.0, wavelength_cells / max(step_cells, 1e-3));\n}\n\n// Base heights (meters, terrain-local like decode_height output). EXACT twin of\n// fp_noise::gradient_fbm2 (freq x2, amp x gain, per-octave seed = seed + i, sum / total) so\n// a sim block built with TerrainHeightsNoise mode 4 continues bit-consistently (f32 vs Fp\n// rounding aside) into the infinite field.\n// Returns (h_m, h_coarse_m): the coarse variant band-limits at 2x the sample spacing = the\n// parent LOD's value, restoring pop-free geomorph for extension tiles. Normalization uses\n// the UNfaded total so amplitude stays consistent with the sim block at any LOD (faded\n// octaves contribute their zero mean).\nfn ori_ext_base(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n let seed = u32(e.base_seed);\n // Division, not s * (1/scale): twin of terrain_gen::noise_heights (the reciprocal's\n // truncation grows with |s| and diverges from the Fp sim on steep recipes).\n let p = s / max(e.base_scale_cells, 1.0);\n var n = 0.0;\n var nc = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var f = 1.0;\n let n_oct = i32(clamp(e.base_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let wl = e.base_scale_cells / f;\n let bf = ori_ext_band(wl, step_cells);\n let bc = ori_ext_band(wl, step_cells * 2.0);\n total = total + amp;\n if (bf > 0.0) {\n let v = ori_ext_gradient2(p * f, seed + u32(i));\n n = n + v * amp * bf;\n nc = nc + v * amp * bc;\n }\n amp = amp * e.base_gain;\n f = f * 2.0;\n }\n n = n / total;\n nc = nc / total;\n return vec2<f32>((n * 0.5 + 0.5) * e.base_amp_m, (nc * 0.5 + 0.5) * e.base_amp_m);\n}\n\n// One band-limited OriErosion accumulation (the octave loop of spec section 4) with its own\n// mask, dir and branch-feedback chain. `step_cells` selects the band weights: evaluating at\n// 2x the step reproduces the PARENT LOD's chain exactly, which is what makes the geomorph\n// identity (child h_coarse == parent h) hold bit-for-bit.\n//\n// The erosion octave is NOT zero-mean: its local mean is ~= the windowed cos average,\n// dc(t) ~= max(0, exp(-K t^2)) with t = |dir| and K = ORI_EROSION_DC_K (numeric fit locked\n// by fp_ori_erosion::tests::dc_fit_matches_constant) - near 1 on flats, ~0 on steep slopes.\n// Band-limited octaves must fade toward that DC, not toward 0, or coarse LOD rings sink by\n// up to 0.5*strength on flats = curved cliff shelves at ring boundaries. Full-detail output\n// (band weights 1) is unchanged by dc.\nfn ori_ext_erosion_chain(pe: vec2<f32>, dir: vec2<f32>, m: f32, e: OriExtParams, step_cells: f32) -> f32 {\n var hx = 0.0;\n var hd = vec2<f32>(0.0);\n var a = 0.5 * m;\n var a_total = 0.0;\n var f = 1.0;\n let seed = u32(e.erosion_seed);\n let n_oct = i32(clamp(e.erosion_octaves, 1.0, 8.0));\n for (var i = 0; i < n_oct; i = i + 1) {\n let bf = ori_ext_band(e.erosion_scale_cells / f, step_cells);\n // Direction snapped to the exact 1/256 grid (floor), twin of fp_ori_erosion: the\n // branch-feedback recursion re-synchronizes with the Fp sim every octave instead of\n // compounding f32-vs-Fp drift. Exact: |bd|*256 < 2^20 stays integer-exact in f32.\n let bd = floor((dir + vec2<f32>(hd.y, -hd.x) * e.branch_strength) * 256.0) / 256.0;\n let dc = exp(-4.26 * dot(bd, bd)); // ORI_EROSION_DC_K\n if (bf > 0.0) {\n // Twin of the sim octave seed stream: seed ^ (0x9e3779b9 * (1 + octave)), wrapping.\n let oseed = seed ^ (0x9e3779b9u * (1u + u32(i)));\n let v = ori_ext_erosion_sample(pe * f, bd, oseed);\n hx = hx + mix(dc, v.x, bf) * a;\n hd = hd + v.yz * a * f * bf;\n } else {\n hx = hx + dc * a;\n }\n a_total = a_total + a;\n // Spectrum from the recipe keys (reference 0.5 / 2); clamps twin fp_ori_erosion.\n a = a * clamp(e.erosion_gain, 0.03125, 1.0);\n f = f * clamp(e.erosion_lacunarity, 1.125, 4.0);\n }\n // Amplitude-sum normalization (twin of fp_ori_erosion): output spans [-m, m] at any\n // octave schedule, so erosion_strength means the same carve depth everywhere.\n if (a_total <= 1e-4) { return 0.0; }\n return hx * m / a_total;\n}\n\nfn ori_ext_height(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec2<f32> {\n return ori_ext_height_user(s, e, step_cells).xy;\n}\n\n// (h, h_coarse, user01): user01 = the erosion accumulation hx * 0.5 + 0.5 = the recipe's\n// erosion/ridge map (the sim op's ErosionMapOut / ctx.user), full-detail chain.\nfn ori_ext_height_user(s: vec2<f32>, e: OriExtParams, step_cells: f32) -> vec3<f32> {\n let base = ori_ext_base(s, e, step_cells);\n // dir = curl of the recovered slope * slope_strength (dir = (ddy, -ddx) * s), twin of\n // ori_erosion_apply_with_base: dd_scale = base_scale_cells / W (heights and W in meters).\n // .x diffs feed the fine chain; .y (parent-band) diffs feed the coarse chain, exactly\n // like the parent LOD would compute them.\n let dd_scale = e.base_scale_cells / max(e.domain_w_m, 1e-6);\n let be = ori_ext_base(s + vec2<f32>(1.0, 0.0), e, step_cells);\n let bw = ori_ext_base(s - vec2<f32>(1.0, 0.0), e, step_cells);\n let bn = ori_ext_base(s + vec2<f32>(0.0, 1.0), e, step_cells);\n let bs = ori_ext_base(s - vec2<f32>(0.0, 1.0), e, step_cells);\n let dir_f = vec2<f32>((bn.x - bs.x), -(be.x - bw.x)) * dd_scale * e.slope_strength;\n // Division, twin of ori_erosion_apply_with_base (see the coordinate-drift note there).\n let pe = s / max(e.erosion_scale_cells, 1.0);\n // Mask on the pre-erosion base (MaskFromHeight twin; start < end enforced at the fill site).\n let m_f = smoothstep(e.mask_start_m, e.mask_end_m, base.x);\n let hx = ori_ext_erosion_chain(pe, dir_f, m_f, e, step_cells);\n // Coarse chain (geomorph target = the parent LOD's value): only distinct when 2x the\n // step actually band-limits something the fine step does not.\n let n_oct = clamp(e.erosion_octaves, 1.0, 8.0);\n let wl_min_ero = e.erosion_scale_cells / pow(clamp(e.erosion_lacunarity, 1.125, 4.0), n_oct - 1.0);\n let wl_min_base = e.base_scale_cells / exp2(clamp(e.base_octaves, 1.0, 8.0) - 1.0);\n var hxc = hx;\n var base_c = base.x;\n if (ori_ext_band(min(wl_min_ero, wl_min_base), step_cells * 2.0) < 1.0) {\n let dir_c = vec2<f32>((bn.y - bs.y), -(be.y - bw.y)) * dd_scale * e.slope_strength;\n let m_c = smoothstep(e.mask_start_m, e.mask_end_m, base.y);\n hxc = ori_ext_erosion_chain(pe, dir_c, m_c, e, step_cells * 2.0);\n base_c = base.y;\n }\n // The -0.5 carve bias is NOT masked - masked zones sink uniformly (the sim op does the\n // same; the water datum depends on it).\n return vec3<f32>(\n base.x + (hx - 0.5) * e.erosion_strength_m,\n base_c + (hxc - 0.5) * e.erosion_strength_m,\n clamp(hx * 0.5 + 0.5, 0.0, 1.0),\n );\n}\n"},{"label":"grass_spawn_box","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// grass_spawn_box.wgsl\n// One grass instance per top-face cell of a grass-flagged cube (`grass = true` world key).\n// Group(0) reuses the terrain GrassSpawn BGL; only the uniform + output are declared here\n// (height/weight textures + sampler are bound as dummies and never read). Group(1) is the\n// surface-weight array pair from the default spawn layout, likewise unused.\n\nstruct BoxSpawnUniform {\n spawn_samples_x: u32,\n spawn_samples_y: u32,\n scale_base: f32,\n _pad0: f32,\n center: vec3<f32>, // top-face center, world render meters\n seed: u32, // per-object decorrelation (object id derived)\n half_yaw: vec4<f32>, // half_x_m, half_z_m, yaw_cos, yaw_sin (sim-Z yaw)\n border: vec4<f32>, // x = mode (1 full, 2 round, 3 noise), y = noise feature size m\n};\n\n@group(0) @binding(3) var<uniform> box_u: BoxSpawnUniform;\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(4) var<storage, read_write> out_instances: array<GrassInstance>;\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\nfn hash_cell(ix: i32, iz: i32, salt: u32) -> u32 {\n return hash_u32(bitcast<u32>(ix) ^ (bitcast<u32>(iz) * 0x9E3779B9u) ^ salt);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let x = gid.x;\n let y = gid.y;\n if (x >= box_u.spawn_samples_x || y >= box_u.spawn_samples_y) { return; }\n let idx = y * box_u.spawn_samples_x + x;\n\n let half_x = box_u.half_yaw.x;\n let half_z = box_u.half_yaw.y;\n let cell_u = 2.0 * half_x / f32(box_u.spawn_samples_x);\n let cell_v = 2.0 * half_z / f32(box_u.spawn_samples_y);\n\n // Deterministic per-cell hash, decorrelated per object. Cells are face-local, so\n // blades keep their spot when the box moves (per-frame respawn re-anchors rigidly).\n let h0 = hash_cell(i32(x), i32(y), 0xA5A5A5A5u ^ box_u.seed);\n let r01 = f32(h0 & 65535u) / 65535.0;\n let r02 = f32(hash_u32(h0 ^ 0xA2C2A2C2u) & 65535u) / 65535.0;\n let r03 = f32(hash_u32(h0 ^ 0x19B9C5F1u) & 65535u) / 65535.0;\n\n var u = -half_x + (f32(x) + 0.5) * cell_u + (r02 - 0.5) * cell_u * 0.85;\n var v = -half_z + (f32(y) + 0.5) * cell_v + (r03 - 0.5) * cell_v * 0.85;\n u = clamp(u, -half_x, half_x);\n v = clamp(v, -half_z, half_z);\n\n // Border mask: 1 = full face; 2 = round (superellipse falloff, carves the corners);\n // 3 = noise (ragged value-noise edge, threshold rises toward the rim).\n var density = 1.0;\n let mode = u32(box_u.border.x);\n if (mode == 2u) {\n let se = pow(abs(u) / max(half_x, 1e-4), 4.0) + pow(abs(v) / max(half_z, 1e-4), 4.0);\n density = 1.0 - smoothstep(0.75, 1.0, se);\n } else if (mode == 3u) {\n let edge = min(half_x - abs(u), half_z - abs(v)); // meters to the nearest rim\n let band = max(box_u.border.y, 0.10); // noise feature size\n let n = ori_value_noise2((u + 1000.0) / band, (v + 1000.0) / band, box_u.seed);\n density = smoothstep(0.0, band, edge + (n - 0.5) * band * 1.6);\n }\n\n let density_boost = clamp(density * 2.5, 0.0, 1.0);\n let do_spawn = select(0.0, 1.0, r01 < density_boost);\n let scale = box_u.scale_base * do_spawn;\n\n // Face-local (u, v) -> world XZ. Sim -> render is (x, y, z) -> (x, z, -y), so the\n // box sim x-axis (c, s, 0) lands at (c, -s) in render XZ and sim y at (-s, -c).\n let c = box_u.half_yaw.z;\n let s = box_u.half_yaw.w;\n let world_x = box_u.center.x + u * c - v * s;\n let world_z = box_u.center.z - u * s - v * c;\n\n let yaw = (f32(hash_u32(h0)) / 4294967295.0) * 6.28318530718;\n out_instances[idx].pos_scale = vec4<f32>(world_x, box_u.center.y, world_z, scale);\n // Keep a fractional component for variation (floor() will be 0 => type 0).\n out_instances[idx].yaw_seed = vec2<f32>(yaw, r01);\n // Flat top: +Y normal, whose octahedral encoding is exactly (0, 0).\n out_instances[idx]._pad = vec2<f32>(0.0, 0.0);\n}\n"},{"label":"grass_cull_lod","code":"// grass_cull_lod.wgsl\n// Output: visible_flags[idx] = 0/1, visible_ids[idx] = u32::MAX\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nstruct GrassCullingParams {\n instance_count: u32,\n enable_frustum: u32,\n type_index: u32, // 0..3 (RGBA)\n _pad_u0: u32,\n min_distance_m: f32,\n max_distance_m: f32,\n frustum_margin_m: f32,\n fade_start_m: f32, // disabled if fade_end_m <= fade_start_m\n fade_end_m: f32,\n far_thinning_power: f32, // 0 => disables thinning\n // Leaf cards only (grass passes 0 = off): scale the distance windows per instance by\n // clamp(size * size_fade_ref_inv, 1, SIZE_FADE_MAX_FACTOR) * size_fade_fov_scale, so\n // big-tree cards persist toward their later mesh->impostor handoff and zoom extends them.\n size_fade_ref_inv: f32,\n size_fade_fov_scale: f32,\n};\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> cameraData: CameraUniform;\n@group(0) @binding(1) var<uniform> params: GrassCullingParams;\n@group(0) @binding(2) var<storage, read> instances: array<GrassInstance>;\n@group(0) @binding(3) var<storage, read_write> visible_flags: array<u32>;\n@group(0) @binding(4) var<storage, read_write> visible_ids: array<u32>;\n\n// Shader-tweakable knobs (Ctrl+Shift+R reload).\n// - LOD_DISTANCE_SCALE scales ALL distance ranges.\n// - params.far_thinning_power affects stochastic thinning between fade_start..fade_end (last LOD).\n// - 1.0 = linear fade\n// - >1.0 = thinner far grass (fewer instances survive near fade_end)\n// - <1.0 = denser far grass\n// - 0.0 = disables thinning (t -> 1)\nconst LOD_DISTANCE_SCALE: f32 = 1.0; // base set by GrassCullDistanceM\nconst SIZE_FADE_MAX_FACTOR: f32 = 2.5; // must match CARD_SIZE_FADE_MAX in tree_leaf_pass.rs\n\nfn clamp3(v: vec3<f32>, lo: vec3<f32>, hi: vec3<f32>) -> vec3<f32> {\n return min(max(v, lo), hi);\n}\n\nfn aabb_distance_sq(p: vec3<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> f32 {\n let q = clamp3(p, bmin, bmax);\n let d = p - q;\n return dot(d, d);\n}\n\n// Ground-plane (XZ) distance to an AABB projected on XZ (ignores Y).\nfn aabb_distance_sq_xz(p_xz: vec2<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> f32 {\n let qx = clamp(p_xz.x, bmin.x, bmax.x);\n let qz = clamp(p_xz.y, bmin.z, bmax.z);\n let dx = p_xz.x - qx;\n let dz = p_xz.y - qz;\n return dx * dx + dz * dz;\n}\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>, margin: f32) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < -margin;\n}\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.instance_count) { return; }\n\n visible_ids[idx] = 0xFFFFFFFFu;\n\n let inst = instances[idx];\n let pos = inst.pos_scale.xyz;\n let scale = inst.pos_scale.w;\n if (scale <= 0.0) {\n visible_flags[idx] = 0u;\n return;\n }\n // Type binning (integer part of yaw_seed.y) - GRASS ONLY: the four RGBA density\n // channels share one instance buffer and cull per channel. Leaf cards\n // (size_fade_ref_inv > 0) pack card ASPECT into that same integer part\n // (pack_aspect_seed in tree_leaf_pass.rs), so binning them against type_index 0\n // silently deleted every aspect >= 1.125 card - mat/shelf species rendered bare.\n if (params.size_fade_ref_inv == 0.0) {\n let ty = u32(floor(inst.yaw_seed.y));\n if (ty != params.type_index) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n // Conservative blade bounds in world space (Y-up).\n // Note: includes room for vertex-stage bending (wind + player benders).\n let ext = vec3<f32>(0.22, 0.35, 0.22) * max(scale / 0.15, 0.2);\n let bmin = pos - ext;\n let bmax = pos + ext;\n\n // Distance range gating.\n // Use ground distance (XZ) so fade/cull match top-down expectations (camera height shouldn't shrink the radius).\n let cam_xz = vec2<f32>(cameraData.camera_position.x, cameraData.camera_position.z);\n let d2 = aabb_distance_sq_xz(cam_xz, bmin, bmax);\n let d = sqrt(max(d2, 0.0));\n // Per-instance range factor (leaf cards only; 1.0 for grass keeps the path bit-identical).\n var range_factor = 1.0;\n if (params.size_fade_ref_inv > 0.0) {\n range_factor = clamp(scale * params.size_fade_ref_inv, 1.0, SIZE_FADE_MAX_FACTOR)\n * max(params.size_fade_fov_scale, 1.0);\n }\n let min_d = max(params.min_distance_m, 0.0) * LOD_DISTANCE_SCALE;\n let max_d = max(params.max_distance_m, 0.0) * LOD_DISTANCE_SCALE * range_factor;\n if (d < min_d || d > max_d) {\n visible_flags[idx] = 0u;\n return;\n }\n\n // Optional fade thinning near far range (used only on the last LOD).\n let fade_start = params.fade_start_m * LOD_DISTANCE_SCALE * range_factor;\n let fade_end = params.fade_end_m * LOD_DISTANCE_SCALE * range_factor;\n // Note: fade_start may be 0 (fade from camera outward). Disable is encoded as fade_end <= fade_start.\n if (fade_end > fade_start && d > fade_start) {\n var t = clamp((fade_end - d) / (fade_end - fade_start), 0.0, 1.0);\n let thin_p = max(params.far_thinning_power, 0.0);\n if (thin_p != 1.0) {\n // Note: thin_p=0 => pow(..,0)=1 => disables thinning.\n t = pow(max(t, 1e-6), thin_p);\n }\n let r01 = f32(hash_u32(idx) & 65535u) / 65535.0;\n if (r01 > t) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n if (params.enable_frustum == 0u) {\n visible_flags[idx] = 1u;\n return;\n }\n\n // Frustum planes from view_proj (WebGPU depth 0..1).\n let m = cameraData.view_proj;\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2)\n );\n for (var pi = 0u; pi < 6u; pi = pi + 1u) {\n if (aabb_outside_plane(planes[pi], bmin, bmax, params.frustum_margin_m)) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n visible_flags[idx] = 1u;\n}\n\n"},{"label":"compact_grass_instances","code":"// compact_grass_instances.wgsl\n\nstruct PrefixSumUniform {\n instance_count: u32,\n submesh_id: u32,\n chunk_count_0: u32,\n chunk_count_1: u32,\n chunk_count_2: u32,\n chunk_count_3: u32,\n};\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var<storage, read> visible_ids: array<u32>;\n@group(0) @binding(1) var<storage, read> original_instances: array<GrassInstance>;\n@group(0) @binding(2) var<storage, read_write> compact_instances: array<GrassInstance>;\n@group(0) @binding(3) var<uniform> params: PrefixSumUniform;\n@group(0) @binding(4) var<storage, read_write> debug_buffer: array<u32>;\n// GrassExtra (engine enrich pass output) rides along with instances.\n@group(0) @binding(5) var<storage, read> original_extras: array<vec2<u32>>;\n@group(0) @binding(6) var<storage, read_write> compact_extras: array<vec2<u32>>;\n\n@compute @workgroup_size(256)\nfn compact_grass_instances(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i < params.instance_count) {\n let original_idx = visible_ids[i];\n if (original_idx != 0xFFFFFFFFu) {\n compact_instances[i] = original_instances[original_idx];\n compact_extras[i] = original_extras[original_idx];\n if (original_idx >= params.instance_count) {\n debug_buffer[0] = 7001u;\n debug_buffer[1] = i;\n debug_buffer[2] = original_idx;\n }\n }\n }\n}\n\n// Leaf-card compaction (tree_leaf_pass) reuses this shader but has no enrich extras;\n// a separate entry point keeps bindings 5/6 out of its pipeline layout requirements.\n@compute @workgroup_size(256)\nfn compact_instances_no_extras(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i < params.instance_count) {\n let original_idx = visible_ids[i];\n if (original_idx != 0xFFFFFFFFu) {\n compact_instances[i] = original_instances[original_idx];\n if (original_idx >= params.instance_count) {\n debug_buffer[0] = 7001u;\n debug_buffer[1] = i;\n debug_buffer[2] = original_idx;\n }\n }\n }\n}\n\n"},{"label":"grass_enrich","code":"// grass_enrich.wgsl\n// Engine-owned enrich pass: derives per-instance clump/blade params (GrassExtra)\n// from spawned GrassInstance positions. Runs after any spawn shader (default or\n// custom), so the 32-byte GrassInstance spawn contract stays frozen.\n//\n// extras[i].x = clump_hue | clump_facing<<8 | clump_height<<16 | clump_tilt<<24 (u8 each, 0..1)\n// extras[i].y = blade_hash | blade_bend<<8 | clump_edge<<16 (u8 each, 0..1)\n\nstruct EnrichUniform {\n instance_count: u32,\n clump_cell_m: f32,\n seed: u32,\n _pad0: u32,\n};\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var<storage, read> instances: array<GrassInstance>;\n@group(0) @binding(1) var<storage, read_write> extras: array<vec2<u32>>;\n@group(0) @binding(2) var<uniform> params: EnrichUniform;\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u; v *= 0x7feb352du;\n v ^= v >> 15u; v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\nfn hash_cell(ix: i32, iz: i32, salt: u32) -> u32 {\n return hash_u32(bitcast<u32>(ix) ^ (bitcast<u32>(iz) * 0x9E3779B9u) ^ salt);\n}\nfn h01(h: u32) -> f32 { return f32(h & 0xFFFFu) / 65535.0; }\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= params.instance_count) { return; }\n let p = instances[i].pos_scale.xz;\n\n // Jittered-grid voronoi: nearest clump center in the 3x3 cell neighborhood.\n let cell_m = max(params.clump_cell_m, 1e-3);\n let base = vec2<i32>(floor(p / cell_m));\n var best_d2 = 1e30;\n var best_h = 0u;\n for (var dz = -1; dz <= 1; dz = dz + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let c = base + vec2<i32>(dx, dz);\n let h = hash_cell(c.x, c.y, params.seed);\n let jitter = vec2<f32>(h01(h), h01(hash_u32(h ^ 0x68bc21ebu))) - 0.5;\n let center = (vec2<f32>(c) + 0.5 + jitter * 0.8) * cell_m;\n let d = p - center;\n let d2 = dot(d, d);\n if (d2 < best_d2) { best_d2 = d2; best_h = h; }\n }\n }\n\n // Per-clump params (shared by all blades in the clump).\n let hue = h01(hash_u32(best_h ^ 0x11u));\n let facing = h01(hash_u32(best_h ^ 0x22u));\n let height = h01(hash_u32(best_h ^ 0x33u));\n let tilt = h01(hash_u32(best_h ^ 0x44u));\n // Distance to clump center normalized by clump radius (0 = center, 1 = edge).\n let edge = clamp(sqrt(best_d2) / (0.75 * cell_m), 0.0, 1.0);\n // Per-blade hash decorrelated from the spawn seed (position-derived, deterministic).\n let bh = hash_u32(bitcast<u32>(p.x) ^ (bitcast<u32>(p.y) * 0x9E3779B9u));\n let blade_hash = h01(bh);\n let blade_bend = h01(hash_u32(bh ^ 0x55u));\n\n extras[i] = vec2<u32>(\n u32(hue * 255.0) | (u32(facing * 255.0) << 8u) | (u32(height * 255.0) << 16u) | (u32(tilt * 255.0) << 24u),\n u32(blade_hash * 255.0) | (u32(blade_bend * 255.0) << 8u) | (u32(edge * 255.0) << 16u)\n );\n}\n"},{"label":"write_indirect_instance_count_all","code":"// write_indirect_instance_count_all.wgsl\n// Read last prefix-sum entry and write instance_count into all DrawIndexedIndirect args.\n\nstruct Params {\n instance_count: u32,\n submesh_count: u32,\n _pad: vec2<u32>,\n};\n\n@group(0) @binding(0) var<storage, read> visible_flags: array<u32>;\n@group(0) @binding(1) var<uniform> params: Params;\n@group(0) @binding(2) var<storage, read_write> indirect_args: array<u32>;\n\nconst STRIDE_U32: u32 = 5u;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let sid = gid.x;\n if (sid >= params.submesh_count) { return; }\n let count = select(0u, visible_flags[params.instance_count - 1u], params.instance_count > 0u);\n let base = sid * STRIDE_U32;\n indirect_args[base + 1u] = count;\n}\n\n"},{"label":"LeafCull","code":"// grass_cull_lod.wgsl\n// Output: visible_flags[idx] = 0/1, visible_ids[idx] = u32::MAX\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nstruct GrassCullingParams {\n instance_count: u32,\n enable_frustum: u32,\n type_index: u32, // 0..3 (RGBA)\n _pad_u0: u32,\n min_distance_m: f32,\n max_distance_m: f32,\n frustum_margin_m: f32,\n fade_start_m: f32, // disabled if fade_end_m <= fade_start_m\n fade_end_m: f32,\n far_thinning_power: f32, // 0 => disables thinning\n // Leaf cards only (grass passes 0 = off): scale the distance windows per instance by\n // clamp(size * size_fade_ref_inv, 1, SIZE_FADE_MAX_FACTOR) * size_fade_fov_scale, so\n // big-tree cards persist toward their later mesh->impostor handoff and zoom extends them.\n size_fade_ref_inv: f32,\n size_fade_fov_scale: f32,\n};\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> cameraData: CameraUniform;\n@group(0) @binding(1) var<uniform> params: GrassCullingParams;\n@group(0) @binding(2) var<storage, read> instances: array<GrassInstance>;\n@group(0) @binding(3) var<storage, read_write> visible_flags: array<u32>;\n@group(0) @binding(4) var<storage, read_write> visible_ids: array<u32>;\n\n// Shader-tweakable knobs (Ctrl+Shift+R reload).\n// - LOD_DISTANCE_SCALE scales ALL distance ranges.\n// - params.far_thinning_power affects stochastic thinning between fade_start..fade_end (last LOD).\n// - 1.0 = linear fade\n// - >1.0 = thinner far grass (fewer instances survive near fade_end)\n// - <1.0 = denser far grass\n// - 0.0 = disables thinning (t -> 1)\nconst LOD_DISTANCE_SCALE: f32 = 1.0; // base set by GrassCullDistanceM\nconst SIZE_FADE_MAX_FACTOR: f32 = 2.5; // must match CARD_SIZE_FADE_MAX in tree_leaf_pass.rs\n\nfn clamp3(v: vec3<f32>, lo: vec3<f32>, hi: vec3<f32>) -> vec3<f32> {\n return min(max(v, lo), hi);\n}\n\nfn aabb_distance_sq(p: vec3<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> f32 {\n let q = clamp3(p, bmin, bmax);\n let d = p - q;\n return dot(d, d);\n}\n\n// Ground-plane (XZ) distance to an AABB projected on XZ (ignores Y).\nfn aabb_distance_sq_xz(p_xz: vec2<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> f32 {\n let qx = clamp(p_xz.x, bmin.x, bmax.x);\n let qz = clamp(p_xz.y, bmin.z, bmax.z);\n let dx = p_xz.x - qx;\n let dz = p_xz.y - qz;\n return dx * dx + dz * dz;\n}\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>, margin: f32) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < -margin;\n}\n\nfn hash_u32(x: u32) -> u32 {\n var v = x;\n v ^= v >> 16u;\n v *= 0x7feb352du;\n v ^= v >> 15u;\n v *= 0x846ca68bu;\n v ^= v >> 16u;\n return v;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= params.instance_count) { return; }\n\n visible_ids[idx] = 0xFFFFFFFFu;\n\n let inst = instances[idx];\n let pos = inst.pos_scale.xyz;\n let scale = inst.pos_scale.w;\n if (scale <= 0.0) {\n visible_flags[idx] = 0u;\n return;\n }\n // Type binning (integer part of yaw_seed.y) - GRASS ONLY: the four RGBA density\n // channels share one instance buffer and cull per channel. Leaf cards\n // (size_fade_ref_inv > 0) pack card ASPECT into that same integer part\n // (pack_aspect_seed in tree_leaf_pass.rs), so binning them against type_index 0\n // silently deleted every aspect >= 1.125 card - mat/shelf species rendered bare.\n if (params.size_fade_ref_inv == 0.0) {\n let ty = u32(floor(inst.yaw_seed.y));\n if (ty != params.type_index) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n // Conservative blade bounds in world space (Y-up).\n // Note: includes room for vertex-stage bending (wind + player benders).\n let ext = vec3<f32>(0.22, 0.35, 0.22) * max(scale / 0.15, 0.2);\n let bmin = pos - ext;\n let bmax = pos + ext;\n\n // Distance range gating.\n // Use ground distance (XZ) so fade/cull match top-down expectations (camera height shouldn't shrink the radius).\n let cam_xz = vec2<f32>(cameraData.camera_position.x, cameraData.camera_position.z);\n let d2 = aabb_distance_sq_xz(cam_xz, bmin, bmax);\n let d = sqrt(max(d2, 0.0));\n // Per-instance range factor (leaf cards only; 1.0 for grass keeps the path bit-identical).\n var range_factor = 1.0;\n if (params.size_fade_ref_inv > 0.0) {\n range_factor = clamp(scale * params.size_fade_ref_inv, 1.0, SIZE_FADE_MAX_FACTOR)\n * max(params.size_fade_fov_scale, 1.0);\n }\n let min_d = max(params.min_distance_m, 0.0) * LOD_DISTANCE_SCALE;\n let max_d = max(params.max_distance_m, 0.0) * LOD_DISTANCE_SCALE * range_factor;\n if (d < min_d || d > max_d) {\n visible_flags[idx] = 0u;\n return;\n }\n\n // Optional fade thinning near far range (used only on the last LOD).\n let fade_start = params.fade_start_m * LOD_DISTANCE_SCALE * range_factor;\n let fade_end = params.fade_end_m * LOD_DISTANCE_SCALE * range_factor;\n // Note: fade_start may be 0 (fade from camera outward). Disable is encoded as fade_end <= fade_start.\n if (fade_end > fade_start && d > fade_start) {\n var t = clamp((fade_end - d) / (fade_end - fade_start), 0.0, 1.0);\n let thin_p = max(params.far_thinning_power, 0.0);\n if (thin_p != 1.0) {\n // Note: thin_p=0 => pow(..,0)=1 => disables thinning.\n t = pow(max(t, 1e-6), thin_p);\n }\n let r01 = f32(hash_u32(idx) & 65535u) / 65535.0;\n if (r01 > t) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n if (params.enable_frustum == 0u) {\n visible_flags[idx] = 1u;\n return;\n }\n\n // Frustum planes from view_proj (WebGPU depth 0..1).\n let m = cameraData.view_proj;\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2)\n );\n for (var pi = 0u; pi < 6u; pi = pi + 1u) {\n if (aabb_outside_plane(planes[pi], bmin, bmax, params.frustum_margin_m)) {\n visible_flags[idx] = 0u;\n return;\n }\n }\n\n visible_flags[idx] = 1u;\n}\n\n"},{"label":"LeafCompact","code":"// compact_grass_instances.wgsl\n\nstruct PrefixSumUniform {\n instance_count: u32,\n submesh_id: u32,\n chunk_count_0: u32,\n chunk_count_1: u32,\n chunk_count_2: u32,\n chunk_count_3: u32,\n};\n\nstruct GrassInstance {\n pos_scale: vec4<f32>,\n yaw_seed: vec2<f32>,\n _pad: vec2<f32>,\n};\n\n@group(0) @binding(0) var<storage, read> visible_ids: array<u32>;\n@group(0) @binding(1) var<storage, read> original_instances: array<GrassInstance>;\n@group(0) @binding(2) var<storage, read_write> compact_instances: array<GrassInstance>;\n@group(0) @binding(3) var<uniform> params: PrefixSumUniform;\n@group(0) @binding(4) var<storage, read_write> debug_buffer: array<u32>;\n// GrassExtra (engine enrich pass output) rides along with instances.\n@group(0) @binding(5) var<storage, read> original_extras: array<vec2<u32>>;\n@group(0) @binding(6) var<storage, read_write> compact_extras: array<vec2<u32>>;\n\n@compute @workgroup_size(256)\nfn compact_grass_instances(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i < params.instance_count) {\n let original_idx = visible_ids[i];\n if (original_idx != 0xFFFFFFFFu) {\n compact_instances[i] = original_instances[original_idx];\n compact_extras[i] = original_extras[original_idx];\n if (original_idx >= params.instance_count) {\n debug_buffer[0] = 7001u;\n debug_buffer[1] = i;\n debug_buffer[2] = original_idx;\n }\n }\n }\n}\n\n// Leaf-card compaction (tree_leaf_pass) reuses this shader but has no enrich extras;\n// a separate entry point keeps bindings 5/6 out of its pipeline layout requirements.\n@compute @workgroup_size(256)\nfn compact_instances_no_extras(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i < params.instance_count) {\n let original_idx = visible_ids[i];\n if (original_idx != 0xFFFFFFFFu) {\n compact_instances[i] = original_instances[original_idx];\n if (original_idx >= params.instance_count) {\n debug_buffer[0] = 7001u;\n debug_buffer[1] = i;\n debug_buffer[2] = original_idx;\n }\n }\n }\n}\n\n"},{"label":"leaf_gbuffer","code":"// leaf_gbuffer.wgsl\n// Tree leaf cluster cards: instanced quads oriented by packed oct normal + yaw, sampling a\n// per-species frond texture array layer (cutout alpha). Instance layout == GrassInstance,\n// with `_pad` packed as: x = oct normal 10+10 bits (integer), y = bitfield\n// layer(5) | ao(7) | shell-center index(12) (exact integer; see pack_card_meta).\n// Shading normal is a per-pixel proxy: the lobe ELLIPSOID's surface normal at the pixel\n// (gradient = offset / radii^2), blended over the flat card facing by the shell's w (from\n// leaf_shell_pct), so crowns shade as smooth rounded volumes while card geometry stays put.\n// A luminance bump from the frond texture adds leafy high-frequency detail on top\n// (leaf_detail_pct), canopy AO darkens trunk-column/bottom cards (leaf_ao_pct), and the\n// vertex stage bows cards by leaf_curl_pct - the three strengths ride the shell entry's\n// radii.w as an exact integer bitfield (curl*50 << 16 | detail*50 << 8 | ao*100).\n// Cards seen nearly edge-on fade out (geometric facing vs view ray) instead of smearing\n// into long anisotropic streaks.\n\nstruct VertexIn {\n // Must match `Vertex` layout (static meshes)\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Leaf instance (== GrassInstance)\n @location(4) pos_scale: vec4<f32>, // xyz = world pos (meters), w = card size (meters)\n @location(5) yaw_seed: vec2<f32>, // x = yaw radians, y = seed [0,1)\n @location(6) packed: vec2<f32>, // x = packed oct normal, y = layer + ao\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n // [render dir x, render dir z, strength 0..1, gustiness 0..1]\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(1) @binding(0) var t_leaf: texture_2d_array<f32>;\n@group(1) @binding(1) var s_leaf: sampler;\n// Shell ellipsoid of this card's lobe/crown: center_k = world center + proxy blend weight,\n// radii = per-axis half extents (world meters).\nstruct LeafShell {\n center_k: vec4<f32>,\n radii: vec4<f32>,\n};\n@group(1) @binding(2) var<storage, read> u_centers: array<LeafShell>;\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) @interpolate(flat) layer: u32,\n @location(4) ao: f32,\n @location(5) world_pos: vec3<f32>,\n @location(6) @interpolate(flat) cidx: u32,\n @location(7) tangent_w: vec3<f32>, // card local +X in world (detail bump frame)\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n};\n\nfn sign_nonzero(x: f32) -> f32 { return select(-1.0, 1.0, x >= 0.0); }\n\nfn oct_decode(p_in: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(p_in.x, 1.0 - abs(p_in.x) - abs(p_in.y), p_in.y);\n if (v.y < 0.0) {\n let x = (1.0 - abs(v.z)) * sign_nonzero(v.x);\n let z = (1.0 - abs(v.x)) * sign_nonzero(v.z);\n v = vec3<f32>(x, -v.y, z);\n }\n return normalize(v);\n}\n\nfn unpack_oct(packed: f32) -> vec2<f32> {\n let u = u32(packed + 0.5);\n let qx = f32(u >> 10u) / 1023.0 * 2.0 - 1.0;\n let qy = f32(u & 1023u) / 1023.0 * 2.0 - 1.0;\n return vec2<f32>(qx, qy);\n}\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n\n let pos = input.pos_scale.xyz;\n let scale = input.pos_scale.w;\n let yaw = input.yaw_seed.x;\n // yaw_seed.y packs card aspect (integer part, 0.25 steps from 1.0) + tone seed (fraction).\n let seed = fract(input.yaw_seed.y);\n let aspect = 1.0 + floor(input.yaw_seed.y) * 0.25;\n // packed.y bitfield: layer(5) | ao(7) | shell-center index(12). (`meta` is reserved in WGSL.)\n let card_meta = u32(input.packed.y + 0.5);\n out.layer = card_meta & 31u;\n out.ao = f32((card_meta >> 5u) & 127u) / 127.0;\n out.cidx = card_meta >> 12u;\n\n // Orientation: card local +Z -> leaf normal n; yaw spins tangent frame around n.\n let n = oct_decode(unpack_oct(input.packed.x));\n let a = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(n.y) > 0.999);\n let t0 = normalize(cross(a, n));\n let b0 = cross(n, t0);\n let c = cos(yaw);\n let s = sin(yaw);\n let t = t0 * c + b0 * s;\n let b = b0 * c - t0 * s;\n let rot = mat3x3<f32>(t, b, n); // local X, Y, Z in world\n\n // Wind: coherent gust translation (same wave as gbuffer.wgsl tree_wind_offset so leaves\n // ride their branches) + per-card tip flutter along the normal.\n let time = u_camera.time_seconds;\n let strength = u_camera.wind.z;\n let wdir = vec3<f32>(u_camera.wind.x, 0.0, u_camera.wind.y);\n let tree_phase = dot(pos, wdir) * 0.15;\n let gust = 0.5 + 0.5 * sin(time * 0.9 - tree_phase) * u_camera.wind.w;\n let bs = sin(time * (1.8 + 1.2 * strength) + seed * 6.2831853 - tree_phase);\n let canopy = strength * (0.10 + 0.22 * gust) + bs * strength * (0.35 + 0.65 * gust) * 0.12;\n let phase = seed * 6.28318 + dot(pos.xz, vec2<f32>(0.31, 0.47));\n let flutter_amp = 0.5 + 0.5 * strength + 0.5 * gust * u_camera.wind.w;\n let sway = (sin(time * 2.1 + phase) * 0.35 + sin(time * 4.7 + phase * 1.7) * 0.15) * flutter_amp;\n let tip_t = input.uv.y;\n // Horizon-style curved cards: bow each quad of the X-cross out of its own plane along its\n // local normal - lateral crescent (peaks mid-width) + tip arch (peaks at the tip), sign and\n // magnitude per card from the seed. Edge-on views then show a curved sliver instead of a\n // vanishing line, and card orientations read more varied. Shading is unaffected: cards\n // light with the shell proxy normal, and the edge fade / detail-bump frames follow the\n // interpolated geometry automatically. Mirror any change in leaf_shadow.wgsl.\n // leaf_curl_pct scales the whole effect (byte 2 of the shell strengths bitfield;\n // 0 = flat cards, 100 = default). Shadow pass keeps the default scale (no shell buffer\n // there); the silhouette mismatch at non-default curl is a few cm and shadows are soft.\n let curl_s = f32(u32(u_centers[out.cidx].radii.w + 0.5) >> 16u) * 0.02;\n let curl = (fract(seed * 13.37) - 0.5) * 1.2 * curl_s; // crescent depth ~0.15 card sizes at 100\n let arch = (fract(seed * 7.91) - 0.5) * 0.5 * curl_s; // tip bend ~0.25 card sizes at 100\n let u_lat = input.uv.x - 0.5;\n let bow = curl * (0.25 - u_lat * u_lat) + arch * tip_t * tip_t;\n let local = vec3<f32>(input.position.x * aspect, input.position.yz) + input.normal * bow;\n var world_pos = pos + rot * (local * scale);\n world_pos += wdir * canopy;\n world_pos += n * (sway * 0.12 * scale * tip_t * tip_t);\n world_pos.x += sin(time * 0.9 + phase) * 0.02 * scale * tip_t;\n\n out.clip_position = u_camera.view_proj * vec4<f32>(world_pos, 1.0);\n // Both X-cross quads light with the instance shell normal (not their geometric normal):\n // the cross exists for silhouette coverage, not to introduce hard sideways lighting.\n out.world_normal = n;\n out.world_pos = world_pos;\n out.tangent_w = t;\n out.uv = input.uv;\n out.seed = seed;\n out.cur_clip = u_camera.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera.prev_unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>, // Rgba8Unorm\n @location(1) normal: vec4<f32>, // Rgba16Float\n @location(2) orm: vec4<f32>, // Rgba8Unorm\n @location(3) velocity: vec2<f32>, // Rg16Float\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_normal: vec3<f32>,\n @location(1) uv: vec2<f32>,\n @location(2) seed: f32,\n @location(3) @interpolate(flat) layer: u32,\n @location(4) ao: f32,\n @location(5) world_pos: vec3<f32>,\n @location(6) @interpolate(flat) cidx: u32,\n @location(7) tangent_w: vec3<f32>,\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n // Frond texture cutout. Card V grows base->tip and the frond bakes grow the tuft\n // along oritex +v, so card UV maps to texture UV directly (no flip).\n let tex = textureSampleBias(t_leaf, s_leaf, uv, layer, u_camera.mip_bias);\n // Detail bump neighbors (sampled before discard: implicit-derivative samples must stay\n // in uniform control flow). Alpha-weighted luminance = height, so leaf clusters bulge\n // and gaps recess. Two octaves: fine 2-texel leaf sparkle + coarse 8-texel cluster lumps\n // (the coarse one is what survives mips and actually reads at gameplay distance).\n let texel = 2.0 / vec2<f32>(textureDimensions(t_leaf));\n let lum_w = vec3<f32>(0.299, 0.587, 0.114);\n let tex_x = textureSampleBias(t_leaf, s_leaf, uv + vec2<f32>(texel.x, 0.0), layer, u_camera.mip_bias);\n let tex_y = textureSampleBias(t_leaf, s_leaf, uv + vec2<f32>(0.0, texel.y), layer, u_camera.mip_bias);\n let tex_x2 = textureSampleBias(t_leaf, s_leaf, uv + vec2<f32>(texel.x * 4.0, 0.0), layer, u_camera.mip_bias);\n let tex_y2 = textureSampleBias(t_leaf, s_leaf, uv + vec2<f32>(0.0, texel.y * 4.0), layer, u_camera.mip_bias);\n\n // Edge-on card fade (standard AAA impostor/card treatment): a quad seen nearly parallel\n // to the view ray covers a sliver of pixels and its texels smear into long anisotropic\n // streaks. Erode the cutout as the GEOMETRIC facing (screen-space derivative normal, so\n // wind bend is included) approaches edge-on; each quad of the X-cross fades on its own,\n // so the face-on partner quad keeps the cluster present and no holes open up.\n let n_geo = cross(dpdx(world_pos), dpdy(world_pos));\n let to_cam = u_camera.camera_position - world_pos;\n let facing = abs(dot(n_geo, to_cam)) / max(length(n_geo) * length(to_cam), 1e-8);\n let edge = smoothstep(0.06, 0.28, facing);\n if (tex.a * edge < 0.5) { discard; }\n\n var out: GBufferOutput;\n // No backface flip (AAA foliage practice): cards keep their outward shell normal from both\n // sides; wrap diffuse + transmission in main_pass handle backlighting. Flipping made\n // back-viewed cards point into the canopy -> dark patches along the crown silhouette.\n // Per-pixel proxy: shade as if the pixel sat on its lobe's shell ELLIPSOID (gradient =\n // offset / radii^2; leaf_clump_pct picks crown vs per-lobe shell CPU-side); blend weight\n // w comes from leaf_shell_pct (0 = flat card facing, 100 = pure ellipsoid). This is the\n // SpeedTree-style smooth-crown normal, continuous across card seams.\n let sh = u_centers[cidx];\n // radii.w bitfield: curl x50 (byte 2, used in VS) | leaf_detail_pct x50 | leaf_ao_pct x100.\n let sw = u32(sh.radii.w + 0.5);\n let detail_s = f32((sw >> 8u) & 255u) * 0.02;\n let ao_s = f32(sw & 255u) * 0.01;\n let inv_r = 1.0 / max(sh.radii.xyz, vec3<f32>(0.05));\n let q = (world_pos - sh.center_k.xyz) * inv_r;\n let proxy = normalize(q * inv_r + vec3<f32>(0.0, 1e-5, 0.0));\n let n_card = normalize(world_normal);\n let n_smooth = normalize(mix(n_card, proxy, sh.center_k.w));\n // Leafy sparkle: bump the smooth proxy with the frond texture's own luminance gradients\n // (self-derived detail normal - no authored normal map needed). Keeps the rounded volume\n // from reading as a plastic balloon. Strength = leaf_detail_pct (0 = off, 100 = default).\n let h0 = dot(tex.rgb, lum_w) * tex.a;\n let hx = dot(tex_x.rgb, lum_w) * tex_x.a;\n let hy = dot(tex_y.rgb, lum_w) * tex_y.a;\n let hx2 = dot(tex_x2.rgb, lum_w) * tex_x2.a;\n let hy2 = dot(tex_y2.rgb, lum_w) * tex_y2.a;\n let b_w = cross(n_card, tangent_w);\n let grad = tangent_w * ((h0 - hx) + (h0 - hx2) * 0.7) + b_w * ((h0 - hy) + (h0 - hy2) * 0.7);\n let n = normalize(n_smooth + grad * (1.6 * detail_s));\n\n // Canopy AO, two signals. The baked per-card `ao` carries the SURFACE-VISIBLE gradient\n // (horizontal distance from the trunk axis + height, see tree_skeleton bake) - outside\n // views only ever see shell-surface pixels, so trunk-ward darkening must live in the\n // bake, not in shell depth. The per-pixel ellipsoid depth (|q| = 1 at the shell, -> 0\n // at the lobe core) then deepens gaps and inside-canopy views; smoothstep keeps it\n // near-flat over the visible outer shell so it can't wash the bake out.\n // leaf_ao_pct scales the total (0 = off, 200 = doubled).\n let shell_depth = clamp(length(q), 0.0, 1.0);\n let ao_eff = ao * mix(0.25, 1.0, smoothstep(0.35, 1.0, shell_depth));\n let ao_mul = max(1.0 - (1.0 - mix(0.62, 1.0, ao_eff)) * ao_s, 0.0);\n let occ = clamp(1.0 - (1.0 - mix(0.65, 1.0, ao_eff)) * ao_s, 0.0, 1.0);\n // Frond texture albedo with per-lobe tone variation (seed is lobe-coherent) + canopy AO.\n out.base_color = vec4<f32>(tex.rgb * (0.82 + 0.34 * seed) * ao_mul, 1.0);\n out.normal = vec4<f32>(n, 0.5); // a = 0.5: foliage class (wrap diffuse + transmission in main_pass)\n // High roughness: grazing-angle Fresnel sky sheen otherwise bleaches steep cards white.\n out.orm = vec4<f32>(occ, 0.92, 0.0, 0.0);\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n return out;\n}\n"},{"label":"leaf_impostor","code":"// leaf_impostor.wgsl\n// Far-distance whole-tree impostor: one billboard quad per tree sampling a hemi-octahedral\n// IMP_GRID x IMP_GRID atlas of baked views (branch mesh + leaf cards, model space). The three\n// frames whose grid triangle encloses the view direction are blended barycentrically, so both\n// yaw orbits and aerial pitches transition without popping. Fades in over the leaf-card\n// thinning band (where the real branch mesh culls) and out at the far range.\n// Shading comes from a second baked atlas (UE octahedral-impostor style): model-space shell\n// normals + pre-strength AO field, the exact inputs leaf_gbuffer uses on near cards - rotated\n// by the tree yaw into world so the billboard relights under the same sun instead of looking\n// flat. GRID must match IMP_GRID in tree_leaf_pass.rs; hemi_oct/basis math mirrors the Rust\n// bake (hemi_oct_decode / imp_view_basis).\n\nconst GRID: f32 = 4.0;\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n // [render dir x, render dir z, strength 0..1, gustiness 0..1]\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(1) @binding(0) var t_atlas: texture_2d_array<f32>;\n@group(1) @binding(1) var s_atlas: sampler;\n// Normal+AO atlas: rgb = model-space shell normal (n*0.5+0.5), a = pre-strength AO field.\n@group(1) @binding(3) var t_atlas_n: texture_2d_array<f32>;\n\nstruct ImpostorParams {\n fade_in_start_m: f32, // meter fade-in band: fallback while handoff_screen_size == 0\n fade_in_end_m: f32,\n fade_out_start_m: f32,\n fade_out_end_m: f32,\n // Mesh->impostor handoff threshold (sphere screen size; matches the static-LOD classify\n // cut). >0: fade in over a screen-size band bracketing it instead of the meter band.\n handoff_screen_size: f32,\n _pad_a: f32,\n _pad_b: f32,\n _pad_c: f32,\n sun_dir: vec3<f32>, // world space, pointing TOWARD the sun (updated per frame)\n _pad0: f32,\n};\n@group(1) @binding(2) var<uniform> u_params: ImpostorParams;\n\n// Primary fade-in: the per-tree card fade end (yaw_seed.y integer part, meters) - the\n// billboard must be opaque exactly when its tree's cards die, whatever the tree size or\n// FOV. Ramp starts at this ratio of that distance (mirrors the old 90->140m band).\nconst IMP_FADE_IN_START_RATIO: f32 = 0.65;\n// Screen-size safety net for trees whose mesh cuts BEFORE their cards die (small trees;\n// frond species, which have no cards): fade band relative to the handoff threshold. The\n// radius estimate below deliberately UNDERestimates the classify bounds sphere (max extent,\n// not box diagonal) so this net can only arrive early (overlap) - never late (bare hole).\nconst HANDOFF_FADE_START_F: f32 = 1.35;\nconst HANDOFF_FADE_END_F: f32 = 1.10;\n\nstruct VertexIn {\n // Card mesh (Vertex layout)\n @location(0) position: vec3<f32>, // x in [-0.5,0.5], y in [0,1]\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n // Instance (GrassInstance layout): pos_scale.xyz = tree center (world m), w = tree size m\n @location(4) pos_scale: vec4<f32>,\n @location(5) yaw_seed: vec2<f32>, // x = tree yaw (rad), y = card fade end (int part, m) + seed\n // x = ellipsoid extents as unit fractions (10+10 bit), y = bitfield\n // layer(5) | leaf_ao x100 (8) | far_shade x100 (8); both exact ints (pack_imp_meta).\n @location(6) packed: vec2<f32>,\n};\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) seed: f32,\n @location(2) @interpolate(flat) layer: u32,\n @location(3) @interpolate(flat) cells: vec3<u32>,\n @location(4) weights: vec3<f32>,\n @location(5) fade: f32,\n // Tree yaw (cos, sin) to rotate baked model-space normals into world + AO strength\n // (leaf_ao_pct) + far-shade brightness (leaf_far_shade_pct).\n @location(6) yaw_ao: vec4<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(7) cur_clip: vec4<f32>,\n @location(8) prev_clip: vec4<f32>,\n};\n\n// Mirrors hemi_oct_encode in tree_leaf_pass.rs (upper hemisphere, model space Y up).\nfn hemi_oct_encode(d: vec3<f32>) -> vec2<f32> {\n let sum = abs(d.x) + max(d.y, 0.0) + abs(d.z);\n let px = d.x / sum;\n let pz = d.z / sum;\n return vec2<f32>(px + pz, px - pz);\n}\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n let center = input.pos_scale.xyz;\n let size = input.pos_scale.w;\n out.seed = input.yaw_seed.y;\n // (`meta` is reserved in WGSL.)\n let imp_meta = u32(input.packed.y + 0.5);\n out.layer = imp_meta & 31u;\n\n let to_cam = u_camera.camera_position - center;\n // 3D distance: matches the branch mesh's static-LOD cutoff metric so aerial views\n // never fall in a mesh-culled-but-impostor-not-faded hole.\n let d = length(to_cam);\n\n let pe = u32(input.packed.x + 0.5);\n let ell = vec2<f32>(f32(pe >> 10u), f32(pe & 1023u)) / 1023.0;\n\n // Integer part = this tree's card fade end (meters, size+FOV scaled); fract = seed.\n // (Hue in fs uses fract(seed * 17.0), unaffected by the integer part.)\n let card_fade_end = floor(input.yaw_seed.y);\n\n // Fade-in = whichever near representation dies first needs covering (alpha dither in\n // fs): meters band synced to the cards, screen-size band synced to the mesh cut.\n var fade_in = 0.0;\n if (card_fade_end > 0.0) {\n let start_m = card_fade_end * IMP_FADE_IN_START_RATIO;\n fade_in = clamp((d - start_m) / max(card_fade_end - start_m, 1e-3), 0.0, 1.0);\n }\n if (u_params.handoff_screen_size > 0.0) {\n let ps = max(0.5 * u_camera.proj[0][0], 0.5 * u_camera.proj[1][1]);\n let r_est = 0.5 * size * max(ell.x, ell.y); // underestimate: see HANDOFF_FADE_*\n let ss = 2.0 * ps * r_est / max(d, 1.0);\n let start_ss = u_params.handoff_screen_size * HANDOFF_FADE_START_F;\n let end_ss = u_params.handoff_screen_size * HANDOFF_FADE_END_F;\n fade_in = max(fade_in, clamp((start_ss - ss) / max(start_ss - end_ss, 1e-6), 0.0, 1.0));\n } else if (card_fade_end <= 0.0) {\n // No handoff threshold and no cards (frond species pre-ready): legacy meter band.\n fade_in = clamp((d - u_params.fade_in_start_m) / max(u_params.fade_in_end_m - u_params.fade_in_start_m, 1e-3), 0.0, 1.0);\n }\n if (fade_in <= 0.0 || d > u_params.fade_out_end_m) {\n out.clip_position = vec4<f32>(2.0, 2.0, 2.0, 1.0); // outside NDC -> clipped\n out.fade = 0.0;\n return out;\n }\n let fade = min(\n fade_in,\n clamp((u_params.fade_out_end_m - d) / max(u_params.fade_out_end_m - u_params.fade_out_start_m, 1e-3), 0.0, 1.0),\n );\n out.fade = fade;\n\n // World view dir -> model space (undo the tree's yaw), clamped to the baked hemisphere.\n let yaw = input.yaw_seed.x;\n let cy = cos(yaw);\n let sy = sin(yaw);\n let dv_w = normalize(to_cam);\n var dv = vec3<f32>(cy * dv_w.x - sy * dv_w.z, dv_w.y, cy * dv_w.z + sy * dv_w.x);\n dv = normalize(vec3<f32>(dv.x, max(dv.y, 0.0), dv.z));\n\n // Enclosing grid triangle + barycentric weights (frames live at cell centers).\n let e01 = hemi_oct_encode(dv) * 0.5 + vec2<f32>(0.5);\n let g = clamp(e01 * GRID - 0.5, vec2<f32>(0.0), vec2<f32>(GRID - 1.0));\n let base = min(floor(g), vec2<f32>(GRID - 2.0));\n let f = g - base;\n let bx = u32(base.x);\n let by = u32(base.y);\n let gi = u32(GRID);\n let a = by * gi + bx;\n if (f.x + f.y <= 1.0) {\n out.cells = vec3<u32>(a, a + 1u, a + gi);\n out.weights = vec3<f32>(1.0 - f.x - f.y, f.x, f.y);\n } else {\n out.cells = vec3<u32>(a + gi + 1u, a + gi, a + 1u);\n out.weights = vec3<f32>(f.x + f.y - 1.0, 1.0 - f.x, 1.0 - f.y);\n }\n\n // Billboard basis = the bake camera's right/up for this view dir (model space, mirrors\n // imp_view_basis), rotated back to world by the tree yaw. Keeps quad screen axes aligned\n // with the baked frames at every pitch, including straight down.\n let up_hint = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), dv.y > 0.98);\n let right_m = normalize(cross(up_hint, dv));\n let up_m = cross(dv, right_m);\n let right_w = vec3<f32>(cy * right_m.x + sy * right_m.z, right_m.y, cy * right_m.z - sy * right_m.x);\n let up_w = vec3<f32>(cy * up_m.x + sy * up_m.z, up_m.y, cy * up_m.z - sy * up_m.x);\n var world_pos = center + right_w * (input.position.x * size)\n + up_w * ((input.position.y - 0.5) * size);\n // Coherent gust drift (same wave as gbuffer tree_wind_offset / leaf cards) so the far\n // canopy keeps moving with the forest; whole-quad shift only, no flutter at this distance.\n let strength = u_camera.wind.z;\n let wdir = vec3<f32>(u_camera.wind.x, 0.0, u_camera.wind.y);\n let tree_phase = dot(center, wdir) * 0.15;\n let gust = 0.5 + 0.5 * sin(u_camera.time_seconds * 0.9 - tree_phase) * u_camera.wind.w;\n world_pos += wdir * (strength * (0.10 + 0.22 * gust)) * input.position.y;\n out.clip_position = u_camera.view_proj * vec4<f32>(world_pos, 1.0);\n // Bake renders +up at texture top -> flip v.\n out.uv = vec2<f32>(input.position.x + 0.5, 1.0 - input.position.y);\n out.yaw_ao = vec4<f32>(cy, sy, f32((imp_meta >> 5u) & 255u) * 0.01, f32(imp_meta >> 13u) * 0.01);\n out.cur_clip = u_camera.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera.prev_unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>,\n @location(1) normal: vec4<f32>,\n @location(2) orm: vec4<f32>,\n @location(3) velocity: vec2<f32>,\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nfn dither4(p: vec2<f32>) -> f32 {\n // 4x4 ordered Bayer, [0,1)\n let x = u32(p.x) % 4u;\n let y = u32(p.y) % 4u;\n let idx = y * 4u + x;\n var bayer = array<f32, 16>(\n 0.0, 8.0, 2.0, 10.0,\n 12.0, 4.0, 14.0, 6.0,\n 3.0, 11.0, 1.0, 9.0,\n 15.0, 7.0, 13.0, 5.0,\n );\n return bayer[idx] / 16.0;\n}\n\nfn sample_cell(cell: u32, uv: vec2<f32>, layer: u32) -> vec4<f32> {\n let gi = u32(GRID);\n let cell_xy = vec2<f32>(f32(cell % gi), f32(cell / gi));\n return textureSample(t_atlas, s_atlas, (cell_xy + uv) / GRID, layer);\n}\n\nfn sample_cell_n(cell: u32, uv: vec2<f32>, layer: u32) -> vec4<f32> {\n let gi = u32(GRID);\n let cell_xy = vec2<f32>(f32(cell % gi), f32(cell / gi));\n return textureSample(t_atlas_n, s_atlas, (cell_xy + uv) / GRID, layer);\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) seed: f32,\n @location(2) @interpolate(flat) layer: u32,\n @location(3) @interpolate(flat) cells: vec3<u32>,\n @location(4) weights: vec3<f32>,\n @location(5) fade: f32,\n @location(6) yaw_ao: vec4<f32>,\n @location(7) cur_clip: vec4<f32>,\n @location(8) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n // Alpha-weighted 3-frame blend: coverage from the blended alpha, color premultiplied so\n // frames with no coverage at this texel don't darken the result.\n let t0 = sample_cell(cells.x, uv, layer);\n let t1 = sample_cell(cells.y, uv, layer);\n let t2 = sample_cell(cells.z, uv, layer);\n let cov = t0.a * weights.x + t1.a * weights.y + t2.a * weights.z;\n // Lower cutoff than the card shader: small atlas mips erode alpha coverage.\n if (cov < 0.35) { discard; }\n if (fade < dither4(clip_position.xy)) { discard; }\n let rgb = (t0.rgb * t0.a * weights.x + t1.rgb * t1.a * weights.y + t2.rgb * t2.a * weights.z) / cov;\n\n // Baked shading normal + AO, blended across the same 3 frames with the same alpha\n // weighting. The normal is model space: rotate by the tree yaw back into world.\n let n0 = sample_cell_n(cells.x, uv, layer);\n let n1 = sample_cell_n(cells.y, uv, layer);\n let n2 = sample_cell_n(cells.z, uv, layer);\n let nm = (n0.rgb * t0.a * weights.x + n1.rgb * t1.a * weights.y + n2.rgb * t2.a * weights.z) / cov;\n let n_model = normalize(nm * 2.0 - vec3<f32>(1.0));\n let cy = yaw_ao.x;\n let sy = yaw_ao.y;\n let n = vec3<f32>(cy * n_model.x + sy * n_model.z, n_model.y, cy * n_model.z - sy * n_model.x);\n // AO field is coverage-premultiplied in the atlas (uncovered texels clear to 0, mips\n // average ao x coverage): dividing by the blended coverage recovers the true mean AO,\n // so silhouette/far mips can't wash the darkening toward \"unoccluded bright\".\n let ao_field = (n0.a * weights.x + n1.a * weights.y + n2.a * weights.z) / cov;\n\n var out: GBufferOutput;\n let hue = fract(seed * 17.0);\n let variation = 0.9 + 0.2 * hue; // per-instance tone so a stand of one species isn't uniform\n // A billboard-range tree lost two darkeners the near tree still has: cast leaf shadows\n // (the cascades end long before this distance) and lighting contrast (the deferred\n // foliage path wrap-lights very softly, and mip-averaged normals flatten). Compensate\n // with three explicit terms, strongest first:\n // 1. SUN FORM SHADING: re-shade the crown with the baked shell normal against the real\n // sun dir - a hard lit-side/shade-side gradient the wrap lighting won't provide.\n let ndl = clamp(dot(n, normalize(u_params.sun_dir)) * 0.5 + 0.5, 0.0, 1.0);\n let form = mix(0.35, 1.05, ndl * ndl);\n // 2. Baked AO field at amplified strength (x2.5: default leaf_ao_pct 40 acts like 100)\n // with deeper floors than the near cards (0.5 vs 0.62).\n let ao_s = min(yaw_ao.z * 2.5, 2.0);\n let ao_mul = max(1.0 - (1.0 - mix(0.5, 1.0, ao_field)) * ao_s, 0.0);\n let occ = clamp(1.0 - (1.0 - mix(0.55, 1.0, ao_field)) * ao_s, 0.0, 1.0);\n // 3. leaf_far_shade_pct: flat artist-tunable brightness (adjuster slider; 100 = neutral).\n let far_s = yaw_ao.w;\n out.base_color = vec4<f32>(rgb * variation * ao_mul * form * far_s, 1.0);\n out.normal = vec4<f32>(n, 0.5); // a = 0.5: foliage class\n out.orm = vec4<f32>(occ * min(far_s, 1.0), 0.92, 0.0, 0.0);\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n return out;\n}\n"},{"label":"shaders/ocean_ripples.wgsl","code":"// ocean_ripples.wgsl \u2014 interactive wake ripple grid (ocean_ripples.rs).\n// One damped-Verlet wave-equation step per rendered frame over a camera-following,\n// WORLD-ANCHORED toroidal grid: uv = world_xz / extent mod 1, so the grid never scrolls \u2014\n// the window just re-labels which world cell each texel represents as the camera moves.\n// Texels that wrapped in from the far side this frame (\"fresh\") restart from zero, and the\n// outer ring is clamped to zero so seam neighbors (physically extent_m away) never leak\n// into the laplacian. State texel: r = height (m), g = previous height (exact prev-frame\n// surface for the MV pass), b = wake foam accumulation, a = unused.\n// Injectors press the surface down; Verlet radiates the ring outward naturally.\n\nstruct RippleParams {\n center: vec2<f32>, // current grid window center, render-world XZ meters\n prev_center: vec2<f32>, // last frame's window center (fresh-texel detection)\n inv_extent: f32,\n extent_m: f32,\n k_lap: f32, // c^2 * dt^2 / dx^2 (CFL: keep < ~0.5)\n damp: f32, // per-step velocity retention, 0..1\n injector_count: u32,\n foam_decay: f32, // per-step foam retention, 0..1\n foam_gain: f32,\n // Per-step height retention. The wave equation CONSERVES displaced volume (damp only\n // bleeds velocity), so without this the every-frame injection pumps a permanent,\n // ever-growing depression that spreads at c (seen as a huge non-decaying white disc).\n height_decay: f32,\n // xy = world XZ m, z = radius m, w = impulse height m (already speed/size scaled)\n injectors: array<vec4<f32>, 64>,\n splash_count: u32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n // One-shot entry/exit splashes (surface crossings): same layout as injectors, but a\n // single frame of impulse with a heavy direct foam deposit.\n splashes: array<vec4<f32>, 16>,\n contact_count: u32,\n _pad3: f32,\n _pad4: f32,\n _pad5: f32,\n // Contact collars: xy = world XZ m, z = footprint rim radius m, w = strength 0..1.\n contacts: array<vec4<f32>, 32>,\n // Dynamic wetness: sea level, per-step drying, then the Gerstner wave set\n // (8 x dir.xy/amp/k + 8 phases packed 4-per-vec4).\n sea_level_y: f32,\n wet_dry_per_step: f32,\n _pad6: f32,\n _pad7: f32,\n shore_waves: array<vec4<f32>, 10>,\n}\n@group(0) @binding(0) var<uniform> rp: RippleParams;\n@group(0) @binding(1) var ripple_src: texture_2d<f32>;\n@group(0) @binding(2) var ripple_dst: texture_storage_2d<rgba16float, write>;\n\nconst RIPPLE_N: i32 = 512;\n\n// World position a texel currently represents: unwrap its 0..1 uv to the period landing\n// inside the window centered on `center`.\nfn ripple_world_of(texel: vec2<i32>) -> vec2<f32> {\n let u = (vec2<f32>(texel) + vec2<f32>(0.5)) / f32(RIPPLE_N);\n let cu = rp.center * rp.inv_extent;\n return (u + round(cu - u)) * rp.extent_m;\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let p = vec2<i32>(gid.xy);\n if (p.x >= RIPPLE_N || p.y >= RIPPLE_N) {\n return;\n }\n let w = ripple_world_of(p);\n\n // Zero boundary ring: seam neighbors are not physical neighbors.\n let dc = abs(w - rp.center) * rp.inv_extent;\n if (max(dc.x, dc.y) > 0.49) {\n textureStore(ripple_dst, p, vec4<f32>(0.0));\n return;\n }\n\n // Fresh texel: its world cell was outside (or at the edge of) last frame's window.\n let dp = abs(w - rp.prev_center) * rp.inv_extent;\n var cur = vec4<f32>(0.0);\n var lap = 0.0;\n if (max(dp.x, dp.y) <= 0.48) {\n cur = textureLoad(ripple_src, p, 0);\n let xm = textureLoad(ripple_src, vec2<i32>((p.x + RIPPLE_N - 1) % RIPPLE_N, p.y), 0).r;\n let xp = textureLoad(ripple_src, vec2<i32>((p.x + 1) % RIPPLE_N, p.y), 0).r;\n let ym = textureLoad(ripple_src, vec2<i32>(p.x, (p.y + RIPPLE_N - 1) % RIPPLE_N), 0).r;\n let yp = textureLoad(ripple_src, vec2<i32>(p.x, (p.y + 1) % RIPPLE_N), 0).r;\n lap = xm + xp + ym + yp - 4.0 * cur.r;\n }\n\n var next = cur.r + (cur.r - cur.g) * rp.damp + rp.k_lap * lap;\n\n var energy = 0.0;\n for (var i = 0u; i < rp.injector_count; i++) {\n let inj = rp.injectors[i];\n let d = distance(w, inj.xy);\n if (d < inj.z) {\n let bump = 1.0 - smoothstep(0.0, inj.z, d);\n next -= inj.w * bump;\n energy += inj.w * bump;\n }\n }\n // Splashes: one frame of impulse, so the foam deposit is much heavier per unit of\n // impulse than the continuous wake churn (which integrates over many frames).\n var splash_foam = 0.0;\n for (var i = 0u; i < rp.splash_count; i++) {\n let s = rp.splashes[i];\n let d = distance(w, s.xy);\n if (d < s.z) {\n let bump = 1.0 - smoothstep(0.0, s.z, d);\n next -= s.w * bump;\n splash_foam += s.w * bump * 3.0;\n }\n }\n // Contact collars: steady FOAM-ONLY annulus deposit at each waterline footprint rim.\n // A constant per-frame deposit reaches equilibrium against foam_decay at\n // deposit * foam_gain / (1 - foam_decay) - 0.0024 lands around 0.65 raw foam.\n var contact_foam = 0.0;\n for (var i = 0u; i < rp.contact_count; i++) {\n let c = rp.contacts[i];\n // Positive radius: thin rim annulus (object waterline collars). NEGATIVE radius:\n // filled disc of |r| (stream/fall mouth churn patches - a 0.35m ring is ~1 texel\n // and bilinear-filters into invisibility at patch scale).\n let r = abs(c.z);\n var band = 1.0 - smoothstep(0.0, 0.35, abs(distance(w, c.xy) - r));\n if (c.z < 0.0) { band = 1.0 - smoothstep(r * 0.55, r, distance(w, c.xy)); }\n contact_foam += 0.0024 * band * c.w;\n }\n\n next = clamp(next * rp.height_decay, -0.6, 0.6);\n\n // Foam comes from the injection churn itself; propagating rings only add a whisper\n // (a full |dh| term painted the whole disturbed disc solid white).\n let foam = clamp(\n cur.b * rp.foam_decay\n + (energy + splash_foam + contact_foam + abs(next - cur.r) * 0.06) * rp.foam_gain,\n 0.0,\n 1.0,\n );\n\n // Dynamic wetness (alpha): the highest ABSOLUTE water height recently seen at this\n // world cell, decaying toward the current level - ground above the falling water\n // stays wet and dries over seconds (the main pass reads this to draw wet ground).\n var gerst = 0.0;\n for (var i = 0u; i < 8u; i++) {\n let gw = rp.shore_waves[i];\n let ph = rp.shore_waves[8u + (i / 4u)][i % 4u];\n gerst += gw.z * sin(gw.w * dot(gw.xy, w) - ph);\n }\n let water_y = rp.sea_level_y + gerst + next;\n let wet = max(water_y, cur.a - rp.wet_dry_per_step);\n\n textureStore(ripple_dst, p, vec4<f32>(next, cur.r, foam, wet));\n}\n"},{"label":"shaders/downsample_linear_depth.wgsl","code":"// downsample_linear_depth.wgsl\n@group(0) @binding(0) var outDepth : texture_storage_2d<r32float, write>;\n@group(0) @binding(1) var inDepth : texture_2d<f32>;\n@group(0) @binding(2) var inSampler : sampler;\n\n@compute @workgroup_size(8, 8)\nfn main(@builtin(global_invocation_id) gid : vec3<u32>) {\n let halfSize = textureDimensions(outDepth);\n if (gid.x >= halfSize.x || gid.y >= halfSize.y) {\n return;\n }\n let srcSize = textureDimensions(inDepth);\n let baseX = f32(gid.x * 2u);\n let baseY = f32(gid.y * 2u);\n\n // Input is the gbuffer NDC depth (sky = 1.0); consumers (ssao.wgsl) linearize to meters.\n var sumDepth = 0.0;\n for (var j = 0u; j < 2u; j++) {\n for (var i = 0u; i < 2u; i++) {\n let fx = (baseX + f32(i) + 0.5) / f32(srcSize.x);\n let fy = (baseY + f32(j) + 0.5) / f32(srcSize.y);\n sumDepth = sumDepth + textureSampleLevel(inDepth, inSampler, vec2<f32>(fx, fy), 0.0).r;\n }\n }\n\n let outVal = sumDepth * 0.25;\n textureStore(outDepth, vec2<i32>(gid.xy), vec4<f32>(outVal, 0.0, 0.0, 0.0));\n}\n"},{"label":"shaders/ssao.wgsl","code":"// ssao.wgsl\n@group(0) @binding(0) var normalTex: texture_2d<f32>;\n@group(0) @binding(1) var linearDepthTex: texture_2d<f32>;\n@group(0) @binding(2) var noiseTex: texture_2d<f32>;\n@group(0) @binding(3) var linearSampler: sampler;\n@group(0) @binding(4) var depthSampler: sampler;\n@group(0) @binding(5) var noiseSampler: sampler;\nstruct SsaoSettings {\n intensity01: f32,\n distance_m: f32,\n _pad0: f32,\n _pad1: f32,\n};\n@group(0) @binding(6) var<uniform> u_ssao: SsaoSettings;\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SAMPLES: u32 = 24; // number of random hemisphere samples\nconst BIAS: f32 = 0.035; // 0.025 // small bias to reduce self-occlusion\n\nconst NOISE_SIZE: f32 = 32.0;\n\nconst HEMISPHERE_SAMPLES: array<vec3<f32>, 32> = array<vec3<f32>, 32>(\n vec3<f32>(0.0, 0.1, 1.0),\n vec3<f32>(0.2, 0.4, 0.9),\n vec3<f32>(-0.3, 0.2, 0.8),\n vec3<f32>(0.4, -0.1, 0.7),\n vec3<f32>(-0.2, -0.5, 0.6),\n vec3<f32>(0.3, 0.5, 0.7),\n vec3<f32>(-0.4, 0.3, 0.8),\n vec3<f32>(0.5, 0.1, 0.6),\n\n vec3<f32>(0.2, 0.3, 0.7),\n vec3<f32>(-0.2, 0.3, 0.9),\n vec3<f32>(-0.3, -0.4, 0.7),\n vec3<f32>(0.25, 0.1, 0.95),\n vec3<f32>(-0.4, 0.0, 0.8),\n vec3<f32>(0.4, -0.3, 0.7),\n vec3<f32>(0.05, 0.6, 0.7),\n vec3<f32>(-0.1, -0.2, 0.7),\n\n vec3<f32>( 0.3, -0.1, 0.95),\n vec3<f32>( 0.1, 0.6, 0.7),\n vec3<f32>( 0.6, 0.2, 0.65),\n vec3<f32>(-0.2, 0.55, 0.75),\n vec3<f32>( 0.2, -0.3, 0.9),\n vec3<f32>(-0.5, 0.3, 0.7),\n vec3<f32>( 0.45, 0.4, 0.65),\n vec3<f32>(-0.3, 0.1, 0.9),\n\n vec3<f32>( 0.2, 0.05, 0.95),\n vec3<f32>(-0.4, -0.3, 0.7),\n vec3<f32>( 0.0, 0.3, 0.8),\n vec3<f32>( 0.55, 0.2, 0.65),\n vec3<f32>(-0.6, 0.1, 0.7),\n vec3<f32>( 0.25, -0.4, 0.75),\n vec3<f32>( 0.1, 0.4, 0.8),\n vec3<f32>(-0.1, 0.2, 0.95),\n);\n\nfn get_view_pos_from_view_z_forward(uv: vec2<f32>, view_z_forward: f32) -> vec3<f32> {\n // camera is at looking down -Z\n let z_view = -view_z_forward;\n let x_ndc = uv.x * 2.0 - 1.0;\n let y_ndc = (1.0 - uv.y) * 2.0 - 1.0; // NDC (0,0) is bottom left, but frag_coord (0,0) is top left\n\n // In a typical perspective_rh matrix:\n // proj[0][0] = 1 / tan(fov_x/2)\n // proj[1][1] = 1 / tan(fov_y/2)\n let x_view = x_ndc / u_camera.proj[0][0] * z_view;\n let y_view = y_ndc / u_camera.proj[1][1] * z_view;\n\n return vec3<f32>(x_view, y_view, z_view);\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // full-screen triangle (3 vertices)\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {\n let screen_dims = vec2<f32>(textureDimensions(normalTex, 0));\n let ssao_size = screen_dims / 2.0; // half resolution\n let uv = fragCoord.xy / ssao_size;\n\n let world_normal = normalize(textureSample(normalTex, linearSampler, uv).xyz);\n // linearDepthTex holds downsampled gbuffer NDC depth; linearize to view meters\n // (sky = 1.0 lands on ~far and is caught by the background check below).\n let ndc_depth = textureSampleLevel(linearDepthTex, depthSampler, uv, 0.0).r;\n let linear_depth = (u_camera.near * u_camera.far)\n / max(u_camera.far - ndc_depth * (u_camera.far - u_camera.near), 1e-6);\n if ndc_depth >= 1.0 || linear_depth >= 9999.0 {\n return vec4<f32>(1.0); // background\n }\n let view_pos = get_view_pos_from_view_z_forward(uv, linear_depth);\n\n var noise_uv_1 = uv * ssao_size / NOISE_SIZE;\n var noise_uv_2 = noise_uv_1 * 0.38;\n let noise_vec_1 = textureSampleLevel(noiseTex, noiseSampler, noise_uv_1, 0.0).xy;\n let noise_vec_2 = textureSampleLevel(noiseTex, noiseSampler, noise_uv_2, 0.0).xy;\n let noise_vec = mix(noise_vec_1, noise_vec_2, 0.5); // 0..1\n\n // Make a TBN (tangent, bitangent, normal) basis \n // so we can rotate hemisphere samples in local space relative to the surface normal:\n let view_normal = -normalize((u_camera.view * vec4<f32>(world_normal, 0.0)).xyz); // TODO: view_normal is reversed?\n let absN = abs(view_normal);\n var up = vec3<f32>(0.0, 1.0, 0.0);\n if absN.x < absN.y && absN.x < absN.z {\n up = vec3<f32>(1.0, 0.0, 0.0);\n } else if absN.z < absN.x && absN.z < absN.y {\n up = vec3<f32>(0.0, 0.0, 1.0);\n }\n let U = normalize(cross(up, view_normal));\n let V = cross(view_normal, U);\n\n // Random angle to rotate around the normal\n let pi = 3.141592653589;\n let random_angle = (noise_vec.x * 2.0 - 1.0) * pi; // [pi..pi]\n let sinA = sin(random_angle);\n let cosA = cos(random_angle);\n let t_prime = cosA * U + sinA * V;\n let b_prime = -sinA * U + cosA * V;\n\n // let distance_frac = (linear_depth - u_camera.near) / (u_camera.far - u_camera.near);\n // let scaled_radius = mix(RADIUS, RADIUS * 4.0, clamp(distance_frac, 0.0, 1.0));\n let scaled_radius = max(u_ssao.distance_m, 0.001);\n\n // AO accumulation\n var occlusion = 0.0;\n // let i = 0u;\n for (var i = 0u; i < SAMPLES; i = i + 1u) {\n var sample_dir = normalize(HEMISPHERE_SAMPLES[i]);\n\n // rotate sample_dir by the TBN basis\n let rotated = sample_dir.x * t_prime + // T\n sample_dir.y * b_prime + // B\n sample_dir.z * view_normal; // N\n\n // let nDotR = dot(view_normal, rotated);\n // if (nDotR < 0.0) {\n // continue;\n // }\n // let angle_bias = BIAS / max(nDotR, 0.1);\n\n let iFrac = f32(i) / f32(SAMPLES);\n let hemi_scale = mix(0.1, 1.0, iFrac * iFrac);\n\n let rand_scale = 0.1 + 0.9 * fract(noise_vec.y + f32(i)*0.317);\n let final_scale = hemi_scale * rand_scale;\n\n // Move the sampling origin outwards by BIAS along the normal\n let sample_origin = view_pos + view_normal * BIAS; \n\n let sample_pos = sample_origin + rotated * (scaled_radius * final_scale);\n\n // project sample_pos -> ndc -> depth\n let sample_pos_h = u_camera.proj * vec4<f32>(sample_pos, 1.0);\n if sample_pos_h.w <= 0.0 {\n continue;\n }\n let sample_ndc_xy = sample_pos_h.xy / sample_pos_h.w;\n\n // convert ndc -> [0..1] uv\n var sample_uv = sample_ndc_xy * 0.5 + 0.5;\n sample_uv.x = 1.0 - sample_uv.x; // checked sample_uv matches uv\n sample_uv = clamp(sample_uv, vec2<f32>(0.0), vec2<f32>(1.0));\n\n // Compare the actual stored depth (NDC -> view meters, same mapping as above)\n let sample_ndc = textureSampleLevel(linearDepthTex, depthSampler, sample_uv, 0.0).r;\n let sample_linear_depth = (u_camera.near * u_camera.far)\n / max(u_camera.far - sample_ndc * (u_camera.far - u_camera.near), 1e-6);\n if sample_ndc >= 1.0 || sample_linear_depth > 9999.0 {\n continue; // background => no occlusion from that sample\n }\n let sample_view_pos = get_view_pos_from_view_z_forward(sample_uv, sample_linear_depth);\n\n // difference\n let depth_diff = sample_view_pos.z - view_pos.z; \n let dist_factor = smoothstep(0.0, scaled_radius, length(view_pos - sample_view_pos)); // smooth fade 0..1 over that radius\n let depth_factor = step(BIAS, depth_diff); // simple test for \u201cin front\u201d\n\n occlusion += (1.0 - dist_factor) * depth_factor;\n\n // let range_check_radius = 0.3;\n // let range_check = smoothstep(0.0, 1.0, range_check_radius / abs(depth_diff));\n\n // occlusion += depth_factor * range_check;\n // occlusion += (1.0 - dist_factor) * depth_factor * range_check;\n }\n\n var ao = 1.0 - occlusion / f32(SAMPLES);\n // Cap max occlusion at 80%; unoccluded surfaces must stay 1.0. (The old\n // clamp(ao, 0.0, 0.8) capped BRIGHTNESS instead, flat-darkening every\n // open surface by 20% before the intensity mix.)\n ao = clamp(ao, 0.2, 1.0);\n ao = mix(1.0, ao, clamp(u_ssao.intensity01, 0.0, 1.0));\n return vec4<f32>(ao, ao, ao, 1.0);\n}\n"},{"label":"shaders/ssao_blur.wgsl","code":"// ssao_blur.wgsl\nstruct SsaoBlurUniform {\n radius: f32,\n sigma: f32,\n is_horizontal: u32,\n _pad: f32,\n};\n@group(0) @binding(0) var<uniform> u_blur: SsaoBlurUniform;\n@group(0) @binding(1) var aoTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_2d<f32>;\n@group(0) @binding(3) var normalTex: texture_2d<f32>;\n@group(0) @binding(4) var linearSampler: sampler;\n@group(0) @binding(5) var depthSampler: sampler;\n\nconst KERNEL_SHIFT: f32 = 0.8; // 0.5\n\nconst KERNEL_SIZE: i32 = 7;\nconst GAUSS_WEIGHTS: array<f32, 7> = array<f32, 7>(0.07, 0.131, 0.191, 0.216, 0.191, 0.131, 0.07);\n// const KERNEL_SIZE: i32 = 5;\n// const GAUSS_WEIGHTS: array<f32, 5> = array<f32, 5>(0.06136, 0.24477, 0.38774, 0.24477, 0.06136);\n// const KERNEL_SIZE: i32 = 9;\n// const GAUSS_WEIGHTS: array<f32, 9> = array<f32, 9>(0.028, 0.066, 0.124, 0.18, 0.204, 0.18, 0.124, 0.066, 0.028);\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // full-screen triangle\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nstruct FragOut {\n @location(0) color: vec4<f32>,\n};\n\n@fragment\nfn fs_main(@builtin(position) position: vec4<f32>) -> FragOut {\n let dims = vec2<f32>(textureDimensions(aoTex));\n let uv = position.xy / dims;\n\n // center pixel's depth & normal\n let center_depth = textureSample(depthTex, depthSampler, uv).r;\n let center_normal = normalize(textureSample(normalTex, linearSampler, uv).xyz);\n\n var sum = 0.0;\n var wsum = 0.0;\n\n for (var i = 0; i < KERNEL_SIZE; i = i + 1) {\n let offset = f32(i - KERNEL_SIZE / 2) * KERNEL_SHIFT;\n let weight = GAUSS_WEIGHTS[i];\n\n var sample_uv = uv;\n if u_blur.is_horizontal == 1u {\n sample_uv += vec2<f32>(offset / dims.x, 0.0);\n } else {\n sample_uv += vec2<f32>(0.0, offset / dims.y);\n }\n\n let ao_val = textureSample(aoTex, linearSampler, sample_uv).r;\n\n // Depth for bilateral weight\n let sample_depth = textureSample(depthTex, depthSampler, sample_uv).r;\n let depth_diff = abs(sample_depth - center_depth);\n\n // Normal for bilateral weight\n // let sample_normal = normalize(textureSample(normalTex, linearSampler, sample_uv).xyz);\n // let normal_diff = max(0.0, 1.0 - dot(center_normal, sample_normal));\n\n // user constants\n let sigma_depth = 0.01 * center_depth;\n // let sigma_normal = 0.1;\n\n // let depth_weight = exp(- (depth_diff*depth_diff) / (2.0 * sigma_depth*sigma_depth)); \n // let normal_weight = exp(- (normal_diff*normal_diff) / (2.0 * sigma_normal*sigma_normal));\n\n let coeff = abs(depth_diff / sigma_depth);\n var depth_weight = exp(-coeff*coeff);\n // (Gaussian: w = exp[-(\u0394z^2)/(sigma^2)]. Using abs(\u0394z)/sigma squared here.)\n let depth_cutoff = center_depth * 0.01;\n if (abs(depth_diff) > depth_cutoff) {\n depth_weight = 0.0;\n }\n\n let total_weight = weight * depth_weight; // * normal_weight;\n\n sum += ao_val * total_weight;\n wsum += total_weight;\n }\n\n var blurred = sum / max(wsum, 1e-5);\n\n if u_blur.is_horizontal == 0u {\n blurred = pow(blurred, 1.5);\n if blurred < 0.25 {\n blurred = 0.15 * blurred / 0.25 + 0.1;\n }\n }\n\n return FragOut(vec4<f32>(blurred, blurred, blurred, 1.0));\n}\n"},{"label":"shaders/ssao_upsample.wgsl","code":"@group(0) @binding(0) var aoTexQuarter: texture_2d<f32>;\n@group(0) @binding(1) var depthTexFull: texture_2d<f32>;\n@group(0) @binding(2) var normalTexFull: texture_2d<f32>;\n@group(0) @binding(3) var linearSampler: sampler;\n@group(0) @binding(4) var depthSampler: sampler;\n\n// For a small 3\u00d73 or 4\u00d74 gather; here\u2019s a simple 3\u00d73 example:\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // Full-screen triangle\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {\n let uv = fragCoord.xy / vec2<f32>(textureDimensions(depthTexFull));\n\n // Center pixel data from full\u2010res buffers:\n let centerDepth = textureSample(depthTexFull, depthSampler, uv).r;\n let centerNormal = normalize(textureSample(normalTexFull, linearSampler, uv).xyz);\n\n var aoSum = 0.0;\n var weightSum = 0.0;\n\n // A small offset kernel around uvQuarter in quarter\u2010res space:\n // For instance, a 3\u00d73 gather in [-1..1].\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let offsetQ = vec2<f32>(f32(dx), f32(dy)) / vec2<f32>(textureDimensions(aoTexQuarter));\n let sampleUV = clamp(uv + offsetQ, vec2<f32>(0.0), vec2<f32>(1.0));\n \n let sampleAO = textureSample(aoTexQuarter, linearSampler, sampleUV).r;\n let sampleDepth = textureSample(depthTexFull, depthSampler, sampleUV).r;\n let sampleNormal = normalize(textureSample(normalTexFull, linearSampler, sampleUV).xyz);\n\n // Compare with center\u2019s depth & normal for \u201cedge awareness\u201d\n let depthDiff = abs(sampleDepth - centerDepth);\n let normalDiff = max(0.0, 1.0 - dot(centerNormal, sampleNormal));\n\n // You can tune these sigmas:\n let sigmaDepth = 0.02; \n let sigmaNormal = 0.1; \n\n let wDepth = exp(- (depthDiff * depthDiff) / (2.0 * sigmaDepth * sigmaDepth));\n let wNormal = exp(- (normalDiff * normalDiff) / (2.0 * sigmaNormal * sigmaNormal));\n let bilateralWeight = wDepth * wNormal;\n\n aoSum += sampleAO * bilateralWeight;\n weightSum += bilateralWeight;\n }\n }\n\n let ao = aoSum / max(weightSum, 1e-5);\n return vec4<f32>(ao, ao, ao, 1.0);\n}\n"},{"label":"shaders/motion_vectors.wgsl","code":"// motion_vectors.wgsl\n// Background (sky/far-plane) camera-motion velocity fill: runs after the gbuffer with depth\n// compare Equal at the clear value, so only pixels no opaque geometry touched get written \u2014\n// everything else keeps the velocity the gbuffer MRT wrote. Output rg = prev_uv - cur_uv (UV\n// units). The reprojection matrix is UNJITTERED on both frames so TAA jitter doesn't leak into\n// velocity, and composed in f64 on the CPU: an f32 inverse+forward round trip through world\n// space drifts with distance from the origin, which the temporal upscalers accumulate into\n// permanent blur (B34). Sky pixels reproject like everything else instead of returning zero:\n// DLSS ghosts and leaves block artifacts when the sky reports no motion during camera rotation\n// (B35), and the clip-space reprojection is exact at the far plane.\n\nstruct MotionVectorUniform {\n reproj: mat4x4<f32>, // current clip -> previous clip\n};\n@group(0) @binding(0) var<uniform> u_mv: MotionVectorUniform;\n@group(0) @binding(1) var depth_texture: texture_depth_2d;\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>( 3.0, 1.0),\n vec2<f32>(-1.0, 1.0)\n );\n // z = 1.0: the depth-Equal test passes only where depth still holds the clear value.\n return vec4<f32>(positions[vertex_index], 1.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec2<f32> {\n let dims = vec2<f32>(textureDimensions(depth_texture));\n let pixel = vec2<i32>(pos.xy);\n let depth = textureLoad(depth_texture, pixel, 0);\n let uv = pos.xy / dims;\n // UV -> NDC (flip y), reproject straight in clip space: (ndc, depth, 1) is the true clip\n // position up to the unknown w, which the homogeneous divide below cancels.\n let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n let prev_clip = u_mv.reproj * vec4<f32>(ndc, depth, 1.0);\n if (prev_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - uv;\n}\n"},{"label":"shaders/ssr_downsample.wgsl","code":"// ssr_downsample.wgsl\n@group(0) @binding(0) var normal_texture: texture_2d<f32>;\n@group(0) @binding(1) var orm_texture: texture_2d<f32>;\n@group(0) @binding(2) var depth_texture: texture_depth_2d;\n@group(0) @binding(3) var scene_color_texture: texture_2d<f32>;\nstruct DownsampleUniform {\n config: vec4<f32>,\n};\n@group(0) @binding(4) var<uniform> u_downsample: DownsampleUniform;\n\nstruct FsOut {\n @location(0) depth_value: vec4<f32>,\n @location(1) repr_depth_value: vec4<f32>,\n @location(2) normal_rough: vec4<f32>,\n @location(3) repr_scene_color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn clamp_px(px: vec2<i32>, dims: vec2<i32>) -> vec2<i32> {\n return clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n}\n\nfn div_ceil_i32(a: i32, b: i32) -> i32 {\n return (a + b - 1) / b;\n}\n\nfn pick_hiz_depth(best_depth: ptr<function, f32>, max_depth: ptr<function, f32>, best_px: ptr<function, vec2<i32>>, full_dims: vec2<i32>, candidate_px: vec2<i32>) {\n let px = clamp_px(candidate_px, full_dims);\n let depth = textureLoad(depth_texture, px, 0);\n // Max INCLUDES background (1.0): any cell touching sky gets max=1.0, which disables\n // the trace behind-skip there (conservative). A geometry-only max was tried and\n // caused severe banding: near-horizon cells behind-skipped past legitimate targets.\n *max_depth = max(*max_depth, depth);\n if depth < 1.0 {\n if (*best_depth < 0.0) || (*best_depth >= 1.0) || depth < *best_depth {\n *best_depth = depth;\n *best_px = px;\n }\n } else if *best_depth < 0.0 {\n *best_depth = depth;\n *best_px = px;\n }\n}\n\nfn target_dims(full_dims: vec2<i32>, full_res: bool) -> vec2<i32> {\n if full_res {\n return full_dims;\n }\n return vec2<i32>(\n max(1, i32(u_downsample.config.y + 0.5)),\n max(1, i32(u_downsample.config.z + 0.5))\n );\n}\n\nfn pick_hiz_depth_range(\n best_depth: ptr<function, f32>,\n max_depth: ptr<function, f32>,\n best_px: ptr<function, vec2<i32>>,\n full_dims: vec2<i32>,\n dst_dims: vec2<i32>,\n dst_px: vec2<i32>,\n) {\n let src0 = vec2<i32>(\n (dst_px.x * full_dims.x) / dst_dims.x,\n (dst_px.y * full_dims.y) / dst_dims.y\n );\n let src1 = vec2<i32>(\n div_ceil_i32((dst_px.x + 1) * full_dims.x, dst_dims.x),\n div_ceil_i32((dst_px.y + 1) * full_dims.y, dst_dims.y)\n );\n var y = src0.y;\n loop {\n if y >= src1.y {\n break;\n }\n var x = src0.x;\n loop {\n if x >= src1.x {\n break;\n }\n pick_hiz_depth(best_depth, max_depth, best_px, full_dims, vec2<i32>(x, y));\n x += 1;\n }\n y += 1;\n }\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FsOut {\n let target_px = vec2<i32>(frag_coord.xy);\n let full_dims = vec2<i32>(textureDimensions(normal_texture));\n let full_res = u_downsample.config.x > 0.5;\n let dst_dims = target_dims(full_dims, full_res);\n var hiz_depth = -1.0;\n var hiz_max_depth = 0.0;\n var hiz_px = clamp_px(target_px, full_dims);\n pick_hiz_depth_range(&hiz_depth, &hiz_max_depth, &hiz_px, full_dims, dst_dims, target_px);\n if hiz_depth < 0.0 {\n hiz_depth = 1.0;\n }\n\n if hiz_depth >= 1.0 {\n return FsOut(vec4<f32>(1.0, 1.0, 0.0, 0.0), vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0));\n }\n\n let repr_depth = hiz_depth;\n let repr_px = hiz_px;\n let world_normal = normalize(textureLoad(normal_texture, repr_px, 0).xyz);\n let roughness = textureLoad(orm_texture, repr_px, 0).g;\n let repr_scene_color = textureLoad(scene_color_texture, repr_px, 0);\n return FsOut(\n vec4<f32>(hiz_depth, hiz_max_depth, 0.0, 0.0),\n vec4<f32>(repr_depth, 0.0, 0.0, 0.0),\n vec4<f32>(world_normal, roughness),\n repr_scene_color\n );\n}\n"},{"label":"shaders/ssr_hiz.wgsl","code":"// ssr_hiz.wgsl\n// Dual Hi-Z reduction: R = min depth (ignoring background), G = max depth (including background).\n@group(0) @binding(0) var prev_hiz: texture_2d<f32>;\n\nstruct FsOut {\n @location(0) depth_value: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn include_depth(best: ptr<function, f32>, worst: ptr<function, f32>, px: vec2<i32>) {\n let d = textureLoad(prev_hiz, px, 0).rg;\n if d.r < 1.0 {\n *best = min(*best, d.r);\n }\n *worst = max(*worst, d.g);\n}\n\nfn reduce_depth(prev_dims: vec2<i32>, dst_px: vec2<i32>) -> vec2<f32> {\n let base_px = dst_px * 2;\n var best = 2.0;\n var worst = 0.0;\n include_depth(&best, &worst, base_px + vec2<i32>(0, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(0, 1));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 1));\n if (prev_dims.x & 1) != 0 {\n include_depth(&best, &worst, base_px + vec2<i32>(2, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(2, 1));\n }\n if (prev_dims.y & 1) != 0 {\n include_depth(&best, &worst, base_px + vec2<i32>(0, 2));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 2));\n }\n if ((prev_dims.x & 1) != 0 && (prev_dims.y & 1) != 0) {\n include_depth(&best, &worst, base_px + vec2<i32>(2, 2));\n }\n return vec2<f32>(select(1.0, best, best < 1.0), worst);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FsOut {\n let prev_dims = vec2<i32>(textureDimensions(prev_hiz));\n let dst_px = vec2<i32>(frag_coord.xy);\n return FsOut(vec4<f32>(reduce_depth(prev_dims, dst_px), 0.0, 0.0));\n}\n"},{"label":"shaders/ssr_trace.wgsl","code":"// ssr_trace.wgsl\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n\nstruct SsrSettingsUniform {\n trace_config: vec4<f32>,\n material_config: vec4<f32>,\n fade_config: vec4<f32>,\n ray_fade_config: vec4<f32>,\n debug_config: vec4<f32>,\n};\n\nstruct SsrHistoryUniform {\n // Current clip -> previous clip, f64-composed; consumed by ssr_temporal_resolve.wgsl.\n reproj: mat4x4<f32>,\n // World -> previous clip: fine for the hit-point reprojection below, whose position\n // comes from forward matrices (inverse_proj + inverse_view), not a composed inverse.\n prev_view_proj: mat4x4<f32>,\n history_state: vec4<u32>,\n};\n\nstruct TraceHit {\n hit_uv: vec2<f32>,\n hit_color: vec3<f32>,\n hit_confidence: f32,\n hit_mip_level: f32,\n debug_data: vec4<f32>,\n debug_vec_a: vec4<f32>,\n debug_vec_b: vec4<f32>,\n};\n\nstruct TraceFsOut {\n @location(0) color: vec4<f32>,\n @location(1) debug_data: vec4<f32>,\n @location(2) debug_vec_a: vec4<f32>,\n @location(3) debug_vec_b: vec4<f32>,\n};\n\n@group(0) @binding(0) var scene_color: texture_2d<f32>;\n@group(0) @binding(1) var repr_scene_color_texture: texture_2d<f32>;\n@group(0) @binding(2) var normal_rough_texture: texture_2d<f32>;\n@group(0) @binding(3) var repr_depth_texture: texture_2d<f32>;\n@group(0) @binding(4) var hiz_texture: texture_2d<f32>;\n@group(0) @binding(5) var linear_sampler: sampler;\n@group(0) @binding(6) var trace_mip_out: texture_storage_2d<r32float, write>;\n@group(0) @binding(7) var<uniform> u_ssr: SsrSettingsUniform;\n@group(0) @binding(8) var<uniform> u_prev: SsrHistoryUniform;\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SSR_MAX_STEPS_HARD: u32 = 96u;\nconst SSR_DEBUG_PIPELINE_STAGE_COUNT: u32 = 39u;\nconst SSR_DEBUG_MARCH_STAGE_COUNT: u32 = 64u;\nconst SSR_HORIZON_Z_HARD_EPS: f32 = 1e-7;\nconst SSR_PARAM_Z_MIN: f32 = 1e-6;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\nconst SSR_TRACE_AXIS_EPS: f32 = 1e-7;\nconst SSR_TRACE_MAX_T: f32 = 1.0e9;\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn max_steps() -> u32 {\n return clamp(u32(u_ssr.trace_config.x), 1u, SSR_MAX_STEPS_HARD);\n}\n\nfn hiz_mip_count() -> u32 {\n return max(1u, u32(u_ssr.trace_config.y));\n}\n\nfn depth_tolerance_m() -> f32 {\n return u_ssr.trace_config.z;\n}\n\nfn reflection_mip_count() -> f32 {\n return max(1.0, u_ssr.trace_config.w);\n}\n\nfn min_roughness() -> f32 { return u_ssr.material_config.x; }\nfn max_roughness() -> f32 { return u_ssr.material_config.y; }\nfn roughness_blur_multiplier() -> f32 { return max(u_ssr.material_config.z, 0.01); }\nfn roughness_mip_clamp_lower() -> f32 { return max(u_ssr.material_config.w, 0.0); }\nfn roughness_mip_clamp_upper(max_mip: f32) -> f32 {\n let raw = u_ssr.fade_config.w;\n return select(max_mip, clamp(raw, 0.0, max_mip), raw > 0.0);\n}\n// Temporary art-tuned floor: keep rough contact reflections from collapsing to mip0\n// until we replace this with a more principled roughness-limiter style solution.\nfn roughness_contact_min_mip(roughness: f32) -> f32 {\n return smoothstep(0.0, 0.25, roughness);\n}\n\nfn geometric_bias_m() -> f32 {\n return u_ssr.fade_config.z;\n}\n\nfn debug_stage() -> u32 {\n return u32(u_ssr.debug_config.x);\n}\n\nfn debug_stage_capture_enabled() -> bool {\n return u_ssr.debug_config.w > 0.5;\n}\n\nfn sign_nonzero(value: f32) -> f32 {\n if abs(value) <= 1e-5 {\n return 0.0;\n }\n return select(-1.0, 1.0, value > 0.0);\n}\n\nfn is_finite1(v: f32) -> bool { return v == v && abs(v) < SSR_FINITE_LIMIT; }\nfn is_finite3(v: vec3<f32>) -> bool { return all(v == v) && all(abs(v) < vec3<f32>(SSR_FINITE_LIMIT)); }\n\nfn safe_axis_t(numer: f32, denom: f32) -> f32 {\n if abs(denom) <= SSR_TRACE_AXIS_EPS {\n return SSR_TRACE_MAX_T;\n }\n return numer / denom;\n}\n\nfn safe_axis_t2(numer: vec2<f32>, denom: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(safe_axis_t(numer.x, denom.x), safe_axis_t(numer.y, denom.y));\n}\n\nfn store_debug_march_iteration(\n hit: ptr<function, TraceHit>,\n stage: u32,\n iter_index: u32,\n cur_screen_pos: vec3<f32>,\n cell_depth: f32,\n mip: i32,\n screen_ray_dir_z_nonnegative: bool,\n use_t_le_depth_predicate: bool,\n is_hit_before_gap_reject: bool,\n gap_reject: bool,\n t: f32,\n edge_t: f32,\n depth_t: f32,\n linear_gap: f32,\n cur_px: vec2<f32>,\n is_hit: bool,\n branch_class: f32,\n) {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n return;\n }\n let march_stage = stage - SSR_DEBUG_PIPELINE_STAGE_COUNT;\n if march_stage >= SSR_DEBUG_MARCH_STAGE_COUNT || march_stage != iter_index {\n return;\n }\n var flags = 0u;\n if screen_ray_dir_z_nonnegative { flags |= 1u; }\n if use_t_le_depth_predicate { flags |= 2u; }\n if is_hit_before_gap_reject { flags |= 4u; }\n if gap_reject { flags |= 8u; }\n if is_hit { flags |= 16u; }\n let packed_mip_flags = (u32(mip) << 8u) | flags;\n (*hit).debug_data = vec4<f32>(cur_screen_pos.xy, cell_depth, f32(packed_mip_flags));\n (*hit).debug_vec_a = vec4<f32>(t, edge_t, depth_t, linear_gap);\n (*hit).debug_vec_b = vec4<f32>(cur_px, f32(flags), branch_class);\n}\n\nfn store_debug_pipeline_capture(\n hit: ptr<function, TraceHit>,\n stage: u32,\n start_view_pos_raw: vec3<f32>,\n start_depth_linear: f32,\n roughness: f32,\n start_uv: vec2<f32>,\n start_view_normal: vec3<f32>,\n start_geom_normal: vec3<f32>,\n geom_bias_amount: f32,\n launch_view_pos: vec3<f32>,\n launch_ray_dir: vec3<f32>,\n launch_ray_length_m: f32,\n source_clip: vec4<f32>,\n source_ndc: vec3<f32>,\n biased_clip: vec4<f32>,\n biased_ndc: vec3<f32>,\n trace_end_clip: vec4<f32>,\n trace_end_ndc: vec3<f32>,\n screen_pos_z: f32,\n screen_end_z: f32,\n raw_screen_ray_dir: vec3<f32>,\n t: f32,\n t_max: f32,\n segment_t: f32,\n screen_ray_dir_xy: vec2<f32>,\n t2: vec2<f32>,\n hit_scene_color: vec3<f32>,\n hit_surface_normal: vec3<f32>,\n hit_surface_roughness: f32,\n hit_uv_exact: vec2<f32>,\n final_hit_depth: f32,\n ray_hit_pos: vec3<f32>,\n scene_hit_pos: vec3<f32>,\n travel_m: f32,\n projected_ray_hit_pos: vec3<f32>,\n ray_recon_delta_m: f32,\n hit_delta_m: f32,\n) {\n if stage >= SSR_DEBUG_PIPELINE_STAGE_COUNT {\n return;\n }\n if stage == 0u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw, start_depth_linear);\n (*hit).debug_vec_b = vec4<f32>(start_view_normal, roughness);\n } else if stage == 1u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw, start_depth_linear);\n (*hit).debug_vec_b = vec4<f32>(start_uv, 0.0, 0.0);\n } else if stage == 2u {\n (*hit).debug_vec_a = vec4<f32>(start_view_normal, roughness);\n (*hit).debug_vec_b = vec4<f32>(start_geom_normal, dot(start_view_normal, start_geom_normal));\n } else if stage == 3u {\n (*hit).debug_vec_a = vec4<f32>(start_geom_normal, geom_bias_amount);\n (*hit).debug_vec_b = vec4<f32>(start_view_normal, dot(start_view_normal, start_geom_normal));\n } else if stage == 5u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw.z, source_clip.z, source_clip.w, source_ndc.z);\n (*hit).debug_vec_b = vec4<f32>(launch_view_pos.z, biased_clip.z, biased_clip.w, biased_ndc.z);\n } else if stage == 6u {\n (*hit).debug_vec_a = vec4<f32>(launch_view_pos.z + launch_ray_dir.z, trace_end_clip.z, trace_end_clip.w, trace_end_ndc.z);\n (*hit).debug_vec_b = vec4<f32>(screen_pos_z, screen_end_z, raw_screen_ray_dir.z, abs(raw_screen_ray_dir.z));\n } else if stage == 7u {\n (*hit).debug_vec_a = vec4<f32>(geom_bias_amount, start_depth_linear, roughness, dot(start_view_normal, start_geom_normal));\n (*hit).debug_vec_b = vec4<f32>(launch_view_pos, 1.0);\n } else if stage == 8u {\n (*hit).debug_vec_a = vec4<f32>(launch_ray_dir, launch_ray_length_m);\n (*hit).debug_vec_b = vec4<f32>(screen_pos_z, screen_end_z, raw_screen_ray_dir.z, abs(raw_screen_ray_dir.z));\n } else if stage == 9u {\n (*hit).debug_vec_a = vec4<f32>(t, t_max, clamp(t / max(t_max, 1e-5), 0.0, 1.0), segment_t);\n (*hit).debug_vec_b = vec4<f32>(screen_ray_dir_xy, t2.x, t2.y);\n } else if stage == 26u {\n (*hit).debug_vec_a = vec4<f32>(hit_scene_color, hit_surface_roughness);\n (*hit).debug_vec_b = vec4<f32>(hit_uv_exact, final_hit_depth, 0.0);\n } else if stage == 27u {\n (*hit).debug_vec_a = vec4<f32>(hit_surface_normal, hit_surface_roughness);\n (*hit).debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 29u {\n (*hit).debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n (*hit).debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 30u {\n (*hit).debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n (*hit).debug_vec_b = vec4<f32>(ray_hit_pos, hit_delta_m);\n }\n}\n\nfn linearize_depth(depth: f32) -> f32 {\n let pos = u_camera.inverse_proj * vec4<f32>(0.0, 0.0, depth, 1.0);\n return pos.z / pos.w;\n}\n\nfn empty_hit() -> TraceHit {\n return TraceHit(vec2<f32>(-1.0, -1.0), vec3<f32>(0.0), 0.0, 0.0, vec4<f32>(0.0), vec4<f32>(0.0), vec4<f32>(0.0));\n}\n\nfn trace_fs_out(color: vec4<f32>, hit: TraceHit) -> TraceFsOut {\n return TraceFsOut(color, hit.debug_data, hit.debug_vec_a, hit.debug_vec_b);\n}\n\nfn store_trace_mip(pixel_coords: vec2<i32>, mip_level: f32) {\n textureStore(trace_mip_out, pixel_coords, vec4<f32>(mip_level, 0.0, 0.0, 0.0));\n}\n\nfn compute_reflection_mip_level(roughness: f32, ray_len: f32, screen_dims: vec2<f32>) -> f32 {\n let max_mip = max(reflection_mip_count() - 1.0, 0.0);\n if roughness <= 0.001 || max_mip <= 0.0 {\n return 0.0;\n }\n var mip_level = 0.0;\n let effective_roughness = min(roughness * roughness_blur_multiplier(), 0.999);\n let cone_angle = effective_roughness * 3.14159265 * 0.5;\n let cone_len = max(ray_len, 1e-4);\n let op_len = 2.0 * tan(cone_angle) * cone_len;\n let a = op_len;\n let h = cone_len;\n let blur_radius = (a * (sqrt(a * a + 4.0 * h * h) - a)) / (4.0 * h);\n let blur_px = blur_radius * max(screen_dims.x, screen_dims.y) / 16.0;\n let distance_mip = clamp(log2(max(blur_px, 1.0)), 0.0, max_mip);\n let far_factor = pow(clamp(1.25 - ray_len, 0.0, 1.0), 0.2);\n mip_level = distance_mip * far_factor;\n let mip_upper = roughness_mip_clamp_upper(max_mip);\n let mip_lower = min(roughness_mip_clamp_lower() + roughness_contact_min_mip(roughness), mip_upper);\n return clamp(mip_level, mip_lower, mip_upper);\n}\n\nfn tone_map_ssr_color(color: vec3<f32>) -> vec3<f32> {\n let rec709_luminance_weights = vec3<f32>(0.2126, 0.7152, 0.0722);\n return color / (1.0 + dot(color, rec709_luminance_weights));\n}\n\nfn world_pos_from_depth(uv_top_left: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec4<f32>(uv_top_left.x * 2.0 - 1.0, (1.0 - uv_top_left.y) * 2.0 - 1.0, depth, 1.0);\n let world_h = u_camera.inverse_view_proj * ndc;\n return world_h.xyz / world_h.w;\n}\n\nfn view_pos_from_depth(uv_top_left: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec4<f32>(uv_top_left.x * 2.0 - 1.0, (1.0 - uv_top_left.y) * 2.0 - 1.0, depth, 1.0);\n let view_h = u_camera.inverse_proj * ndc;\n return view_h.xyz / view_h.w;\n}\n\nfn project_view_to_screen(view_pos: vec3<f32>) -> vec3<f32> {\n let clip = u_camera.proj * vec4<f32>(view_pos, 1.0);\n let ndc = clip.xyz / clip.w;\n // XY are viewport UVs; Z stays in projected NDC depth space (unitless, same convention as the depth/Hi-Z textures), not meters.\n return vec3<f32>(ndc.x * 0.5 + 0.5, 1.0 - (ndc.y * 0.5 + 0.5), ndc.z);\n}\n\nfn load_hiz_depth(px: vec2<i32>, mip: u32) -> f32 {\n let dims = vec2<i32>(textureDimensions(hiz_texture, mip));\n let clamped = clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n return textureLoad(hiz_texture, clamped, i32(mip)).r;\n}\n\nfn uv_from_px(px: vec2<i32>, dims: vec2<i32>) -> vec2<f32> {\n return (vec2<f32>(px) + 0.5) / vec2<f32>(dims);\n}\n\n// Godot-style screen-space edge fade. Corner-heavy, axis-light:\n// margin_grad = distance (in pixels) to the nearest screen edge per axis.\n// margin_blend = smoothstep(0, margin.x*margin.y, margin_grad.x*margin_grad.y)\n// with margin = (W + H) * 0.05. Because the product margin_grad.x * margin_grad.y\n// grows fast along each axis, axis-midpoint reflections stay visible until within\n// ~(margin.x*margin.y / (half_screen)) px of the top/bottom edge, while corners\n// fade deep into the image. Reflections projecting off-frame on any axis get a\n// negative grad, which smoothstep clamps to 0 \u2014 a natural hard-cut. Scale\n// invariant: using half-res or full-res textureDimensions gives the same result.\nfn edge_fade(uv: vec2<f32>) -> f32 {\n let screen_size = vec2<f32>(textureDimensions(repr_depth_texture));\n let px = uv * screen_size;\n let margin_scalar = (screen_size.x + screen_size.y) * 0.05;\n let margin_sq = margin_scalar * margin_scalar;\n let grad = min(px, screen_size - px);\n return smoothstep(0.0, margin_sq, grad.x * grad.y);\n}\n\nfn load_repr_depth(px: vec2<i32>) -> f32 {\n let dims = vec2<i32>(textureDimensions(repr_depth_texture));\n let clamped = clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n return textureLoad(repr_depth_texture, clamped, 0).r;\n}\n\nfn choose_neighbor(depth_c: f32, a_px: vec2<i32>, b_px: vec2<i32>, dims: vec2<i32>) -> vec3<f32> {\n let a_depth = load_repr_depth(a_px);\n let b_depth = load_repr_depth(b_px);\n let choose_a = abs(a_depth - depth_c) <= abs(b_depth - depth_c);\n let chosen_px = select(clamp(b_px, vec2<i32>(0), dims - vec2<i32>(1, 1)), clamp(a_px, vec2<i32>(0), dims - vec2<i32>(1, 1)), choose_a);\n let chosen_depth = select(b_depth, a_depth, choose_a);\n return view_pos_from_depth(uv_from_px(chosen_px, dims), chosen_depth);\n}\n\nfn compute_geometric_normal(pixel_px: vec2<i32>, depth_c: f32, view_c: vec3<f32>, dims: vec2<i32>, view_normal: vec3<f32>) -> vec3<f32> {\n let h_pos = choose_neighbor(depth_c, pixel_px + vec2<i32>(-1, 0), pixel_px + vec2<i32>(1, 0), dims);\n let v_pos = choose_neighbor(depth_c, pixel_px + vec2<i32>(0, -1), pixel_px + vec2<i32>(0, 1), dims);\n let h_der = h_pos - view_c;\n let v_der = v_pos - view_c;\n if length(h_der) <= 1e-5 || length(v_der) <= 1e-5 {\n return view_normal;\n }\n var geom_normal = normalize(cross(v_der, h_der));\n if dot(geom_normal, view_normal) < 0.0 {\n geom_normal = -geom_normal;\n }\n return geom_normal;\n}\n\nfn trace_hiz(\n start_uv: vec2<f32>,\n start_depth: f32,\n start_view_pos: vec3<f32>,\n view_normal: vec3<f32>,\n geom_normal: vec3<f32>,\n roughness: f32,\n) -> TraceHit {\n var hit = empty_hit();\n let stage = debug_stage();\n let half_dims = vec2<i32>(textureDimensions(repr_depth_texture));\n let half_dims_f = vec2<f32>(half_dims);\n // Stage 01-09: source inputs sampled at the current pixel, then the launch terms derived from them.\n let start_depth_linear = abs(start_view_pos.z);\n let start_roughness_mask = 1.0 - smoothstep(min_roughness(), max_roughness(), roughness);\n\n // Add a small bias toward the geometry normal to help prevent immediate self-intersections.\n let bias = geometric_bias_m() * (1.0 - pow(clamp(dot(view_normal, geom_normal), 0.0, 1.0), 8.0));\n var view_pos = start_view_pos + geom_normal * bias;\n var ray_dir = normalize(reflect(normalize(view_pos), view_normal));\n if dot(ray_dir, geom_normal) < 0.0 {\n ray_dir = normalize(reflect(ray_dir, geom_normal));\n }\n if !is_finite3(ray_dir) {\n hit.debug_data = vec4<f32>(19.0, 0.0, 0.0, 0.0);\n return hit;\n }\n\n var screen_pos = project_view_to_screen(view_pos);\n if !is_finite3(screen_pos) {\n hit.debug_data = vec4<f32>(20.0, 0.0, 0.0, 0.0);\n return hit;\n }\n // clip that segment to the near plane.\n var trace_end_pos = view_pos + ray_dir;\n if trace_end_pos.z > -0.0001 {\n if abs(ray_dir.z) <= SSR_TRACE_AXIS_EPS {\n hit.debug_data = vec4<f32>(21.0, trace_end_pos.z, ray_dir.z, 0.0);\n return hit;\n }\n trace_end_pos -= ray_dir / ray_dir.z * (trace_end_pos.z + 0.0001);\n }\n let source_clip = u_camera.proj * vec4<f32>(start_view_pos, 1.0);\n let source_ndc = source_clip.xyz / source_clip.w;\n let biased_clip = u_camera.proj * vec4<f32>(view_pos, 1.0);\n let biased_ndc = biased_clip.xyz / biased_clip.w;\n let trace_end_clip = u_camera.proj * vec4<f32>(trace_end_pos, 1.0);\n let trace_end_ndc = trace_end_clip.xyz / trace_end_clip.w;\n let screen_end = project_view_to_screen(trace_end_pos);\n if !is_finite3(screen_end) {\n hit.debug_data = vec4<f32>(22.0, 0.0, 0.0, 0.0);\n return hit;\n }\n let raw_screen_ray_dir = screen_end - screen_pos;\n let raw_screen_ray_z_abs = abs(raw_screen_ray_dir.z);\n if raw_screen_ray_z_abs <= SSR_HORIZON_Z_HARD_EPS {\n hit.debug_data = vec4<f32>(1.0, ray_dir.z, raw_screen_ray_dir.z, 0.0);\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n 0.0, 0.0, 0.0, vec2<f32>(0.0), vec2<f32>(0.0), vec3<f32>(0.0), vec3<f32>(0.0), 0.0, start_uv, start_depth,\n vec3<f32>(0.0), vec3<f32>(0.0), 0.0, vec3<f32>(0.0), 0.0, 0.0\n );\n return hit;\n }\n let param_z_abs = max(raw_screen_ray_z_abs, SSR_PARAM_Z_MIN);\n let screen_ray_dir = raw_screen_ray_dir / param_z_abs;\n let facing_camera = screen_ray_dir.z <= 0.0;\n let start_px = clamp(vec2<i32>(screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let t0 = safe_axis_t2(vec2<f32>(0.0) - screen_pos.xy, screen_ray_dir.xy);\n let t1 = safe_axis_t2(vec2<f32>(1.0) - screen_pos.xy, screen_ray_dir.xy);\n let t2 = max(t0, t1);\n let t_max = min(t2.x, t2.y);\n if !is_finite1(t_max) || t_max <= 0.0 {\n hit.debug_data = vec4<f32>(2.0, t_max, screen_ray_dir.x, screen_ray_dir.y);\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n 0.0, t_max, 0.0, screen_ray_dir.xy, t2, vec3<f32>(0.0), vec3<f32>(0.0), 0.0, start_uv, start_depth,\n vec3<f32>(0.0), vec3<f32>(0.0), 0.0, vec3<f32>(0.0), 0.0, 0.0\n );\n return hit;\n }\n\n let cell_step = vec2<f32>(\n select(1.0, -1.0, screen_ray_dir.x < 0.0),\n select(1.0, -1.0, screen_ray_dir.y < 0.0),\n );\n let start_cell = floor(screen_pos.xy * half_dims_f);\n let next_cell = start_cell + clamp(cell_step, vec2<f32>(0.0), vec2<f32>(1.0));\n let next_pos = next_cell / half_dims_f + cell_step * 0.000001;\n let start_t = safe_axis_t2(next_pos - screen_pos.xy, screen_ray_dir.xy);\n let initial_t = min(start_t.x, start_t.y);\n if !is_finite1(initial_t) {\n hit.debug_data = vec4<f32>(23.0, initial_t, t_max, 0.0);\n return hit;\n }\n if stage == 0u {\n hit.debug_vec_a = vec4<f32>(start_view_pos, start_depth);\n hit.debug_vec_b = vec4<f32>(ray_dir, initial_t);\n }\n var t = initial_t;\n var cur_level: i32 = 0;\n let max_level = i32(hiz_mip_count()) - 1;\n var steps_left = i32(max_steps());\n var validity = 1.0;\n var debug_diag_code_override = -1.0;\n var debug_depth_t = -1.0;\n var debug_edge_t = -1.0;\n var debug_linear_gap = -1.0;\n var debug_hit_mip = -1.0;\n var debug_steps_taken = 0.0;\n var debug_max_px_delta = -1.0;\n var dbg_decisive_branch_class = 0.0;\n var dbg_decisive_cell_uv = vec2<f32>(0.0);\n var dbg_decisive_cur_px = vec2<f32>(0.0);\n var dbg_decisive_depth_t_edge_sign = 0.0;\n var dbg_decisive_accept_hit_vs_continue = 0.0;\n var march_iter = 0u;\n var saw_coarse_hit = false;\n var mip0_confirmed = false;\n var last_hit_mip = -1.0;\n var last_coarse_hit_mip = -1.0;\n var last_coarse_hit_t = -1.0;\n var last_coarse_hit_depth_t = -1.0;\n var last_coarse_hit_edge_t = -1.0;\n var last_coarse_hit_screen_xy = vec2<f32>(0.0);\n var last_coarse_hit_stale = false;\n var last_valid_bracket_mip = -1.0;\n var mip0_entry_t = -1.0;\n var mip0_accept_t = -1.0;\n var mip0_accept_depth_t = -1.0;\n var mip0_steps_after_entry = 0u;\n\n // Stage 10-18: Hi-Z traversal, mip descent/ascent, and the decisive accept-vs-continue branch.\n loop {\n if !(cur_level >= 0 && steps_left > 0 && t < t_max) {\n break;\n }\n let mip = u32(cur_level);\n let cell_dims = vec2<f32>(textureDimensions(hiz_texture, mip));\n let cur_screen_pos = screen_pos + screen_ray_dir * t;\n if !is_finite3(cur_screen_pos) {\n debug_diag_code_override = 24.0;\n validity = 0.0;\n break;\n }\n let cell_index = clamp(vec2<i32>(floor(cur_screen_pos.xy * cell_dims)), vec2<i32>(0), vec2<i32>(cell_dims) - vec2<i32>(1, 1));\n let cell_minmax = textureLoad(hiz_texture, cell_index, cur_level).rg;\n let cell_depth = cell_minmax.r;\n let cell_depth_max = cell_minmax.g;\n\n let cur_px_i = clamp(vec2<i32>(cur_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px_f = vec2<f32>(cur_px_i);\n let cell_uv = (vec2<f32>(cell_index) + vec2<f32>(0.5)) / cell_dims;\n let next_cell_index = vec2<f32>(cell_index) + clamp(cell_step, vec2<f32>(0.0), vec2<f32>(1.0));\n let next_cell_pos = next_cell_index / cell_dims + cell_step * 0.000001;\n let pos_t = safe_axis_t2(next_cell_pos - screen_pos.xy, screen_ray_dir.xy);\n let edge_t = min(pos_t.x, pos_t.y);\n var dbg_iter_depth_t = -999.0;\n var dbg_iter_linear_gap = -999.0;\n var dbg_iter_is_hit = false;\n var dbg_iter_branch_class = 0.0;\n\n let depth_t = (cell_depth - screen_pos.z) / screen_ray_dir.z;\n if !is_finite1(edge_t) || !is_finite1(depth_t) {\n debug_diag_code_override = 25.0;\n validity = 0.0;\n break;\n }\n dbg_iter_depth_t = depth_t;\n debug_depth_t = depth_t;\n debug_edge_t = edge_t;\n debug_hit_mip = f32(cur_level);\n debug_steps_taken = f32(max_steps()) - f32(steps_left);\n let screen_ray_dir_z_nonnegative = screen_ray_dir.z >= 0.0;\n let use_t_le_depth_predicate = facing_camera;\n var is_hit = select(depth_t <= edge_t, t <= depth_t, use_t_le_depth_predicate);\n let is_hit_before_gap_reject = is_hit;\n dbg_iter_is_hit = is_hit;\n var mip_offset = select(1, -1, is_hit);\n // Dual Hi-Z behind-geometry skip: the depth buffer only stores front surfaces,\n // so a ray whose nearest point in this cell is deeper than the cell's MAX depth\n // (+ thickness tolerance) cannot hit anything recorded here. Skip the whole cell\n // at coarse mips instead of descending to mip0 and gap-rejecting 1px at a time.\n // This is what lets reflections continue past occluders (ball, arm, post).\n if is_hit && cur_level > 0 && cell_depth_max < 1.0 {\n let ray_nearest_t = select(t, min(edge_t, t_max), use_t_le_depth_predicate);\n let ray_nearest_z = screen_pos.z + screen_ray_dir.z * ray_nearest_t;\n let behind_gap = linearize_depth(cell_depth_max) - linearize_depth(ray_nearest_z);\n if behind_gap > depth_tolerance_m() {\n is_hit = false;\n mip_offset = 1;\n }\n }\n let depth_t_edge_sign = sign_nonzero(depth_t - edge_t);\n let bracket_eps_t = 2e-6;\n let stale_hit = is_hit && cur_level > 0 && !facing_camera && (depth_t + bracket_eps_t < t);\n let interval_hit = is_hit && cur_level > 0 && (depth_t + bracket_eps_t >= t) && (depth_t <= edge_t + bracket_eps_t);\n var dbg_iter_gap_reject = false;\n if cur_level == 0 {\n if saw_coarse_hit && mip0_entry_t < 0.0 {\n mip0_entry_t = t;\n }\n if mip0_entry_t >= 0.0 {\n mip0_steps_after_entry += 1u;\n }\n // Godot's mip0 thickness reject: (z0 - z1) > tolerance. Both linearize_depth\n // impls return negative view-z forward, so this rejects rays that penetrated\n // more than depth_tolerance BEHIND the surface (thin-object false hits),\n // letting the ray continue marching instead of accepting then zeroing alpha.\n let z0 = linearize_depth(cell_depth);\n let z1 = linearize_depth(cur_screen_pos.z);\n let linear_gap = z1 - z0; // positive = open gap in front, negative = penetration\n let depth_tolerance = depth_tolerance_m();\n dbg_iter_linear_gap = linear_gap;\n debug_linear_gap = linear_gap;\n let gap_reject = -linear_gap > depth_tolerance;\n if gap_reject {\n dbg_iter_gap_reject = true;\n if is_hit {\n dbg_iter_branch_class = 3.0;\n dbg_decisive_branch_class = 3.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 0.0;\n hit.debug_data = vec4<f32>(5.0, linear_gap, t, t_max);\n }\n is_hit = false;\n mip_offset = 0;\n }\n }\n\n if is_hit {\n last_hit_mip = f32(cur_level);\n if cur_level == 0 {\n mip0_confirmed = true;\n } else {\n saw_coarse_hit = true;\n last_coarse_hit_mip = f32(cur_level);\n last_coarse_hit_t = t;\n last_coarse_hit_depth_t = depth_t;\n last_coarse_hit_edge_t = edge_t;\n last_coarse_hit_screen_xy = cur_screen_pos.xy;\n last_coarse_hit_stale = stale_hit;\n if interval_hit {\n last_valid_bracket_mip = f32(cur_level);\n }\n }\n dbg_decisive_branch_class = 7.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 1.0;\n dbg_iter_branch_class = 7.0;\n dbg_iter_is_hit = true;\n if stage == 1u || stage == 2u {\n let scene_pos = view_pos_from_depth(cur_screen_pos.xy, cell_depth);\n let travel_m = length(scene_pos - start_view_pos);\n if stage == 1u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos, t);\n hit.debug_vec_b = vec4<f32>(scene_pos, travel_m);\n } else {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cell_index), cell_depth, edge_t);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, travel_m, t_max);\n }\n }\n }\n\n if !is_hit {\n if dbg_decisive_branch_class <= 0.0 || dbg_decisive_branch_class < 3.0 {\n dbg_iter_branch_class = 2.0;\n dbg_decisive_branch_class = 2.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 0.0;\n }\n }\n\n dbg_iter_is_hit = is_hit;\n if dbg_iter_branch_class <= 0.0 && is_hit {\n dbg_iter_branch_class = 7.0;\n }\n store_debug_march_iteration(\n &hit, stage, march_iter, cur_screen_pos, cell_depth, cur_level, screen_ray_dir_z_nonnegative, use_t_le_depth_predicate,\n is_hit_before_gap_reject, dbg_iter_gap_reject, t, edge_t, dbg_iter_depth_t, dbg_iter_linear_gap, cur_px_f, dbg_iter_is_hit, dbg_iter_branch_class\n );\n\n if is_hit {\n if !facing_camera {\n t = max(t, depth_t);\n }\n if cur_level == 0 {\n mip0_accept_t = t;\n mip0_accept_depth_t = depth_t;\n }\n } else {\n t = edge_t;\n }\n cur_level = min(cur_level + mip_offset, max_level);\n steps_left -= 1;\n march_iter += 1u;\n }\n\n // Stage 19-29: resolve the final screen coordinate and compare against mip0 depth at the direct hit UV, closer to Godot's path.\n let cur_screen_pos = screen_pos + screen_ray_dir * t;\n if !is_finite3(cur_screen_pos) {\n hit.debug_data = vec4<f32>(26.0, t, t_max, 0.0);\n return hit;\n }\n let segment_t = clamp(t / param_z_abs, 0.0, 1.0);\n let segment_ray_hit_pos = view_pos + (trace_end_pos - view_pos) * segment_t;\n let segment_screen_pos = project_view_to_screen(segment_ray_hit_pos);\n if !is_finite3(segment_ray_hit_pos) || !is_finite3(segment_screen_pos) {\n hit.debug_data = vec4<f32>(27.0, t, segment_t, 0.0);\n return hit;\n }\n let scene_dims_f = vec2<f32>(textureDimensions(scene_color));\n let scene_uv_margin = vec2<f32>(0.5) / scene_dims_f;\n let resolved_screen_xy = clamp(cur_screen_pos.xy, scene_uv_margin, vec2<f32>(1.0) - scene_uv_margin);\n let cur_px_from_cur_screen_xy_i = clamp(vec2<i32>(cur_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px_from_segment_screen_xy_i = clamp(vec2<i32>(segment_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px = cur_px_from_cur_screen_xy_i;\n let repr_uv = uv_from_px(cur_px, half_dims);\n let cur_px_uv = repr_uv;\n let hit_uv_exact = resolved_screen_xy;\n let final_hit_depth = load_hiz_depth(cur_px, 0u);\n let hit_sample_nr = textureLoad(normal_rough_texture, cur_px, 0);\n let projected_ray_hit_pos = view_pos_from_depth(hit_uv_exact, cur_screen_pos.z);\n let ray_hit_pos = projected_ray_hit_pos;\n let scene_hit_pos = view_pos_from_depth(hit_uv_exact, final_hit_depth);\n\n // Reproject hit world-pos through prev_view_proj so scene color is sampled at\n // the pixel where this hit point was shown last frame, not at the current-frame\n // hit pixel. Correct under camera rotation/translation.\n // On first frame (history_state.x==0) prev_hdr_scene is cleared; sampling returns 0\n // and we default prev_vignette = 1.0 so SSR isn't spuriously killed before history\n // stabilizes.\n let hit_world_pos = (u_camera.inverse_view * vec4<f32>(scene_hit_pos, 1.0)).xyz;\n let trace_exhausted = steps_left <= 0 && t < t_max;\n let coarse_hit_lost = saw_coarse_hit && !mip0_confirmed;\n let background_hit = final_hit_depth >= 1.0;\n let hit_prev_clip = u_prev.prev_view_proj * vec4<f32>(hit_world_pos, 1.0);\n var hit_scene_color = vec3<f32>(0.0);\n // Godot-style edge fade keyed on the PREV-frame hit UV (the source pixel the color\n // comes from). Defaults to 1.0 on first frame / valid center, drops toward 0 near\n // the prev-frame screen edges, and goes to 0 when the hit was behind the camera.\n var prev_vignette = 1.0;\n if u_prev.history_state.x != 0u {\n if hit_prev_clip.w > 0.0 {\n let hit_prev_ndc = hit_prev_clip.xyz / hit_prev_clip.w;\n let hit_prev_uv = vec2<f32>(hit_prev_ndc.x * 0.5 + 0.5, 1.0 - (hit_prev_ndc.y * 0.5 + 0.5));\n prev_vignette = edge_fade(hit_prev_uv);\n // Sample even when slightly out-of-frame; clamp UV and rely on prev_vignette\n // (which goes to 0 for |ndc|>=1) to zero-weight the contribution.\n let sample_uv = clamp(hit_prev_uv, vec2<f32>(0.0), vec2<f32>(1.0));\n hit_scene_color = textureSampleLevel(scene_color, linear_sampler, sample_uv, 0.0).rgb;\n } else {\n prev_vignette = 0.0;\n }\n if background_hit {\n // UE-style SSR can reflect sky from scene color. For true background misses there\n // is no world-space hit to reproject, so use the traced screen exit UV directly.\n prev_vignette = mix(0.35, 1.0, edge_fade(hit_uv_exact));\n hit_scene_color = textureSampleLevel(scene_color, linear_sampler, hit_uv_exact, 0.0).rgb;\n }\n }\n let hit_delta_vec = scene_hit_pos - ray_hit_pos;\n let hit_delta_m = length(hit_delta_vec);\n let ray_recon_delta_m = length(projected_ray_hit_pos - segment_ray_hit_pos);\n let segment_screen_xy_delta = segment_screen_pos.xy - cur_screen_pos.xy;\n let segment_screen_z_delta = segment_screen_pos.z - cur_screen_pos.z;\n let cur_px_coords = vec2<f32>(cur_px);\n let resolved_screen_px_fract = fract(resolved_screen_xy * half_dims_f);\n let cur_px_from_cur_screen_xy = vec2<f32>(cur_px_from_cur_screen_xy_i);\n let cur_px_from_segment_screen_xy = vec2<f32>(cur_px_from_segment_screen_xy_i);\n let cur_px_difference = cur_px_from_segment_screen_xy - cur_px_from_cur_screen_xy;\n if dbg_decisive_branch_class <= 0.0 {\n dbg_decisive_branch_class = 8.0;\n }\n let last_coarse_hit_overshoot_t = max(last_coarse_hit_t - last_coarse_hit_depth_t, 0.0);\n let coarse_ray_screen_z = screen_pos.z + screen_ray_dir.z * last_coarse_hit_t;\n let coarse_scene_screen_z = screen_pos.z + screen_ray_dir.z * last_coarse_hit_depth_t;\n let coarse_ray_view_pos = view_pos_from_depth(last_coarse_hit_screen_xy, coarse_ray_screen_z);\n let coarse_scene_view_pos = view_pos_from_depth(last_coarse_hit_screen_xy, coarse_scene_screen_z);\n let last_coarse_hit_overshoot_m = select(\n 0.0,\n length(coarse_ray_view_pos - coarse_scene_view_pos),\n last_coarse_hit_mip >= 0.0\n );\n let mip0_refine_delta_t = select(0.0, mip0_entry_t - mip0_accept_t, mip0_entry_t >= 0.0 && mip0_accept_t >= 0.0);\n let mip0_final_overshoot_t = max(mip0_accept_t - mip0_accept_depth_t, 0.0);\n let travel_m = length(scene_hit_pos - start_view_pos);\n let reflection_ray_len = length(screen_ray_dir.xy * t);\n if !is_finite1(hit_delta_m) || !is_finite1(reflection_ray_len) || !is_finite3(hit_scene_color) {\n validity = 0.0;\n debug_diag_code_override = 28.0;\n }\n hit.hit_mip_level = 0.0;\n if is_finite1(reflection_ray_len) {\n hit.hit_mip_level = compute_reflection_mip_level(roughness, reflection_ray_len, half_dims_f);\n }\n if stage == 1u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos, t);\n } else if stage == 2u {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cur_px), final_hit_depth, t_max);\n } else if stage == 7u {\n hit.debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n } else if stage == 8u {\n hit.debug_vec_a = vec4<f32>(hit_scene_color, hit_sample_nr.w);\n } else if stage == 9u {\n hit.debug_vec_a = vec4<f32>(segment_ray_hit_pos, segment_t);\n } else if stage == 10u {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cur_px), hit_uv_exact);\n } else if stage == 11u {\n hit.debug_vec_a = vec4<f32>(debug_depth_t, debug_edge_t, t, t_max);\n }\n if stage == 11u {\n hit.debug_vec_b = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n }\n if t >= t_max || final_hit_depth >= 1.0 {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n debug_diag_code_override = 3.0;\n }\n validity = 0.0;\n }\n // Godot-parity exhaustion handling: an exhausted/unconfirmed trace keeps its validity\n // and the hit_delta confidence term below decides. Hard-zeroing here turned every\n // exhausted grazing ray into a hard-black hole (B30); soft confidence falls back to IBL.\n if trace_exhausted {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n debug_diag_code_override = 17.0;\n }\n } else if !mip0_confirmed {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT && debug_diag_code_override < 0.0 {\n debug_diag_code_override = 18.0;\n }\n }\n if background_hit {\n debug_diag_code_override = 11.0;\n validity = 1.0;\n }\n let short_ray_screen_threshold = 2.0 / half_dims_f;\n if !background_hit && all(abs(screen_ray_dir.xy * t) < short_ray_screen_threshold) {\n let hit_normal_view = normalize((u_camera.view * vec4<f32>(normalize(hit_sample_nr.xyz), 0.0)).xyz);\n if dot(ray_dir, hit_normal_view) >= 0.0 {\n validity = 0.0;\n }\n }\n\n if stage == 1u {\n hit.debug_vec_b = vec4<f32>(scene_hit_pos, travel_m);\n } else if stage == 2u {\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, travel_m, t);\n } else if stage == 4u {\n hit.debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n } else if stage == 8u {\n hit.debug_vec_b = vec4<f32>(hit_sample_nr.xyz, final_hit_depth);\n } else if stage == 9u {\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 10u {\n hit.debug_vec_b = vec4<f32>(repr_uv, cur_screen_pos.xy);\n }\n // Stage 30-39: compare ray hit vs scene hit, then apply Godot-style post-hit confidence and fades.\n // Match Godot's post-hit confidence path here: use the base depth tolerance directly,\n // without extra local shells, then square the confidence weight.\n let validity_pre_confidence = validity;\n let confidence_tolerance = depth_tolerance_m();\n if !background_hit && coarse_hit_lost && hit_delta_m > confidence_tolerance {\n debug_diag_code_override = 16.0;\n }\n let confidence = select(1.0 - smoothstep(0.0, confidence_tolerance, hit_delta_m), 1.0, background_hit);\n let confidence_term = clamp(confidence * confidence, 0.0, 1.0);\n validity *= confidence_term;\n let validity_post_confidence = validity;\n let hit_uv = hit_uv_exact;\n // Godot-style: fade is based on the PREV-frame hit UV only (where the source color\n // actually came from). Reflections near the current-frame screen edge stay fully\n // visible as long as their reprojected source was safely in-frame last frame.\n // prev_vignette was seeded to 1.0 on first frame (no fade) and 0.0 when the hit\n // projected behind the camera last frame.\n let margin_blend = prev_vignette;\n // Ray fade runs on NORMALIZED along-ray progress (t / t_max), not absolute screen-UV\n // ray length: t_max is where this ray exits the screen, so progress is scale- and\n // direction-invariant. The old absolute length hard-zeroed any ray covering >= 1.0\n // screen UV and made alpha a function of on-screen travel distance, which painted\n // the B46 down-screen fade band.\n let ray_progress = clamp(t / max(t_max, 1e-5), 0.0, 1.0);\n let near_power = max(u_ssr.ray_fade_config.x, 0.0);\n let far_power = max(u_ssr.ray_fade_config.y, 0.0);\n var fade_in = 1.0;\n var fade_out = 1.0;\n if near_power > 0.0 {\n fade_in = pow(ray_progress, near_power);\n }\n if far_power > 0.0 {\n fade_out = pow(1.0 - ray_progress, far_power);\n }\n let fade = select(fade_in * fade_out, 1.0, fade_in * fade_out > 0.999);\n // UE5-style intensity scalar applied once at the end; on premultiplied output this\n // scales alpha and color together, equivalent to UE5's `OutColor *= SSRParams.r`.\n // The CPU-side early-out for intensity < 0.01 normally skips the whole SSR pipeline.\n validity *= fade * margin_blend * u_ssr.fade_config.x;\n\n let alpha_real = validity;\n let alpha = alpha_real;\n // Alt+Y stage readback follows the fixed SSR stage order.\n if stage == 0u {\n hit.debug_vec_a = vec4<f32>(start_depth_linear, start_depth, roughness, start_roughness_mask);\n hit.debug_vec_b = vec4<f32>(0.0, 0.0, bias, 0.0);\n } else if stage == 1u {\n hit.debug_vec_a = vec4<f32>(start_view_pos, start_depth);\n hit.debug_vec_b = vec4<f32>(start_uv, 0.0, 0.0);\n } else if stage == 2u {\n hit.debug_vec_a = vec4<f32>(view_normal, roughness);\n hit.debug_vec_b = vec4<f32>(geom_normal, dot(view_normal, geom_normal));\n } else if stage == 3u {\n hit.debug_vec_a = vec4<f32>(geom_normal, bias);\n hit.debug_vec_b = vec4<f32>(view_normal, dot(view_normal, geom_normal));\n } else if stage == 4u {\n hit.debug_vec_a = vec4<f32>(start_roughness_mask, roughness, 0.0, 0.0);\n hit.debug_vec_b = vec4<f32>(start_depth_linear, min_roughness(), max_roughness(), 0.0);\n } else if stage == 5u {\n hit.debug_vec_a = vec4<f32>(start_view_pos.z, source_clip.z, source_clip.w, source_ndc.z);\n hit.debug_vec_b = vec4<f32>(view_pos.z, biased_clip.z, biased_clip.w, biased_ndc.z);\n } else if stage == 6u {\n hit.debug_vec_a = vec4<f32>(trace_end_pos.z, trace_end_clip.z, trace_end_clip.w, trace_end_ndc.z);\n hit.debug_vec_b = vec4<f32>(screen_pos.z, screen_end.z, raw_screen_ray_dir.z, raw_screen_ray_z_abs);\n } else if stage == 7u {\n hit.debug_vec_a = vec4<f32>(bias, start_depth_linear, roughness, dot(view_normal, geom_normal));\n hit.debug_vec_b = vec4<f32>(view_pos, 1.0);\n } else if stage == 8u {\n hit.debug_vec_a = vec4<f32>(ray_dir, distance(view_pos, trace_end_pos));\n hit.debug_vec_b = vec4<f32>(screen_pos.z, screen_end.z, raw_screen_ray_dir.z, raw_screen_ray_z_abs);\n } else if stage == 9u {\n hit.debug_vec_a = vec4<f32>(t, t_max, clamp(t / max(t_max, 1e-5), 0.0, 1.0), segment_t);\n hit.debug_vec_b = vec4<f32>(screen_ray_dir.xy, t2.x, t2.y);\n } else if stage == 10u {\n hit.debug_vec_a = vec4<f32>(debug_depth_t, debug_edge_t, debug_linear_gap, debug_max_px_delta);\n hit.debug_vec_b = vec4<f32>(t, t_max, segment_t, dbg_decisive_accept_hit_vs_continue);\n } else if stage == 11u {\n hit.debug_vec_a = vec4<f32>(debug_edge_t, debug_depth_t, t, t_max);\n hit.debug_vec_b = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n } else if stage == 12u {\n hit.debug_vec_a = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue, 0.0);\n } else if stage == 13u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue, 0.0);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n } else if stage == 14u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n hit.debug_vec_b = vec4<f32>(debug_depth_t, debug_edge_t, debug_linear_gap, debug_max_px_delta);\n } else if stage == 15u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_cur_px, debug_max_px_delta, dbg_decisive_branch_class);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue);\n } else if stage == 16u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_depth_t_edge_sign, debug_depth_t, debug_edge_t, debug_linear_gap);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_accept_hit_vs_continue, 0.0, 0.0);\n } else if stage == 17u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_accept_hit_vs_continue, dbg_decisive_branch_class, debug_hit_mip, debug_steps_taken);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n } else if stage == 18u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos.xy, cur_screen_pos.z, t);\n hit.debug_vec_b = vec4<f32>(segment_screen_pos.xy, segment_screen_pos.z, segment_t);\n } else if stage == 19u {\n hit.debug_vec_a = vec4<f32>(segment_screen_pos.xy, segment_screen_pos.z, segment_t);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, cur_screen_pos.z, t);\n } else if stage == 20u {\n hit.debug_vec_a = vec4<f32>(resolved_screen_xy, resolved_screen_px_fract);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, segment_screen_pos.xy);\n } else if stage == 21u {\n hit.debug_vec_a = vec4<f32>(resolved_screen_px_fract, resolved_screen_xy);\n hit.debug_vec_b = vec4<f32>(repr_uv, hit_uv_exact);\n } else if stage == 22u {\n hit.debug_vec_a = vec4<f32>(cur_px_coords, final_hit_depth, 0.0);\n hit.debug_vec_b = vec4<f32>(repr_uv, cur_screen_pos.xy);\n } else if stage == 23u {\n hit.debug_vec_a = vec4<f32>(cur_px_from_cur_screen_xy, cur_screen_pos.xy);\n hit.debug_vec_b = vec4<f32>(resolved_screen_xy, t, segment_t);\n } else if stage == 24u {\n hit.debug_vec_a = vec4<f32>(cur_px_from_segment_screen_xy, segment_screen_pos.xy);\n hit.debug_vec_b = vec4<f32>(resolved_screen_xy, t, segment_t);\n } else if stage == 25u {\n hit.debug_vec_a = vec4<f32>(cur_px_difference, resolved_screen_px_fract);\n hit.debug_vec_b = vec4<f32>(cur_px_from_cur_screen_xy, cur_px_from_segment_screen_xy);\n } else if stage == 26u {\n hit.debug_vec_a = vec4<f32>(hit_scene_color, hit_sample_nr.w);\n hit.debug_vec_b = vec4<f32>(hit_uv_exact, final_hit_depth, 0.0);\n } else if stage == 27u {\n hit.debug_vec_a = vec4<f32>(hit_sample_nr.xyz, hit_sample_nr.w);\n hit.debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 28u {\n hit.debug_vec_a = vec4<f32>(hit_sample_nr.w, start_roughness_mask, 0.0, 0.0);\n hit.debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 29u {\n hit.debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 30u {\n hit.debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n hit.debug_vec_b = vec4<f32>(ray_hit_pos, hit_delta_m);\n } else if stage == 31u {\n hit.debug_vec_a = vec4<f32>(hit_delta_vec, hit_delta_m);\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 32u {\n hit.debug_vec_a = vec4<f32>(hit_delta_m, travel_m, debug_linear_gap, ray_recon_delta_m);\n hit.debug_vec_b = vec4<f32>(debug_depth_t, debug_edge_t, t, t_max);\n } else if stage == 33u {\n hit.debug_vec_a = vec4<f32>(last_coarse_hit_mip, last_coarse_hit_t, last_coarse_hit_depth_t, last_coarse_hit_edge_t);\n hit.debug_vec_b = vec4<f32>(last_coarse_hit_overshoot_t, last_coarse_hit_overshoot_m, select(0.0, 1.0, last_coarse_hit_stale), last_valid_bracket_mip);\n } else if stage == 34u {\n hit.debug_vec_a = vec4<f32>(confidence, confidence_term, validity_pre_confidence, validity_post_confidence);\n hit.debug_vec_b = vec4<f32>(alpha_real, alpha, alpha, hit.hit_confidence);\n } else if stage == 35u {\n hit.debug_vec_a = vec4<f32>(mip0_entry_t, mip0_accept_t, mip0_accept_depth_t, mip0_refine_delta_t);\n hit.debug_vec_b = vec4<f32>(mip0_final_overshoot_t, f32(mip0_steps_after_entry), select(0.0, 1.0, mip0_entry_t >= 0.0), select(0.0, 1.0, mip0_accept_t >= 0.0));\n } else if stage == 36u {\n hit.debug_vec_a = vec4<f32>(validity_pre_confidence, validity_post_confidence, confidence, confidence_term);\n hit.debug_vec_b = vec4<f32>(margin_blend, fade, alpha_real, alpha);\n } else if stage == 37u {\n hit.debug_vec_a = vec4<f32>(validity_post_confidence, validity_pre_confidence, confidence, confidence_term);\n hit.debug_vec_b = vec4<f32>(margin_blend, fade, alpha_real, alpha);\n } else if stage == 38u {\n hit.debug_vec_a = vec4<f32>(alpha_real, alpha, alpha, hit.hit_confidence);\n hit.debug_vec_b = vec4<f32>(confidence, confidence_term, validity_post_confidence, margin_blend * fade);\n }\n if !is_finite1(alpha) || !is_finite3(hit_scene_color) || alpha <= 1e-4 {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n let diag_code = select(7.0, debug_diag_code_override, debug_diag_code_override >= 0.0);\n hit.debug_data = vec4<f32>(diag_code, confidence, validity, alpha);\n }\n } else {\n hit.hit_uv = hit_uv;\n hit.hit_confidence = min(alpha, 1.0);\n // Premultiplied contract: tonemapped color is scaled by alpha exactly once,\n // here. Spatial resolve un-premultiplies before untonemapping and main_pass\n // composites rgb directly into env radiance before the split-sum\n // (F0*brdf.x+brdf.y) term \u2014 the fresnel formerly approximated by\n // visible_weight is applied there, consistently with IBL.\n hit.hit_color = tone_map_ssr_color(hit_scene_color) * hit.hit_confidence;\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n hit.debug_data = vec4<f32>(10.0, confidence, validity, hit.hit_confidence);\n }\n }\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n t, t_max, segment_t, screen_ray_dir.xy, t2, hit_scene_color, hit_sample_nr.xyz, hit_sample_nr.w, hit_uv_exact, final_hit_depth,\n ray_hit_pos, scene_hit_pos, travel_m, projected_ray_hit_pos, ray_recon_delta_m, hit_delta_m\n );\n return hit;\n}\n\n// Shared trace body. Two entry points wrap it: production `fs_main` writes only the color\n// target (the 3 debug MRTs cost ~24 B/px of ROP writes at trace res every frame); debug\n// bursts use `fs_main_debug` with the full 4-MRT output for pixel readback.\nfn trace_fragment(frag_coord: vec4<f32>) -> TraceFsOut {\n if u_ssr.trace_config.x <= 0.0 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(frag_coord.xy), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(100.0, 0.0, 0.0, 0.0), hit.debug_vec_a, hit.debug_vec_b));\n }\n\n let half_size_u = textureDimensions(normal_rough_texture);\n let pixel_coords = min(vec2<u32>(frag_coord.xy), half_size_u - vec2<u32>(1u, 1u));\n let half_size = vec2<f32>(half_size_u);\n let uv = (vec2<f32>(pixel_coords) + 0.5) / half_size;\n let depth = textureLoad(repr_depth_texture, vec2<i32>(pixel_coords), 0).r;\n if depth >= 1.0 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(101.0, depth, 0.0, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n\n let normal_rough = textureLoad(normal_rough_texture, vec2<i32>(pixel_coords), 0);\n let roughness = clamp(normal_rough.w, 0.0, 1.0);\n if roughness >= max_roughness() {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(102.0, roughness, max_roughness(), 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n let roughness_mask = 1.0 - smoothstep(min_roughness(), max_roughness(), roughness);\n if roughness_mask <= 1e-4 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(103.0, roughness, roughness_mask, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n\n let world_normal = normalize(normal_rough.xyz);\n let view_normal = normalize((u_camera.view * vec4<f32>(world_normal, 0.0)).xyz);\n let view_pos = view_pos_from_depth(uv, depth);\n\n let world_pos = world_pos_from_depth(uv, depth);\n // Underwater bail (fade_config.y = water plane Y minus eps; -1e30 when no water in view):\n // seafloor SSR is only ever seen through depth-attenuated water refraction, and the water\n // surface itself reflects via the planar pass \u2014 tracing below the plane is wasted work.\n if world_pos.y < u_ssr.fade_config.y {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(104.0, world_pos.y, u_ssr.fade_config.y, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n let geom_normal = compute_geometric_normal(vec2<i32>(pixel_coords), depth, view_pos, vec2<i32>(half_size_u), view_normal);\n let hit = trace_hiz(uv, depth, view_pos, view_normal, geom_normal, roughness);\n store_trace_mip(vec2<i32>(pixel_coords), hit.hit_mip_level);\n if hit.hit_confidence > 1e-4 {\n return trace_fs_out(vec4<f32>(hit.hit_color, hit.hit_confidence), hit);\n }\n return trace_fs_out(vec4<f32>(0.0), hit);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n return trace_fragment(frag_coord).color;\n}\n\n@fragment\nfn fs_main_debug(@builtin(position) frag_coord: vec4<f32>) -> TraceFsOut {\n return trace_fragment(frag_coord);\n}\n"},{"label":"shaders/ssr_filter.wgsl","code":"@group(0) @binding(0) var source_ssr: texture_2d<f32>;\n\nstruct FilterUniform {\n config: vec4<f32>,\n};\n\n@group(0) @binding(1) var<uniform> u_filter: FilterUniform;\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn div_ceil_i32(a: i32, b: i32) -> i32 {\n return (a + b - 1) / b;\n}\n\nconst GAUSS_WEIGHTS: array<f32, 7> = array<f32, 7>(\n 0.07130343,\n 0.13151412,\n 0.18987924,\n 0.21460643,\n 0.18987924,\n 0.13151412,\n 0.07130343,\n);\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn filter_weight(color: vec4<f32>) -> f32 {\n let mip_level = clamp(u_filter.config.x, 0.0, 8.0);\n return mix(clamp(mip_level * 0.2, 0.0, 1.0), 1.0, clamp(color.a, 0.0, 1.0));\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let src_dims = vec2<i32>(textureDimensions(source_ssr));\n let dst_dims = max(vec2<i32>(div_ceil_i32(src_dims.x, 2), div_ceil_i32(src_dims.y, 2)), vec2<i32>(1, 1));\n let dst_px = vec2<i32>(frag_coord.xy);\n let src_center = vec2<f32>(\n ((f32(dst_px.x) + 0.5) * f32(src_dims.x)) / f32(dst_dims.x) - 0.5,\n ((f32(dst_px.y) + 0.5) * f32(src_dims.y)) / f32(dst_dims.y) - 0.5,\n );\n let src_center_px = vec2<i32>(round(src_center));\n var sum = vec4<f32>(0.0);\n var weight_sum = 0.0;\n\n for (var oy = -3; oy <= 3; oy += 1) {\n for (var ox = -3; ox <= 3; ox += 1) {\n let sample_px = clamp(src_center_px + vec2<i32>(ox, oy), vec2<i32>(0), src_dims - vec2<i32>(1, 1));\n let color = textureLoad(source_ssr, sample_px, 0);\n if !is_finite4(color) {\n continue;\n }\n let gaussian_weight = GAUSS_WEIGHTS[u32(ox + 3)] * GAUSS_WEIGHTS[u32(oy + 3)];\n let weight = gaussian_weight * filter_weight(color);\n sum += color * weight;\n weight_sum += weight;\n }\n }\n\n if weight_sum <= 1e-5 {\n return vec4<f32>(0.0);\n }\n return sum / weight_sum;\n}\n"},{"label":"shaders/ssr_spatial_resolve.wgsl","code":"@group(0) @binding(0) var filtered_ssr: texture_2d<f32>;\n@group(0) @binding(1) var trace_mip_level: texture_2d<f32>;\n@group(0) @binding(2) var full_depth: texture_depth_2d;\n@group(0) @binding(3) var full_normal: texture_2d<f32>;\n@group(0) @binding(4) var full_orm: texture_2d<f32>;\n@group(0) @binding(5) var half_depth: texture_2d<f32>;\n@group(0) @binding(6) var half_normal_rough: texture_2d<f32>;\n@group(0) @binding(7) var linear_sampler: sampler;\n\nconst SSR_RESOLVE_DEPTH_WEIGHT: f32 = 2048.0;\nconst SSR_RESOLVE_NORMAL_WEIGHT: f32 = 32.0;\nconst SSR_RESOLVE_ROUGHNESS_WEIGHT: f32 = 16.0;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite1(v: f32) -> bool { return v == v && abs(v) < SSR_FINITE_LIMIT; }\nfn is_finite3(v: vec3<f32>) -> bool { return all(v == v) && all(abs(v) < vec3<f32>(SSR_FINITE_LIMIT)); }\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn untone_map_ssr_color(color: vec3<f32>) -> vec3<f32> {\n let rec709_luminance_weights = vec3<f32>(0.2126, 0.7152, 0.0722);\n return color / max(1.0 - dot(color, rec709_luminance_weights), 1e-4);\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn sample_weight(\n full_depth_value: f32,\n full_normal_value: vec3<f32>,\n full_roughness_value: f32,\n sample_depth_value: f32,\n sample_normal_value: vec3<f32>,\n sample_roughness_value: f32,\n) -> f32 {\n let depth_weight = exp(-abs(full_depth_value - sample_depth_value) * SSR_RESOLVE_DEPTH_WEIGHT);\n let normal_delta = max(0.0, 1.0 - dot(full_normal_value, sample_normal_value));\n let normal_weight = exp(-normal_delta * SSR_RESOLVE_NORMAL_WEIGHT);\n let roughness_weight = exp(-abs(full_roughness_value - sample_roughness_value) * SSR_RESOLVE_ROUGHNESS_WEIGHT);\n return depth_weight * normal_weight * roughness_weight;\n}\n\nfn sample_half_resolve(\n tap_px_unclamped: vec2<i32>,\n bilinear_weight: f32,\n full_depth_value: f32,\n full_normal_value: vec3<f32>,\n full_roughness_value: f32,\n half_dims: vec2<i32>,\n half_dims_f: vec2<f32>,\n) -> vec4<f32> {\n let tap_px = clamp(tap_px_unclamped, vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let sample_depth_value = textureLoad(half_depth, tap_px, 0).r;\n if sample_depth_value >= 1.0 {\n return vec4<f32>(0.0);\n }\n let sample_nr = textureLoad(half_normal_rough, tap_px, 0);\n let sample_normal_value = normalize(sample_nr.xyz);\n let sample_roughness_value = clamp(sample_nr.w, 0.0, 1.0);\n let stored_mip = textureLoad(trace_mip_level, tap_px, 0).x;\n let tap_uv = (vec2<f32>(tap_px) + 0.5) / half_dims_f;\n let ssr_sample = textureSampleLevel(filtered_ssr, linear_sampler, tap_uv, select(0.0, stored_mip, is_finite1(stored_mip)));\n if !is_finite4(ssr_sample) || ssr_sample.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Trace output is premultiplied in tonemapped space. Un-premultiply first: the\n // untonemap curve is nonlinear, so untonemapping the alpha-scaled value would\n // re-weight the color by validity a second time (radiance ~ validity^2, B46's\n // measured 2.4x-dark defect). Then re-premultiply so the weighted sum below stays\n // a premultiplied blend.\n let straight_tm = ssr_sample.rgb / clamp(ssr_sample.a, 1e-5, 1.0);\n let linear_rgb = untone_map_ssr_color(straight_tm);\n if !is_finite3(linear_rgb) {\n return vec4<f32>(0.0);\n }\n let weight = bilinear_weight * sample_weight(\n full_depth_value, full_normal_value, full_roughness_value,\n sample_depth_value, sample_normal_value, sample_roughness_value\n );\n return vec4<f32>(linear_rgb * ssr_sample.a * weight, ssr_sample.a * weight);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let full_dims_u = textureDimensions(full_depth);\n let full_dims = vec2<i32>(full_dims_u);\n let full_px = clamp(vec2<i32>(frag_coord.xy), vec2<i32>(0), full_dims - vec2<i32>(1, 1));\n let full_depth_value = textureLoad(full_depth, full_px, 0);\n if full_depth_value >= 1.0 {\n return vec4<f32>(0.0);\n }\n let full_normal_value = normalize(textureLoad(full_normal, full_px, 0).xyz);\n let full_roughness_value = clamp(textureLoad(full_orm, full_px, 0).g, 0.0, 1.0);\n let half_dims_u = textureDimensions(trace_mip_level);\n let half_dims = vec2<i32>(half_dims_u);\n let half_dims_f = vec2<f32>(half_dims_u);\n\n // Full-res SSR (SsrHigh): trace/spatial are 1:1 with the gbuffer, so the half->full\n // 2x2 bilinear reconstruction collapses to a single weighted tap at the same pixel.\n // Using the original *0.5, /2 math here would sample a shifted, blurred neighborhood.\n if all(half_dims_u == full_dims_u) {\n let s = sample_half_resolve(full_px, 1.0, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n if s.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Output stays premultiplied (linear HDR): rgb already carries alpha.\n return vec4<f32>(s.rgb, clamp(s.a, 0.0, 1.0));\n }\n\n // Half-res SSR (SsrLow): reconstruct back to full via a 2x2 neighborhood.\n let half_tex_coord = (vec2<f32>(full_px) + 0.5) * 0.5;\n let bilinear_weights = fract(half_tex_coord);\n let base_px = (full_px - vec2<i32>(1, 1)) / 2;\n\n let s00 = sample_half_resolve(base_px + vec2<i32>(0, 0), bilinear_weights.x * bilinear_weights.y, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s10 = sample_half_resolve(base_px + vec2<i32>(1, 0), (1.0 - bilinear_weights.x) * bilinear_weights.y, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s01 = sample_half_resolve(base_px + vec2<i32>(0, 1), bilinear_weights.x * (1.0 - bilinear_weights.y), full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s11 = sample_half_resolve(base_px + vec2<i32>(1, 1), (1.0 - bilinear_weights.x) * (1.0 - bilinear_weights.y), full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let sum = s00 + s10 + s01 + s11;\n if sum.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Output stays premultiplied (linear HDR): rgb already carries alpha (bilinear\n // weights sum to 1, so no renormalization is needed).\n return vec4<f32>(sum.rgb, clamp(sum.a, 0.0, 1.0));\n}\n"},{"label":"shaders/ssr_history_copy_normal.wgsl","code":"// ssr_history_copy_normal.wgsl\n@group(0) @binding(0) var normal_texture: texture_2d<f32>;\n\nstruct FSOutput {\n @location(0) color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FSOutput {\n let dims = vec2<i32>(textureDimensions(normal_texture));\n let px = clamp(vec2<i32>(frag_coord.xy), vec2<i32>(0), dims - vec2<i32>(1, 1));\n return FSOutput(textureLoad(normal_texture, px, 0));\n}\n"},{"label":"shaders/ssr_temporal_resolve.wgsl","code":"// ssr_temporal_resolve.wgsl\n// Operates on the spatial-resolve output: rgb is PREMULTIPLIED linear radiance\n// (already scaled by a), a is confidence. All blends below are linear, so they are\n// valid directly on the premultiplied pair.\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n\nstruct SsrHistoryUniform {\n // Current clip -> previous clip, f64-composed on the CPU (B34 pattern): avoids the\n // f32 inverse+forward world round trip whose error grows with distance from the origin.\n reproj: mat4x4<f32>,\n // World -> previous clip; used by ssr_trace.wgsl only (hit-point reprojection).\n prev_view_proj: mat4x4<f32>,\n history_state: vec4<u32>,\n};\n\nstruct SsrSettingsUniform {\n trace_config: vec4<f32>,\n material_config: vec4<f32>,\n fade_config: vec4<f32>,\n ray_fade_config: vec4<f32>,\n debug_config: vec4<f32>,\n};\n\n@group(0) @binding(0) var current_ssr: texture_2d<f32>;\n@group(0) @binding(1) var prev_ssr: texture_2d<f32>;\n@group(0) @binding(2) var current_depth: texture_depth_2d;\n@group(0) @binding(3) var current_normal: texture_2d<f32>;\n@group(0) @binding(4) var current_orm: texture_2d<f32>;\n@group(0) @binding(5) var prev_depth: texture_depth_2d;\n@group(0) @binding(6) var prev_normal: texture_2d<f32>;\n@group(0) @binding(7) var<uniform> u_prev: SsrHistoryUniform;\n@group(0) @binding(8) var<uniform> u_ssr: SsrSettingsUniform;\n@group(0) @binding(9) var linear_sampler: sampler;\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SSR_DEPTH_REJECT_EPSILON: f32 = 0.0025;\nconst SSR_NORMAL_ACCEPT_DOT: f32 = 0.95;\nconst SSR_HISTORY_ONLY_DECAY: f32 = 0.65;\nconst SSR_HISTORY_MOTION_START: f32 = 0.003;\nconst SSR_HISTORY_MOTION_END: f32 = 0.03;\nconst SSR_HISTORY_VISIBLE_START: f32 = 0.08;\nconst SSR_HISTORY_VISIBLE_END: f32 = 0.35;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn sanitize_ssr(v: vec4<f32>) -> vec4<f32> {\n if !is_finite4(v) || v.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n return vec4<f32>(v.rgb, clamp(v.a, 0.0, 1.0));\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let ssr_size = vec2<f32>(textureDimensions(current_ssr));\n let full_size_u = textureDimensions(current_depth);\n let full_size = vec2<f32>(full_size_u);\n let uv = frag_coord.xy / ssr_size;\n let current = sanitize_ssr(textureSampleLevel(current_ssr, linear_sampler, uv, 0.0));\n if u_prev.history_state.x == 0u {\n return current;\n }\n\n let full_px = vec2<i32>(min(vec2<u32>(floor(uv * full_size)), full_size_u - vec2<u32>(1u, 1u)));\n let full_uv = (vec2<f32>(full_px) + 0.5) / full_size;\n let depth = textureLoad(current_depth, full_px, 0);\n if depth >= 1.0 {\n return current;\n }\n\n let current_roughness = clamp(textureLoad(current_orm, full_px, 0).g, 0.0, 1.0);\n let current_roughness_mask = 1.0 - smoothstep(u_ssr.material_config.x, u_ssr.material_config.y, current_roughness);\n if current_roughness_mask <= 1e-4 {\n return current;\n }\n\n let world_normal = normalize(textureLoad(current_normal, full_px, 0).xyz);\n // Reproject straight in clip space: (ndc, depth, 1) is the true clip position up to the\n // unknown w, which the homogeneous divide below cancels.\n let ndc = vec4<f32>(full_uv.x * 2.0 - 1.0, (1.0 - full_uv.y) * 2.0 - 1.0, depth, 1.0);\n let prev_clip = u_prev.reproj * ndc;\n if prev_clip.w <= 0.0 {\n return current;\n }\n\n let prev_ndc = prev_clip.xyz / prev_clip.w;\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 1.0 - (prev_ndc.y * 0.5 + 0.5));\n if any(prev_uv <= vec2<f32>(0.0)) || any(prev_uv >= vec2<f32>(1.0)) {\n return current;\n }\n let motion_factor = 1.0 - smoothstep(SSR_HISTORY_MOTION_START, SSR_HISTORY_MOTION_END, distance(prev_uv, full_uv));\n if motion_factor <= 1e-3 {\n return current;\n }\n\n let prev_px = vec2<i32>(min(vec2<u32>(floor(prev_uv * full_size)), full_size_u - vec2<u32>(1u, 1u)));\n let prev_depth_value = textureLoad(prev_depth, prev_px, 0);\n if prev_depth_value >= 1.0 || abs(prev_depth_value - prev_ndc.z) > SSR_DEPTH_REJECT_EPSILON {\n return current;\n }\n\n let prev_world_normal = normalize(textureLoad(prev_normal, prev_px, 0).xyz);\n if dot(world_normal, prev_world_normal) < SSR_NORMAL_ACCEPT_DOT {\n return current;\n }\n\n let history = sanitize_ssr(textureSampleLevel(prev_ssr, linear_sampler, prev_uv, 0.0));\n let history_conf = clamp(history.a * motion_factor, 0.0, 1.0);\n if history_conf <= 1e-4 {\n return current;\n }\n let history_visible = smoothstep(SSR_HISTORY_VISIBLE_START, SSR_HISTORY_VISIBLE_END, history_conf);\n let history_rgb = history.rgb * history_visible;\n\n let current_conf = clamp(current.a, 0.0, 1.0);\n let history_mix = clamp((0.15 + 0.40 * history_conf / max(current_conf + history_conf, 1e-4)) * motion_factor, 0.0, 0.55);\n if current_conf <= 1e-4 {\n return vec4<f32>(history_rgb * SSR_HISTORY_ONLY_DECAY, history_conf * SSR_HISTORY_ONLY_DECAY);\n } else {\n return vec4<f32>(mix(current.rgb, history_rgb, history_mix), max(current_conf, history_conf * 0.85));\n }\n}\n"},{"label":"shaders/bloom.wgsl","code":"// bloom.wgsl\nstruct BloomSettings {\n threshold: f32,\n soft_knee: f32,\n intensity: f32,\n clamp_value: f32,\n downsample_offset:f32,\n upsample_offset: f32,\n _pad0: f32,\n _pad1: f32,\n};\n\n@group(0) @binding(0) var tex_a: texture_2d<f32>;\n@group(0) @binding(1) var tex_b: texture_2d<f32>; // only used by upsample\n@group(0) @binding(2) var samp_linear: sampler;\n@group(0) @binding(3) var<uniform> settings: BloomSettings;\n\n// Fullscreen triangle\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n // 3-vertex fullscreen tri. Clip positions: (-1,-1),(3,-1),(-1,3)\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let pos = vec2<f32>(px, py) * 2.0 - 1.0; // -1..3 pattern\n o.pos = vec4<f32>(pos, 0.0, 1.0);\n o.uv = vec2<f32>(px, py); // Fullscreen triangle: verts use {0,2}; inside viewport uv interpolates to [0,1], so no *0.5 needed\n return o;\n}\n\n// Helpers\nfn soft_threshold(bright: f32, thresh: f32, knee_frac: f32) -> f32 {\n let knee = max(thresh * knee_frac, 1e-5);\n return smoothstep(thresh - knee, thresh + knee, bright);\n}\nfn flip_uv(v: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(v.x, 1.0 - v.y);\n}\nfn kawase4(tex: texture_2d<f32>, smp: sampler, uv: vec2<f32>, texel: vec2<f32>, offset: f32) -> vec3<f32> {\n let d = texel * offset;\n var s = vec3<f32>(0.0);\n s += textureSample(tex, smp, uv + vec2<f32>( d.x, d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>(-d.x, d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>( d.x, -d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>(-d.x, -d.y)).rgb;\n return s * 0.25;\n}\n\n// Prefilter (full -> 1/2)\n@fragment\nfn fs_prefilter(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let uvf = flip_uv(uv);\n var c = textureSample(tex_a, samp_linear, uvf);\n let b = max(c.r, max(c.g, c.b));\n let w = soft_threshold(b, settings.threshold, settings.soft_knee);\n\n var outc = c * w;\n if (settings.clamp_value > 0.0) {\n outc = vec4<f32>(min(outc.rgb, vec3<f32>(settings.clamp_value)), outc.a);\n }\n outc.a = 1.0;\n return outc;\n}\n\n// Kawase downsample (1/2 -> 1/4 -> ...)\n@fragment\nfn fs_down(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let dim = textureDimensions(tex_a);\n let texel = 1.0 / vec2<f32>(f32(dim.x), f32(dim.y));\n let uvf = flip_uv(uv);\n let rgb = kawase4(tex_a, samp_linear, uvf, texel, settings.downsample_offset);\n return vec4<f32>(rgb, 1.0);\n}\n\n// Kawase upsample with additive ping ( ... -> 1/4 -> 1/2 )\n@fragment\nfn fs_up(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let dim = textureDimensions(tex_a);\n let texel = 1.0 / vec2<f32>(f32(dim.x), f32(dim.y));\n\n let uvf = flip_uv(uv);\n let up_rgb = kawase4(tex_a, samp_linear, uvf, texel, settings.upsample_offset);\n let add_rgb = textureSample(tex_b, samp_linear, uvf).rgb;\n\n var out_rgb = up_rgb + add_rgb;\n if (settings.clamp_value > 0.0) {\n out_rgb = min(out_rgb, vec3<f32>(settings.clamp_value));\n }\n return vec4<f32>(out_rgb, 1.0);\n}\n\n// The chain ends at half-res up[0]; tonemap.wgsl samples it and applies\n// intensity/clamp (bloom_params) \u2014 no full-res upsample/composite entry points.\n"},{"label":"volumetric_light","code":"// Half-res raymarched sun shafts (god rays): march camera->surface against the\n// directional shadow cascades, Henyey-Greenstein in-scatter, additive composite\n// upstream (volumetric_light_upsample.wgsl). Groups 1/2 declare subsets of the\n// shared dir-light/camera layouts (subset-of-layout declaration is sanctioned;\n// see ocean_surface.wgsl); group 0/3 are pass-owned.\n\nstruct ShadowCascadeData {\n light_view_proj: mat4x4<f32>,\n split_depth: f32,\n _padding: vec3<f32>,\n};\nstruct DirectionalLightShadowUniform {\n cascade_data: array<ShadowCascadeData, 4>,\n light_dir: vec4<f32>,\n light_color_with_intensity: vec4<f32>,\n ambient_tint: vec4<f32>,\n cascade_count: u32,\n shadow_opacity: f32,\n light_debug_mode: u32,\n shadow_blur_radius: f32,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\nstruct VolumetricLightSettings {\n // x = intensity, y = density per meter, z = steps, w = max march distance m\n params0: vec4<f32>,\n // x = HG anisotropy g, y = frame index (dither rotation), z = shadow distance m\n // (march band; beyond it visibility is treated as lit), w = height fade top Y m\n // (0 = no height fade).\n params1: vec4<f32>,\n};\n\n@group(0) @binding(0) var depth_texture: texture_depth_2d;\n\n@group(1) @binding(0) var<uniform> u_directional_light: DirectionalLightShadowUniform;\n@group(1) @binding(1) var shadow_map_array: texture_depth_2d_array;\n@group(1) @binding(2) var shadow_sampler: sampler_comparison;\n\n@group(2) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(3) @binding(0) var<uniform> u_vol: VolumetricLightSettings;\n\nstruct VSOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_id: u32) -> VSOut {\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>(-1.0, 1.0),\n vec2<f32>( 3.0, 1.0),\n );\n let pos = positions[vertex_id];\n var out: VSOut;\n out.pos = vec4<f32>(pos, 0.0, 1.0);\n out.uv = pos * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);\n return out;\n}\n\n// One-tap cascade visibility at a world point. Level form (no implicit derivatives)\n// because it runs inside the march loop; strict select-chain cascade pick because the\n// browser validators reject the break-based loop under non-uniform flow.\nfn march_visibility(world_pos: vec3<f32>) -> f32 {\n let view_z = -(u_camera.view * vec4<f32>(world_pos, 1.0)).z;\n var cascade_index = u_directional_light.cascade_count - 1u;\n let overlap = 0.5;\n var found = false;\n for (var i = 0u; i < u_directional_light.cascade_count; i = i + 1u) {\n let cond = view_z <= (u_directional_light.cascade_data[i].split_depth + overlap);\n let use_i = select(false, true, cond && !found);\n cascade_index = select(cascade_index, i, use_i);\n found = found || use_i;\n }\n let clip_pos =\n u_directional_light.cascade_data[cascade_index].light_view_proj * vec4<f32>(world_pos, 1.0);\n let ndc = clip_pos.xyz / max(clip_pos.w, 1e-6);\n let uv = vec2<f32>(ndc.x, -ndc.y) * 0.5 + vec2<f32>(0.5);\n if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {\n return 1.0; // outside every cascade: treat as lit\n }\n // Clamp the compare ref (see main_pass: beyond-far ndc.z > 1 would read a giant\n // false-shadow box against the cleared map).\n let depth = clamp(ndc.z, 0.0, 1.0);\n return textureSampleCompareLevel(shadow_map_array, shadow_sampler, uv, cascade_index, depth);\n}\n\n// Interleaved gradient noise: per-pixel march offset, rotated per frame so the\n// temporal upscalers resolve the dither into a smooth shaft.\nfn ign(px: vec2<f32>, frame: f32) -> f32 {\n let p = px + fract(frame * 0.618034) * 5.588238;\n return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));\n}\n\nfn henyey_greenstein(cos_theta: f32, g: f32) -> f32 {\n let g2 = g * g;\n let denom = 1.0 + g2 - 2.0 * g * cos_theta;\n return (1.0 - g2) / (12.566371 * pow(max(denom, 1e-4), 1.5));\n}\n\n@fragment\nfn fs_march(in: VSOut) -> @location(0) vec4<f32> {\n let depth_size = vec2<f32>(textureDimensions(depth_texture));\n let px = clamp(\n vec2<i32>(in.uv * depth_size),\n vec2<i32>(0, 0),\n vec2<i32>(depth_size) - vec2<i32>(1, 1),\n );\n let raw_depth = textureLoad(depth_texture, px, 0);\n\n // Reconstruct the surface point (sky depth 1.0 lands past far; distance clamps below).\n let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, raw_depth, 1.0);\n let world_h = u_camera.inverse_view_proj * ndc;\n let world_pos = world_h.xyz / max(world_h.w, 1e-6);\n\n let ray_o = u_camera.camera_position;\n let to_surface = world_pos - ray_o;\n let surface_dist = length(to_surface);\n let ray_d = to_surface / max(surface_dist, 1e-4);\n\n let intensity = u_vol.params0.x;\n let density = u_vol.params0.y;\n let steps = max(u_vol.params0.z, 4.0);\n let max_dist = max(u_vol.params0.w, 1.0);\n let g = u_vol.params1.x;\n let shadow_dist = max(u_vol.params1.z, 1.0);\n let height_top = u_vol.params1.w;\n\n let total_len = min(surface_dist, max_dist);\n // March only the cascade-covered band at full step density; the segment beyond\n // shadow_distance has no occlusion data and integrates as lit haze below.\n let march_len = min(total_len, shadow_dist);\n let dt = march_len / steps;\n let sun_l = normalize(-u_directional_light.light_dir.xyz);\n let phase = henyey_greenstein(dot(ray_d, sun_l), g);\n\n var t = (ign(in.pos.xy, u_vol.params1.y) + 0.5) * dt;\n var acc = 0.0;\n var transmittance = 1.0;\n let step_count = i32(steps);\n for (var i = 0; i < step_count; i = i + 1) {\n let p = ray_o + ray_d * t;\n var sigma = density;\n if (height_top > 0.0) {\n // Ground-hugging falloff so shafts sit low instead of tinting the sky band.\n sigma *= clamp(1.0 - p.y / height_top, 0.0, 1.0);\n }\n let vis = march_visibility(p);\n let absorb = exp(-sigma * dt);\n acc += transmittance * vis * sigma * dt;\n transmittance *= absorb;\n t += dt;\n }\n // Analytic lit tail beyond the shadow band (uniform haze; no occlusion data there).\n let tail_len = max(total_len - march_len, 0.0);\n if (tail_len > 0.0) {\n var sigma = density;\n if (height_top > 0.0) {\n let p = ray_o + ray_d * (march_len + tail_len * 0.5);\n sigma *= clamp(1.0 - p.y / height_top, 0.0, 1.0);\n }\n let tail_absorb = exp(-sigma * tail_len);\n acc += transmittance * (1.0 - tail_absorb);\n transmittance *= tail_absorb;\n }\n\n let scatter =\n acc * phase * u_directional_light.light_color_with_intensity.rgb * intensity;\n return vec4<f32>(scatter, 1.0 - transmittance);\n}\n"},{"label":"volumetric_light_upsample","code":"// Depth-aware bilateral upsample of the half-res volumetric target, composited\n// ADDITIVELY onto hdr_scene (blend state One/One set pipeline-side). Bilateral\n// weighting mirrors particle_composite.wgsl so shaft edges don't bleed across\n// geometry silhouettes.\n\nstruct VSOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var volumetric_half: texture_2d<f32>;\n@group(0) @binding(1) var depth_texture: texture_depth_2d;\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_id: u32) -> VSOut {\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>(-1.0, 1.0),\n vec2<f32>( 3.0, 1.0),\n );\n let pos = positions[vertex_id];\n var out: VSOut;\n out.pos = vec4<f32>(pos, 0.0, 1.0);\n out.uv = pos * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);\n return out;\n}\n\nconst DEPTH_REJECT: f32 = 5000.0;\n\nfn tap_depth(px: vec2<i32>, half_size: vec2<f32>, depth_size: vec2<f32>, depth_max_px: vec2<i32>) -> f32 {\n let uv = (vec2<f32>(px) + 0.5) / half_size;\n let dpx = clamp(vec2<i32>(floor(uv * depth_size)), vec2<i32>(0, 0), depth_max_px);\n let d = textureLoad(depth_texture, dpx, 0);\n return select(d, 0.0, d >= 1.0);\n}\n\n@fragment\nfn fs_main(in: VSOut) -> @location(0) vec4<f32> {\n let half_size_u = textureDimensions(volumetric_half);\n let half_size = vec2<f32>(half_size_u);\n let depth_size = vec2<f32>(textureDimensions(depth_texture));\n let depth_max_px = vec2<i32>(depth_size) - vec2<i32>(1, 1);\n let full_px = clamp(vec2<i32>(in.pos.xy), vec2<i32>(0, 0), depth_max_px);\n let center_raw = textureLoad(depth_texture, full_px, 0);\n let center_depth = select(center_raw, 0.0, center_raw >= 1.0);\n\n let pos_h = in.uv * half_size - 0.5;\n let base = vec2<i32>(floor(pos_h));\n let f = pos_h - floor(pos_h);\n let bilinear = vec4<f32>(\n (1.0 - f.x) * (1.0 - f.y), f.x * (1.0 - f.y),\n (1.0 - f.x) * f.y, f.x * f.y,\n );\n let half_max_px = vec2<i32>(half_size_u) - vec2<i32>(1, 1);\n var color = vec4<f32>(0.0);\n var w_sum = 0.0;\n for (var i = 0; i < 4; i++) {\n let px = clamp(base + vec2<i32>(i & 1, i >> 1), vec2<i32>(0, 0), half_max_px);\n let d = tap_depth(px, half_size, depth_size, depth_max_px);\n let w = bilinear[i] / (1.0 + DEPTH_REJECT * abs(d - center_depth));\n color += textureLoad(volumetric_half, px, 0) * w;\n w_sum += w;\n }\n let resolved = color / max(w_sum, 1e-6);\n // Additive target: rgb adds in-scatter; alpha is unused by the blend (Zero/One).\n return vec4<f32>(resolved.rgb, 0.0);\n}\n"},{"label":"shaders/particle_composite.wgsl","code":"// particle_composite.wgsl\n// Depth-aware (nearest-depth bilateral) upsample of the half-res particle accumulation target.\n// Each full-res pixel re-weights its 4 bilinear taps by scene-depth similarity, so particle color\n// that was depth-clipped on one side of a geometry edge doesn't bleed across it (halo fix).\n\nstruct VSOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var particleColor: texture_2d<f32>;\n@group(0) @binding(1) var depthTexture: texture_2d<f32>;\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_id: u32) -> VSOut {\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>(-1.0, 1.0),\n vec2<f32>( 3.0, 1.0),\n );\n let pos = positions[vertex_id];\n var out: VSOut;\n out.pos = vec4<f32>(pos, 0.0, 1.0);\n out.uv = pos * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);\n return out;\n}\n\nstruct FSOut {\n @location(0) color: vec4<f32>,\n @location(1) reactive: f32,\n};\n\n// Bilateral edge-stopping strength in the R32 scene-depth encoding (clip.z / clip.w, 0 = cleared).\n// Same-surface taps keep ~bilinear weight; across-edge taps are strongly suppressed.\nconst DEPTH_REJECT: f32 = 5000.0;\n\n// Scene depth a half-res texel was rasterized against: same texel-center -> full-res depth texel\n// mapping the particle render shader uses for its manual depth test.\n// depthTexture is the gbuffer depth (sky = 1.0); remap to the retired R32F attachment's\n// cleared-0 convention so sky-adjacent bilateral edge stopping keeps its strength.\nfn tap_depth(px: vec2<i32>, half_size: vec2<f32>, depth_size: vec2<f32>, depth_max_px: vec2<i32>) -> f32 {\n let uv = (vec2<f32>(px) + 0.5) / half_size;\n let dpx = clamp(vec2<i32>(floor(uv * depth_size)), vec2<i32>(0, 0), depth_max_px);\n let d = textureLoad(depthTexture, dpx, 0).r;\n return select(d, 0.0, d >= 1.0);\n}\n\n@fragment\nfn fs_main(in: VSOut) -> FSOut {\n let half_size_u = textureDimensions(particleColor);\n let half_size = vec2<f32>(half_size_u);\n let depth_size = vec2<f32>(textureDimensions(depthTexture));\n let depth_max_px = vec2<i32>(depth_size) - vec2<i32>(1, 1);\n let full_px = clamp(vec2<i32>(in.pos.xy), vec2<i32>(0, 0), depth_max_px);\n // Same sky remap as tap_depth so sky-vs-sky taps keep full weight.\n let center_raw = textureLoad(depthTexture, full_px, 0).r;\n let center_depth = select(center_raw, 0.0, center_raw >= 1.0);\n\n // Bilinear footprint in half-res texel space.\n let pos_h = in.uv * half_size - 0.5;\n let base = vec2<i32>(floor(pos_h));\n let f = pos_h - floor(pos_h);\n let bilinear = vec4<f32>(\n (1.0 - f.x) * (1.0 - f.y), f.x * (1.0 - f.y),\n (1.0 - f.x) * f.y, f.x * f.y,\n );\n let half_max_px = vec2<i32>(half_size_u) - vec2<i32>(1, 1);\n // particleColor is premultiplied RGBA, so re-weighting stays a valid premultiplied blend.\n var color = vec4<f32>(0.0);\n var w_sum = 0.0;\n for (var i = 0; i < 4; i++) {\n let px = clamp(base + vec2<i32>(i & 1, i >> 1), vec2<i32>(0, 0), half_max_px);\n let d = tap_depth(px, half_size, depth_size, depth_max_px);\n let w = bilinear[i] / (1.0 + DEPTH_REJECT * abs(d - center_depth));\n color += textureLoad(particleColor, px, 0) * w;\n w_sum += w;\n }\n var out: FSOut;\n // All-rejected fallback degrades to plain bilinear via normalization.\n out.color = color / max(w_sum, 1e-6);\n // Temporal reactivity: alpha catches blended particles, luminance catches additive ones\n // (alpha=0 in the accumulation target). Capped below 1 per FFX guidance (max-blended).\n let luma = dot(out.color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n out.reactive = min(max(out.color.a, luma * 0.7), 0.9);\n return out;\n}\n"},{"label":"shaders/main_pass_iphone.wgsl","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// main_pass_iphone.wgsl\n// Source-sliced iPhone Safari main pass: PBR/IBL, clustered lights, directional shadows,\n// SSAO, SSR, and local shadows. Desktop-only debug branches stay out.\nstruct ShadowCascadeData {\n light_view_proj: mat4x4<f32>,\n split_depth: f32,\n _padding: vec3<f32>,\n};\nstruct DirectionalLightShadowUniform {\n cascade_data: array<ShadowCascadeData, 4>,\n light_dir: vec4<f32>,\n light_color_with_intensity: vec4<f32>,\n ambient_tint: vec4<f32>,\n cascade_count: u32,\n shadow_opacity: f32,\n light_debug_mode: u32,\n shadow_blur_radius: f32,\n local_shadow_blur_radius: f32,\n sun_specular_scale: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n _pad3: f32,\n _pad4: f32,\n _pad5: f32,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\nstruct PointLight { position_radius: vec4<f32>, color_intensity: vec4<f32>, diffuse_spec_scale: vec4<f32>, };\nstruct SpotLight { position_range: vec4<f32>, color_intensity: vec4<f32>, dir_cos_inner: vec4<f32>, cos_outer_pad: vec4<f32>, };\nstruct AreaLight { center_radius: vec4<f32>, color_intensity: vec4<f32>, axis_u_half: vec4<f32>, axis_v_half: vec4<f32>, diffuse_spec_scale: vec4<f32>, };\nstruct ClusterLightUniform {\n view: mat4x4<f32>,\n view_proj: mat4x4<f32>,\n proj_scale: vec2<f32>,\n z_params: vec2<f32>,\n tile_counts: vec4<u32>,\n inv_screen_size: vec2<f32>,\n num_point_lights: u32,\n num_spot_lights: u32,\n num_area_lights: u32,\n};\nstruct ShadowView {\n lightViewProj: mat4x4<f32>,\n atlasRectMin: vec2<f32>,\n atlasRectMax: vec2<f32>,\n slice: u32,\n typeFace: u32,\n depthBias: f32,\n slopeBias: f32,\n};\nstruct LocalLightShadowIndex { first: u32, count: u32, };\n\n@group(0) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(0) @binding(1) var normal_texture: texture_2d<f32>;\n@group(0) @binding(2) var orm_texture: texture_2d<f32>;\n@group(0) @binding(3) var depth_texture: texture_depth_2d;\n@group(0) @binding(4) var ssao_texture: texture_2d<f32>;\n@group(0) @binding(5) var g_sampler: sampler;\n@group(0) @binding(6) var noise_texture: texture_2d<f32>;\n@group(0) @binding(7) var noise_sampler: sampler;\n@group(0) @binding(8) var ssr_resolved_texture: texture_2d<f32>;\n// Fog folded from the retired standalone fog pass.\nstruct FogSettings {\n color: vec3<f32>, // linear HDR, pre-tonemap\n mode: u32, // 0: Linear, 1: Exp, 2: Exp2\n start: f32, // Linear mode only\n end_: f32, // Linear mode only\n density: f32, // Exp / Exp2 only\n height_enabled: u32,\n height_weight: f32, // 0..1\n height_bottom: f32, // world Y band\n height_top: f32,\n height_softness: f32,\n sky_affect: f32,\n};\n@group(0) @binding(9) var<uniform> u_fog: FogSettings;\n// Shore wetness band (see main_pass.wgsl): x = strength (0 = off), y = sea level Y m,\n// z = band height above sea level, w = roughness drop 0..1.\nstruct ShoreParams {\n a: vec4<f32>, // strength (0 = off), sea level Y m, band height m, roughness drop\n waves: array<vec4<f32>, 8>, // Gerstner set: dir.xy, amp m, k rad/m (matches the ocean)\n phases: array<vec4<f32>, 2>, // 8 phases, packed 4 per vec4\n grid: vec4<f32>, // wet-grid window center xz, 1/extent, enabled\n};\n@group(0) @binding(10) var<uniform> u_shore: ShoreParams;\n// Ocean ripple grid; alpha = dynamic wet height (highest recent water level per world\n// cell, drying over seconds). World-anchored toroidal addressing, so a plain fract\n// lookup lands on the right texel; textureLoad avoids needing a sampler.\n@group(0) @binding(12) var ripple_wet_tex: texture_2d<f32>;\n@group(0) @binding(13) var caustic_tex: texture_2d<f32>;\n\n// x = current ripple/splash height at this cell (lifts the meniscus with wakes and\n// splashes), y = dynamic wet height (recent high-water mark, drying over seconds).\nfn caustic_texel(t: vec2<i32>) -> f32 {\n return textureLoad(caustic_tex, (t % 256 + vec2<i32>(256)) % 256, 0).r;\n}\n// Manual bilinear over the periodic 256 tile; p2 in tile units (1.0 = one repeat).\nfn caustic_bilinear(p2: vec2<f32>) -> f32 {\n let g = p2 * 256.0 - 0.5;\n let b = vec2<i32>(floor(g));\n let f = g - floor(g);\n return mix(\n mix(caustic_texel(b), caustic_texel(b + vec2<i32>(1, 0)), f.x),\n mix(caustic_texel(b + vec2<i32>(0, 1)), caustic_texel(b + vec2<i32>(1, 1)), f.x),\n f.y,\n );\n}\n\nfn shore_dynamic_wet_texel(t: vec2<i32>) -> vec4<f32> {\n // Toroidal wrap (world-anchored grid, uv = world/extent mod 1).\n let w = (t % 512 + vec2<i32>(512)) % 512;\n return textureLoad(ripple_wet_tex, w, 0);\n}\nfn shore_dynamic_wet(world_xz: vec2<f32>) -> vec2<f32> {\n // Manual bilinear over the 4 neighboring texels: textureLoad is nearest-texel, and\n // on FLAT ground near the wet threshold the per-texel wet/dry decision renders as\n // 31cm blocks (512 texels / 160m). Sloped beaches hid it; flat wading shelves don't.\n let g = world_xz * u_shore.grid.z * 512.0 - 0.5;\n let base = vec2<i32>(floor(g));\n let f = g - floor(g);\n let t00 = shore_dynamic_wet_texel(base);\n let t10 = shore_dynamic_wet_texel(base + vec2<i32>(1, 0));\n let t01 = shore_dynamic_wet_texel(base + vec2<i32>(0, 1));\n let t11 = shore_dynamic_wet_texel(base + vec2<i32>(1, 1));\n let t = mix(mix(t00, t10, f.x), mix(t01, t11, f.x), f.y);\n return vec2<f32>(t.r, t.a);\n}\n\n// Exact vertical Gerstner sum (same convention as ocean_displace): the waterline on\n// geometry lands on the same crests the ocean surface renders. Callers gate on being\n// near sea level, so the 8 sins only run in the shore band.\nfn shore_waterline_y(world_xz: vec2<f32>) -> f32 {\n var y = u_shore.a.y;\n for (var i = 0u; i < 8u; i++) {\n let w = u_shore.waves[i];\n let phase = u_shore.phases[i / 4u][i % 4u];\n y += w.z * sin(w.w * dot(w.xy, world_xz) - phase);\n }\n return y;\n}\n\nfn shore_wet_factor(world_y: f32, waterline_y: f32) -> f32 {\n let h = world_y - waterline_y;\n let top = u_shore.a.z;\n return smoothstep(-2.0, -1.0, h) * (1.0 - smoothstep(top * 0.75, top, h));\n}\n@group(1) @binding(0) var<uniform> u_directional_light: DirectionalLightShadowUniform;\n@group(1) @binding(1) var shadow_map_array: texture_depth_2d_array;\n@group(1) @binding(2) var shadow_sampler: sampler_comparison;\n@group(2) @binding(0) var<uniform> u_camera: CameraUniform;\n@group(3) @binding(0) var ibl_prefiltered_specular: texture_cube<f32>;\n@group(3) @binding(1) var ibl_irradiance: texture_cube<f32>;\n@group(3) @binding(2) var brdf_lut: texture_2d<f32>;\n@group(3) @binding(3) var ibl_sampler: sampler;\n@group(3) @binding(4) var<storage, read> point_lights: array<PointLight>;\n@group(3) @binding(5) var<storage, read> spot_lights: array<SpotLight>;\n@group(3) @binding(6) var<storage, read> area_lights: array<AreaLight>;\n@group(3) @binding(7) var<uniform> u_cluster_lights: ClusterLightUniform;\n@group(3) @binding(8) var<storage, read> cluster_light_count_fragment: array<u32>;\n@group(3) @binding(9) var<storage, read> cluster_light_indices: array<u32>;\n@group(3) @binding(10) var localShadowAtlas: texture_depth_2d;\n@group(3) @binding(11) var<storage, read> shadowViews: array<ShadowView>;\n@group(3) @binding(12) var<storage, read> pointShadowIndex: array<LocalLightShadowIndex>;\n@group(3) @binding(13) var<storage, read> spotShadowIndex: array<LocalLightShadowIndex>;\n\nconst PI: f32 = 3.141592653589793238;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\nconst SPOT_TYPE_BIT: u32 = 0x80000000u;\nconst AREA_TYPE_BIT: u32 = 0x40000000u;\nconst TYPE_BITS: u32 = SPOT_TYPE_BIT | AREA_TYPE_BIT;\nconst SPEC_RADIUS_SCALE: f32 = 1.25;\nconst POISSON9: array<vec2<f32>, 9> = array<vec2<f32>, 9>(\n vec2<f32>(-0.026, -0.240), vec2<f32>( 0.234, -0.130), vec2<f32>(-0.150, 0.126),\n vec2<f32>( 0.140, 0.234), vec2<f32>( 0.030, -0.070), vec2<f32>(-0.076, -0.020),\n vec2<f32>(-0.230, 0.160), vec2<f32>( 0.190, -0.200), vec2<f32>( 0.220, 0.070),\n);\n\nfn fresnel_schlick(cos_theta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);\n}\nfn distribution_ggx(NdotH: f32, alpha: f32) -> f32 {\n let a2 = alpha * alpha;\n let denom = NdotH * NdotH * (a2 - 1.0) + 1.0;\n return a2 / (PI * denom * denom);\n}\nfn geometry_smith(NdotV: f32, NdotL: f32, alpha: f32) -> f32 {\n let a2 = alpha * alpha;\n let gv = NdotV * sqrt(a2 + (1.0 - a2) * NdotL * NdotL);\n let gl = NdotL * sqrt(a2 + (1.0 - a2) * NdotV * NdotV);\n return (2.0 * NdotV * NdotL) / max(gv + gl, 1e-4);\n}\nfn sanitize_ssr(v: vec4<f32>) -> vec4<f32> {\n if !(all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT))) || v.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n return vec4<f32>(v.rgb, clamp(v.a, 0.0, 1.0));\n}\nfn smootherstep(a: f32, b: f32, x: f32) -> f32 {\n let t = clamp((x - a) / max(1e-6, b - a), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\nfn rotate_offset(base_offset: vec2<f32>, angle: f32) -> vec2<f32> {\n let s = sin(angle);\n let c = cos(angle);\n let rot = mat2x2<f32>(\n vec2<f32>( c, -s),\n vec2<f32>( s, c),\n );\n return rot * base_offset;\n}\nfn cubeface_from_dir(d: vec3<f32>) -> u32 {\n let a = abs(d);\n if a.x >= a.y && a.x >= a.z {\n return select(1u, 0u, d.x >= 0.0);\n } else if a.y >= a.x && a.y >= a.z {\n return select(3u, 2u, d.y >= 0.0);\n }\n return select(5u, 4u, d.z >= 0.0);\n}\nfn cube_uv_m11_from_dir_on_face(d: vec3<f32>, face: u32) -> vec2<f32> {\n let a = abs(d);\n if face == 0u { return vec2<f32>(-d.z / max(a.x, 1e-6), -d.y / max(a.x, 1e-6)); }\n if face == 1u { return vec2<f32>( d.z / max(a.x, 1e-6), -d.y / max(a.x, 1e-6)); }\n if face == 2u { return vec2<f32>( d.x / max(a.y, 1e-6), d.z / max(a.y, 1e-6)); }\n if face == 3u { return vec2<f32>( d.x / max(a.y, 1e-6), -d.z / max(a.y, 1e-6)); }\n if face == 4u { return vec2<f32>( d.x / max(a.z, 1e-6), -d.y / max(a.z, 1e-6)); }\n return vec2<f32>(-d.x / max(a.z, 1e-6), -d.y / max(a.z, 1e-6));\n}\nfn dir_from_cube_face_uv_m11(face: u32, uv: vec2<f32>) -> vec3<f32> {\n let u = uv.x;\n let v = uv.y;\n if face == 0u { return normalize(vec3<f32>( 1.0, -v, -u)); }\n if face == 1u { return normalize(vec3<f32>(-1.0, -v, u)); }\n if face == 2u { return normalize(vec3<f32>( u, 1.0, v)); }\n if face == 3u { return normalize(vec3<f32>( u, -1.0, -v)); }\n if face == 4u { return normalize(vec3<f32>( u, -v, 1.0)); }\n return normalize(vec3<f32>(-u, -v, -1.0));\n}\nfn local_shadow_compare_single(sv: ShadowView, world_pos: vec3<f32>) -> f32 {\n let lp = sv.lightViewProj * vec4<f32>(world_pos, 1.0);\n let ndc = lp.xyz / lp.w;\n let uv_l = ndc.xy * 0.5 + vec2<f32>(0.5);\n let uv = vec2<f32>(uv_l.x, 1.0 - uv_l.y);\n let uv_atlas = mix(sv.atlasRectMin, sv.atlasRectMax, uv);\n let ref_z = clamp(ndc.z - sv.depthBias, 0.0, 1.0);\n let in_rect = all(uv_atlas >= sv.atlasRectMin) && all(uv_atlas <= sv.atlasRectMax);\n let lit = textureSampleCompareLevel(localShadowAtlas, shadow_sampler, uv_atlas, ref_z);\n return select(select(1.0, lit, in_rect), 1.0, ndc.z > 1.0);\n}\nfn sample_local_shadow_spot(sv: ShadowView, world_pos: vec3<f32>, frag_coord: vec2<f32>) -> f32 {\n let dims_i = textureDimensions(localShadowAtlas);\n let dims = vec2<f32>(f32(dims_i.x), f32(dims_i.y));\n let rect_px = (sv.atlasRectMax - sv.atlasRectMin) * dims;\n let texel = 1.0 / max(rect_px, vec2<f32>(1.0));\n let rect_min = min(rect_px.x, rect_px.y);\n let pcf_radius = min(min(u_directional_light.local_shadow_blur_radius, 64.0), 0.25 * rect_min);\n let n01 = textureSampleLevel(noise_texture, noise_sampler, frag_coord / 32.0, 0.0).rg;\n let angle = atan2((n01 * 2.0 - 1.0).y, (n01 * 2.0 - 1.0).x);\n let lp = sv.lightViewProj * vec4<f32>(world_pos, 1.0);\n let ndc = lp.xyz / lp.w;\n let uv_l = ndc.xy * 0.5 + vec2<f32>(0.5);\n let uv = vec2<f32>(uv_l.x, 1.0 - uv_l.y);\n let uv_atlas = mix(sv.atlasRectMin, sv.atlasRectMax, uv);\n let ref_z = clamp(ndc.z - sv.depthBias, 0.0, 1.0);\n\n var sum = 0.0;\n for (var i = 0; i < 9; i = i + 1) {\n let uv_tap = uv_atlas + rotate_offset(POISSON9[i], angle) * pcf_radius * texel;\n let in_rect = all(uv_tap >= sv.atlasRectMin) && all(uv_tap <= sv.atlasRectMax);\n let lit = textureSampleCompareLevel(localShadowAtlas, shadow_sampler, uv_tap, ref_z);\n sum += select(1.0, lit, in_rect);\n }\n return select(sum / 9.0, 1.0, ndc.z > 1.0);\n}\nfn sample_local_shadow_point(ind: LocalLightShadowIndex, light_pos: vec3<f32>, world_pos: vec3<f32>, frag_coord: vec2<f32>) -> f32 {\n let v = world_pos - light_pos;\n let dist = max(length(v), 1e-6);\n let dir = v / dist;\n let base_face = cubeface_from_dir(dir);\n let uv_base = cube_uv_m11_from_dir_on_face(dir, base_face);\n let base_sv = shadowViews[ind.first + base_face];\n let dims_i = textureDimensions(localShadowAtlas);\n let dims = vec2<f32>(f32(dims_i.x), f32(dims_i.y));\n let rect_px = (base_sv.atlasRectMax - base_sv.atlasRectMin) * dims;\n let rect_min = max(1.0, min(rect_px.x, rect_px.y));\n let pcf_radius = min(min(u_directional_light.local_shadow_blur_radius, 64.0), 0.25 * rect_min);\n let texel_m11 = 2.0 / max(rect_px, vec2<f32>(1.0));\n let n01 = textureSampleLevel(noise_texture, noise_sampler, frag_coord / 32.0, 0.0).rg;\n let angle = atan2((n01 * 2.0 - 1.0).y, (n01 * 2.0 - 1.0).x);\n\n var sum = 0.0;\n for (var i = 0; i < 9; i = i + 1) {\n let uv_tap = uv_base + rotate_offset(POISSON9[i], angle) * pcf_radius * texel_m11;\n let dir_tap = dir_from_cube_face_uv_m11(base_face, uv_tap);\n let face = cubeface_from_dir(dir_tap);\n let sv = shadowViews[ind.first + face];\n sum += local_shadow_compare_single(sv, light_pos + dir_tap * dist);\n }\n return sum / 9.0;\n}\nfn iphone_directional_shadow(world_pos: vec3<f32>, frag_coord: vec2<f32>) -> f32 {\n let view_z = -(u_camera.view * vec4<f32>(world_pos, 1.0)).z;\n let cascade_count = min(max(u_directional_light.cascade_count, 1u), 4u);\n var cascade_index = cascade_count - 1u;\n let overlap = 0.5;\n var found = false;\n for (var i = 0u; i < 4u; i = i + 1u) {\n let use_i = i < cascade_count && !found && view_z <= (u_directional_light.cascade_data[i].split_depth + overlap);\n cascade_index = select(cascade_index, i, use_i);\n found = found || use_i;\n }\n\n let lp = u_directional_light.cascade_data[cascade_index].light_view_proj * vec4<f32>(world_pos, 1.0);\n let ndc = lp.xyz / lp.w;\n let uv = ndc.xy * 0.5 + vec2<f32>(0.5);\n let flipped_uv = vec2<f32>(uv.x, 1.0 - uv.y);\n let in_rect = all(flipped_uv >= vec2<f32>(0.0)) && all(flipped_uv <= vec2<f32>(1.0)) && ndc.z >= 0.0 && ndc.z <= 1.0;\n let depth = clamp(ndc.z, 0.0, 1.0);\n let dims = textureDimensions(shadow_map_array, 0);\n let texel_size = 1.0 / f32(dims.x);\n let radius = min(u_directional_light.shadow_blur_radius, 15.0);\n let noise_uv = frag_coord / 32.0;\n let n01 = textureSampleLevel(noise_texture, noise_sampler, noise_uv, 0.0).rg;\n let n = n01 * 2.0 - 1.0;\n let angle = atan2(n.y, n.x);\n\n var shadow_sum = 0.0;\n for (var i = 0; i < 9; i = i + 1) {\n let offset_uv = flipped_uv + rotate_offset(POISSON9[i], angle) * (radius * texel_size);\n shadow_sum += textureSampleCompare(shadow_map_array, shadow_sampler, offset_uv, cascade_index, depth);\n }\n let lit = select(1.0, shadow_sum / 9.0, in_rect);\n return 1.0 - u_directional_light.shadow_opacity * (1.0 - lit);\n}\nfn luminance(rgb: vec3<f32>) -> f32 {\n return dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\nconst MAX_SHADOWED_LIGHTS_PER_PIXEL: u32 = 2u;\nstruct ShadowCandidate {\n raw_index: u32,\n score: f32,\n unshaded_diff: vec3<f32>,\n unshaded_spec: vec3<f32>,\n};\nfn iphone_cluster_id(frag_coord: vec2<f32>, world_pos: vec3<f32>) -> u32 {\n let tiles = u_cluster_lights.tile_counts;\n let uv = frag_coord * u_cluster_lights.inv_screen_size;\n let tx = clamp(u32(floor(uv.x * f32(tiles.x))), 0u, tiles.x - 1u);\n let ty = clamp(u32(floor(uv.y * f32(tiles.y))), 0u, tiles.y - 1u);\n let view_z = -(u_camera.view * vec4<f32>(world_pos, 1.0)).z;\n let near = u_cluster_lights.z_params.x;\n let far = u_cluster_lights.z_params.y;\n let slice_f = log2(max(view_z, near) / near) / log2(far / near) * f32(tiles.z);\n let tz = clamp(u32(slice_f), 0u, tiles.z - 1u);\n return (tz * tiles.y + ty) * tiles.x + tx;\n}\nfn iphone_cluster_lighting(world_pos: vec3<f32>, N: vec3<f32>, V: vec3<f32>, base: vec3<f32>, F0: vec3<f32>, alpha: f32, metallic: f32, frag_coord: vec2<f32>, receive_shadow: bool) -> vec3<f32> {\n let cid = iphone_cluster_id(frag_coord, world_pos);\n let tiles = u_cluster_lights.tile_counts;\n let n = min(cluster_light_count_fragment[cid], tiles.w);\n var Lo_diff = vec3<f32>(0.0);\n var Lo_spec = vec3<f32>(0.0);\n var cand: array<ShadowCandidate, 2>;\n cand[0] = ShadowCandidate(0u, -1.0, vec3<f32>(0.0), vec3<f32>(0.0));\n cand[1] = ShadowCandidate(0u, -1.0, vec3<f32>(0.0), vec3<f32>(0.0));\n for (var i = 0u; i < n; i = i + 1u) {\n let raw = cluster_light_indices[cid * tiles.w + i];\n let idx = raw & ~TYPE_BITS;\n let is_spot = (raw & SPOT_TYPE_BIT) != 0u;\n let is_area = (raw & AREA_TYPE_BIT) != 0u;\n var L = vec3<f32>(0.0);\n var is_diffuse_only = false;\n var radiance_diff = vec3<f32>(0.0);\n var radiance_spec = vec3<f32>(0.0);\n if is_spot {\n if idx >= u_cluster_lights.num_spot_lights { continue; }\n let sl = spot_lights[idx];\n let to_light = sl.position_range.xyz - world_pos;\n let dist = length(to_light);\n if dist > sl.position_range.w * SPEC_RADIUS_SCALE { continue; }\n L = to_light / max(dist, 1e-6);\n let cos_theta = dot(normalize(sl.dir_cos_inner.xyz), -L);\n if cos_theta <= sl.cos_outer_pad.x { continue; }\n let spot_att = clamp((cos_theta - sl.cos_outer_pad.x) / max(sl.dir_cos_inner.w - sl.cos_outer_pad.x, 1e-4), 0.0, 1.0);\n let inv_r2 = 1.0 / max(dist * dist, 1e-4);\n var atten_diff = max(0.0, 1.0 - dist / sl.position_range.w);\n atten_diff = atten_diff * atten_diff;\n var atten_spec = dist / (sl.position_range.w * SPEC_RADIUS_SCALE);\n atten_spec = select(1.0, 1.0 - (atten_spec - 0.4) / 0.6, atten_spec > 0.4);\n radiance_diff = sl.color_intensity.rgb * (spot_att * atten_diff * inv_r2) * sl.cos_outer_pad.y;\n radiance_spec = sl.color_intensity.rgb * (spot_att * atten_spec * inv_r2) * sl.cos_outer_pad.z;\n } else if (raw & AREA_TYPE_BIT) != 0u {\n if idx >= u_cluster_lights.num_area_lights { continue; }\n let al = area_lights[idx];\n let U = normalize(al.axis_u_half.xyz);\n let V2 = normalize(al.axis_v_half.xyz);\n let C = al.center_radius.xyz;\n let center_d = length(world_pos - C);\n if al.center_radius.w > 0.0 && center_d > al.center_radius.w * SPEC_RADIUS_SCALE { continue; }\n let local = world_pos - C;\n let light_pos = C + clamp(dot(local, U), -al.axis_u_half.w, al.axis_u_half.w) * U + clamp(dot(local, V2), -al.axis_v_half.w, al.axis_v_half.w) * V2;\n let to_light = light_pos - world_pos;\n let dist = max(length(to_light), 1e-6);\n L = to_light / dist;\n let inv_r2 = 1.0 / max(dist * dist, 1e-4);\n var atten_diff = 1.0;\n var atten_spec = 1.0;\n if al.center_radius.w > 0.0 {\n let t_diff = clamp(1.0 - center_d / al.center_radius.w, 0.0, 1.0);\n atten_diff = t_diff * t_diff;\n atten_spec = dist / (al.center_radius.w * SPEC_RADIUS_SCALE);\n atten_spec = select(1.0, 1.0 - (atten_spec - 0.4) / 0.6, atten_spec > 0.4);\n }\n let Ln = -normalize(cross(U, V2));\n let emit_cos = max(dot(Ln, -L), 0.0);\n radiance_diff = al.color_intensity.rgb * (emit_cos * atten_diff * inv_r2) * al.diffuse_spec_scale.x;\n radiance_spec = al.color_intensity.rgb * (emit_cos * atten_spec * inv_r2) * al.diffuse_spec_scale.y;\n } else {\n if idx >= u_cluster_lights.num_point_lights { continue; }\n let pl = point_lights[idx];\n let to_light = pl.position_radius.xyz - world_pos;\n let dist = length(to_light);\n is_diffuse_only = pl.diffuse_spec_scale.w > 0.5;\n let light_radius = pl.position_radius.w;\n let cull_radius = select(light_radius * SPEC_RADIUS_SCALE, light_radius, is_diffuse_only);\n if dist > cull_radius { continue; }\n L = to_light / max(dist, 1e-6);\n if is_diffuse_only {\n let fade_width = clamp(pl.diffuse_spec_scale.z, 0.0, light_radius);\n let core_radius = max(0.0, light_radius - fade_width);\n let shell_fade = 1.0 - smootherstep(core_radius, light_radius, dist);\n let atten_diff = select(shell_fade, 1.0, dist <= core_radius || fade_width <= 1e-6);\n radiance_diff = pl.color_intensity.rgb * atten_diff * pl.diffuse_spec_scale.x;\n radiance_spec = vec3<f32>(0.0);\n } else {\n let inv_r2 = 1.0 / max(dist * dist, 0.001);\n var atten_diff = max(0.0, 1.0 - dist / light_radius);\n atten_diff = atten_diff * atten_diff;\n var atten_spec = dist / (light_radius * SPEC_RADIUS_SCALE);\n atten_spec = select(1.0, 1.0 - (atten_spec - 0.4) / 0.6, atten_spec > 0.4);\n radiance_diff = pl.color_intensity.rgb * (atten_diff * inv_r2) * pl.diffuse_spec_scale.x;\n radiance_spec = pl.color_intensity.rgb * (atten_spec * inv_r2) * pl.diffuse_spec_scale.y;\n }\n }\n let NdotL_raw = dot(N, L);\n if !is_diffuse_only && NdotL_raw <= 0.0 { continue; }\n let NdotL = max(NdotL_raw, 0.0);\n let H = normalize(V + L);\n let NdotV = max(dot(N, V), 0.0001);\n let NdotH = max(dot(N, H), 0.0);\n let F = fresnel_schlick(max(dot(H, V), 0.0), F0);\n let D = distribution_ggx(NdotH, alpha);\n let G = geometry_smith(NdotV, NdotL, alpha);\n let spec = (D * G * F) / (4.0 * max(NdotL, 1e-4) * max(NdotV, 1e-4));\n let diff = select((1.0 - F) * (1.0 - metallic) * base / PI, base / PI, is_diffuse_only);\n let diffuse_facing = select(NdotL, smoothstep(-0.05, 0.05, NdotL_raw), is_diffuse_only);\n let unshaded_diff = diff * radiance_diff * diffuse_facing;\n let unshaded_spec = spec * radiance_spec * NdotL;\n var has_shadow = false;\n if is_spot {\n if idx < arrayLength(&spotShadowIndex) {\n has_shadow = spotShadowIndex[idx].count != 0u;\n }\n } else if !is_area {\n if idx < arrayLength(&pointShadowIndex) {\n has_shadow = pointShadowIndex[idx].count != 0u;\n }\n }\n if has_shadow {\n let score = luminance(radiance_diff + radiance_spec) * NdotL;\n if score > cand[0].score {\n let evict = cand[1];\n cand[1] = cand[0];\n cand[0] = ShadowCandidate(raw, score, unshaded_diff, unshaded_spec);\n if evict.score >= 0.0 {\n Lo_diff += evict.unshaded_diff;\n Lo_spec += evict.unshaded_spec;\n }\n } else if score > cand[1].score {\n let evict = cand[1];\n cand[1] = ShadowCandidate(raw, score, unshaded_diff, unshaded_spec);\n if evict.score >= 0.0 {\n Lo_diff += evict.unshaded_diff;\n Lo_spec += evict.unshaded_spec;\n }\n } else {\n Lo_diff += unshaded_diff;\n Lo_spec += unshaded_spec;\n }\n } else {\n Lo_diff += unshaded_diff;\n Lo_spec += unshaded_spec;\n }\n }\n\n for (var k = 0u; k < MAX_SHADOWED_LIGHTS_PER_PIXEL; k = k + 1u) {\n let c = cand[k];\n if c.score < 0.0 {\n break;\n }\n if !receive_shadow {\n Lo_diff += c.unshaded_diff;\n Lo_spec += c.unshaded_spec;\n continue;\n }\n let raw = c.raw_index;\n let idx = raw & ~TYPE_BITS;\n let is_spot = (raw & SPOT_TYPE_BIT) != 0u;\n var L = vec3<f32>(0.0);\n var shadow = 1.0;\n if is_spot {\n if idx >= u_cluster_lights.num_spot_lights || idx >= arrayLength(&spotShadowIndex) { continue; }\n let sl = spot_lights[idx];\n let to_light = sl.position_range.xyz - world_pos;\n let dist = length(to_light);\n if dist > sl.position_range.w * SPEC_RADIUS_SCALE { continue; }\n L = to_light / max(dist, 1e-6);\n let cos_theta = dot(normalize(sl.dir_cos_inner.xyz), -L);\n if cos_theta <= sl.cos_outer_pad.x { continue; }\n let ind = spotShadowIndex[idx];\n if ind.count != 0u && ind.first < arrayLength(&shadowViews) {\n shadow = sample_local_shadow_spot(shadowViews[ind.first], world_pos, frag_coord);\n }\n } else {\n if idx >= u_cluster_lights.num_point_lights || idx >= arrayLength(&pointShadowIndex) { continue; }\n let pl = point_lights[idx];\n let to_light = pl.position_radius.xyz - world_pos;\n let dist = length(to_light);\n if dist > pl.position_radius.w * SPEC_RADIUS_SCALE { continue; }\n L = to_light / max(dist, 1e-6);\n let ind = pointShadowIndex[idx];\n if ind.count >= 6u && ind.first + 5u < arrayLength(&shadowViews) {\n shadow = sample_local_shadow_point(ind, pl.position_radius.xyz, world_pos, frag_coord);\n }\n }\n if max(dot(N, L), 0.0) == 0.0 { continue; }\n let shadow_f = 1.0 - u_directional_light.shadow_opacity * (1.0 - shadow);\n Lo_diff += c.unshaded_diff * shadow_f;\n Lo_spec += c.unshaded_spec * shadow_f;\n }\n return Lo_diff + Lo_spec;\n}\n\n@fragment\nfn fullscreen_fs(@builtin(position) clip_position: vec4<f32>) -> @location(0) vec4<f32> {\n let size_f = vec2<f32>(textureDimensions(base_color_texture));\n let pixel_coords = clamp(vec2<i32>(clip_position.xy), vec2<i32>(0), vec2<i32>(size_f) - vec2<i32>(1));\n let depth = textureLoad(depth_texture, pixel_coords, 0);\n if depth >= 1.0 {\n discard;\n }\n\n let uv = clip_position.xy / size_f;\n let ndc = vec4<f32>(vec2<f32>(uv.x, 1.0 - uv.y) * 2.0 - 1.0, depth, 1.0);\n let world_pos_h = u_camera.inverse_view_proj * ndc;\n let world_pos = world_pos_h.xyz / world_pos_h.w;\n let base_full = textureLoad(base_color_texture, pixel_coords, 0);\n var base = base_full.rgb;\n let receive_shadow = base_full.a > 0.5;\n let N = normalize(textureLoad(normal_texture, pixel_coords, 0).xyz);\n let orm = textureLoad(orm_texture, pixel_coords, 0);\n let occlusion = orm.r;\n var roughness = clamp(orm.g, 0.05, 1.0);\n let metallic = orm.b;\n if (u_shore.a.x > 0.0 && abs(world_pos.y - u_shore.a.y) < u_shore.a.z + 3.0) {\n // Mirrors main_pass.wgsl: wave-following waterline, noisy wet edge, meniscus.\n let waterline = shore_waterline_y(world_pos.xz);\n let wn = 0.5 + 0.5 * sin(world_pos.x * 7.3) * sin(world_pos.z * 6.1);\n // Inside the ripple grid window the wet band comes from the DYNAMIC wet height\n // (real runup: waves wet the ground, the mark dries in seconds); the analytic\n // Y-band is the out-of-window / mobile fallback.\n var wet: f32;\n var waterline_px = waterline;\n let grid_rel = (world_pos.xz - u_shore.grid.xy) * u_shore.grid.z;\n if (u_shore.grid.w > 0.5 && max(abs(grid_rel.x), abs(grid_rel.y)) < 0.49) {\n let dw = shore_dynamic_wet(world_pos.xz);\n waterline_px += dw.x; // wakes/splashes lift the local waterline\n let h = world_pos.y - dw.y;\n wet = smoothstep(-2.0, -1.0, world_pos.y - waterline_px)\n * (1.0 - smoothstep(-0.02, 0.05, h))\n * u_shore.a.x;\n } else {\n wet = shore_wet_factor(world_pos.y + (wn - 0.5) * 0.5 * u_shore.a.z, waterline)\n * u_shore.a.x;\n }\n base *= 1.0 - wet * 0.4;\n roughness = mix(roughness, roughness * 0.55, wet * u_shore.a.w);\n let men = (1.0 - clamp(abs(world_pos.y - waterline_px - 0.01) / 0.05, 0.0, 1.0)) * wet;\n base += vec3<f32>(0.22, 0.23, 0.25) * men * men * (0.45 + 0.55 * wn);\n roughness = mix(roughness, 0.08, men);\n\n // Caustics: sun filaments dancing on everything below the waterline - two\n // counter-panning layers of the periodic Worley-filament tile, deeper = dimmer.\n // Albedo-multiplicative, so sun/shadow/AO light them for free (and they vanish\n // in shadow, which is physically right).\n let under_w = smoothstep(0.02, 0.3, waterline_px - world_pos.y);\n if (under_w > 0.0) {\n let tc = u_camera.time_seconds;\n let c1 = caustic_bilinear(world_pos.xz * 0.36 + vec2<f32>(tc * 0.021, tc * 0.014));\n let c2 = caustic_bilinear(world_pos.xz * 0.21 - vec2<f32>(tc * 0.017, tc * 0.011));\n // min(), not product: sharp sparse filaments multiplied are nonzero\n // only where two thin lines INTERSECT (nearly nowhere). min keeps a line\n // wherever both layers have energy - the classic two-phase caustic combine.\n let caus = min(c1, c2) * 1.7;\n let depth_fade = exp(-(waterline_px - world_pos.y) * 0.5);\n base *= 1.0 + caus * under_w * depth_fade * 1.5;\n }\n }\n\n let V = normalize(u_camera.camera_position - world_pos);\n let L = normalize(-u_directional_light.light_dir.xyz);\n let H = normalize(V + L);\n let NdotV = max(dot(N, V), 0.0001);\n let NdotL = max(dot(N, L), 0.0);\n let NdotH = max(dot(N, H), 0.0);\n let F0 = mix(vec3<f32>(0.04), base, metallic);\n let F = fresnel_schlick(max(dot(H, V), 0.0), F0);\n let alpha = roughness * roughness;\n let specular = (distribution_ggx(NdotH, alpha) * geometry_smith(NdotV, NdotL, alpha) * F) / (4.0 * max(NdotL, 1e-4) * max(NdotV, 1e-4));\n let diffuse = (1.0 - F) * (1.0 - metallic) * base / PI;\n // AO (material occlusion x SSAO) applies to AMBIENT only, matching UE and main_pass.wgsl.\n let ssao = textureSampleLevel(ssao_texture, g_sampler, uv, 0.0).r;\n let ao_ambient = occlusion * ssao;\n let shadow_raw = select(1.0, iphone_directional_shadow(world_pos, clip_position.xy), receive_shadow);\n let diffuse_shadow_factor = 1.0 - u_directional_light.shadow_opacity * (1.0 - shadow_raw);\n let specular_shadow_factor = shadow_raw;\n let direct = (diffuse * diffuse_shadow_factor + specular * u_directional_light.sun_specular_scale * specular_shadow_factor) * NdotL * u_directional_light.light_color_with_intensity.rgb;\n\n let diffuse_ibl = max(textureSampleLevel(ibl_irradiance, ibl_sampler, N, 0.0).rgb, vec3<f32>(0.0)) * base * (1.0 - metallic) * ao_ambient;\n let reflect_dir = reflect(-V, N);\n let mip_count = f32(textureNumLevels(ibl_prefiltered_specular));\n let prefiltered_level = pow(roughness, 1.0 / 5.0) * max(0.0, mip_count - 1.0);\n var prefiltered = max(textureSampleLevel(ibl_prefiltered_specular, ibl_sampler, reflect_dir, prefiltered_level).rgb, vec3<f32>(0.0));\n let brdf = textureSampleLevel(brdf_lut, ibl_sampler, vec2<f32>(NdotV, roughness), 0.0).xy;\n // SSR is premultiplied (rgb already scaled by a); composite into env radiance\n // before the split-sum term, matching main_pass.wgsl. AO stays on IBL only.\n let ssr_sample = sanitize_ssr(textureLoad(ssr_resolved_texture, pixel_coords, 0));\n let env_specular = prefiltered * ao_ambient * (1.0 - ssr_sample.a) + ssr_sample.rgb;\n let specular_ibl = env_specular * (F0 * brdf.x + brdf.y);\n let ambient_tint = u_directional_light.ambient_tint;\n let ibl_sum = diffuse_ibl + specular_ibl;\n let ibl_lum = dot(ibl_sum, vec3<f32>(0.2126, 0.7152, 0.0722));\n let ambient = mix(vec3<f32>(ibl_lum), ibl_sum, max(0.0, ambient_tint.a)) * ambient_tint.rgb;\n let spec_lobe_squeeze = select(0.0, 0.08 * pow(1.0 - roughness, 2.0), u_directional_light.light_debug_mode != 9u);\n let alpha_local = clamp(alpha * (1.0 - spec_lobe_squeeze), 1e-4, 1.0);\n let clustered = iphone_cluster_lighting(world_pos, N, V, base, F0, alpha_local, metallic, clip_position.xy, receive_shadow);\n\n const EMISSIVE_STOPS: f32 = 10.0;\n let emissive = exp2(clamp(orm.a, 0.0, 1.0) * EMISSIVE_STOPS) - 1.0;\n var emissive_tint = base;\n if u_directional_light.light_debug_mode == 10u {\n emissive_tint = vec3<f32>(0.1, 0.25, 1.0);\n }\n\n var final_color = direct + ambient + clustered + emissive_tint * emissive;\n\n // Distance/height fog (folded from the retired fog pass), over the radial camera\n // distance in meters (start/end_ are authored in meters; the retired pass fed NDC\n // depth here, so linear distance fog was silently inert).\n let fog_dist_m = distance(world_pos, u_camera.camera_position);\n var fog_a: f32;\n switch (u_fog.mode) {\n default { fog_a = clamp((fog_dist_m - u_fog.start) / max(u_fog.end_ - u_fog.start, 1e-6), 0.0, 1.0); }\n case 1u { fog_a = 1.0 - exp(-u_fog.density * fog_dist_m); }\n case 2u { fog_a = 1.0 - exp(-u_fog.density * fog_dist_m * u_fog.density * fog_dist_m); }\n }\n fog_a = clamp(fog_a, 0.0, 1.0);\n if (u_fog.height_enabled != 0u) {\n let lo = min(u_fog.height_bottom, u_fog.height_top);\n let hi = max(u_fog.height_bottom, u_fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(u_fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, world_pos.y);\n fog_a *= mix(1.0, h, clamp(u_fog.height_weight, 0.0, 1.0));\n }\n final_color = mix(final_color, u_fog.color, fog_a);\n\n return vec4<f32>(final_color, 1.0);\n}\n"},{"label":"shaders/fullscreen_triangle_vs.wgsl","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n@vertex\nfn fullscreen_vs(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {\n var p: vec2<f32>;\n switch (vid) {\n case 0u: {\n p = vec2<f32>(-1.0, -1.0);\n }\n case 1u: {\n p = vec2<f32>(3.0, -1.0);\n }\n default: {\n p = vec2<f32>(-1.0, 3.0);\n }\n }\n return vec4<f32>(p, 0.0, 1.0);\n}\n"},{"label":"shaders/main_pass_fallback.wgsl","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// Lean stand-in for the main_pass_iphone.wgsl resolve while the browser compiles that mega\n// shader (~5s on a first-ever visit; Chrome's shader disk cache makes revisits instant).\n// Declares a subset of the mega pipeline's group(0) so it runs on the same pipeline layout;\n// flat albedo + fixed-sun lambert, background discarded for the skybox exactly like the mega\n// fragment. Chosen by the wasm health gate in lib.rs; never used on native.\n\n@group(0) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(0) @binding(1) var normal_texture: texture_2d<f32>;\n@group(0) @binding(3) var depth_texture: texture_depth_2d;\n\n@fragment\nfn fullscreen_fs(@builtin(position) clip_position: vec4<f32>) -> @location(0) vec4<f32> {\n let size_f = vec2<f32>(textureDimensions(base_color_texture));\n let pixel_coords = clamp(vec2<i32>(clip_position.xy), vec2<i32>(0), vec2<i32>(size_f) - vec2<i32>(1));\n let depth = textureLoad(depth_texture, pixel_coords, 0);\n if depth >= 1.0 {\n discard;\n }\n let base = textureLoad(base_color_texture, pixel_coords, 0).rgb;\n let n = normalize(textureLoad(normal_texture, pixel_coords, 0).xyz);\n // Rough default-daylight direction; only seen for the few seconds before the real\n // resolve compiles, so a pleasant approximation beats exact sun plumbing.\n let sun_dir = normalize(vec3<f32>(0.35, 0.8, 0.45));\n let lambert = 0.55 + 0.45 * max(dot(n, sun_dir), 0.0);\n return vec4<f32>(base * lambert, 1.0);\n}\n"},{"label":"water","code":"const WATER_DEBUG_TAP: bool = false;\n// placed_water.wgsl \u2014 placed water MESHES: still pools, flowing streams, waterfalls and\n// fountain jets. Instanced water-flagged meshes shaded by the shared water_shading_body\n// (same body as the global ocean; Rust-side concat, see water_pass::placed_water_source).\n// Successor to the retired water.wgsl (archived: agent_docs/shader_reference/\n// water_legacy.wgsl) and keeps its eye-tuned STILL-WATER animation constants \u2014 flat water\n// fakes all its motion in the fragment shader, so it animates faster than the displaced\n// ocean \u2014 plus its waterfall (auto on steep surfaces, forced for jets via SetWaterFlow).\n// Per-body data rides the instance stream: shallow/deep colors + mix depths in\n// mesh_color/emission_rgbi (see render_base water branch), transparency byte in\n// inst_flags[24..31], flow speed 0..127 * 0.025 m/s + forced-fall bit in inst_flags[16..23].\n// Flow DIRECTION is the mesh's local +X axis: rotate the mesh to steer the stream.\n// No planar reflection here (weight 0): planar is ocean-only; pools read the skybox.\nstruct CameraUniform {\n view_proj : mat4x4<f32>,\n inverse_view_proj : mat4x4<f32>,\n inverse_proj : mat4x4<f32>,\n view : mat4x4<f32>,\n proj : mat4x4<f32>,\n camera_position : vec3<f32>,\n time_seconds : f32,\n near : f32,\n far : f32,\n _padding2 : f32,\n _padding3 : f32,\n camera_right : vec3<f32>,\n _padding4 : f32,\n camera_up : vec3<f32>,\n _padding5 : f32,\n inverse_view : mat4x4<f32>,\n};\nstruct VSIn {\n @location(0) pos: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv0: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n @location(4) model0: vec4<f32>,\n @location(5) model1: vec4<f32>,\n @location(6) model2: vec4<f32>,\n @location(7) model3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) emission_rgbi: vec4<f32>,\n @location(10) inst_flags: u32,\n};\nstruct VSOut {\n @builtin(position) clip : vec4<f32>,\n @location(0) world_pos: vec3<f32>,\n @location(1) T: vec3<f32>,\n @location(2) B: vec3<f32>,\n @location(3) N: vec3<f32>,\n @location(4) mesh_color: vec4<f32>,\n @location(5) emission_rgbi: vec4<f32>,\n @interpolate(flat) @location(6) inst_flags: u32,\n // xy = world-XZ flow dir * speed (m/s), z = forced-fall 0/1, w unused\n @interpolate(flat) @location(7) flow_fall: vec4<f32>,\n};\nstruct WaterParams {\n ior: f32,\n strength: f32,\n sea_level_y: f32, // ocean waterline (m); placed water fades out below it\n ocean_enabled: f32, // 0/1 gate for the submerge fade\n refract_thick_fade: vec2<f32>, // x = start, y = end\n skybox_horizon_tilt_sin: f32,\n skybox_yaw: f32,\n skybox_tint: vec3<f32>,\n skybox_saturation: f32,\n skybox_exposure: f32,\n skybox_hue: f32,\n skybox_contrast: f32,\n _pad1: f32,\n};\nstruct FogSettings {\n color: vec3<f32>,\n mode: u32,\n start: f32,\n end_: f32,\n density: f32,\n height_enabled:u32,\n height_weight: f32,\n height_bottom: f32,\n height_top: f32,\n height_softness: f32,\n sky_affect: f32,\n};\nstruct PlanarReflectionParams {\n view_proj: mat4x4<f32>,\n plane_y: f32,\n is_active: u32,\n strength: f32,\n distortion: f32,\n};\nstruct ShadowCascadeData {\n light_view_proj: mat4x4<f32>,\n split_depth: f32,\n _padding: vec3<f32>,\n};\nstruct DirectionalLightShadowUniform {\n cascade_data: array<ShadowCascadeData, 4>,\n light_dir: vec4<f32>,\n light_color_with_intensity: vec4<f32>,\n ambient_tint: vec4<f32>,\n\n cascade_count: u32,\n shadow_opacity: f32,\n light_debug_mode: u32,\n shadow_blur_radius: f32,\n local_shadow_blur_radius: f32,\n sun_specular_scale: f32,\n _pad_local1: u32,\n _pad_local2: u32,\n};\n\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n@group(1) @binding(0) var sceneColor : texture_2d<f32>;\n@group(1) @binding(1) var sceneSample : sampler;\n@group(1) @binding(2) var view_z_m : texture_2d<f32>;\n@group(1) @binding(3) var view_z_m_view : sampler; // NON-filtering\n@group(1) @binding(4) var<uniform> p : WaterParams;\n@group(1) @binding(5) var normalA : texture_2d<f32>;\n@group(1) @binding(6) var normalB : texture_2d<f32>;\n@group(1) @binding(7) var normalSample : sampler; // filtering, repeat\n@group(1) @binding(8) var refractNoiseNormal : texture_2d<f32>;\n@group(1) @binding(9) var skybox_texture : texture_cube<f32>;\n@group(1) @binding(10) var<uniform> fog: FogSettings;\n@group(1) @binding(11) var microNormalA : texture_2d<f32>;\n@group(1) @binding(12) var microNormalB : texture_2d<f32>;\n@group(1) @binding(13) var foamRGB : texture_2d<f32>;\n@group(1) @binding(14) var fbmTex : texture_2d<f32>;\n@group(1) @binding(15) var sceneWorldNormal : texture_2d<f32>;\n@group(1) @binding(16) var planarReflection : texture_2d<f32>;\n@group(1) @binding(19) var<uniform> planar : PlanarReflectionParams;\n@group(1) @binding(20) var<storage, read_write> waterDebug: array<vec4<f32>, 144>;\n@group(2) @binding(0) var<uniform> u_directional_light: DirectionalLightShadowUniform;\n\n// --- Waterfall helpers (placed-water only; the flat ocean never falls) ---\nfn slope_fall_mask(n_geo: vec3<f32>, angle_start_deg: f32, angle_full_deg: f32) -> f32 {\n // Mask = 0 on flat (< angle_start), 1 when vertical (>= angle_full)\n let c0 = cos(angle_start_deg * (PI / 180.0));\n let c1 = cos(angle_full_deg * (PI / 180.0));\n let a = min(c0, c1);\n let b = max(c0, c1);\n let k = fwidth(n_geo.y) * 2.0;\n return 1.0 - smoothstep(a - k, b + k, n_geo.y);\n}\n// Gravity projected into the surface (downhill direction).\nfn flow_dir_on_surface(N: vec3<f32>) -> vec3<f32> {\n let g = vec3<f32>(0.0, -1.0, 0.0);\n let t = g - N * dot(g, N);\n let L = length(t);\n return select(t / max(L, 1e-6), vec3<f32>(0.0, 0.0, -1.0), L <= 1e-6);\n}\n// Convert a world-space direction to a screen-UV direction at a point.\nfn screen_dir_from_world(origin_w: vec3<f32>, dir_w: vec3<f32>) -> vec2<f32> {\n let eps = 0.05;\n let o_c = u_camera.view_proj * vec4<f32>(origin_w, 1.0);\n let p_c = u_camera.view_proj * vec4<f32>(origin_w + dir_w * eps, 1.0);\n let o_ndc = o_c.xy / o_c.w;\n let p_ndc = p_c.xy / p_c.w;\n return (p_ndc - o_ndc) * 0.5;\n}\n\nfn water_out(color: vec4<f32>) -> FSOut {\n var o: FSOut;\n o.color = color;\n o.reactive = 0.35 * color.a;\n return o;\n}\n\n@vertex fn vs_main(i: VSIn) -> VSOut {\n var o: VSOut;\n let model = mat4x4<f32>(i.model0, i.model1, i.model2, i.model3);\n var world = model * vec4<f32>(i.pos, 1.0);\n\n // world-space TBN\n let M3 = mat3x3<f32>(i.model0.xyz, i.model1.xyz, i.model2.xyz);\n\n // Prefer mesh tangent if present; else fall back to +X\n let has_tangent = any(i.tangent.xyz != vec3<f32>(0.0));\n var T_obj = vec3<f32>(1.0, 0.0, 0.0);\n var handedness = 1.0;\n if (has_tangent) {\n T_obj = normalize(i.tangent.xyz);\n handedness = i.tangent.w; // usually +1 or -1\n }\n\n var N_w = normalize(M3 * normalize(i.normal));\n var T_w = normalize(M3 * T_obj);\n // Gram\u2013Schmidt to ensure T \u27c2 N (keeps basis stable)\n T_w = normalize(T_w - N_w * dot(T_w, N_w));\n let B_w = normalize(cross(N_w, T_w) * handedness);\n\n // Per-body flow (Object.SetWaterFlow): speed byte in inst_flags[16..23] = 7-bit\n // quantized speed (0.025 m/s steps) + forced-fall top bit; direction = the mesh's\n // world local +X axis flattened to XZ (rotate the mesh to steer the stream).\n let flow_byte = (i.inst_flags >> 16u) & 0xffu;\n let flow_speed = f32(flow_byte & 0x7fu) * 0.025;\n let fall_forced = f32((flow_byte >> 7u) & 1u);\n let axis = M3 * vec3<f32>(1.0, 0.0, 0.0);\n let axis_xz = vec2<f32>(axis.x, axis.z);\n let axis_len = length(axis_xz);\n let flow_dir = select(vec2<f32>(1.0, 0.0), axis_xz / max(axis_len, 1e-5), axis_len > 1e-5);\n o.flow_fall = vec4<f32>(flow_dir * flow_speed, fall_forced, 0.0);\n\n // (VS undulation retired: displacing along per-FACE vertex normals cracks the rims\n // of closed slabs - top and side faces move apart at shared corners. Bring it back\n // if/when single-sided sheet meshes exist; the aeration layers carry the motion.)\n o.clip = u_camera.view_proj * world;\n o.world_pos = world.xyz;\n o.T = T_w;\n o.B = B_w;\n o.N = N_w;\n o.mesh_color = i.mesh_color;\n o.emission_rgbi = i.emission_rgbi;\n o.inst_flags = i.inst_flags;\n return o;\n}\n\n@fragment fn fs_main(i: VSOut, @builtin(front_facing) front: bool) -> FSOut {\n // Per-instance water look (see render_base water branch for the packing)\n let transparency = f32((i.inst_flags >> 24u) & 0xffu) / 255.0;\n let shallow_color = i.mesh_color.rgb;\n let deep_color = i.emission_rgbi.rgb;\n let shallow_m = i.mesh_color.a;\n let deep_m = i.emission_rgbi.a;\n\n let N_macro = normalize(i.N);\n let flow_speed = length(i.flow_fall.xy);\n let has_flow = flow_speed > 1e-4;\n\n // Waterfall mask: automatic on steep surfaces (spillways, cascade meshes), forced for\n // jets/sheets whose surface is not steep (fountain arcs) via SetWaterFlow.\n let fall01 = max(slope_fall_mask(N_macro, 20.0, 80.0), i.flow_fall.z);\n\n // Anisotropic falling-streak refraction (water.wgsl waterfall, verbatim tuning):\n // fbm stretched along the fall direction, projected into screen space.\n // Fall frame, shared by the streak layer AND the base-layer remap in the body:\n // gravity-projected downhill. Forced sheets only override the direction where\n // downhill is DEGENERATE (near-horizontal faces, e.g. a jet's apex) - a 55deg face\n // has a perfectly good downhill, and hijacking it with the horizontal flow axis\n // made falls scroll sideways.\n var Tfall_w = flow_dir_on_surface(N_macro);\n if (i.flow_fall.z > 0.5 && N_macro.y > 0.98) {\n Tfall_w = normalize(vec3<f32>(i.flow_fall.x, 0.0, i.flow_fall.y) / max(flow_speed, 1e-4));\n }\n let Bfall_w = normalize(cross(N_macro, Tfall_w));\n\n var fall_off_px = vec2<f32>(0.0);\n var fall_white = 0.0;\n if (fall01 > 1e-3) {\n let fbm_tile_u = 0.35 * 0.2;\n let fbm_tile_v = 1.7 * 0.2;\n let fall_speed = 1.2;\n let fall_refract_px = 0.014 * 2.0; // px in screen\n let uvWaterfall = vec2<f32>(\n dot(i.world_pos, Tfall_w) * fbm_tile_u - u_camera.time_seconds * fall_speed,\n dot(i.world_pos, Bfall_w) * fbm_tile_v\n );\n let fbmWaterfall = textureSampleLevel(fbmTex, normalSample, uvWaterfall, 0.0).rg * 2.0 - 1.0;\n let t_scr = normalize(screen_dir_from_world(i.world_pos, Tfall_w) + vec2<f32>(1e-6));\n let b_scr = normalize(screen_dir_from_world(i.world_pos, Bfall_w) + vec2<f32>(1e-6));\n fall_off_px = (fbmWaterfall.x * t_scr + fbmWaterfall.y * b_scr) * fall_refract_px * fall01;\n\n // Layered aeration (AAA fall recipe): layer 1 = the streak fbm itself; layer 2 =\n // a RIDGED remap of the same texture at higher V-frequency and faster scroll\n // (sharp bright streak lines, parallax depth); + Fresnel edge thickening so the\n // sheet silhouette reads thick/white at grazing angles.\n let streak1 = clamp(fbmWaterfall.x * 0.5 + 0.5, 0.0, 1.0);\n let uvW2 = vec2<f32>(uvWaterfall.x * 2.3 - u_camera.time_seconds * 0.9, uvWaterfall.y * 1.4);\n let n2 = textureSampleLevel(fbmTex, normalSample, uvW2, 0.0).r * 2.0 - 1.0;\n let ridged2 = 1.0 - abs(n2);\n let Vv = normalize(u_camera.camera_position - i.world_pos);\n let edge01 = pow(1.0 - abs(dot(N_macro, Vv)), 3.0);\n fall_white = min(\n fall01 * (0.025 + streak1 * 0.05 + ridged2 * ridged2 * 0.07 + edge01 * 0.35),\n 0.6,\n );\n }\n\n var ctx: ShadeCtx;\n ctx.n_g = N_macro;\n ctx.crest01 = 0.0;\n ctx.crest_foam_mask = 0.0;\n ctx.fft_foam = 0.0;\n ctx.ripple_foam = 0.0;\n ctx.t_anim = u_camera.time_seconds;\n // Streams advect refraction + foam along their own flow; still water keeps the\n // retired water.wgsl's gentle default drift.\n let still_drift = vec2<f32>(0.3, 0.24);\n ctx.flow_vec = select(still_drift * 0.39, i.flow_fall.xy * 0.6, has_flow);\n ctx.foam_drift = select(still_drift * 0.02, i.flow_fall.xy * 0.05, has_flow);\n // Still-water tuning (water.wgsl heritage): flat water fakes ALL its motion in the\n // fragment shader, so it animates faster than the displaced ocean.\n ctx.time_scale = 0.15;\n ctx.ns_reflect = 0.05;\n ctx.ns_refract = 0.5;\n ctx.micro_strength = 0.22;\n ctx.refract_z_floor_m = 1.0;\n ctx.shallow_color = shallow_color;\n ctx.deep_color = deep_color;\n ctx.shallow_m = shallow_m;\n ctx.deep_m = deep_m;\n ctx.transparency = transparency;\n ctx.planar_scale = 0.0; // planar reflection is ocean-only; placed water reads the skybox\n ctx.planar_hf_a = 0.5;\n ctx.planar_hf_b = 2.0;\n ctx.fall01 = fall01;\n ctx.fall_off_px = fall_off_px;\n ctx.fall_white01 = fall_white;\n ctx.fall_t = Tfall_w;\n ctx.fall_b = Bfall_w;\n // Falling water accelerates: base-layer scroll outruns the nominal flow a bit.\n ctx.fall_scroll = max(flow_speed * 1.5, 1.2);\n // Near-horizontal gate: fall/jet sheet BACKFACES (visible since the cull-off) must\n // keep normal water shading; only pool/ocean-like tops seen from below go Snell.\n ctx.underside01 = select(\n 0.0, 1.0,\n !front && N_macro.y > 0.6 && u_camera.camera_position.y < i.world_pos.y,\n );\n\n var dbg16: array<vec4<f32>, 16>;\n var color = shade_water(i.world_pos, i.clip, ctx, &dbg16);\n\n // Submerge fade: where the global ocean is on, placed water dissolves as it drops\n // below the moving waterline instead of double-shading under the sea (stream mouths:\n // end the mesh just above max swell with a forced-fall lip; this fade covers the rest).\n if (p.ocean_enabled > 0.5) {\n color.a *= smoothstep(p.sea_level_y - 0.4, p.sea_level_y - 0.05, i.world_pos.y);\n }\n\n // Debug sample tap: compiled out of the production pipeline (WATER_DEBUG_TAP is\n // prepended by create_pipeline; arming a sample swaps in the debug variant).\n if (WATER_DEBUG_TAP) {\n let frag_xy = vec2<u32>(u32(i.clip.x), u32(i.clip.y));\n for (var slot = 0u; slot < 9u; slot++) {\n let base = slot * 16u;\n let debug_xy = vec2<u32>(u32(waterDebug[base].x), u32(waterDebug[base].y));\n if (waterDebug[base].z > 0.5 && all(debug_xy == frag_xy)) {\n for (var row = 1u; row < 16u; row++) {\n waterDebug[base + row] = dbg16[row];\n }\n waterDebug[base] = vec4<f32>(waterDebug[base].xy, 0.0, waterDebug[base].w);\n }\n }\n }\n\n return water_out(color);\n}\n\n// water_shading_body.wgsl\n// Shared water surface shading: consumed by ocean_surface.wgsl (global displaced ocean)\n// and placed_water.wgsl (placed still/stream/fall water meshes). Each wrapper declares\n// the group(0)/(1)/(2) bindings by the SAME names (WGSL module scope is order-free), does\n// its own geometry/normal prologue, fills ShadeCtx, and wraps the returned color with its\n// reactive-mask policy. Extracted verbatim from ocean_surface.wgsl (itself adapted from\n// the retired water.wgsl - archived at agent_docs/shader_reference/water_legacy.wgsl).\n\nstruct ShadeCtx {\n n_g: vec3<f32>, // geometric water normal (waves folded in for the ocean)\n crest01: f32, // gerstner crest mask (SSS boost); 0 for placed water\n crest_foam_mask: f32, // fold * crest01 * gain; 0 for placed water\n fft_foam: f32, // FFT Jacobian whitecap accumulation; 0 for placed water\n ripple_foam: f32, // wake-grid foam trail; 0 for placed water\n t_anim: f32, // conditioned animation clock\n flow_vec: vec2<f32>, // refraction-advection direction*speed\n foam_drift: vec2<f32>, // foam UV drift per second\n time_scale: f32, // detail normal scroll speed (still water is faster: no real motion)\n ns_reflect: f32,\n ns_refract: f32,\n micro_strength: f32,\n refract_z_floor_m: f32, // near-camera refraction wobble floor\n shallow_color: vec3<f32>,\n deep_color: vec3<f32>,\n shallow_m: f32,\n deep_m: f32,\n transparency: f32,\n planar_scale: f32, // 1 = sample planar reflection, 0 = skybox only (placed water)\n planar_hf_a: f32, // planar height-fade start/end (ocean widens by swell amplitude)\n planar_hf_b: f32,\n fall01: f32, // waterfall/aeration mask (auto slope or forced); 0 = calm surface\n fall_off_px: vec2<f32>, // anisotropic falling-streak refraction offset (pre preMask)\n fall_white01: f32, // layered aeration whitening (streaks + Fresnel edge); 0 = flat 5% legacy via fall01 only\n fall_t: vec3<f32>, // fall-surface frame: downhill tangent (zero for the ocean)\n fall_b: vec3<f32>, // fall-surface frame: across tangent\n fall_scroll: f32, // downhill texture speed m/s on falls\n underside01: f32, // 1 = near-horizontal surface seen from BELOW (submerged camera)\n}\n\nconst PI: f32 = 3.141592653589793;\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n return vec3<f32>(y, -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b, 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(ypbpr.x + 1.5748 * ypbpr.z, ypbpr.x - 0.187324 * ypbpr.y - 0.468124 * ypbpr.z, ypbpr.x + 1.8556 * ypbpr.y);\n}\nfn apply_skybox_grade(color: vec3<f32>) -> vec3<f32> {\n var ypbpr = rgb_to_ypbpr709(color);\n let hue = radians(p.skybox_hue);\n let c = cos(hue);\n let s = sin(hue);\n ypbpr = vec3<f32>(ypbpr.x, (ypbpr.y * c - ypbpr.z * s) * max(0.0, p.skybox_saturation), (ypbpr.y * s + ypbpr.z * c) * max(0.0, p.skybox_saturation));\n var rgb = ypbpr709_to_rgb(ypbpr);\n rgb = (rgb - vec3<f32>(0.5)) * max(0.0, p.skybox_contrast) + vec3<f32>(0.5);\n return rgb * p.skybox_tint * exp2(p.skybox_exposure);\n}\n\nfn planar_uv(world_pos: vec3<f32>) -> vec3<f32> {\n let clip = planar.view_proj * vec4<f32>(world_pos, 1.0);\n if (clip.w <= 0.0) {\n return vec3<f32>(0.0, 0.0, 0.0);\n }\n let ndc = clip.xyz / clip.w;\n let uv = ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n let valid = select(0.0, 1.0, all(uv >= vec2<f32>(0.0)) && all(uv <= vec2<f32>(1.0)) && ndc.z >= 0.0 && ndc.z <= 1.0);\n return vec3<f32>(uv, valid);\n}\nfn planar_edge_fade(uv: vec2<f32>) -> f32 {\n let d = min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y));\n return smoothstep(0.0, 0.08, d);\n}\n\nfn linearize_depth_0to1(d: f32, zNear: f32, zFar: f32) -> f32 {\n return (zNear * zFar) / (zFar - d * (zFar - zNear));\n}\nfn remap01(x: f32, a: f32, b: f32) -> f32 {\n return clamp((x - a) / max(1e-6, b - a), 0.0, 1.0);\n}\nfn smootherstep(a: f32, b: f32, x: f32) -> f32 {\n let t = clamp((x - a) / max(1e-6, b - a), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\nfn band_bump(x: f32, inner0: f32, inner1: f32, outer0: f32, outer1: f32) -> f32 {\n let left = smoothstep(inner0, inner1, x);\n let right = 1.0 - smoothstep(outer0, outer1, x);\n return clamp(left * right, 0.0, 1.0);\n}\nfn min_px_fade01(v: f32, base_width_norm: f32, px_min: f32) -> f32 {\n let w_norm = max(base_width_norm, px_min * fwidth(v));\n return clamp(v / max(w_norm, 1e-6), 0.0, 1.0);\n}\nfn view_pos_from_viewZ(uv: vec2<f32>, dist_m: f32) -> vec3<f32> {\n let x_ndc = uv.x * 2.0 - 1.0;\n let y_ndc = (1.0 - uv.y) * 2.0 - 1.0;\n let x_view = x_ndc * dist_m / u_camera.proj[0][0];\n let y_view = y_ndc * dist_m / u_camera.proj[1][1];\n return vec3<f32>(x_view, y_view, -dist_m);\n}\nfn world_pos_from_view(vpos: vec3<f32>) -> vec3<f32> {\n return (u_camera.inverse_view * vec4<f32>(vpos, 1.0)).xyz;\n}\nfn normal_from_xy(xy: vec2<f32>) -> vec3<f32> {\n let xy_c = clamp(xy, vec2<f32>(-0.999), vec2<f32>(0.999));\n let z = sqrt(max(0.0, 1.0 - dot(xy_c, xy_c)));\n return normalize(vec3<f32>(xy_c, z));\n}\nfn rnm_blend(n1: vec3<f32>, n2: vec3<f32>) -> vec3<f32> {\n let t = vec3<f32>(n1.xy + n2.xy, n1.z * n2.z - dot(n1.xy, n2.xy));\n return normalize(t);\n}\nfn ts_normal_from_rg(rg: vec2<f32>) -> vec3<f32> {\n let xy = clamp(rg * 2.0 - 1.0, vec2<f32>(-0.999), vec2<f32>(0.999));\n let z = sqrt(max(0.0, 1.0 - dot(xy, xy)));\n return normalize(vec3<f32>(xy, z));\n}\nfn fresnel_schlick(cos_theta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);\n}\nfn distribution_ggx(NdotH: f32, alpha: f32) -> f32 {\n let a2 = alpha * alpha;\n let denom = (NdotH * NdotH) * (a2 - 1.0) + 1.0;\n return a2 / (PI * denom * denom);\n}\nfn G1_schlick_ggx(nDotX: f32, rough: f32) -> f32 {\n let r = rough + 1.0;\n let k = (r * r) / 8.0;\n return nDotX / (nDotX * (1.0 - k) + k);\n}\nfn geometry_smith(nDotV: f32, nDotL: f32, rough: f32) -> f32 {\n return G1_schlick_ggx(nDotV, rough) * G1_schlick_ggx(nDotL, rough);\n}\n// x = distance from the waterline ALONG the underlying surface (depth / sin(angle):\n// flat beach ~ horizontal distance, vertical wall = vertical distance down the wall);\n// y = wall01 (0 = flat ground, 1 = vertical surface) for band-width shaping.\nfn shore_distance_from_scene_normal(uv: vec2<f32>, depth_vertical: f32) -> vec2<f32> {\n let Nscene = normalize(textureSample(sceneWorldNormal, sceneSample, uv).xyz);\n let ny = abs(Nscene.y);\n let sin_t = sqrt(max(0.0, 1.0 - ny * ny));\n return vec2<f32>(depth_vertical / max(sin_t, 1e-3), smoothstep(0.5, 0.9, sin_t));\n}\nfn fix_nohit_depth(z_ndc: f32) -> f32 {\n // view_z_m is the gbuffer NDC depth; linearize to view-space meters, which is what\n // the thickness/fade math expects. Sky (1.0) lands on far naturally.\n return linearize_depth_0to1(z_ndc, u_camera.near, u_camera.far);\n}\n\nstruct FSOut {\n @location(0) color: vec4<f32>,\n @location(1) reactive: f32,\n};\n\n\n// `dbg` mirrors the retired water.wgsl waterDebug pixel-probe rows (1..15; row 0 is the\n// wrapper's target header). Callers that don't tap it pass a throwaway local - the\n// writes are dead-code-eliminated.\nfn shade_water(wp: vec3<f32>, clip: vec4<f32>, ctx: ShadeCtx, dbg: ptr<function, array<vec4<f32>, 16>>) -> vec4<f32> {\n let t_anim = ctx.t_anim;\n let transparency = ctx.transparency;\n let absorption_falloff_m = 5.0 * vec3<f32>(1.0, 1.0, 1.0);\n // Refraction advection + detail-normal constants shared by every water variant.\n let flow_tile = 0.9 * vec2<f32>(1.0, 1.0);\n let flow_phase_speed = vec2<f32>(0.6, 0.12) * 0.1;\n let refract_strength = 0.045;\n let wave_dir1 = vec2<f32>( 0.5, -0.2);\n let wave_dir2 = vec2<f32>(-0.5, 0.5);\n let inv_scale = 0.1;\n let time_scale = ctx.time_scale;\n let normal_strength_reflect = ctx.ns_reflect;\n let normal_strength_refract = ctx.ns_refract;\n\n let dims = vec2<f32>(textureDimensions(view_z_m));\n let uv = clip.xy / dims;\n let hack_offset = 0.1; // matches the retired water.wgsl (see its TODO)\n\n let dist_m = length((u_camera.view * vec4<f32>(wp, 1.0)).xyz);\n // Fade texture detail out with distance: far water is shaped by the geometric normal\n // alone (specular-AA variance handles the rest); kills far-field normal-map shimmer.\n let detail_fade = 1.0 - smoothstep(60.0, 260.0, dist_m);\n\n // Opaque (pre-water) linear depth at this pixel (meters)\n let zOpaque_here = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv).r) + hack_offset;\n let Pv_here = view_pos_from_viewZ(uv, zOpaque_here);\n let Pw_here = world_pos_from_view(Pv_here);\n let depth_world_here = max(0.0, wp.y - Pw_here.y);\n\n let zWater_here = linearize_depth_0to1(clip.z, u_camera.near, u_camera.far);\n let thickness = max(0.0, zOpaque_here - zWater_here);\n\n // Tangent frame on the (near-horizontal) Gerstner surface\n var T_w = normalize(vec3<f32>(1.0, 0.0, 0.0) - ctx.n_g * ctx.n_g.x);\n let B_w = normalize(cross(ctx.n_g, T_w));\n\n // (Flow-aligned stream UV stretching was tried and retired 19Jul26 - it never read\n // right and streams are parked for now; flowing placed water keeps only the correct\n // downstream refraction advection via flow_vec. The fall frame below stays: falls\n // and fountains are live features.)\n // Fall-SURFACE mapping: on steep/falling water every planar-XZ layer remaps into\n // the (across, downhill-scrolling) surface frame. An XZ mapping on a near-vertical\n // face both smears (degenerate footprint) and scrolls SIDEWAYS (a horizontal drift\n // has no downhill component) - that planar-on-vertical mismatch, not any Y-up/Z-up\n // axis mixup, is the perennial \"water flows sideways on falls\".\n // Anisotropic on purpose: falling water elongates features ALONG the fall.\n // Across compresses (x2.5 frequency), along stretches (x0.18) - equal scales made\n // narrow sheets read as HORIZONTAL stripes (across-axis sub-period while the along\n // axis crossed several foam periods).\n let p_fall = vec2<f32>(\n dot(wp, ctx.fall_b) * 2.5,\n (dot(wp, ctx.fall_t) - ctx.fall_scroll * t_anim) * 0.18,\n );\n\n var fresnel: f32;\n var Vw: vec3<f32>;\n var Nw_reflect: vec3<f32>;\n var Nw_refract: vec3<f32>;\n var Nw_specular: vec3<f32>;\n var cosI_reflect: f32;\n var cosI_refract: f32;\n var wave_xy_reflect: vec2<f32>;\n {\n let uv_base = mix(wp.xz, p_fall, ctx.fall01) * inv_scale;\n // Falls remove the sideways wind-chop scroll entirely: t1/t2 are WORLD-frame\n // vectors and the fall UVs are a rotated surface frame - a world-frame drift\n // added to rotated coords slides sideways.\n let cross_damp = 1.0 - ctx.fall01;\n let t1 = t_anim * wave_dir1 * time_scale * cross_damp;\n let t2 = t_anim * wave_dir2 * time_scale * cross_damp;\n let rg1 = textureSample(normalA, normalSample, uv_base + t1 * inv_scale).rg * 2.0 - 1.0;\n let rg2 = textureSample(normalB, normalSample, uv_base + t2 * inv_scale).rg * 2.0 - 1.0;\n let n1 = normal_from_xy(rg1);\n let n2 = normal_from_xy(rg2);\n let n_ts_base = -rnm_blend(n1, n2);\n let n_ts_reflect = normal_from_xy(n_ts_base.xy * normal_strength_reflect * detail_fade);\n let n_ts_refract = normal_from_xy(n_ts_base.xy * normal_strength_refract * detail_fade);\n wave_xy_reflect = n_ts_reflect.xy;\n let n_macro = -n_ts_base;\n\n // Halved vs water.wgsl's 0.22: on the displaced ocean the micro layer reads as\n // near-field boiling on top of the real wave motion (lakes are flat, oceans aren't).\n let micro_scale = 2.5;\n let micro_strength = ctx.micro_strength * detail_fade;\n let xz_micro = mix(wp.xz, p_fall, ctx.fall01);\n let uv_m1 = xz_micro / micro_scale + t_anim * vec2<f32>( 0.31, 0.17) * 0.08 * cross_damp;\n let uv_m2 = xz_micro / micro_scale + t_anim * vec2<f32>(-0.23, 0.29) * 0.08 * cross_damp;\n let n_micro1 = ts_normal_from_rg(textureSample(microNormalA, normalSample, uv_m1).rg);\n let n_micro2 = ts_normal_from_rg(textureSample(microNormalB, normalSample, uv_m2).rg);\n let n_micro = rnm_blend(n_micro1, n_micro2);\n let n_ts_spec_base = rnm_blend(\n normal_from_xy(n_macro.xy * detail_fade),\n normal_from_xy(n_micro.xy * micro_strength),\n );\n\n Nw_reflect = normalize(n_ts_reflect.x * T_w + n_ts_reflect.y * B_w + n_ts_reflect.z * ctx.n_g);\n Nw_refract = normalize(n_ts_refract.x * T_w + n_ts_refract.y * B_w + n_ts_refract.z * ctx.n_g);\n Nw_specular = normalize(n_ts_spec_base.x * T_w + n_ts_spec_base.y * B_w + n_ts_spec_base.z * ctx.n_g);\n\n Vw = normalize(u_camera.camera_position - wp);\n cosI_refract = clamp(dot(Nw_refract, Vw), 0.0, 1.0);\n cosI_reflect = clamp(dot(Nw_reflect, Vw), 0.0, 1.0);\n\n let f0 = pow((1.0 - p.ior) / (1.0 + p.ior), 2.0);\n let floor_cosI = min(cosI_refract, 0.2);\n fresnel = f0 + (1.0 - f0) * pow(1.0 - floor_cosI, 5.0);\n }\n\n let depth_smooth_range = 0.05;\n let preMask = smoothstep(0.0, depth_smooth_range, thickness);\n\n // Optical path length along the transmitted ray\n let eta = 1.0 / p.ior;\n let sin2T = eta * eta * max(0.0, 1.0 - cosI_refract * cosI_refract);\n let cosT = sqrt(max(0.0, 1.0 - sin2T));\n let T_ref = normalize(eta * (-Vw) + (eta * cosI_refract - cosT) * Nw_refract);\n let T_v = normalize((u_camera.view * vec4<f32>(T_ref, 0.0)).xyz);\n let cos_to_viewZ = abs(T_v.z);\n let L_ray = thickness / max(1e-4, cos_to_viewZ);\n let optical_thickness = remap01(L_ray, p.refract_thick_fade.x, p.refract_thick_fade.y);\n\n // Refraction offset via flow advection, depth/height gated (water.wgsl scheme)\n var off_px_raw: vec2<f32>;\n {\n // MINUS: subtracting from the sample coordinate advects the pattern TOWARD\n // +flow_vec. The retired water.wgsl used plus (pattern glides toward -flow) -\n // invisible on ponds/ocean where drift direction is arbitrary, but on streams\n // the refraction layer visibly ran UPSTREAM against the foam.\n let uvF = mix(wp.xz, p_fall, ctx.fall01) * flow_tile\n - t_anim * ctx.flow_vec * (1.0 - ctx.fall01);\n let flow_raw = textureSample(refractNoiseNormal, normalSample, uvF).rg * 2.0 - 1.0;\n let flow_dir = normalize(vec2<f32>(flow_raw.x, flow_raw.y + 1e-6));\n let phase = fract(t_anim * flow_phase_speed.x);\n let w = 1.0 - abs(phase * 2.0 - 1.0);\n let nA_rg = textureSample(refractNoiseNormal, normalSample, uvF + flow_dir * flow_phase_speed.y).rg * 2.0 - 1.0;\n let nB_rg = textureSample(refractNoiseNormal, normalSample, uvF - flow_dir * flow_phase_speed.y).rg * 2.0 - 1.0;\n let nA_ts = vec3<f32>(nA_rg, sqrt(max(0.0, 1.0 - dot(nA_rg, nA_rg))));\n let nB_ts = vec3<f32>(nB_rg, sqrt(max(0.0, 1.0 - dot(nB_rg, nB_rg))));\n let n_ts_adv = normalize(mix(nA_ts, nB_ts, w));\n // Floor at 3m (water.wgsl uses 1m): the 1/z gain made near-camera refraction wobble\n // dominate the near field on the displaced surface.\n off_px_raw = n_ts_adv.xy * refract_strength * optical_thickness / max(zWater_here, ctx.refract_z_floor_m) * preMask;\n }\n // Waterfall aeration adds its own anisotropic streak refraction (prologue-computed).\n off_px_raw += ctx.fall_off_px * preMask;\n let uv_off0 = clamp(uv + off_px_raw, vec2<f32>(0.0), vec2<f32>(1.0));\n\n let zScene0 = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv_off0).r) + hack_offset;\n let depthGate0 = smoothstep(0.0, depth_smooth_range, zScene0 - zWater_here);\n\n let height_eps = 0.015;\n let Pv0 = view_pos_from_viewZ(uv_off0, zScene0);\n let Pw0 = world_pos_from_view(Pv0);\n let heightDiff0 = wp.y - Pw0.y;\n let gateW = max(fwidth(heightDiff0) * 2.0, 0.01);\n // Falling sheets are near-vertical: the water-above-scene height gate is meaningless\n // there, so the waterfall mask bypasses it (water.wgsl heritage).\n var heightGate0 = smoothstep(0.0, gateW, heightDiff0 - height_eps);\n if (ctx.fall01 > 1e-3) { heightGate0 = 1.0; }\n\n let off_px = off_px_raw * depthGate0 * heightGate0;\n let uv_off = clamp(uv + off_px, vec2<f32>(0.0), vec2<f32>(1.0));\n\n let zScene1 = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv_off).r) + hack_offset;\n let refractColor = textureSample(sceneColor, sceneSample, uv_off).rgb;\n\n // --- Reflections: graded skybox + planar scene reflection ---\n let Rw = normalize(reflect(-Vw, Nw_reflect));\n let c = cos(p.skybox_yaw);\n let s = sin(p.skybox_yaw);\n let rot_x = Rw.x * c - Rw.z * s;\n let rot_z = Rw.x * s + Rw.z * c;\n let dir_y = Rw.y + p.skybox_horizon_tilt_sin;\n let flip = vec3<f32>(1.0, 1.0, -1.0);\n let Rw_rot = normalize(vec3<f32>(rot_x, dir_y, rot_z) * flip);\n var skybox_reflect_color = textureSample(skybox_texture, sceneSample, Rw_rot).rgb;\n skybox_reflect_color = apply_skybox_grade(skybox_reflect_color);\n\n let planar_proj = planar_uv(wp);\n let planar_uv_d = clamp(planar_proj.xy + wave_xy_reflect * planar.distortion,\n vec2<f32>(0.0), vec2<f32>(1.0));\n let planar_fade = planar_edge_fade(planar_uv_d) * planar_proj.z * f32(planar.is_active);\n let planar_color = textureSample(planarReflection, sceneSample, planar_uv_d).rgb;\n // The planar image is mirrored about plane_y (= sea level): displaced crests may sit up to\n // total_amplitude away, so widen the height fade accordingly instead of water.wgsl's 0.5..2.\n let hf_a = ctx.planar_hf_a;\n let hf_b = ctx.planar_hf_b;\n let height_fade = 1.0 - smoothstep(hf_a, hf_b, abs(wp.y - planar.plane_y));\n let planar_weight = planar_fade * planar.strength * height_fade * ctx.planar_scale;\n\n // --- Direct sun glint (GGX with variance AA; roughness widens with distance) ---\n var sun_specular_color = vec3<f32>(0.0);\n {\n let N = Nw_specular;\n let V = Vw;\n let L = normalize(-u_directional_light.light_dir.xyz);\n let NdotL = max(dot(N, L), 0.0);\n let NdotV = max(dot(N, V), 0.0);\n let dNx = dpdx(N);\n let dNy = dpdy(N);\n let variance = clamp(dot(dNx, dNx) + dot(dNy, dNy), 0.0, 1.0);\n if (NdotL > 0.0 && NdotV > 0.0) {\n let sun_roughness = mix(0.03, 0.16, smoothstep(30.0, 400.0, dist_m));\n let sun_strength = 0.5;\n let r_p = clamp(sun_roughness, 0.04, 1.0);\n var alpha = r_p * r_p;\n alpha = sqrt(alpha * alpha + 0.5 * variance);\n let alphaSun = 0.0025;\n alpha = sqrt(alpha * alpha + alphaSun * alphaSun);\n let H = normalize(L + V);\n let NdotH = max(dot(N, H), 0.0);\n let f0_s = pow((1.0 - p.ior) / (1.0 + p.ior), 2.0);\n let F = fresnel_schlick(max(dot(H, V), 0.0), vec3<f32>(f0_s));\n let D = distribution_ggx(NdotH, alpha);\n let G = geometry_smith(NdotV, NdotL, r_p);\n let spec = (D * G) / max(4.0 * NdotL * NdotV, 1e-4);\n let sun_radiance = u_directional_light.light_color_with_intensity.rgb\n * sun_strength * u_directional_light.sun_specular_scale;\n // Firefly clamp: unbounded GGX peaks pop in/out per frame on animated normals\n // (single-pixel sparkle reads as water \"shaking\"; stability_probe max_px hit\n // the 3.8 luminance ceiling before this).\n sun_specular_color = min(sun_radiance * F * spec * NdotL, vec3<f32>(6.0));\n }\n }\n\n // --- Beer\u2013Lambert absorption + shallow/deep ramp (colors from ocean params) ---\n let refractedThickness = max(0.0, zScene1 - zWater_here);\n let refractedLRay = refractedThickness / max(1e-4, cos_to_viewZ);\n var transmittance: vec3<f32>;\n {\n let k = 1.0 / max(absorption_falloff_m, vec3<f32>(1e-6));\n let t_lin = clamp(vec3<f32>(1.0) - k * refractedLRay, vec3<f32>(0.0), vec3<f32>(1.0));\n let t_exp = exp(-k * refractedLRay);\n let Lm = (absorption_falloff_m.x + absorption_falloff_m.y + absorption_falloff_m.z) * (1.0 / 3.0);\n let w = smoothstep(0.6 * Lm, 2.0 * Lm, refractedLRay);\n transmittance = mix(t_lin, t_exp, vec3<f32>(w));\n }\n // Aerated whitewater doesn't absorb (waterfall).\n transmittance = mix(transmittance, vec3<f32>(1.0), ctx.fall01);\n\n let shallow_color = ctx.shallow_color;\n let deep_color = ctx.deep_color;\n let shallow_m = ctx.shallow_m;\n let deep_m = ctx.deep_m;\n let clear_shallow_m = shallow_m * 0.2;\n\n let w_depth = remap01(refractedLRay, shallow_m, deep_m) * transparency;\n let rampColor = mix(shallow_color, deep_color, w_depth);\n let shallowColorFactor = clamp((heightDiff0 - clear_shallow_m) / max(shallow_m - clear_shallow_m, 1e-4), 0.0, 1.0);\n let shallowTintMul = mix(vec3<f32>(1.0), shallow_color, shallowColorFactor);\n let refractAttenuated = mix(deep_color, refractColor * shallowTintMul, transmittance);\n var refractWithTransparency = mix(rampColor, refractAttenuated, transparency);\n\n // SSS crest boost: sun shining through wave tops toward the viewer.\n {\n let L_travel = normalize(u_directional_light.light_dir.xyz);\n let backlight = pow(max(dot(-Vw, normalize(L_travel + ctx.n_g * 0.4)), 0.0), 3.0);\n let sss = backlight * ctx.crest01 * (1.0 - fresnel);\n refractWithTransparency += shallow_color\n * u_directional_light.light_color_with_intensity.rgb * (0.25 * sss);\n }\n\n // Depth-fade toward the deep color (world-height based, same shape as water.wgsl)\n let depth_fade_factor = 1.5;\n let fade_start_m = shallow_m;\n let fade_end_m = deep_m * depth_fade_factor;\n let depth_fade = smootherstep(fade_start_m, fade_end_m, depth_world_here);\n let depth_color_fade = smootherstep(fade_start_m,\n fade_end_m - (fade_end_m - fade_start_m) * 0.5, depth_world_here);\n let depth_color = mix(shallow_color, deep_color, depth_color_fade);\n var refractDepthFaded = mix(refractWithTransparency, depth_color, depth_fade);\n // No deep-color fade on a falling sheet (it has no meaningful water column).\n if (ctx.fall01 > 1e-3) { refractDepthFaded = refractWithTransparency; }\n\n // Macro reflectivity by angle\n let headOnR = 0.08;\n let grazingR = 0.92;\n let wAngle = smoothstep(0.30, 0.95, 1.0 - cosI_reflect);\n var reflectWeight = clamp(mix(headOnR, grazingR, wAngle), 0.0, 0.98);\n // Waterfall whitewater: aeration mattes the mirror and lightens the sheet.\n reflectWeight *= mix(1.0, 0.35, ctx.fall01);\n\n var tintedReflectColor: vec3<f32>;\n {\n let sat_factor = max(0.0, u_directional_light.ambient_tint.a);\n let lum = dot(skybox_reflect_color, LUMA709);\n let sat_skybox = mix(vec3<f32>(lum), skybox_reflect_color, sat_factor)\n * u_directional_light.ambient_tint.rgb;\n tintedReflectColor = mix(sat_skybox, planar_color, planar_weight) + sun_specular_color;\n }\n\n var color = tintedReflectColor * reflectWeight + refractDepthFaded * (1.0 - reflectWeight);\n // Aeration whitening: fall_white01 carries the layered streak + Fresnel-edge mix\n // (placed wrapper); the flat 5% is the floor so bare fall01 callers still whiten.\n color = mix(color, vec3<f32>(0.96, 0.965, 0.97), max(ctx.fall01 * 0.05, ctx.fall_white01));\n let dbg_after_reflect_mix = color;\n\n // --- Foam: crest whitecaps (fold-driven) + shore contact band ---\n var dbg_foam_alpha = 0.0;\n {\n let foam_tint = vec3<f32>(0.96, 0.965, 0.97);\n let foam_tile_big = 0.98;\n let foam_tile_small = 5.0;\n let foam_drift = ctx.foam_drift * t_anim;\n // Unlike water.wgsl, NO screen-space refraction wobble in these UVs: on open-ocean\n // whitecap patches it re-jitters the whole foam pattern every frame (frame-diff\n // verified \u2014 the foam areas flashed wholesale; a thin shore band never showed it).\n let xz_foam = mix(wp.xz, p_fall, ctx.fall01);\n // Same frame rule: the world-frame drift zeroes out on falls (their scroll is\n // already inside the rotated frame).\n let foam_drift_w = foam_drift * (1.0 - ctx.fall01);\n let uv_big = xz_foam * foam_tile_big - foam_drift_w;\n let uv_small = xz_foam * foam_tile_small + foam_drift_w * 1.7;\n let atlas_big = textureSample(foamRGB, normalSample, uv_big);\n let atlas_small = textureSample(foamRGB, normalSample, uv_small);\n let heavy = max(atlas_big.r, atlas_big.g);\n let detail = max(atlas_small.r, atlas_small.b);\n let breakup = mix(heavy, detail, 0.6) * (0.3 + 0.7 * atlas_small.g);\n\n // Shore/contact band in WORLD distance-to-waterline: d_shore = depth/slope\n // reconstructs the horizontal distance to the waterline independent of slope,\n // so ONE formulation covers flat beaches, ramps, and steep walls (this replaces\n // the old optical-thickness band + steep-only gate that erased beach foam).\n // The outer edge laps in/out on a world-anchored phase - no screen-space inputs,\n // so the foam pattern can't re-jitter per frame (see the wobble note above).\n let ds = shore_distance_from_scene_normal(uv, depth_world_here);\n let d_shore = ds.x;\n // No water-side band on steep surfaces at all (it never reads right at grazing\n // angles); walls get their contact from the grid collar + object-side meniscus.\n let edge_scale = mix(1.0, 0.0, ds.y);\n let lap_phase = t_anim * 1.7\n + dot(wp.xz, vec2<f32>(0.021, 0.017))\n + atlas_big.g * 6.283;\n let lap_edge = mix(0.45, 1.15, 0.5 + 0.5 * sin(lap_phase)) * edge_scale;\n let band = 1.0 - smoothstep(lap_edge * 0.3, lap_edge, d_shore);\n // Faint residue line lingering just beyond the lapping edge (water pulling back).\n let residue =\n (1.0 - smoothstep(0.05, 0.45, abs(d_shore - lap_edge * 1.3))) * 0.35;\n // Band breakup at HALF the whitecap detail frequency: the 5/m tile reads busy on\n // a meter-wide strip.\n let uv_band = xz_foam * (foam_tile_small * 0.5) - foam_drift_w;\n let atlas_band = textureSample(foamRGB, normalSample, uv_band);\n let band_breakup =\n mix(heavy, max(atlas_band.r, atlas_band.b), 0.6) * (0.3 + 0.7 * atlas_band.g);\n let shore_foam = clamp(\n (band * pow(band_breakup, 1.5)\n + residue * band_breakup * band_breakup * (1.0 - ds.y)) * 0.85,\n 0.0,\n 0.8,\n );\n\n // Crest whitecaps: Gerstner fold gates the same foam texture; detail_fade keeps the\n // far field clean (far crests read through the normal + glint instead).\n let crest_mask = ctx.crest_foam_mask;\n let crest_foam = clamp(crest_mask * (0.35 + 0.65 * pow(breakup, 1.2)), 0.0, 0.9)\n * max(detail_fade, 0.25);\n // FFT Jacobian whitecaps (temporally accumulated in ocean_derivatives.wgsl),\n // broken up by the same foam texture so they don't read as flat decals.\n let fft_whitecap = clamp(ctx.fft_foam * (0.4 + 0.6 * breakup), 0.0, 0.9);\n // Wake foam trail (ocean_ripples accumulation): its own low-frequency breakup \u2014\n // the 5/m detail tile reads as noise on the meters-wide trail behind a wader\n // (~0.4/m, user-tuned x12 below the whitecap detail).\n let uv_wake = wp.xz * (foam_tile_small * 0.083) + foam_drift * 1.7;\n let atlas_wake = textureSample(foamRGB, normalSample, uv_wake);\n let wake_breakup =\n mix(heavy, max(atlas_wake.r, atlas_wake.b), 0.6) * (0.3 + 0.7 * atlas_wake.g);\n let wake_foam = clamp(ctx.ripple_foam * (0.5 + 0.5 * wake_breakup), 0.0, 0.9);\n\n let foam_alpha = max(max(shore_foam, wake_foam), max(crest_foam, fft_whitecap));\n color = mix(color, foam_tint, foam_alpha);\n dbg_foam_alpha = foam_alpha;\n }\n\n // Shore fade in screen space (\u22653px on-screen width)\n let shore_fade = min_px_fade01(optical_thickness, 0.1, 3.0);\n color = mix(refractColor, color, shore_fade);\n let dbg_after_foam_shore = color;\n\n // Underside (submerged camera under a near-horizontal surface): foam floats ON the\n // surface and the sky cannot be REFLECTED from below, so the composed topside lobes\n // are all wrong there. AAA model, overwrite-style: Snell's window (the refracted\n // above-water scene - the scene buffer behind the surface IS that) inside the\n // critical angle, total-internal-reflection murk (the underwater fog color, which\n // the submersion grade sets to the water tint) outside it.\n if (ctx.underside01 > 0.5) {\n // The underside look IS the animated wave normals (Subnautica/SoT): boost the\n // detail perturbation and derive EVERYTHING from it - the window/TIR boundary\n // then ripples per-pixel instead of sitting as a flat gradient.\n // Distance-ATTENUATED boost (never zero): amplifying the detail normal also\n // amplifies its texel noise, but collapsing to the smooth geometric normal\n // makes the window/TIR frontier a clean cutoff curve - the REAL softness of\n // that boundary is waves shredding it, so some waviness must survive at range\n // (the thick murk covers the residual noise).\n let uw_boost = mix(0.5, 1.5, 1.0 - smoothstep(12.0, 45.0, dist_m));\n let n_uw = normalize(ctx.n_g + (Nw_refract - ctx.n_g) * (1.0 + uw_boost));\n let cos_up = abs(dot(n_uw, Vw));\n // PHYSICAL water->air Fresnel transmittance: high near vertical, dives steeply\n // but smoothly toward the critical angle, exactly zero beyond (TIR). No ad-hoc\n // smoothstep band - per-pixel wavy normals provide the natural boundary break-up.\n let sin_t2 = (1.333 * 1.333) * max(0.0, 1.0 - cos_up * cos_up);\n var window01 = 0.0;\n if (sin_t2 < 1.0) {\n let cos_t = sqrt(1.0 - sin_t2);\n let rs = (1.333 * cos_up - cos_t) / (1.333 * cos_up + cos_t);\n let rp = (1.333 * cos_t - cos_up) / (1.333 * cos_t + cos_up);\n window01 = 1.0 - 0.5 * (rs * rs + rp * rp);\n }\n // Full-strength wavy refraction - comparable to the topside look-down, not the\n // optically-gated leftovers. (SampleLevel: textureSample is illegal here.)\n let refr_uv = clamp(\n uv + n_uw.xz * 0.055 + off_px_raw,\n vec2<f32>(0.0),\n vec2<f32>(1.0),\n );\n let refr = textureSampleLevel(sceneColor, sceneSample, refr_uv, 0.0).rgb;\n // Rim glow hugs the critical angle itself (sin_t2 = 1).\n let rim = 1.0 - clamp(abs(sin_t2 - 1.0) / 0.22, 0.0, 1.0);\n // Bright rippling ceiling: the TIR zone brightens toward up-facing wave slopes\n // and shimmers with the same normals; the window shows the warped above world.\n let ceil_bright = 1.0 + 0.25 * (n_uw.x + n_uw.z) + 0.18 * cos_up;\n color = mix(fog.color * ceil_bright, refr * vec3<f32>(0.85, 0.95, 1.05), window01)\n + vec3<f32>(0.10, 0.12, 0.13) * rim;\n }\n\n if (u_directional_light.light_debug_mode == 6u) {\n return vec4<f32>(tintedReflectColor, 1.0);\n }\n if (u_directional_light.light_debug_mode == 7u) {\n return vec4<f32>(refractDepthFaded, 1.0);\n }\n if (u_directional_light.light_debug_mode == 16u) {\n return vec4<f32>(planar_color, 1.0);\n }\n\n // Air fog from camera to surface (post fog pass already ran; water fogs itself)\n var fog_a: f32;\n {\n switch (fog.mode) {\n default { fog_a = clamp((dist_m - fog.start) / max(fog.end_ - fog.start, 1e-6), 0.0, 1.0); }\n case 1u { fog_a = 1.0 - exp(-fog.density * dist_m); }\n case 2u { fog_a = 1.0 - exp(-fog.density * dist_m * fog.density * dist_m); }\n }\n fog_a = clamp(fog_a, 0.0, 1.0);\n if (fog.height_enabled != 0u) {\n let lo = min(fog.height_bottom, fog.height_top);\n let hi = max(fog.height_bottom, fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, wp.y);\n let w = clamp(fog.height_weight, 0.0, 1.0);\n fog_a *= mix(1.0, h, w);\n }\n }\n\n let final_rgb = mix(color, fog.color, fog_a);\n (*dbg)[1] = vec4<f32>(skybox_reflect_color, dot(skybox_reflect_color, LUMA709));\n (*dbg)[2] = vec4<f32>(planar_color, planar_weight);\n (*dbg)[3] = vec4<f32>(sun_specular_color, dot(sun_specular_color, LUMA709));\n (*dbg)[4] = vec4<f32>(tintedReflectColor, reflectWeight);\n (*dbg)[5] = vec4<f32>(refractColor, shore_fade);\n (*dbg)[6] = vec4<f32>(refractDepthFaded, dot(refractDepthFaded, LUMA709));\n (*dbg)[7] = vec4<f32>(shallow_color, shallow_m);\n (*dbg)[8] = vec4<f32>(deep_color, deep_m);\n (*dbg)[9] = vec4<f32>(fresnel, reflectWeight, 0.0, cosI_reflect);\n (*dbg)[10] = vec4<f32>(thickness, L_ray, refractedLRay, depth_world_here);\n (*dbg)[11] = vec4<f32>(ctx.fall01, dbg_foam_alpha, depth_fade, fog_a);\n (*dbg)[12] = vec4<f32>(tintedReflectColor, dot(tintedReflectColor, LUMA709));\n (*dbg)[13] = vec4<f32>(dbg_after_reflect_mix, dot(dbg_after_reflect_mix, LUMA709));\n (*dbg)[14] = vec4<f32>(dbg_after_foam_shore, dot(dbg_after_foam_shore, LUMA709));\n (*dbg)[15] = vec4<f32>(final_rgb, dot(final_rgb, LUMA709));\n return vec4<f32>(final_rgb, 1.0);\n\n}\n"},{"label":"ocean_surface","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// ocean_gerstner.wgsl \u2014 shared Gerstner displacement core.\n// Concatenated (Rust-side) in front of ocean_surface.wgsl and ocean_mv.wgsl so the color and\n// motion-vector passes displace identically. Phases are integrated in f64 on the CPU and\n// uploaded pre-wrapped per wave (cur + prev frame), so there is no f32 time blowup and the MV\n// pass gets exact previous-frame positions without history textures.\n// The wave table is derived deterministically from a few sim attributes (see\n// ocean_pass.rs::derive_wave_table); a future Fp buoyancy query mirrors that derivation.\n\nconst OCEAN_WAVE_COUNT: u32 = 8u;\n\nstruct OceanWave {\n // xy = unit travel dir (render XZ), z = amplitude (m), w = angular wavenumber k (rad/m)\n dir_amp: vec4<f32>,\n // x = phase at t (rad, wrapped), y = phase at prev t, z = Q (chop), w = wavelength (m)\n phase_q: vec4<f32>,\n};\n\nstruct OceanParams {\n waves: array<OceanWave, 8>,\n sea_level_y: f32, // render meters (Y-up)\n disp_fade_start: f32, // radial m: displacement starts fading (stable horizon + MV calm)\n disp_fade_end: f32, // radial m: fully flat beyond this\n cell_growth: f32, // local mesh cell size ~ max(cell_min, r * cell_growth)\n shallow_color: vec3<f32>,\n transparency: f32, // 0..1 refraction mix\n deep_color: vec3<f32>,\n color_mix_range_m: f32, // water depth over which shallow blends to deep\n total_amplitude: f32, // m; sum of derived wave amplitudes (crest01 normalizer)\n cell_min: f32, // m; finest mesh cell near the camera\n reactive: f32, // reactive-mask value (lower than flat water: we write real MVs)\n crest_foam: f32, // 0..1 crest whitecap strength\n // FFT detail cascades (Phase 2, desktop tier): x/y = 1/patch_size per cascade,\n // z/w = radial fade distance per cascade.\n fft0: vec4<f32>,\n // x = fft enabled (0/1), y = foam gain, z = detail normal gain, w = unused.\n fft1: vec4<f32>,\n // Interactive wake ripples (Phase C, ocean_ripples.rs). xy = grid window center\n // (render XZ m), z = 1/extent, w = height gain (0 = disabled).\n ripple0: vec4<f32>,\n // xy = previous frame's window center (MV sampling), z = foam gain, w = unused.\n ripple1: vec4<f32>,\n};\n\n@group(1) @binding(30) var<uniform> ocean: OceanParams;\n\nfn ocean_global_fade(r_m: f32) -> f32 {\n return 1.0 - smoothstep(ocean.disp_fade_start, ocean.disp_fade_end, r_m);\n}\n\n// Attenuate waves the local mesh density cannot represent (radial rings grow geometrically).\n// Full contribution only at >= 6 vertices per wavelength: with fewer (the previous 2..4\n// window), phase advance makes the piecewise-linear surface between vertices boil \u2014 the\n// \"whole ocean shaking\" artifact. 3 cells/lambda (Nyquist-ish) is fully out.\nfn ocean_wave_atten(lambda_m: f32, r_m: f32) -> f32 {\n let local_cell = max(ocean.cell_min, r_m * ocean.cell_growth);\n return smoothstep(4.0, 8.0, lambda_m / local_cell);\n}\n\n// Displaced world position for the base (undisplaced) point. use_prev picks the\n// previous-frame phase set (motion vectors).\nfn ocean_displace(base_xz: vec2<f32>, r_m: f32, use_prev: bool) -> vec3<f32> {\n var pos = vec3<f32>(base_xz.x, ocean.sea_level_y, base_xz.y);\n let global_fade = ocean_global_fade(r_m);\n if (global_fade <= 0.0) {\n return pos;\n }\n for (var i = 0u; i < OCEAN_WAVE_COUNT; i++) {\n let w = ocean.waves[i];\n let amp = w.dir_amp.z * ocean_wave_atten(w.phase_q.w, r_m) * global_fade;\n let k = w.dir_amp.w;\n let phase_t = select(w.phase_q.x, w.phase_q.y, use_prev);\n let theta = k * dot(w.dir_amp.xy, base_xz) - phase_t;\n let c = cos(theta);\n let s = sin(theta);\n let q = w.phase_q.z;\n pos.x += q * amp * w.dir_amp.x * c;\n pos.z += q * amp * w.dir_amp.y * c;\n pos.y += amp * s;\n }\n pos.y += ocean_ripple_height(base_xz, use_prev);\n return pos;\n}\n\n// Analytic surface normal + fold. xyz = normal (Y-up), w = fold01 (Jacobian-style crest\n// pinch proxy: 0 flat, ->1 at pinched crests; drives whitecap foam).\nfn ocean_normal_fold(base_xz: vec2<f32>, r_m: f32) -> vec4<f32> {\n var nx = 0.0;\n var nz = 0.0;\n var pinch = 0.0; // sum of Q k A sin(theta): 1 - pinch is the Gerstner \"fold\" term\n let global_fade = ocean_global_fade(r_m);\n if (global_fade > 0.0) {\n for (var i = 0u; i < OCEAN_WAVE_COUNT; i++) {\n let w = ocean.waves[i];\n let amp = w.dir_amp.z * ocean_wave_atten(w.phase_q.w, r_m) * global_fade;\n let k = w.dir_amp.w;\n let theta = k * dot(w.dir_amp.xy, base_xz) - w.phase_q.x;\n let c = cos(theta);\n let s = sin(theta);\n let ka = k * amp;\n nx -= w.dir_amp.x * ka * c;\n nz -= w.dir_amp.y * ka * c;\n pinch += w.phase_q.z * ka * s;\n }\n }\n let n = normalize(vec3<f32>(nx, 1.0 - pinch, nz));\n // Only well-pinched crests foam: the threshold keeps mid-slope water clean.\n let fold = clamp(pinch * 1.15 - 0.28, 0.0, 1.0);\n return vec4<f32>(n, fold);\n}\n\n// ---- FFT detail cascades (ocean_fft.rs; zero-filled dummies when disabled) ----\n// disp maps: xyz = (Dx, Dy, Dz) meters, tiling with 1/patch_size uv scale.\n@group(1) @binding(31) var ocean_fft_disp0: texture_2d<f32>;\n@group(1) @binding(32) var ocean_fft_disp1: texture_2d<f32>;\n@group(1) @binding(35) var ocean_fft_sampler: sampler;\n\nfn ocean_fft_fade(r_m: f32, fade_r: f32) -> f32 {\n return 1.0 - smoothstep(fade_r * 0.5, fade_r, r_m);\n}\n\n// ---- Interactive wake ripples (ocean_ripples.rs; zero dummy when disabled) ----\n// World-anchored toroidal grid: uv = world_xz / extent (Repeat sampler wraps). Texel\n// r = height m, g = previous-frame height m, b = wake foam. Validity fades to zero\n// toward the window edge (the state there is either zero or another world period's).\n@group(1) @binding(38) var ocean_ripple_tex: texture_2d<f32>;\n\nfn ocean_ripple_fade(base_xz: vec2<f32>, center: vec2<f32>) -> f32 {\n let d = abs(base_xz - center) * ocean.ripple0.z;\n return 1.0 - smoothstep(0.38, 0.47, max(d.x, d.y));\n}\n\nfn ocean_ripple_sample(base_xz: vec2<f32>) -> vec3<f32> {\n if (ocean.ripple0.w <= 0.0) {\n return vec3<f32>(0.0);\n }\n let fade = ocean_ripple_fade(base_xz, ocean.ripple0.xy);\n if (fade <= 0.0) {\n return vec3<f32>(0.0);\n }\n let s = textureSampleLevel(ocean_ripple_tex, ocean_fft_sampler, base_xz * ocean.ripple0.z, 0.0);\n return vec3<f32>(s.r, s.g, s.b) * fade;\n}\n\n// Ripple height for displacement; use_prev picks the previous-frame surface (exact MVs),\n// faded by the PREVIOUS window since that's the region it was simulated for.\nfn ocean_ripple_height(base_xz: vec2<f32>, use_prev: bool) -> f32 {\n if (ocean.ripple0.w <= 0.0) {\n return 0.0;\n }\n let center = select(ocean.ripple0.xy, ocean.ripple1.xy, use_prev);\n let fade = ocean_ripple_fade(base_xz, center);\n if (fade <= 0.0) {\n return 0.0;\n }\n let s = textureSampleLevel(ocean_ripple_tex, ocean_fft_sampler, base_xz * ocean.ripple0.z, 0.0);\n return select(s.r, s.g, use_prev) * fade * ocean.ripple0.w;\n}\n\n// Mip level matching the local mesh cell (VS has no derivatives): waves shorter than the\n// vertex spacing average out in the mip chain instead of aliasing into per-frame jitter.\nfn ocean_fft_lod(inv_l: f32, r_m: f32) -> f32 {\n let texel_m = 1.0 / (inv_l * 256.0);\n let local_cell = max(ocean.cell_min, r_m * ocean.cell_growth);\n return clamp(log2(max(local_cell / texel_m, 1.0)), 0.0, 8.0);\n}\n\n// Explicit-LOD sampling only: legal in any control flow.\nfn ocean_fft_displace(base_xz: vec2<f32>, r_m: f32) -> vec3<f32> {\n if (ocean.fft1.x < 0.5) {\n return vec3<f32>(0.0);\n }\n let f0 = ocean_fft_fade(r_m, ocean.fft0.z);\n let f1 = ocean_fft_fade(r_m, ocean.fft0.w);\n var d = vec3<f32>(0.0);\n if (f0 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.x, r_m);\n d += textureSampleLevel(ocean_fft_disp0, ocean_fft_sampler, base_xz * ocean.fft0.x, lod).xyz * f0;\n }\n if (f1 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.y, r_m);\n d += textureSampleLevel(ocean_fft_disp1, ocean_fft_sampler, base_xz * ocean.fft0.y, lod).xyz * f1;\n }\n return d;\n}\n\n// water_shading_body.wgsl\n// Shared water surface shading: consumed by ocean_surface.wgsl (global displaced ocean)\n// and placed_water.wgsl (placed still/stream/fall water meshes). Each wrapper declares\n// the group(0)/(1)/(2) bindings by the SAME names (WGSL module scope is order-free), does\n// its own geometry/normal prologue, fills ShadeCtx, and wraps the returned color with its\n// reactive-mask policy. Extracted verbatim from ocean_surface.wgsl (itself adapted from\n// the retired water.wgsl - archived at agent_docs/shader_reference/water_legacy.wgsl).\n\nstruct ShadeCtx {\n n_g: vec3<f32>, // geometric water normal (waves folded in for the ocean)\n crest01: f32, // gerstner crest mask (SSS boost); 0 for placed water\n crest_foam_mask: f32, // fold * crest01 * gain; 0 for placed water\n fft_foam: f32, // FFT Jacobian whitecap accumulation; 0 for placed water\n ripple_foam: f32, // wake-grid foam trail; 0 for placed water\n t_anim: f32, // conditioned animation clock\n flow_vec: vec2<f32>, // refraction-advection direction*speed\n foam_drift: vec2<f32>, // foam UV drift per second\n time_scale: f32, // detail normal scroll speed (still water is faster: no real motion)\n ns_reflect: f32,\n ns_refract: f32,\n micro_strength: f32,\n refract_z_floor_m: f32, // near-camera refraction wobble floor\n shallow_color: vec3<f32>,\n deep_color: vec3<f32>,\n shallow_m: f32,\n deep_m: f32,\n transparency: f32,\n planar_scale: f32, // 1 = sample planar reflection, 0 = skybox only (placed water)\n planar_hf_a: f32, // planar height-fade start/end (ocean widens by swell amplitude)\n planar_hf_b: f32,\n fall01: f32, // waterfall/aeration mask (auto slope or forced); 0 = calm surface\n fall_off_px: vec2<f32>, // anisotropic falling-streak refraction offset (pre preMask)\n fall_white01: f32, // layered aeration whitening (streaks + Fresnel edge); 0 = flat 5% legacy via fall01 only\n fall_t: vec3<f32>, // fall-surface frame: downhill tangent (zero for the ocean)\n fall_b: vec3<f32>, // fall-surface frame: across tangent\n fall_scroll: f32, // downhill texture speed m/s on falls\n underside01: f32, // 1 = near-horizontal surface seen from BELOW (submerged camera)\n}\n\nconst PI: f32 = 3.141592653589793;\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\nfn rgb_to_ypbpr709(rgb: vec3<f32>) -> vec3<f32> {\n let y = dot(rgb, LUMA709);\n return vec3<f32>(y, -0.114572 * rgb.r - 0.385428 * rgb.g + 0.5 * rgb.b, 0.5 * rgb.r - 0.454153 * rgb.g - 0.045847 * rgb.b);\n}\nfn ypbpr709_to_rgb(ypbpr: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(ypbpr.x + 1.5748 * ypbpr.z, ypbpr.x - 0.187324 * ypbpr.y - 0.468124 * ypbpr.z, ypbpr.x + 1.8556 * ypbpr.y);\n}\nfn apply_skybox_grade(color: vec3<f32>) -> vec3<f32> {\n var ypbpr = rgb_to_ypbpr709(color);\n let hue = radians(p.skybox_hue);\n let c = cos(hue);\n let s = sin(hue);\n ypbpr = vec3<f32>(ypbpr.x, (ypbpr.y * c - ypbpr.z * s) * max(0.0, p.skybox_saturation), (ypbpr.y * s + ypbpr.z * c) * max(0.0, p.skybox_saturation));\n var rgb = ypbpr709_to_rgb(ypbpr);\n rgb = (rgb - vec3<f32>(0.5)) * max(0.0, p.skybox_contrast) + vec3<f32>(0.5);\n return rgb * p.skybox_tint * exp2(p.skybox_exposure);\n}\n\nfn planar_uv(world_pos: vec3<f32>) -> vec3<f32> {\n let clip = planar.view_proj * vec4<f32>(world_pos, 1.0);\n if (clip.w <= 0.0) {\n return vec3<f32>(0.0, 0.0, 0.0);\n }\n let ndc = clip.xyz / clip.w;\n let uv = ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);\n let valid = select(0.0, 1.0, all(uv >= vec2<f32>(0.0)) && all(uv <= vec2<f32>(1.0)) && ndc.z >= 0.0 && ndc.z <= 1.0);\n return vec3<f32>(uv, valid);\n}\nfn planar_edge_fade(uv: vec2<f32>) -> f32 {\n let d = min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y));\n return smoothstep(0.0, 0.08, d);\n}\n\nfn linearize_depth_0to1(d: f32, zNear: f32, zFar: f32) -> f32 {\n return (zNear * zFar) / (zFar - d * (zFar - zNear));\n}\nfn remap01(x: f32, a: f32, b: f32) -> f32 {\n return clamp((x - a) / max(1e-6, b - a), 0.0, 1.0);\n}\nfn smootherstep(a: f32, b: f32, x: f32) -> f32 {\n let t = clamp((x - a) / max(1e-6, b - a), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\nfn band_bump(x: f32, inner0: f32, inner1: f32, outer0: f32, outer1: f32) -> f32 {\n let left = smoothstep(inner0, inner1, x);\n let right = 1.0 - smoothstep(outer0, outer1, x);\n return clamp(left * right, 0.0, 1.0);\n}\nfn min_px_fade01(v: f32, base_width_norm: f32, px_min: f32) -> f32 {\n let w_norm = max(base_width_norm, px_min * fwidth(v));\n return clamp(v / max(w_norm, 1e-6), 0.0, 1.0);\n}\nfn view_pos_from_viewZ(uv: vec2<f32>, dist_m: f32) -> vec3<f32> {\n let x_ndc = uv.x * 2.0 - 1.0;\n let y_ndc = (1.0 - uv.y) * 2.0 - 1.0;\n let x_view = x_ndc * dist_m / u_camera.proj[0][0];\n let y_view = y_ndc * dist_m / u_camera.proj[1][1];\n return vec3<f32>(x_view, y_view, -dist_m);\n}\nfn world_pos_from_view(vpos: vec3<f32>) -> vec3<f32> {\n return (u_camera.inverse_view * vec4<f32>(vpos, 1.0)).xyz;\n}\nfn normal_from_xy(xy: vec2<f32>) -> vec3<f32> {\n let xy_c = clamp(xy, vec2<f32>(-0.999), vec2<f32>(0.999));\n let z = sqrt(max(0.0, 1.0 - dot(xy_c, xy_c)));\n return normalize(vec3<f32>(xy_c, z));\n}\nfn rnm_blend(n1: vec3<f32>, n2: vec3<f32>) -> vec3<f32> {\n let t = vec3<f32>(n1.xy + n2.xy, n1.z * n2.z - dot(n1.xy, n2.xy));\n return normalize(t);\n}\nfn ts_normal_from_rg(rg: vec2<f32>) -> vec3<f32> {\n let xy = clamp(rg * 2.0 - 1.0, vec2<f32>(-0.999), vec2<f32>(0.999));\n let z = sqrt(max(0.0, 1.0 - dot(xy, xy)));\n return normalize(vec3<f32>(xy, z));\n}\nfn fresnel_schlick(cos_theta: f32, F0: vec3<f32>) -> vec3<f32> {\n return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);\n}\nfn distribution_ggx(NdotH: f32, alpha: f32) -> f32 {\n let a2 = alpha * alpha;\n let denom = (NdotH * NdotH) * (a2 - 1.0) + 1.0;\n return a2 / (PI * denom * denom);\n}\nfn G1_schlick_ggx(nDotX: f32, rough: f32) -> f32 {\n let r = rough + 1.0;\n let k = (r * r) / 8.0;\n return nDotX / (nDotX * (1.0 - k) + k);\n}\nfn geometry_smith(nDotV: f32, nDotL: f32, rough: f32) -> f32 {\n return G1_schlick_ggx(nDotV, rough) * G1_schlick_ggx(nDotL, rough);\n}\n// x = distance from the waterline ALONG the underlying surface (depth / sin(angle):\n// flat beach ~ horizontal distance, vertical wall = vertical distance down the wall);\n// y = wall01 (0 = flat ground, 1 = vertical surface) for band-width shaping.\nfn shore_distance_from_scene_normal(uv: vec2<f32>, depth_vertical: f32) -> vec2<f32> {\n let Nscene = normalize(textureSample(sceneWorldNormal, sceneSample, uv).xyz);\n let ny = abs(Nscene.y);\n let sin_t = sqrt(max(0.0, 1.0 - ny * ny));\n return vec2<f32>(depth_vertical / max(sin_t, 1e-3), smoothstep(0.5, 0.9, sin_t));\n}\nfn fix_nohit_depth(z_ndc: f32) -> f32 {\n // view_z_m is the gbuffer NDC depth; linearize to view-space meters, which is what\n // the thickness/fade math expects. Sky (1.0) lands on far naturally.\n return linearize_depth_0to1(z_ndc, u_camera.near, u_camera.far);\n}\n\nstruct FSOut {\n @location(0) color: vec4<f32>,\n @location(1) reactive: f32,\n};\n\n\n// `dbg` mirrors the retired water.wgsl waterDebug pixel-probe rows (1..15; row 0 is the\n// wrapper's target header). Callers that don't tap it pass a throwaway local - the\n// writes are dead-code-eliminated.\nfn shade_water(wp: vec3<f32>, clip: vec4<f32>, ctx: ShadeCtx, dbg: ptr<function, array<vec4<f32>, 16>>) -> vec4<f32> {\n let t_anim = ctx.t_anim;\n let transparency = ctx.transparency;\n let absorption_falloff_m = 5.0 * vec3<f32>(1.0, 1.0, 1.0);\n // Refraction advection + detail-normal constants shared by every water variant.\n let flow_tile = 0.9 * vec2<f32>(1.0, 1.0);\n let flow_phase_speed = vec2<f32>(0.6, 0.12) * 0.1;\n let refract_strength = 0.045;\n let wave_dir1 = vec2<f32>( 0.5, -0.2);\n let wave_dir2 = vec2<f32>(-0.5, 0.5);\n let inv_scale = 0.1;\n let time_scale = ctx.time_scale;\n let normal_strength_reflect = ctx.ns_reflect;\n let normal_strength_refract = ctx.ns_refract;\n\n let dims = vec2<f32>(textureDimensions(view_z_m));\n let uv = clip.xy / dims;\n let hack_offset = 0.1; // matches the retired water.wgsl (see its TODO)\n\n let dist_m = length((u_camera.view * vec4<f32>(wp, 1.0)).xyz);\n // Fade texture detail out with distance: far water is shaped by the geometric normal\n // alone (specular-AA variance handles the rest); kills far-field normal-map shimmer.\n let detail_fade = 1.0 - smoothstep(60.0, 260.0, dist_m);\n\n // Opaque (pre-water) linear depth at this pixel (meters)\n let zOpaque_here = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv).r) + hack_offset;\n let Pv_here = view_pos_from_viewZ(uv, zOpaque_here);\n let Pw_here = world_pos_from_view(Pv_here);\n let depth_world_here = max(0.0, wp.y - Pw_here.y);\n\n let zWater_here = linearize_depth_0to1(clip.z, u_camera.near, u_camera.far);\n let thickness = max(0.0, zOpaque_here - zWater_here);\n\n // Tangent frame on the (near-horizontal) Gerstner surface\n var T_w = normalize(vec3<f32>(1.0, 0.0, 0.0) - ctx.n_g * ctx.n_g.x);\n let B_w = normalize(cross(ctx.n_g, T_w));\n\n // (Flow-aligned stream UV stretching was tried and retired 19Jul26 - it never read\n // right and streams are parked for now; flowing placed water keeps only the correct\n // downstream refraction advection via flow_vec. The fall frame below stays: falls\n // and fountains are live features.)\n // Fall-SURFACE mapping: on steep/falling water every planar-XZ layer remaps into\n // the (across, downhill-scrolling) surface frame. An XZ mapping on a near-vertical\n // face both smears (degenerate footprint) and scrolls SIDEWAYS (a horizontal drift\n // has no downhill component) - that planar-on-vertical mismatch, not any Y-up/Z-up\n // axis mixup, is the perennial \"water flows sideways on falls\".\n // Anisotropic on purpose: falling water elongates features ALONG the fall.\n // Across compresses (x2.5 frequency), along stretches (x0.18) - equal scales made\n // narrow sheets read as HORIZONTAL stripes (across-axis sub-period while the along\n // axis crossed several foam periods).\n let p_fall = vec2<f32>(\n dot(wp, ctx.fall_b) * 2.5,\n (dot(wp, ctx.fall_t) - ctx.fall_scroll * t_anim) * 0.18,\n );\n\n var fresnel: f32;\n var Vw: vec3<f32>;\n var Nw_reflect: vec3<f32>;\n var Nw_refract: vec3<f32>;\n var Nw_specular: vec3<f32>;\n var cosI_reflect: f32;\n var cosI_refract: f32;\n var wave_xy_reflect: vec2<f32>;\n {\n let uv_base = mix(wp.xz, p_fall, ctx.fall01) * inv_scale;\n // Falls remove the sideways wind-chop scroll entirely: t1/t2 are WORLD-frame\n // vectors and the fall UVs are a rotated surface frame - a world-frame drift\n // added to rotated coords slides sideways.\n let cross_damp = 1.0 - ctx.fall01;\n let t1 = t_anim * wave_dir1 * time_scale * cross_damp;\n let t2 = t_anim * wave_dir2 * time_scale * cross_damp;\n let rg1 = textureSample(normalA, normalSample, uv_base + t1 * inv_scale).rg * 2.0 - 1.0;\n let rg2 = textureSample(normalB, normalSample, uv_base + t2 * inv_scale).rg * 2.0 - 1.0;\n let n1 = normal_from_xy(rg1);\n let n2 = normal_from_xy(rg2);\n let n_ts_base = -rnm_blend(n1, n2);\n let n_ts_reflect = normal_from_xy(n_ts_base.xy * normal_strength_reflect * detail_fade);\n let n_ts_refract = normal_from_xy(n_ts_base.xy * normal_strength_refract * detail_fade);\n wave_xy_reflect = n_ts_reflect.xy;\n let n_macro = -n_ts_base;\n\n // Halved vs water.wgsl's 0.22: on the displaced ocean the micro layer reads as\n // near-field boiling on top of the real wave motion (lakes are flat, oceans aren't).\n let micro_scale = 2.5;\n let micro_strength = ctx.micro_strength * detail_fade;\n let xz_micro = mix(wp.xz, p_fall, ctx.fall01);\n let uv_m1 = xz_micro / micro_scale + t_anim * vec2<f32>( 0.31, 0.17) * 0.08 * cross_damp;\n let uv_m2 = xz_micro / micro_scale + t_anim * vec2<f32>(-0.23, 0.29) * 0.08 * cross_damp;\n let n_micro1 = ts_normal_from_rg(textureSample(microNormalA, normalSample, uv_m1).rg);\n let n_micro2 = ts_normal_from_rg(textureSample(microNormalB, normalSample, uv_m2).rg);\n let n_micro = rnm_blend(n_micro1, n_micro2);\n let n_ts_spec_base = rnm_blend(\n normal_from_xy(n_macro.xy * detail_fade),\n normal_from_xy(n_micro.xy * micro_strength),\n );\n\n Nw_reflect = normalize(n_ts_reflect.x * T_w + n_ts_reflect.y * B_w + n_ts_reflect.z * ctx.n_g);\n Nw_refract = normalize(n_ts_refract.x * T_w + n_ts_refract.y * B_w + n_ts_refract.z * ctx.n_g);\n Nw_specular = normalize(n_ts_spec_base.x * T_w + n_ts_spec_base.y * B_w + n_ts_spec_base.z * ctx.n_g);\n\n Vw = normalize(u_camera.camera_position - wp);\n cosI_refract = clamp(dot(Nw_refract, Vw), 0.0, 1.0);\n cosI_reflect = clamp(dot(Nw_reflect, Vw), 0.0, 1.0);\n\n let f0 = pow((1.0 - p.ior) / (1.0 + p.ior), 2.0);\n let floor_cosI = min(cosI_refract, 0.2);\n fresnel = f0 + (1.0 - f0) * pow(1.0 - floor_cosI, 5.0);\n }\n\n let depth_smooth_range = 0.05;\n let preMask = smoothstep(0.0, depth_smooth_range, thickness);\n\n // Optical path length along the transmitted ray\n let eta = 1.0 / p.ior;\n let sin2T = eta * eta * max(0.0, 1.0 - cosI_refract * cosI_refract);\n let cosT = sqrt(max(0.0, 1.0 - sin2T));\n let T_ref = normalize(eta * (-Vw) + (eta * cosI_refract - cosT) * Nw_refract);\n let T_v = normalize((u_camera.view * vec4<f32>(T_ref, 0.0)).xyz);\n let cos_to_viewZ = abs(T_v.z);\n let L_ray = thickness / max(1e-4, cos_to_viewZ);\n let optical_thickness = remap01(L_ray, p.refract_thick_fade.x, p.refract_thick_fade.y);\n\n // Refraction offset via flow advection, depth/height gated (water.wgsl scheme)\n var off_px_raw: vec2<f32>;\n {\n // MINUS: subtracting from the sample coordinate advects the pattern TOWARD\n // +flow_vec. The retired water.wgsl used plus (pattern glides toward -flow) -\n // invisible on ponds/ocean where drift direction is arbitrary, but on streams\n // the refraction layer visibly ran UPSTREAM against the foam.\n let uvF = mix(wp.xz, p_fall, ctx.fall01) * flow_tile\n - t_anim * ctx.flow_vec * (1.0 - ctx.fall01);\n let flow_raw = textureSample(refractNoiseNormal, normalSample, uvF).rg * 2.0 - 1.0;\n let flow_dir = normalize(vec2<f32>(flow_raw.x, flow_raw.y + 1e-6));\n let phase = fract(t_anim * flow_phase_speed.x);\n let w = 1.0 - abs(phase * 2.0 - 1.0);\n let nA_rg = textureSample(refractNoiseNormal, normalSample, uvF + flow_dir * flow_phase_speed.y).rg * 2.0 - 1.0;\n let nB_rg = textureSample(refractNoiseNormal, normalSample, uvF - flow_dir * flow_phase_speed.y).rg * 2.0 - 1.0;\n let nA_ts = vec3<f32>(nA_rg, sqrt(max(0.0, 1.0 - dot(nA_rg, nA_rg))));\n let nB_ts = vec3<f32>(nB_rg, sqrt(max(0.0, 1.0 - dot(nB_rg, nB_rg))));\n let n_ts_adv = normalize(mix(nA_ts, nB_ts, w));\n // Floor at 3m (water.wgsl uses 1m): the 1/z gain made near-camera refraction wobble\n // dominate the near field on the displaced surface.\n off_px_raw = n_ts_adv.xy * refract_strength * optical_thickness / max(zWater_here, ctx.refract_z_floor_m) * preMask;\n }\n // Waterfall aeration adds its own anisotropic streak refraction (prologue-computed).\n off_px_raw += ctx.fall_off_px * preMask;\n let uv_off0 = clamp(uv + off_px_raw, vec2<f32>(0.0), vec2<f32>(1.0));\n\n let zScene0 = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv_off0).r) + hack_offset;\n let depthGate0 = smoothstep(0.0, depth_smooth_range, zScene0 - zWater_here);\n\n let height_eps = 0.015;\n let Pv0 = view_pos_from_viewZ(uv_off0, zScene0);\n let Pw0 = world_pos_from_view(Pv0);\n let heightDiff0 = wp.y - Pw0.y;\n let gateW = max(fwidth(heightDiff0) * 2.0, 0.01);\n // Falling sheets are near-vertical: the water-above-scene height gate is meaningless\n // there, so the waterfall mask bypasses it (water.wgsl heritage).\n var heightGate0 = smoothstep(0.0, gateW, heightDiff0 - height_eps);\n if (ctx.fall01 > 1e-3) { heightGate0 = 1.0; }\n\n let off_px = off_px_raw * depthGate0 * heightGate0;\n let uv_off = clamp(uv + off_px, vec2<f32>(0.0), vec2<f32>(1.0));\n\n let zScene1 = fix_nohit_depth(textureSample(view_z_m, view_z_m_view, uv_off).r) + hack_offset;\n let refractColor = textureSample(sceneColor, sceneSample, uv_off).rgb;\n\n // --- Reflections: graded skybox + planar scene reflection ---\n let Rw = normalize(reflect(-Vw, Nw_reflect));\n let c = cos(p.skybox_yaw);\n let s = sin(p.skybox_yaw);\n let rot_x = Rw.x * c - Rw.z * s;\n let rot_z = Rw.x * s + Rw.z * c;\n let dir_y = Rw.y + p.skybox_horizon_tilt_sin;\n let flip = vec3<f32>(1.0, 1.0, -1.0);\n let Rw_rot = normalize(vec3<f32>(rot_x, dir_y, rot_z) * flip);\n var skybox_reflect_color = textureSample(skybox_texture, sceneSample, Rw_rot).rgb;\n skybox_reflect_color = apply_skybox_grade(skybox_reflect_color);\n\n let planar_proj = planar_uv(wp);\n let planar_uv_d = clamp(planar_proj.xy + wave_xy_reflect * planar.distortion,\n vec2<f32>(0.0), vec2<f32>(1.0));\n let planar_fade = planar_edge_fade(planar_uv_d) * planar_proj.z * f32(planar.is_active);\n let planar_color = textureSample(planarReflection, sceneSample, planar_uv_d).rgb;\n // The planar image is mirrored about plane_y (= sea level): displaced crests may sit up to\n // total_amplitude away, so widen the height fade accordingly instead of water.wgsl's 0.5..2.\n let hf_a = ctx.planar_hf_a;\n let hf_b = ctx.planar_hf_b;\n let height_fade = 1.0 - smoothstep(hf_a, hf_b, abs(wp.y - planar.plane_y));\n let planar_weight = planar_fade * planar.strength * height_fade * ctx.planar_scale;\n\n // --- Direct sun glint (GGX with variance AA; roughness widens with distance) ---\n var sun_specular_color = vec3<f32>(0.0);\n {\n let N = Nw_specular;\n let V = Vw;\n let L = normalize(-u_directional_light.light_dir.xyz);\n let NdotL = max(dot(N, L), 0.0);\n let NdotV = max(dot(N, V), 0.0);\n let dNx = dpdx(N);\n let dNy = dpdy(N);\n let variance = clamp(dot(dNx, dNx) + dot(dNy, dNy), 0.0, 1.0);\n if (NdotL > 0.0 && NdotV > 0.0) {\n let sun_roughness = mix(0.03, 0.16, smoothstep(30.0, 400.0, dist_m));\n let sun_strength = 0.5;\n let r_p = clamp(sun_roughness, 0.04, 1.0);\n var alpha = r_p * r_p;\n alpha = sqrt(alpha * alpha + 0.5 * variance);\n let alphaSun = 0.0025;\n alpha = sqrt(alpha * alpha + alphaSun * alphaSun);\n let H = normalize(L + V);\n let NdotH = max(dot(N, H), 0.0);\n let f0_s = pow((1.0 - p.ior) / (1.0 + p.ior), 2.0);\n let F = fresnel_schlick(max(dot(H, V), 0.0), vec3<f32>(f0_s));\n let D = distribution_ggx(NdotH, alpha);\n let G = geometry_smith(NdotV, NdotL, r_p);\n let spec = (D * G) / max(4.0 * NdotL * NdotV, 1e-4);\n let sun_radiance = u_directional_light.light_color_with_intensity.rgb\n * sun_strength * u_directional_light.sun_specular_scale;\n // Firefly clamp: unbounded GGX peaks pop in/out per frame on animated normals\n // (single-pixel sparkle reads as water \"shaking\"; stability_probe max_px hit\n // the 3.8 luminance ceiling before this).\n sun_specular_color = min(sun_radiance * F * spec * NdotL, vec3<f32>(6.0));\n }\n }\n\n // --- Beer\u2013Lambert absorption + shallow/deep ramp (colors from ocean params) ---\n let refractedThickness = max(0.0, zScene1 - zWater_here);\n let refractedLRay = refractedThickness / max(1e-4, cos_to_viewZ);\n var transmittance: vec3<f32>;\n {\n let k = 1.0 / max(absorption_falloff_m, vec3<f32>(1e-6));\n let t_lin = clamp(vec3<f32>(1.0) - k * refractedLRay, vec3<f32>(0.0), vec3<f32>(1.0));\n let t_exp = exp(-k * refractedLRay);\n let Lm = (absorption_falloff_m.x + absorption_falloff_m.y + absorption_falloff_m.z) * (1.0 / 3.0);\n let w = smoothstep(0.6 * Lm, 2.0 * Lm, refractedLRay);\n transmittance = mix(t_lin, t_exp, vec3<f32>(w));\n }\n // Aerated whitewater doesn't absorb (waterfall).\n transmittance = mix(transmittance, vec3<f32>(1.0), ctx.fall01);\n\n let shallow_color = ctx.shallow_color;\n let deep_color = ctx.deep_color;\n let shallow_m = ctx.shallow_m;\n let deep_m = ctx.deep_m;\n let clear_shallow_m = shallow_m * 0.2;\n\n let w_depth = remap01(refractedLRay, shallow_m, deep_m) * transparency;\n let rampColor = mix(shallow_color, deep_color, w_depth);\n let shallowColorFactor = clamp((heightDiff0 - clear_shallow_m) / max(shallow_m - clear_shallow_m, 1e-4), 0.0, 1.0);\n let shallowTintMul = mix(vec3<f32>(1.0), shallow_color, shallowColorFactor);\n let refractAttenuated = mix(deep_color, refractColor * shallowTintMul, transmittance);\n var refractWithTransparency = mix(rampColor, refractAttenuated, transparency);\n\n // SSS crest boost: sun shining through wave tops toward the viewer.\n {\n let L_travel = normalize(u_directional_light.light_dir.xyz);\n let backlight = pow(max(dot(-Vw, normalize(L_travel + ctx.n_g * 0.4)), 0.0), 3.0);\n let sss = backlight * ctx.crest01 * (1.0 - fresnel);\n refractWithTransparency += shallow_color\n * u_directional_light.light_color_with_intensity.rgb * (0.25 * sss);\n }\n\n // Depth-fade toward the deep color (world-height based, same shape as water.wgsl)\n let depth_fade_factor = 1.5;\n let fade_start_m = shallow_m;\n let fade_end_m = deep_m * depth_fade_factor;\n let depth_fade = smootherstep(fade_start_m, fade_end_m, depth_world_here);\n let depth_color_fade = smootherstep(fade_start_m,\n fade_end_m - (fade_end_m - fade_start_m) * 0.5, depth_world_here);\n let depth_color = mix(shallow_color, deep_color, depth_color_fade);\n var refractDepthFaded = mix(refractWithTransparency, depth_color, depth_fade);\n // No deep-color fade on a falling sheet (it has no meaningful water column).\n if (ctx.fall01 > 1e-3) { refractDepthFaded = refractWithTransparency; }\n\n // Macro reflectivity by angle\n let headOnR = 0.08;\n let grazingR = 0.92;\n let wAngle = smoothstep(0.30, 0.95, 1.0 - cosI_reflect);\n var reflectWeight = clamp(mix(headOnR, grazingR, wAngle), 0.0, 0.98);\n // Waterfall whitewater: aeration mattes the mirror and lightens the sheet.\n reflectWeight *= mix(1.0, 0.35, ctx.fall01);\n\n var tintedReflectColor: vec3<f32>;\n {\n let sat_factor = max(0.0, u_directional_light.ambient_tint.a);\n let lum = dot(skybox_reflect_color, LUMA709);\n let sat_skybox = mix(vec3<f32>(lum), skybox_reflect_color, sat_factor)\n * u_directional_light.ambient_tint.rgb;\n tintedReflectColor = mix(sat_skybox, planar_color, planar_weight) + sun_specular_color;\n }\n\n var color = tintedReflectColor * reflectWeight + refractDepthFaded * (1.0 - reflectWeight);\n // Aeration whitening: fall_white01 carries the layered streak + Fresnel-edge mix\n // (placed wrapper); the flat 5% is the floor so bare fall01 callers still whiten.\n color = mix(color, vec3<f32>(0.96, 0.965, 0.97), max(ctx.fall01 * 0.05, ctx.fall_white01));\n let dbg_after_reflect_mix = color;\n\n // --- Foam: crest whitecaps (fold-driven) + shore contact band ---\n var dbg_foam_alpha = 0.0;\n {\n let foam_tint = vec3<f32>(0.96, 0.965, 0.97);\n let foam_tile_big = 0.98;\n let foam_tile_small = 5.0;\n let foam_drift = ctx.foam_drift * t_anim;\n // Unlike water.wgsl, NO screen-space refraction wobble in these UVs: on open-ocean\n // whitecap patches it re-jitters the whole foam pattern every frame (frame-diff\n // verified \u2014 the foam areas flashed wholesale; a thin shore band never showed it).\n let xz_foam = mix(wp.xz, p_fall, ctx.fall01);\n // Same frame rule: the world-frame drift zeroes out on falls (their scroll is\n // already inside the rotated frame).\n let foam_drift_w = foam_drift * (1.0 - ctx.fall01);\n let uv_big = xz_foam * foam_tile_big - foam_drift_w;\n let uv_small = xz_foam * foam_tile_small + foam_drift_w * 1.7;\n let atlas_big = textureSample(foamRGB, normalSample, uv_big);\n let atlas_small = textureSample(foamRGB, normalSample, uv_small);\n let heavy = max(atlas_big.r, atlas_big.g);\n let detail = max(atlas_small.r, atlas_small.b);\n let breakup = mix(heavy, detail, 0.6) * (0.3 + 0.7 * atlas_small.g);\n\n // Shore/contact band in WORLD distance-to-waterline: d_shore = depth/slope\n // reconstructs the horizontal distance to the waterline independent of slope,\n // so ONE formulation covers flat beaches, ramps, and steep walls (this replaces\n // the old optical-thickness band + steep-only gate that erased beach foam).\n // The outer edge laps in/out on a world-anchored phase - no screen-space inputs,\n // so the foam pattern can't re-jitter per frame (see the wobble note above).\n let ds = shore_distance_from_scene_normal(uv, depth_world_here);\n let d_shore = ds.x;\n // No water-side band on steep surfaces at all (it never reads right at grazing\n // angles); walls get their contact from the grid collar + object-side meniscus.\n let edge_scale = mix(1.0, 0.0, ds.y);\n let lap_phase = t_anim * 1.7\n + dot(wp.xz, vec2<f32>(0.021, 0.017))\n + atlas_big.g * 6.283;\n let lap_edge = mix(0.45, 1.15, 0.5 + 0.5 * sin(lap_phase)) * edge_scale;\n let band = 1.0 - smoothstep(lap_edge * 0.3, lap_edge, d_shore);\n // Faint residue line lingering just beyond the lapping edge (water pulling back).\n let residue =\n (1.0 - smoothstep(0.05, 0.45, abs(d_shore - lap_edge * 1.3))) * 0.35;\n // Band breakup at HALF the whitecap detail frequency: the 5/m tile reads busy on\n // a meter-wide strip.\n let uv_band = xz_foam * (foam_tile_small * 0.5) - foam_drift_w;\n let atlas_band = textureSample(foamRGB, normalSample, uv_band);\n let band_breakup =\n mix(heavy, max(atlas_band.r, atlas_band.b), 0.6) * (0.3 + 0.7 * atlas_band.g);\n let shore_foam = clamp(\n (band * pow(band_breakup, 1.5)\n + residue * band_breakup * band_breakup * (1.0 - ds.y)) * 0.85,\n 0.0,\n 0.8,\n );\n\n // Crest whitecaps: Gerstner fold gates the same foam texture; detail_fade keeps the\n // far field clean (far crests read through the normal + glint instead).\n let crest_mask = ctx.crest_foam_mask;\n let crest_foam = clamp(crest_mask * (0.35 + 0.65 * pow(breakup, 1.2)), 0.0, 0.9)\n * max(detail_fade, 0.25);\n // FFT Jacobian whitecaps (temporally accumulated in ocean_derivatives.wgsl),\n // broken up by the same foam texture so they don't read as flat decals.\n let fft_whitecap = clamp(ctx.fft_foam * (0.4 + 0.6 * breakup), 0.0, 0.9);\n // Wake foam trail (ocean_ripples accumulation): its own low-frequency breakup \u2014\n // the 5/m detail tile reads as noise on the meters-wide trail behind a wader\n // (~0.4/m, user-tuned x12 below the whitecap detail).\n let uv_wake = wp.xz * (foam_tile_small * 0.083) + foam_drift * 1.7;\n let atlas_wake = textureSample(foamRGB, normalSample, uv_wake);\n let wake_breakup =\n mix(heavy, max(atlas_wake.r, atlas_wake.b), 0.6) * (0.3 + 0.7 * atlas_wake.g);\n let wake_foam = clamp(ctx.ripple_foam * (0.5 + 0.5 * wake_breakup), 0.0, 0.9);\n\n let foam_alpha = max(max(shore_foam, wake_foam), max(crest_foam, fft_whitecap));\n color = mix(color, foam_tint, foam_alpha);\n dbg_foam_alpha = foam_alpha;\n }\n\n // Shore fade in screen space (\u22653px on-screen width)\n let shore_fade = min_px_fade01(optical_thickness, 0.1, 3.0);\n color = mix(refractColor, color, shore_fade);\n let dbg_after_foam_shore = color;\n\n // Underside (submerged camera under a near-horizontal surface): foam floats ON the\n // surface and the sky cannot be REFLECTED from below, so the composed topside lobes\n // are all wrong there. AAA model, overwrite-style: Snell's window (the refracted\n // above-water scene - the scene buffer behind the surface IS that) inside the\n // critical angle, total-internal-reflection murk (the underwater fog color, which\n // the submersion grade sets to the water tint) outside it.\n if (ctx.underside01 > 0.5) {\n // The underside look IS the animated wave normals (Subnautica/SoT): boost the\n // detail perturbation and derive EVERYTHING from it - the window/TIR boundary\n // then ripples per-pixel instead of sitting as a flat gradient.\n // Distance-ATTENUATED boost (never zero): amplifying the detail normal also\n // amplifies its texel noise, but collapsing to the smooth geometric normal\n // makes the window/TIR frontier a clean cutoff curve - the REAL softness of\n // that boundary is waves shredding it, so some waviness must survive at range\n // (the thick murk covers the residual noise).\n let uw_boost = mix(0.5, 1.5, 1.0 - smoothstep(12.0, 45.0, dist_m));\n let n_uw = normalize(ctx.n_g + (Nw_refract - ctx.n_g) * (1.0 + uw_boost));\n let cos_up = abs(dot(n_uw, Vw));\n // PHYSICAL water->air Fresnel transmittance: high near vertical, dives steeply\n // but smoothly toward the critical angle, exactly zero beyond (TIR). No ad-hoc\n // smoothstep band - per-pixel wavy normals provide the natural boundary break-up.\n let sin_t2 = (1.333 * 1.333) * max(0.0, 1.0 - cos_up * cos_up);\n var window01 = 0.0;\n if (sin_t2 < 1.0) {\n let cos_t = sqrt(1.0 - sin_t2);\n let rs = (1.333 * cos_up - cos_t) / (1.333 * cos_up + cos_t);\n let rp = (1.333 * cos_t - cos_up) / (1.333 * cos_t + cos_up);\n window01 = 1.0 - 0.5 * (rs * rs + rp * rp);\n }\n // Full-strength wavy refraction - comparable to the topside look-down, not the\n // optically-gated leftovers. (SampleLevel: textureSample is illegal here.)\n let refr_uv = clamp(\n uv + n_uw.xz * 0.055 + off_px_raw,\n vec2<f32>(0.0),\n vec2<f32>(1.0),\n );\n let refr = textureSampleLevel(sceneColor, sceneSample, refr_uv, 0.0).rgb;\n // Rim glow hugs the critical angle itself (sin_t2 = 1).\n let rim = 1.0 - clamp(abs(sin_t2 - 1.0) / 0.22, 0.0, 1.0);\n // Bright rippling ceiling: the TIR zone brightens toward up-facing wave slopes\n // and shimmers with the same normals; the window shows the warped above world.\n let ceil_bright = 1.0 + 0.25 * (n_uw.x + n_uw.z) + 0.18 * cos_up;\n color = mix(fog.color * ceil_bright, refr * vec3<f32>(0.85, 0.95, 1.05), window01)\n + vec3<f32>(0.10, 0.12, 0.13) * rim;\n }\n\n if (u_directional_light.light_debug_mode == 6u) {\n return vec4<f32>(tintedReflectColor, 1.0);\n }\n if (u_directional_light.light_debug_mode == 7u) {\n return vec4<f32>(refractDepthFaded, 1.0);\n }\n if (u_directional_light.light_debug_mode == 16u) {\n return vec4<f32>(planar_color, 1.0);\n }\n\n // Air fog from camera to surface (post fog pass already ran; water fogs itself)\n var fog_a: f32;\n {\n switch (fog.mode) {\n default { fog_a = clamp((dist_m - fog.start) / max(fog.end_ - fog.start, 1e-6), 0.0, 1.0); }\n case 1u { fog_a = 1.0 - exp(-fog.density * dist_m); }\n case 2u { fog_a = 1.0 - exp(-fog.density * dist_m * fog.density * dist_m); }\n }\n fog_a = clamp(fog_a, 0.0, 1.0);\n if (fog.height_enabled != 0u) {\n let lo = min(fog.height_bottom, fog.height_top);\n let hi = max(fog.height_bottom, fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, wp.y);\n let w = clamp(fog.height_weight, 0.0, 1.0);\n fog_a *= mix(1.0, h, w);\n }\n }\n\n let final_rgb = mix(color, fog.color, fog_a);\n (*dbg)[1] = vec4<f32>(skybox_reflect_color, dot(skybox_reflect_color, LUMA709));\n (*dbg)[2] = vec4<f32>(planar_color, planar_weight);\n (*dbg)[3] = vec4<f32>(sun_specular_color, dot(sun_specular_color, LUMA709));\n (*dbg)[4] = vec4<f32>(tintedReflectColor, reflectWeight);\n (*dbg)[5] = vec4<f32>(refractColor, shore_fade);\n (*dbg)[6] = vec4<f32>(refractDepthFaded, dot(refractDepthFaded, LUMA709));\n (*dbg)[7] = vec4<f32>(shallow_color, shallow_m);\n (*dbg)[8] = vec4<f32>(deep_color, deep_m);\n (*dbg)[9] = vec4<f32>(fresnel, reflectWeight, 0.0, cosI_reflect);\n (*dbg)[10] = vec4<f32>(thickness, L_ray, refractedLRay, depth_world_here);\n (*dbg)[11] = vec4<f32>(ctx.fall01, dbg_foam_alpha, depth_fade, fog_a);\n (*dbg)[12] = vec4<f32>(tintedReflectColor, dot(tintedReflectColor, LUMA709));\n (*dbg)[13] = vec4<f32>(dbg_after_reflect_mix, dot(dbg_after_reflect_mix, LUMA709));\n (*dbg)[14] = vec4<f32>(dbg_after_foam_shore, dot(dbg_after_foam_shore, LUMA709));\n (*dbg)[15] = vec4<f32>(final_rgb, dot(final_rgb, LUMA709));\n return vec4<f32>(final_rgb, 1.0);\n\n}\n\n// ocean_surface.wgsl \u2014 global displaced ocean surface (Gerstner swell).\n// Prefixed at pipeline build with ocean_gerstner.wgsl (shared displacement core + `ocean`\n// uniform at group(1) binding(30)). Shading is adapted from water.wgsl (refraction from the\n// HDR scene copy, planar + skybox reflection, sun glint, shore foam, manual fog) with ocean\n// additions: analytic Gerstner normals, crest whitecaps from the fold term, SSS crest boost.\n// Unlike flat water this pass writes real motion vectors (ocean_mv.wgsl), so the reactive\n// mask value is lower (ocean.reactive).\n// y-up right-handed; mesh is a camera-centered radial disc, displaced in the VS.\n\nstruct CameraUniform {\n view_proj : mat4x4<f32>,\n inverse_view_proj : mat4x4<f32>,\n inverse_proj : mat4x4<f32>,\n view : mat4x4<f32>,\n proj : mat4x4<f32>,\n camera_position : vec3<f32>,\n time_seconds : f32,\n near : f32,\n far : f32,\n _padding2 : f32,\n _padding3 : f32,\n camera_right : vec3<f32>,\n _padding4 : f32,\n camera_up : vec3<f32>,\n _padding5 : f32,\n inverse_view : mat4x4<f32>,\n};\n\nstruct VSIn {\n @location(0) offset: vec2<f32>, // radial-disc offset from the camera, render XZ meters\n};\nstruct VSOut {\n @builtin(position) clip : vec4<f32>,\n @location(0) world_pos: vec3<f32>,\n @location(1) base_xz: vec2<f32>, // undisplaced world XZ (Gerstner input)\n @location(2) radius_m: f32, // distance from disc center (wave attenuation input)\n @location(3) crest01: f32, // displaced height / total amplitude, 0..1\n};\nstruct WaterParams {\n ior: f32,\n strength: f32,\n _pad0: vec2<f32>,\n refract_thick_fade: vec2<f32>, // x = start, y = end\n skybox_horizon_tilt_sin: f32,\n skybox_yaw: f32,\n skybox_tint: vec3<f32>,\n skybox_saturation: f32,\n skybox_exposure: f32,\n skybox_hue: f32,\n skybox_contrast: f32,\n _pad1: f32,\n};\nstruct FogSettings {\n color: vec3<f32>,\n mode: u32,\n start: f32,\n end_: f32,\n density: f32,\n height_enabled:u32,\n height_weight: f32,\n height_bottom: f32,\n height_top: f32,\n height_softness: f32,\n sky_affect: f32,\n};\nstruct PlanarReflectionParams {\n view_proj: mat4x4<f32>,\n plane_y: f32,\n is_active: u32,\n strength: f32,\n distortion: f32,\n};\n\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@vertex fn vs_main(i: VSIn) -> VSOut {\n var o: VSOut;\n let base_xz = u_camera.camera_position.xz + i.offset;\n let r = length(i.offset);\n var pos = ocean_displace(base_xz, r, false);\n // crest01 from the Gerstner swell only: the fast FFT detail would re-modulate crest\n // foam at sub-second rates (temporal foam flicker).\n o.crest01 = clamp((pos.y - ocean.sea_level_y) / max(ocean.total_amplitude, 1e-4), 0.0, 1.0);\n pos += ocean_fft_displace(base_xz, r);\n o.clip = u_camera.view_proj * vec4<f32>(pos, 1.0);\n o.world_pos = pos;\n o.base_xz = base_xz;\n o.radius_m = r;\n return o;\n}\n\n@group(1) @binding(0) var sceneColor : texture_2d<f32>;\n@group(1) @binding(1) var sceneSample : sampler;\n@group(1) @binding(2) var view_z_m : texture_2d<f32>;\n@group(1) @binding(3) var view_z_m_view : sampler; // NON-filtering\n@group(1) @binding(4) var<uniform> p : WaterParams;\n@group(1) @binding(5) var normalA : texture_2d<f32>;\n@group(1) @binding(6) var normalB : texture_2d<f32>;\n@group(1) @binding(7) var normalSample : sampler; // filtering, repeat\n@group(1) @binding(8) var refractNoiseNormal : texture_2d<f32>;\n@group(1) @binding(9) var skybox_texture : texture_cube<f32>;\n@group(1) @binding(10) var<uniform> fog: FogSettings;\n@group(1) @binding(11) var microNormalA : texture_2d<f32>;\n@group(1) @binding(12) var microNormalB : texture_2d<f32>;\n@group(1) @binding(13) var foamRGB : texture_2d<f32>;\n@group(1) @binding(15) var sceneWorldNormal : texture_2d<f32>;\n@group(1) @binding(16) var planarReflection : texture_2d<f32>;\n@group(1) @binding(19) var<uniform> planar : PlanarReflectionParams;\n// FFT gradient maps: xy = (dDy/dx, dDy/dz), z = accumulated whitecap foam, w = Jacobian.\n@group(1) @binding(36) var ocean_fft_grad0 : texture_2d<f32>;\n@group(1) @binding(37) var ocean_fft_grad1 : texture_2d<f32>;\n\nstruct ShadowCascadeData {\n light_view_proj: mat4x4<f32>,\n split_depth: f32,\n _padding: vec3<f32>,\n};\nstruct DirectionalLightShadowUniform {\n cascade_data: array<ShadowCascadeData, 4>,\n light_dir: vec4<f32>,\n light_color_with_intensity: vec4<f32>,\n ambient_tint: vec4<f32>,\n\n cascade_count: u32,\n shadow_opacity: f32,\n light_debug_mode: u32,\n shadow_blur_radius: f32,\n local_shadow_blur_radius: f32,\n sun_specular_scale: f32,\n _pad_local1: u32,\n _pad_local2: u32,\n};\n@group(2) @binding(0) var<uniform> u_directional_light: DirectionalLightShadowUniform;\n\nfn ocean_out(color: vec4<f32>) -> FSOut {\n var o: FSOut;\n o.color = color;\n o.reactive = ocean.reactive * color.a;\n return o;\n}\n\n@fragment fn fs_main(i: VSOut, @builtin(front_facing) front: bool) -> FSOut {\n // --- Normals: analytic Gerstner base + FFT gradients + scrolling detail + micro ---\n let g_nf = ocean_normal_fold(i.base_xz, i.radius_m);\n // Grad LOD from the actual screen footprint (fwidth outside the branch for uniformity);\n // point-sampling mip 0 at minified footprints shimmered per frame before the mip chain.\n let uv_g0 = i.base_xz * ocean.fft0.x;\n let uv_g1 = i.base_xz * ocean.fft0.y;\n let fw0 = fwidth(uv_g0) * 256.0;\n let fw1 = fwidth(uv_g1) * 256.0;\n let lod_g0 = clamp(log2(max(max(fw0.x, fw0.y), 1.0)), 0.0, 8.0);\n let lod_g1 = clamp(log2(max(max(fw1.x, fw1.y), 1.0)), 0.0, 8.0);\n var fft_slope = vec2<f32>(0.0);\n var fft_foam = 0.0;\n if (ocean.fft1.x > 0.5) {\n let f0 = ocean_fft_fade(i.radius_m, ocean.fft0.z);\n let f1 = ocean_fft_fade(i.radius_m, ocean.fft0.w);\n let g0 = textureSampleLevel(ocean_fft_grad0, ocean_fft_sampler, uv_g0, lod_g0);\n let g1 = textureSampleLevel(ocean_fft_grad1, ocean_fft_sampler, uv_g1, lod_g1);\n fft_slope = (g0.xy * f0 + g1.xy * f1) * ocean.fft1.z;\n fft_foam = clamp(g0.z * f0 + g1.z * 0.5 * f1, 0.0, 1.0) * ocean.fft1.y;\n }\n // Interactive wake ripples: central-difference slope (one texel step) + foam trail.\n var ripple_slope = vec2<f32>(0.0);\n var ripple_foam = 0.0;\n if (ocean.ripple0.w > 0.0) {\n let rs = ocean_ripple_sample(i.base_xz);\n let texel_m = 1.0 / (ocean.ripple0.z * 512.0);\n let hx = ocean_ripple_sample(i.base_xz + vec2<f32>(texel_m, 0.0)).x;\n let hz = ocean_ripple_sample(i.base_xz + vec2<f32>(0.0, texel_m)).x;\n ripple_slope = vec2<f32>(hx - rs.x, hz - rs.x) * (ocean.ripple0.w / texel_m);\n // Soft knee: saturated churn right behind the hull tapers into streaks with age\n // instead of a hard-edged solid ribbon.\n let f = rs.z * ocean.ripple1.z;\n ripple_foam = smoothstep(0.06, 0.9, f) * (0.55 + 0.45 * smoothstep(0.5, 0.95, f));\n }\n // Slope-add is a good approximation while the surface stays near-horizontal.\n let N_g = normalize(vec3<f32>(\n g_nf.x - fft_slope.x - ripple_slope.x,\n g_nf.y,\n g_nf.z - fft_slope.y - ripple_slope.y,\n ));\n let fold = g_nf.w;\n\n\n // Conditioned animation clock (ocean_pass.rs), NOT u_camera.time_seconds: raw wall\n // time judders with frame hitches and visibly shakes every scrolling shading layer\n // while the (same-clock-conditioned) geometry stays smooth.\n var ctx: ShadeCtx;\n ctx.n_g = N_g;\n ctx.crest01 = i.crest01;\n ctx.crest_foam_mask = fold * i.crest01 * ocean.crest_foam;\n ctx.fft_foam = fft_foam;\n ctx.ripple_foam = ripple_foam;\n ctx.t_anim = ocean.fft1.w;\n // Negated to compensate the body's minus-advection (see uvF comment there): the\n // ocean's rendered drift stays BIT-EXACT with its historical direction.\n ctx.flow_vec = vec2<f32>(0.3, 0.24) * -0.18;\n ctx.foam_drift = vec2<f32>(0.3, 0.24) * 0.02;\n ctx.time_scale = 0.06;\n ctx.ns_reflect = 0.05;\n ctx.ns_refract = 0.35;\n // Halved vs the retired water.wgsl's 0.22: on the displaced ocean the micro layer\n // reads as near-field boiling on top of the real wave motion.\n ctx.micro_strength = 0.08;\n // 3m floor (water.wgsl used 1m): the 1/z gain made near-camera refraction wobble\n // dominate the near field on the displaced surface.\n ctx.refract_z_floor_m = 3.0;\n ctx.shallow_color = ocean.shallow_color;\n ctx.deep_color = ocean.deep_color;\n ctx.shallow_m = ocean.color_mix_range_m * 0.3;\n ctx.deep_m = ocean.color_mix_range_m;\n ctx.transparency = ocean.transparency;\n ctx.planar_scale = 1.0;\n // The planar image is mirrored about plane_y (= sea level): displaced crests sit up\n // to total_amplitude away, so widen the height fade accordingly.\n ctx.planar_hf_a = 0.5 + ocean.total_amplitude;\n ctx.planar_hf_b = 2.0 + 3.0 * ocean.total_amplitude;\n ctx.fall01 = 0.0;\n ctx.fall_off_px = vec2<f32>(0.0);\n ctx.fall_white01 = 0.0;\n ctx.fall_t = vec3<f32>(0.0);\n ctx.fall_b = vec3<f32>(0.0);\n ctx.fall_scroll = 0.0;\n // Near-horizontal check keeps steep wave faces' backsides on normal shading.\n ctx.underside01 = select(\n 0.0, 1.0,\n !front && N_g.y > 0.6 && u_camera.camera_position.y < i.world_pos.y,\n );\n var dbg_unused: array<vec4<f32>, 16>;\n return ocean_out(shade_water(i.world_pos, i.clip, ctx, &dbg_unused));\n}\n"},{"label":"ocean_mv","code":"// ORI_VERTEX_UNPACK_V1 (injected by preprocess_wgsl into every module, like the noise\n// prelude). Decodes the octahedral snorm16x2 static-vertex normal; CPU encoder mirror is\n// render_base::oct_encode_normal.\nfn ori_oct_decode_normal(e: vec2<f32>) -> vec3<f32> {\n var v = vec3<f32>(e.xy, 1.0 - abs(e.x) - abs(e.y));\n if (v.z < 0.0) {\n let s = select(vec2<f32>(-1.0), vec2<f32>(1.0), v.xy >= vec2<f32>(0.0));\n v = vec3<f32>((1.0 - abs(v.yx)) * s, v.z);\n }\n return normalize(v);\n}\n// ORI_NOISE_PRELUDE_V1 (engine-injected; do not write this marker in user shaders)\n// Mirrors sim-side fp_noise (oriverse_core_minimal): identical u32 lattice hashes, so values\n// match the Weave Hash2/ValueNoise2/Fbm2 built-ins exactly at integer lattice points\n// (f32 interpolation off-lattice is approximate vs the sim's fixed-point).\nfn ori_avalanche32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u; x = x * 0x7feb352du;\n x ^= x >> 15u; x = x * 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn ori_hash2_cell(cx: i32, cy: i32, seed: u32) -> u32 {\n var h = seed ^ 0x9e3779b9u;\n h = ori_avalanche32(h ^ (bitcast<u32>(cx) * 0x85ebca6bu));\n h = ori_avalanche32(h ^ (bitcast<u32>(cy) * 0xc2b2ae35u));\n return h;\n}\nfn ori_hash2(x: f32, y: f32, seed: u32) -> f32 {\n return f32(ori_hash2_cell(i32(floor(x)), i32(floor(y)), seed) >> 16u) / 65536.0;\n}\nfn ori_value_noise2(x: f32, y: f32, seed: u32) -> f32 {\n let cx = i32(floor(x));\n let cy = i32(floor(y));\n let tx = x - floor(x);\n let ty = y - floor(y);\n let h00 = f32(ori_hash2_cell(cx, cy, seed) >> 16u) / 65536.0;\n let h10 = f32(ori_hash2_cell(cx + 1, cy, seed) >> 16u) / 65536.0;\n let h01 = f32(ori_hash2_cell(cx, cy + 1, seed) >> 16u) / 65536.0;\n let h11 = f32(ori_hash2_cell(cx + 1, cy + 1, seed) >> 16u) / 65536.0;\n let u = tx * tx * (3.0 - 2.0 * tx);\n let v = ty * ty * (3.0 - 2.0 * ty);\n return mix(mix(h00, h10, u), mix(h01, h11, u), v);\n}\nfn ori_fbm2(x: f32, y: f32, octaves: i32, seed: u32) -> f32 {\n return ori_fbm2_gain(x, y, octaves, seed, 0.5);\n}\n// gain = per-octave amplitude factor (classic 0.5; ~0.95 = near-equal-energy rough detail).\n// Mirrors the sim's fbm2 gain parameter; total-normalized so range stays gain-independent.\nfn ori_fbm2_gain(x: f32, y: f32, octaves: i32, seed: u32, gain: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n sum += ori_value_noise2(x * freq, y * freq, seed + u32(i)) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// Band-limited fbm for LIVE (per-frame) shading: octaves finer than the pixel footprint fade\n// toward their 0.5 mean instead of aliasing/shimmering - the live-shader replacement for the\n// mip prefiltering a baked texture would get. px_step = noise-domain units per screen pixel,\n// e.g. length(vec4(dpdx(p), dpdy(p))) for the base (freq 1) domain coordinate p.\nfn ori_fbm2_bl(x: f32, y: f32, octaves: i32, seed: u32, gain: f32, px_step: f32) -> f32 {\n var sum = 0.0;\n var amp = 1.0;\n var total = 0.0;\n var freq = 1.0;\n let n = clamp(octaves, 1, 8);\n for (var i = 0; i < n; i = i + 1) {\n // Octave wavelength = 1/freq; fully present above 4 px, gone below 2 px.\n let bl = smoothstep(2.0, 4.0, 1.0 / max(freq * px_step, 1e-6));\n sum += mix(0.5, ori_value_noise2(x * freq, y * freq, seed + u32(i)), bl) * amp;\n total += amp;\n amp *= gain;\n freq *= 2.0;\n }\n return sum / total;\n}\n// ORI_NOISE_PRELUDE_END\n// ocean_gerstner.wgsl \u2014 shared Gerstner displacement core.\n// Concatenated (Rust-side) in front of ocean_surface.wgsl and ocean_mv.wgsl so the color and\n// motion-vector passes displace identically. Phases are integrated in f64 on the CPU and\n// uploaded pre-wrapped per wave (cur + prev frame), so there is no f32 time blowup and the MV\n// pass gets exact previous-frame positions without history textures.\n// The wave table is derived deterministically from a few sim attributes (see\n// ocean_pass.rs::derive_wave_table); a future Fp buoyancy query mirrors that derivation.\n\nconst OCEAN_WAVE_COUNT: u32 = 8u;\n\nstruct OceanWave {\n // xy = unit travel dir (render XZ), z = amplitude (m), w = angular wavenumber k (rad/m)\n dir_amp: vec4<f32>,\n // x = phase at t (rad, wrapped), y = phase at prev t, z = Q (chop), w = wavelength (m)\n phase_q: vec4<f32>,\n};\n\nstruct OceanParams {\n waves: array<OceanWave, 8>,\n sea_level_y: f32, // render meters (Y-up)\n disp_fade_start: f32, // radial m: displacement starts fading (stable horizon + MV calm)\n disp_fade_end: f32, // radial m: fully flat beyond this\n cell_growth: f32, // local mesh cell size ~ max(cell_min, r * cell_growth)\n shallow_color: vec3<f32>,\n transparency: f32, // 0..1 refraction mix\n deep_color: vec3<f32>,\n color_mix_range_m: f32, // water depth over which shallow blends to deep\n total_amplitude: f32, // m; sum of derived wave amplitudes (crest01 normalizer)\n cell_min: f32, // m; finest mesh cell near the camera\n reactive: f32, // reactive-mask value (lower than flat water: we write real MVs)\n crest_foam: f32, // 0..1 crest whitecap strength\n // FFT detail cascades (Phase 2, desktop tier): x/y = 1/patch_size per cascade,\n // z/w = radial fade distance per cascade.\n fft0: vec4<f32>,\n // x = fft enabled (0/1), y = foam gain, z = detail normal gain, w = unused.\n fft1: vec4<f32>,\n // Interactive wake ripples (Phase C, ocean_ripples.rs). xy = grid window center\n // (render XZ m), z = 1/extent, w = height gain (0 = disabled).\n ripple0: vec4<f32>,\n // xy = previous frame's window center (MV sampling), z = foam gain, w = unused.\n ripple1: vec4<f32>,\n};\n\n@group(1) @binding(30) var<uniform> ocean: OceanParams;\n\nfn ocean_global_fade(r_m: f32) -> f32 {\n return 1.0 - smoothstep(ocean.disp_fade_start, ocean.disp_fade_end, r_m);\n}\n\n// Attenuate waves the local mesh density cannot represent (radial rings grow geometrically).\n// Full contribution only at >= 6 vertices per wavelength: with fewer (the previous 2..4\n// window), phase advance makes the piecewise-linear surface between vertices boil \u2014 the\n// \"whole ocean shaking\" artifact. 3 cells/lambda (Nyquist-ish) is fully out.\nfn ocean_wave_atten(lambda_m: f32, r_m: f32) -> f32 {\n let local_cell = max(ocean.cell_min, r_m * ocean.cell_growth);\n return smoothstep(4.0, 8.0, lambda_m / local_cell);\n}\n\n// Displaced world position for the base (undisplaced) point. use_prev picks the\n// previous-frame phase set (motion vectors).\nfn ocean_displace(base_xz: vec2<f32>, r_m: f32, use_prev: bool) -> vec3<f32> {\n var pos = vec3<f32>(base_xz.x, ocean.sea_level_y, base_xz.y);\n let global_fade = ocean_global_fade(r_m);\n if (global_fade <= 0.0) {\n return pos;\n }\n for (var i = 0u; i < OCEAN_WAVE_COUNT; i++) {\n let w = ocean.waves[i];\n let amp = w.dir_amp.z * ocean_wave_atten(w.phase_q.w, r_m) * global_fade;\n let k = w.dir_amp.w;\n let phase_t = select(w.phase_q.x, w.phase_q.y, use_prev);\n let theta = k * dot(w.dir_amp.xy, base_xz) - phase_t;\n let c = cos(theta);\n let s = sin(theta);\n let q = w.phase_q.z;\n pos.x += q * amp * w.dir_amp.x * c;\n pos.z += q * amp * w.dir_amp.y * c;\n pos.y += amp * s;\n }\n pos.y += ocean_ripple_height(base_xz, use_prev);\n return pos;\n}\n\n// Analytic surface normal + fold. xyz = normal (Y-up), w = fold01 (Jacobian-style crest\n// pinch proxy: 0 flat, ->1 at pinched crests; drives whitecap foam).\nfn ocean_normal_fold(base_xz: vec2<f32>, r_m: f32) -> vec4<f32> {\n var nx = 0.0;\n var nz = 0.0;\n var pinch = 0.0; // sum of Q k A sin(theta): 1 - pinch is the Gerstner \"fold\" term\n let global_fade = ocean_global_fade(r_m);\n if (global_fade > 0.0) {\n for (var i = 0u; i < OCEAN_WAVE_COUNT; i++) {\n let w = ocean.waves[i];\n let amp = w.dir_amp.z * ocean_wave_atten(w.phase_q.w, r_m) * global_fade;\n let k = w.dir_amp.w;\n let theta = k * dot(w.dir_amp.xy, base_xz) - w.phase_q.x;\n let c = cos(theta);\n let s = sin(theta);\n let ka = k * amp;\n nx -= w.dir_amp.x * ka * c;\n nz -= w.dir_amp.y * ka * c;\n pinch += w.phase_q.z * ka * s;\n }\n }\n let n = normalize(vec3<f32>(nx, 1.0 - pinch, nz));\n // Only well-pinched crests foam: the threshold keeps mid-slope water clean.\n let fold = clamp(pinch * 1.15 - 0.28, 0.0, 1.0);\n return vec4<f32>(n, fold);\n}\n\n// ---- FFT detail cascades (ocean_fft.rs; zero-filled dummies when disabled) ----\n// disp maps: xyz = (Dx, Dy, Dz) meters, tiling with 1/patch_size uv scale.\n@group(1) @binding(31) var ocean_fft_disp0: texture_2d<f32>;\n@group(1) @binding(32) var ocean_fft_disp1: texture_2d<f32>;\n@group(1) @binding(35) var ocean_fft_sampler: sampler;\n\nfn ocean_fft_fade(r_m: f32, fade_r: f32) -> f32 {\n return 1.0 - smoothstep(fade_r * 0.5, fade_r, r_m);\n}\n\n// ---- Interactive wake ripples (ocean_ripples.rs; zero dummy when disabled) ----\n// World-anchored toroidal grid: uv = world_xz / extent (Repeat sampler wraps). Texel\n// r = height m, g = previous-frame height m, b = wake foam. Validity fades to zero\n// toward the window edge (the state there is either zero or another world period's).\n@group(1) @binding(38) var ocean_ripple_tex: texture_2d<f32>;\n\nfn ocean_ripple_fade(base_xz: vec2<f32>, center: vec2<f32>) -> f32 {\n let d = abs(base_xz - center) * ocean.ripple0.z;\n return 1.0 - smoothstep(0.38, 0.47, max(d.x, d.y));\n}\n\nfn ocean_ripple_sample(base_xz: vec2<f32>) -> vec3<f32> {\n if (ocean.ripple0.w <= 0.0) {\n return vec3<f32>(0.0);\n }\n let fade = ocean_ripple_fade(base_xz, ocean.ripple0.xy);\n if (fade <= 0.0) {\n return vec3<f32>(0.0);\n }\n let s = textureSampleLevel(ocean_ripple_tex, ocean_fft_sampler, base_xz * ocean.ripple0.z, 0.0);\n return vec3<f32>(s.r, s.g, s.b) * fade;\n}\n\n// Ripple height for displacement; use_prev picks the previous-frame surface (exact MVs),\n// faded by the PREVIOUS window since that's the region it was simulated for.\nfn ocean_ripple_height(base_xz: vec2<f32>, use_prev: bool) -> f32 {\n if (ocean.ripple0.w <= 0.0) {\n return 0.0;\n }\n let center = select(ocean.ripple0.xy, ocean.ripple1.xy, use_prev);\n let fade = ocean_ripple_fade(base_xz, center);\n if (fade <= 0.0) {\n return 0.0;\n }\n let s = textureSampleLevel(ocean_ripple_tex, ocean_fft_sampler, base_xz * ocean.ripple0.z, 0.0);\n return select(s.r, s.g, use_prev) * fade * ocean.ripple0.w;\n}\n\n// Mip level matching the local mesh cell (VS has no derivatives): waves shorter than the\n// vertex spacing average out in the mip chain instead of aliasing into per-frame jitter.\nfn ocean_fft_lod(inv_l: f32, r_m: f32) -> f32 {\n let texel_m = 1.0 / (inv_l * 256.0);\n let local_cell = max(ocean.cell_min, r_m * ocean.cell_growth);\n return clamp(log2(max(local_cell / texel_m, 1.0)), 0.0, 8.0);\n}\n\n// Explicit-LOD sampling only: legal in any control flow.\nfn ocean_fft_displace(base_xz: vec2<f32>, r_m: f32) -> vec3<f32> {\n if (ocean.fft1.x < 0.5) {\n return vec3<f32>(0.0);\n }\n let f0 = ocean_fft_fade(r_m, ocean.fft0.z);\n let f1 = ocean_fft_fade(r_m, ocean.fft0.w);\n var d = vec3<f32>(0.0);\n if (f0 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.x, r_m);\n d += textureSampleLevel(ocean_fft_disp0, ocean_fft_sampler, base_xz * ocean.fft0.x, lod).xyz * f0;\n }\n if (f1 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.y, r_m);\n d += textureSampleLevel(ocean_fft_disp1, ocean_fft_sampler, base_xz * ocean.fft0.y, lod).xyz * f1;\n }\n return d;\n}\n\n// ocean_mv.wgsl \u2014 motion vectors for the displaced ocean surface.\n// Prefixed at pipeline build with ocean_gerstner.wgsl. For each base point we evaluate the\n// Gerstner displacement at the current and previous frame phases (both uploaded pre-wrapped,\n// integrated in f64 on the CPU), so the MV is the exact surface motion \u2014 no history textures.\n// Rasterizes with the JITTERED camera view_proj against the scene depth (read-only LessEqual,\n// like motion_vectors_geom.wgsl); MV values use the UNJITTERED cur/prev matrices and the same\n// `prev_uv - cur_uv` convention.\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct OceanMvUniform {\n view_proj: mat4x4<f32>, // current frame, unjittered\n prev_view_proj: mat4x4<f32>, // previous frame, unjittered\n};\n@group(1) @binding(0) var<uniform> u_mv: OceanMvUniform;\n\nstruct VsIn {\n @location(0) offset: vec2<f32>,\n};\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @location(0) cur_clip: vec4<f32>,\n @location(1) prev_clip: vec4<f32>,\n};\n\n// Previous-frame FFT displacement maps (copied before this frame's cascade update), so\n// the FFT detail contributes exact motion vectors too.\n@group(1) @binding(33) var ocean_fft_disp0_prev: texture_2d<f32>;\n@group(1) @binding(34) var ocean_fft_disp1_prev: texture_2d<f32>;\n\nfn ocean_fft_displace_prev(base_xz: vec2<f32>, r_m: f32) -> vec3<f32> {\n if (ocean.fft1.x < 0.5) {\n return vec3<f32>(0.0);\n }\n let f0 = ocean_fft_fade(r_m, ocean.fft0.z);\n let f1 = ocean_fft_fade(r_m, ocean.fft0.w);\n var d = vec3<f32>(0.0);\n if (f0 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.x, r_m);\n d += textureSampleLevel(ocean_fft_disp0_prev, ocean_fft_sampler, base_xz * ocean.fft0.x, lod).xyz * f0;\n }\n if (f1 > 0.0) {\n let lod = ocean_fft_lod(ocean.fft0.y, r_m);\n d += textureSampleLevel(ocean_fft_disp1_prev, ocean_fft_sampler, base_xz * ocean.fft0.y, lod).xyz * f1;\n }\n return d;\n}\n\n@vertex\nfn vs_main(in: VsIn) -> VsOut {\n let base_xz = u_camera.camera_position.xz + in.offset;\n let r = length(in.offset);\n let cur = vec4<f32>(ocean_displace(base_xz, r, false) + ocean_fft_displace(base_xz, r), 1.0);\n let prev = vec4<f32>(ocean_displace(base_xz, r, true) + ocean_fft_displace_prev(base_xz, r), 1.0);\n var out: VsOut;\n out.clip_pos = u_camera.view_proj * cur;\n out.cur_clip = u_mv.view_proj * cur;\n out.prev_clip = u_mv.prev_view_proj * prev;\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec2<f32> {\n if (in.prev_clip.w <= 0.0 || in.cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = in.cur_clip.xy / in.cur_clip.w;\n let prev_ndc = in.prev_clip.xy / in.prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n"},{"label":"shaders/ocean_spectrum.wgsl","code":"// ocean_spectrum.wgsl \u2014 FFT ocean cascade spectrum (init + per-frame evolve).\n// One 256x256 cascade per dispatch pair. Frequency convention: texel index m maps to\n// signed mode m' = m for m < 128 else m - 256, k = TAU * m' / patch_size. No fftshift;\n// h0(-k) is the modulo-mirrored texel. The evolve pass packs two Hermitian spectra into\n// one rgba32f texture: rg = Dy + i*Dx, ba = Dz (+ i*0), so a single complex IFFT chain\n// yields Dy = out.r, Dx = out.g, Dz = out.b (see ocean_fft.wgsl / ocean_derivatives.wgsl).\n\nconst N: i32 = 256;\nconst TAU: f32 = 6.283185307179586;\nconst GRAVITY: f32 = 9.81;\n\nstruct SpectrumParams {\n patch_size_m: f32,\n amplitude: f32, // Phillips-style A, pre-scaled on the CPU from the ocean settings\n k_lo: f32, // band-pass: modes outside [k_lo, k_hi) are zeroed (the low band\n k_hi: f32, // belongs to the Gerstner swell, higher bands to other cascades)\n wind_dir: vec2<f32>,\n chop: f32, // horizontal displacement scale (applied to the Dx/Dz spectra)\n time_s: f32,\n speed_scale: f32, // SetOceanWaves SpeedPercent / 100 on the dispersion relation\n seed: u32,\n _pad0: f32,\n _pad1: f32,\n};\n@group(0) @binding(0) var<uniform> sp: SpectrumParams;\n@group(0) @binding(1) var h0_tex: texture_2d<f32>;\n@group(0) @binding(2) var out_tex: texture_storage_2d<rgba32float, write>;\n\nfn wave_k(id: vec2<i32>) -> vec2<f32> {\n let mx = select(id.x, id.x - N, id.x >= N / 2);\n let my = select(id.y, id.y - N, id.y >= N / 2);\n return TAU * vec2<f32>(f32(mx), f32(my)) / sp.patch_size_m;\n}\n\n// PCG-ish hash -> [0,1)\nfn hash_u32(v: u32) -> u32 {\n var s = v * 747796405u + 2891336453u;\n s = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;\n return (s >> 22u) ^ s;\n}\nfn rand01(texel: vec2<u32>, salt: u32) -> f32 {\n let h = hash_u32(texel.x ^ (texel.y << 16u) ^ (salt * 0x9e3779b9u) ^ sp.seed);\n return f32(h) / 4294967296.0;\n}\n\n// h0(k) = sqrt(P(k)/2) * (gauss, gauss); directional Phillips band-passed per cascade.\n@compute @workgroup_size(8, 8, 1)\nfn cs_init(@builtin(global_invocation_id) gid: vec3<u32>) {\n let id = vec2<i32>(gid.xy);\n let k = wave_k(id);\n let klen = length(k);\n var h0 = vec2<f32>(0.0);\n if (klen >= sp.k_lo && klen < sp.k_hi) {\n let khat = k / klen;\n // Directional spread: wind-aligned squared cosine + small omnidirectional floor.\n let d = dot(khat, sp.wind_dir);\n let dir_amp = 0.08 + 0.92 * d * d * select(0.35, 1.0, d > 0.0); // damp upwind waves\n // Phillips-style falloff with a small-wave viscosity cutoff (l = 4cm).\n let l_small = 0.04;\n let p = sp.amplitude / max(klen * klen * klen * klen, 1e-8)\n * dir_amp * exp(-klen * klen * l_small * l_small);\n // Box-Muller gaussian pair\n let u1 = max(rand01(gid.xy, 17u), 1e-6);\n let u2 = rand01(gid.xy, 43u);\n let mag = sqrt(-2.0 * log(u1));\n let g = vec2<f32>(mag * cos(TAU * u2), mag * sin(TAU * u2));\n h0 = sqrt(p * 0.5) * g;\n }\n textureStore(out_tex, id, vec4<f32>(h0, 0.0, 0.0));\n}\n\nfn cmul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);\n}\nfn conj(a: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(a.x, -a.y);\n}\n\n// h(k,t) = h0(k) e^{i w t} + conj(h0(-k)) e^{-i w t}; pack Dy + i*Dx into rg, Dz into ba.\n@compute @workgroup_size(8, 8, 1)\nfn cs_evolve(@builtin(global_invocation_id) gid: vec3<u32>) {\n let id = vec2<i32>(gid.xy);\n let k = wave_k(id);\n let klen = length(k);\n let h0k = textureLoad(h0_tex, id, 0).rg;\n let mirror = vec2<i32>((N - id.x) % N, (N - id.y) % N);\n let h0mk = textureLoad(h0_tex, mirror, 0).rg;\n\n let w = sqrt(GRAVITY * max(klen, 1e-6)) * sp.speed_scale;\n let ph = w * sp.time_s;\n let e = vec2<f32>(cos(ph), sin(ph));\n let h = cmul(h0k, e) + cmul(conj(h0mk), conj(e)); // Hermitian: real Dy field\n\n var dx = vec2<f32>(0.0);\n var dz = vec2<f32>(0.0);\n if (klen > 1e-6) {\n let khat = k / klen;\n // i * khat * h, scaled by chop (horizontal \"Gerstner-like\" pinch)\n dx = sp.chop * khat.x * vec2<f32>(-h.y, h.x);\n dz = sp.chop * khat.y * vec2<f32>(-h.y, h.x);\n }\n // Pack: c0 = Dy + i*Dx (rg), c1 = Dz + i*0 (ba)\n let c0 = vec2<f32>(h.x - dx.y, h.y + dx.x);\n textureStore(out_tex, id, vec4<f32>(c0, dz));\n}\n"},{"label":"shaders/ocean_fft.wgsl","code":"// ocean_fft.wgsl \u2014 256-point separable inverse DFT (brute force, one thread per output).\n// Deliberately NOT a shared-memory FFT: the LDS Stockham version produced small\n// nondeterministic per-frame output wobble on the D3D12/FXC path (bit-identical inputs,\n// varying outputs \u2014 verified via ORI_FFT_HASH readbacks vs a CPU reference), which\n// re-jittered the whole ocean every frame. A barrier-free O(N) loop per output texel is\n// bulletproof and still cheap at 256^2 x 2 cascades (~67M MACs/frame, well under the\n// pass budget). Convention: +i twiddles (inverse), no 1/N scaling \u2014 normalization is\n// folded into the spectrum amplitude. Both packed complex signals (rg, ba) transform\n// together. CPU mirror in ocean_fft.rs::tests validates the math.\n\nconst N: u32 = 256u;\nconst TAU: f32 = 6.283185307179586;\n\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var dst: texture_storage_2d<rgba32float, write>;\n\nfn cmul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);\n}\n\n// out[k] = sum_n src[n] * e^{+i TAU k n / N}, along x (horizontal) or y (vertical).\nfn dft_line(out_xy: vec2<u32>, k: u32, horizontal: bool) {\n // Incremental twiddle rotation: w_n = e^{+i TAU k n / N} = w_{n-1} * step.\n let ang = TAU * f32(k) / f32(N);\n let step = vec2<f32>(cos(ang), sin(ang));\n var w = vec2<f32>(1.0, 0.0);\n var acc0 = vec2<f32>(0.0);\n var acc1 = vec2<f32>(0.0);\n for (var n = 0u; n < N; n++) {\n let coord = select(vec2<u32>(out_xy.x, n), vec2<u32>(n, out_xy.y), horizontal);\n let v = textureLoad(src, vec2<i32>(coord), 0);\n acc0 += cmul(w, v.rg);\n acc1 += cmul(w, v.ba);\n w = cmul(w, step);\n // Re-normalize the rotator every 64 steps: pure f32 rotation drifts ~1e-5/step.\n if ((n & 63u) == 63u) {\n w = normalize(w) * 1.0;\n let exact = TAU * f32(k) * f32(n + 1u) / f32(N);\n w = vec2<f32>(cos(exact), sin(exact));\n }\n }\n textureStore(dst, vec2<i32>(out_xy), vec4<f32>(acc0, acc1));\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn cs_h(@builtin(global_invocation_id) gid: vec3<u32>) {\n // Horizontal: output (x=k, y=row); sum runs over source x.\n dft_line(gid.xy, gid.x, true);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn cs_v(@builtin(global_invocation_id) gid: vec3<u32>) {\n // Vertical: output (x=col, y=k); sum runs over source y.\n dft_line(gid.xy, gid.y, false);\n}\n"},{"label":"shaders/ocean_derivatives.wgsl","code":"// ocean_derivatives.wgsl \u2014 unpack the IFFT spatial field into sampleable maps:\n// disp (rgba16f): xyz = (Dx, Dy, Dz) displacement in meters, w = Jacobian J\n// grad (rgba16f): xy = (dDy/dx, dDy/dz) height gradient, z = foam (temporally\n// accumulated whitecap coverage from Jacobian pinching), w = J\n// Finite differences with wrap addressing (the field is periodic). Foam decays\n// exponentially and is re-injected wherever J drops below the bias (crest about to fold).\n\nconst N: i32 = 256;\n\nstruct DerivParams {\n texel_size_m: f32, // patch_size / 256\n dt_s: f32,\n foam_decay: f32, // 1/s\n foam_bias: f32, // J below this injects foam\n foam_gain: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n@group(0) @binding(0) var<uniform> dp: DerivParams;\n@group(0) @binding(1) var spatial: texture_2d<f32>; // rgba32f: r=Dy, g=Dx, b=Dz\n@group(0) @binding(2) var disp_out: texture_storage_2d<rgba16float, write>;\n@group(0) @binding(3) var grad_out: texture_storage_2d<rgba16float, write>;\n@group(0) @binding(4) var grad_prev: texture_2d<f32>;\n\nfn wrap(p: vec2<i32>) -> vec2<i32> {\n return vec2<i32>((p.x + N) % N, (p.y + N) % N);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let id = vec2<i32>(gid.xy);\n let c = textureLoad(spatial, id, 0);\n let xp = textureLoad(spatial, wrap(id + vec2<i32>(1, 0)), 0);\n let xm = textureLoad(spatial, wrap(id - vec2<i32>(1, 0)), 0);\n let zp = textureLoad(spatial, wrap(id + vec2<i32>(0, 1)), 0);\n let zm = textureLoad(spatial, wrap(id - vec2<i32>(0, 1)), 0);\n\n let inv2h = 1.0 / (2.0 * dp.texel_size_m);\n // Layout reminder: .r = Dy (height), .g = Dx, .b = Dz\n let ddy_dx = (xp.r - xm.r) * inv2h;\n let ddy_dz = (zp.r - zm.r) * inv2h;\n let ddx_dx = (xp.g - xm.g) * inv2h;\n let ddx_dz = (zp.g - zm.g) * inv2h;\n let ddz_dx = (xp.b - xm.b) * inv2h;\n let ddz_dz = (zp.b - zm.b) * inv2h;\n\n let jacobian = (1.0 + ddx_dx) * (1.0 + ddz_dz) - ddx_dz * ddz_dx;\n\n // Rate-based accumulation in BOTH directions: `max(inject, prev)` made fresh foam pop\n // in within one frame (frame-diff showed whole patches flashing); now foam takes\n // ~0.25s to build and ~1.5s to fade, tracking the Jacobian smoothly.\n let inject = clamp((dp.foam_bias - jacobian) * 2.0, 0.0, 1.0);\n let prev = textureLoad(grad_prev, id, 0).z * exp(-dp.foam_decay * dp.dt_s);\n let foam = clamp(prev + inject * dp.foam_gain * dp.dt_s, 0.0, 1.0);\n\n textureStore(disp_out, id, vec4<f32>(c.g, c.r, c.b, jacobian));\n textureStore(grad_out, id, vec4<f32>(ddy_dx, ddy_dz, foam, jacobian));\n}\n"},{"label":"shaders/ocean_mip.wgsl","code":"// ocean_mip.wgsl \u2014 2x2 box downsample for the FFT ocean disp/grad mip chains.\n// Mips let the VS/FS sample at the local mesh-cell / pixel footprint: waves shorter than\n// the sampling density average toward zero instead of aliasing into per-frame jitter\n// (the \"whole ocean shaking\" root cause, see ocean_gerstner.wgsl::ocean_fft_lod).\n\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var dst: texture_storage_2d<rgba16float, write>;\n\n@compute @workgroup_size(8, 8, 1)\nfn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let dims = textureDimensions(dst);\n if (gid.x >= dims.x || gid.y >= dims.y) {\n return;\n }\n let p = vec2<i32>(gid.xy) * 2;\n let v = (textureLoad(src, p, 0)\n + textureLoad(src, p + vec2<i32>(1, 0), 0)\n + textureLoad(src, p + vec2<i32>(0, 1), 0)\n + textureLoad(src, p + vec2<i32>(1, 1), 0)) * 0.25;\n textureStore(dst, vec2<i32>(gid.xy), v);\n}\n"},{"label":"shaders/stability_probe.wgsl","code":"// stability_probe.wgsl \u2014 temporal stability metric (QA `stability_probe` command).\n// Compares this frame's HDR scene (post water/transparents, pre bloom) against the probe's\n// own previous-frame copy over a screen rect, and accumulates one stats slot per frame:\n// accum[slot*4+0] += sum(|lum - prev_lum|) * 1024 (luminance clamped to [0,4])\n// accum[slot*4+1] max= bitcast(|delta|) (positive f32 bits order like u32)\n// accum[slot*4+2] += sum(lum) * 64\n// accum[slot*4+3] += pixel count\n// Read back once at the end: shimmer/shaking/upscaler flicker shows as elevated per-frame\n// means or spikes that single screenshots can't reveal.\n\nstruct ProbeParams {\n rect_min: vec2<u32>,\n rect_max: vec2<u32>,\n frame_slot: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n@group(0) @binding(0) var<uniform> pp: ProbeParams;\n@group(0) @binding(1) var cur_tex: texture_2d<f32>;\n@group(0) @binding(2) var prev_tex: texture_2d<f32>;\n@group(0) @binding(3) var<storage, read_write> accum: array<atomic<u32>>;\n\nconst LUMA709: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);\n\nvar<workgroup> wg_delta: atomic<u32>;\nvar<workgroup> wg_lum: atomic<u32>;\nvar<workgroup> wg_max: atomic<u32>;\nvar<workgroup> wg_px: atomic<u32>;\n\n@compute @workgroup_size(8, 8, 1)\nfn cs_main(\n @builtin(global_invocation_id) gid: vec3<u32>,\n @builtin(local_invocation_index) lidx: u32,\n) {\n let p = pp.rect_min + gid.xy;\n if (all(p < pp.rect_max)) {\n let cur = textureLoad(cur_tex, vec2<i32>(p), 0).rgb;\n let prev = textureLoad(prev_tex, vec2<i32>(p), 0).rgb;\n let lum = clamp(dot(cur, LUMA709), 0.0, 4.0);\n let plum = clamp(dot(prev, LUMA709), 0.0, 4.0);\n let d = abs(lum - plum);\n atomicAdd(&wg_delta, u32(d * 1024.0));\n atomicAdd(&wg_lum, u32(lum * 64.0));\n atomicMax(&wg_max, bitcast<u32>(d));\n atomicAdd(&wg_px, 1u);\n }\n workgroupBarrier();\n if (lidx == 0u) {\n let base = pp.frame_slot * 4u;\n atomicAdd(&accum[base + 0u], atomicLoad(&wg_delta));\n atomicMax(&accum[base + 1u], atomicLoad(&wg_max));\n atomicAdd(&accum[base + 2u], atomicLoad(&wg_lum));\n atomicAdd(&accum[base + 3u], atomicLoad(&wg_px));\n }\n}\n"},{"label":"particle_system_spawn","code":"// particle_spawn.wgsl\nstruct EmitterInstanceParams {\n world_matrix: mat4x4<f32>,\n world_matrix_inv: mat4x4<f32>,\n emitter_time: f32,\n delta_time: f32,\n emitter_id: u32,\n system_scale: f32,\n\n instance_tint: vec3<f32>,\n _pad_tint: f32,\n hard_clear: u32, // 1 => kill this emitter's live particles instantly (DestroyImmediate)\n _pad3: u32,\n\n spawnStartDelay: f32,\n spawnRate: f32,\n\n isLooping: i32,\n duration: f32, // 0 duration means running indefinitely\n burstParticleCount: u32,\n burstInterval: f32,\n\n emitterShapeType: i32, // 0 = Sphere, 1 = Box, 2 = Cylinder, 3 = Disc, etc.\n shapeStyle: i32, // 0 = Volume, 1 = Surface\n radius: f32, // Sphere, Cylinder, Disc\n cylinderHeight: f32,\n\n boxSize: vec3<f32>,\n shapePartial: f32,\n\n lifeTimeRange: vec2<f32>,\n speedRange: vec2<f32>,\n spreadAngle: vec2<f32>, // angle of the spread of the particles in x,y\n shapeInOut: i32, // velocity pointing inwards or outwards, 0 = inward, 1 = outward, 2 = both\n\n lockedToEmitter : i32, // 0 = world\u2011space (default), 1 = local\u2011space\n // Note: requires 16-bytes alignment\n};\nstruct DeadList { \n dead_count: atomic<u32>,\n spawn_counter: atomic<u32>,\n data: array<u32>, \n};\nstruct ParticleState { \n position: vec4<f32>, \n velocity: vec4<f32>,\n emitter_id: u32,\n seed: u32,\n flags: u32,\n _pad: u32,\n};\n\nstruct ParticleSpawnInfo {\n localSpawnPos: vec3<f32>,\n localMotionDirection: vec3<f32>,\n};\n\n@group(0) @binding(0) var<storage, read> emitters: array<EmitterInstanceParams>;\n@group(1) @binding(0) var<storage, read_write> particles: array<ParticleState>;\n@group(2) @binding(0) var<storage, read_write> dead: DeadList;\n@group(2) @binding(1) var<storage, read_write> debug: atomic<i32>;\n\nconst PI: f32 = 3.141592653589793;\nconst FLAG_LOCKED: u32 = 1u;\n\n@compute @workgroup_size(1) // 1 thread per emitter\nfn cs_main(@builtin(global_invocation_id) gid : vec3<u32>) {\n let slot = gid.x;\n if (slot >= arrayLength(&emitters)) { \n return; \n }\n\n let e = emitters[slot];\n var t = e.emitter_time;\n let dt = e.delta_time;\n\n if (dt <= 0.0) {\n return; // prevent further spawning from this dead emitter slot\n }\n\n if (e.isLooping == 1 && e.duration > 0.0) {\n let cycleTime = e.spawnStartDelay + e.duration;\n t = t % cycleTime; // wrap time for looping\n }\n if (t < e.spawnStartDelay) {\n return; // before the start delay, do nothing\n }\n \n let localTime = t - e.spawnStartDelay; // how long since spawn actually began?\n\n if (e.isLooping == 0) {\n if (e.duration > 0.0 && localTime > e.duration) {\n return; // Explicit duration\n }\n }\n\n // burst\n var burstCount = 0u;\n if (e.burstInterval > 0.0) {\n // special case with duration==0 and looping, just give out first burst at time 0\n let oneBurstDurationZeroDone = e.isLooping == 0 && e.duration == 0.0 && localTime > 0.1; // TODO: why need 0.1 and not 1/60.0\n if (!oneBurstDurationZeroDone) \n {\n // Prevent tiny negative `(localTime - dt)` caused by fp roundoff from triggering a second\n // \"time 0\" burst on the next frame.\n let eps = 1e-4;\n let n0 = floor((localTime + eps) / e.burstInterval);\n let n1 = floor(((localTime - dt) + eps) / e.burstInterval); // round down towards -inf, so first burst is at 0\n if (n0 != n1) {\n burstCount = e.burstParticleCount;\n }\n }\n }\n\n // Deterministic low\u2011rate spawning\n // - accumulate the *integral* of spawnRate over time and emit the difference between the current and previous frame.\n // - guarantees that exactly \u230at \u00b7 spawnRate\u230b particles have been produced after t seconds\n let totalPrev = floor(max(localTime - dt, 0.0) * e.spawnRate);\n let totalCurr = floor(localTime * e.spawnRate);\n var toSpawn = burstCount + u32(max(totalCurr - totalPrev, 0.0));\n\n // pull indices from the free list\n loop {\n if (toSpawn == 0u) { break; }\n let old = atomicLoad(&dead.dead_count);\n if (old == 0u) { break; } // list empty\n\n if (atomicCompareExchangeWeak(&dead.dead_count, old, old - 1u).exchanged) {\n let idx = dead.data[old - 1u];\n dead.data[old - 1u] = 0xFFFFFFFFu;\n toSpawn -= 1u;\n atomicAdd(&debug, 1);\n\n let uniqueId = atomicAdd(&dead.spawn_counter, 1u);\n let seed = hash32(uniqueId);\n let randomA = rand01(seed);\n let randomB = rand01(seed ^ 0x9e3779b9u);\n let randomC = rand01(seed ^ 0x2345u);\n let randomD = rand01(seed ^ 0x3456u);\n let randomE = rand01(seed ^ 0x4567u);\n let randomF = rand01(seed ^ 0x5678u);\n\n // shape\n var spawnInfo: ParticleSpawnInfo;\n switch (e.emitterShapeType) {\n default: { // sphere\n spawnInfo = spawnSphere(randomA, randomB, randomC, e.shapePartial, e.radius, e.shapeStyle);\n }\n case 1: { // box\n spawnInfo = spawnBox(randomA, randomB, randomC, e.boxSize, e.shapeStyle);\n }\n case 2: { // cylinder\n spawnInfo = spawnCylinder(randomA, randomB, randomC, e.radius, e.cylinderHeight, e.shapeStyle, e.shapePartial);\n }\n case 3: { // disc\n spawnInfo = spawnDisc(randomA, randomB, randomC, e.radius, e.shapePartial, e.shapeStyle, e.cylinderHeight);\n }\n }\n let localSpawnPos = spawnInfo.localSpawnPos;\n var localMotionDir = spawnInfo.localMotionDirection;\n switch(e.shapeInOut) {\n case 0: { // Inward\n localMotionDir = -localMotionDir;\n }\n case 2: { // Both\n let flip = select(-1.0, 1.0, randomF < 0.5); // 50% chance to flip sign\n localMotionDir = flip * localMotionDir;\n }\n default: {}\n };\n // Apply spreadAngle\n {\n let randG = rand01(seed ^ 0x6789u);\n let randH = rand01(seed ^ 0x789au);\n\n // Convert \u00b1spread degrees \u2192 radians\n let angleY = radians((randG * 2.0 - 1.0) * e.spreadAngle.x); // horizontal (Y axis)\n let angleX = radians((randH * 2.0 - 1.0) * e.spreadAngle.y); // vertical (X axis)\n\n // Rotate around local X (pitch)\n if (abs(angleX) > 1e-6) {\n let cx = cos(angleX);\n let sx = sin(angleX);\n localMotionDir = vec3<f32>(\n localMotionDir.x,\n localMotionDir.y * cx - localMotionDir.z * sx,\n localMotionDir.y * sx + localMotionDir.z * cx\n );\n }\n // Rotate around local Y (yaw)\n if (abs(angleY) > 1e-6) {\n let cy = cos(angleY);\n let sy = sin(angleY);\n localMotionDir = vec3<f32>(\n localMotionDir.x * cy + localMotionDir.z * sy,\n localMotionDir.y,\n -localMotionDir.x * sy + localMotionDir.z * cy\n );\n }\n\n // Ensure we still have a unit direction\n localMotionDir = safeNormalize(localMotionDir);\n }\n\n let isLocked = e.lockedToEmitter == 1;\n let s = select(e.system_scale, 1.0, e.system_scale == 0.0);\n var spawnPos = vec4<f32>(localSpawnPos * s, 1.0);\n let speed = mix(e.speedRange.x, e.speedRange.y, randomE);\n var motionDir = vec4<f32>(localMotionDir * speed * s, 0.0);\n if (!isLocked) {\n spawnPos = e.world_matrix * spawnPos;\n motionDir = e.world_matrix * motionDir;\n }\n\n // position and age\n let lifetime = mix(e.lifeTimeRange.x, e.lifeTimeRange.y, randomD);\n particles[idx].position = vec4<f32>(spawnPos.xyz, lifetime); // remainingLife\n particles[idx].velocity = vec4<f32>(motionDir.xyz, lifetime); // store total life\n particles[idx].emitter_id = e.emitter_id;\n particles[idx].seed = seed;\n particles[idx].flags = select(0u, FLAG_LOCKED, isLocked);\n }\n }\n}\n\n//\n// Helpers\n//\nfn hash32(x_in: u32) -> u32 {\n var x = x_in;\n x ^= x >> 16u;\n x *= 0x7feb352du;\n x ^= x >> 15u;\n x *= 0x846ca68bu;\n x ^= x >> 16u;\n return x;\n}\nfn rand01(seed: u32) -> f32 {\n return f32(hash32(seed)) * (1.0 / 4294967296.0); // Convert to [0,1) with 24 bits of mantissa precision\n}\nfn safeNormalize(v: vec3<f32>) -> vec3<f32> {\n let len = length(v);\n if (len < 1e-7) {\n return vec3<f32>(0.0, 0.0, 1.0);\n }\n return v / len;\n}\nfn spawnSphere(randomA: f32, randomB: f32, randomC: f32, partial: f32, radius: f32, style: i32) -> ParticleSpawnInfo {\n let theta = 2.0 * PI * randomA;\n let phi = acos(2.0 * randomB - 1.0) * partial; // partial sphere => multiply random by partial\n let localMotionDirection = vec3<f32>(\n sin(phi) * cos(theta),\n sin(phi) * sin(theta),\n cos(phi)\n );\n var r = radius;\n if (style == 0) { // Volume = 0, Surface = 1\n r *= pow(randomC, 1.0/3.0); \n }\n return ParticleSpawnInfo(r * localMotionDirection, localMotionDirection);\n}\nfn spawnBox(randomA: f32, randomB: f32, randomC: f32, size: vec3<f32>, style: i32) -> ParticleSpawnInfo {\n if (style == 1) { // Surface\n var localMotionDirection = vec3<f32>(0.0, 0.0, 1.0);\n var localSpawnPos = vec3<f32>(\n (randomB - 0.5) * size.x, \n (randomC - 0.5) * size.y, \n 0.5 * size.z\n );\n return ParticleSpawnInfo(localSpawnPos, localMotionDirection);\n } else { // Volume\n let localSpawnPos = vec3<f32>(\n (randomA - 0.5) * size.x,\n (randomB - 0.5) * size.y,\n (randomC - 0.5) * size.z\n );\n let localMotionDirection = vec3<f32>(0.0, 0.0, 1.0); // same as roblox\n return ParticleSpawnInfo(localSpawnPos, localMotionDirection);\n }\n}\nfn spawnCylinder(randomA: f32, randomB: f32, randomC: f32, radius: f32, height: f32, style: i32, shapePartial: f32) -> ParticleSpawnInfo {\n let angle = 2.0 * PI * randomA;\n let localMotionDirection = vec3<f32>(0.0, cos(angle), sin(angle));\n var r = radius;\n if (style == 0) { // Volume = 0\n r *= sqrt(randomC);\n }\n let z0to1 = randomC;\n r *= shapePartial + (1.0 - shapePartial) * (1.0 - z0to1); // shape partial (cone at shapePartial = 0)\n let localSpawnPos = vec3<f32>(\n (z0to1 - 0.5) * height,\n r * cos(angle),\n r * sin(angle),\n );\n return ParticleSpawnInfo(localSpawnPos, localMotionDirection); \n}\nfn spawnDisc(randomA: f32, randomB: f32, randomC: f32, radius: f32, partial: f32, style: i32, discHeight: f32) -> ParticleSpawnInfo {\n // partial is the fraction that reduces the innerRadius\n let innerRadius = radius * (1.0 - partial); // r in [innerRadius..radius]\n let angle = 2.0 * PI * randomA;\n\n var r = sqrt(randomB * (radius * radius - innerRadius * innerRadius) + innerRadius * innerRadius);\n // if (style == 1) { // Surface\n // r = radius;\n // } else { // Volume\n // r = sqrt(randomB * (radius * radius - innerRadius * innerRadius) + innerRadius * innerRadius);\n // }\n var height = 0.0;\n if (style == 0) { // Volume = 0\n height = discHeight * (randomC - 0.5);\n }\n return ParticleSpawnInfo(\n vec3<f32>(r * cos(angle), r * sin(angle), height), \n vec3<f32>(0.0, 0.0, 1.0) // same as roblox\n );\n}"},{"label":"particle_system_update","code":"// particle_system_update.wgsl\nstruct EmitterInstanceParams {\n world_matrix : mat4x4<f32>,\n world_matrix_inv : mat4x4<f32>,\n emitter_time: f32,\n delta_time: f32,\n emitter_id: u32,\n system_scale: f32,\n\n instance_tint: vec3<f32>,\n _pad_tint: f32,\n hard_clear: u32, // 1 => kill this emitter's live particles instantly (DestroyImmediate)\n _pad3: u32,\n\n spawnStartDelay: f32,\n spawnRate: f32,\n isLooping: i32,\n duration: f32, // 0 duration means running indefinitely\n burstParticleCount: u32,\n burstInterval: f32,\n\n emitterShapeType: i32, // 0 = Sphere, 1 = Box, 2 = Cylinder, 3 = Disc, etc.\n shapeStyle: i32, // 0 = Volume, 1 = Surface\n radius: f32, // Sphere, Cylinder, Disc\n cylinderHeight: f32,\n\n boxSize: vec3<f32>,\n shapePartial: f32,\n\n lifeTimeRange: vec2<f32>,\n speedRange: vec2<f32>,\n spreadAngle: vec2<f32>, // angle of the spread of the particles in x,y\n shapeInOut: i32, // velocity pointing inwards or outwards, 0 = inward, 1 = outward, 2 = both\n\n lockedToEmitter : i32, // 0 = world\u2011space (default), 1 = local\u2011space\n};\nstruct ParticleTypeUniforms {\n // System parameter\n time: f32,\n delta_time: f32,\n\n // Flipbook\n framesX: f32,\n framesY: f32,\n totalFrames: f32,\n flipbookAnimationSpeed: f32,\n flipbookStartRandom: i32,\n\n // Update\n sizeConstant: f32,\n sizeSequence: array<vec4<f32>, 8>,\n opacityConstant: f32,\n opacitySequence: array<vec4<f32>, 8>,\n colorConstant: vec3<f32>,\n colorSequence: array<vec4<f32>, 8>,\n\n accelerationVector: vec3<f32>, // constant acceleration\n drag: f32, // time (sec) to halve velocity\n\n rotationRange: vec2<f32>,\n rotationSpeedRange: vec2<f32>,\n\n isHidden: i32,\n additiveBlend: i32,\n orientation: i32, // 0 = FacingCam, 1 = FacingCamWorldUp, 2 = VelParallel, 3 = VelPerpendicular\n softParticleFadeDisabled: i32,\n softParticleFadeDistance: f32, // keep layout identical to render wgsl\n squashConstant: f32, // 0 = none, >0 squash Up, <0 squash Side\n squashSequence: array<vec4<f32>, 8>,\n // .x: 1 => orphaned particles hard-cut (no dying tail), e.g. editor holograms.\n // vec4 (not i32) keeps the uniform struct 16-byte aligned to match the reflected buffer size.\n instantClearOnRemove: vec4<i32>,\n // .x: analytic icon card drawn instead of the flipbook texture (0 = none, 1 = play, 2 = build).\n iconMode: vec4<i32>,\n};\nstruct DeadList {\n dead_count: atomic<u32>,\n spawn_counter: atomic<u32>,\n data: array<u32>,\n};\nstruct ParticleState {\n position: vec4<f32>, // xyz + remainingLife\n velocity: vec4<f32>, // xyz + totalLifetime\n emitter_id: u32,\n seed: u32,\n flags: u32,\n _pad: u32,\n};\n\n@group(0) @binding(0) var<uniform> u: ParticleTypeUniforms;\n@group(1) @binding(0) var<storage, read_write> particles: array<ParticleState>;\n@group(1) @binding(1) var<storage, read> emitters: array<EmitterInstanceParams>;\n@group(2) @binding(0) var<storage, read_write> dead: DeadList;\n@group(2) @binding(1) var<storage, read_write> debug: atomic<i32>;\n\nconst FLAG_LOCKED: u32 = 1u;\nconst FLAG_DYING: u32 = 2u;\nconst DEAD_SENTINEL: f32 = -100000000.0;\n\n@compute @workgroup_size(64)\nfn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let idx = gid.x;\n if (idx >= arrayLength(&particles)) { return; }\n\n var p = particles[idx];\n\n // unlock lockedToEmitter particles once if the original emitter is gone\n let wasLocked = (p.flags & FLAG_LOCKED) != 0u;\n let slot = p.emitter_id & 0x000FFFFF; // slot extracted even if ID stale\n var emitterValid = false;\n if (slot < arrayLength(&emitters)) {\n if (emitters[slot].emitter_id == p.emitter_id) {\n emitterValid = true;\n } else {\n p.emitter_id = 0xFFFFFFFFu;\n }\n }\n // Cache system scale while the emitter is still valid.\n // This allows render to keep applying the original scale for \"dying\" particles after the emitter is removed.\n if (emitterValid) {\n let system_scale = emitters[slot].system_scale;\n let s = select(system_scale, 1.0, system_scale == 0.0);\n p._pad = bitcast<u32>(s);\n }\n let isDying = (p.flags & FLAG_DYING) != 0u;\n // DestroyImmediate: per-slot hard_clear kills this emitter's particles instantly.\n // Applies to live particles of a still-valid flagged emitter, and to orphans at their\n // tombstone transition (the freed slot still holds our tombstone params that frame).\n var hardClear = false;\n if (slot < arrayLength(&emitters) && emitters[slot].hard_clear != 0u) {\n hardClear = emitterValid || !isDying;\n }\n if (hardClear || (!emitterValid && !isDying && u.instantClearOnRemove.x != 0)) {\n // Hard cut (editor holograms / DestroyImmediate): vanish with no dying tail.\n if (p.position.w > DEAD_SENTINEL + 1.0) { // was alive => recycle its slot exactly once\n let old = atomicAdd(&dead.dead_count, 1u);\n dead.data[old] = idx;\n atomicAdd(&debug, -1);\n }\n p.position.w = DEAD_SENTINEL;\n p.emitter_id = 0xFFFFFFFFu;\n p.flags = 0u;\n particles[idx] = p;\n return;\n }\n if (!emitterValid && !isDying) {\n if (wasLocked) {\n // convert to world, then short tail\n if (slot < arrayLength(&emitters)) {\n // best effort \u2013 only convert if slot still in bounds\n let w = emitters[slot].world_matrix;\n p.position = vec4<f32>((w * vec4<f32>(p.position.xyz , 1.0)).xyz, p.position.w);\n p.velocity = vec4<f32>((w * vec4<f32>(p.velocity.xyz , 0.0)).xyz, p.velocity.w);\n }\n p.flags &= ~FLAG_LOCKED; // clear the lock\u2011bit\n\n let lifeClamp = 1.0; // clamp both remaining and total life\n p.position.w = min(p.position.w , lifeClamp);\n p.velocity.w = min(p.velocity.w , lifeClamp);\n } else {\n let lifeClamp = 5.0;\n p.position.w = min(p.position.w , lifeClamp);\n p.velocity.w = min(p.velocity.w , lifeClamp);\n }\n p.flags |= FLAG_DYING; // set dying flag\n }\n\n // dead check\n if (p.position.w <= 0.0) {\n return;\n }\n\n let dt = u.delta_time;\n p.position.w -= dt; // update time\n\n if (p.position.w <= 0.0) { // dead\n if (p.position.w > DEAD_SENTINEL + 1.0) { // just died\n let old = atomicAdd(&dead.dead_count, 1u);\n dead.data[old] = idx;\n p.position.w = DEAD_SENTINEL;\n p.emitter_id = 0xFFFFFFFFu;\n p.flags = 0u;\n particles[idx] = p;\n\n atomicAdd(&debug, -1);\n }\n return;\n }\n\n let acc = u.accelerationVector; // in z-up world space\n var acc_space_corrected = vec3<f32>(acc.x, acc.z, -acc.y); // Z\u2011up \u279c Y\u2011up\n if ((p.flags & FLAG_LOCKED) != 0u) { // locked \u2192 convert to emitter local\n let slot = p.emitter_id & 0x000FFFFF;\n acc_space_corrected = (emitters[slot].world_matrix_inv * vec4<f32>(acc_space_corrected, 0.0)).xyz;\n }\n // scale acceleration by emitter system scale (default 1.0 if unset)\n {\n let slot = p.emitter_id & 0x000FFFFF;\n if (slot < arrayLength(&emitters)) {\n let system_scale = emitters[slot].system_scale;\n let s = select(system_scale, 1.0, system_scale == 0.0);\n acc_space_corrected *= s;\n }\n }\n var v = p.velocity.xyz + acc_space_corrected * dt;\n\n if (u.drag > 0.0) {\n v *= exp(-log(2.0) * dt / u.drag);\n }\n p.velocity = vec4<f32>(v, p.velocity.w);\n p.position += vec4<f32>(v * dt, 0.0);\n\n particles[idx] = p;\n}\n"},{"label":"particle_system_dead_append","code":"// Append new dead indices to the dead list buffer on GPU.\n// Layout of dead buffer (u32 words):\n// [0] = dead_count, [1] = spawn_counter, [2..] = indices\n// Uniform params: old_particle_capacity, extra, pad0, pad1\n\nstruct Params {\n old_cap: u32,\n extra: u32,\n _pad0: u32,\n _pad1: u32,\n};\n\n@group(0) @binding(0)\nvar<uniform> params: Params;\n\n@group(1) @binding(0)\nvar<storage, read_write> dead_buffer: array<u32>;\n\n// Single-threaded append to avoid atomics; dispatch exactly one workgroup\n@compute @workgroup_size(1)\nfn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x != 0u) { return; }\n let old_dead_count = dead_buffer[0];\n for (var i: u32 = 0u; i < params.extra; i = i + 1u) {\n let write_pos = 2u + old_dead_count + i;\n dead_buffer[write_pos] = params.old_cap + i;\n }\n dead_buffer[0] = old_dead_count + params.extra;\n}\n\n"},{"label":"particle_system","code":"// particle_system_render.wgsl\nstruct ParticleTypeUniforms {\n // System parameter\n time: f32,\n delta_time: f32,\n \n // Flipbook\n framesX: f32,\n framesY: f32,\n totalFrames: f32,\n flipbookAnimationSpeed: f32,\n flipbookStartRandom: i32,\n\n // Update\n sizeConstant: f32,\n sizeSequence: array<vec4<f32>, 8>,\n opacityConstant: f32,\n opacitySequence: array<vec4<f32>, 8>,\n colorConstant: vec3<f32>,\n colorSequence: array<vec4<f32>, 8>,\n\n accelerationVector: vec3<f32>, // constant acceleration\n drag: f32, // time (sec) to halve velocity\n\n rotationRange: vec2<f32>,\n rotationSpeedRange: vec2<f32>,\n\n isHidden: i32,\n additiveBlend: i32,\n orientation: i32, // 0 = FacingCam, 1 = FacingCamWorldUp, 2 = VelParallel, 3 = VelPerpendicular\n softParticleFadeDisabled: i32,\n softParticleFadeDistance: f32, // in clip-depth units; 0 => default\n lit: i32, // 1 => sun+sky wrap-lit billboard (smoke reads as a lit volume, not flat unlit grey)\n squashConstant: f32, // 0 = none, >0 squash Up, <0 squash Side\n squashSequence: array<vec4<f32>, 8>,\n // .x: 1 => orphaned particles hard-cut (no dying tail), e.g. editor holograms.\n // vec4 (not i32) keeps the uniform struct 16-byte aligned to match the reflected buffer size.\n instantClearOnRemove: vec4<i32>,\n // .x: analytic icon card drawn instead of the flipbook texture (0 = none, 1 = play, 2 = build).\n iconMode: vec4<i32>,\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n sun_direction: vec4<f32>,\n sun_color: vec4<f32>,\n sky_ambient_color: vec4<f32>,\n};\nstruct ParticleState {\n position: vec4<f32>, // xyz + remainingLife in w\n velocity: vec4<f32>, // xyz + totalLifetime in w\n emitter_id: u32,\n seed: u32,\n flags: u32,\n _pad: u32,\n};\nstruct EmitterInstanceParams {\n world_matrix : mat4x4<f32>,\n world_matrix_inv : mat4x4<f32>,\n emitter_time: f32,\n delta_time: f32,\n emitter_id: u32,\n system_scale: f32,\n \n instance_tint: vec3<f32>, // when non-zero, overrides u.colorConstant/sequence\n _pad_tint: f32,\n hard_clear: u32, // 1 => kill this emitter's live particles instantly (DestroyImmediate)\n _pad3: u32,\n\n spawnStartDelay: f32,\n spawnRate: f32,\n\n isLooping: i32,\n duration: f32, // 0 duration means running indefinitely\n burstParticleCount: u32,\n burstInterval: f32,\n\n emitterShapeType: i32, // 0 = Sphere, 1 = Box, 2 = Cylinder, 3 = Disc, etc.\n shapeStyle: i32, // 0 = Volume, 1 = Surface\n radius: f32, // Sphere, Cylinder, Disc\n cylinderHeight: f32,\n\n boxSize: vec3<f32>,\n shapePartial: f32,\n\n lifeTimeRange: vec2<f32>,\n speedRange: vec2<f32>,\n spreadAngle: vec2<f32>, // angle of the spread of the particles in x,y\n shapeInOut: i32, // velocity pointing inwards or outwards, 0 = inward, 1 = outward, 2 = both\n\n lockedToEmitter : i32, // 0 = world\u2011space (default), 1 = local\u2011space\n};\n\n@group(1) @binding(0) var<uniform> u: ParticleTypeUniforms; // first to ensure it gets read first by naga\n@group(2) @binding(0) var flipbookTexture: texture_2d<f32>;\n@group(2) @binding(1) var flipbookSampler: sampler;\n@group(2) @binding(2) var depthTexture: texture_2d<f32>;\n@group(2) @binding(3) var depthSampler: sampler;\nstruct FogSettings {\n color: vec3<f32>,\n mode: u32,\n start: f32,\n end_: f32,\n density: f32,\n height_enabled:u32,\n height_weight: f32,\n height_bottom: f32,\n height_top: f32,\n height_softness: f32,\n sky_affect: f32,\n};\n@group(2) @binding(4) var<uniform> fog: FogSettings;\nstruct ParticleRenderTargetParams {\n // xy = particle color target size, zw = full-resolution scene depth texture size.\n particle_and_depth_size: vec4<f32>,\n};\n@group(2) @binding(5) var<uniform> rt: ParticleRenderTargetParams;\n@group(3) @binding(0) var<storage, read> particles: array<ParticleState>;\n@group(3) @binding(1) var<storage, read> emitters : array<EmitterInstanceParams>;\n\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nconst PI: f32 = 3.141592653589793;\nconst FLAG_LOCKED: u32 = 1u;\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vertex_id: u32,\n @builtin(instance_index) instance_id: u32,\n) -> VSOutput {\n var out: VSOutput;\n\n let p = particles[instance_id];\n let remainingLife = p.position.w;\n if (remainingLife <= 0.0) {\n out.clipPosition = vec4<f32>(2.0, 2.0, 2.0, 1.0); // discard\n return out;\n }\n let totalLife = max(p.velocity.w, 1e-6);\n let spawnTime = u.time - (totalLife - remainingLife);\n let localTime = u.time - spawnTime;\n let lifeProgress = clamp(localTime / totalLife, 0.0, 1.0);\n let emitterSlot = p.emitter_id & 0x000FFFFFu;\n var emitterValid = false;\n if (emitterSlot < arrayLength(&emitters)) {\n emitterValid = emitters[emitterSlot].emitter_id == p.emitter_id;\n }\n \n // let particleCenter = p.position.xyz;\n\n var particleCenter : vec3<f32>;\n let isLocked = (p.flags & FLAG_LOCKED) != 0u;\n if (isLocked && emitterValid) {\n particleCenter = (emitters[emitterSlot].world_matrix * vec4<f32>(p.position.xyz, 1.0)).xyz;\n } else {\n particleCenter = p.position.xyz;\n }\n\n // velocity in world space\n var worldVel = p.velocity.xyz;\n if (isLocked && emitterValid) {\n let e = emitters[emitterSlot];\n worldVel = (e.world_matrix * vec4<f32>(worldVel, 0.0)).xyz;\n }\n if (length(worldVel) < 1e-4) {\n worldVel = vec3<f32>(0.0, 0.01, 0.0);\n }\n\n // random\n var seed = p.seed;\n let randomA = xorshift32(&seed);\n let randomB = xorshift32(&seed);\n let randomC = xorshift32(&seed);\n\n // Flipbook logic\n let framesX_u = max(u32(max(u.framesX, 1.0)), 1u);\n let framesY_u = max(u32(max(u.framesY, 1.0)), 1u);\n let maxTiles = max(framesX_u * framesY_u, 1u);\n let totalFrames_u = max(1u, min(max(u32(max(u.totalFrames, 1.0)), 1u), maxTiles));\n let framesX_f = f32(framesX_u);\n let framesY_f = f32(framesY_u);\n let totalFrames_f = f32(totalFrames_u);\n\n let startFrameOffset = select(0.0, floor(randomA * totalFrames_f), u.flipbookStartRandom == 1);\n let frameFloat = (startFrameOffset + localTime * u.flipbookAnimationSpeed) % totalFrames_f;\n let currentFrameF = floor(frameFloat);\n out.blendFactor = frameFloat - currentFrameF; // fraction used to blend frames\n let tileSize = 1.0 / vec2<f32>(framesX_f, framesY_f);\n\n // Current frame\n let currentFrameU = u32(currentFrameF) % totalFrames_u;\n let uFrameU = currentFrameU % framesX_u;\n let vFrameU = currentFrameU / framesX_u;\n let currentFrameOffset = vec2<f32>(f32(uFrameU), f32(vFrameU)) * tileSize;\n\n // Next frame\n let nextFrameU = (currentFrameU + 1u) % totalFrames_u;\n let uNextU = nextFrameU % framesX_u;\n let vNextU = nextFrameU / framesX_u;\n let nextFrameOffset = vec2<f32>(f32(uNextU), f32(vNextU)) * tileSize;\n\n // quad corners\n const CORNERS = array<vec2<f32>, 4>(\n vec2<f32>(-0.5, -0.5),\n vec2<f32>( 0.5, -0.5),\n vec2<f32>(-0.5, 0.5),\n vec2<f32>( 0.5, 0.5),\n );\n let corner = CORNERS[vertex_id];\n \n // rotation\n let baseRotation = mix(u.rotationRange.x, u.rotationRange.y, randomB);\n let rotationSpeed = mix(u.rotationSpeedRange.x, u.rotationSpeedRange.y, randomC);\n let angle = radians(baseRotation + rotationSpeed * localTime);\n let cosr = cos(angle);\n let sinr = sin(angle);\n var corner2 = vec2<f32>(\n corner.x * cosr - corner.y * sinr,\n corner.x * sinr + corner.y * cosr\n );\n out.lit_corner = corner2 * 2.0; // corners are +-0.5; normal math wants [-1,1]\n\n // size\n var size = u.sizeConstant;\n if (size == 0.0) {\n size = sampleSequence(u.sizeSequence, lifeProgress).x;\n }\n size = select(size, 0.0, u.isHidden == 1);\n\n // scale billboard size by systemScale (default 1.0 if unset)\n {\n var s = 1.0;\n if (emitterValid) {\n s = emitters[emitterSlot].system_scale;\n } else {\n // Emitter slot reused/tombstoned or invalid: use cached scale stored per-particle.\n s = bitcast<f32>(p._pad);\n }\n size *= select(s, 1.0, s == 0.0);\n }\n\n // Orientation options\n var right: vec3<f32>;\n var up: vec3<f32>;\n\n switch (u.orientation) {\n case 0: { // FacingCam\n right = camera.camera_right;\n up = camera.camera_up;\n }\n case 1: { // FacingCamWorldUp\n let toCam = normalize(camera.camera_position - particleCenter);\n right = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), toCam));\n up = vec3<f32>(0.0, 1.0, 0.0);\n }\n case 2: { // 2 \u2500 VelocityParallel\n var upDir = normalize(worldVel);\n if (length(upDir) < 1e-4) { // particle is stationary?\n upDir = camera.camera_up; // graceful fallback\n }\n let toCam = normalize(camera.camera_position - particleCenter);\n right = cross(upDir, toCam);\n if (length(right) < 1e-4) { // velocity \u2225 view?\n right = camera.camera_right; // fallback avoids NaNs\n } else {\n right = normalize(right);\n }\n up = upDir;\n }\n case 3: { // VelPerpendicular (stable world-space UV frame)\n // Build a camera-independent tangent frame from the velocity direction\n var n = normalize(worldVel);\n if (length(n) < 1e-4) {\n n = vec3<f32>(0.0, 1.0, 0.0); // fallback normal\n }\n // Choose a stable reference axis not parallel to n\n var refAxis = vec3<f32>(0.0, 0.0, 1.0);\n if (abs(dot(n, refAxis)) > 0.99) {\n refAxis = vec3<f32>(1.0, 0.0, 0.0);\n }\n // Tangent frame that does not depend on camera view direction\n right = normalize(cross(n, refAxis));\n up = normalize(cross(right, n));\n }\n case 4: { // HorizontalFlat: quad lies in the world XZ plane (ground/water decals,\n // e.g. footstep splash rings). World-fixed frame, camera-independent.\n right = vec3<f32>(1.0, 0.0, 0.0);\n up = vec3<f32>(0.0, 0.0, 1.0);\n }\n default: {\n right = camera.camera_right;\n up = camera.camera_up;\n }\n }\n\n // squash\n var s = u.squashConstant;\n if (s == 0.0) {\n let hasSeq = (u.squashSequence[1].x != 0.0) || // if this x-key is 0, then only 1 point\n (u.squashSequence[0].y != 0.0) ||\n (u.squashSequence[0].z != 0.0) ||\n (u.squashSequence[0].w != 0.0);\n if (hasSeq) {\n s = sampleSequence(u.squashSequence, lifeProgress).x;\n }\n }\n var sx = size;\n var sy = size;\n if (s != 0.0) {\n let k = 1.0 + abs(s); // strength; keeps area ~constant\n if (s > 0.0) { // squash Y\n sx = size * k;\n sy = size / k;\n } else { // squash X\n sx = size / k;\n sy = size * k;\n }\n }\n\n let localPos = particleCenter\n + corner2.x * right * sy\n + corner2.y * up * sx;\n\n\n // 0..1 coordinates inside the tile (inset by ~half a texel to avoid atlas bleeding)\n var tileUV = corner + vec2<f32>(0.5, 0.5);\n tileUV.y = 1.0 - tileUV.y;\n let texDim = vec2<f32>(textureDimensions(flipbookTexture));\n let halfTexel = 0.5 / max(texDim, vec2<f32>(1.0));\n let inset = min(halfTexel, tileSize * 0.49);\n out.uv_current = tileUV * (tileSize - 2.0 * inset) + (currentFrameOffset + inset);\n out.uv_next = tileUV * (tileSize - 2.0 * inset) + (nextFrameOffset + inset);\n \n // Transform to clip space with camera\n let worldPos = vec4<f32>(localPos, 1.0);\n out.clipPosition = camera.view_proj * worldPos;\n\n // Color and opacity\n var color = u.colorConstant;\n if (emitterValid) {\n color = emitters[emitterSlot].instance_tint;\n }\n let eps: f32 = 0.0005;\n let is_near_black = all(color <= vec3<f32>(eps));\n let is_near_white = all(abs(color - vec3<f32>(1.0)) <= vec3<f32>(eps));\n if (is_near_black || is_near_white) {\n color = u.colorConstant;\n let is_const_near_black = all(color <= vec3<f32>(eps));\n if (is_const_near_black) {\n color = sampleSequence(u.colorSequence, lifeProgress);\n }\n }\n var opacity = u.opacityConstant;\n if (opacity == 0.0) {\n opacity = sampleSequence(u.opacitySequence, lifeProgress).r;\n }\n out.tint_color = vec4<f32>(color, opacity);\n\n // out.tint_color = vec4<f32>(emitter_color(p.emitter_id), opacity); // debug emitter_id\n\n return out;\n}\n\nstruct VSOutput {\n @builtin(position) clipPosition: vec4<f32>,\n @location(0) uv_current: vec2<f32>,\n @location(1) uv_next: vec2<f32>,\n @location(2) blendFactor: f32,\n @location(3) tint_color: vec4<f32>,\n // Rotated quad corner in [-1,1]; drives the spherical pseudo-normal for lit billboards.\n @location(4) lit_corner: vec2<f32>,\n};\nfn flip_uv(v: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(v.x, 1.0 - v.y);\n}\n\n// ---- Analytic Play/Edit hologram icon cards (u.iconMode.x: 1 = play, 2 = build) ----\n// Drawn procedurally (SDF + fwidth AA) instead of sampling the flipbook texture, so the panels\n// stay crisp at any distance. Shapes/levels match the original T_PlayIcon/T_EditIcon2 art;\n// authored in sRGB grayscale and converted to linear at the end (the texture path decodes sRGB).\n// Offline preview of the same math: temp_weave/icon_sdf_preview.py.\n\nfn icon_sd_rbox(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {\n let q = abs(p) - b + vec2<f32>(r);\n return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - r;\n}\n\nfn icon_sd_tri(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> f32 {\n let e0 = b - a; let e1 = c - b; let e2 = a - c;\n let v0 = p - a; let v1 = p - b; let v2 = p - c;\n let pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);\n let pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);\n let pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);\n let s = sign(e0.x * e2.y - e0.y * e2.x);\n let d = min(min(vec2<f32>(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)),\n vec2<f32>(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x))),\n vec2<f32>(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)));\n return -sqrt(d.x) * sign(d.y);\n}\n\nfn icon_aa(d: f32) -> f32 { // 1 inside, 0 outside, ~1px smooth edge\n return clamp(0.5 - d / max(fwidth(d), 1e-5), 0.0, 1.0);\n}\n\nfn icon_aa_line(d_center: f32, half_w: f32) -> f32 {\n // Stroke around an SDF iso-line, clamped to a minimum screen-space width so thin frame\n // lines still read when the card is far away instead of dissolving below one pixel.\n let fw = max(fwidth(d_center), 1e-5);\n return clamp(0.5 - (abs(d_center) - max(half_w, 0.7 * fw)) / fw, 0.0, 1.0);\n}\n\nfn icon_glyph_sdf(p: vec2<f32>, mode: i32) -> f32 {\n if (mode == 1) { // play: rounded right-pointing triangle\n let gs = 0.78;\n return (icon_sd_tri(p / gs, vec2<f32>(-0.26, 0.36), vec2<f32>(-0.26, -0.36), vec2<f32>(0.34, 0.0)) - 0.06) * gs;\n }\n // build: hammer. Design frame = head horizontal on top (claw at left end, hooking down),\n // handle straight down; rotated 45deg CW so the handle points bottom-left like the old art.\n let c45 = 0.70710678;\n var h = vec2<f32>(c45 * (p.x - p.y), c45 * (p.x + p.y));\n h.y = h.y - 0.05;\n let d_handle = icon_sd_rbox(h + vec2<f32>(0.0, 0.34), vec2<f32>(0.09, 0.46), 0.09);\n let d_head = icon_sd_rbox(h - vec2<f32>(-0.06, 0.30), vec2<f32>(0.26, 0.135), 0.08);\n let d_neck = icon_sd_rbox(h - vec2<f32>(0.0, 0.16), vec2<f32>(0.11, 0.12), 0.05);\n // claw: thick arc curling down-left to a blunt tip\n let cc = h - vec2<f32>(-0.18, 0.10);\n let d_ring = abs(length(cc) - 0.28) - 0.10;\n let d_claw = max(d_ring, max(cc.x, 0.342 * cc.x - 0.940 * cc.y));\n return min(min(d_handle, d_head), min(d_neck, d_claw));\n}\n\nfn icon_card_color(uv: vec2<f32>, mode: i32) -> vec4<f32> {\n let p = vec2<f32>((uv.x - 0.5) * 2.0, (0.5 - uv.y) * 2.0);\n // card frame: rounded box with subtle 45deg corner cuts\n let cham = (abs(p.x) + abs(p.y)) * 0.70710678 - 1.135;\n let d_frame = max(icon_sd_rbox(p, vec2<f32>(0.90, 0.74), 0.05), cham);\n let frame_line = icon_aa_line(d_frame, 0.010);\n // corner accents: second line just outside the frame, near corners only\n let corner = select(0.0, 1.0, abs(p.x) > 0.70 && abs(p.y) > 0.52);\n let acc_line = icon_aa_line(d_frame - 0.028, 0.007) * corner;\n let panel = icon_aa(max(icon_sd_rbox(p, vec2<f32>(0.84, 0.68), 0.035), cham + 0.030));\n let glyph = icon_aa(icon_glyph_sdf(p, mode));\n // grayscale compose (sRGB space): panel 0.395, glyph white, scanlines pulled toward 0.60\n var col = mix(0.395, 1.0, glyph);\n let sf = uv.y * 42.5;\n let fw = fwidth(sf);\n let f = fract(sf);\n let e = max(0.06, fw);\n var stripe = clamp((f - 0.22) / e, 0.0, 1.0) * (1.0 - clamp((f - 0.72) / e, 0.0, 1.0));\n stripe = mix(stripe, 0.5, clamp(fw * 1.5 - 0.2, 0.0, 1.0)); // fade to average when minified\n col = col + (0.60 - col) * 0.75 * stripe;\n col = col * panel;\n let line_v = max(frame_line, acc_line);\n col = mix(col, 1.0, line_v);\n let alpha = max(panel, line_v);\n let lin = pow(max(col, 0.0), 2.2);\n return vec4<f32>(lin, lin, lin, alpha);\n}\n\nfn particle_fragment(in: VSOutput) -> vec4<f32> {\n let particleTargetSize = max(rt.particle_and_depth_size.xy, vec2<f32>(1.0, 1.0));\n let depthTextureSize = max(rt.particle_and_depth_size.zw, vec2<f32>(1.0, 1.0));\n let screenUv = clamp(\n in.clipPosition.xy / particleTargetSize,\n vec2<f32>(0.0, 0.0),\n vec2<f32>(0.999999, 0.999999),\n );\n\n let depthPxF = floor(screenUv * depthTextureSize);\n let depthMaxPx = vec2<i32>(i32(depthTextureSize.x) - 1, i32(depthTextureSize.y) - 1);\n let depthPx = clamp(\n vec2<i32>(i32(depthPxF.x), i32(depthPxF.y)),\n vec2<i32>(0, 0),\n depthMaxPx,\n );\n let sceneDepthRaw = textureLoad(depthTexture, depthPx, 0).r;\n // depthTexture is the gbuffer NDC depth; linearize to view meters so the occlusion/fade\n // math below runs in consistent units (sky = 1.0 lands on far). The retired R32F\n // attachment only held ~meters for terrain-family writers; this fixes mesh occluders too.\n let sceneDepth =\n (camera.near * camera.far) / (camera.far - sceneDepthRaw * (camera.far - camera.near));\n let particleDepth = in.clipPosition.z / in.clipPosition.w;\n\n // Half-res particles cannot use the full-res depth attachment. Keep hard occlusion in shader\n // regardless of the soft-particle fade toggle, otherwise disabled soft particles leak through walls.\n let hardOcclusionEpsilon = 0.0001;\n if (sceneDepth != 0.0 && particleDepth > sceneDepth + hardOcclusionEpsilon) {\n discard;\n }\n\n var color: vec4<f32>;\n if (u.iconMode.x != 0) {\n // Analytic Play/Edit hologram card; branch is uniform (u), so this is uniform control flow.\n color = icon_card_color(in.uv_current, u.iconMode.x) * in.tint_color;\n } else {\n let colorCurrent = textureSample(flipbookTexture, flipbookSampler, in.uv_current);\n let colorNext = textureSample(flipbookTexture, flipbookSampler, in.uv_next);\n color = mix(colorCurrent, colorNext, in.blendFactor) * in.tint_color;\n }\n\n // Sun+sky wrap lighting on a spherical pseudo-normal (uniform branch: u.lit).\n if (u.lit != 0) {\n var lit_ndc = vec4<f32>(flip_uv(screenUv) * 2.0 - 1.0, in.clipPosition.z, 1.0);\n let lit_world_h = camera.inverse_view_proj * lit_ndc;\n let lit_world = lit_world_h.xyz / max(lit_world_h.w, 1e-6);\n let to_cam = normalize(camera.camera_position - lit_world);\n let r2 = clamp(dot(in.lit_corner, in.lit_corner), 0.0, 1.0);\n let n = normalize(in.lit_corner.x * normalize(camera.camera_right)\n + in.lit_corner.y * normalize(camera.camera_up)\n + sqrt(1.0 - r2) * to_cam);\n let sun_l = normalize(-camera.sun_direction.xyz);\n let wrap = clamp(dot(n, sun_l) * 0.5 + 0.5, 0.0, 1.0);\n color = vec4<f32>(\n color.rgb * (camera.sky_ambient_color.rgb + camera.sun_color.rgb * wrap * wrap),\n color.a,\n );\n }\n\n // Soft particle fade (runtime toggle). Hard occlusion above intentionally does not depend on this.\n var alphaFade: f32 = 1.0;\n if (u.softParticleFadeDisabled == 0) {\n let fadeDistance = select(0.05, u.softParticleFadeDistance, u.softParticleFadeDistance > 0.0);\n let diff = sceneDepth - particleDepth;\n if (sceneDepth == 0.0) {\n alphaFade = 1.0;\n } else {\n alphaFade = clamp(diff / fadeDistance, 0.0, 1.0);\n }\n }\n color.a *= alphaFade;\n\n // Store premultiplied RGB for additive particles. Additive target alpha is preserved by the\n // additive pipeline blend state, so pure emissive contribution stays alpha=0 for composite.\n if (u.additiveBlend == 1) {\n color = vec4<f32>(color.rgb * color.a, color.a);\n }\n\n // Air fog from camera to particle (apply in-material; post fog already applied to background).\n var ndc = vec4<f32>(flip_uv(screenUv) * 2.0 - 1.0, in.clipPosition.z, 1.0);\n let view_h = camera.inverse_proj * ndc;\n let view_pos = view_h.xyz / max(view_h.w, 1e-6);\n let dist_m = length(view_pos);\n let world_h = camera.inverse_view_proj * ndc;\n let world_pos = world_h.xyz / max(world_h.w, 1e-6);\n\n var f: f32;\n switch (fog.mode) {\n default { f = clamp((dist_m - fog.start) / max(fog.end_ - fog.start, 1e-6), 0.0, 1.0); }\n case 1u { f = 1.0 - exp(-fog.density * dist_m); }\n case 2u { f = 1.0 - exp(-fog.density * dist_m * fog.density * dist_m); }\n }\n f = clamp(f, 0.0, 1.0);\n if (fog.height_enabled != 0u) {\n let lo = min(fog.height_bottom, fog.height_top);\n let hi = max(fog.height_bottom, fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, world_pos.y);\n let w = clamp(fog.height_weight, 0.0, 1.0);\n f = f * mix(1.0, h, w);\n }\n\n var rgb = color.rgb;\n if (u.additiveBlend == 1) {\n rgb *= (1.0 - f); // Attenuate emissive add by transmittance.\n } else {\n rgb = mix(rgb, vec3<f32>(fog.color), f);\n }\n color = vec4<f32>(rgb, color.a);\n\n if (color.a < 0.01 && length(color.rgb) < 0.0001) {\n discard;\n }\n return color;\n}\n\n// Half-res path: accumulation buffer only (composite derives reactivity when upsampling).\n@fragment\nfn fs_main(in: VSOutput) -> @location(0) vec4<f32> {\n return particle_fragment(in);\n}\n\nstruct FSOutReactive {\n @location(0) color: vec4<f32>,\n @location(1) reactive: f32,\n};\n\n// Full-res path: draws straight into the scene target, so it must also write the temporal\n// reactivity mask itself (max-blended R8). Additive components carry premultiplied RGB with\n// alpha preserved, so luminance catches them like the composite's formula does.\n@fragment\nfn fs_main_reactive(in: VSOutput) -> FSOutReactive {\n var out: FSOutReactive;\n out.color = particle_fragment(in);\n let luma = dot(out.color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n out.reactive = min(max(out.color.a, luma * 0.7), 0.9);\n return out;\n}\n\nfn xorshift32(state: ptr<function,u32>) -> f32 {\n var x = *state;\n x ^= x << 13u;\n x ^= x >> 17u;\n x ^= x << 5u;\n *state = x;\n return f32(x) * (1.0 / 4294967296.0);\n}\n\n// Note: default to 1.0 if no sequence is found\nfn sampleSequence(seq: array<vec4<f32>, 8>, t: f32) -> vec3<f32> {\n var prevKey = 0.0;\n var prevVal = vec3<f32>(0.0);\n var foundFirstKey = false;\n var outVal = vec3<f32>(0.0);\n\n for (var i = 0; i < 8; i = i + 1) {\n let pair = seq[i];\n let key = pair.x;\n let val = pair.yzw;\n\n if ((key == 0.0) && (i > 0)) {\n outVal = prevVal; // If key == 0 and it's not the first pair, we assume no more data\n if (prevKey == 0.0) { // double key 0,0 is invalid\n outVal = vec3<f32>(1.0); // default to 1.0 if no sequence is found\n }\n break;\n }\n\n if (!foundFirstKey) {\n foundFirstKey = true;\n prevKey = key;\n prevVal = val;\n if (t <= key) {\n outVal = val; // t is below the first key, clamp to the first value\n break;\n }\n } else {\n if (t < key) {\n // t is in [prevKey..key], we interpolate\n let alpha = (t - prevKey) / max(key - prevKey, 1e-6);\n outVal = mix(prevVal, val, alpha);\n break;\n } else if (t == key) {\n outVal = val; // Exactly on a key\n break;\n } else {\n // haven't found the interval yet, keep moving \n prevKey = key;\n prevVal = val;\n }\n }\n\n if (i == 7) {\n outVal = val; // If we got to i=7 with no break, then clamp to last val\n }\n }\n return outVal;\n}\n\nfn emitter_color(id: u32) -> vec3<f32> {\n var s = id;\n // three decorrelated random numbers\n let r = xorshift32(&s);\n let g = xorshift32(&s);\n let b = xorshift32(&s);\n\n // push values away from 0 so no color is too dark\n return vec3<f32>(r, g, b) * 0.7 + 0.3;\n}\n"},{"label":"particle_ribbon","code":"// particle_ribbon_render.wgsl\n// `\"type\": \"ribbon\"` particle components: one connected camera-facing strip through the\n// emitter's recent world-space path (boost trails, sword arcs, tracers). Points are sampled\n// CPU-side into a per-slot ring (slot-major flat array); the VS expands consecutive live ring\n// pairs into camera-facing quads (segment axis x view dir); the FS mirrors the billboard\n// shader's blending/soft-fade/fog exactly. Uses only constructs already browser-validated in\n// particle_system_render.wgsl.\nstruct RibbonTypeUniforms {\n // System (keep first: time uniform is written per frame at this reflected offset)\n time: f32,\n delta_time: f32,\n\n // Flipbook (frame selected by emitter time; the whole strip shares one frame)\n framesX: f32,\n framesY: f32,\n totalFrames: f32,\n flipbookAnimationSpeed: f32,\n flipbookStartRandom: i32,\n\n // Ribbon\n trailSeconds: f32, // point max age = trail length\n pointsPerSecond: f32, // CPU sampling rate (informational for the shader)\n ribbonPoints: f32, // ring capacity per emitter slot\n ribbonUv: i32, // 0 = stretch (head..tail 0..1), 1 = repeat_per_meter\n\n // Width/appearance over point age (width in meters, like billboard size)\n sizeConstant: f32,\n sizeSequence: array<vec4<f32>, 8>,\n opacityConstant: f32,\n opacitySequence: array<vec4<f32>, 8>,\n colorConstant: vec3<f32>,\n colorSequence: array<vec4<f32>, 8>,\n\n isHidden: i32,\n additiveBlend: i32,\n softParticleFadeDisabled: i32,\n softParticleFadeDistance: f32, // in view meters; 0 => default\n};\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n};\nstruct RibbonPoint {\n position_time: vec4<f32>, // xyz world pos + spawn time (u.time domain) in w\n cum_len: f32, // path meters, integer-wrapped CPU-side (fract = repeat UV)\n flags: u32, // bit 0: break - do not connect this point to the previous one\n _pad0: u32,\n _pad1: u32,\n};\n// Header layout identical to EmitterInstanceParams (shared CPU write/tombstone offsets),\n// then the ribbon ring cursor.\nstruct RibbonEmitterParams {\n world_matrix: mat4x4<f32>,\n world_matrix_inv: mat4x4<f32>,\n emitter_time: f32,\n delta_time: f32,\n emitter_id: u32,\n system_scale: f32,\n\n instance_tint: vec3<f32>, // when non-zero, overrides u.colorConstant/sequence\n _pad_tint: f32,\n hard_clear: u32, // 1 => strip vanishes instantly (DestroyImmediate / tombstone backstop)\n _pad3: u32,\n\n head: u32, // ring index the NEXT point will be written to\n count: u32, // live points in the ring (<= ribbonPoints)\n};\n\n@group(1) @binding(0) var<uniform> u: RibbonTypeUniforms; // first to ensure it gets read first by naga\n@group(2) @binding(0) var flipbookTexture: texture_2d<f32>;\n@group(2) @binding(1) var flipbookSampler: sampler;\n@group(2) @binding(2) var depthTexture: texture_2d<f32>;\n@group(2) @binding(3) var depthSampler: sampler;\nstruct FogSettings {\n color: vec3<f32>,\n mode: u32,\n start: f32,\n end_: f32,\n density: f32,\n height_enabled:u32,\n height_weight: f32,\n height_bottom: f32,\n height_top: f32,\n height_softness: f32,\n sky_affect: f32,\n};\n@group(2) @binding(4) var<uniform> fog: FogSettings;\nstruct ParticleRenderTargetParams {\n // xy = particle color target size, zw = full-resolution scene depth texture size.\n particle_and_depth_size: vec4<f32>,\n};\n@group(2) @binding(5) var<uniform> rt: ParticleRenderTargetParams;\n@group(3) @binding(0) var<storage, read> points: array<RibbonPoint>;\n@group(3) @binding(1) var<storage, read> emitters: array<RibbonEmitterParams>;\n\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nconst RIBBON_FLAG_BREAK: u32 = 1u;\n\nstruct VSOutput {\n @builtin(position) clipPosition: vec4<f32>,\n @location(0) uv_current: vec2<f32>,\n @location(1) uv_next: vec2<f32>,\n @location(2) blendFactor: f32,\n @location(3) tint_color: vec4<f32>,\n};\n\nfn degenerate() -> VSOutput {\n var out: VSOutput;\n out.clipPosition = vec4<f32>(2.0, 2.0, 2.0, 1.0); // offscreen discard\n return out;\n}\n\nfn hash01(v: u32) -> f32 { // wang hash -> 0..1, stable per emitter slot\n var x = v;\n x = (x ^ 61u) ^ (x >> 16u);\n x = x * 9u;\n x = x ^ (x >> 4u);\n x = x * 0x27d4eb2du;\n x = x ^ (x >> 15u);\n return f32(x) * (1.0 / 4294967296.0);\n}\n\nfn strip_width(age01: f32, system_scale: f32) -> f32 {\n var w = u.sizeConstant;\n if (w == 0.0) {\n w = sampleSequence(u.sizeSequence, age01).x;\n }\n w = select(w, 0.0, u.isHidden == 1);\n let s = select(system_scale, 1.0, system_scale == 0.0);\n return w * s;\n}\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vertex_id: u32,\n @builtin(instance_index) instance_id: u32,\n) -> VSOutput {\n let cap = max(u32(max(u.ribbonPoints, 2.0)), 2u);\n let segs_per_slot = cap - 1u;\n let slot = instance_id / segs_per_slot;\n let seg = instance_id % segs_per_slot;\n if (slot >= arrayLength(&emitters)) {\n return degenerate();\n }\n let e = emitters[slot];\n // seg 0 is the newest segment; walk backwards from head-1.\n if (e.hard_clear == 1u || e.count < 2u || seg + 1u >= e.count) {\n return degenerate();\n }\n let idx_new = (e.head + cap - 1u - seg) % cap;\n let idx_old = (idx_new + cap - 1u) % cap;\n let pn = points[slot * cap + idx_new];\n let po = points[slot * cap + idx_old];\n if ((pn.flags & RIBBON_FLAG_BREAK) != 0u) { // teleport: never connect across the jump\n return degenerate();\n }\n let trail = max(u.trailSeconds, 1e-3);\n let age_new = u.time - pn.position_time.w;\n let age_old = u.time - po.position_time.w;\n if (age_new > trail && age_old > trail) { // both ends aged out\n return degenerate();\n }\n let axis = pn.position_time.xyz - po.position_time.xyz;\n let seg_len = length(axis);\n if (seg_len < 1e-5) {\n return degenerate();\n }\n\n // Quad corners: x picks the segment end (old/new), y the side.\n const CORNERS = array<vec2<f32>, 4>(\n vec2<f32>(-0.5, -0.5),\n vec2<f32>( 0.5, -0.5),\n vec2<f32>(-0.5, 0.5),\n vec2<f32>( 0.5, 0.5),\n );\n let corner = CORNERS[vertex_id];\n let is_new_end = corner.x > 0.0;\n let end_pos = select(po.position_time.xyz, pn.position_time.xyz, is_new_end);\n let end_age01 = clamp(select(age_old, age_new, is_new_end) / trail, 0.0, 1.0);\n\n // Camera-facing expansion: segment axis x view dir (VelocityParallel pattern),\n // with the same fallbacks for axis-parallel-to-view.\n let mid = (pn.position_time.xyz + po.position_time.xyz) * 0.5;\n let toCam = normalize(camera.camera_position - mid);\n let axis_dir = axis / seg_len;\n var side = cross(axis_dir, toCam);\n if (length(side) < 1e-4) {\n side = camera.camera_right;\n } else {\n side = normalize(side);\n }\n let half_width = strip_width(end_age01, e.system_scale) * 0.5;\n let worldPos = end_pos + side * (corner.y * 2.0 * half_width);\n\n var out: VSOutput;\n out.clipPosition = camera.view_proj * vec4<f32>(worldPos, 1.0);\n\n // Flipbook frame from emitter time: the whole strip shares one frame per draw.\n let framesX_u = max(u32(max(u.framesX, 1.0)), 1u);\n let framesY_u = max(u32(max(u.framesY, 1.0)), 1u);\n let maxTiles = max(framesX_u * framesY_u, 1u);\n let totalFrames_u = max(1u, min(max(u32(max(u.totalFrames, 1.0)), 1u), maxTiles));\n let framesX_f = f32(framesX_u);\n let framesY_f = f32(framesY_u);\n let totalFrames_f = f32(totalFrames_u);\n let startFrameOffset =\n select(0.0, floor(hash01(slot) * totalFrames_f), u.flipbookStartRandom == 1);\n let frameFloat = (startFrameOffset + e.emitter_time * u.flipbookAnimationSpeed) % totalFrames_f;\n let currentFrameF = floor(frameFloat);\n out.blendFactor = frameFloat - currentFrameF;\n let tileSize = 1.0 / vec2<f32>(framesX_f, framesY_f);\n let currentFrameU = u32(currentFrameF) % totalFrames_u;\n let currentFrameOffset =\n vec2<f32>(f32(currentFrameU % framesX_u), f32(currentFrameU / framesX_u)) * tileSize;\n let nextFrameU = (currentFrameU + 1u) % totalFrames_u;\n let nextFrameOffset =\n vec2<f32>(f32(nextFrameU % framesX_u), f32(nextFrameU / framesX_u)) * tileSize;\n\n // Strip UVs: u along the strip, v across. repeat_per_meter is world-anchored: cum_len is\n // path meters (CPU wraps it at an integer bound, so fract() stays continuous and precise\n // for trails alive for hours) - the pattern stays painted on the path, no crawl.\n var strip_u: f32;\n if (u.ribbonUv == 1) { // repeat_per_meter: one tile per meter of path\n strip_u = min(fract(select(po.cum_len, pn.cum_len, is_new_end)), 0.999);\n } else { // stretch: age01 1.0 stays at the tile's far edge, never fract-wraps to 0\n strip_u = min(end_age01, 0.999);\n }\n var tileUV = vec2<f32>(strip_u, corner.y + 0.5);\n tileUV.y = 1.0 - tileUV.y;\n let texDim = vec2<f32>(textureDimensions(flipbookTexture));\n let halfTexel = 0.5 / max(texDim, vec2<f32>(1.0));\n let inset = min(halfTexel, tileSize * 0.49);\n out.uv_current = tileUV * (tileSize - 2.0 * inset) + (currentFrameOffset + inset);\n out.uv_next = tileUV * (tileSize - 2.0 * inset) + (nextFrameOffset + inset);\n\n // Color and opacity over point age; instance tint overrides like billboards\n // (ribbon emitters are always slot-valid: the ring is per-slot, cleared on claim).\n var color = e.instance_tint;\n let eps: f32 = 0.0005;\n let is_near_black = all(color <= vec3<f32>(eps));\n let is_near_white = all(abs(color - vec3<f32>(1.0)) <= vec3<f32>(eps));\n if (is_near_black || is_near_white) {\n color = u.colorConstant;\n let is_const_near_black = all(color <= vec3<f32>(eps));\n if (is_const_near_black) {\n color = sampleSequence(u.colorSequence, end_age01);\n }\n }\n var opacity = u.opacityConstant;\n if (opacity == 0.0) {\n opacity = sampleSequence(u.opacitySequence, end_age01).r;\n }\n out.tint_color = vec4<f32>(color, opacity);\n return out;\n}\n\nfn flip_uv(v: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(v.x, 1.0 - v.y);\n}\n\n// Fragment path: byte-for-byte the billboard particle_fragment minus the hologram icon\n// branch (ribbons have no iconMode) - hard occlusion, soft fade, fog, additive premultiply.\nfn ribbon_fragment(in: VSOutput) -> vec4<f32> {\n let particleTargetSize = max(rt.particle_and_depth_size.xy, vec2<f32>(1.0, 1.0));\n let depthTextureSize = max(rt.particle_and_depth_size.zw, vec2<f32>(1.0, 1.0));\n let screenUv = clamp(\n in.clipPosition.xy / particleTargetSize,\n vec2<f32>(0.0, 0.0),\n vec2<f32>(0.999999, 0.999999),\n );\n\n let depthPxF = floor(screenUv * depthTextureSize);\n let depthMaxPx = vec2<i32>(i32(depthTextureSize.x) - 1, i32(depthTextureSize.y) - 1);\n let depthPx = clamp(\n vec2<i32>(i32(depthPxF.x), i32(depthPxF.y)),\n vec2<i32>(0, 0),\n depthMaxPx,\n );\n let sceneDepthRaw = textureLoad(depthTexture, depthPx, 0).r;\n let sceneDepth =\n (camera.near * camera.far) / (camera.far - sceneDepthRaw * (camera.far - camera.near));\n let particleDepth = in.clipPosition.z / in.clipPosition.w;\n\n let hardOcclusionEpsilon = 0.0001;\n if (sceneDepth != 0.0 && particleDepth > sceneDepth + hardOcclusionEpsilon) {\n discard;\n }\n\n let colorCurrent = textureSample(flipbookTexture, flipbookSampler, in.uv_current);\n let colorNext = textureSample(flipbookTexture, flipbookSampler, in.uv_next);\n var color = mix(colorCurrent, colorNext, in.blendFactor) * in.tint_color;\n\n var alphaFade: f32 = 1.0;\n if (u.softParticleFadeDisabled == 0) {\n let fadeDistance = select(0.05, u.softParticleFadeDistance, u.softParticleFadeDistance > 0.0);\n let diff = sceneDepth - particleDepth;\n if (sceneDepth == 0.0) {\n alphaFade = 1.0;\n } else {\n alphaFade = clamp(diff / fadeDistance, 0.0, 1.0);\n }\n }\n color.a *= alphaFade;\n\n if (u.additiveBlend == 1) {\n color = vec4<f32>(color.rgb * color.a, color.a);\n }\n\n var ndc = vec4<f32>(flip_uv(screenUv) * 2.0 - 1.0, in.clipPosition.z, 1.0);\n let view_h = camera.inverse_proj * ndc;\n let view_pos = view_h.xyz / max(view_h.w, 1e-6);\n let dist_m = length(view_pos);\n let world_h = camera.inverse_view_proj * ndc;\n let world_pos = world_h.xyz / max(world_h.w, 1e-6);\n\n var f: f32;\n switch (fog.mode) {\n default { f = clamp((dist_m - fog.start) / max(fog.end_ - fog.start, 1e-6), 0.0, 1.0); }\n case 1u { f = 1.0 - exp(-fog.density * dist_m); }\n case 2u { f = 1.0 - exp(-fog.density * dist_m * fog.density * dist_m); }\n }\n f = clamp(f, 0.0, 1.0);\n if (fog.height_enabled != 0u) {\n let lo = min(fog.height_bottom, fog.height_top);\n let hi = max(fog.height_bottom, fog.height_top);\n let fade = max(hi - lo, 1e-6) * clamp(fog.height_softness, 0.02, 1.0);\n let h = 1.0 - smoothstep(hi - fade, hi, world_pos.y);\n let w = clamp(fog.height_weight, 0.0, 1.0);\n f = f * mix(1.0, h, w);\n }\n\n var rgb = color.rgb;\n if (u.additiveBlend == 1) {\n rgb *= (1.0 - f);\n } else {\n rgb = mix(rgb, vec3<f32>(fog.color), f);\n }\n color = vec4<f32>(rgb, color.a);\n\n if (color.a < 0.01 && length(color.rgb) < 0.0001) {\n discard;\n }\n return color;\n}\n\n// Half-res path: accumulation buffer only.\n@fragment\nfn fs_main(in: VSOutput) -> @location(0) vec4<f32> {\n return ribbon_fragment(in);\n}\n\nstruct FSOutReactive {\n @location(0) color: vec4<f32>,\n @location(1) reactive: f32,\n};\n\n// Full-res path: scene target + temporal reactivity mask (max-blended R8).\n@fragment\nfn fs_main_reactive(in: VSOutput) -> FSOutReactive {\n var out: FSOutReactive;\n out.color = ribbon_fragment(in);\n let luma = dot(out.color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));\n out.reactive = min(max(out.color.a, luma * 0.7), 0.9);\n return out;\n}\n\n// Note: default to 1.0 if no sequence is found (identical to the billboard shader)\nfn sampleSequence(seq: array<vec4<f32>, 8>, t: f32) -> vec3<f32> {\n var prevKey = 0.0;\n var prevVal = vec3<f32>(0.0);\n var foundFirstKey = false;\n var outVal = vec3<f32>(0.0);\n\n for (var i = 0; i < 8; i = i + 1) {\n let pair = seq[i];\n let key = pair.x;\n let val = pair.yzw;\n\n if ((key == 0.0) && (i > 0)) {\n outVal = prevVal; // If key == 0 and it's not the first pair, we assume no more data\n if (prevKey == 0.0) { // double key 0,0 is invalid\n outVal = vec3<f32>(1.0); // default to 1.0 if no sequence is found\n }\n break;\n }\n\n if (!foundFirstKey) {\n foundFirstKey = true;\n prevKey = key;\n prevVal = val;\n if (t <= key) {\n outVal = val; // t is below the first key, clamp to the first value\n break;\n }\n } else {\n if (t < key) {\n // t is in [prevKey..key], we interpolate\n let alpha = (t - prevKey) / max(key - prevKey, 1e-6);\n outVal = mix(prevVal, val, alpha);\n break;\n } else if (t == key) {\n outVal = val; // Exactly on a key\n break;\n } else {\n // haven't found the interval yet, keep moving\n prevKey = key;\n prevVal = val;\n }\n }\n }\n return outVal;\n}\n"},{"label":"Custom Shader Module","code":"// region_transparent.wgsl\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Instance transform rows\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n\n // Instance color (including alpha)\n @location(8) mesh_color: vec4<f32>,\n};\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec4<f32>,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n // Build a mat4 from the instance rows:\n let model_matrix = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n\n let world_pos = model_matrix * vec4<f32>(input.position, 1.0);\n output.clip_position = u_camera.view_proj * world_pos;\n output.color = input.mesh_color; // Pass the instance color (including alpha)\n return output;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n // Just output the tinted color with alpha from 'mesh_color.a'\n return input.color;\n}\n"},{"label":"Custom Shader Module","code":"// region_transparent.wgsl\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Instance transform rows\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n\n // Instance color (including alpha)\n @location(8) mesh_color: vec4<f32>,\n};\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec4<f32>,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n // Build a mat4 from the instance rows:\n let model_matrix = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n\n let world_pos = model_matrix * vec4<f32>(input.position, 1.0);\n output.clip_position = u_camera.view_proj * world_pos;\n output.color = input.mesh_color; // Pass the instance color (including alpha)\n return output;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n // Just output the tinted color with alpha from 'mesh_color.a'\n return input.color;\n}\n"},{"label":"Custom Shader Module","code":"// selection_ring.wgsl - per-player ground ring (SDF annulus on a unit quad)\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // Instance transform rows\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n\n // Instance color (including alpha)\n @location(8) mesh_color: vec4<f32>,\n};\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec4<f32>,\n @location(1) uv: vec2<f32>,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n let model_matrix = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3\n );\n let world_pos = model_matrix * vec4<f32>(input.position, 1.0);\n output.clip_position = u_camera.view_proj * world_pos;\n output.color = input.mesh_color;\n output.uv = input.uv;\n return output;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n // d: 0 at quad center, 1.0 at the inscribed circle (quad edge)\n let d = length(input.uv - vec2<f32>(0.5)) * 2.0;\n // Annulus band [0.78 .. 1.0] with soft inner/outer edges, faint fill inside\n let outer = 1.0 - smoothstep(0.96, 1.0, d);\n let inner = smoothstep(0.78, 0.86, d);\n let band = outer * inner;\n let fill = (1.0 - inner) * 0.12; // subtle disc fill inside the ring\n let a = input.color.a * max(band, fill);\n if a <= 0.001 { discard; }\n return vec4<f32>(input.color.rgb, a); // pipeline blends with SrcAlpha\n\n}\n"},{"label":"shaders/debug_lines.wgsl","code":"struct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> camera: CameraUniform;\n\nstruct VSInput {\n @location(0) position: vec3<f32>,\n @location(1) color: vec3<f32>,\n};\nstruct VSOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec3<f32>,\n};\n\n@vertex\nfn vs_main(input: VSInput) -> VSOutput {\n var out: VSOutput;\n out.clip_position = camera.view_proj * vec4<f32>(input.position, 1.0);\n out.color = input.color;\n return out;\n}\n@fragment\nfn fs_main(input: VSOutput) -> @location(0) vec4<f32> {\n return vec4<f32>(input.color, 1.0);\n}\n"},{"label":"shaders/debug_texture_pass.wgsl","code":"// debug_texture_pass.wgsl\nstruct DebugTextureOffset {\n offset: vec2<f32>,\n rect_size: vec2<f32>,\n mode: u32,\n _pad: u32,\n}\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n}\n@group(0) @binding(0) var texture: texture_2d<f32>;\n@group(0) @binding(1) var<uniform> uOffset: DebugTextureOffset;\n@group(0) @binding(2) var<uniform> u_camera: CameraUniform;\n\nstruct FSOutput {\n @location(0) color: vec4<f32>,\n};\n\nfn linearize_depth_m(depth: f32) -> f32 {\n let pos = u_camera.inverse_proj * vec4<f32>(0.0, 0.0, depth, 1.0);\n return abs(pos.z / pos.w);\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {\n // Fullscreen triangle\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>( 3.0, 1.0),\n vec2<f32>(-1.0, 1.0)\n );\n return vec4<f32>(positions[vertex_index], 0.0, 1.0);\n}\n\n// Occlusion Culling Depth Pyramid Debug\n// @fragment\n// fn fs_main(@builtin(position) pos: vec4<f32>) -> FSOutput {\n// let dims = vec2<f32>(textureDimensions(texture));\n// let uv = pos.xy / dims;\n\n// let divisor = 128;\n// let pixel_coords = vec2<i32>(floor(uv * dims)) / divisor;\n\n// if pixel_coords.x < 0 || pixel_coords.y < 0 || pixel_coords.x >= i32(dims.x) || pixel_coords.y >= i32(dims.y) {\n// return FSOutput(vec4<f32>(0.0, 0.0, 0.05, 1.0));\n// }\n\n// if divisor > 8 && (fract(floor(uv.x * dims.x) / f32(divisor)) < 0.01 || fract(floor(uv.y * dims.y) / f32(divisor)) < 0.01) {\n// return FSOutput(vec4<f32>(1.0, 0.5, 0.5, 1.0)); // Pixel grid\n// }\n\n// var value = textureLoad(texture, pixel_coords, 0).r;\n// if value >= 0.9999 {\n// return FSOutput(vec4<f32>(0.0, 0.05, 0.0, 1.0));\n// }\n// value = 1.0;\n// // value = fract(value * 10.0); // * 10000.0\n// return FSOutput(vec4<f32>(value, value, value, 0.5));\n// // return FSOutput(vec4<f32>(textureLoad(texture, clamped, 0).rgb, 1.0));\n// }\n\n@fragment\nfn fs_main(@builtin(position) pos: vec4<f32>) -> FSOutput {\n let dims = vec2<f32>(textureDimensions(texture));\n var uv = (pos.xy - uOffset.offset) / uOffset.rect_size;\n uv = clamp(uv, vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0));\n\n // Compute integer pixel coordinate; vary by mode\n var pixel_coords: vec2<i32> = vec2<i32>(floor(uv * dims));\n if (uOffset.mode == 1u) {\n // Magnify\n pixel_coords *= 4;\n } else if (uOffset.mode == 2u) {\n // Downsample (noise view)\n pixel_coords /= 4;\n } else if (uOffset.mode == 3u) {\n // Depth pyramid coarse grid\n pixel_coords /= 128;\n }\n\n // Clamp coords to valid [0 .. dims-1]\n let max_i = vec2<i32>(max(vec2<f32>(dims - vec2<f32>(1.0, 1.0)), vec2<f32>(0.0, 0.0)));\n pixel_coords = clamp(pixel_coords, vec2<i32>(0, 0), max_i);\n\n // Mode-specific formatting\n if (uOffset.mode == 7u) {\n // Presenter sanity check: visible without sampling the source texture.\n return FSOutput(vec4<f32>(uv.x, uv.y, 1.0, 1.0));\n } else if (uOffset.mode == 2u) {\n // Noise (plain color)\n return FSOutput(vec4<f32>(textureLoad(texture, pixel_coords, 0).rgb, 1.0));\n } else if (uOffset.mode == 3u) {\n // Depth/Occlusion\n var value = textureLoad(texture, pixel_coords, 0).r;\n if (value >= 0.9999) {\n return FSOutput(vec4<f32>(0.0, 0.05, 0.0, 1.0));\n }\n value = fract(value * 100.0);\n return FSOutput(vec4<f32>(value, value, value, 0.5));\n } else if (uOffset.mode == 5u) {\n // Linearized 1-meter depth bands for human inspection.\n let value = textureLoad(texture, pixel_coords, 0).r;\n if (value >= 0.9999) {\n return FSOutput(vec4<f32>(0.0, 0.05, 0.0, 1.0));\n }\n let meters = linearize_depth_m(value);\n let meter_frac = fract(meters);\n let edge = select(0.0, 1.0, meter_frac < 0.03 || meter_frac > 0.97);\n var base = vec3<f32>(0.0);\n if (meters < 1.0) {\n base = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(1.0, 0.92, 0.35), meter_frac);\n } else if (meters < 2.0) {\n base = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.62, 1.0, 0.55), meter_frac);\n } else {\n let tone = 0.18 + 0.72 * meter_frac;\n base = vec3<f32>(tone * 0.95, tone * 0.93 + 0.03, tone);\n }\n let color = base + vec3<f32>(0.06, 0.06, 0.02) * edge;\n return FSOutput(vec4<f32>(clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0));\n } else if (uOffset.mode == 6u) {\n // Raw linear depth in meters; show repeated 10m bands so non-empty depth is obvious.\n let meters = textureLoad(texture, pixel_coords, 0).r;\n if (meters <= 0.0) {\n return FSOutput(vec4<f32>(0.0, 0.0, 0.03, 1.0));\n }\n let band = fract(meters * 0.1);\n return FSOutput(vec4<f32>(band, band, band, 1.0));\n } else if (uOffset.mode == 4u) {\n // Tonemapped HDR visualization\n let hdr = max(textureLoad(texture, pixel_coords, 0).rgb, vec3<f32>(0.0));\n let ldr = hdr / (1.0 + hdr);\n return FSOutput(vec4<f32>(ldr, 1.0));\n } else if (uOffset.mode == 8u) {\n // Motion vectors (UV units): amplified, 0.5-biased. Gray = static, red/green = motion.\n let mv = textureLoad(texture, pixel_coords, 0).rg;\n return FSOutput(vec4<f32>(clamp(mv * 20.0 + vec2<f32>(0.5), vec2<f32>(0.0), vec2<f32>(1.0)), 0.25, 1.0));\n } else {\n // Generic color view\n return FSOutput(vec4<f32>(textureLoad(texture, pixel_coords, 0).rgb, 1.0));\n }\n}"},{"label":"Gizmo Shader \u2011 Visible","code":"const OCCLUDED: bool = false;\n // gizmo.wgsl\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct VSInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>, \n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // instance transform\n @location(4) model0: vec4<f32>,\n @location(5) model1: vec4<f32>,\n @location(6) model2: vec4<f32>,\n @location(7) model3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>\n};\nstruct VSOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(input: VSInput) -> VSOutput {\n let model = mat4x4<f32>(\n input.model0,\n input.model1,\n input.model2,\n input.model3\n );\n let world_pos = model * vec4<f32>(input.position, 1.0);\n let clip_pos = u_camera.view_proj * world_pos;\n\n return VSOutput(clip_pos, input.mesh_color);\n}\n\n@fragment\nfn fs_main(input: VSOutput) -> @location(0) vec4<f32> {\n var c = input.color;\n if OCCLUDED {\n c.a = c.a * 0.2; // dim the whole gizmo when it is behind opaque geometry\n }\n return c;\n}"},{"label":"Gizmo Shader \u2011 Occluded","code":"const OCCLUDED: bool = true;\n // gizmo.wgsl\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct VSInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>, \n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n // instance transform\n @location(4) model0: vec4<f32>,\n @location(5) model1: vec4<f32>,\n @location(6) model2: vec4<f32>,\n @location(7) model3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>\n};\nstruct VSOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(input: VSInput) -> VSOutput {\n let model = mat4x4<f32>(\n input.model0,\n input.model1,\n input.model2,\n input.model3\n );\n let world_pos = model * vec4<f32>(input.position, 1.0);\n let clip_pos = u_camera.view_proj * world_pos;\n\n return VSOutput(clip_pos, input.mesh_color);\n}\n\n@fragment\nfn fs_main(input: VSOutput) -> @location(0) vec4<f32> {\n var c = input.color;\n if OCCLUDED {\n c.a = c.a * 0.2; // dim the whole gizmo when it is behind opaque geometry\n }\n return c;\n}"},{"label":"shaders/msaa_blit.wgsl","code":"// msaa_blit.wgsl\n@group(0) @binding(0) var t_color : texture_2d<f32>;\n@group(0) @binding(1) var s_color : sampler;\n\n@vertex fn vs(@builtin(vertex_index) v : u32) -> @builtin(position) vec4<f32> {\n var pos = array<vec2<f32>, 3>(vec2(-1, -1), vec2(3, -1), vec2(-1, 3));\n return vec4(pos[v], 0, 1);\n}\n@fragment fn fs(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {\n let uv = p.xy / vec2<f32>(textureDimensions(t_color));\n return textureSample(t_color, s_color, uv);\n}\n"},{"label":"../shaders/ui_text.wgsl","code":"// ui_text.wgsl\nstruct UiSharedUniforms {\n world_to_proj : mat4x4<f32>,\n};\nstruct UiTextPropertiesUniforms {\n text_color : vec4<f32>,\n dst_is_srgb: u32,\n is_sdf: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n@group(0) @binding(0) var<uniform> u_ui : UiSharedUniforms;\n@group(0) @binding(1) var glyphSampler : sampler;\n@group(0) @binding(2) var glyphTexture : texture_2d<f32>;\n@group(1) @binding(0) var<uniform> u_text : UiTextPropertiesUniforms;\n\nstruct VSInput {\n @location(0) position : vec2<f32>,\n @location(1) uv : vec2<f32>,\n};\nstruct VSOutput {\n @builtin(position) pos : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\nfn srgb_to_linear_01(x: f32) -> f32 {\n // Exact sRGB EOTF (IEC 61966-2-1) for values in [0,1].\n if (x <= 0.04045) { return x / 12.92; }\n return pow((x + 0.055) / 1.055, 2.4);\n}\nfn srgb_to_linear_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n srgb_to_linear_01(c.r),\n srgb_to_linear_01(c.g),\n srgb_to_linear_01(c.b),\n );\n}\nfn linear_to_srgb_01(x: f32) -> f32 {\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn linear_to_srgb_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n linear_to_srgb_01(c.r),\n linear_to_srgb_01(c.g),\n linear_to_srgb_01(c.b),\n );\n}\n\n@vertex\nfn vs_main(input: VSInput) -> VSOutput {\n var out : VSOutput;\n out.pos = u_ui.world_to_proj * vec4<f32>(input.position, 0.0, 1.0);\n out.uv = input.uv;\n return out;\n}\n\n@fragment\nfn fs_main(in: VSOutput) -> @location(0) vec4<f32> {\n // sample R channel for glyph shape (coverage, or normalized signed\n // distance for SDF atlases: reconstruct a crisp edge at any draw scale)\n var glyph_alpha = textureSample(glyphTexture, glyphSampler, in.uv).r;\n if (u_text.is_sdf == 1u) {\n let w = max(fwidth(glyph_alpha), 1e-5);\n glyph_alpha = smoothstep(0.5 - w, 0.5 + w, glyph_alpha);\n }\n \n // UI colors are authored/picked in sRGB; convert to linear for correct output to an sRGB swapchain.\n let rgb_lin = srgb_to_linear_rgb(clamp(u_text.text_color.rgb, vec3<f32>(0.0), vec3<f32>(1.0)));\n var out = vec4<f32>(rgb_lin, clamp(u_text.text_color.a, 0.0, 1.0) * glyph_alpha);\n if (u_text.dst_is_srgb == 0u) {\n out = vec4<f32>(linear_to_srgb_rgb(clamp(out.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), out.a);\n }\n return out;\n}"},{"label":"../shaders/ui_frame.wgsl","code":"// ui_frame.wgsl\nstruct UiSharedUniforms {\n world_to_proj : mat4x4<f32>,\n};\n\nstruct UiFramePropertiesUniforms {\n color : vec4<f32>, // fill stop A (solid when gradient kind = 0)\n color2 : vec4<f32>, // fill stop B\n border_color : vec4<f32>, // border stop A\n border_color2 : vec4<f32>, // border stop B\n rect_size_px : vec2<f32>,\n // Value-bar mode (hover HP/SP bars): bar_fraction < 0 = plain frame; otherwise the fill\n // covers uv.x < bar_fraction, a damage ghost trails to bar_ghost_fraction, and\n // bar_background_color covers the rest. Layout mirrors UiFramePropertiesUniforms in Rust.\n bar_fraction : f32,\n bar_ghost_fraction : f32,\n corner_radii : vec4<f32>, // TL, TR, BR, BL\n border_weight : vec4<f32>, // T, R, B, L\n // fill_kind, fill_angle_rad, border_kind, border_angle_rad (kind: 0 solid, 1 linear, 2 radial)\n gradient_params: vec4<f32>,\n bar_background_color : vec4<f32>,\n dst_is_srgb : u32,\n shimmer : u32, // loading skeleton: sweeping highlight band\n spinner : u32, // rotating-arc spinner (ring in fill color, not a rect)\n _pad2: u32,\n};\n\nstruct UiTimeUniforms {\n time_seconds: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n\n@group(0) @binding(0) var<uniform> u_ui : UiSharedUniforms;\n@group(0) @binding(1) var<uniform> u_frame : UiFramePropertiesUniforms;\n@group(0) @binding(2) var<uniform> u_time : UiTimeUniforms;\n\nstruct VSInput {\n @location(0) position : vec2<f32>,\n @location(1) uv : vec2<f32>,\n};\n\nstruct VSOutput {\n @builtin(position) pos : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\nfn srgb_to_linear_01(x: f32) -> f32 {\n if (x <= 0.04045) { return x / 12.92; }\n return pow((x + 0.055) / 1.055, 2.4);\n}\nfn srgb_to_linear_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(srgb_to_linear_01(c.r), srgb_to_linear_01(c.g), srgb_to_linear_01(c.b));\n}\nfn linear_to_srgb_01(x: f32) -> f32 {\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn linear_to_srgb_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(linear_to_srgb_01(c.r), linear_to_srgb_01(c.g), linear_to_srgb_01(c.b));\n}\n\n// Rounded-rect SDF. radii ordered TL, TR, BR, BL. Radius 0 gives a sharp box SDF.\nfn rr_sdf(p: vec2<f32>, half_ext: vec2<f32>, radii: vec4<f32>) -> f32 {\n let r_top = select(radii.x, radii.y, p.x > 0.0);\n let r_bot = select(radii.w, radii.z, p.x > 0.0);\n let r = max(select(r_top, r_bot, p.y > 0.0), 0.0);\n let q = abs(p) - half_ext + vec2<f32>(r);\n return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r;\n}\n// Per-side border weight picker. `w` is (top, right, bottom, left).\nfn side_border(p: vec2<f32>, w: vec4<f32>) -> f32 {\n let bx = select(w.w, w.y, p.x > 0.0); // left vs right\n let by = select(w.x, w.z, p.y > 0.0); // top vs bottom\n return max(bx, by);\n}\n\n// Two-stop gradient factor at pixel p (px, rect-centered; +y down). kind 1 = linear, 2 = radial.\n// Linear: angle 0 rad = top->bottom, spanning the rect's full projection along the gradient\n// direction (CSS-like). Radial: center = 0, corners = 1.\nfn gradient_t(p: vec2<f32>, he: vec2<f32>, kind: f32, angle: f32) -> f32 {\n if (kind > 1.5) {\n return clamp(length(p) / max(length(he), 1e-4), 0.0, 1.0);\n }\n let dir = vec2<f32>(sin(angle), cos(angle));\n let span = abs(dir.x) * he.x + abs(dir.y) * he.y;\n return clamp(0.5 + 0.5 * dot(p, dir) / max(span, 1e-4), 0.0, 1.0);\n}\n\n// Fill / border color at this pixel, gradient-mixed in sRGB space (stops are sRGB).\nfn fill_color_at(p: vec2<f32>, he: vec2<f32>) -> vec4<f32> {\n let kind = u_frame.gradient_params.x;\n if (kind < 0.5) { return u_frame.color; }\n let t = gradient_t(p, he, kind, u_frame.gradient_params.y);\n return mix(u_frame.color, u_frame.color2, t);\n}\nfn border_color_at(p: vec2<f32>, he: vec2<f32>) -> vec4<f32> {\n let kind = u_frame.gradient_params.z;\n if (kind < 0.5) { return u_frame.border_color; }\n let t = gradient_t(p, he, kind, u_frame.gradient_params.w);\n return mix(u_frame.border_color, u_frame.border_color2, t);\n}\n\n@vertex\nfn vs_main(input: VSInput) -> VSOutput {\n var out : VSOutput;\n out.pos = u_ui.world_to_proj * vec4<f32>(input.position, 0.0, 1.0);\n out.uv = input.uv;\n return out;\n}\n\n// Composite border over fill under straight-alpha blending (BlendState::ALPHA_BLENDING).\n// `fill_cov` is the outer rounded-rect coverage (0..1); `border_mask` is the border region\n// within that coverage (0..1). Returns straight RGBA.\nfn compose_frame(fill_rgb: vec3<f32>, fill_alpha: f32,\n border_rgb: vec3<f32>, border_alpha: f32,\n fill_cov: f32, border_mask: f32) -> vec4<f32> {\n let fa = fill_alpha;\n let ba = border_alpha * border_mask;\n let one_m_ba = 1.0 - ba;\n let inner_a = ba + fa * one_m_ba;\n let inner_rgb = select(\n (border_rgb * ba + fill_rgb * fa * one_m_ba) / max(inner_a, 1e-5),\n fill_rgb,\n inner_a < 1e-5\n );\n return vec4<f32>(inner_rgb, inner_a * fill_cov);\n}\n\n@fragment\nfn fs_main(in: VSOutput) -> @location(0) vec4<f32> {\n let he = 0.5 * u_frame.rect_size_px;\n let p = (in.uv - vec2<f32>(0.5)) * u_frame.rect_size_px;\n if (u_frame.spinner != 0u) {\n // Rotating 270-degree ring with round caps, in the fill color. Uniform-condition\n // branch, so the fwidth below is fine (same precedent as the bar branch).\n let r_ring = min(he.x, he.y) * 0.72;\n let th = max(min(he.x, he.y) * 0.24, 1.0);\n let rot = u_time.time_seconds * 5.5;\n let cr = cos(rot);\n let sr = sin(rot);\n let q = vec2<f32>(cr * p.x + sr * p.y, -sr * p.x + cr * p.y);\n let a = atan2(q.y, q.x);\n let arc_half = 2.35619449; // 135 deg: 270-degree arc, 90-degree gap\n var d: f32;\n if (abs(a) <= arc_half) {\n d = abs(length(q) - r_ring);\n } else {\n let cap = vec2<f32>(cos(arc_half), select(-sin(arc_half), sin(arc_half), a > 0.0)) * r_ring;\n d = length(q - cap);\n }\n let sd_arc = d - 0.5 * th;\n let aa_arc = max(fwidth(sd_arc), 1e-4);\n let cov = clamp(0.5 - sd_arc / aa_arc, 0.0, 1.0);\n let col = clamp(u_frame.color, vec4<f32>(0.0), vec4<f32>(1.0));\n var o = vec4<f32>(srgb_to_linear_rgb(col.rgb), col.a * cov);\n if (u_frame.dst_is_srgb == 0u) {\n o = vec4<f32>(linear_to_srgb_rgb(clamp(o.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), o.a);\n }\n return o;\n }\n let sd = rr_sdf(p, he, u_frame.corner_radii);\n let aa = max(fwidth(sd), 1e-4);\n let fill_cov = clamp(0.5 - sd / aa, 0.0, 1.0);\n\n let bw = side_border(p, u_frame.border_weight);\n // border_mask = 1 when sd in [-bw, 0], smoothed by aa. 0 when bw == 0 (no border).\n let inner_cov = clamp(0.5 - (sd + bw) / aa, 0.0, 1.0);\n let border_mask = select(fill_cov - inner_cov, 0.0, bw <= 0.0);\n\n let fill_col = fill_color_at(p, he);\n let border_col = border_color_at(p, he);\n var fill_srgb = clamp(fill_col.rgb, vec3<f32>(0.0), vec3<f32>(1.0));\n var fa = clamp(fill_col.a, 0.0, 1.0);\n if (u_frame.shimmer != 0u) {\n // Loading skeleton: soft highlight band sweeping left->right (slightly\n // diagonal), looping past both edges so the wave visibly enters/exits.\n let center = mix(-0.4, 1.4, fract(u_time.time_seconds * 0.7));\n let d = abs(in.uv.x + (in.uv.y - 0.5) * 0.25 - center);\n // Note: smoothstep with const low >= high is a WGSL shader-creation error.\n fill_srgb += vec3<f32>(0.09) * (1.0 - smoothstep(0.0, 0.35, d));\n }\n if (u_frame.bar_fraction >= 0.0) {\n // Value bar: fill up to bar_fraction, damage ghost to bar_ghost_fraction,\n // background beyond, split along uv.x with AA edges.\n let aa_x = max(fwidth(in.uv.x), 1e-4);\n let fill_mask = 1.0 - smoothstep(u_frame.bar_fraction - aa_x, u_frame.bar_fraction + aa_x, in.uv.x);\n let ghost_edge = max(u_frame.bar_ghost_fraction, u_frame.bar_fraction);\n let ghost_mask = clamp(\n (1.0 - smoothstep(ghost_edge - aa_x, ghost_edge + aa_x, in.uv.x)) - fill_mask,\n 0.0, 1.0);\n let bg_mask = clamp(1.0 - fill_mask - ghost_mask, 0.0, 1.0);\n\n // Subtle vertical gradient so the fill reads as lit from above.\n let fill_shaded = clamp(fill_srgb * (1.15 - 0.3 * in.uv.y), vec3<f32>(0.0), vec3<f32>(1.0));\n // Ghost: lightened fill color, reads as \"damage just taken\".\n let ghost_srgb = mix(fill_shaded, vec3<f32>(1.0), 0.55);\n let bg_srgb = clamp(u_frame.bar_background_color.rgb, vec3<f32>(0.0), vec3<f32>(1.0));\n let bg_a = clamp(u_frame.bar_background_color.a, 0.0, 1.0);\n\n fill_srgb = fill_shaded * fill_mask + ghost_srgb * ghost_mask + bg_srgb * bg_mask;\n fa = fa * (fill_mask + ghost_mask) + bg_a * bg_mask;\n }\n let fill_rgb = srgb_to_linear_rgb(min(fill_srgb, vec3<f32>(1.0)));\n let border_rgb = srgb_to_linear_rgb(clamp(border_col.rgb, vec3<f32>(0.0), vec3<f32>(1.0)));\n let ba = clamp(border_col.a, 0.0, 1.0);\n\n var out = compose_frame(fill_rgb, fa, border_rgb, ba, fill_cov, border_mask);\n if (u_frame.dst_is_srgb == 0u) {\n out = vec4<f32>(linear_to_srgb_rgb(clamp(out.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), out.a);\n }\n return out;\n}\n"},{"label":"../shaders/ui_image.wgsl","code":"struct VsOut {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\nstruct UiSharedUniforms {\n world_to_proj: mat4x4<f32>,\n};\nstruct UiImageUniforms {\n tint: vec4<f32>,\n dst_is_srgb: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n\n@group(0) @binding(0) var<uniform> ui_shared: UiSharedUniforms;\n@group(0) @binding(1) var ui_sampler: sampler;\n@group(0) @binding(2) var ui_tex: texture_2d<f32>;\n@group(1) @binding(0) var<uniform> ui_image: UiImageUniforms;\n\n@vertex\nfn vs_main(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>) -> VsOut {\n var out: VsOut;\n out.position = ui_shared.world_to_proj * vec4<f32>(position, 0.0, 1.0);\n out.uv = uv;\n return out;\n}\n\nfn srgb_to_linear_01(x: f32) -> f32 {\n // Exact sRGB EOTF (IEC 61966-2-1) for values in [0,1].\n if (x <= 0.04045) { return x / 12.92; }\n return pow((x + 0.055) / 1.055, 2.4);\n}\nfn srgb_to_linear_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n srgb_to_linear_01(c.r),\n srgb_to_linear_01(c.g),\n srgb_to_linear_01(c.b),\n );\n}\nfn linear_to_srgb_01(x: f32) -> f32 {\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn linear_to_srgb_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n linear_to_srgb_01(c.r),\n linear_to_srgb_01(c.g),\n linear_to_srgb_01(c.b),\n );\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec4<f32> {\n let color = textureSample(ui_tex, ui_sampler, in.uv);\n // `color.rgb` is already linear because UI textures are bound as *Srgb.\n // `ui_image.tint.rgb` comes from UI-picked colors (sRGB), so convert it before applying.\n let tint_rgb_lin = srgb_to_linear_rgb(clamp(ui_image.tint.rgb, vec3<f32>(0.0), vec3<f32>(1.0)));\n var out = vec4<f32>(color.rgb * tint_rgb_lin, color.a * clamp(ui_image.tint.a, 0.0, 1.0));\n\n // UI authoring (e.g. Figma) often expects display-space-looking alpha ramps.\n // Our UI blends in linear space into an sRGB swapchain, which makes mid alpha look \"too opaque\".\n // Convert alpha from sRGB-ish (perceptual) to linear.\n out.a = srgb_to_linear_01(clamp(out.a, 0.0, 1.0));\n if (ui_image.dst_is_srgb == 0u) {\n out = vec4<f32>(linear_to_srgb_rgb(clamp(out.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), out.a);\n }\n return out;\n}\n\n\n"},{"label":"../shaders/ui_hsv.wgsl","code":"struct UiSharedUniforms {\n world_to_proj: mat4x4<f32>,\n};\n\n// kind: 0 = HueBar, 1 = SvSquare, 2 = SatBar, 3 = ValBar\nstruct UiHsvUniforms {\n kind: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n hue: f32,\n dst_is_srgb: u32,\n sat: f32,\n val: f32,\n};\n\n@group(0) @binding(0)\nvar<uniform> ui: UiSharedUniforms;\n\n@group(0) @binding(1)\nvar<uniform> hsv: UiHsvUniforms;\n\nstruct VsIn {\n @location(0) position: vec2<f32>,\n @location(1) uv: vec2<f32>,\n};\n\nstruct VsOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(input: VsIn) -> VsOut {\n var out: VsOut;\n out.pos = ui.world_to_proj * vec4<f32>(input.position, 0.0, 1.0);\n out.uv = input.uv;\n return out;\n}\n\nfn hsv_to_rgb(h: f32, s: f32, v: f32) -> vec3<f32> {\n let hh = fract(h) * 6.0;\n let c = v * s;\n let x = c * (1.0 - abs(fract(hh * 0.5) * 2.0 - 1.0));\n let m = v - c;\n var rgb = vec3<f32>(0.0, 0.0, 0.0);\n if (hh < 1.0) {\n rgb = vec3<f32>(c, x, 0.0);\n } else if (hh < 2.0) {\n rgb = vec3<f32>(x, c, 0.0);\n } else if (hh < 3.0) {\n rgb = vec3<f32>(0.0, c, x);\n } else if (hh < 4.0) {\n rgb = vec3<f32>(0.0, x, c);\n } else if (hh < 5.0) {\n rgb = vec3<f32>(x, 0.0, c);\n } else {\n rgb = vec3<f32>(c, 0.0, x);\n }\n return rgb + vec3<f32>(m, m, m);\n}\nfn linear_to_srgb_01(x: f32) -> f32 {\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn linear_to_srgb_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n linear_to_srgb_01(c.r),\n linear_to_srgb_01(c.g),\n linear_to_srgb_01(c.b),\n );\n}\n\n@fragment\nfn fs_main(input: VsOut) -> @location(0) vec4<f32> {\n let uv = clamp(input.uv, vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0));\n // Avoid h==1.0 wrapping to 0.0 due to fract() in hsv_to_rgb.\n let HUE_MAX: f32 = 0.99902344; // 1 - 1/1024\n var h: f32 = 0.0;\n var s: f32 = 1.0;\n var v: f32 = 1.0;\n\n if (hsv.kind == 0u) {\n // Hue bar: hue across X\n h = min(uv.x, HUE_MAX);\n s = 1.0;\n v = 1.0;\n } else if (hsv.kind == 2u) {\n // Sat bar: saturation across X at the current hue/value\n h = min(hsv.hue, HUE_MAX);\n s = uv.x;\n v = hsv.val;\n } else if (hsv.kind == 3u) {\n // Value bar: value across X at the current hue/saturation\n h = min(hsv.hue, HUE_MAX);\n s = hsv.sat;\n v = uv.x;\n } else {\n // SV square: saturation across X, value increases upward (so v = 1 - uv.y in screen-down coords)\n h = min(hsv.hue, HUE_MAX);\n s = uv.x;\n v = 1.0 - uv.y;\n }\n\n var out = vec4<f32>(hsv_to_rgb(h, s, v), 1.0);\n if (hsv.dst_is_srgb == 0u) {\n out = vec4<f32>(linear_to_srgb_rgb(clamp(out.rgb, vec3<f32>(0.0), vec3<f32>(1.0))), out.a);\n }\n return out;\n}\n\n\n"},{"label":"../shaders/ui_vec_icon.wgsl","code":"struct UiSharedUniforms {\n world_to_proj: mat4x4<f32>,\n};\n\n// icon ids match UiVecIcon in ui_render_data.rs:\n// 0 = Undo, 1 = ScaleDiag, 2 = MoveVert, 3 = RotateZ, 4 = LockClosed, 5 = LockOpen\nstruct UiVecIconUniforms {\n icon: u32,\n dst_is_srgb: u32,\n // Dark contrast ring width OUTSIDE the glyph, in 48-box units (0 = none).\n // Background-less icons (floating padlock) need it to stay visible on bright worlds.\n outline_px: f32,\n _pad0: f32,\n tint: vec4<f32>, // linear\n};\n\n@group(0) @binding(0)\nvar<uniform> ui: UiSharedUniforms;\n\n@group(0) @binding(1)\nvar<uniform> vi: UiVecIconUniforms;\n\nstruct VsIn {\n @location(0) position: vec2<f32>,\n @location(1) uv: vec2<f32>,\n};\n\nstruct VsOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(input: VsIn) -> VsOut {\n var out: VsOut;\n out.pos = ui.world_to_proj * vec4<f32>(input.position, 0.0, 1.0);\n out.uv = input.uv;\n return out;\n}\n\n// All glyph geometry lives in a 48x48 box (y down), ported 1:1 from\n// temp_plans/mobile_editor_b8_icons.svg. Arcs are round-capped polylines\n// (min over segments == smooth stroke), so no angular-wrap math is needed.\n\nfn sd_seg(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {\n let pa = p - a;\n let ba = b - a;\n let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n return length(pa - ba * h);\n}\n\nfn sd_tri(p: vec2<f32>, p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>) -> f32 {\n let e0 = p1 - p0;\n let e1 = p2 - p1;\n let e2 = p0 - p2;\n let v0 = p - p0;\n let v1 = p - p1;\n let v2 = p - p2;\n let pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);\n let pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);\n let pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);\n let s = sign(e0.x * e2.y - e0.y * e2.x);\n let d = min(\n min(\n vec2<f32>(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)),\n vec2<f32>(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x)),\n ),\n vec2<f32>(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)),\n );\n return -sqrt(d.x) * sign(d.y);\n}\n\nfn sd_rbox(p: vec2<f32>, c: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {\n let q = abs(p - c) - b + vec2<f32>(r, r);\n return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;\n}\n\nfn undo_sdf(p: vec2<f32>) -> f32 {\n // Arc: circle c=(24,24) r=12, screen angles 30deg -> 150deg the long way over the top.\n var pts = array<vec2<f32>, 9>(\n vec2<f32>(34.39, 30.0),\n vec2<f32>(36.0, 24.0),\n vec2<f32>(34.39, 18.0),\n vec2<f32>(30.0, 13.61),\n vec2<f32>(24.0, 12.0),\n vec2<f32>(18.0, 13.61),\n vec2<f32>(13.61, 18.0),\n vec2<f32>(12.0, 24.0),\n vec2<f32>(13.61, 30.0),\n );\n var d = 1e5;\n for (var i = 0u; i < 8u; i = i + 1u) {\n d = min(d, sd_seg(p, pts[i], pts[i + 1u]));\n }\n d = d - 2.5;\n return min(d, sd_tri(p, vec2<f32>(17.9, 37.4), vec2<f32>(7.8, 32.0), vec2<f32>(18.2, 25.9)));\n}\n\nfn scale_diag_sdf(p: vec2<f32>) -> f32 {\n var d = sd_seg(p, vec2<f32>(29.8, 18.2), vec2<f32>(18.2, 29.8)) - 4.0;\n d = min(d, sd_tri(p, vec2<f32>(39.0, 9.0), vec2<f32>(24.2, 12.5), vec2<f32>(35.5, 23.9)));\n return min(d, sd_tri(p, vec2<f32>(9.0, 39.0), vec2<f32>(23.8, 35.5), vec2<f32>(12.5, 24.1)));\n}\n\nfn move_vert_sdf(p: vec2<f32>) -> f32 {\n var d = sd_seg(p, vec2<f32>(24.0, 10.0), vec2<f32>(24.0, 38.0)) - 2.75;\n d = min(d, sd_tri(p, vec2<f32>(24.0, 1.0), vec2<f32>(16.5, 12.0), vec2<f32>(31.5, 12.0)));\n return min(d, sd_tri(p, vec2<f32>(24.0, 47.0), vec2<f32>(16.5, 36.0), vec2<f32>(31.5, 36.0)));\n}\n\nfn rot_z_sdf(p: vec2<f32>) -> f32 {\n // Quadratic (8,19)-(24,33)-(40,19) sampled at t = 0,.2,.4,.6,.8,1 (arc bows DOWN).\n var pts = array<vec2<f32>, 6>(\n vec2<f32>(8.0, 19.0),\n vec2<f32>(14.4, 23.48),\n vec2<f32>(20.8, 25.72),\n vec2<f32>(27.2, 25.72),\n vec2<f32>(33.6, 23.48),\n vec2<f32>(40.0, 19.0),\n );\n var d = 1e5;\n for (var i = 0u; i < 5u; i = i + 1u) {\n d = min(d, sd_seg(p, pts[i], pts[i + 1u]));\n }\n d = d - 2.5;\n d = min(d, sd_tri(p, vec2<f32>(1.25, 13.1), vec2<f32>(4.04, 23.5), vec2<f32>(11.96, 14.5)));\n return min(d, sd_tri(p, vec2<f32>(46.75, 13.1), vec2<f32>(43.96, 23.5), vec2<f32>(36.04, 14.5)));\n}\n\nfn lock_body_sdf(p: vec2<f32>) -> f32 {\n let body = sd_rbox(p, vec2<f32>(24.0, 33.5), vec2<f32>(13.0, 9.5), 4.0);\n let keyhole = length(p - vec2<f32>(24.0, 32.5)) - 2.8;\n return max(body, -keyhole);\n}\n\nfn lock_closed_sdf(p: vec2<f32>) -> f32 {\n var pts = array<vec2<f32>, 7>(\n vec2<f32>(17.0, 24.0),\n vec2<f32>(17.0, 18.0),\n vec2<f32>(19.05, 13.05),\n vec2<f32>(24.0, 11.0),\n vec2<f32>(28.95, 13.05),\n vec2<f32>(31.0, 18.0),\n vec2<f32>(31.0, 24.0),\n );\n var d = 1e5;\n for (var i = 0u; i < 6u; i = i + 1u) {\n d = min(d, sd_seg(p, pts[i], pts[i + 1u]));\n }\n return min(d - 2.3, lock_body_sdf(p));\n}\n\nfn lock_open_sdf(p: vec2<f32>) -> f32 {\n // Closed shackle rotated -28deg around (17,24) (points precomputed).\n var pts = array<vec2<f32>, 7>(\n vec2<f32>(17.0, 24.0),\n vec2<f32>(13.25, 16.94),\n vec2<f32>(12.74, 11.61),\n vec2<f32>(16.15, 7.48),\n vec2<f32>(21.48, 6.97),\n vec2<f32>(25.61, 10.37),\n vec2<f32>(27.02, 13.02),\n );\n var d = 1e5;\n for (var i = 0u; i < 6u; i = i + 1u) {\n d = min(d, sd_seg(p, pts[i], pts[i + 1u]));\n }\n return min(d - 2.3, lock_body_sdf(p));\n}\n\nfn linear_to_srgb_01(x: f32) -> f32 {\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn linear_to_srgb_rgb(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n linear_to_srgb_01(c.r),\n linear_to_srgb_01(c.g),\n linear_to_srgb_01(c.b),\n );\n}\n\n@fragment\nfn fs_main(input: VsOut) -> @location(0) vec4<f32> {\n let p = clamp(input.uv, vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0)) * 48.0;\n\n var d = 1e5;\n switch (vi.icon) {\n case 0u: { d = undo_sdf(p); }\n case 1u: { d = scale_diag_sdf(p); }\n case 2u: { d = move_vert_sdf(p); }\n case 3u: { d = rot_z_sdf(p); }\n case 4u: { d = lock_closed_sdf(p); }\n case 5u: { d = lock_open_sdf(p); }\n default: {}\n }\n\n let w = max(fwidth(d), 0.001);\n let fill = clamp(0.5 - d / w, 0.0, 1.0);\n var out_a = fill * vi.tint.a;\n var out_rgb = vi.tint.rgb * out_a;\n if (vi.outline_px > 0.0) {\n let dilated = clamp(0.5 - (d - vi.outline_px) / w, 0.0, 1.0);\n let ring = max(dilated - fill, 0.0) * 0.85 * vi.tint.a;\n out_a = out_a + ring;\n out_rgb = out_rgb + vec3<f32>(0.013, 0.016, 0.023) * ring;\n }\n\n let a = max(out_a, 0.0001);\n var rgb = out_rgb / a;\n if (vi.dst_is_srgb == 0u) {\n rgb = linear_to_srgb_rgb(clamp(rgb, vec3<f32>(0.0), vec3<f32>(1.0)));\n }\n return vec4<f32>(rgb, out_a);\n}\n"},{"label":"../shaders/ui_image_resample.wgsl","code":"// Offscreen UI image resample: source (full mip chain) -> exact widget-size target.\n// Picks the mip just above the target size, then a 4-tap tent filter for the residual\n// <2x downscale. RGB taps are alpha-weighted (straight-alpha sources bleed background\n// color at transparent edges otherwise; mirrors build_rgba8_mips on the CPU side).\n\nstruct ResampleParams {\n dst_size: vec2<f32>,\n lod: f32,\n _pad: f32,\n};\n\n@group(0) @binding(0) var<uniform> params: ResampleParams;\n@group(0) @binding(1) var src_sampler: sampler;\n@group(0) @binding(2) var src_tex: texture_2d<f32>;\n\nstruct VsOut {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {\n // Fullscreen triangle: (-1,-1) (3,-1) (-1,3).\n var out: VsOut;\n let x = f32(i32(vi & 1u) * 4 - 1);\n let y = f32(i32(vi >> 1u) * 4 - 1);\n out.position = vec4<f32>(x, y, 0.0, 1.0);\n out.uv = vec2<f32>(x, -y) * 0.5 + 0.5;\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec4<f32> {\n // Tap offsets cover the destination pixel footprint (1/dst in uv units).\n let o = 0.25 / params.dst_size;\n let c0 = textureSampleLevel(src_tex, src_sampler, in.uv + vec2<f32>(-o.x, -o.y), params.lod);\n let c1 = textureSampleLevel(src_tex, src_sampler, in.uv + vec2<f32>(o.x, -o.y), params.lod);\n let c2 = textureSampleLevel(src_tex, src_sampler, in.uv + vec2<f32>(-o.x, o.y), params.lod);\n let c3 = textureSampleLevel(src_tex, src_sampler, in.uv + vec2<f32>(o.x, o.y), params.lod);\n let rgb_sum = c0.rgb * c0.a + c1.rgb * c1.a + c2.rgb * c2.a + c3.rgb * c3.a;\n let a_sum = c0.a + c1.a + c2.a + c3.a;\n var rgb = vec3<f32>(0.0);\n if (a_sum > 0.0) {\n rgb = rgb_sum / a_sum;\n }\n return vec4<f32>(rgb, a_sum * 0.25);\n}\n"},{"label":"picking_static","code":"// picking_static.wgsl\n// Minimal ID-buffer picking shader for static (instanced) meshes.\n// Outputs a per-instance `CompactOriObjectId` (u32) into an `R32Uint` render target.\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct PickIds {\n ids: array<u32>,\n};\n@group(1) @binding(0) var<storage, read> pick_ids: PickIds;\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal_oct: vec2<f32>, // packed vertex; unused here\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n @location(4) model_matrix_0: vec4<f32>,\n @location(5) model_matrix_1: vec4<f32>,\n @location(6) model_matrix_2: vec4<f32>,\n @location(7) model_matrix_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) emission_rgbi: vec4<f32>,\n @location(10) flags: u32,\n};\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @interpolate(flat) @location(0) pick_id: u32,\n};\n\n@vertex\nfn vs_main(@builtin(instance_index) instance_id: u32, input: VertexInput) -> VsOut {\n let model = mat4x4<f32>(\n input.model_matrix_0,\n input.model_matrix_1,\n input.model_matrix_2,\n input.model_matrix_3,\n );\n let world_pos = model * vec4<f32>(input.position, 1.0);\n\n var out: VsOut;\n out.clip_pos = u_camera.view_proj * world_pos;\n out.pick_id = pick_ids.ids[instance_id];\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) u32 {\n return in.pick_id;\n}\n\n// Selection-mask variant: keep only fragments whose pick_id is in the selected/hovered set,\n// discard the rest. Used to build an occlusion-free silhouette mask (selected-only, no depth) that\n// the outline pass edge-detects, so the outline is always-visible. `sel_ids` is zero-terminated\n// (real CompactOriObjectId values are never 0), so we break at the first 0.\n@group(2) @binding(0) var<storage, read> sel_ids: array<u32>;\n\n@fragment\nfn fs_filtered(in: VsOut) -> @location(0) u32 {\n let n = arrayLength(&sel_ids);\n var hit = false;\n for (var i = 0u; i < n; i = i + 1u) {\n let s = sel_ids[i];\n if (s == 0u) { break; }\n if (s == in.pick_id) { hit = true; break; }\n }\n if (!hit) { discard; }\n return in.pick_id;\n}\n"},{"label":"picking_skinned","code":"// picking_skinned.wgsl\n// Minimal ID-buffer picking shader for skinned (instanced) meshes.\n// Outputs a per-instance `CompactOriObjectId` (u32) into an `R32Uint` render target.\n\nstruct SkinnedVertexInput {\n // per-vertex\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n @location(4) joints: vec4<u32>,\n @location(5) weights: vec4<f32>,\n\n // per-instance\n @location(6) model_0: vec4<f32>,\n @location(7) model_1: vec4<f32>,\n @location(8) model_2: vec4<f32>,\n @location(9) model_3: vec4<f32>,\n @location(10) mesh_color: vec4<f32>,\n @location(11) emission_rgbi: vec4<f32>,\n @location(12) anim_misc: vec4<u32>, // [flags, clip_id, skin_id, palette_offset]\n @location(13) percent_progress: f32,\n};\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct PickIds {\n ids: array<u32>,\n};\n@group(1) @binding(0) var<storage, read> pick_ids: PickIds;\n\n// Palette buffer: packed joint matrices for all skins/instances.\n@group(2) @binding(0) var<storage, read> u_palettes: array<mat4x4<f32>>;\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @interpolate(flat) @location(0) pick_id: u32,\n};\n\n@vertex\nfn vs_main(@builtin(instance_index) instance_id: u32, in: SkinnedVertexInput) -> VsOut {\n // Normalize weights (defensive)\n let weight_sum = max(in.weights.x + in.weights.y + in.weights.z + in.weights.w, 1e-5);\n let weights = in.weights / weight_sum;\n\n // Skinning (linear blend)\n var skinned_pos = vec4<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n let joint_index = in.joints[i] + in.anim_misc.w;\n let joint_mat = u_palettes[joint_index];\n skinned_pos += joint_mat * vec4<f32>(in.position, 1.0) * weights[i];\n }\n\n // Instance model transform\n let model = mat4x4<f32>(in.model_0, in.model_1, in.model_2, in.model_3);\n let world_pos = model * skinned_pos;\n\n var out: VsOut;\n out.clip_pos = u_camera.view_proj * world_pos;\n out.pick_id = pick_ids.ids[instance_id];\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) u32 {\n return in.pick_id;\n}\n\n// Selection-mask variant: keep only fragments whose pick_id is in the selected/hovered set,\n// discard the rest (occlusion-free silhouette mask for the always-visible outline). `sel_ids` is\n// zero-terminated (real CompactOriObjectId values are never 0). group(3): group(2) is the palette.\n@group(3) @binding(0) var<storage, read> sel_ids: array<u32>;\n\n@fragment\nfn fs_filtered(in: VsOut) -> @location(0) u32 {\n let n = arrayLength(&sel_ids);\n var hit = false;\n for (var i = 0u; i < n; i = i + 1u) {\n let s = sel_ids[i];\n if (s == 0u) { break; }\n if (s == in.pick_id) { hit = true; break; }\n }\n if (!hit) { discard; }\n return in.pick_id;\n}\n"},{"label":"picking_proxy","code":"// picking_proxy.wgsl\n// Proxy picking shader for small helper meshes (regions, effects, etc.).\n// Per-instance attributes provide model matrix + pick_id (u32 CompactOriObjectId).\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct VertexIn {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n\n @location(4) model_0: vec4<f32>,\n @location(5) model_1: vec4<f32>,\n @location(6) model_2: vec4<f32>,\n @location(7) model_3: vec4<f32>,\n\n @location(8) pick_id: u32,\n};\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @interpolate(flat) @location(0) pick_id: u32,\n};\n\n@vertex\nfn vs_main(in: VertexIn) -> VsOut {\n let model = mat4x4<f32>(in.model_0, in.model_1, in.model_2, in.model_3);\n let world_pos = model * vec4<f32>(in.position, 1.0);\n var out: VsOut;\n out.clip_pos = u_camera.view_proj * world_pos;\n out.pick_id = in.pick_id;\n return out;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) u32 {\n return in.pick_id;\n}\n\n// Selection-mask variant: keep only fragments whose pick_id is in the selected/hovered set,\n// discard the rest (occlusion-free silhouette mask for the always-visible outline). `sel_ids` is\n// zero-terminated (real CompactOriObjectId values are never 0). group(1): proxy only uses camera(0).\n@group(1) @binding(0) var<storage, read> sel_ids: array<u32>;\n\n@fragment\nfn fs_filtered(in: VsOut) -> @location(0) u32 {\n let n = arrayLength(&sel_ids);\n var hit = false;\n for (var i = 0u; i < n; i = i + 1u) {\n let s = sel_ids[i];\n if (s == 0u) { break; }\n if (s == in.pick_id) { hit = true; break; }\n }\n if (!hit) { discard; }\n return in.pick_id;\n}\n"},{"label":"pick_pencil_cull","code":"// pick_pencil_cull.wgsl\n// Editor pick pencil cull: compacts one instance group's survivors of a tiny \"pencil\"\n// sub-frustum (a few pixels around the cursor) into a scratch region, so the pick pass\n// draws dozens of instances instead of every instance in the world. Tests the same\n// per-instance world AABBs the occlusion/frustum culls maintain (bounds_buffer). One\n// dispatch per surviving group (slot); cs_fixup clamps the indirect instance counts to\n// the slot capacity and records overflow for CPU-side logging (no silent truncation).\n\nstruct PencilParams {\n planes: array<vec4<f32>, 6>, // pencil sub-frustum planes (world space, inward positive)\n cam_far: vec4<f32>, // xyz camera pos, w pick far clamp (m); beyond it impostors own the pixel\n counts: vec4<u32>, // x instance_count, y slot, z slot capacity, w unused\n};\n\nstruct Inst { // must match render_base::InstanceData (112 bytes)\n m0: vec4<f32>,\n m1: vec4<f32>,\n m2: vec4<f32>,\n m3: vec4<f32>,\n color: vec4<f32>,\n emission: vec4<f32>,\n flags: vec4<u32>,\n};\n\nstruct Aabb { // must match render_base::InstanceAABB (32 bytes)\n min_m: vec4<f32>, // xyz min, w occlusion_depth_margin\n max_m: vec4<f32>, // xyz max, w pad\n};\n\n@group(0) @binding(0) var<uniform> pc: PencilParams; // dynamic offset per slot\n\n@group(1) @binding(0) var<storage, read> src_inst: array<Inst>;\n@group(1) @binding(1) var<storage, read> src_bounds: array<Aabb>;\n@group(1) @binding(2) var<storage, read> src_ids: array<u32>;\n\n@group(2) @binding(0) var<storage, read_write> dst_inst: array<Inst>;\n@group(2) @binding(1) var<storage, read_write> dst_ids: array<u32>;\n@group(2) @binding(2) var<storage, read_write> args: array<u32>; // 5 u32 per slot (DrawIndexedIndirect)\n@group(2) @binding(3) var<storage, read_write> counters: array<atomic<u32>>; // survivors per slot\n@group(2) @binding(4) var<storage, read_write> overflow: array<u32>; // clamped survivors per slot\n\n@compute @workgroup_size(64)\nfn cs_cull(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= pc.counts.x) {\n return;\n }\n let bb_min = src_bounds[i].min_m.xyz;\n let bb_max = src_bounds[i].max_m.xyz;\n // Far clamp: distant instances are impostor pixels; the pick falls through to terrain.\n let center = (bb_min + bb_max) * 0.5;\n let half = (bb_max - bb_min) * 0.5;\n let radius = length(half);\n let to_cam = center - pc.cam_far.xyz;\n if (dot(to_cam, to_cam) > (pc.cam_far.w + radius) * (pc.cam_far.w + radius)) {\n return;\n }\n // AABB vs planes: positive-vertex test (same convention as the frustum cull shaders).\n for (var p = 0u; p < 6u; p++) {\n let pl = pc.planes[p];\n let pos_v = vec3<f32>(\n select(bb_min.x, bb_max.x, pl.x >= 0.0),\n select(bb_min.y, bb_max.y, pl.y >= 0.0),\n select(bb_min.z, bb_max.z, pl.z >= 0.0),\n );\n if (dot(pl.xyz, pos_v) + pl.w < 0.0) {\n return;\n }\n }\n let slot = pc.counts.y;\n let cap = pc.counts.z;\n let idx = atomicAdd(&counters[slot], 1u);\n if (idx < cap) {\n let base = slot * cap;\n dst_inst[base + idx] = src_inst[i];\n dst_ids[base + idx] = src_ids[i];\n }\n}\n\n// One thread per slot: clamp the indirect instance_count (args[slot*5+1]) to capacity and\n// record overflow so the CPU can log it from the pick readback.\n@compute @workgroup_size(64)\nfn cs_fixup(@builtin(global_invocation_id) gid: vec3<u32>) {\n let slot = gid.x;\n let slot_count = arrayLength(&args) / 5u;\n if (slot >= slot_count) {\n return;\n }\n let cap = arrayLength(&dst_ids) / slot_count;\n let n = atomicLoad(&counters[slot]);\n args[slot * 5u + 1u] = min(n, cap);\n overflow[slot] = n - min(n, cap);\n}\n"},{"label":"pick_pencil_skinned_cull","code":"// pick_pencil_skinned_cull.wgsl\n// Skinned twin of pick_pencil_cull.wgsl: compacts one skinned instance group's survivors of\n// the cursor pencil sub-frustum (records + pick ids) so the pick pass draws dozens of\n// stand-in instances instead of the whole crowd. Skinned groups have no per-instance AABB\n// buffer; the cull sphere is derived from the live instance matrix (model x bind_center,\n// radius_factor x max axis scale) - the same calibration the skinned Hi-Z cull uses. That\n// also makes walker-follow GPU-patched matrices \"just work\": this reads the patched buffer.\n\nstruct PencilParams {\n planes: array<vec4<f32>, 6>, // pencil sub-frustum planes (world space, inward positive)\n cam_far: vec4<f32>, // xyz camera pos, w pick far clamp (m)\n counts: vec4<u32>, // x instance_count, y slot, z slot capacity, w unused\n sphere: vec4<f32>, // xyz bind/scene-space cull center, w radius factor\n};\n\nstruct SkinnedInst { // must match render_base::SkinnedInstanceData (144 bytes)\n m0: vec4<f32>,\n m1: vec4<f32>,\n m2: vec4<f32>,\n m3: vec4<f32>,\n color: vec4<f32>,\n emission: vec4<f32>,\n misc0: vec4<u32>, // flags, clip_id, skin_id, palette_offset\n misc1: vec4<u32>, // percent(f32 bits), upper_clip, upper_percent(f32 bits), pad\n aim: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> pc: PencilParams; // dynamic offset per slot\n\n@group(1) @binding(0) var<storage, read> src_inst: array<SkinnedInst>;\n@group(1) @binding(1) var<storage, read> src_ids: array<u32>;\n\n@group(2) @binding(0) var<storage, read_write> dst_inst: array<SkinnedInst>;\n@group(2) @binding(1) var<storage, read_write> dst_ids: array<u32>;\n@group(2) @binding(2) var<storage, read_write> args: array<u32>; // 5 u32 per slot (DrawIndexedIndirect)\n@group(2) @binding(3) var<storage, read_write> counters: array<atomic<u32>>; // survivors per slot\n@group(2) @binding(4) var<storage, read_write> overflow: array<u32>; // clamped survivors per slot\n\n@compute @workgroup_size(64)\nfn cs_cull(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= pc.counts.x) {\n return;\n }\n let inst = src_inst[i];\n let m = mat4x4<f32>(inst.m0, inst.m1, inst.m2, inst.m3);\n let center = (m * vec4<f32>(pc.sphere.xyz, 1.0)).xyz;\n let axis_scale = max(length(inst.m0.xyz), max(length(inst.m1.xyz), length(inst.m2.xyz)));\n let radius = pc.sphere.w * axis_scale;\n // Far clamp: distant characters are impostor pixels; the pick falls through to terrain.\n let to_cam = center - pc.cam_far.xyz;\n if (dot(to_cam, to_cam) > (pc.cam_far.w + radius) * (pc.cam_far.w + radius)) {\n return;\n }\n // Sphere vs planes (inward positive, same convention as the static pencil cull).\n for (var p = 0u; p < 6u; p++) {\n if (dot(pc.planes[p].xyz, center) + pc.planes[p].w < -radius) {\n return;\n }\n }\n let slot = pc.counts.y;\n let cap = pc.counts.z;\n let idx = atomicAdd(&counters[slot], 1u);\n if (idx < cap) {\n let base = slot * cap;\n dst_inst[base + idx] = inst;\n dst_ids[base + idx] = src_ids[i];\n }\n}\n\n// One thread per slot: clamp the indirect instance_count (args[slot*5+1]) to capacity and\n// record overflow so the CPU can log it from the pick readback.\n@compute @workgroup_size(64)\nfn cs_fixup(@builtin(global_invocation_id) gid: vec3<u32>) {\n let slot = gid.x;\n let slot_count = arrayLength(&args) / 5u;\n if (slot >= slot_count) {\n return;\n }\n let cap = arrayLength(&dst_ids) / slot_count;\n let n = atomicLoad(&counters[slot]);\n args[slot * 5u + 1u] = min(n, cap);\n overflow[slot] = n - min(n, cap);\n}\n"},{"label":"shaders/motion_vectors_geom.wgsl","code":"// motion_vectors_geom.wgsl\n// Static instances that moved this frame (small dynamic list, cur+prev matrices), overwriting\n// the camera-term velocity the gbuffer static pipelines wrote. Rasterization uses the JITTERED\n// camera view_proj so fragments land exactly on the main pass's depth samples (depth test\n// LessEqual, no write). Values use UNJITTERED cur/prev matrices, `prev_uv - cur_uv` convention.\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\nstruct MvGeomUniform {\n view_proj: mat4x4<f32>, // current frame, unjittered\n prev_view_proj: mat4x4<f32>, // previous frame, unjittered\n};\n@group(1) @binding(0) var<uniform> u_mv: MvGeomUniform;\n\nstruct VsOut {\n @builtin(position) clip_pos: vec4<f32>,\n @location(0) cur_clip: vec4<f32>,\n @location(1) prev_clip: vec4<f32>,\n};\n\nfn mv_vs_out(world: vec4<f32>, prev_world: vec4<f32>) -> VsOut {\n var out: VsOut;\n out.clip_pos = u_camera.view_proj * world;\n out.cur_clip = u_mv.view_proj * world;\n out.prev_clip = u_mv.prev_view_proj * prev_world;\n return out;\n}\n\n// ---- Static movers: per-instance current + previous model matrices ----\n\nstruct StaticMvInput {\n @location(0) position: vec3<f32>,\n @location(4) cur_0: vec4<f32>,\n @location(5) cur_1: vec4<f32>,\n @location(6) cur_2: vec4<f32>,\n @location(7) cur_3: vec4<f32>,\n @location(8) prev_0: vec4<f32>,\n @location(9) prev_1: vec4<f32>,\n @location(10) prev_2: vec4<f32>,\n @location(11) prev_3: vec4<f32>,\n};\n\n@vertex\nfn vs_static(in: StaticMvInput) -> VsOut {\n let cur = mat4x4<f32>(in.cur_0, in.cur_1, in.cur_2, in.cur_3);\n let prev = mat4x4<f32>(in.prev_0, in.prev_1, in.prev_2, in.prev_3);\n let p = vec4<f32>(in.position, 1.0);\n return mv_vs_out(cur * p, prev * p);\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec2<f32> {\n if (in.prev_clip.w <= 0.0 || in.cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = in.cur_clip.xy / in.cur_clip.w;\n let prev_ndc = in.prev_clip.xy / in.prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n"},{"label":"shaders/downsample_linear_depth.wgsl","code":"// downsample_linear_depth.wgsl\n@group(0) @binding(0) var outDepth : texture_storage_2d<r32float, write>;\n@group(0) @binding(1) var inDepth : texture_2d<f32>;\n@group(0) @binding(2) var inSampler : sampler;\n\n@compute @workgroup_size(8, 8)\nfn main(@builtin(global_invocation_id) gid : vec3<u32>) {\n let halfSize = textureDimensions(outDepth);\n if (gid.x >= halfSize.x || gid.y >= halfSize.y) {\n return;\n }\n let srcSize = textureDimensions(inDepth);\n let baseX = f32(gid.x * 2u);\n let baseY = f32(gid.y * 2u);\n\n // Input is the gbuffer NDC depth (sky = 1.0); consumers (ssao.wgsl) linearize to meters.\n var sumDepth = 0.0;\n for (var j = 0u; j < 2u; j++) {\n for (var i = 0u; i < 2u; i++) {\n let fx = (baseX + f32(i) + 0.5) / f32(srcSize.x);\n let fy = (baseY + f32(j) + 0.5) / f32(srcSize.y);\n sumDepth = sumDepth + textureSampleLevel(inDepth, inSampler, vec2<f32>(fx, fy), 0.0).r;\n }\n }\n\n let outVal = sumDepth * 0.25;\n textureStore(outDepth, vec2<i32>(gid.xy), vec4<f32>(outVal, 0.0, 0.0, 0.0));\n}\n"},{"label":"shaders/ssao.wgsl","code":"// ssao.wgsl\n@group(0) @binding(0) var normalTex: texture_2d<f32>;\n@group(0) @binding(1) var linearDepthTex: texture_2d<f32>;\n@group(0) @binding(2) var noiseTex: texture_2d<f32>;\n@group(0) @binding(3) var linearSampler: sampler;\n@group(0) @binding(4) var depthSampler: sampler;\n@group(0) @binding(5) var noiseSampler: sampler;\nstruct SsaoSettings {\n intensity01: f32,\n distance_m: f32,\n _pad0: f32,\n _pad1: f32,\n};\n@group(0) @binding(6) var<uniform> u_ssao: SsaoSettings;\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n};\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SAMPLES: u32 = 24; // number of random hemisphere samples\nconst BIAS: f32 = 0.035; // 0.025 // small bias to reduce self-occlusion\n\nconst NOISE_SIZE: f32 = 32.0;\n\nconst HEMISPHERE_SAMPLES: array<vec3<f32>, 32> = array<vec3<f32>, 32>(\n vec3<f32>(0.0, 0.1, 1.0),\n vec3<f32>(0.2, 0.4, 0.9),\n vec3<f32>(-0.3, 0.2, 0.8),\n vec3<f32>(0.4, -0.1, 0.7),\n vec3<f32>(-0.2, -0.5, 0.6),\n vec3<f32>(0.3, 0.5, 0.7),\n vec3<f32>(-0.4, 0.3, 0.8),\n vec3<f32>(0.5, 0.1, 0.6),\n\n vec3<f32>(0.2, 0.3, 0.7),\n vec3<f32>(-0.2, 0.3, 0.9),\n vec3<f32>(-0.3, -0.4, 0.7),\n vec3<f32>(0.25, 0.1, 0.95),\n vec3<f32>(-0.4, 0.0, 0.8),\n vec3<f32>(0.4, -0.3, 0.7),\n vec3<f32>(0.05, 0.6, 0.7),\n vec3<f32>(-0.1, -0.2, 0.7),\n\n vec3<f32>( 0.3, -0.1, 0.95),\n vec3<f32>( 0.1, 0.6, 0.7),\n vec3<f32>( 0.6, 0.2, 0.65),\n vec3<f32>(-0.2, 0.55, 0.75),\n vec3<f32>( 0.2, -0.3, 0.9),\n vec3<f32>(-0.5, 0.3, 0.7),\n vec3<f32>( 0.45, 0.4, 0.65),\n vec3<f32>(-0.3, 0.1, 0.9),\n\n vec3<f32>( 0.2, 0.05, 0.95),\n vec3<f32>(-0.4, -0.3, 0.7),\n vec3<f32>( 0.0, 0.3, 0.8),\n vec3<f32>( 0.55, 0.2, 0.65),\n vec3<f32>(-0.6, 0.1, 0.7),\n vec3<f32>( 0.25, -0.4, 0.75),\n vec3<f32>( 0.1, 0.4, 0.8),\n vec3<f32>(-0.1, 0.2, 0.95),\n);\n\nfn get_view_pos_from_view_z_forward(uv: vec2<f32>, view_z_forward: f32) -> vec3<f32> {\n // camera is at looking down -Z\n let z_view = -view_z_forward;\n let x_ndc = uv.x * 2.0 - 1.0;\n let y_ndc = (1.0 - uv.y) * 2.0 - 1.0; // NDC (0,0) is bottom left, but frag_coord (0,0) is top left\n\n // In a typical perspective_rh matrix:\n // proj[0][0] = 1 / tan(fov_x/2)\n // proj[1][1] = 1 / tan(fov_y/2)\n let x_view = x_ndc / u_camera.proj[0][0] * z_view;\n let y_view = y_ndc / u_camera.proj[1][1] * z_view;\n\n return vec3<f32>(x_view, y_view, z_view);\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // full-screen triangle (3 vertices)\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {\n let screen_dims = vec2<f32>(textureDimensions(normalTex, 0));\n let ssao_size = screen_dims / 2.0; // half resolution\n let uv = fragCoord.xy / ssao_size;\n\n let world_normal = normalize(textureSample(normalTex, linearSampler, uv).xyz);\n // linearDepthTex holds downsampled gbuffer NDC depth; linearize to view meters\n // (sky = 1.0 lands on ~far and is caught by the background check below).\n let ndc_depth = textureSampleLevel(linearDepthTex, depthSampler, uv, 0.0).r;\n let linear_depth = (u_camera.near * u_camera.far)\n / max(u_camera.far - ndc_depth * (u_camera.far - u_camera.near), 1e-6);\n if ndc_depth >= 1.0 || linear_depth >= 9999.0 {\n return vec4<f32>(1.0); // background\n }\n let view_pos = get_view_pos_from_view_z_forward(uv, linear_depth);\n\n var noise_uv_1 = uv * ssao_size / NOISE_SIZE;\n var noise_uv_2 = noise_uv_1 * 0.38;\n let noise_vec_1 = textureSampleLevel(noiseTex, noiseSampler, noise_uv_1, 0.0).xy;\n let noise_vec_2 = textureSampleLevel(noiseTex, noiseSampler, noise_uv_2, 0.0).xy;\n let noise_vec = mix(noise_vec_1, noise_vec_2, 0.5); // 0..1\n\n // Make a TBN (tangent, bitangent, normal) basis \n // so we can rotate hemisphere samples in local space relative to the surface normal:\n let view_normal = -normalize((u_camera.view * vec4<f32>(world_normal, 0.0)).xyz); // TODO: view_normal is reversed?\n let absN = abs(view_normal);\n var up = vec3<f32>(0.0, 1.0, 0.0);\n if absN.x < absN.y && absN.x < absN.z {\n up = vec3<f32>(1.0, 0.0, 0.0);\n } else if absN.z < absN.x && absN.z < absN.y {\n up = vec3<f32>(0.0, 0.0, 1.0);\n }\n let U = normalize(cross(up, view_normal));\n let V = cross(view_normal, U);\n\n // Random angle to rotate around the normal\n let pi = 3.141592653589;\n let random_angle = (noise_vec.x * 2.0 - 1.0) * pi; // [pi..pi]\n let sinA = sin(random_angle);\n let cosA = cos(random_angle);\n let t_prime = cosA * U + sinA * V;\n let b_prime = -sinA * U + cosA * V;\n\n // let distance_frac = (linear_depth - u_camera.near) / (u_camera.far - u_camera.near);\n // let scaled_radius = mix(RADIUS, RADIUS * 4.0, clamp(distance_frac, 0.0, 1.0));\n let scaled_radius = max(u_ssao.distance_m, 0.001);\n\n // AO accumulation\n var occlusion = 0.0;\n // let i = 0u;\n for (var i = 0u; i < SAMPLES; i = i + 1u) {\n var sample_dir = normalize(HEMISPHERE_SAMPLES[i]);\n\n // rotate sample_dir by the TBN basis\n let rotated = sample_dir.x * t_prime + // T\n sample_dir.y * b_prime + // B\n sample_dir.z * view_normal; // N\n\n // let nDotR = dot(view_normal, rotated);\n // if (nDotR < 0.0) {\n // continue;\n // }\n // let angle_bias = BIAS / max(nDotR, 0.1);\n\n let iFrac = f32(i) / f32(SAMPLES);\n let hemi_scale = mix(0.1, 1.0, iFrac * iFrac);\n\n let rand_scale = 0.1 + 0.9 * fract(noise_vec.y + f32(i)*0.317);\n let final_scale = hemi_scale * rand_scale;\n\n // Move the sampling origin outwards by BIAS along the normal\n let sample_origin = view_pos + view_normal * BIAS; \n\n let sample_pos = sample_origin + rotated * (scaled_radius * final_scale);\n\n // project sample_pos -> ndc -> depth\n let sample_pos_h = u_camera.proj * vec4<f32>(sample_pos, 1.0);\n if sample_pos_h.w <= 0.0 {\n continue;\n }\n let sample_ndc_xy = sample_pos_h.xy / sample_pos_h.w;\n\n // convert ndc -> [0..1] uv\n var sample_uv = sample_ndc_xy * 0.5 + 0.5;\n sample_uv.x = 1.0 - sample_uv.x; // checked sample_uv matches uv\n sample_uv = clamp(sample_uv, vec2<f32>(0.0), vec2<f32>(1.0));\n\n // Compare the actual stored depth (NDC -> view meters, same mapping as above)\n let sample_ndc = textureSampleLevel(linearDepthTex, depthSampler, sample_uv, 0.0).r;\n let sample_linear_depth = (u_camera.near * u_camera.far)\n / max(u_camera.far - sample_ndc * (u_camera.far - u_camera.near), 1e-6);\n if sample_ndc >= 1.0 || sample_linear_depth > 9999.0 {\n continue; // background => no occlusion from that sample\n }\n let sample_view_pos = get_view_pos_from_view_z_forward(sample_uv, sample_linear_depth);\n\n // difference\n let depth_diff = sample_view_pos.z - view_pos.z; \n let dist_factor = smoothstep(0.0, scaled_radius, length(view_pos - sample_view_pos)); // smooth fade 0..1 over that radius\n let depth_factor = step(BIAS, depth_diff); // simple test for \u201cin front\u201d\n\n occlusion += (1.0 - dist_factor) * depth_factor;\n\n // let range_check_radius = 0.3;\n // let range_check = smoothstep(0.0, 1.0, range_check_radius / abs(depth_diff));\n\n // occlusion += depth_factor * range_check;\n // occlusion += (1.0 - dist_factor) * depth_factor * range_check;\n }\n\n var ao = 1.0 - occlusion / f32(SAMPLES);\n // Cap max occlusion at 80%; unoccluded surfaces must stay 1.0. (The old\n // clamp(ao, 0.0, 0.8) capped BRIGHTNESS instead, flat-darkening every\n // open surface by 20% before the intensity mix.)\n ao = clamp(ao, 0.2, 1.0);\n ao = mix(1.0, ao, clamp(u_ssao.intensity01, 0.0, 1.0));\n return vec4<f32>(ao, ao, ao, 1.0);\n}\n"},{"label":"shaders/ssao_blur.wgsl","code":"// ssao_blur.wgsl\nstruct SsaoBlurUniform {\n radius: f32,\n sigma: f32,\n is_horizontal: u32,\n _pad: f32,\n};\n@group(0) @binding(0) var<uniform> u_blur: SsaoBlurUniform;\n@group(0) @binding(1) var aoTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_2d<f32>;\n@group(0) @binding(3) var normalTex: texture_2d<f32>;\n@group(0) @binding(4) var linearSampler: sampler;\n@group(0) @binding(5) var depthSampler: sampler;\n\nconst KERNEL_SHIFT: f32 = 0.8; // 0.5\n\nconst KERNEL_SIZE: i32 = 7;\nconst GAUSS_WEIGHTS: array<f32, 7> = array<f32, 7>(0.07, 0.131, 0.191, 0.216, 0.191, 0.131, 0.07);\n// const KERNEL_SIZE: i32 = 5;\n// const GAUSS_WEIGHTS: array<f32, 5> = array<f32, 5>(0.06136, 0.24477, 0.38774, 0.24477, 0.06136);\n// const KERNEL_SIZE: i32 = 9;\n// const GAUSS_WEIGHTS: array<f32, 9> = array<f32, 9>(0.028, 0.066, 0.124, 0.18, 0.204, 0.18, 0.124, 0.066, 0.028);\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // full-screen triangle\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nstruct FragOut {\n @location(0) color: vec4<f32>,\n};\n\n@fragment\nfn fs_main(@builtin(position) position: vec4<f32>) -> FragOut {\n let dims = vec2<f32>(textureDimensions(aoTex));\n let uv = position.xy / dims;\n\n // center pixel's depth & normal\n let center_depth = textureSample(depthTex, depthSampler, uv).r;\n let center_normal = normalize(textureSample(normalTex, linearSampler, uv).xyz);\n\n var sum = 0.0;\n var wsum = 0.0;\n\n for (var i = 0; i < KERNEL_SIZE; i = i + 1) {\n let offset = f32(i - KERNEL_SIZE / 2) * KERNEL_SHIFT;\n let weight = GAUSS_WEIGHTS[i];\n\n var sample_uv = uv;\n if u_blur.is_horizontal == 1u {\n sample_uv += vec2<f32>(offset / dims.x, 0.0);\n } else {\n sample_uv += vec2<f32>(0.0, offset / dims.y);\n }\n\n let ao_val = textureSample(aoTex, linearSampler, sample_uv).r;\n\n // Depth for bilateral weight\n let sample_depth = textureSample(depthTex, depthSampler, sample_uv).r;\n let depth_diff = abs(sample_depth - center_depth);\n\n // Normal for bilateral weight\n // let sample_normal = normalize(textureSample(normalTex, linearSampler, sample_uv).xyz);\n // let normal_diff = max(0.0, 1.0 - dot(center_normal, sample_normal));\n\n // user constants\n let sigma_depth = 0.01 * center_depth;\n // let sigma_normal = 0.1;\n\n // let depth_weight = exp(- (depth_diff*depth_diff) / (2.0 * sigma_depth*sigma_depth)); \n // let normal_weight = exp(- (normal_diff*normal_diff) / (2.0 * sigma_normal*sigma_normal));\n\n let coeff = abs(depth_diff / sigma_depth);\n var depth_weight = exp(-coeff*coeff);\n // (Gaussian: w = exp[-(\u0394z^2)/(sigma^2)]. Using abs(\u0394z)/sigma squared here.)\n let depth_cutoff = center_depth * 0.01;\n if (abs(depth_diff) > depth_cutoff) {\n depth_weight = 0.0;\n }\n\n let total_weight = weight * depth_weight; // * normal_weight;\n\n sum += ao_val * total_weight;\n wsum += total_weight;\n }\n\n var blurred = sum / max(wsum, 1e-5);\n\n if u_blur.is_horizontal == 0u {\n blurred = pow(blurred, 1.5);\n if blurred < 0.25 {\n blurred = 0.15 * blurred / 0.25 + 0.1;\n }\n }\n\n return FragOut(vec4<f32>(blurred, blurred, blurred, 1.0));\n}\n"},{"label":"shaders/ssao_upsample.wgsl","code":"@group(0) @binding(0) var aoTexQuarter: texture_2d<f32>;\n@group(0) @binding(1) var depthTexFull: texture_2d<f32>;\n@group(0) @binding(2) var normalTexFull: texture_2d<f32>;\n@group(0) @binding(3) var linearSampler: sampler;\n@group(0) @binding(4) var depthSampler: sampler;\n\n// For a small 3\u00d73 or 4\u00d74 gather; here\u2019s a simple 3\u00d73 example:\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n // Full-screen triangle\n let x = f32((idx << 1u) & 2u);\n let y = f32((idx & 2u));\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {\n let uv = fragCoord.xy / vec2<f32>(textureDimensions(depthTexFull));\n\n // Center pixel data from full\u2010res buffers:\n let centerDepth = textureSample(depthTexFull, depthSampler, uv).r;\n let centerNormal = normalize(textureSample(normalTexFull, linearSampler, uv).xyz);\n\n var aoSum = 0.0;\n var weightSum = 0.0;\n\n // A small offset kernel around uvQuarter in quarter\u2010res space:\n // For instance, a 3\u00d73 gather in [-1..1].\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let offsetQ = vec2<f32>(f32(dx), f32(dy)) / vec2<f32>(textureDimensions(aoTexQuarter));\n let sampleUV = clamp(uv + offsetQ, vec2<f32>(0.0), vec2<f32>(1.0));\n \n let sampleAO = textureSample(aoTexQuarter, linearSampler, sampleUV).r;\n let sampleDepth = textureSample(depthTexFull, depthSampler, sampleUV).r;\n let sampleNormal = normalize(textureSample(normalTexFull, linearSampler, sampleUV).xyz);\n\n // Compare with center\u2019s depth & normal for \u201cedge awareness\u201d\n let depthDiff = abs(sampleDepth - centerDepth);\n let normalDiff = max(0.0, 1.0 - dot(centerNormal, sampleNormal));\n\n // You can tune these sigmas:\n let sigmaDepth = 0.02; \n let sigmaNormal = 0.1; \n\n let wDepth = exp(- (depthDiff * depthDiff) / (2.0 * sigmaDepth * sigmaDepth));\n let wNormal = exp(- (normalDiff * normalDiff) / (2.0 * sigmaNormal * sigmaNormal));\n let bilateralWeight = wDepth * wNormal;\n\n aoSum += sampleAO * bilateralWeight;\n weightSum += bilateralWeight;\n }\n }\n\n let ao = aoSum / max(weightSum, 1e-5);\n return vec4<f32>(ao, ao, ao, 1.0);\n}\n"},{"label":"shaders/motion_vectors.wgsl","code":"// motion_vectors.wgsl\n// Background (sky/far-plane) camera-motion velocity fill: runs after the gbuffer with depth\n// compare Equal at the clear value, so only pixels no opaque geometry touched get written \u2014\n// everything else keeps the velocity the gbuffer MRT wrote. Output rg = prev_uv - cur_uv (UV\n// units). The reprojection matrix is UNJITTERED on both frames so TAA jitter doesn't leak into\n// velocity, and composed in f64 on the CPU: an f32 inverse+forward round trip through world\n// space drifts with distance from the origin, which the temporal upscalers accumulate into\n// permanent blur (B34). Sky pixels reproject like everything else instead of returning zero:\n// DLSS ghosts and leaves block artifacts when the sky reports no motion during camera rotation\n// (B35), and the clip-space reprojection is exact at the far plane.\n\nstruct MotionVectorUniform {\n reproj: mat4x4<f32>, // current clip -> previous clip\n};\n@group(0) @binding(0) var<uniform> u_mv: MotionVectorUniform;\n@group(0) @binding(1) var depth_texture: texture_depth_2d;\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {\n let positions = array<vec2<f32>, 3>(\n vec2<f32>(-1.0, -3.0),\n vec2<f32>( 3.0, 1.0),\n vec2<f32>(-1.0, 1.0)\n );\n // z = 1.0: the depth-Equal test passes only where depth still holds the clear value.\n return vec4<f32>(positions[vertex_index], 1.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec2<f32> {\n let dims = vec2<f32>(textureDimensions(depth_texture));\n let pixel = vec2<i32>(pos.xy);\n let depth = textureLoad(depth_texture, pixel, 0);\n let uv = pos.xy / dims;\n // UV -> NDC (flip y), reproject straight in clip space: (ndc, depth, 1) is the true clip\n // position up to the unknown w, which the homogeneous divide below cancels.\n let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n let prev_clip = u_mv.reproj * vec4<f32>(ndc, depth, 1.0);\n if (prev_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - uv;\n}\n"},{"label":"shaders/ssr_downsample.wgsl","code":"// ssr_downsample.wgsl\n@group(0) @binding(0) var normal_texture: texture_2d<f32>;\n@group(0) @binding(1) var orm_texture: texture_2d<f32>;\n@group(0) @binding(2) var depth_texture: texture_depth_2d;\n@group(0) @binding(3) var scene_color_texture: texture_2d<f32>;\nstruct DownsampleUniform {\n config: vec4<f32>,\n};\n@group(0) @binding(4) var<uniform> u_downsample: DownsampleUniform;\n\nstruct FsOut {\n @location(0) depth_value: vec4<f32>,\n @location(1) repr_depth_value: vec4<f32>,\n @location(2) normal_rough: vec4<f32>,\n @location(3) repr_scene_color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn clamp_px(px: vec2<i32>, dims: vec2<i32>) -> vec2<i32> {\n return clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n}\n\nfn div_ceil_i32(a: i32, b: i32) -> i32 {\n return (a + b - 1) / b;\n}\n\nfn pick_hiz_depth(best_depth: ptr<function, f32>, max_depth: ptr<function, f32>, best_px: ptr<function, vec2<i32>>, full_dims: vec2<i32>, candidate_px: vec2<i32>) {\n let px = clamp_px(candidate_px, full_dims);\n let depth = textureLoad(depth_texture, px, 0);\n // Max INCLUDES background (1.0): any cell touching sky gets max=1.0, which disables\n // the trace behind-skip there (conservative). A geometry-only max was tried and\n // caused severe banding: near-horizon cells behind-skipped past legitimate targets.\n *max_depth = max(*max_depth, depth);\n if depth < 1.0 {\n if (*best_depth < 0.0) || (*best_depth >= 1.0) || depth < *best_depth {\n *best_depth = depth;\n *best_px = px;\n }\n } else if *best_depth < 0.0 {\n *best_depth = depth;\n *best_px = px;\n }\n}\n\nfn target_dims(full_dims: vec2<i32>, full_res: bool) -> vec2<i32> {\n if full_res {\n return full_dims;\n }\n return vec2<i32>(\n max(1, i32(u_downsample.config.y + 0.5)),\n max(1, i32(u_downsample.config.z + 0.5))\n );\n}\n\nfn pick_hiz_depth_range(\n best_depth: ptr<function, f32>,\n max_depth: ptr<function, f32>,\n best_px: ptr<function, vec2<i32>>,\n full_dims: vec2<i32>,\n dst_dims: vec2<i32>,\n dst_px: vec2<i32>,\n) {\n let src0 = vec2<i32>(\n (dst_px.x * full_dims.x) / dst_dims.x,\n (dst_px.y * full_dims.y) / dst_dims.y\n );\n let src1 = vec2<i32>(\n div_ceil_i32((dst_px.x + 1) * full_dims.x, dst_dims.x),\n div_ceil_i32((dst_px.y + 1) * full_dims.y, dst_dims.y)\n );\n var y = src0.y;\n loop {\n if y >= src1.y {\n break;\n }\n var x = src0.x;\n loop {\n if x >= src1.x {\n break;\n }\n pick_hiz_depth(best_depth, max_depth, best_px, full_dims, vec2<i32>(x, y));\n x += 1;\n }\n y += 1;\n }\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FsOut {\n let target_px = vec2<i32>(frag_coord.xy);\n let full_dims = vec2<i32>(textureDimensions(normal_texture));\n let full_res = u_downsample.config.x > 0.5;\n let dst_dims = target_dims(full_dims, full_res);\n var hiz_depth = -1.0;\n var hiz_max_depth = 0.0;\n var hiz_px = clamp_px(target_px, full_dims);\n pick_hiz_depth_range(&hiz_depth, &hiz_max_depth, &hiz_px, full_dims, dst_dims, target_px);\n if hiz_depth < 0.0 {\n hiz_depth = 1.0;\n }\n\n if hiz_depth >= 1.0 {\n return FsOut(vec4<f32>(1.0, 1.0, 0.0, 0.0), vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 1.0), vec4<f32>(0.0));\n }\n\n let repr_depth = hiz_depth;\n let repr_px = hiz_px;\n let world_normal = normalize(textureLoad(normal_texture, repr_px, 0).xyz);\n let roughness = textureLoad(orm_texture, repr_px, 0).g;\n let repr_scene_color = textureLoad(scene_color_texture, repr_px, 0);\n return FsOut(\n vec4<f32>(hiz_depth, hiz_max_depth, 0.0, 0.0),\n vec4<f32>(repr_depth, 0.0, 0.0, 0.0),\n vec4<f32>(world_normal, roughness),\n repr_scene_color\n );\n}\n"},{"label":"shaders/ssr_hiz.wgsl","code":"// ssr_hiz.wgsl\n// Dual Hi-Z reduction: R = min depth (ignoring background), G = max depth (including background).\n@group(0) @binding(0) var prev_hiz: texture_2d<f32>;\n\nstruct FsOut {\n @location(0) depth_value: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn include_depth(best: ptr<function, f32>, worst: ptr<function, f32>, px: vec2<i32>) {\n let d = textureLoad(prev_hiz, px, 0).rg;\n if d.r < 1.0 {\n *best = min(*best, d.r);\n }\n *worst = max(*worst, d.g);\n}\n\nfn reduce_depth(prev_dims: vec2<i32>, dst_px: vec2<i32>) -> vec2<f32> {\n let base_px = dst_px * 2;\n var best = 2.0;\n var worst = 0.0;\n include_depth(&best, &worst, base_px + vec2<i32>(0, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(0, 1));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 1));\n if (prev_dims.x & 1) != 0 {\n include_depth(&best, &worst, base_px + vec2<i32>(2, 0));\n include_depth(&best, &worst, base_px + vec2<i32>(2, 1));\n }\n if (prev_dims.y & 1) != 0 {\n include_depth(&best, &worst, base_px + vec2<i32>(0, 2));\n include_depth(&best, &worst, base_px + vec2<i32>(1, 2));\n }\n if ((prev_dims.x & 1) != 0 && (prev_dims.y & 1) != 0) {\n include_depth(&best, &worst, base_px + vec2<i32>(2, 2));\n }\n return vec2<f32>(select(1.0, best, best < 1.0), worst);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FsOut {\n let prev_dims = vec2<i32>(textureDimensions(prev_hiz));\n let dst_px = vec2<i32>(frag_coord.xy);\n return FsOut(vec4<f32>(reduce_depth(prev_dims, dst_px), 0.0, 0.0));\n}\n"},{"label":"shaders/ssr_trace.wgsl","code":"// ssr_trace.wgsl\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n\nstruct SsrSettingsUniform {\n trace_config: vec4<f32>,\n material_config: vec4<f32>,\n fade_config: vec4<f32>,\n ray_fade_config: vec4<f32>,\n debug_config: vec4<f32>,\n};\n\nstruct SsrHistoryUniform {\n // Current clip -> previous clip, f64-composed; consumed by ssr_temporal_resolve.wgsl.\n reproj: mat4x4<f32>,\n // World -> previous clip: fine for the hit-point reprojection below, whose position\n // comes from forward matrices (inverse_proj + inverse_view), not a composed inverse.\n prev_view_proj: mat4x4<f32>,\n history_state: vec4<u32>,\n};\n\nstruct TraceHit {\n hit_uv: vec2<f32>,\n hit_color: vec3<f32>,\n hit_confidence: f32,\n hit_mip_level: f32,\n debug_data: vec4<f32>,\n debug_vec_a: vec4<f32>,\n debug_vec_b: vec4<f32>,\n};\n\nstruct TraceFsOut {\n @location(0) color: vec4<f32>,\n @location(1) debug_data: vec4<f32>,\n @location(2) debug_vec_a: vec4<f32>,\n @location(3) debug_vec_b: vec4<f32>,\n};\n\n@group(0) @binding(0) var scene_color: texture_2d<f32>;\n@group(0) @binding(1) var repr_scene_color_texture: texture_2d<f32>;\n@group(0) @binding(2) var normal_rough_texture: texture_2d<f32>;\n@group(0) @binding(3) var repr_depth_texture: texture_2d<f32>;\n@group(0) @binding(4) var hiz_texture: texture_2d<f32>;\n@group(0) @binding(5) var linear_sampler: sampler;\n@group(0) @binding(6) var trace_mip_out: texture_storage_2d<r32float, write>;\n@group(0) @binding(7) var<uniform> u_ssr: SsrSettingsUniform;\n@group(0) @binding(8) var<uniform> u_prev: SsrHistoryUniform;\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SSR_MAX_STEPS_HARD: u32 = 96u;\nconst SSR_DEBUG_PIPELINE_STAGE_COUNT: u32 = 39u;\nconst SSR_DEBUG_MARCH_STAGE_COUNT: u32 = 64u;\nconst SSR_HORIZON_Z_HARD_EPS: f32 = 1e-7;\nconst SSR_PARAM_Z_MIN: f32 = 1e-6;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\nconst SSR_TRACE_AXIS_EPS: f32 = 1e-7;\nconst SSR_TRACE_MAX_T: f32 = 1.0e9;\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn max_steps() -> u32 {\n return clamp(u32(u_ssr.trace_config.x), 1u, SSR_MAX_STEPS_HARD);\n}\n\nfn hiz_mip_count() -> u32 {\n return max(1u, u32(u_ssr.trace_config.y));\n}\n\nfn depth_tolerance_m() -> f32 {\n return u_ssr.trace_config.z;\n}\n\nfn reflection_mip_count() -> f32 {\n return max(1.0, u_ssr.trace_config.w);\n}\n\nfn min_roughness() -> f32 { return u_ssr.material_config.x; }\nfn max_roughness() -> f32 { return u_ssr.material_config.y; }\nfn roughness_blur_multiplier() -> f32 { return max(u_ssr.material_config.z, 0.01); }\nfn roughness_mip_clamp_lower() -> f32 { return max(u_ssr.material_config.w, 0.0); }\nfn roughness_mip_clamp_upper(max_mip: f32) -> f32 {\n let raw = u_ssr.fade_config.w;\n return select(max_mip, clamp(raw, 0.0, max_mip), raw > 0.0);\n}\n// Temporary art-tuned floor: keep rough contact reflections from collapsing to mip0\n// until we replace this with a more principled roughness-limiter style solution.\nfn roughness_contact_min_mip(roughness: f32) -> f32 {\n return smoothstep(0.0, 0.25, roughness);\n}\n\nfn geometric_bias_m() -> f32 {\n return u_ssr.fade_config.z;\n}\n\nfn debug_stage() -> u32 {\n return u32(u_ssr.debug_config.x);\n}\n\nfn debug_stage_capture_enabled() -> bool {\n return u_ssr.debug_config.w > 0.5;\n}\n\nfn sign_nonzero(value: f32) -> f32 {\n if abs(value) <= 1e-5 {\n return 0.0;\n }\n return select(-1.0, 1.0, value > 0.0);\n}\n\nfn is_finite1(v: f32) -> bool { return v == v && abs(v) < SSR_FINITE_LIMIT; }\nfn is_finite3(v: vec3<f32>) -> bool { return all(v == v) && all(abs(v) < vec3<f32>(SSR_FINITE_LIMIT)); }\n\nfn safe_axis_t(numer: f32, denom: f32) -> f32 {\n if abs(denom) <= SSR_TRACE_AXIS_EPS {\n return SSR_TRACE_MAX_T;\n }\n return numer / denom;\n}\n\nfn safe_axis_t2(numer: vec2<f32>, denom: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(safe_axis_t(numer.x, denom.x), safe_axis_t(numer.y, denom.y));\n}\n\nfn store_debug_march_iteration(\n hit: ptr<function, TraceHit>,\n stage: u32,\n iter_index: u32,\n cur_screen_pos: vec3<f32>,\n cell_depth: f32,\n mip: i32,\n screen_ray_dir_z_nonnegative: bool,\n use_t_le_depth_predicate: bool,\n is_hit_before_gap_reject: bool,\n gap_reject: bool,\n t: f32,\n edge_t: f32,\n depth_t: f32,\n linear_gap: f32,\n cur_px: vec2<f32>,\n is_hit: bool,\n branch_class: f32,\n) {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n return;\n }\n let march_stage = stage - SSR_DEBUG_PIPELINE_STAGE_COUNT;\n if march_stage >= SSR_DEBUG_MARCH_STAGE_COUNT || march_stage != iter_index {\n return;\n }\n var flags = 0u;\n if screen_ray_dir_z_nonnegative { flags |= 1u; }\n if use_t_le_depth_predicate { flags |= 2u; }\n if is_hit_before_gap_reject { flags |= 4u; }\n if gap_reject { flags |= 8u; }\n if is_hit { flags |= 16u; }\n let packed_mip_flags = (u32(mip) << 8u) | flags;\n (*hit).debug_data = vec4<f32>(cur_screen_pos.xy, cell_depth, f32(packed_mip_flags));\n (*hit).debug_vec_a = vec4<f32>(t, edge_t, depth_t, linear_gap);\n (*hit).debug_vec_b = vec4<f32>(cur_px, f32(flags), branch_class);\n}\n\nfn store_debug_pipeline_capture(\n hit: ptr<function, TraceHit>,\n stage: u32,\n start_view_pos_raw: vec3<f32>,\n start_depth_linear: f32,\n roughness: f32,\n start_uv: vec2<f32>,\n start_view_normal: vec3<f32>,\n start_geom_normal: vec3<f32>,\n geom_bias_amount: f32,\n launch_view_pos: vec3<f32>,\n launch_ray_dir: vec3<f32>,\n launch_ray_length_m: f32,\n source_clip: vec4<f32>,\n source_ndc: vec3<f32>,\n biased_clip: vec4<f32>,\n biased_ndc: vec3<f32>,\n trace_end_clip: vec4<f32>,\n trace_end_ndc: vec3<f32>,\n screen_pos_z: f32,\n screen_end_z: f32,\n raw_screen_ray_dir: vec3<f32>,\n t: f32,\n t_max: f32,\n segment_t: f32,\n screen_ray_dir_xy: vec2<f32>,\n t2: vec2<f32>,\n hit_scene_color: vec3<f32>,\n hit_surface_normal: vec3<f32>,\n hit_surface_roughness: f32,\n hit_uv_exact: vec2<f32>,\n final_hit_depth: f32,\n ray_hit_pos: vec3<f32>,\n scene_hit_pos: vec3<f32>,\n travel_m: f32,\n projected_ray_hit_pos: vec3<f32>,\n ray_recon_delta_m: f32,\n hit_delta_m: f32,\n) {\n if stage >= SSR_DEBUG_PIPELINE_STAGE_COUNT {\n return;\n }\n if stage == 0u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw, start_depth_linear);\n (*hit).debug_vec_b = vec4<f32>(start_view_normal, roughness);\n } else if stage == 1u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw, start_depth_linear);\n (*hit).debug_vec_b = vec4<f32>(start_uv, 0.0, 0.0);\n } else if stage == 2u {\n (*hit).debug_vec_a = vec4<f32>(start_view_normal, roughness);\n (*hit).debug_vec_b = vec4<f32>(start_geom_normal, dot(start_view_normal, start_geom_normal));\n } else if stage == 3u {\n (*hit).debug_vec_a = vec4<f32>(start_geom_normal, geom_bias_amount);\n (*hit).debug_vec_b = vec4<f32>(start_view_normal, dot(start_view_normal, start_geom_normal));\n } else if stage == 5u {\n (*hit).debug_vec_a = vec4<f32>(start_view_pos_raw.z, source_clip.z, source_clip.w, source_ndc.z);\n (*hit).debug_vec_b = vec4<f32>(launch_view_pos.z, biased_clip.z, biased_clip.w, biased_ndc.z);\n } else if stage == 6u {\n (*hit).debug_vec_a = vec4<f32>(launch_view_pos.z + launch_ray_dir.z, trace_end_clip.z, trace_end_clip.w, trace_end_ndc.z);\n (*hit).debug_vec_b = vec4<f32>(screen_pos_z, screen_end_z, raw_screen_ray_dir.z, abs(raw_screen_ray_dir.z));\n } else if stage == 7u {\n (*hit).debug_vec_a = vec4<f32>(geom_bias_amount, start_depth_linear, roughness, dot(start_view_normal, start_geom_normal));\n (*hit).debug_vec_b = vec4<f32>(launch_view_pos, 1.0);\n } else if stage == 8u {\n (*hit).debug_vec_a = vec4<f32>(launch_ray_dir, launch_ray_length_m);\n (*hit).debug_vec_b = vec4<f32>(screen_pos_z, screen_end_z, raw_screen_ray_dir.z, abs(raw_screen_ray_dir.z));\n } else if stage == 9u {\n (*hit).debug_vec_a = vec4<f32>(t, t_max, clamp(t / max(t_max, 1e-5), 0.0, 1.0), segment_t);\n (*hit).debug_vec_b = vec4<f32>(screen_ray_dir_xy, t2.x, t2.y);\n } else if stage == 26u {\n (*hit).debug_vec_a = vec4<f32>(hit_scene_color, hit_surface_roughness);\n (*hit).debug_vec_b = vec4<f32>(hit_uv_exact, final_hit_depth, 0.0);\n } else if stage == 27u {\n (*hit).debug_vec_a = vec4<f32>(hit_surface_normal, hit_surface_roughness);\n (*hit).debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 29u {\n (*hit).debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n (*hit).debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 30u {\n (*hit).debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n (*hit).debug_vec_b = vec4<f32>(ray_hit_pos, hit_delta_m);\n }\n}\n\nfn linearize_depth(depth: f32) -> f32 {\n let pos = u_camera.inverse_proj * vec4<f32>(0.0, 0.0, depth, 1.0);\n return pos.z / pos.w;\n}\n\nfn empty_hit() -> TraceHit {\n return TraceHit(vec2<f32>(-1.0, -1.0), vec3<f32>(0.0), 0.0, 0.0, vec4<f32>(0.0), vec4<f32>(0.0), vec4<f32>(0.0));\n}\n\nfn trace_fs_out(color: vec4<f32>, hit: TraceHit) -> TraceFsOut {\n return TraceFsOut(color, hit.debug_data, hit.debug_vec_a, hit.debug_vec_b);\n}\n\nfn store_trace_mip(pixel_coords: vec2<i32>, mip_level: f32) {\n textureStore(trace_mip_out, pixel_coords, vec4<f32>(mip_level, 0.0, 0.0, 0.0));\n}\n\nfn compute_reflection_mip_level(roughness: f32, ray_len: f32, screen_dims: vec2<f32>) -> f32 {\n let max_mip = max(reflection_mip_count() - 1.0, 0.0);\n if roughness <= 0.001 || max_mip <= 0.0 {\n return 0.0;\n }\n var mip_level = 0.0;\n let effective_roughness = min(roughness * roughness_blur_multiplier(), 0.999);\n let cone_angle = effective_roughness * 3.14159265 * 0.5;\n let cone_len = max(ray_len, 1e-4);\n let op_len = 2.0 * tan(cone_angle) * cone_len;\n let a = op_len;\n let h = cone_len;\n let blur_radius = (a * (sqrt(a * a + 4.0 * h * h) - a)) / (4.0 * h);\n let blur_px = blur_radius * max(screen_dims.x, screen_dims.y) / 16.0;\n let distance_mip = clamp(log2(max(blur_px, 1.0)), 0.0, max_mip);\n let far_factor = pow(clamp(1.25 - ray_len, 0.0, 1.0), 0.2);\n mip_level = distance_mip * far_factor;\n let mip_upper = roughness_mip_clamp_upper(max_mip);\n let mip_lower = min(roughness_mip_clamp_lower() + roughness_contact_min_mip(roughness), mip_upper);\n return clamp(mip_level, mip_lower, mip_upper);\n}\n\nfn tone_map_ssr_color(color: vec3<f32>) -> vec3<f32> {\n let rec709_luminance_weights = vec3<f32>(0.2126, 0.7152, 0.0722);\n return color / (1.0 + dot(color, rec709_luminance_weights));\n}\n\nfn world_pos_from_depth(uv_top_left: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec4<f32>(uv_top_left.x * 2.0 - 1.0, (1.0 - uv_top_left.y) * 2.0 - 1.0, depth, 1.0);\n let world_h = u_camera.inverse_view_proj * ndc;\n return world_h.xyz / world_h.w;\n}\n\nfn view_pos_from_depth(uv_top_left: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec4<f32>(uv_top_left.x * 2.0 - 1.0, (1.0 - uv_top_left.y) * 2.0 - 1.0, depth, 1.0);\n let view_h = u_camera.inverse_proj * ndc;\n return view_h.xyz / view_h.w;\n}\n\nfn project_view_to_screen(view_pos: vec3<f32>) -> vec3<f32> {\n let clip = u_camera.proj * vec4<f32>(view_pos, 1.0);\n let ndc = clip.xyz / clip.w;\n // XY are viewport UVs; Z stays in projected NDC depth space (unitless, same convention as the depth/Hi-Z textures), not meters.\n return vec3<f32>(ndc.x * 0.5 + 0.5, 1.0 - (ndc.y * 0.5 + 0.5), ndc.z);\n}\n\nfn load_hiz_depth(px: vec2<i32>, mip: u32) -> f32 {\n let dims = vec2<i32>(textureDimensions(hiz_texture, mip));\n let clamped = clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n return textureLoad(hiz_texture, clamped, i32(mip)).r;\n}\n\nfn uv_from_px(px: vec2<i32>, dims: vec2<i32>) -> vec2<f32> {\n return (vec2<f32>(px) + 0.5) / vec2<f32>(dims);\n}\n\n// Godot-style screen-space edge fade. Corner-heavy, axis-light:\n// margin_grad = distance (in pixels) to the nearest screen edge per axis.\n// margin_blend = smoothstep(0, margin.x*margin.y, margin_grad.x*margin_grad.y)\n// with margin = (W + H) * 0.05. Because the product margin_grad.x * margin_grad.y\n// grows fast along each axis, axis-midpoint reflections stay visible until within\n// ~(margin.x*margin.y / (half_screen)) px of the top/bottom edge, while corners\n// fade deep into the image. Reflections projecting off-frame on any axis get a\n// negative grad, which smoothstep clamps to 0 \u2014 a natural hard-cut. Scale\n// invariant: using half-res or full-res textureDimensions gives the same result.\nfn edge_fade(uv: vec2<f32>) -> f32 {\n let screen_size = vec2<f32>(textureDimensions(repr_depth_texture));\n let px = uv * screen_size;\n let margin_scalar = (screen_size.x + screen_size.y) * 0.05;\n let margin_sq = margin_scalar * margin_scalar;\n let grad = min(px, screen_size - px);\n return smoothstep(0.0, margin_sq, grad.x * grad.y);\n}\n\nfn load_repr_depth(px: vec2<i32>) -> f32 {\n let dims = vec2<i32>(textureDimensions(repr_depth_texture));\n let clamped = clamp(px, vec2<i32>(0), dims - vec2<i32>(1, 1));\n return textureLoad(repr_depth_texture, clamped, 0).r;\n}\n\nfn choose_neighbor(depth_c: f32, a_px: vec2<i32>, b_px: vec2<i32>, dims: vec2<i32>) -> vec3<f32> {\n let a_depth = load_repr_depth(a_px);\n let b_depth = load_repr_depth(b_px);\n let choose_a = abs(a_depth - depth_c) <= abs(b_depth - depth_c);\n let chosen_px = select(clamp(b_px, vec2<i32>(0), dims - vec2<i32>(1, 1)), clamp(a_px, vec2<i32>(0), dims - vec2<i32>(1, 1)), choose_a);\n let chosen_depth = select(b_depth, a_depth, choose_a);\n return view_pos_from_depth(uv_from_px(chosen_px, dims), chosen_depth);\n}\n\nfn compute_geometric_normal(pixel_px: vec2<i32>, depth_c: f32, view_c: vec3<f32>, dims: vec2<i32>, view_normal: vec3<f32>) -> vec3<f32> {\n let h_pos = choose_neighbor(depth_c, pixel_px + vec2<i32>(-1, 0), pixel_px + vec2<i32>(1, 0), dims);\n let v_pos = choose_neighbor(depth_c, pixel_px + vec2<i32>(0, -1), pixel_px + vec2<i32>(0, 1), dims);\n let h_der = h_pos - view_c;\n let v_der = v_pos - view_c;\n if length(h_der) <= 1e-5 || length(v_der) <= 1e-5 {\n return view_normal;\n }\n var geom_normal = normalize(cross(v_der, h_der));\n if dot(geom_normal, view_normal) < 0.0 {\n geom_normal = -geom_normal;\n }\n return geom_normal;\n}\n\nfn trace_hiz(\n start_uv: vec2<f32>,\n start_depth: f32,\n start_view_pos: vec3<f32>,\n view_normal: vec3<f32>,\n geom_normal: vec3<f32>,\n roughness: f32,\n) -> TraceHit {\n var hit = empty_hit();\n let stage = debug_stage();\n let half_dims = vec2<i32>(textureDimensions(repr_depth_texture));\n let half_dims_f = vec2<f32>(half_dims);\n // Stage 01-09: source inputs sampled at the current pixel, then the launch terms derived from them.\n let start_depth_linear = abs(start_view_pos.z);\n let start_roughness_mask = 1.0 - smoothstep(min_roughness(), max_roughness(), roughness);\n\n // Add a small bias toward the geometry normal to help prevent immediate self-intersections.\n let bias = geometric_bias_m() * (1.0 - pow(clamp(dot(view_normal, geom_normal), 0.0, 1.0), 8.0));\n var view_pos = start_view_pos + geom_normal * bias;\n var ray_dir = normalize(reflect(normalize(view_pos), view_normal));\n if dot(ray_dir, geom_normal) < 0.0 {\n ray_dir = normalize(reflect(ray_dir, geom_normal));\n }\n if !is_finite3(ray_dir) {\n hit.debug_data = vec4<f32>(19.0, 0.0, 0.0, 0.0);\n return hit;\n }\n\n var screen_pos = project_view_to_screen(view_pos);\n if !is_finite3(screen_pos) {\n hit.debug_data = vec4<f32>(20.0, 0.0, 0.0, 0.0);\n return hit;\n }\n // clip that segment to the near plane.\n var trace_end_pos = view_pos + ray_dir;\n if trace_end_pos.z > -0.0001 {\n if abs(ray_dir.z) <= SSR_TRACE_AXIS_EPS {\n hit.debug_data = vec4<f32>(21.0, trace_end_pos.z, ray_dir.z, 0.0);\n return hit;\n }\n trace_end_pos -= ray_dir / ray_dir.z * (trace_end_pos.z + 0.0001);\n }\n let source_clip = u_camera.proj * vec4<f32>(start_view_pos, 1.0);\n let source_ndc = source_clip.xyz / source_clip.w;\n let biased_clip = u_camera.proj * vec4<f32>(view_pos, 1.0);\n let biased_ndc = biased_clip.xyz / biased_clip.w;\n let trace_end_clip = u_camera.proj * vec4<f32>(trace_end_pos, 1.0);\n let trace_end_ndc = trace_end_clip.xyz / trace_end_clip.w;\n let screen_end = project_view_to_screen(trace_end_pos);\n if !is_finite3(screen_end) {\n hit.debug_data = vec4<f32>(22.0, 0.0, 0.0, 0.0);\n return hit;\n }\n let raw_screen_ray_dir = screen_end - screen_pos;\n let raw_screen_ray_z_abs = abs(raw_screen_ray_dir.z);\n if raw_screen_ray_z_abs <= SSR_HORIZON_Z_HARD_EPS {\n hit.debug_data = vec4<f32>(1.0, ray_dir.z, raw_screen_ray_dir.z, 0.0);\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n 0.0, 0.0, 0.0, vec2<f32>(0.0), vec2<f32>(0.0), vec3<f32>(0.0), vec3<f32>(0.0), 0.0, start_uv, start_depth,\n vec3<f32>(0.0), vec3<f32>(0.0), 0.0, vec3<f32>(0.0), 0.0, 0.0\n );\n return hit;\n }\n let param_z_abs = max(raw_screen_ray_z_abs, SSR_PARAM_Z_MIN);\n let screen_ray_dir = raw_screen_ray_dir / param_z_abs;\n let facing_camera = screen_ray_dir.z <= 0.0;\n let start_px = clamp(vec2<i32>(screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let t0 = safe_axis_t2(vec2<f32>(0.0) - screen_pos.xy, screen_ray_dir.xy);\n let t1 = safe_axis_t2(vec2<f32>(1.0) - screen_pos.xy, screen_ray_dir.xy);\n let t2 = max(t0, t1);\n let t_max = min(t2.x, t2.y);\n if !is_finite1(t_max) || t_max <= 0.0 {\n hit.debug_data = vec4<f32>(2.0, t_max, screen_ray_dir.x, screen_ray_dir.y);\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n 0.0, t_max, 0.0, screen_ray_dir.xy, t2, vec3<f32>(0.0), vec3<f32>(0.0), 0.0, start_uv, start_depth,\n vec3<f32>(0.0), vec3<f32>(0.0), 0.0, vec3<f32>(0.0), 0.0, 0.0\n );\n return hit;\n }\n\n let cell_step = vec2<f32>(\n select(1.0, -1.0, screen_ray_dir.x < 0.0),\n select(1.0, -1.0, screen_ray_dir.y < 0.0),\n );\n let start_cell = floor(screen_pos.xy * half_dims_f);\n let next_cell = start_cell + clamp(cell_step, vec2<f32>(0.0), vec2<f32>(1.0));\n let next_pos = next_cell / half_dims_f + cell_step * 0.000001;\n let start_t = safe_axis_t2(next_pos - screen_pos.xy, screen_ray_dir.xy);\n let initial_t = min(start_t.x, start_t.y);\n if !is_finite1(initial_t) {\n hit.debug_data = vec4<f32>(23.0, initial_t, t_max, 0.0);\n return hit;\n }\n if stage == 0u {\n hit.debug_vec_a = vec4<f32>(start_view_pos, start_depth);\n hit.debug_vec_b = vec4<f32>(ray_dir, initial_t);\n }\n var t = initial_t;\n var cur_level: i32 = 0;\n let max_level = i32(hiz_mip_count()) - 1;\n var steps_left = i32(max_steps());\n var validity = 1.0;\n var debug_diag_code_override = -1.0;\n var debug_depth_t = -1.0;\n var debug_edge_t = -1.0;\n var debug_linear_gap = -1.0;\n var debug_hit_mip = -1.0;\n var debug_steps_taken = 0.0;\n var debug_max_px_delta = -1.0;\n var dbg_decisive_branch_class = 0.0;\n var dbg_decisive_cell_uv = vec2<f32>(0.0);\n var dbg_decisive_cur_px = vec2<f32>(0.0);\n var dbg_decisive_depth_t_edge_sign = 0.0;\n var dbg_decisive_accept_hit_vs_continue = 0.0;\n var march_iter = 0u;\n var saw_coarse_hit = false;\n var mip0_confirmed = false;\n var last_hit_mip = -1.0;\n var last_coarse_hit_mip = -1.0;\n var last_coarse_hit_t = -1.0;\n var last_coarse_hit_depth_t = -1.0;\n var last_coarse_hit_edge_t = -1.0;\n var last_coarse_hit_screen_xy = vec2<f32>(0.0);\n var last_coarse_hit_stale = false;\n var last_valid_bracket_mip = -1.0;\n var mip0_entry_t = -1.0;\n var mip0_accept_t = -1.0;\n var mip0_accept_depth_t = -1.0;\n var mip0_steps_after_entry = 0u;\n\n // Stage 10-18: Hi-Z traversal, mip descent/ascent, and the decisive accept-vs-continue branch.\n loop {\n if !(cur_level >= 0 && steps_left > 0 && t < t_max) {\n break;\n }\n let mip = u32(cur_level);\n let cell_dims = vec2<f32>(textureDimensions(hiz_texture, mip));\n let cur_screen_pos = screen_pos + screen_ray_dir * t;\n if !is_finite3(cur_screen_pos) {\n debug_diag_code_override = 24.0;\n validity = 0.0;\n break;\n }\n let cell_index = clamp(vec2<i32>(floor(cur_screen_pos.xy * cell_dims)), vec2<i32>(0), vec2<i32>(cell_dims) - vec2<i32>(1, 1));\n let cell_minmax = textureLoad(hiz_texture, cell_index, cur_level).rg;\n let cell_depth = cell_minmax.r;\n let cell_depth_max = cell_minmax.g;\n\n let cur_px_i = clamp(vec2<i32>(cur_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px_f = vec2<f32>(cur_px_i);\n let cell_uv = (vec2<f32>(cell_index) + vec2<f32>(0.5)) / cell_dims;\n let next_cell_index = vec2<f32>(cell_index) + clamp(cell_step, vec2<f32>(0.0), vec2<f32>(1.0));\n let next_cell_pos = next_cell_index / cell_dims + cell_step * 0.000001;\n let pos_t = safe_axis_t2(next_cell_pos - screen_pos.xy, screen_ray_dir.xy);\n let edge_t = min(pos_t.x, pos_t.y);\n var dbg_iter_depth_t = -999.0;\n var dbg_iter_linear_gap = -999.0;\n var dbg_iter_is_hit = false;\n var dbg_iter_branch_class = 0.0;\n\n let depth_t = (cell_depth - screen_pos.z) / screen_ray_dir.z;\n if !is_finite1(edge_t) || !is_finite1(depth_t) {\n debug_diag_code_override = 25.0;\n validity = 0.0;\n break;\n }\n dbg_iter_depth_t = depth_t;\n debug_depth_t = depth_t;\n debug_edge_t = edge_t;\n debug_hit_mip = f32(cur_level);\n debug_steps_taken = f32(max_steps()) - f32(steps_left);\n let screen_ray_dir_z_nonnegative = screen_ray_dir.z >= 0.0;\n let use_t_le_depth_predicate = facing_camera;\n var is_hit = select(depth_t <= edge_t, t <= depth_t, use_t_le_depth_predicate);\n let is_hit_before_gap_reject = is_hit;\n dbg_iter_is_hit = is_hit;\n var mip_offset = select(1, -1, is_hit);\n // Dual Hi-Z behind-geometry skip: the depth buffer only stores front surfaces,\n // so a ray whose nearest point in this cell is deeper than the cell's MAX depth\n // (+ thickness tolerance) cannot hit anything recorded here. Skip the whole cell\n // at coarse mips instead of descending to mip0 and gap-rejecting 1px at a time.\n // This is what lets reflections continue past occluders (ball, arm, post).\n if is_hit && cur_level > 0 && cell_depth_max < 1.0 {\n let ray_nearest_t = select(t, min(edge_t, t_max), use_t_le_depth_predicate);\n let ray_nearest_z = screen_pos.z + screen_ray_dir.z * ray_nearest_t;\n let behind_gap = linearize_depth(cell_depth_max) - linearize_depth(ray_nearest_z);\n if behind_gap > depth_tolerance_m() {\n is_hit = false;\n mip_offset = 1;\n }\n }\n let depth_t_edge_sign = sign_nonzero(depth_t - edge_t);\n let bracket_eps_t = 2e-6;\n let stale_hit = is_hit && cur_level > 0 && !facing_camera && (depth_t + bracket_eps_t < t);\n let interval_hit = is_hit && cur_level > 0 && (depth_t + bracket_eps_t >= t) && (depth_t <= edge_t + bracket_eps_t);\n var dbg_iter_gap_reject = false;\n if cur_level == 0 {\n if saw_coarse_hit && mip0_entry_t < 0.0 {\n mip0_entry_t = t;\n }\n if mip0_entry_t >= 0.0 {\n mip0_steps_after_entry += 1u;\n }\n // Godot's mip0 thickness reject: (z0 - z1) > tolerance. Both linearize_depth\n // impls return negative view-z forward, so this rejects rays that penetrated\n // more than depth_tolerance BEHIND the surface (thin-object false hits),\n // letting the ray continue marching instead of accepting then zeroing alpha.\n let z0 = linearize_depth(cell_depth);\n let z1 = linearize_depth(cur_screen_pos.z);\n let linear_gap = z1 - z0; // positive = open gap in front, negative = penetration\n let depth_tolerance = depth_tolerance_m();\n dbg_iter_linear_gap = linear_gap;\n debug_linear_gap = linear_gap;\n let gap_reject = -linear_gap > depth_tolerance;\n if gap_reject {\n dbg_iter_gap_reject = true;\n if is_hit {\n dbg_iter_branch_class = 3.0;\n dbg_decisive_branch_class = 3.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 0.0;\n hit.debug_data = vec4<f32>(5.0, linear_gap, t, t_max);\n }\n is_hit = false;\n mip_offset = 0;\n }\n }\n\n if is_hit {\n last_hit_mip = f32(cur_level);\n if cur_level == 0 {\n mip0_confirmed = true;\n } else {\n saw_coarse_hit = true;\n last_coarse_hit_mip = f32(cur_level);\n last_coarse_hit_t = t;\n last_coarse_hit_depth_t = depth_t;\n last_coarse_hit_edge_t = edge_t;\n last_coarse_hit_screen_xy = cur_screen_pos.xy;\n last_coarse_hit_stale = stale_hit;\n if interval_hit {\n last_valid_bracket_mip = f32(cur_level);\n }\n }\n dbg_decisive_branch_class = 7.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 1.0;\n dbg_iter_branch_class = 7.0;\n dbg_iter_is_hit = true;\n if stage == 1u || stage == 2u {\n let scene_pos = view_pos_from_depth(cur_screen_pos.xy, cell_depth);\n let travel_m = length(scene_pos - start_view_pos);\n if stage == 1u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos, t);\n hit.debug_vec_b = vec4<f32>(scene_pos, travel_m);\n } else {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cell_index), cell_depth, edge_t);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, travel_m, t_max);\n }\n }\n }\n\n if !is_hit {\n if dbg_decisive_branch_class <= 0.0 || dbg_decisive_branch_class < 3.0 {\n dbg_iter_branch_class = 2.0;\n dbg_decisive_branch_class = 2.0;\n dbg_decisive_cell_uv = cell_uv;\n dbg_decisive_cur_px = cur_px_f;\n dbg_decisive_depth_t_edge_sign = depth_t_edge_sign;\n dbg_decisive_accept_hit_vs_continue = 0.0;\n }\n }\n\n dbg_iter_is_hit = is_hit;\n if dbg_iter_branch_class <= 0.0 && is_hit {\n dbg_iter_branch_class = 7.0;\n }\n store_debug_march_iteration(\n &hit, stage, march_iter, cur_screen_pos, cell_depth, cur_level, screen_ray_dir_z_nonnegative, use_t_le_depth_predicate,\n is_hit_before_gap_reject, dbg_iter_gap_reject, t, edge_t, dbg_iter_depth_t, dbg_iter_linear_gap, cur_px_f, dbg_iter_is_hit, dbg_iter_branch_class\n );\n\n if is_hit {\n if !facing_camera {\n t = max(t, depth_t);\n }\n if cur_level == 0 {\n mip0_accept_t = t;\n mip0_accept_depth_t = depth_t;\n }\n } else {\n t = edge_t;\n }\n cur_level = min(cur_level + mip_offset, max_level);\n steps_left -= 1;\n march_iter += 1u;\n }\n\n // Stage 19-29: resolve the final screen coordinate and compare against mip0 depth at the direct hit UV, closer to Godot's path.\n let cur_screen_pos = screen_pos + screen_ray_dir * t;\n if !is_finite3(cur_screen_pos) {\n hit.debug_data = vec4<f32>(26.0, t, t_max, 0.0);\n return hit;\n }\n let segment_t = clamp(t / param_z_abs, 0.0, 1.0);\n let segment_ray_hit_pos = view_pos + (trace_end_pos - view_pos) * segment_t;\n let segment_screen_pos = project_view_to_screen(segment_ray_hit_pos);\n if !is_finite3(segment_ray_hit_pos) || !is_finite3(segment_screen_pos) {\n hit.debug_data = vec4<f32>(27.0, t, segment_t, 0.0);\n return hit;\n }\n let scene_dims_f = vec2<f32>(textureDimensions(scene_color));\n let scene_uv_margin = vec2<f32>(0.5) / scene_dims_f;\n let resolved_screen_xy = clamp(cur_screen_pos.xy, scene_uv_margin, vec2<f32>(1.0) - scene_uv_margin);\n let cur_px_from_cur_screen_xy_i = clamp(vec2<i32>(cur_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px_from_segment_screen_xy_i = clamp(vec2<i32>(segment_screen_pos.xy * half_dims_f), vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let cur_px = cur_px_from_cur_screen_xy_i;\n let repr_uv = uv_from_px(cur_px, half_dims);\n let cur_px_uv = repr_uv;\n let hit_uv_exact = resolved_screen_xy;\n let final_hit_depth = load_hiz_depth(cur_px, 0u);\n let hit_sample_nr = textureLoad(normal_rough_texture, cur_px, 0);\n let projected_ray_hit_pos = view_pos_from_depth(hit_uv_exact, cur_screen_pos.z);\n let ray_hit_pos = projected_ray_hit_pos;\n let scene_hit_pos = view_pos_from_depth(hit_uv_exact, final_hit_depth);\n\n // Reproject hit world-pos through prev_view_proj so scene color is sampled at\n // the pixel where this hit point was shown last frame, not at the current-frame\n // hit pixel. Correct under camera rotation/translation.\n // On first frame (history_state.x==0) prev_hdr_scene is cleared; sampling returns 0\n // and we default prev_vignette = 1.0 so SSR isn't spuriously killed before history\n // stabilizes.\n let hit_world_pos = (u_camera.inverse_view * vec4<f32>(scene_hit_pos, 1.0)).xyz;\n let trace_exhausted = steps_left <= 0 && t < t_max;\n let coarse_hit_lost = saw_coarse_hit && !mip0_confirmed;\n let background_hit = final_hit_depth >= 1.0;\n let hit_prev_clip = u_prev.prev_view_proj * vec4<f32>(hit_world_pos, 1.0);\n var hit_scene_color = vec3<f32>(0.0);\n // Godot-style edge fade keyed on the PREV-frame hit UV (the source pixel the color\n // comes from). Defaults to 1.0 on first frame / valid center, drops toward 0 near\n // the prev-frame screen edges, and goes to 0 when the hit was behind the camera.\n var prev_vignette = 1.0;\n if u_prev.history_state.x != 0u {\n if hit_prev_clip.w > 0.0 {\n let hit_prev_ndc = hit_prev_clip.xyz / hit_prev_clip.w;\n let hit_prev_uv = vec2<f32>(hit_prev_ndc.x * 0.5 + 0.5, 1.0 - (hit_prev_ndc.y * 0.5 + 0.5));\n prev_vignette = edge_fade(hit_prev_uv);\n // Sample even when slightly out-of-frame; clamp UV and rely on prev_vignette\n // (which goes to 0 for |ndc|>=1) to zero-weight the contribution.\n let sample_uv = clamp(hit_prev_uv, vec2<f32>(0.0), vec2<f32>(1.0));\n hit_scene_color = textureSampleLevel(scene_color, linear_sampler, sample_uv, 0.0).rgb;\n } else {\n prev_vignette = 0.0;\n }\n if background_hit {\n // UE-style SSR can reflect sky from scene color. For true background misses there\n // is no world-space hit to reproject, so use the traced screen exit UV directly.\n prev_vignette = mix(0.35, 1.0, edge_fade(hit_uv_exact));\n hit_scene_color = textureSampleLevel(scene_color, linear_sampler, hit_uv_exact, 0.0).rgb;\n }\n }\n let hit_delta_vec = scene_hit_pos - ray_hit_pos;\n let hit_delta_m = length(hit_delta_vec);\n let ray_recon_delta_m = length(projected_ray_hit_pos - segment_ray_hit_pos);\n let segment_screen_xy_delta = segment_screen_pos.xy - cur_screen_pos.xy;\n let segment_screen_z_delta = segment_screen_pos.z - cur_screen_pos.z;\n let cur_px_coords = vec2<f32>(cur_px);\n let resolved_screen_px_fract = fract(resolved_screen_xy * half_dims_f);\n let cur_px_from_cur_screen_xy = vec2<f32>(cur_px_from_cur_screen_xy_i);\n let cur_px_from_segment_screen_xy = vec2<f32>(cur_px_from_segment_screen_xy_i);\n let cur_px_difference = cur_px_from_segment_screen_xy - cur_px_from_cur_screen_xy;\n if dbg_decisive_branch_class <= 0.0 {\n dbg_decisive_branch_class = 8.0;\n }\n let last_coarse_hit_overshoot_t = max(last_coarse_hit_t - last_coarse_hit_depth_t, 0.0);\n let coarse_ray_screen_z = screen_pos.z + screen_ray_dir.z * last_coarse_hit_t;\n let coarse_scene_screen_z = screen_pos.z + screen_ray_dir.z * last_coarse_hit_depth_t;\n let coarse_ray_view_pos = view_pos_from_depth(last_coarse_hit_screen_xy, coarse_ray_screen_z);\n let coarse_scene_view_pos = view_pos_from_depth(last_coarse_hit_screen_xy, coarse_scene_screen_z);\n let last_coarse_hit_overshoot_m = select(\n 0.0,\n length(coarse_ray_view_pos - coarse_scene_view_pos),\n last_coarse_hit_mip >= 0.0\n );\n let mip0_refine_delta_t = select(0.0, mip0_entry_t - mip0_accept_t, mip0_entry_t >= 0.0 && mip0_accept_t >= 0.0);\n let mip0_final_overshoot_t = max(mip0_accept_t - mip0_accept_depth_t, 0.0);\n let travel_m = length(scene_hit_pos - start_view_pos);\n let reflection_ray_len = length(screen_ray_dir.xy * t);\n if !is_finite1(hit_delta_m) || !is_finite1(reflection_ray_len) || !is_finite3(hit_scene_color) {\n validity = 0.0;\n debug_diag_code_override = 28.0;\n }\n hit.hit_mip_level = 0.0;\n if is_finite1(reflection_ray_len) {\n hit.hit_mip_level = compute_reflection_mip_level(roughness, reflection_ray_len, half_dims_f);\n }\n if stage == 1u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos, t);\n } else if stage == 2u {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cur_px), final_hit_depth, t_max);\n } else if stage == 7u {\n hit.debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n } else if stage == 8u {\n hit.debug_vec_a = vec4<f32>(hit_scene_color, hit_sample_nr.w);\n } else if stage == 9u {\n hit.debug_vec_a = vec4<f32>(segment_ray_hit_pos, segment_t);\n } else if stage == 10u {\n hit.debug_vec_a = vec4<f32>(vec2<f32>(cur_px), hit_uv_exact);\n } else if stage == 11u {\n hit.debug_vec_a = vec4<f32>(debug_depth_t, debug_edge_t, t, t_max);\n }\n if stage == 11u {\n hit.debug_vec_b = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n }\n if t >= t_max || final_hit_depth >= 1.0 {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n debug_diag_code_override = 3.0;\n }\n validity = 0.0;\n }\n // Godot-parity exhaustion handling: an exhausted/unconfirmed trace keeps its validity\n // and the hit_delta confidence term below decides. Hard-zeroing here turned every\n // exhausted grazing ray into a hard-black hole (B30); soft confidence falls back to IBL.\n if trace_exhausted {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n debug_diag_code_override = 17.0;\n }\n } else if !mip0_confirmed {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT && debug_diag_code_override < 0.0 {\n debug_diag_code_override = 18.0;\n }\n }\n if background_hit {\n debug_diag_code_override = 11.0;\n validity = 1.0;\n }\n let short_ray_screen_threshold = 2.0 / half_dims_f;\n if !background_hit && all(abs(screen_ray_dir.xy * t) < short_ray_screen_threshold) {\n let hit_normal_view = normalize((u_camera.view * vec4<f32>(normalize(hit_sample_nr.xyz), 0.0)).xyz);\n if dot(ray_dir, hit_normal_view) >= 0.0 {\n validity = 0.0;\n }\n }\n\n if stage == 1u {\n hit.debug_vec_b = vec4<f32>(scene_hit_pos, travel_m);\n } else if stage == 2u {\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, travel_m, t);\n } else if stage == 4u {\n hit.debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n } else if stage == 8u {\n hit.debug_vec_b = vec4<f32>(hit_sample_nr.xyz, final_hit_depth);\n } else if stage == 9u {\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 10u {\n hit.debug_vec_b = vec4<f32>(repr_uv, cur_screen_pos.xy);\n }\n // Stage 30-39: compare ray hit vs scene hit, then apply Godot-style post-hit confidence and fades.\n // Match Godot's post-hit confidence path here: use the base depth tolerance directly,\n // without extra local shells, then square the confidence weight.\n let validity_pre_confidence = validity;\n let confidence_tolerance = depth_tolerance_m();\n if !background_hit && coarse_hit_lost && hit_delta_m > confidence_tolerance {\n debug_diag_code_override = 16.0;\n }\n let confidence = select(1.0 - smoothstep(0.0, confidence_tolerance, hit_delta_m), 1.0, background_hit);\n let confidence_term = clamp(confidence * confidence, 0.0, 1.0);\n validity *= confidence_term;\n let validity_post_confidence = validity;\n let hit_uv = hit_uv_exact;\n // Godot-style: fade is based on the PREV-frame hit UV only (where the source color\n // actually came from). Reflections near the current-frame screen edge stay fully\n // visible as long as their reprojected source was safely in-frame last frame.\n // prev_vignette was seeded to 1.0 on first frame (no fade) and 0.0 when the hit\n // projected behind the camera last frame.\n let margin_blend = prev_vignette;\n // Ray fade runs on NORMALIZED along-ray progress (t / t_max), not absolute screen-UV\n // ray length: t_max is where this ray exits the screen, so progress is scale- and\n // direction-invariant. The old absolute length hard-zeroed any ray covering >= 1.0\n // screen UV and made alpha a function of on-screen travel distance, which painted\n // the B46 down-screen fade band.\n let ray_progress = clamp(t / max(t_max, 1e-5), 0.0, 1.0);\n let near_power = max(u_ssr.ray_fade_config.x, 0.0);\n let far_power = max(u_ssr.ray_fade_config.y, 0.0);\n var fade_in = 1.0;\n var fade_out = 1.0;\n if near_power > 0.0 {\n fade_in = pow(ray_progress, near_power);\n }\n if far_power > 0.0 {\n fade_out = pow(1.0 - ray_progress, far_power);\n }\n let fade = select(fade_in * fade_out, 1.0, fade_in * fade_out > 0.999);\n // UE5-style intensity scalar applied once at the end; on premultiplied output this\n // scales alpha and color together, equivalent to UE5's `OutColor *= SSRParams.r`.\n // The CPU-side early-out for intensity < 0.01 normally skips the whole SSR pipeline.\n validity *= fade * margin_blend * u_ssr.fade_config.x;\n\n let alpha_real = validity;\n let alpha = alpha_real;\n // Alt+Y stage readback follows the fixed SSR stage order.\n if stage == 0u {\n hit.debug_vec_a = vec4<f32>(start_depth_linear, start_depth, roughness, start_roughness_mask);\n hit.debug_vec_b = vec4<f32>(0.0, 0.0, bias, 0.0);\n } else if stage == 1u {\n hit.debug_vec_a = vec4<f32>(start_view_pos, start_depth);\n hit.debug_vec_b = vec4<f32>(start_uv, 0.0, 0.0);\n } else if stage == 2u {\n hit.debug_vec_a = vec4<f32>(view_normal, roughness);\n hit.debug_vec_b = vec4<f32>(geom_normal, dot(view_normal, geom_normal));\n } else if stage == 3u {\n hit.debug_vec_a = vec4<f32>(geom_normal, bias);\n hit.debug_vec_b = vec4<f32>(view_normal, dot(view_normal, geom_normal));\n } else if stage == 4u {\n hit.debug_vec_a = vec4<f32>(start_roughness_mask, roughness, 0.0, 0.0);\n hit.debug_vec_b = vec4<f32>(start_depth_linear, min_roughness(), max_roughness(), 0.0);\n } else if stage == 5u {\n hit.debug_vec_a = vec4<f32>(start_view_pos.z, source_clip.z, source_clip.w, source_ndc.z);\n hit.debug_vec_b = vec4<f32>(view_pos.z, biased_clip.z, biased_clip.w, biased_ndc.z);\n } else if stage == 6u {\n hit.debug_vec_a = vec4<f32>(trace_end_pos.z, trace_end_clip.z, trace_end_clip.w, trace_end_ndc.z);\n hit.debug_vec_b = vec4<f32>(screen_pos.z, screen_end.z, raw_screen_ray_dir.z, raw_screen_ray_z_abs);\n } else if stage == 7u {\n hit.debug_vec_a = vec4<f32>(bias, start_depth_linear, roughness, dot(view_normal, geom_normal));\n hit.debug_vec_b = vec4<f32>(view_pos, 1.0);\n } else if stage == 8u {\n hit.debug_vec_a = vec4<f32>(ray_dir, distance(view_pos, trace_end_pos));\n hit.debug_vec_b = vec4<f32>(screen_pos.z, screen_end.z, raw_screen_ray_dir.z, raw_screen_ray_z_abs);\n } else if stage == 9u {\n hit.debug_vec_a = vec4<f32>(t, t_max, clamp(t / max(t_max, 1e-5), 0.0, 1.0), segment_t);\n hit.debug_vec_b = vec4<f32>(screen_ray_dir.xy, t2.x, t2.y);\n } else if stage == 10u {\n hit.debug_vec_a = vec4<f32>(debug_depth_t, debug_edge_t, debug_linear_gap, debug_max_px_delta);\n hit.debug_vec_b = vec4<f32>(t, t_max, segment_t, dbg_decisive_accept_hit_vs_continue);\n } else if stage == 11u {\n hit.debug_vec_a = vec4<f32>(debug_edge_t, debug_depth_t, t, t_max);\n hit.debug_vec_b = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n } else if stage == 12u {\n hit.debug_vec_a = vec4<f32>(debug_hit_mip, debug_steps_taken, debug_linear_gap, debug_max_px_delta);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue, 0.0);\n } else if stage == 13u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue, 0.0);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n } else if stage == 14u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n hit.debug_vec_b = vec4<f32>(debug_depth_t, debug_edge_t, debug_linear_gap, debug_max_px_delta);\n } else if stage == 15u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_cur_px, debug_max_px_delta, dbg_decisive_branch_class);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_depth_t_edge_sign, dbg_decisive_accept_hit_vs_continue);\n } else if stage == 16u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_depth_t_edge_sign, debug_depth_t, debug_edge_t, debug_linear_gap);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_branch_class, dbg_decisive_accept_hit_vs_continue, 0.0, 0.0);\n } else if stage == 17u {\n hit.debug_vec_a = vec4<f32>(dbg_decisive_accept_hit_vs_continue, dbg_decisive_branch_class, debug_hit_mip, debug_steps_taken);\n hit.debug_vec_b = vec4<f32>(dbg_decisive_cell_uv, dbg_decisive_cur_px.x, dbg_decisive_cur_px.y);\n } else if stage == 18u {\n hit.debug_vec_a = vec4<f32>(cur_screen_pos.xy, cur_screen_pos.z, t);\n hit.debug_vec_b = vec4<f32>(segment_screen_pos.xy, segment_screen_pos.z, segment_t);\n } else if stage == 19u {\n hit.debug_vec_a = vec4<f32>(segment_screen_pos.xy, segment_screen_pos.z, segment_t);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, cur_screen_pos.z, t);\n } else if stage == 20u {\n hit.debug_vec_a = vec4<f32>(resolved_screen_xy, resolved_screen_px_fract);\n hit.debug_vec_b = vec4<f32>(cur_screen_pos.xy, segment_screen_pos.xy);\n } else if stage == 21u {\n hit.debug_vec_a = vec4<f32>(resolved_screen_px_fract, resolved_screen_xy);\n hit.debug_vec_b = vec4<f32>(repr_uv, hit_uv_exact);\n } else if stage == 22u {\n hit.debug_vec_a = vec4<f32>(cur_px_coords, final_hit_depth, 0.0);\n hit.debug_vec_b = vec4<f32>(repr_uv, cur_screen_pos.xy);\n } else if stage == 23u {\n hit.debug_vec_a = vec4<f32>(cur_px_from_cur_screen_xy, cur_screen_pos.xy);\n hit.debug_vec_b = vec4<f32>(resolved_screen_xy, t, segment_t);\n } else if stage == 24u {\n hit.debug_vec_a = vec4<f32>(cur_px_from_segment_screen_xy, segment_screen_pos.xy);\n hit.debug_vec_b = vec4<f32>(resolved_screen_xy, t, segment_t);\n } else if stage == 25u {\n hit.debug_vec_a = vec4<f32>(cur_px_difference, resolved_screen_px_fract);\n hit.debug_vec_b = vec4<f32>(cur_px_from_cur_screen_xy, cur_px_from_segment_screen_xy);\n } else if stage == 26u {\n hit.debug_vec_a = vec4<f32>(hit_scene_color, hit_sample_nr.w);\n hit.debug_vec_b = vec4<f32>(hit_uv_exact, final_hit_depth, 0.0);\n } else if stage == 27u {\n hit.debug_vec_a = vec4<f32>(hit_sample_nr.xyz, hit_sample_nr.w);\n hit.debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 28u {\n hit.debug_vec_a = vec4<f32>(hit_sample_nr.w, start_roughness_mask, 0.0, 0.0);\n hit.debug_vec_b = vec4<f32>(hit_scene_color, final_hit_depth);\n } else if stage == 29u {\n hit.debug_vec_a = vec4<f32>(ray_hit_pos, hit_delta_m);\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 30u {\n hit.debug_vec_a = vec4<f32>(scene_hit_pos, travel_m);\n hit.debug_vec_b = vec4<f32>(ray_hit_pos, hit_delta_m);\n } else if stage == 31u {\n hit.debug_vec_a = vec4<f32>(hit_delta_vec, hit_delta_m);\n hit.debug_vec_b = vec4<f32>(projected_ray_hit_pos, ray_recon_delta_m);\n } else if stage == 32u {\n hit.debug_vec_a = vec4<f32>(hit_delta_m, travel_m, debug_linear_gap, ray_recon_delta_m);\n hit.debug_vec_b = vec4<f32>(debug_depth_t, debug_edge_t, t, t_max);\n } else if stage == 33u {\n hit.debug_vec_a = vec4<f32>(last_coarse_hit_mip, last_coarse_hit_t, last_coarse_hit_depth_t, last_coarse_hit_edge_t);\n hit.debug_vec_b = vec4<f32>(last_coarse_hit_overshoot_t, last_coarse_hit_overshoot_m, select(0.0, 1.0, last_coarse_hit_stale), last_valid_bracket_mip);\n } else if stage == 34u {\n hit.debug_vec_a = vec4<f32>(confidence, confidence_term, validity_pre_confidence, validity_post_confidence);\n hit.debug_vec_b = vec4<f32>(alpha_real, alpha, alpha, hit.hit_confidence);\n } else if stage == 35u {\n hit.debug_vec_a = vec4<f32>(mip0_entry_t, mip0_accept_t, mip0_accept_depth_t, mip0_refine_delta_t);\n hit.debug_vec_b = vec4<f32>(mip0_final_overshoot_t, f32(mip0_steps_after_entry), select(0.0, 1.0, mip0_entry_t >= 0.0), select(0.0, 1.0, mip0_accept_t >= 0.0));\n } else if stage == 36u {\n hit.debug_vec_a = vec4<f32>(validity_pre_confidence, validity_post_confidence, confidence, confidence_term);\n hit.debug_vec_b = vec4<f32>(margin_blend, fade, alpha_real, alpha);\n } else if stage == 37u {\n hit.debug_vec_a = vec4<f32>(validity_post_confidence, validity_pre_confidence, confidence, confidence_term);\n hit.debug_vec_b = vec4<f32>(margin_blend, fade, alpha_real, alpha);\n } else if stage == 38u {\n hit.debug_vec_a = vec4<f32>(alpha_real, alpha, alpha, hit.hit_confidence);\n hit.debug_vec_b = vec4<f32>(confidence, confidence_term, validity_post_confidence, margin_blend * fade);\n }\n if !is_finite1(alpha) || !is_finite3(hit_scene_color) || alpha <= 1e-4 {\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n let diag_code = select(7.0, debug_diag_code_override, debug_diag_code_override >= 0.0);\n hit.debug_data = vec4<f32>(diag_code, confidence, validity, alpha);\n }\n } else {\n hit.hit_uv = hit_uv;\n hit.hit_confidence = min(alpha, 1.0);\n // Premultiplied contract: tonemapped color is scaled by alpha exactly once,\n // here. Spatial resolve un-premultiplies before untonemapping and main_pass\n // composites rgb directly into env radiance before the split-sum\n // (F0*brdf.x+brdf.y) term \u2014 the fresnel formerly approximated by\n // visible_weight is applied there, consistently with IBL.\n hit.hit_color = tone_map_ssr_color(hit_scene_color) * hit.hit_confidence;\n if stage < SSR_DEBUG_PIPELINE_STAGE_COUNT {\n hit.debug_data = vec4<f32>(10.0, confidence, validity, hit.hit_confidence);\n }\n }\n store_debug_pipeline_capture(\n &hit, stage, start_view_pos, start_depth_linear, roughness, start_uv, view_normal, geom_normal, bias, view_pos, ray_dir, distance(view_pos, trace_end_pos),\n source_clip, source_ndc, biased_clip, biased_ndc, trace_end_clip, trace_end_ndc, screen_pos.z, screen_end.z, raw_screen_ray_dir,\n t, t_max, segment_t, screen_ray_dir.xy, t2, hit_scene_color, hit_sample_nr.xyz, hit_sample_nr.w, hit_uv_exact, final_hit_depth,\n ray_hit_pos, scene_hit_pos, travel_m, projected_ray_hit_pos, ray_recon_delta_m, hit_delta_m\n );\n return hit;\n}\n\n// Shared trace body. Two entry points wrap it: production `fs_main` writes only the color\n// target (the 3 debug MRTs cost ~24 B/px of ROP writes at trace res every frame); debug\n// bursts use `fs_main_debug` with the full 4-MRT output for pixel readback.\nfn trace_fragment(frag_coord: vec4<f32>) -> TraceFsOut {\n if u_ssr.trace_config.x <= 0.0 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(frag_coord.xy), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(100.0, 0.0, 0.0, 0.0), hit.debug_vec_a, hit.debug_vec_b));\n }\n\n let half_size_u = textureDimensions(normal_rough_texture);\n let pixel_coords = min(vec2<u32>(frag_coord.xy), half_size_u - vec2<u32>(1u, 1u));\n let half_size = vec2<f32>(half_size_u);\n let uv = (vec2<f32>(pixel_coords) + 0.5) / half_size;\n let depth = textureLoad(repr_depth_texture, vec2<i32>(pixel_coords), 0).r;\n if depth >= 1.0 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(101.0, depth, 0.0, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n\n let normal_rough = textureLoad(normal_rough_texture, vec2<i32>(pixel_coords), 0);\n let roughness = clamp(normal_rough.w, 0.0, 1.0);\n if roughness >= max_roughness() {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(102.0, roughness, max_roughness(), 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n let roughness_mask = 1.0 - smoothstep(min_roughness(), max_roughness(), roughness);\n if roughness_mask <= 1e-4 {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(103.0, roughness, roughness_mask, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n\n let world_normal = normalize(normal_rough.xyz);\n let view_normal = normalize((u_camera.view * vec4<f32>(world_normal, 0.0)).xyz);\n let view_pos = view_pos_from_depth(uv, depth);\n\n let world_pos = world_pos_from_depth(uv, depth);\n // Underwater bail (fade_config.y = water plane Y minus eps; -1e30 when no water in view):\n // seafloor SSR is only ever seen through depth-attenuated water refraction, and the water\n // surface itself reflects via the planar pass \u2014 tracing below the plane is wasted work.\n if world_pos.y < u_ssr.fade_config.y {\n let hit = empty_hit();\n store_trace_mip(vec2<i32>(pixel_coords), 0.0);\n return trace_fs_out(vec4<f32>(0.0), TraceHit(hit.hit_uv, hit.hit_color, hit.hit_confidence, 0.0, vec4<f32>(104.0, world_pos.y, u_ssr.fade_config.y, 0.0), vec4<f32>(0.0, 0.0, 0.0, depth), hit.debug_vec_b));\n }\n let geom_normal = compute_geometric_normal(vec2<i32>(pixel_coords), depth, view_pos, vec2<i32>(half_size_u), view_normal);\n let hit = trace_hiz(uv, depth, view_pos, view_normal, geom_normal, roughness);\n store_trace_mip(vec2<i32>(pixel_coords), hit.hit_mip_level);\n if hit.hit_confidence > 1e-4 {\n return trace_fs_out(vec4<f32>(hit.hit_color, hit.hit_confidence), hit);\n }\n return trace_fs_out(vec4<f32>(0.0), hit);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n return trace_fragment(frag_coord).color;\n}\n\n@fragment\nfn fs_main_debug(@builtin(position) frag_coord: vec4<f32>) -> TraceFsOut {\n return trace_fragment(frag_coord);\n}\n"},{"label":"shaders/ssr_filter.wgsl","code":"@group(0) @binding(0) var source_ssr: texture_2d<f32>;\n\nstruct FilterUniform {\n config: vec4<f32>,\n};\n\n@group(0) @binding(1) var<uniform> u_filter: FilterUniform;\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn div_ceil_i32(a: i32, b: i32) -> i32 {\n return (a + b - 1) / b;\n}\n\nconst GAUSS_WEIGHTS: array<f32, 7> = array<f32, 7>(\n 0.07130343,\n 0.13151412,\n 0.18987924,\n 0.21460643,\n 0.18987924,\n 0.13151412,\n 0.07130343,\n);\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn filter_weight(color: vec4<f32>) -> f32 {\n let mip_level = clamp(u_filter.config.x, 0.0, 8.0);\n return mix(clamp(mip_level * 0.2, 0.0, 1.0), 1.0, clamp(color.a, 0.0, 1.0));\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let src_dims = vec2<i32>(textureDimensions(source_ssr));\n let dst_dims = max(vec2<i32>(div_ceil_i32(src_dims.x, 2), div_ceil_i32(src_dims.y, 2)), vec2<i32>(1, 1));\n let dst_px = vec2<i32>(frag_coord.xy);\n let src_center = vec2<f32>(\n ((f32(dst_px.x) + 0.5) * f32(src_dims.x)) / f32(dst_dims.x) - 0.5,\n ((f32(dst_px.y) + 0.5) * f32(src_dims.y)) / f32(dst_dims.y) - 0.5,\n );\n let src_center_px = vec2<i32>(round(src_center));\n var sum = vec4<f32>(0.0);\n var weight_sum = 0.0;\n\n for (var oy = -3; oy <= 3; oy += 1) {\n for (var ox = -3; ox <= 3; ox += 1) {\n let sample_px = clamp(src_center_px + vec2<i32>(ox, oy), vec2<i32>(0), src_dims - vec2<i32>(1, 1));\n let color = textureLoad(source_ssr, sample_px, 0);\n if !is_finite4(color) {\n continue;\n }\n let gaussian_weight = GAUSS_WEIGHTS[u32(ox + 3)] * GAUSS_WEIGHTS[u32(oy + 3)];\n let weight = gaussian_weight * filter_weight(color);\n sum += color * weight;\n weight_sum += weight;\n }\n }\n\n if weight_sum <= 1e-5 {\n return vec4<f32>(0.0);\n }\n return sum / weight_sum;\n}\n"},{"label":"shaders/ssr_spatial_resolve.wgsl","code":"@group(0) @binding(0) var filtered_ssr: texture_2d<f32>;\n@group(0) @binding(1) var trace_mip_level: texture_2d<f32>;\n@group(0) @binding(2) var full_depth: texture_depth_2d;\n@group(0) @binding(3) var full_normal: texture_2d<f32>;\n@group(0) @binding(4) var full_orm: texture_2d<f32>;\n@group(0) @binding(5) var half_depth: texture_2d<f32>;\n@group(0) @binding(6) var half_normal_rough: texture_2d<f32>;\n@group(0) @binding(7) var linear_sampler: sampler;\n\nconst SSR_RESOLVE_DEPTH_WEIGHT: f32 = 2048.0;\nconst SSR_RESOLVE_NORMAL_WEIGHT: f32 = 32.0;\nconst SSR_RESOLVE_ROUGHNESS_WEIGHT: f32 = 16.0;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite1(v: f32) -> bool { return v == v && abs(v) < SSR_FINITE_LIMIT; }\nfn is_finite3(v: vec3<f32>) -> bool { return all(v == v) && all(abs(v) < vec3<f32>(SSR_FINITE_LIMIT)); }\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn untone_map_ssr_color(color: vec3<f32>) -> vec3<f32> {\n let rec709_luminance_weights = vec3<f32>(0.2126, 0.7152, 0.0722);\n return color / max(1.0 - dot(color, rec709_luminance_weights), 1e-4);\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\nfn sample_weight(\n full_depth_value: f32,\n full_normal_value: vec3<f32>,\n full_roughness_value: f32,\n sample_depth_value: f32,\n sample_normal_value: vec3<f32>,\n sample_roughness_value: f32,\n) -> f32 {\n let depth_weight = exp(-abs(full_depth_value - sample_depth_value) * SSR_RESOLVE_DEPTH_WEIGHT);\n let normal_delta = max(0.0, 1.0 - dot(full_normal_value, sample_normal_value));\n let normal_weight = exp(-normal_delta * SSR_RESOLVE_NORMAL_WEIGHT);\n let roughness_weight = exp(-abs(full_roughness_value - sample_roughness_value) * SSR_RESOLVE_ROUGHNESS_WEIGHT);\n return depth_weight * normal_weight * roughness_weight;\n}\n\nfn sample_half_resolve(\n tap_px_unclamped: vec2<i32>,\n bilinear_weight: f32,\n full_depth_value: f32,\n full_normal_value: vec3<f32>,\n full_roughness_value: f32,\n half_dims: vec2<i32>,\n half_dims_f: vec2<f32>,\n) -> vec4<f32> {\n let tap_px = clamp(tap_px_unclamped, vec2<i32>(0), half_dims - vec2<i32>(1, 1));\n let sample_depth_value = textureLoad(half_depth, tap_px, 0).r;\n if sample_depth_value >= 1.0 {\n return vec4<f32>(0.0);\n }\n let sample_nr = textureLoad(half_normal_rough, tap_px, 0);\n let sample_normal_value = normalize(sample_nr.xyz);\n let sample_roughness_value = clamp(sample_nr.w, 0.0, 1.0);\n let stored_mip = textureLoad(trace_mip_level, tap_px, 0).x;\n let tap_uv = (vec2<f32>(tap_px) + 0.5) / half_dims_f;\n let ssr_sample = textureSampleLevel(filtered_ssr, linear_sampler, tap_uv, select(0.0, stored_mip, is_finite1(stored_mip)));\n if !is_finite4(ssr_sample) || ssr_sample.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Trace output is premultiplied in tonemapped space. Un-premultiply first: the\n // untonemap curve is nonlinear, so untonemapping the alpha-scaled value would\n // re-weight the color by validity a second time (radiance ~ validity^2, B46's\n // measured 2.4x-dark defect). Then re-premultiply so the weighted sum below stays\n // a premultiplied blend.\n let straight_tm = ssr_sample.rgb / clamp(ssr_sample.a, 1e-5, 1.0);\n let linear_rgb = untone_map_ssr_color(straight_tm);\n if !is_finite3(linear_rgb) {\n return vec4<f32>(0.0);\n }\n let weight = bilinear_weight * sample_weight(\n full_depth_value, full_normal_value, full_roughness_value,\n sample_depth_value, sample_normal_value, sample_roughness_value\n );\n return vec4<f32>(linear_rgb * ssr_sample.a * weight, ssr_sample.a * weight);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let full_dims_u = textureDimensions(full_depth);\n let full_dims = vec2<i32>(full_dims_u);\n let full_px = clamp(vec2<i32>(frag_coord.xy), vec2<i32>(0), full_dims - vec2<i32>(1, 1));\n let full_depth_value = textureLoad(full_depth, full_px, 0);\n if full_depth_value >= 1.0 {\n return vec4<f32>(0.0);\n }\n let full_normal_value = normalize(textureLoad(full_normal, full_px, 0).xyz);\n let full_roughness_value = clamp(textureLoad(full_orm, full_px, 0).g, 0.0, 1.0);\n let half_dims_u = textureDimensions(trace_mip_level);\n let half_dims = vec2<i32>(half_dims_u);\n let half_dims_f = vec2<f32>(half_dims_u);\n\n // Full-res SSR (SsrHigh): trace/spatial are 1:1 with the gbuffer, so the half->full\n // 2x2 bilinear reconstruction collapses to a single weighted tap at the same pixel.\n // Using the original *0.5, /2 math here would sample a shifted, blurred neighborhood.\n if all(half_dims_u == full_dims_u) {\n let s = sample_half_resolve(full_px, 1.0, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n if s.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Output stays premultiplied (linear HDR): rgb already carries alpha.\n return vec4<f32>(s.rgb, clamp(s.a, 0.0, 1.0));\n }\n\n // Half-res SSR (SsrLow): reconstruct back to full via a 2x2 neighborhood.\n let half_tex_coord = (vec2<f32>(full_px) + 0.5) * 0.5;\n let bilinear_weights = fract(half_tex_coord);\n let base_px = (full_px - vec2<i32>(1, 1)) / 2;\n\n let s00 = sample_half_resolve(base_px + vec2<i32>(0, 0), bilinear_weights.x * bilinear_weights.y, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s10 = sample_half_resolve(base_px + vec2<i32>(1, 0), (1.0 - bilinear_weights.x) * bilinear_weights.y, full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s01 = sample_half_resolve(base_px + vec2<i32>(0, 1), bilinear_weights.x * (1.0 - bilinear_weights.y), full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let s11 = sample_half_resolve(base_px + vec2<i32>(1, 1), (1.0 - bilinear_weights.x) * (1.0 - bilinear_weights.y), full_depth_value, full_normal_value, full_roughness_value, half_dims, half_dims_f);\n let sum = s00 + s10 + s01 + s11;\n if sum.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n // Output stays premultiplied (linear HDR): rgb already carries alpha (bilinear\n // weights sum to 1, so no renormalization is needed).\n return vec4<f32>(sum.rgb, clamp(sum.a, 0.0, 1.0));\n}\n"},{"label":"shaders/ssr_history_copy_normal.wgsl","code":"// ssr_history_copy_normal.wgsl\n@group(0) @binding(0) var normal_texture: texture_2d<f32>;\n\nstruct FSOutput {\n @location(0) color: vec4<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> FSOutput {\n let dims = vec2<i32>(textureDimensions(normal_texture));\n let px = clamp(vec2<i32>(frag_coord.xy), vec2<i32>(0), dims - vec2<i32>(1, 1));\n return FSOutput(textureLoad(normal_texture, px, 0));\n}\n"},{"label":"shaders/ssr_temporal_resolve.wgsl","code":"// ssr_temporal_resolve.wgsl\n// Operates on the spatial-resolve output: rgb is PREMULTIPLIED linear radiance\n// (already scaled by a), a is confidence. All blends below are linear, so they are\n// valid directly on the premultiplied pair.\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n _padding2: f32,\n _padding3: f32,\n camera_right: vec3<f32>,\n _padding4: f32,\n camera_up: vec3<f32>,\n _padding5: f32,\n inverse_view: mat4x4<f32>,\n};\n\nstruct SsrHistoryUniform {\n // Current clip -> previous clip, f64-composed on the CPU (B34 pattern): avoids the\n // f32 inverse+forward world round trip whose error grows with distance from the origin.\n reproj: mat4x4<f32>,\n // World -> previous clip; used by ssr_trace.wgsl only (hit-point reprojection).\n prev_view_proj: mat4x4<f32>,\n history_state: vec4<u32>,\n};\n\nstruct SsrSettingsUniform {\n trace_config: vec4<f32>,\n material_config: vec4<f32>,\n fade_config: vec4<f32>,\n ray_fade_config: vec4<f32>,\n debug_config: vec4<f32>,\n};\n\n@group(0) @binding(0) var current_ssr: texture_2d<f32>;\n@group(0) @binding(1) var prev_ssr: texture_2d<f32>;\n@group(0) @binding(2) var current_depth: texture_depth_2d;\n@group(0) @binding(3) var current_normal: texture_2d<f32>;\n@group(0) @binding(4) var current_orm: texture_2d<f32>;\n@group(0) @binding(5) var prev_depth: texture_depth_2d;\n@group(0) @binding(6) var prev_normal: texture_2d<f32>;\n@group(0) @binding(7) var<uniform> u_prev: SsrHistoryUniform;\n@group(0) @binding(8) var<uniform> u_ssr: SsrSettingsUniform;\n@group(0) @binding(9) var linear_sampler: sampler;\n@group(1) @binding(0) var<uniform> u_camera: CameraUniform;\n\nconst SSR_DEPTH_REJECT_EPSILON: f32 = 0.0025;\nconst SSR_NORMAL_ACCEPT_DOT: f32 = 0.95;\nconst SSR_HISTORY_ONLY_DECAY: f32 = 0.65;\nconst SSR_HISTORY_MOTION_START: f32 = 0.003;\nconst SSR_HISTORY_MOTION_END: f32 = 0.03;\nconst SSR_HISTORY_VISIBLE_START: f32 = 0.08;\nconst SSR_HISTORY_VISIBLE_END: f32 = 0.35;\nconst SSR_FINITE_LIMIT: f32 = 1.0e20;\n\nfn is_finite4(v: vec4<f32>) -> bool { return all(v == v) && all(abs(v) < vec4<f32>(SSR_FINITE_LIMIT)); }\n\nfn sanitize_ssr(v: vec4<f32>) -> vec4<f32> {\n if !is_finite4(v) || v.a <= 1e-5 {\n return vec4<f32>(0.0);\n }\n return vec4<f32>(v.rgb, clamp(v.a, 0.0, 1.0));\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {\n let x = f32((idx << 1u) & 2u);\n let y = f32(idx & 2u);\n return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n}\n\n@fragment\nfn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {\n let ssr_size = vec2<f32>(textureDimensions(current_ssr));\n let full_size_u = textureDimensions(current_depth);\n let full_size = vec2<f32>(full_size_u);\n let uv = frag_coord.xy / ssr_size;\n let current = sanitize_ssr(textureSampleLevel(current_ssr, linear_sampler, uv, 0.0));\n if u_prev.history_state.x == 0u {\n return current;\n }\n\n let full_px = vec2<i32>(min(vec2<u32>(floor(uv * full_size)), full_size_u - vec2<u32>(1u, 1u)));\n let full_uv = (vec2<f32>(full_px) + 0.5) / full_size;\n let depth = textureLoad(current_depth, full_px, 0);\n if depth >= 1.0 {\n return current;\n }\n\n let current_roughness = clamp(textureLoad(current_orm, full_px, 0).g, 0.0, 1.0);\n let current_roughness_mask = 1.0 - smoothstep(u_ssr.material_config.x, u_ssr.material_config.y, current_roughness);\n if current_roughness_mask <= 1e-4 {\n return current;\n }\n\n let world_normal = normalize(textureLoad(current_normal, full_px, 0).xyz);\n // Reproject straight in clip space: (ndc, depth, 1) is the true clip position up to the\n // unknown w, which the homogeneous divide below cancels.\n let ndc = vec4<f32>(full_uv.x * 2.0 - 1.0, (1.0 - full_uv.y) * 2.0 - 1.0, depth, 1.0);\n let prev_clip = u_prev.reproj * ndc;\n if prev_clip.w <= 0.0 {\n return current;\n }\n\n let prev_ndc = prev_clip.xyz / prev_clip.w;\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 1.0 - (prev_ndc.y * 0.5 + 0.5));\n if any(prev_uv <= vec2<f32>(0.0)) || any(prev_uv >= vec2<f32>(1.0)) {\n return current;\n }\n let motion_factor = 1.0 - smoothstep(SSR_HISTORY_MOTION_START, SSR_HISTORY_MOTION_END, distance(prev_uv, full_uv));\n if motion_factor <= 1e-3 {\n return current;\n }\n\n let prev_px = vec2<i32>(min(vec2<u32>(floor(prev_uv * full_size)), full_size_u - vec2<u32>(1u, 1u)));\n let prev_depth_value = textureLoad(prev_depth, prev_px, 0);\n if prev_depth_value >= 1.0 || abs(prev_depth_value - prev_ndc.z) > SSR_DEPTH_REJECT_EPSILON {\n return current;\n }\n\n let prev_world_normal = normalize(textureLoad(prev_normal, prev_px, 0).xyz);\n if dot(world_normal, prev_world_normal) < SSR_NORMAL_ACCEPT_DOT {\n return current;\n }\n\n let history = sanitize_ssr(textureSampleLevel(prev_ssr, linear_sampler, prev_uv, 0.0));\n let history_conf = clamp(history.a * motion_factor, 0.0, 1.0);\n if history_conf <= 1e-4 {\n return current;\n }\n let history_visible = smoothstep(SSR_HISTORY_VISIBLE_START, SSR_HISTORY_VISIBLE_END, history_conf);\n let history_rgb = history.rgb * history_visible;\n\n let current_conf = clamp(current.a, 0.0, 1.0);\n let history_mix = clamp((0.15 + 0.40 * history_conf / max(current_conf + history_conf, 1e-4)) * motion_factor, 0.0, 0.55);\n if current_conf <= 1e-4 {\n return vec4<f32>(history_rgb * SSR_HISTORY_ONLY_DECAY, history_conf * SSR_HISTORY_ONLY_DECAY);\n } else {\n return vec4<f32>(mix(current.rgb, history_rgb, history_mix), max(current_conf, history_conf * 0.85));\n }\n}\n"},{"label":"shaders/bloom.wgsl","code":"// bloom.wgsl\nstruct BloomSettings {\n threshold: f32,\n soft_knee: f32,\n intensity: f32,\n clamp_value: f32,\n downsample_offset:f32,\n upsample_offset: f32,\n _pad0: f32,\n _pad1: f32,\n};\n\n@group(0) @binding(0) var tex_a: texture_2d<f32>;\n@group(0) @binding(1) var tex_b: texture_2d<f32>; // only used by upsample\n@group(0) @binding(2) var samp_linear: sampler;\n@group(0) @binding(3) var<uniform> settings: BloomSettings;\n\n// Fullscreen triangle\nstruct VsOut {\n @builtin(position) pos : vec4<f32>,\n @location(0) uv : vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n // 3-vertex fullscreen tri. Clip positions: (-1,-1),(3,-1),(-1,3)\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n let pos = vec2<f32>(px, py) * 2.0 - 1.0; // -1..3 pattern\n o.pos = vec4<f32>(pos, 0.0, 1.0);\n o.uv = vec2<f32>(px, py); // Fullscreen triangle: verts use {0,2}; inside viewport uv interpolates to [0,1], so no *0.5 needed\n return o;\n}\n\n// Helpers\nfn soft_threshold(bright: f32, thresh: f32, knee_frac: f32) -> f32 {\n let knee = max(thresh * knee_frac, 1e-5);\n return smoothstep(thresh - knee, thresh + knee, bright);\n}\nfn flip_uv(v: vec2<f32>) -> vec2<f32> {\n return vec2<f32>(v.x, 1.0 - v.y);\n}\nfn kawase4(tex: texture_2d<f32>, smp: sampler, uv: vec2<f32>, texel: vec2<f32>, offset: f32) -> vec3<f32> {\n let d = texel * offset;\n var s = vec3<f32>(0.0);\n s += textureSample(tex, smp, uv + vec2<f32>( d.x, d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>(-d.x, d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>( d.x, -d.y)).rgb;\n s += textureSample(tex, smp, uv + vec2<f32>(-d.x, -d.y)).rgb;\n return s * 0.25;\n}\n\n// Prefilter (full -> 1/2)\n@fragment\nfn fs_prefilter(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let uvf = flip_uv(uv);\n var c = textureSample(tex_a, samp_linear, uvf);\n let b = max(c.r, max(c.g, c.b));\n let w = soft_threshold(b, settings.threshold, settings.soft_knee);\n\n var outc = c * w;\n if (settings.clamp_value > 0.0) {\n outc = vec4<f32>(min(outc.rgb, vec3<f32>(settings.clamp_value)), outc.a);\n }\n outc.a = 1.0;\n return outc;\n}\n\n// Kawase downsample (1/2 -> 1/4 -> ...)\n@fragment\nfn fs_down(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let dim = textureDimensions(tex_a);\n let texel = 1.0 / vec2<f32>(f32(dim.x), f32(dim.y));\n let uvf = flip_uv(uv);\n let rgb = kawase4(tex_a, samp_linear, uvf, texel, settings.downsample_offset);\n return vec4<f32>(rgb, 1.0);\n}\n\n// Kawase upsample with additive ping ( ... -> 1/4 -> 1/2 )\n@fragment\nfn fs_up(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let dim = textureDimensions(tex_a);\n let texel = 1.0 / vec2<f32>(f32(dim.x), f32(dim.y));\n\n let uvf = flip_uv(uv);\n let up_rgb = kawase4(tex_a, samp_linear, uvf, texel, settings.upsample_offset);\n let add_rgb = textureSample(tex_b, samp_linear, uvf).rgb;\n\n var out_rgb = up_rgb + add_rgb;\n if (settings.clamp_value > 0.0) {\n out_rgb = min(out_rgb, vec3<f32>(settings.clamp_value));\n }\n return vec4<f32>(out_rgb, 1.0);\n}\n\n// The chain ends at half-res up[0]; tonemap.wgsl samples it and applies\n// intensity/clamp (bloom_params) \u2014 no full-res upsample/composite entry points.\n"},{"label":"skinned_impostor_bake","code":"// skinned_impostor_bake.wgsl\n// Offscreen whole-character bake: renders one skinned submesh (posed by the live bone\n// palettes of a chosen source instance, model space) into a hemi-octahedral atlas cell with\n// flat albedo. One draw per (submesh, cell); the ortho view/proj frames the mesh's calibrated\n// bounds so the billboard shader (skinned_impostor.wgsl) can reconstruct the exact mapping.\n\nstruct BakeView {\n view_proj: mat4x4<f32>,\n // Palette base of the bake source instance inside this submesh group's palette buffer.\n palette_base: u32,\n _pad0: u32,\n _pad1: u32,\n _pad2: u32,\n};\n@group(0) @binding(0) var<uniform> u_view: BakeView;\n\n@group(1) @binding(0) var base_color_texture: texture_2d<f32>;\n@group(1) @binding(1) var normal_texture: texture_2d<f32>;\n@group(1) @binding(2) var orm_texture: texture_2d<f32>;\n@group(1) @binding(3) var material_sampler: sampler;\n\n@group(2) @binding(0) var<storage, read> u_palettes: array<mat4x4<f32>>;\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n @location(4) joints: vec4<u32>,\n @location(5) weights: vec4<f32>,\n) -> VSOut {\n // Same palette skinning as gbuffer_skinned.wgsl, frozen at the bake frame's pose.\n let weight_sum = max(weights.x + weights.y + weights.z + weights.w, 1e-5);\n let w = weights / weight_sum;\n var skinned_pos = vec4<f32>(0.0);\n for (var i = 0u; i < 4u; i = i + 1u) {\n skinned_pos += u_palettes[joints[i] + u_view.palette_base] * vec4<f32>(position, 1.0) * w[i];\n }\n var out: VSOut;\n out.clip_position = u_view.view_proj * skinned_pos;\n out.uv = uv;\n return out;\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n // Flat albedo: at impostor distances (sub-1% screen) per-pixel lighting detail is below\n // the atlas texel footprint; the billboard shader supplies a capsule proxy normal instead.\n let c = textureSampleLevel(base_color_texture, material_sampler, uv, 0.0);\n if (c.a < 0.5) {\n discard; // cutout submeshes (hair cards etc.) keep their silhouette\n }\n return vec4<f32>(c.rgb, 1.0);\n}\n"},{"label":"skinned_impostor_mip","code":"// leaf_impostor_mip.wgsl\n// Bake-time mip downsample for the impostor atlas: fullscreen triangle sampling the\n// previous mip (linear filter = 2x2 box). Runs once per (layer, mip) after each bake.\n\n@group(0) @binding(0) var t_src: texture_2d<f32>;\n@group(0) @binding(1) var s_src: sampler;\n\nstruct VSOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {\n var out: VSOut;\n let x = f32(i32(vi & 1u) * 4 - 1);\n let y = f32(i32(vi >> 1u) * 4 - 1);\n out.pos = vec4<f32>(x, y, 0.0, 1.0);\n out.uv = vec2<f32>(x * 0.5 + 0.5, 0.5 - y * 0.5);\n return out;\n}\n\n@fragment\nfn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {\n let c = textureSample(t_src, s_src, uv);\n // Alpha-coverage preservation: sparse canopies otherwise erode below the impostor\n // alpha cutoff after a few mips (distant trees turn invisible). Empty texels are\n // black, so the box filter effectively premultiplies rgb by coverage; divide it\n // back out to keep leaf color, and boost alpha to preserve silhouette coverage.\n let rgb = c.rgb / max(c.a, 1e-4);\n return vec4<f32>(rgb, min(c.a * 1.8, 1.0));\n}\n"},{"label":"skinned_impostor","code":"// skinned_impostor.wgsl\n// Far-crowd whole-character impostor: one billboard quad per routed skinned instance,\n// sampling a hemi-octahedral GRID x GRID atlas of baked views (skinned_impostor_bake.wgsl).\n// Instances arrive through the skinned Hi-Z cull's impostor region (skinned_hiz_cull.wgsl\n// routes sub-threshold instances here instead of a mesh LOD slot; misc1.w carries the atlas\n// layer). The three frames whose grid triangle encloses the view direction blend\n// barycentrically; a screen-size dither band overlaps the smallest mesh LOD so the handoff\n// never pops. GRID must match SKINNED_IMP_GRID in skinned_impostor.rs; hemi_oct/basis math\n// mirrors the Rust bake (impostor_math.rs).\n\nconst GRID: f32 = 4.0;\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n near: f32,\n far: f32,\n reflection_clip_y: f32,\n reflection_clip_enabled: f32,\n camera_right: vec3<f32>,\n mip_bias: f32,\n camera_up: vec3<f32>,\n _pad5: f32,\n inverse_view: mat4x4<f32>,\n wind: vec4<f32>,\n unjittered_view_proj: mat4x4<f32>,\n prev_unjittered_view_proj: mat4x4<f32>,\n};\n@group(0) @binding(0) var<uniform> u_camera: CameraUniform;\n\n@group(1) @binding(0) var t_atlas: texture_2d_array<f32>;\n@group(1) @binding(1) var s_atlas: sampler;\n\n// Mirrors SkinnedCullParams in skinned_occlusion.rs (the group's cull params uniform).\nstruct SkinnedCullParams {\n instance_count: u32,\n capacity: u32,\n radius_factor: f32,\n margin_m: f32,\n bind_center: vec3<f32>,\n lod_size_factor: f32,\n lod1_screen_size: f32,\n lod2_screen_size: f32,\n lod3_screen_size: f32,\n hiz_enabled: u32,\n bucket_map: vec4<u32>,\n imp_mode: u32,\n imp_region: u32,\n imp_layer: u32,\n _imp_pad: u32,\n imp_start_screen: f32,\n imp_full_screen: f32,\n imp_half_factor: f32,\n imp_ell_x: f32,\n imp_ell_y: f32,\n _imp_pad1: f32,\n _imp_pad2: f32,\n _imp_pad3: f32,\n};\n@group(2) @binding(0) var<uniform> u_params: SkinnedCullParams;\n\nstruct VertexIn {\n // Quad mesh (Vertex layout): x in [-0.5,0.5], y in [0,1]\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) tangent: vec4<f32>,\n // Instance (SkinnedInstanceData in the compacted impostor region)\n @location(4) model_0: vec4<f32>,\n @location(5) model_1: vec4<f32>,\n @location(6) model_2: vec4<f32>,\n @location(7) model_3: vec4<f32>,\n @location(8) mesh_color: vec4<f32>,\n @location(9) misc1: vec4<u32>, // w = atlas layer (written by the cull route)\n};\n\nstruct VSOut {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n @location(2) @interpolate(flat) layer: u32,\n @location(3) @interpolate(flat) cells: vec3<u32>,\n @location(4) weights: vec3<f32>,\n @location(5) fade: f32,\n // Billboard basis (world) for the capsule proxy shading normal.\n @location(6) right_w: vec3<f32>,\n @location(7) up_w: vec3<f32>,\n // Unjittered cur/prev clip positions for the velocity gbuffer output.\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n};\n\n// Mirrors hemi_oct_encode in impostor_math.rs (upper hemisphere, model space Y up).\nfn hemi_oct_encode(d: vec3<f32>) -> vec2<f32> {\n let sum = abs(d.x) + max(d.y, 0.0) + abs(d.z);\n let px = d.x / sum;\n let pz = d.z / sum;\n return vec2<f32>(px + pz, px - pz);\n}\n\n@vertex\nfn vs_main(input: VertexIn) -> VSOut {\n var out: VSOut;\n let model = mat4x4<f32>(input.model_0, input.model_1, input.model_2, input.model_3);\n let axis_scale = max(length(model[0].xyz), max(length(model[1].xyz), length(model[2].xyz)));\n let center = (model * vec4<f32>(u_params.bind_center, 1.0)).xyz;\n out.mesh_color = input.mesh_color;\n out.layer = input.misc1.w;\n\n let to_cam = u_camera.camera_position - center;\n let d = max(length(to_cam), 1e-4);\n // Same projected-size metric as the cull/LOD classify; the fade band overlaps the\n // smallest mesh LOD (which keeps drawing down to imp_full_screen).\n let projection_scale = max(0.5 * u_camera.proj[0][0], 0.5 * u_camera.proj[1][1]);\n let ss = 2.0 * projection_scale * u_params.lod_size_factor * axis_scale / max(d, 1.0);\n if (ss >= u_params.imp_start_screen) {\n out.clip_position = vec4<f32>(2.0, 2.0, 2.0, 1.0); // outside NDC -> clipped\n out.fade = 0.0;\n return out;\n }\n out.fade = clamp(\n (u_params.imp_start_screen - ss)\n / max(u_params.imp_start_screen - u_params.imp_full_screen, 1e-6),\n 0.0, 1.0,\n );\n\n // World view dir -> model space (undo the instance rotation; villagers scale uniformly,\n // so the normalized rotation columns transpose-multiply is exact), clamped to the baked\n // upper hemisphere.\n let r0 = normalize(model[0].xyz);\n let r1 = normalize(model[1].xyz);\n let r2 = normalize(model[2].xyz);\n let dv_w = to_cam / d;\n var dv = vec3<f32>(dot(dv_w, r0), dot(dv_w, r1), dot(dv_w, r2));\n dv = normalize(vec3<f32>(dv.x, max(dv.y, 0.0), dv.z));\n\n // Enclosing grid triangle + barycentric weights (frames live at cell centers).\n let e01 = hemi_oct_encode(dv) * 0.5 + vec2<f32>(0.5);\n let g = clamp(e01 * GRID - 0.5, vec2<f32>(0.0), vec2<f32>(GRID - 1.0));\n let base = min(floor(g), vec2<f32>(GRID - 2.0));\n let f = g - base;\n let bx = u32(base.x);\n let by = u32(base.y);\n let gi = u32(GRID);\n let a = by * gi + bx;\n if (f.x + f.y <= 1.0) {\n out.cells = vec3<u32>(a, a + 1u, a + gi);\n out.weights = vec3<f32>(1.0 - f.x - f.y, f.x, f.y);\n } else {\n out.cells = vec3<u32>(a + gi + 1u, a + gi, a + 1u);\n out.weights = vec3<f32>(f.x + f.y - 1.0, 1.0 - f.x, 1.0 - f.y);\n }\n\n // Billboard basis = the bake camera's right/up for this view dir (model space, mirrors\n // imp_view_basis), rotated back to world by the instance rotation.\n let up_hint = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), dv.y > 0.98);\n let right_m = normalize(cross(up_hint, dv));\n let up_m = cross(dv, right_m);\n let right_w = r0 * right_m.x + r1 * right_m.y + r2 * right_m.z;\n let up_w = r0 * up_m.x + r1 * up_m.y + r2 * up_m.z;\n // Quad size = the bake ortho's full extent, so texel footprint matches the baked frame.\n let size = 2.0 * u_params.imp_half_factor * axis_scale;\n let world_pos = center + right_w * (input.position.x * size)\n + up_w * ((input.position.y - 0.5) * size);\n out.clip_position = u_camera.view_proj * vec4<f32>(world_pos, 1.0);\n // Bake renders +up at texture top -> flip v.\n out.uv = vec2<f32>(input.position.x + 0.5, 1.0 - input.position.y);\n out.right_w = right_w;\n out.up_w = up_w;\n out.cur_clip = u_camera.unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n out.prev_clip = u_camera.prev_unjittered_view_proj * vec4<f32>(world_pos, 1.0);\n return out;\n}\n\nstruct GBufferOutput {\n @location(0) base_color: vec4<f32>,\n @location(1) normal: vec4<f32>,\n @location(2) orm: vec4<f32>,\n @location(3) velocity: vec2<f32>,\n};\n\n// Velocity gbuffer term (`prev_uv - cur_uv`, unjittered; w<=0 => zero for secondary cameras).\nfn gbuffer_velocity(cur_clip: vec4<f32>, prev_clip: vec4<f32>) -> vec2<f32> {\n if (prev_clip.w <= 0.0 || cur_clip.w <= 0.0) {\n return vec2<f32>(0.0, 0.0);\n }\n let cur_ndc = cur_clip.xy / cur_clip.w;\n let prev_ndc = prev_clip.xy / prev_clip.w;\n let cur_uv = vec2<f32>(cur_ndc.x * 0.5 + 0.5, 0.5 - cur_ndc.y * 0.5);\n let prev_uv = vec2<f32>(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);\n return prev_uv - cur_uv;\n}\n\nfn dither4(p: vec2<f32>) -> f32 {\n // 4x4 ordered Bayer, [0,1)\n let x = u32(p.x) % 4u;\n let y = u32(p.y) % 4u;\n let idx = y * 4u + x;\n var bayer = array<f32, 16>(\n 0.0, 8.0, 2.0, 10.0,\n 12.0, 4.0, 14.0, 6.0,\n 3.0, 11.0, 1.0, 9.0,\n 15.0, 7.0, 13.0, 5.0,\n );\n return bayer[idx] / 16.0;\n}\n\nfn sample_cell(cell: u32, uv: vec2<f32>, layer: u32) -> vec4<f32> {\n let gi = u32(GRID);\n let cell_xy = vec2<f32>(f32(cell % gi), f32(cell / gi));\n return textureSample(t_atlas, s_atlas, (cell_xy + uv) / GRID, layer);\n}\n\n@fragment\nfn fs_main(\n @builtin(position) clip_position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n @location(1) mesh_color: vec4<f32>,\n @location(2) @interpolate(flat) layer: u32,\n @location(3) @interpolate(flat) cells: vec3<u32>,\n @location(4) weights: vec3<f32>,\n @location(5) fade: f32,\n @location(6) right_w: vec3<f32>,\n @location(7) up_w: vec3<f32>,\n @location(8) cur_clip: vec4<f32>,\n @location(9) prev_clip: vec4<f32>,\n) -> GBufferOutput {\n // Alpha-weighted 3-frame blend: coverage from the blended alpha, color premultiplied so\n // frames with no coverage at this texel don't darken the result.\n let t0 = sample_cell(cells.x, uv, layer);\n let t1 = sample_cell(cells.y, uv, layer);\n let t2 = sample_cell(cells.z, uv, layer);\n let cov = t0.a * weights.x + t1.a * weights.y + t2.a * weights.z;\n if (cov < 0.35) { discard; }\n if (fade < dither4(clip_position.xy)) { discard; }\n let rgb = (t0.rgb * t0.a * weights.x + t1.rgb * t1.a * weights.y + t2.rgb * t2.a * weights.z) / cov;\n\n var out: GBufferOutput;\n out.base_color = vec4<f32>(rgb * mesh_color.rgb, 1.0);\n // Capsule proxy shading normal: treat the character as its bounding ellipsoid (radii\n // from the mesh proportions, quad-relative) and take the front-surface normal at this\n // texel; outside the ellipse clamp to the in-plane rim direction.\n let lq = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0); // quad-local, +y up\n let rx = max(u_params.imp_ell_x, 0.05);\n let ry = max(u_params.imp_ell_y, 0.05);\n let q = 1.0 - (lq.x * lq.x) / (rx * rx) - (lq.y * lq.y) / (ry * ry);\n let fwd = normalize(cross(right_w, up_w)); // toward camera (basis is orthonormal)\n let n = normalize(right_w * (lq.x / (rx * rx)) + up_w * (lq.y / (ry * ry)) + fwd * (sqrt(max(q, 0.0)) / rx));\n out.normal = vec4<f32>(n, 0.0);\n out.orm = vec4<f32>(1.0, 0.85, 0.0, 0.0);\n out.velocity = gbuffer_velocity(cur_clip, prev_clip);\n return out;\n}\n"},{"label":"shaders/walker_follow_patch.wgsl","code":"// Walker-follow patch: evaluates each instance slot's current path segment at the\n// display tick and overwrites the instance buffer's model matrix in place, before\n// the skinned Hi-Z cull and every draw pass read it. The end-of-encoder prev\n// snapshot then captures these patched matrices, so the MV pass's prev-instance\n// fetch returns evaluated previous-frame positions for free (exact walker MVs,\n// including impostor-routed ones).\n// Position math mirrors oriverse_world tile_path_follow_sample exactly \u2014 see the\n// parity test in walker_follow_gpu.rs.\n\nstruct FollowRec {\n // c0..c2: basis columns of the final model matrix at this segment's target\n // facing; w components = the segment-start translation column (render m).\n c0: vec4<f32>,\n c1: vec4<f32>,\n c2: vec4<f32>,\n // xyz = segment-target translation column (render m), w = signed yaw turn of\n // this segment vs the previous facing (radians; blended over TURN_TICKS).\n v3: vec4<f32>,\n // x = seg start tick, y = seg arrive tick (TICK_HOLD = hold at start pos),\n // z = flags (bit0 active), w = move per tick (render m, f32 bits).\n t: vec4<u32>,\n};\n\nstruct Params {\n tick: u32,\n count: u32,\n _pad0: u32,\n _pad1: u32,\n};\n\nstruct Inst {\n model: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission: vec4<f32>,\n misc0: vec4<u32>,\n misc1: vec4<u32>,\n aim_pitch_quat: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u: Params;\n@group(0) @binding(1) var<storage, read> recs: array<FollowRec>;\n@group(0) @binding(2) var<storage, read_write> insts: array<Inst>;\n\nconst TURN_TICKS: f32 = 9.0; // ~0.15 s at 60 ticks/s\nconst TICK_HOLD: u32 = 0xffffffffu;\n\nfn yaw_rotate(c: vec3<f32>, ca: f32, sa: f32) -> vec3<f32> {\n // Render-space Y-up rotation; a sim Z-up rotation of the same signed angle.\n return vec3<f32>(c.x * ca + c.z * sa, c.y, -c.x * sa + c.z * ca);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= u.count) {\n return;\n }\n let r = recs[i];\n if ((r.t.z & 1u) == 0u) {\n return;\n }\n let seg_from = vec3<f32>(r.c0.w, r.c1.w, r.c2.w);\n let seg_target = r.v3.xyz;\n let t0 = r.t.x;\n let t1 = r.t.y;\n // Integer tick delta first (u32-precision safe), then small-float math.\n var dt = 0u;\n if (u.tick > t0) {\n dt = u.tick - t0;\n }\n var pos = seg_from;\n if (t1 != TICK_HOLD && dt > 0u) {\n if (u.tick >= t1) {\n pos = seg_target;\n } else {\n let d = seg_target - seg_from;\n let dist = length(d);\n let mv = bitcast<f32>(r.t.w) * f32(dt);\n pos = seg_from + (d / max(dist, 1e-6)) * min(mv, dist);\n }\n }\n // Turn blend: the basis is stored at the segment's final facing; rotate it back\n // by the unfinished part of the turn. The mesh-center offset baked into the\n // translation columns is vertical (render Y), which this yaw leaves invariant,\n // so lerping translations while blending the basis stays exact.\n let s = clamp(f32(min(dt, u32(TURN_TICKS))), 0.0, TURN_TICKS) / TURN_TICKS;\n let a = (s - 1.0) * r.v3.w;\n let ca = cos(a);\n let sa = sin(a);\n insts[i].model = mat4x4<f32>(\n vec4<f32>(yaw_rotate(r.c0.xyz, ca, sa), 0.0),\n vec4<f32>(yaw_rotate(r.c1.xyz, ca, sa), 0.0),\n vec4<f32>(yaw_rotate(r.c2.xyz, ca, sa), 0.0),\n vec4<f32>(pos, 1.0),\n );\n}\n"},{"label":"skinned_hiz_cull","code":"// Shared frustum + Hi-Z occlusion helpers, prepended via include_str! concat to consumer\n// shaders (static_lod_global_prefix_classify.wgsl, skinned_hiz_cull.wgsl). Consumers must\n// declare the module-scope bindings `camera_data: CameraUniform` and\n// `depth_pyramid: texture_2d<f32>` (WGSL module-scope declarations are order-independent).\n\nstruct CameraUniform {\n view_proj: mat4x4<f32>,\n inverse_view_proj: mat4x4<f32>,\n inverse_proj: mat4x4<f32>,\n view: mat4x4<f32>,\n proj: mat4x4<f32>,\n camera_position: vec3<f32>,\n time_seconds: f32,\n};\n\nfn normalize_plane(p: vec4<f32>) -> vec4<f32> {\n let n = p.xyz;\n let inv_len = inverseSqrt(max(dot(n, n), 1e-12));\n return p * inv_len;\n}\n\nfn aabb_outside_plane(plane: vec4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let n = plane.xyz;\n let px = select(bmin.x, bmax.x, n.x >= 0.0);\n let py = select(bmin.y, bmax.y, n.y >= 0.0);\n let pz = select(bmin.z, bmax.z, n.z >= 0.0);\n return dot(n, vec3<f32>(px, py, pz)) + plane.w < 0.0;\n}\n\n// Frustum test against an arbitrary view_proj (perspective or ortho \u2014 e.g. a shadow\n// cascade's light volume); plane extraction is form-agnostic.\nfn aabb_visible_vp(m: mat4x4<f32>, bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n let r0 = vec4<f32>(m[0][0], m[1][0], m[2][0], m[3][0]);\n let r1 = vec4<f32>(m[0][1], m[1][1], m[2][1], m[3][1]);\n let r2 = vec4<f32>(m[0][2], m[1][2], m[2][2], m[3][2]);\n let r3 = vec4<f32>(m[0][3], m[1][3], m[2][3], m[3][3]);\n let planes = array<vec4<f32>, 6>(\n normalize_plane(r3 + r0),\n normalize_plane(r3 - r0),\n normalize_plane(r3 + r1),\n normalize_plane(r3 - r1),\n normalize_plane(r2),\n normalize_plane(r3 - r2),\n );\n for (var i = 0u; i < 6u; i = i + 1u) {\n if (aabb_outside_plane(planes[i], bmin, bmax)) { return false; }\n }\n return true;\n}\n\nfn aabb_visible(bmin: vec3<f32>, bmax: vec3<f32>) -> bool {\n return aabb_visible_vp(camera_data.view_proj, bmin, bmax);\n}\n\n// Hi-Z occlusion test against the last-frame MAX-depth pyramid ([0,1] depth, 0 = near, 1 = far).\n// Every coarse texel over-estimates occluder depth and `margin_m` pulls the instance toward the\n// camera before comparing, so all uncertainty (one frame of motion, TAA jitter, near-plane or\n// behind-camera corners, ortho cameras) resolves to \"visible\".\nfn hiz_visible(bmin: vec3<f32>, bmax: vec3<f32>, margin_m: f32) -> bool {\n // Margin math below assumes a perspective projection (clip.w = view depth).\n if (camera_data.proj[3][3] != 0.0) { return true; }\n var min_ndc = vec2<f32>(1e10, 1e10);\n var max_ndc = vec2<f32>(-1e10, -1e10);\n var nearest_z = 1e10;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let corner = vec3<f32>(\n select(bmin.x, bmax.x, (i & 1u) != 0u),\n select(bmin.y, bmax.y, (i & 2u) != 0u),\n select(bmin.z, bmax.z, (i & 4u) != 0u),\n );\n let clip = camera_data.view_proj * vec4<f32>(corner, 1.0);\n // Corner at/behind the camera plane, or within `margin_m` of it: depth is meaningless\n // there, keep visible (this also force-passes near-plane intersections).\n let w_adj = clip.w - margin_m;\n if (w_adj <= 1e-4) { return true; }\n // Pull the corner margin_m meters toward the camera along view depth:\n // clip.z = -c22*w + c32 => z(w - m) = clip.z + c22*m.\n let z_adj = clip.z + camera_data.proj[2][2] * margin_m;\n nearest_z = min(nearest_z, z_adj / w_adj);\n let ndc = clip.xy / clip.w;\n min_ndc = min(min_ndc, ndc);\n max_ndc = max(max_ndc, ndc);\n }\n if (nearest_z <= 0.0) { return true; }\n\n // NDC -> UV: y flips, so the y min/max swap sides (max_ndc.y becomes min_uv.y).\n var min_uv = vec2<f32>(min_ndc.x * 0.5 + 0.5, 0.5 - max_ndc.y * 0.5);\n var max_uv = vec2<f32>(max_ndc.x * 0.5 + 0.5, 0.5 - min_ndc.y * 0.5);\n min_uv = clamp(min_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n max_uv = clamp(max_uv, vec2<f32>(0.0), vec2<f32>(0.9999));\n\n // Pyramid mip0 is half the scene resolution (first reduction happens while reading live\n // depth); all footprint math below is in pyramid-texel space, so that only shifts every\n // selection one level coarser in screen terms (minimum granularity 2 screen px).\n let pyr_res = textureDimensions(depth_pyramid);\n let size_uv = max_uv - min_uv;\n let size_px = max(size_uv.x * f32(pyr_res.x), size_uv.y * f32(pyr_res.y));\n let num_mips = textureNumLevels(depth_pyramid);\n // Pick the mip where the footprint spans <= 2 texels per axis so the 2x2 gather covers it;\n // bump once if misalignment still crosses a third texel (guaranteed enough at half size).\n var mip = u32(clamp(ceil(log2((size_px + 1e-6) / 2.0)), 0.0, f32(num_mips - 1u)));\n var mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n var px00 = vec2<i32>(min_uv * mip_size);\n var px11 = vec2<i32>(max_uv * mip_size);\n if (px11.x > px00.x + 1 || px11.y > px00.y + 1) {\n mip = min(mip + 1u, num_mips - 1u);\n mip_size = vec2<f32>(vec2<u32>(max(pyr_res.x >> mip, 1u), max(pyr_res.y >> mip, 1u)));\n px00 = vec2<i32>(min_uv * mip_size);\n px11 = vec2<i32>(max_uv * mip_size);\n }\n let d00 = textureLoad(depth_pyramid, px00, i32(mip)).r;\n let d01 = textureLoad(depth_pyramid, vec2<i32>(px00.x, px11.y), i32(mip)).r;\n let d10 = textureLoad(depth_pyramid, vec2<i32>(px11.x, px00.y), i32(mip)).r;\n let d11 = textureLoad(depth_pyramid, px11, i32(mip)).r;\n let occluder_depth = max(max(d00, d01), max(d10, d11));\n return nearest_z <= occluder_depth;\n}\n\n// Skinned-instance frustum + Hi-Z cull with LOD bucketing (one dispatch per submesh group).\n// Loaded with shaders/hiz_shared.wgsl prepended (CameraUniform, aabb_visible, hiz_visible).\n//\n// Bounds are the reflection-cull sphere (planar_reflection_pass::skinned_instance_sphere):\n// raw mesh-space extents are unreliable for skinned meshes (bone palettes bake the armature\n// scale), so center = model * bind_center and radius = radius_factor * max_axis_scale(model)\n// with radius_factor = half_diag(json_y_up_bb) / tail_scale * anim_slack precomputed on CPU.\n//\n// Visible instances pick a LOD bucket from projected sphere size (same thresholds/metric as the\n// static classify; lod_size_factor drops the anim slack so LOD sizing tracks the real\n// silhouette), remap through bucket_map (CPU-deduped chain of available LOD variants, identity\n// [0,0,0,0] when the mesh has no usable variants), and compact by atomicAdd on that slot's\n// indirect instance_count into region `slot * capacity` of the compacted buffer. The CPU\n// pre-writes each slot's indirect index range to the matching LOD variant geometry (all skinned\n// LODs share the global vertex/index buffers). The 144-byte record is copied verbatim\n// (palette_offset inside it still indexes the unmoved palette buffer, so palettes need no\n// compaction and drive every LOD, exactly like the far-shadow-cascade LOD overrides).\n\n// Mirrors render_base::SkinnedInstanceData (144 bytes).\nstruct SkinnedInst {\n model: mat4x4<f32>,\n mesh_color: vec4<f32>,\n emission: vec4<f32>,\n misc0: vec4<u32>, // flags, clip_id, skin_id, palette_offset\n misc1: vec4<u32>, // percent(f32 bits), upper_clip, upper_percent(f32 bits), pad\n // (compacted copies re-use .w: LOD regions = original instance\n // index, impostor region = atlas layer)\n aim_pitch_quat: vec4<f32>,\n};\n\nstruct SkinnedCullParams {\n instance_count: u32,\n capacity: u32, // compacted-region stride (instances per LOD slot)\n radius_factor: f32, // half_diag(json_bb)/tail_scale * anim_slack\n margin_m: f32,\n bind_center: vec3<f32>, // bind-space bb center (raw bb through the mesh node transform)\n lod_size_factor: f32, // radius_factor without anim slack (real silhouette for LOD sizing)\n lod1_screen_size: f32,\n lod2_screen_size: f32,\n lod3_screen_size: f32,\n hiz_enabled: u32, // 0 = frustum + LOD bucketing only (no valid depth pyramid this frame)\n bucket_map: vec4<u32>, // screen-size bucket -> compaction slot\n // Impostor routing (skinned_impostor.rs): 0 = off, 1 = cull-only (sibling submesh of an\n // impostor-owning mesh: drop sub-threshold instances, the owner draws the whole\n // character), 2 = owner (route sub-threshold instances into the impostor region).\n imp_mode: u32,\n imp_region: u32, // compacted-region index of the impostor slot (owner only)\n imp_layer: u32, // atlas layer for this mesh, stamped into routed records' misc1.w\n _imp_pad: u32,\n imp_start_screen: f32, // impostor fade-in starts (mesh + dithered impostor overlap)\n imp_full_screen: f32, // mesh stops; impostor fully opaque\n imp_half_factor: f32, // bake ortho half-extent factor (impostor draw sizes quads with it)\n imp_ell_x: f32, // capsule proxy radii (quad-relative), impostor fs only\n imp_ell_y: f32,\n _imp_pad1: f32,\n _imp_pad2: f32,\n _imp_pad3: f32,\n // Cascade-0 shadow caster routing (skinned casters intersecting the near cascade's\n // light volume; independent of camera visibility so off-screen casters keep casting).\n // shadow_mode 0 disables (blend-only submesh / shadows off / no valid volume).\n cascade0_view_proj: mat4x4<f32>,\n shadow_mode: u32,\n shadow_hi_region: u32,\n shadow_proxy_region: u32,\n _shadow_pad0: u32,\n shadow_hi_screen: f32, // caster screen size at/above which cascade 0 draws hi geometry\n _shadow_pad1: f32,\n _shadow_pad2: f32,\n _shadow_pad3: f32,\n // Cascade-1 caster routing: same volume test one slice out, minus the hi split (every\n // caster draws the stand-in). Sub-box-px casters of box-carrying skins use the per-skin\n // 36-idx box instead: box_mode 2 = owner (route into box region), 1 = box owned by a\n // sibling submesh (drop; the box spans the whole skin's silhouette), 0 = no box.\n cascade1_view_proj: mat4x4<f32>,\n shadow_c1_mode: u32,\n shadow_c1_region: u32,\n shadow_c1_box_mode: u32,\n shadow_c1_box_region: u32,\n shadow_c1_box_screen: f32, // caster screen size below which the box replaces the stand-in\n _shadow_c1_pad0: f32,\n _shadow_c1_pad1: f32,\n _shadow_c1_pad2: f32,\n};\n\nstruct DrawIndexedIndirect {\n index_count: u32,\n instance_count: atomic<u32>,\n first_index: u32,\n base_vertex: i32,\n first_instance: u32,\n};\n\n// Indirect entries past the LOD slots: impostor quad draw, the cascade-0 shadow draws,\n// then the cascade-1 stand-in + box draws.\nconst IMP_ARG_INDEX: u32 = 4u;\nconst SHADOW_HI_ARG_INDEX: u32 = 5u;\nconst SHADOW_PROXY_ARG_INDEX: u32 = 6u;\nconst SHADOW_C1_ARG_INDEX: u32 = 7u;\nconst SHADOW_C1_BOX_ARG_INDEX: u32 = 8u;\n\n@group(0) @binding(0) var<uniform> camera_data: CameraUniform;\n@group(0) @binding(1) var<uniform> params: SkinnedCullParams;\n@group(0) @binding(2) var<storage, read> instances: array<SkinnedInst>;\n@group(0) @binding(3) var<storage, read_write> compacted: array<SkinnedInst>;\n@group(0) @binding(4) var<storage, read_write> indirect: array<DrawIndexedIndirect, 9>;\n@group(0) @binding(5) var depth_pyramid: texture_2d<f32>;\n\nfn lod_bucket(screen_size: f32) -> u32 {\n if (screen_size <= params.lod3_screen_size) { return 3u; }\n if (screen_size <= params.lod2_screen_size) { return 2u; }\n if (screen_size <= params.lod1_screen_size) { return 1u; }\n return 0u;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= params.instance_count) { return; }\n let inst = instances[i];\n let m = inst.model;\n let center = (m * vec4<f32>(params.bind_center, 1.0)).xyz;\n let axis_scale = max(length(m[0].xyz), max(length(m[1].xyz), length(m[2].xyz)));\n let radius = params.radius_factor * axis_scale;\n let bmin = center - vec3<f32>(radius);\n let bmax = center + vec3<f32>(radius);\n // Same projected-size metric as the static classify's sphere_screen_size (used by both\n // the color LOD buckets and the cascade-0 hi/proxy caster split).\n let projection_scale = max(0.5 * camera_data.proj[0][0], 0.5 * camera_data.proj[1][1]);\n let distance_m = max(distance(camera_data.camera_position, center), 1.0);\n let screen_size = 2.0 * projection_scale * params.lod_size_factor * axis_scale / distance_m;\n\n // Cascade-0 shadow caster routing, independent of camera visibility: an off-screen\n // caster inside the near cascade's light volume still shadows visible ground. Big\n // casters keep hi geometry (sharp near shadows); the rest use the proxy region.\n if (params.shadow_mode != 0u && (inst.misc0.x & 1u) == 0u\n && aabb_visible_vp(params.cascade0_view_proj, bmin, bmax)) {\n if (screen_size >= params.shadow_hi_screen) {\n let idx = atomicAdd(&indirect[SHADOW_HI_ARG_INDEX].instance_count, 1u);\n compacted[params.shadow_hi_region * params.capacity + idx] = inst;\n } else {\n let idx = atomicAdd(&indirect[SHADOW_PROXY_ARG_INDEX].instance_count, 1u);\n compacted[params.shadow_proxy_region * params.capacity + idx] = inst;\n }\n }\n\n // Cascade-1 caster routing (previously c1 drew the stand-in over the FULL instance\n // list with zero culling \u2014 the dominant remaining crowd-shadow term).\n if (params.shadow_c1_mode != 0u && (inst.misc0.x & 1u) == 0u\n && aabb_visible_vp(params.cascade1_view_proj, bmin, bmax)) {\n if (params.shadow_c1_box_mode != 0u && screen_size < params.shadow_c1_box_screen) {\n if (params.shadow_c1_box_mode == 2u) {\n let idx = atomicAdd(&indirect[SHADOW_C1_BOX_ARG_INDEX].instance_count, 1u);\n compacted[params.shadow_c1_box_region * params.capacity + idx] = inst;\n }\n } else {\n let idx = atomicAdd(&indirect[SHADOW_C1_ARG_INDEX].instance_count, 1u);\n compacted[params.shadow_c1_region * params.capacity + idx] = inst;\n }\n }\n\n if (aabb_visible(bmin, bmax)\n && (params.hiz_enabled == 0u || hiz_visible(bmin, bmax, params.margin_m))) {\n // Impostor routing: below the fade band the character leaves the skinned system\n // entirely (owner group emits the billboard record; sibling submeshes just drop).\n // Inside the band the mesh keeps drawing while the owner also emits a dithered\n // impostor copy, so the handoff crossfades instead of popping.\n if (params.imp_mode != 0u && screen_size < params.imp_start_screen) {\n if (params.imp_mode == 2u) {\n var rec = inst;\n rec.misc1.w = params.imp_layer;\n let iidx = atomicAdd(&indirect[IMP_ARG_INDEX].instance_count, 1u);\n compacted[params.imp_region * params.capacity + iidx] = rec;\n }\n if (screen_size < params.imp_full_screen) {\n return;\n }\n }\n // LOD records carry their original instance index in misc1.w (src_index in the\n // source, same value): compaction reorders per frame, and the gbuffer skinned VS\n // fetches the prev-frame velocity snapshot (stored in original order) by this index.\n var lod_rec = inst;\n lod_rec.misc1.w = i;\n let slot = params.bucket_map[lod_bucket(screen_size)];\n let idx = atomicAdd(&indirect[slot].instance_count, 1u);\n compacted[slot * params.capacity + idx] = lod_rec;\n }\n}\n"},{"label":"shaders/taau.wgsl","code":"// taau.wgsl\n// Temporal AA + upscale (TSR/STP-lite), the wasm/mobile-safe counterpart of FSR3: one fragment\n// pass at upscale resolution, zero storage bindings. Inputs are the jittered post-tonemap\n// scene-res color, scene depth and the motion-vector texture (prev_uv - cur_uv, camera base +\n// per-object overwrites). History is upscale-res, reprojected with 9-tap Catmull-Rom through the\n// closest-depth-dilated motion vector and variance-clipped to the current 3x3 neighborhood.\n\nstruct VsOut {\n @builtin(position) pos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@vertex\nfn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {\n var o: VsOut;\n let px = f32((vid << 1u) & 2u);\n let py = f32(vid & 2u);\n o.uv = vec2<f32>(px, py);\n o.pos = vec4<f32>(o.uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);\n return o;\n}\n\nstruct TaauUniform {\n render_size: vec2<f32>,\n inv_render_size: vec2<f32>,\n upscale_size: vec2<f32>,\n inv_upscale_size: vec2<f32>,\n // World point at unjittered uv appears at uv + jitter_uv in this frame's input color.\n jitter_uv: vec2<f32>,\n history_blend: f32, // base current-frame weight; >= 1.0 discards history (reset)\n _pad: f32,\n};\n\n@group(0) @binding(0) var s_linear: sampler;\n@group(0) @binding(1) var t_color: texture_2d<f32>; // scene res, jittered, post-tonemap\n@group(0) @binding(2) var t_history: texture_2d<f32>; // upscale res, previous output\n@group(0) @binding(3) var t_mv: texture_2d<f32>; // scene res, prev_uv - cur_uv\n@group(0) @binding(4) var t_depth: texture_depth_2d; // scene res\n@group(0) @binding(5) var<uniform> u: TaauUniform;\n@group(0) @binding(6) var t_reactive: texture_2d<f32>; // scene res R8: particles/water reactivity\n\n// 9-tap Catmull-Rom in 5 bilinear fetches worth of positions (Jimenez); avoids the cumulative\n// blur of plain bilinear history resampling. Weights sum to 1; result clamped (CR overshoots).\nfn sample_history(uv: vec2<f32>) -> vec3<f32> {\n let sample_pos = uv * u.upscale_size;\n let tex_pos1 = floor(sample_pos - 0.5) + 0.5;\n let f = sample_pos - tex_pos1;\n let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));\n let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);\n let w2 = f * (0.5 + f * (2.0 - 1.5 * f));\n let w3 = f * f * (-0.5 + 0.5 * f);\n let w12 = w1 + w2;\n let p0 = (tex_pos1 - 1.0) * u.inv_upscale_size;\n let p3 = (tex_pos1 + 2.0) * u.inv_upscale_size;\n let p12 = (tex_pos1 + w2 / w12) * u.inv_upscale_size;\n // 5-fetch variant (Jimenez): the 4 corner terms carry ~2% of the energy \u2014 drop them and\n // renormalize. Visually equivalent, 4 fewer bilinear fetches per pixel.\n var r = vec3<f32>(0.0);\n r += textureSampleLevel(t_history, s_linear, vec2<f32>(p12.x, p0.y), 0.0).rgb * w12.x * w0.y;\n r += textureSampleLevel(t_history, s_linear, vec2<f32>(p0.x, p12.y), 0.0).rgb * w0.x * w12.y;\n r += textureSampleLevel(t_history, s_linear, vec2<f32>(p12.x, p12.y), 0.0).rgb * w12.x * w12.y;\n r += textureSampleLevel(t_history, s_linear, vec2<f32>(p3.x, p12.y), 0.0).rgb * w3.x * w12.y;\n r += textureSampleLevel(t_history, s_linear, vec2<f32>(p12.x, p3.y), 0.0).rgb * w12.x * w3.y;\n let wsum = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y;\n return max(r / max(wsum, 1e-5), vec3<f32>(0.0));\n}\n\n// Clip toward the neighborhood box center (Playdead-style) instead of hard clamping: keeps some\n// history direction, kills ghosting outside the current neighborhood.\nfn clip_aabb(lo: vec3<f32>, hi: vec3<f32>, c: vec3<f32>) -> vec3<f32> {\n let center = 0.5 * (hi + lo);\n let extents = max(0.5 * (hi - lo), vec3<f32>(1e-4));\n let d = c - center;\n let t = abs(d / extents);\n let m = max(t.x, max(t.y, t.z));\n if (m > 1.0) {\n return center + d / m;\n }\n return c;\n}\n\n@fragment\nfn fs_main(in: VsOut) -> @location(0) vec4<f32> {\n let uv = in.uv;\n let src_uv = uv + u.jitter_uv;\n let cur = textureSampleLevel(t_color, s_linear, src_uv, 0.0).rgb;\n\n // 3x3 render-res neighborhood: color moments for variance clipping + closest-depth texel for\n // motion-vector dilation (edge pixels take the foreground object's motion).\n let src_px = src_uv * u.render_size - 0.5;\n let base = vec2<i32>(floor(src_px + 0.5));\n let max_px = vec2<i32>(u.render_size) - 1;\n var m1 = vec3<f32>(0.0);\n var m2 = vec3<f32>(0.0);\n var best_depth = 1.0e9;\n var best_px = clamp(base, vec2<i32>(0), max_px);\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let p = clamp(base + vec2<i32>(dx, dy), vec2<i32>(0), max_px);\n let c = textureLoad(t_color, p, 0).rgb;\n m1 += c;\n m2 += c * c;\n let d = textureLoad(t_depth, p, 0);\n if (d < best_depth) {\n best_depth = d;\n best_px = p;\n }\n }\n }\n let mean = m1 / 9.0;\n let sigma = sqrt(max(m2 / 9.0 - mean * mean, vec3<f32>(0.0)));\n\n let mv = textureLoad(t_mv, best_px, 0).xy;\n let prev_uv = uv + mv;\n if (u.history_blend >= 1.0\n || prev_uv.x < 0.0 || prev_uv.x > 1.0 || prev_uv.y < 0.0 || prev_uv.y > 1.0) {\n return vec4<f32>(cur, 1.0);\n }\n\n let gamma = 1.1; // variance-clip width: lower = less ghosting, more flicker\n var hist = sample_history(prev_uv);\n hist = clip_aabb(mean - gamma * sigma, mean + gamma * sigma, hist);\n\n // Current-sample confidence: when upscaling, the nearest jittered input sample can be up to\n // ~0.7 render texels away from this output pixel; lean on history when it is (STP-style).\n let f_off = src_px - vec2<f32>(base);\n // Reactive mask (particles/water): fragment-animated content has no motion vectors, so its\n // history is misprojected -> raise the current-frame weight instead of trusting reprojection.\n let reactive = textureLoad(t_reactive, clamp(base, vec2<i32>(0), max_px), 0).r;\n let alpha = max(u.history_blend * exp2(-3.0 * dot(f_off, f_off)), reactive);\n return vec4<f32>(mix(hist, cur, alpha), 1.0);\n}\n\n// RCAS sharpen (port of FFX FsrRcasF, no denoise): the final blit while TAAU is active. Reads the\n// accumulated output 1:1 (post-tonemap gamma space, as FSR1's RCAS expects) and writes the\n// sharpened result straight to the swapchain. Runs after the history copy, so sharpening never\n// compounds through history (same ordering as FSR3's built-in RCAS).\nconst RCAS_LIMIT: f32 = 0.1875; // 0.25 - 1/16\n// exp2(-stops), stops = 2 - 2*sharpness; sharpness 0.4 matches the FSR3 path (fsr3_pass.rs).\nconst RCAS_CON: f32 = 0.43528;\n\n@group(0) @binding(0) var t_rcas_in: texture_2d<f32>;\n\n@fragment\nfn fs_rcas(in: VsOut) -> @location(0) vec4<f32> {\n // 5-tap cross: b (e = center)\n // d e f\n // h\n let max_px = vec2<i32>(textureDimensions(t_rcas_in)) - 1;\n let sp = clamp(vec2<i32>(in.pos.xy), vec2<i32>(0), max_px);\n let b = textureLoad(t_rcas_in, clamp(sp + vec2<i32>(0, -1), vec2<i32>(0), max_px), 0).rgb;\n let d = textureLoad(t_rcas_in, clamp(sp + vec2<i32>(-1, 0), vec2<i32>(0), max_px), 0).rgb;\n let e = textureLoad(t_rcas_in, sp, 0).rgb;\n let f = textureLoad(t_rcas_in, clamp(sp + vec2<i32>(1, 0), vec2<i32>(0), max_px), 0).rgb;\n let h = textureLoad(t_rcas_in, clamp(sp + vec2<i32>(0, 1), vec2<i32>(0), max_px), 0).rgb;\n let mn4 = min(min(b, d), min(f, h));\n let mx4 = max(max(b, d), max(f, h));\n // Per-channel lobe limiters so no channel clips below 0 or above 1. Epsilon clamps keep the\n // denominators sign-safe on flat black/white (FFX relies on rcp() inf semantics there).\n let hit_min = min(mn4, e) / max(4.0 * mx4, vec3<f32>(1e-4));\n let hit_max = (vec3<f32>(1.0) - max(mx4, e)) / min(4.0 * mn4 - 4.0, vec3<f32>(-1e-4));\n let lobe_rgb = max(-hit_min, hit_max);\n // Negative lobe = sharpening strength; RCAS_LIMIT bounds the ring weight.\n let lobe = max(-RCAS_LIMIT, min(max(lobe_rgb.r, max(lobe_rgb.g, lobe_rgb.b)), 0.0)) * RCAS_CON;\n return vec4<f32>((lobe * (b + d + f + h) + e) / (4.0 * lobe + 1.0), 1.0);\n}\n"}],"device":{"requiredLimits":{"maxTextureDimension1D":8192,"maxTextureDimension2D":8192,"maxTextureDimension3D":2048,"maxTextureArrayLayers":256,"maxBindGroups":4,"maxBindingsPerBindGroup":1000,"maxDynamicUniformBuffersPerPipelineLayout":8,"maxDynamicStorageBuffersPerPipelineLayout":4,"maxSampledTexturesPerShaderStage":22,"maxSamplersPerShaderStage":16,"maxStorageBuffersPerShaderStage":9,"maxStorageTexturesPerShaderStage":4,"maxUniformBuffersPerShaderStage":12,"maxUniformBufferBindingSize":65536,"maxStorageBufferBindingSize":134217728,"minUniformBufferOffsetAlignment":256,"minStorageBufferOffsetAlignment":256,"maxVertexBuffers":8,"maxBufferSize":268435456,"maxVertexAttributes":16,"maxVertexBufferArrayStride":2048,"maxInterStageShaderVariables":16,"maxColorAttachments":8,"maxColorAttachmentBytesPerSample":32,"maxComputeWorkgroupStorageSize":16384,"maxComputeInvocationsPerWorkgroup":256,"maxComputeWorkgroupSizeX":256,"maxComputeWorkgroupSizeY":256,"maxComputeWorkgroupSizeZ":64,"maxComputeWorkgroupsPerDimension":65535},"requiredFeatures":["texture-compression-bc","timestamp-query","rg11b10ufloat-renderable"]},"pipelines":[{"kind":"compute","desc":{"layout":{"__layout":0},"compute":{"module":{"__mod":0},"entryPoint":"main"},"label":"BoneCompute Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":0},"compute":{"module":{"__mod":0},"entryPoint":"main_levels"},"label":"BoneCompute Levels Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen T LUT","fragment":{"module":{"__mod":1},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_transmittance_lut"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen MS LUT","fragment":{"module":{"__mod":1},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_ms_lut"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen BRDF LUT","fragment":{"module":{"__mod":1},"targets":[{"format":"rg8unorm","writeMask":15}],"entryPoint":"fs_brdf_lut"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen Sky Face","fragment":{"module":{"__mod":1},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_sky_face"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen Irradiance","fragment":{"module":{"__mod":1},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_irradiance"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":1},"vertex":{"module":{"__mod":1},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SkyGen Prefilter","fragment":{"module":{"__mod":1},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_prefilter"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":2},"vertex":{"module":{"__mod":2},"entryPoint":"vs_main","buffers":[]},"label":"Skybox Pipeline","fragment":{"module":{"__mod":2},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":2},"vertex":{"module":{"__mod":2},"entryPoint":"vs_main","buffers":[]},"label":"Skybox Pipeline (depth-tested)","depthStencil":{"format":"depth32float","depthCompare":"equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":2},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":3},"compute":{"module":{"__mod":3},"entryPoint":"cs_points"},"label":"Cluster Build Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":3},"compute":{"module":{"__mod":3},"entryPoint":"cs_spots"},"label":"Cluster Build Spots"}},{"kind":"compute","desc":{"layout":{"__layout":3},"compute":{"module":{"__mod":3},"entryPoint":"cs_areas"},"label":"Cluster Build Areas"}},{"kind":"compute","desc":{"layout":{"__layout":3},"compute":{"module":{"__mod":3},"entryPoint":"cs_gather"},"label":"Cluster Build Gather"}},{"kind":"render","desc":{"layout":{"__layout":4},"vertex":{"module":{"__mod":4},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"LocalShadow Pipeline (static)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":5},"vertex":{"module":{"__mod":5},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"LocalShadow Pipeline (skinned)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":6},"vertex":{"module":{"__mod":6},"entryPoint":"vs_fullscreen","buffers":[]},"label":"LocalShadow Rect Clear","depthStencil":{"format":"depth32float","depthCompare":"always","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":6},"targets":[],"entryPoint":"fs_depth1"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":7},"compute":{"module":{"__mod":7},"entryPoint":"main"},"label":"StaticLodDebug Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":8},"compute":{"module":{"__mod":8},"entryPoint":"main"},"label":"StaticLodGlobalPrefixClassify Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":9},"compute":{"module":{"__mod":9},"entryPoint":"main"},"label":"StaticLodGlobalPrefixWriteCounts Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":10},"compute":{"module":{"__mod":10},"entryPoint":"main"},"label":"StaticLodGlobalPrefixScatterPacked Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":11},"compute":{"module":{"__mod":11},"entryPoint":"main"},"label":"StaticLodScatterIndirect Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":12},"compute":{"module":{"__mod":12},"entryPoint":"main"},"label":"StaticLodShadowClassify Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":13},"compute":{"module":{"__mod":13},"entryPoint":"main"},"label":"StaticLodShadowScatter Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":14},"compute":{"module":{"__mod":14},"entryPoint":"main"},"label":"StaticLodShadowWriteIndirect Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":15},"vertex":{"module":{"__mod":15},"entryPoint":"vs_main","buffers":[{"arrayStride":12,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0}],"stepMode":"vertex"}]},"label":"StaticLodShadow Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":16},"vertex":{"module":{"__mod":16},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"}]},"label":"StaticLodShadow Alpha Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":16},"targets":[],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":17},"vertex":{"module":{"__mod":17},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"}]},"label":"StaticLod DepthPrepass Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"local_prefix_sum"},"label":"StaticLodGlobal/local_prefix_sum"}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"prefix_sum_partial_sums_1"},"label":"StaticLodGlobal/prefix_sum_partial_sums_1"}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"prefix_sum_partial_sums_2"},"label":"StaticLodGlobal/prefix_sum_partial_sums_2"}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"prefix_sum_partial_sums_3"},"label":"StaticLodGlobal/prefix_sum_partial_sums_3"}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"prefix_sum_partial_sums_4"},"label":"StaticLodGlobal/prefix_sum_partial_sums_4"}},{"kind":"compute","desc":{"layout":{"__layout":18},"compute":{"module":{"__mod":18},"entryPoint":"add_partial_sums"},"label":"StaticLodGlobal/add_partial_sums"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"local_prefix_sum"},"label":"StaticLodShadow/local_prefix_sum"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"prefix_sum_partial_sums_1"},"label":"StaticLodShadow/prefix_sum_partial_sums_1"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"prefix_sum_partial_sums_2"},"label":"StaticLodShadow/prefix_sum_partial_sums_2"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"prefix_sum_partial_sums_3"},"label":"StaticLodShadow/prefix_sum_partial_sums_3"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"prefix_sum_partial_sums_4"},"label":"StaticLodShadow/prefix_sum_partial_sums_4"}},{"kind":"compute","desc":{"layout":{"__layout":19},"compute":{"module":{"__mod":19},"entryPoint":"add_partial_sums"},"label":"StaticLodShadow/add_partial_sums"}},{"kind":"render","desc":{"layout":{"__layout":20},"vertex":{"module":{"__mod":20},"entryPoint":"vs_main","buffers":[{"arrayStride":12,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Shadow Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":21},"vertex":{"module":{"__mod":21},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Shadow Pipeline (Alpha)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":21},"targets":[],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":22},"vertex":{"module":{"__mod":22},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"Shadow Pipeline (Skinned)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":23},"vertex":{"module":{"__mod":23},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"Shadow Pipeline (Skinned Alpha)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":1,"depthBiasClamp":0,"depthBiasSlopeScale":2.5,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":23},"targets":[],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":24},"vertex":{"module":{"__mod":24},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"GBuffer Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":24},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":24},"vertex":{"module":{"__mod":25},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"GBuffer Pipeline Masked","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":25},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":25},"vertex":{"module":{"__mod":24},"entryPoint":"vs_main_lod","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"}]},"label":"GBuffer LOD Pipeline","depthStencil":{"format":"depth32float","depthCompare":"equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":24},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":25},"vertex":{"module":{"__mod":25},"entryPoint":"vs_main_lod","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"}]},"label":"GBuffer LOD Pipeline Masked","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":25},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":25},"vertex":{"module":{"__mod":24},"entryPoint":"vs_main_lod","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"}]},"label":"GBuffer LOD Pipeline Reflection","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":24},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":24},"vertex":{"module":{"__mod":24},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"GBuffer Pipeline Reflection","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":24},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":24},"vertex":{"module":{"__mod":25},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"GBuffer Pipeline Masked Reflection","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":25},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":32},"vertex":{"module":{"__mod":26},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"GBuffer (Skinned)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":26},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":32},"vertex":{"module":{"__mod":27},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"GBuffer (Skinned Masked)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":27},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":32},"vertex":{"module":{"__mod":26},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"GBuffer (Skinned Reflection)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":26},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":32},"vertex":{"module":{"__mod":27},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"GBuffer (Skinned Masked Reflection)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":27},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":33},"vertex":{"module":{"__mod":28},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Forward Unlit Blend Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":28},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":34},"vertex":{"module":{"__mod":29},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"Forward Unlit Blend Pipeline (Skinned)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":29},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":35},"vertex":{"module":{"__mod":30},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SMAA Edges Pipeline","fragment":{"module":{"__mod":30},"targets":[{"format":"rg8unorm","writeMask":15}],"entryPoint":"fs_edges"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":36},"vertex":{"module":{"__mod":31},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SMAA Blend Pipeline","fragment":{"module":{"__mod":31},"targets":[{"format":"rgba8unorm","writeMask":15}],"entryPoint":"fs_weights"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":37},"vertex":{"module":{"__mod":32},"entryPoint":"vs_fullscreen","buffers":[]},"label":"SMAA Neighborhood Pipeline","fragment":{"module":{"__mod":32},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_neighborhood"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":38},"compute":{"module":{"__mod":33},"entryPoint":"cs_main"},"label":"SMAA DebugReadback Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":39},"vertex":{"module":{"__mod":34},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Upscale Pipeline","fragment":{"module":{"__mod":34},"targets":[{"format":"bgra8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":40},"vertex":{"module":{"__mod":35},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Tonemap Pipeline","fragment":{"module":{"__mod":35},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_tonemap"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":41},"vertex":{"module":{"__mod":36},"entryPoint":"vs_main","buffers":[]},"label":"SelectionOutline Pipeline","fragment":{"module":{"__mod":36},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":42},"vertex":{"module":{"__mod":37},"entryPoint":"vs_main","buffers":[]},"label":"HighlightOutline Pipeline","fragment":{"module":{"__mod":37},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":43},"compute":{"module":{"__mod":39},"entryPoint":"main"},"label":"Decal Global Cull Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":44},"compute":{"module":{"__mod":40},"entryPoint":"main"},"label":"Decal Bucket Ranges Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":45},"vertex":{"module":{"__mod":38},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Decal Global Texture Pipeline","fragment":{"module":{"__mod":38},"targets":[{"format":"rgba8unorm","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15},{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15},{"format":"rgba8unorm","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_decal"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":47},"compute":{"module":{"__mod":44},"entryPoint":"main"},"label":"terrain_tile_bake_pipeline"}},{"kind":"render","desc":{"layout":{"__layout":48},"vertex":{"module":{"__mod":41},"entryPoint":"vs_main","buffers":[]},"label":"terrain_pipeline_default","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":42},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":49},"vertex":{"module":{"__mod":41},"entryPoint":"vs_main","buffers":[]},"label":"terrain_pipeline_splat_layers","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":45},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":50},"vertex":{"module":{"__mod":41},"entryPoint":"vs_main","buffers":[]},"label":"terrain_pipeline_pick","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":43},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":"auto","compute":{"module":{"__mod":46},"entryPoint":"recipe_check_main"},"label":"terrain_recipe_check"}},{"kind":"compute","desc":{"layout":{"__layout":51},"compute":{"module":{"__mod":47},"entryPoint":"ring_heights"},"label":"ring_heights"}},{"kind":"compute","desc":{"layout":{"__layout":52},"compute":{"module":{"__mod":47},"entryPoint":"ring_normals"},"label":"ring_normals"}},{"kind":"compute","desc":{"layout":"auto","compute":{"module":{"__mod":48},"entryPoint":"terrain_hiz_cull"},"label":"terrain_hiz_cull"}},{"kind":"render","desc":{"layout":"auto","vertex":{"module":{"__mod":49},"entryPoint":"vs_main","buffers":[]},"label":"surface_blit_srgb","fragment":{"module":{"__mod":49},"targets":[{"format":"rgba8unorm-srgb","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":"auto","vertex":{"module":{"__mod":49},"entryPoint":"vs_main","buffers":[]},"label":"surface_blit_rg","fragment":{"module":{"__mod":49},"targets":[{"format":"rg8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":"auto","vertex":{"module":{"__mod":49},"entryPoint":"vs_main","buffers":[]},"label":"surface_blit_rgba","fragment":{"module":{"__mod":49},"targets":[{"format":"rgba8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":54},"vertex":{"module":{"__mod":41},"entryPoint":"vs_main","buffers":[]},"label":"terrain_pipeline_surface_blend","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":50},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":55},"compute":{"module":{"__mod":54},"entryPoint":"main"},"label":"GrassInteraction Debug Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":56},"vertex":{"module":{"__mod":51},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":32,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x2","offset":16,"shaderLocation":5},{"format":"float32x2","offset":24,"shaderLocation":6}],"stepMode":"instance"},{"arrayStride":8,"attributes":[{"format":"uint32x2","offset":0,"shaderLocation":7}],"stepMode":"instance"}]},"label":"Grass GBuffer Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":51},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":57},"vertex":{"module":{"__mod":52},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":32,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x2","offset":16,"shaderLocation":5},{"format":"float32x2","offset":24,"shaderLocation":6}],"stepMode":"instance"},{"arrayStride":8,"attributes":[{"format":"uint32x2","offset":0,"shaderLocation":7}],"stepMode":"instance"}]},"label":"Grass GBuffer Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":52},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":58},"vertex":{"module":{"__mod":53},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":32,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x2","offset":16,"shaderLocation":5},{"format":"float32x2","offset":24,"shaderLocation":6}],"stepMode":"instance"},{"arrayStride":8,"attributes":[{"format":"uint32x2","offset":0,"shaderLocation":7}],"stepMode":"instance"}]},"label":"Grass GBuffer Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":53},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":59},"compute":{"module":{"__mod":55},"entryPoint":"main"},"label":"GrassSpawn Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":60},"compute":{"module":{"__mod":56},"entryPoint":"main"},"label":"GrassSpawn Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":61},"compute":{"module":{"__mod":57},"entryPoint":"main"},"label":"GrassCull Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":62},"compute":{"module":{"__mod":58},"entryPoint":"compact_grass_instances"},"label":"CompactGrass Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":63},"compute":{"module":{"__mod":59},"entryPoint":"main"},"label":"GrassEnrich Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":64},"compute":{"module":{"__mod":60},"entryPoint":"main"},"label":"GrassIndirectWrite Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":65},"compute":{"module":{"__mod":61},"entryPoint":"main"},"label":"LeafCull"}},{"kind":"compute","desc":{"layout":{"__layout":66},"compute":{"module":{"__mod":62},"entryPoint":"compact_instances_no_extras"},"label":"LeafCompact"}},{"kind":"render","desc":{"layout":{"__layout":67},"vertex":{"module":{"__mod":63},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":32,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x2","offset":16,"shaderLocation":5},{"format":"float32x2","offset":24,"shaderLocation":6}],"stepMode":"instance"}]},"label":"Leaf GBuffer Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":63},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":68},"vertex":{"module":{"__mod":64},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":32,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x2","offset":16,"shaderLocation":5},{"format":"float32x2","offset":24,"shaderLocation":6}],"stepMode":"instance"}]},"label":"Leaf Impostor Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":64},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":69},"compute":{"module":{"__mod":65},"entryPoint":"cs_main"},"label":"OceanRipples Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":70},"compute":{"module":{"__mod":66},"entryPoint":"main"},"label":"Downsample Depth Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":71},"vertex":{"module":{"__mod":67},"entryPoint":"vs_main","buffers":[]},"label":"SSAO Pipeline","fragment":{"module":{"__mod":67},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":72},"vertex":{"module":{"__mod":68},"entryPoint":"vs_main","buffers":[]},"label":"Blur Pipeline","fragment":{"module":{"__mod":68},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":73},"vertex":{"module":{"__mod":69},"entryPoint":"vs_main","buffers":[]},"label":"SSAO Upsample Pipeline","fragment":{"module":{"__mod":69},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":74},"vertex":{"module":{"__mod":70},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Motion Vector Pipeline","depthStencil":{"format":"depth32float","depthCompare":"equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":70},"targets":[{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":75},"vertex":{"module":{"__mod":71},"entryPoint":"vs_main","buffers":[]},"label":"SSR Downsample Pipeline","fragment":{"module":{"__mod":71},"targets":[{"format":"rg32float","writeMask":3},{"format":"r32float","writeMask":1},{"format":"rgba16float","writeMask":15},{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":76},"vertex":{"module":{"__mod":72},"entryPoint":"vs_main","buffers":[]},"label":"SSR Hi-Z Pipeline","fragment":{"module":{"__mod":72},"targets":[{"format":"rg32float","writeMask":3}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":77},"vertex":{"module":{"__mod":73},"entryPoint":"vs_main","buffers":[]},"label":"SSR Trace Pipeline","fragment":{"module":{"__mod":73},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":78},"vertex":{"module":{"__mod":74},"entryPoint":"vs_main","buffers":[]},"label":"SSR Filter Pipeline","fragment":{"module":{"__mod":74},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":79},"vertex":{"module":{"__mod":75},"entryPoint":"vs_main","buffers":[]},"label":"SSR Spatial Resolve Pipeline","fragment":{"module":{"__mod":75},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":80},"vertex":{"module":{"__mod":76},"entryPoint":"vs_main","buffers":[]},"label":"SSR History Copy Pipeline","fragment":{"module":{"__mod":76},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":81},"vertex":{"module":{"__mod":77},"entryPoint":"vs_main","buffers":[]},"label":"SSR Resolve Pipeline","fragment":{"module":{"__mod":77},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":82},"vertex":{"module":{"__mod":78},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Prefilter","fragment":{"module":{"__mod":78},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_prefilter"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":83},"vertex":{"module":{"__mod":78},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Down","fragment":{"module":{"__mod":78},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_down"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":84},"vertex":{"module":{"__mod":78},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Up","fragment":{"module":{"__mod":78},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_up"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":85},"vertex":{"module":{"__mod":79},"entryPoint":"vs_fullscreen","buffers":[]},"label":"volumetric_march","fragment":{"module":{"__mod":79},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_march"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":86},"vertex":{"module":{"__mod":80},"entryPoint":"vs_fullscreen","buffers":[]},"label":"volumetric_composite","fragment":{"module":{"__mod":80},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":87},"vertex":{"module":{"__mod":81},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Particle Composite Pipeline","fragment":{"module":{"__mod":81},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":88},"vertex":{"module":{"__mod":83},"entryPoint":"fullscreen_vs","buffers":[]},"label":"Main Pass Fallback Pipeline","fragment":{"module":{"__mod":84},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fullscreen_fs"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":88},"vertex":{"module":{"__mod":83},"entryPoint":"fullscreen_vs","buffers":[]},"label":"Main Pass Pipeline","fragment":{"module":{"__mod":82},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fullscreen_fs"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":88},"vertex":{"module":{"__mod":83},"entryPoint":"fullscreen_vs","buffers":[]},"label":"Planar Reflection Main Pass Pipeline","fragment":{"module":{"__mod":82},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fullscreen_fs"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":89},"vertex":{"module":{"__mod":85},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Water Pipeline (reload)","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":85},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":90},"vertex":{"module":{"__mod":86},"entryPoint":"vs_main","buffers":[{"arrayStride":8,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0}],"stepMode":"vertex"}]},"label":"Ocean Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":86},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":91},"vertex":{"module":{"__mod":87},"entryPoint":"vs_main","buffers":[{"arrayStride":8,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0}],"stepMode":"vertex"}]},"label":"Ocean MV Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":87},"targets":[{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":92},"compute":{"module":{"__mod":88},"entryPoint":"cs_init"},"label":"OceanFft Init"}},{"kind":"compute","desc":{"layout":{"__layout":93},"compute":{"module":{"__mod":88},"entryPoint":"cs_evolve"},"label":"OceanFft Evolve"}},{"kind":"compute","desc":{"layout":{"__layout":94},"compute":{"module":{"__mod":89},"entryPoint":"cs_h"},"label":"OceanFft H"}},{"kind":"compute","desc":{"layout":{"__layout":95},"compute":{"module":{"__mod":89},"entryPoint":"cs_v"},"label":"OceanFft V"}},{"kind":"compute","desc":{"layout":{"__layout":96},"compute":{"module":{"__mod":90},"entryPoint":"cs_main"},"label":"OceanFft Deriv"}},{"kind":"compute","desc":{"layout":{"__layout":97},"compute":{"module":{"__mod":91},"entryPoint":"cs_main"},"label":"OceanFft Mip"}},{"kind":"compute","desc":{"layout":{"__layout":98},"compute":{"module":{"__mod":92},"entryPoint":"cs_main"},"label":"StabilityProbe Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":99},"compute":{"module":{"__mod":93},"entryPoint":"cs_main"},"label":"particle_spawn_cs"}},{"kind":"compute","desc":{"layout":{"__layout":100},"compute":{"module":{"__mod":94},"entryPoint":"cs_main"},"label":"particle_update_cs"}},{"kind":"compute","desc":{"layout":{"__layout":101},"compute":{"module":{"__mod":95},"entryPoint":"cs_main"},"label":"particle_dead_append_cs"}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":96},"entryPoint":"vs_main","buffers":[]},"label":"particle_pipeline_alpha","fragment":{"module":{"__mod":96},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":4,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":96},"entryPoint":"vs_main","buffers":[]},"label":"particle_pipeline_additive","fragment":{"module":{"__mod":96},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":4,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":96},"entryPoint":"vs_main","buffers":[]},"label":"particle_pipeline_alpha_fullres","fragment":{"module":{"__mod":96},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main_reactive"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":96},"entryPoint":"vs_main","buffers":[]},"label":"particle_pipeline_additive_fullres","fragment":{"module":{"__mod":96},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one","operation":"add","srcFactor":"one"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main_reactive"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":97},"entryPoint":"vs_main","buffers":[]},"label":"particle_ribbon_alpha","fragment":{"module":{"__mod":97},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":4,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":97},"entryPoint":"vs_main","buffers":[]},"label":"particle_ribbon_additive","fragment":{"module":{"__mod":97},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":4,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":97},"entryPoint":"vs_main","buffers":[]},"label":"particle_ribbon_alpha_fullres","fragment":{"module":{"__mod":97},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main_reactive"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":102},"vertex":{"module":{"__mod":97},"entryPoint":"vs_main","buffers":[]},"label":"particle_ribbon_additive_fullres","fragment":{"module":{"__mod":97},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one","operation":"add","srcFactor":"zero"},"color":{"dstFactor":"one","operation":"add","srcFactor":"one"}},"writeMask":15},{"format":"r8unorm","blend":{"alpha":{"dstFactor":"one","operation":"max","srcFactor":"one"},"color":{"dstFactor":"one","operation":"max","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main_reactive"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-strip","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":103},"vertex":{"module":{"__mod":98},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Custom Transparent Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":98},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":104},"vertex":{"module":{"__mod":99},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Custom Transparent Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":99},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":105},"vertex":{"module":{"__mod":100},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Custom Transparent Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":100},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":106},"vertex":{"module":{"__mod":101},"entryPoint":"vs_main","buffers":[{"arrayStride":24,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"Lines Pipeline","fragment":{"module":{"__mod":101},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"line-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":107},"vertex":{"module":{"__mod":102},"entryPoint":"vs_main","buffers":[]},"label":"debug_texture_pipeline","fragment":{"module":{"__mod":102},"targets":[{"format":"bgra8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":108},"vertex":{"module":{"__mod":103},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Gizmo Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":103},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":108},"vertex":{"module":{"__mod":104},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Gizmo Pipeline","depthStencil":{"format":"depth32float","depthCompare":"greater","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":104},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":109},"vertex":{"module":{"__mod":105},"entryPoint":"vs","buffers":[]},"label":"Gizmo blit pipeline","fragment":{"module":{"__mod":105},"targets":[{"format":"rgba16float","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":110},"vertex":{"module":{"__mod":106},"entryPoint":"vs_main","buffers":[{"arrayStride":16,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0},{"format":"float32x2","offset":8,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"UiText Pipeline","fragment":{"module":{"__mod":106},"targets":[{"format":"bgra8unorm","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":111},"vertex":{"module":{"__mod":107},"entryPoint":"vs_main","buffers":[{"arrayStride":16,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0},{"format":"float32x2","offset":8,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"UiFrame Pipeline","fragment":{"module":{"__mod":107},"targets":[{"format":"bgra8unorm","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":113},"vertex":{"module":{"__mod":108},"entryPoint":"vs_main","buffers":[{"arrayStride":16,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0},{"format":"float32x2","offset":8,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"UiImage Pipeline","fragment":{"module":{"__mod":108},"targets":[{"format":"bgra8unorm","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":114},"vertex":{"module":{"__mod":109},"entryPoint":"vs_main","buffers":[{"arrayStride":16,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0},{"format":"float32x2","offset":8,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"UiHSV Pipeline","fragment":{"module":{"__mod":109},"targets":[{"format":"bgra8unorm","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":115},"vertex":{"module":{"__mod":110},"entryPoint":"vs_main","buffers":[{"arrayStride":16,"attributes":[{"format":"float32x2","offset":0,"shaderLocation":0},{"format":"float32x2","offset":8,"shaderLocation":1}],"stepMode":"vertex"}]},"label":"UiVecIcon Pipeline","fragment":{"module":{"__mod":110},"targets":[{"format":"bgra8unorm","blend":{"alpha":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"one"},"color":{"dstFactor":"one-minus-src-alpha","operation":"add","srcFactor":"src-alpha"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":116},"vertex":{"module":{"__mod":111},"entryPoint":"vs_main","buffers":[]},"label":"UiImageResample Pipeline","fragment":{"module":{"__mod":111},"targets":[{"format":"rgba8unorm-srgb","blend":{"alpha":{"dstFactor":"zero","operation":"add","srcFactor":"one"},"color":{"dstFactor":"zero","operation":"add","srcFactor":"one"}},"writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":117},"vertex":{"module":{"__mod":112},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Pick Static Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":112},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":118},"vertex":{"module":{"__mod":113},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"Pick Skinned Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":113},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":119},"vertex":{"module":{"__mod":114},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":80,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"uint32","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Pick Proxy Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":114},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":120},"vertex":{"module":{"__mod":112},"entryPoint":"vs_main","buffers":[{"arrayStride":32,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"snorm16x2","offset":12,"shaderLocation":1},{"format":"float32x2","offset":16,"shaderLocation":2},{"format":"snorm16x4","offset":24,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":112,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"uint32","offset":96,"shaderLocation":10}],"stepMode":"instance"}]},"label":"Pick Static Filter Pipeline","fragment":{"module":{"__mod":112},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_filtered"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":121},"vertex":{"module":{"__mod":113},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":6},{"format":"float32x4","offset":16,"shaderLocation":7},{"format":"float32x4","offset":32,"shaderLocation":8},{"format":"float32x4","offset":48,"shaderLocation":9},{"format":"float32x4","offset":64,"shaderLocation":10},{"format":"float32x4","offset":80,"shaderLocation":11},{"format":"uint32x4","offset":96,"shaderLocation":12},{"format":"float32","offset":112,"shaderLocation":13},{"format":"uint32","offset":124,"shaderLocation":14}],"stepMode":"instance"}]},"label":"Pick Skinned Filter Pipeline","fragment":{"module":{"__mod":113},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_filtered"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":122},"vertex":{"module":{"__mod":114},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":80,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"uint32","offset":64,"shaderLocation":8}],"stepMode":"instance"}]},"label":"Pick Proxy Filter Pipeline","fragment":{"module":{"__mod":114},"targets":[{"format":"r32uint","writeMask":15}],"entryPoint":"fs_filtered"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":123},"compute":{"module":{"__mod":115},"entryPoint":"cs_cull"},"label":"Pencil Cull Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":123},"compute":{"module":{"__mod":115},"entryPoint":"cs_fixup"},"label":"Pencil Cull Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":124},"compute":{"module":{"__mod":116},"entryPoint":"cs_cull"},"label":"Skinned Pencil Cull Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":124},"compute":{"module":{"__mod":116},"entryPoint":"cs_fixup"},"label":"Skinned Pencil Cull Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":125},"vertex":{"module":{"__mod":117},"entryPoint":"vs_static","buffers":[{"arrayStride":12,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0}],"stepMode":"vertex"},{"arrayStride":128,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"float32x4","offset":80,"shaderLocation":9},{"format":"float32x4","offset":96,"shaderLocation":10},{"format":"float32x4","offset":112,"shaderLocation":11}],"stepMode":"instance"}]},"label":"MV Static Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":117},"targets":[{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"back","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":126},"compute":{"module":{"__mod":118},"entryPoint":"main"},"label":"Downsample Depth Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":127},"vertex":{"module":{"__mod":119},"entryPoint":"vs_main","buffers":[]},"label":"SSAO Pipeline","fragment":{"module":{"__mod":119},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":128},"vertex":{"module":{"__mod":120},"entryPoint":"vs_main","buffers":[]},"label":"Blur Pipeline","fragment":{"module":{"__mod":120},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":129},"vertex":{"module":{"__mod":121},"entryPoint":"vs_main","buffers":[]},"label":"SSAO Upsample Pipeline","fragment":{"module":{"__mod":121},"targets":[{"format":"r8unorm","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":130},"vertex":{"module":{"__mod":122},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Motion Vector Pipeline","depthStencil":{"format":"depth32float","depthCompare":"equal","depthWriteEnabled":false,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":122},"targets":[{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":131},"vertex":{"module":{"__mod":123},"entryPoint":"vs_main","buffers":[]},"label":"SSR Downsample Pipeline","fragment":{"module":{"__mod":123},"targets":[{"format":"rg32float","writeMask":3},{"format":"r32float","writeMask":1},{"format":"rgba16float","writeMask":15},{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":132},"vertex":{"module":{"__mod":124},"entryPoint":"vs_main","buffers":[]},"label":"SSR Hi-Z Pipeline","fragment":{"module":{"__mod":124},"targets":[{"format":"rg32float","writeMask":3}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":133},"vertex":{"module":{"__mod":125},"entryPoint":"vs_main","buffers":[]},"label":"SSR Trace Pipeline","fragment":{"module":{"__mod":125},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":134},"vertex":{"module":{"__mod":126},"entryPoint":"vs_main","buffers":[]},"label":"SSR Filter Pipeline","fragment":{"module":{"__mod":126},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":135},"vertex":{"module":{"__mod":127},"entryPoint":"vs_main","buffers":[]},"label":"SSR Spatial Resolve Pipeline","fragment":{"module":{"__mod":127},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":136},"vertex":{"module":{"__mod":128},"entryPoint":"vs_main","buffers":[]},"label":"SSR History Copy Pipeline","fragment":{"module":{"__mod":128},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":137},"vertex":{"module":{"__mod":129},"entryPoint":"vs_main","buffers":[]},"label":"SSR Resolve Pipeline","fragment":{"module":{"__mod":129},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":138},"vertex":{"module":{"__mod":130},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Prefilter","fragment":{"module":{"__mod":130},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_prefilter"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":139},"vertex":{"module":{"__mod":130},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Down","fragment":{"module":{"__mod":130},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_down"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":140},"vertex":{"module":{"__mod":130},"entryPoint":"vs_fullscreen","buffers":[]},"label":"Bloom Up","fragment":{"module":{"__mod":130},"targets":[{"format":"rgba16float","writeMask":15}],"entryPoint":"fs_up"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":141},"vertex":{"module":{"__mod":131},"entryPoint":"vs_main","buffers":[{"arrayStride":72,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3},{"format":"uint16x4","offset":48,"shaderLocation":4},{"format":"float32x4","offset":56,"shaderLocation":5}],"stepMode":"vertex"}]},"label":"SkinnedImp Bake Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":131},"targets":[{"format":"rgba8unorm-srgb","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":"auto","vertex":{"module":{"__mod":132},"entryPoint":"vs_main","buffers":[]},"label":"SkinnedImp Mip Pipeline","fragment":{"module":{"__mod":132},"targets":[{"format":"rgba8unorm-srgb","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":142},"vertex":{"module":{"__mod":133},"entryPoint":"vs_main","buffers":[{"arrayStride":48,"attributes":[{"format":"float32x3","offset":0,"shaderLocation":0},{"format":"float32x3","offset":12,"shaderLocation":1},{"format":"float32x2","offset":24,"shaderLocation":2},{"format":"float32x4","offset":32,"shaderLocation":3}],"stepMode":"vertex"},{"arrayStride":144,"attributes":[{"format":"float32x4","offset":0,"shaderLocation":4},{"format":"float32x4","offset":16,"shaderLocation":5},{"format":"float32x4","offset":32,"shaderLocation":6},{"format":"float32x4","offset":48,"shaderLocation":7},{"format":"float32x4","offset":64,"shaderLocation":8},{"format":"uint32x4","offset":112,"shaderLocation":9}],"stepMode":"instance"}]},"label":"SkinnedImp Draw Pipeline","depthStencil":{"format":"depth32float","depthCompare":"less-equal","depthWriteEnabled":true,"depthBias":0,"depthBiasClamp":0,"depthBiasSlopeScale":0,"stencilBack":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilFront":{"compare":"always","depthFailOp":"keep","failOp":"keep","passOp":"keep"},"stencilReadMask":0,"stencilWriteMask":0},"fragment":{"module":{"__mod":133},"targets":[{"format":"rgba8unorm","writeMask":15},{"format":"rgba16float","writeMask":15},{"format":"rgba8unorm","writeMask":15},{"format":"rg16float","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"compute","desc":{"layout":{"__layout":143},"compute":{"module":{"__mod":134},"entryPoint":"main"},"label":"WalkerFollowPatch Pipeline"}},{"kind":"compute","desc":{"layout":{"__layout":144},"compute":{"module":{"__mod":135},"entryPoint":"main"},"label":"SkinnedHizCull Pipeline"}},{"kind":"render","desc":{"layout":{"__layout":145},"vertex":{"module":{"__mod":136},"entryPoint":"vs_fullscreen","buffers":[]},"label":"TAAU Pipeline","fragment":{"module":{"__mod":136},"targets":[{"format":"rg11b10ufloat","writeMask":15}],"entryPoint":"fs_main"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}},{"kind":"render","desc":{"layout":{"__layout":146},"vertex":{"module":{"__mod":136},"entryPoint":"vs_fullscreen","buffers":[]},"label":"TAAU RCAS Pipeline","fragment":{"module":{"__mod":136},"targets":[{"format":"bgra8unorm","writeMask":15}],"entryPoint":"fs_rcas"},"multisample":{"count":1,"mask":4294967295,"alphaToCoverageEnabled":false},"primitive":{"cullMode":"none","frontFace":"ccw","topology":"triangle-list","unclippedDepth":false}}}],"bgls":[{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":80,"type":"uniform"}}],"label":"Material Bind Group Layout"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"Palette BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":160,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":7,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":8,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"BoneCompute BGL"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}}],"label":"Camera Bind Group Layout"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}}],"label":"Camera Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":112,"type":"uniform"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":16,"type":"uniform"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SkyGen BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}}],"label":"Skybox Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":288,"type":"uniform"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}}],"label":"Skybox Params BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"sampler":{"type":"filtering"}}],"label":"Sky Billboard BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":208,"type":"uniform"}}],"label":"Lights BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Cluster lists BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"LocalShadow Sample BGL (group=4)"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":true,"minBindingSize":96,"type":"uniform"}}],"label":"LocalShadow LightCamera BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":208,"type":"uniform"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":9,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":10,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":11,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":12,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":13,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":14,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":15,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":16,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":288,"type":"uniform"}}],"label":"IBL + Cluster BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":304,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodDebug BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":7,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":8,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodGlobalPrefixClassify BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodGlobalPrefixWriteCounts BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodGlobalPrefixScatterPacked BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodScatterIndirect BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":52,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":7,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":8,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":384,"type":"uniform"}},{"binding":9,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"StaticLodShadowClassify BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":52,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":7,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":8,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodShadowScatter BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":52,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StaticLodShadowWriteIndirect BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":1,"buffer":{"hasDynamicOffset":true,"minBindingSize":16,"type":"uniform"}},{"binding":4,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"StaticLodShadowDraw BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":1,"buffer":{"hasDynamicOffset":true,"minBindingSize":16,"type":"uniform"}},{"binding":4,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"StaticLod Camera InstanceFetch BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":96,"type":"uniform"}}],"label":"StaticLodShadow Light BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Prefix Sum Bind Group Layout"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Prefix Sum Bind Group Layout"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":96,"type":"uniform"}}],"label":"Light Camera Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":480,"type":"uniform"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d-array"}},{"binding":2,"visibility":2,"sampler":{"type":"comparison"}}],"label":"Directional Light Shadow Bind Group Layout"},{"entries":[{"binding":0,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":3,"sampler":{"type":"filtering"}},{"binding":17,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Mesh Material16 BGL"},{"entries":[{"binding":0,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":3,"sampler":{"type":"filtering"}},{"binding":16,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Mesh Material16 Forward BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}}],"label":"Forward Material16 Scene BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"SMAA Edges BGL"},{"entries":[{"binding":0,"visibility":2,"sampler":{"type":"filtering"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"SMAA Blend BGL"},{"entries":[{"binding":0,"visibility":2,"sampler":{"type":"filtering"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"SMAA Neighborhood BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":52,"type":"storage"}}],"label":"SMAA DebugReadback BGL"},{"entries":[{"binding":0,"visibility":2,"sampler":{"type":"filtering"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"Upscale BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":176,"type":"uniform"}}],"label":"screen_shader_bgl"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":96,"type":"uniform"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"Tonemap BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":2,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SelectionOutline BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"HighlightOutline BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":144,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Decal Global Cull BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Decal Bucket Ranges BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":16,"type":"uniform"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":10,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"Decal Global Texture BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"sampler":{"type":"filtering"}},{"binding":5,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":16,"type":"uniform"}},{"binding":6,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"Decal Global Material Receiver BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":2,"sampler":{"type":"filtering"}},{"binding":12,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Decal Material BGL"},{"entries":[{"binding":0,"visibility":1,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":1,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":192,"type":"uniform"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":1,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":7,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}}],"label":"Terrain BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":112,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"storageTexture":{"format":"rg32float","access":"write-only","viewDimension":"2d"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Terrain Tile Bake BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":2,"sampler":{"type":"filtering"}},{"binding":17,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Terrain Material BGL"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"Terrain PickId BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":true,"minBindingSize":144,"type":"uniform"}},{"binding":1,"visibility":4,"storageTexture":{"format":"rg32float","access":"write-only","viewDimension":"2d"}}],"label":"ring_heights_bgl"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":true,"minBindingSize":144,"type":"uniform"}},{"binding":2,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":4,"storageTexture":{"format":"rgba8unorm","access":"write-only","viewDimension":"2d-array"}}],"label":"ring_normals_bgl"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":true,"minBindingSize":144,"type":"uniform"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":4,"storageTexture":{"format":"rgba8unorm","access":"write-only","viewDimension":"2d-array"}},{"binding":3,"visibility":4,"storageTexture":{"format":"rgba8unorm","access":"write-only","viewDimension":"2d-array"}}],"label":"ring_weights_bgl"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":4,"visibility":2,"sampler":{"type":"filtering"}},{"binding":5,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":80,"type":"uniform"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}}],"label":"terrain_surface_blend_bgl"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":1,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"GrassInteraction BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"GrassInteraction Debug BGL"},{"entries":[{"binding":0,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":3,"sampler":{"type":"filtering"}},{"binding":17,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Grass Material BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":4,"sampler":{"type":"filtering"}},{"binding":15,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"Grass Spawn Material14 BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"uint","viewDimension":"2d"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":4,"sampler":{"type":"filtering"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":384,"type":"uniform"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"GrassSpawn BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d-array"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d-array"}}],"label":"GrassSpawnWeights BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"GrassCull BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":5,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":6,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"CompactGrass BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"GrassEnrich BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"GrassIndirectWrite BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"LeafCull BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"LeafCompact BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":3,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"LeafTex BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":3,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}}],"label":"LeafImpostor BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":1,"visibility":3,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"LeafImpostorBakeView BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}}],"label":"LeafImpostorBark BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"sampler":{"type":"non-filtering"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":10,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":192,"type":"uniform"}},{"binding":11,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":12,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"Main Pass G-Buffer Bind Group Layout"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":2048,"type":"uniform"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":4,"storageTexture":{"format":"rgba16float","access":"write-only","viewDimension":"2d"}}],"label":"OceanRipples BGL"},{"entries":[{"binding":0,"visibility":4,"storageTexture":{"format":"r32float","access":"write-only","viewDimension":"2d"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":4,"sampler":{"type":"non-filtering"}}],"label":"Downsample Depth BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"sampler":{"type":"non-filtering"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"SSAO Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"sampler":{"type":"filtering"}},{"binding":5,"visibility":2,"sampler":{"type":"non-filtering"}}],"label":"Blur Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"sampler":{"type":"non-filtering"}}],"label":"SSAO Upsample Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}}],"label":"Motion Vector BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Downsample BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}}],"label":"SSR Hi-Z BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"storageTexture":{"format":"r32float","access":"write-only","viewDimension":"2d"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Trace BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Filter BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SSR Spatial Resolve BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"SSR History Copy BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":9,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SSR Resolve BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Prefilter BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Down BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Up BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}}],"label":"volumetric_depth_bgl"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"volumetric_settings_bgl"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}}],"label":"volumetric_upsample_bgl"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}}],"label":"Particle Composite BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"non-filtering"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"sampler":{"type":"filtering"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":10,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":11,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":19,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":80,"type":"uniform"}},{"binding":20,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Water BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"non-filtering"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"sampler":{"type":"filtering"}},{"binding":8,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"cube"}},{"binding":10,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":11,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":19,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":80,"type":"uniform"}},{"binding":30,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":384,"type":"uniform"}},{"binding":31,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":32,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":35,"visibility":3,"sampler":{"type":"filtering"}},{"binding":36,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":37,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":38,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"Ocean BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}},{"binding":30,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":384,"type":"uniform"}},{"binding":31,"visibility":1,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":32,"visibility":1,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":33,"visibility":1,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":34,"visibility":1,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":35,"visibility":1,"sampler":{"type":"filtering"}},{"binding":38,"visibility":1,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"Ocean MV BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":4,"storageTexture":{"format":"rgba32float","access":"write-only","viewDimension":"2d"}}],"label":"OceanFft Spectrum BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":4,"storageTexture":{"format":"rgba32float","access":"write-only","viewDimension":"2d"}}],"label":"OceanFft FFT BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":4,"storageTexture":{"format":"rgba16float","access":"write-only","viewDimension":"2d"}},{"binding":3,"visibility":4,"storageTexture":{"format":"rgba16float","access":"write-only","viewDimension":"2d"}},{"binding":4,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"OceanFft Deriv BGL"},{"entries":[{"binding":0,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":4,"storageTexture":{"format":"rgba16float","access":"write-only","viewDimension":"2d"}}],"label":"OceanFft Mip BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":4,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"StabilityProbe BGL"},{"entries":[{"binding":0,"visibility":7,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"particle_system_uniform_bgl"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"particle_emitters_bgl"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"particles_bgl_compute"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"particles_bgl_vertex"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"particle_dead_bgl"},{"entries":[{"binding":0,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":5,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"particle_flipbook_bgl"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":24,"type":"uniform"}},{"binding":2,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"debug_texture_bind_group_layout"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}}],"label":"Gizmo blit BGL"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"UiTextBindGroupLayout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":32,"type":"uniform"}}],"label":"UiTextPropsBindGroupLayout"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":160,"type":"uniform"}},{"binding":2,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"UiFrameBindGroupLayout"},{"entries":[{"binding":0,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":6,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":8,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":9,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":10,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":11,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":12,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":13,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":14,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":15,"visibility":3,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":16,"visibility":3,"sampler":{"type":"filtering"}}],"label":"UiFrame Material Texture BGL"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":true,"minBindingSize":128,"type":"uniform"}}],"label":"UiFrame Material Params BGL"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"UiImageTextureBindGroupLayout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":32,"type":"uniform"}}],"label":"UiImagePropsBindGroupLayout"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":32,"type":"uniform"}}],"label":"UiHsvBindGroupLayout"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":true,"minBindingSize":32,"type":"uniform"}}],"label":"UiVecIconBindGroupLayout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"UiImageResampleBindGroupLayout"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"PickIds BGL"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"SelectionFilter BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"Pencil Src BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":true,"type":"uniform"}}],"label":"Pencil Uniform BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Pencil Out BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}}],"label":"Skinned Pencil Src BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"Skinned Pencil Out BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":128,"type":"uniform"}}],"label":"MV Geometry BGL"},{"entries":[{"binding":0,"visibility":4,"storageTexture":{"format":"r32float","access":"write-only","viewDimension":"2d"}},{"binding":1,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":4,"sampler":{"type":"non-filtering"}}],"label":"Downsample Depth BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"sampler":{"type":"non-filtering"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}}],"label":"SSAO Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":16,"type":"uniform"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"sampler":{"type":"filtering"}},{"binding":5,"visibility":2,"sampler":{"type":"non-filtering"}}],"label":"Blur Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"sampler":{"type":"filtering"}},{"binding":4,"visibility":2,"sampler":{"type":"non-filtering"}}],"label":"SSAO Upsample Bind Group Layout"},{"entries":[{"binding":0,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":64,"type":"uniform"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}}],"label":"Motion Vector BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Downsample BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}}],"label":"SSR Hi-Z BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":5,"visibility":2,"sampler":{"type":"filtering"}},{"binding":6,"visibility":2,"storageTexture":{"format":"r32float","access":"write-only","viewDimension":"2d"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Trace BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":1,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}}],"label":"SSR Filter BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SSR Spatial Resolve BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"SSR History Copy BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":5,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":7,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":8,"visibility":2,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":9,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SSR Resolve BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Prefilter BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Down BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"sampler":{"type":"filtering"}},{"binding":3,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":32,"type":"uniform"}}],"label":"Bloom Up BGL"},{"entries":[{"binding":0,"visibility":1,"buffer":{"hasDynamicOffset":false,"minBindingSize":80,"type":"uniform"}}],"label":"SkinnedImp Bake View BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d-array"}},{"binding":1,"visibility":2,"sampler":{"type":"filtering"}}],"label":"SkinnedImp Atlas BGL"},{"entries":[{"binding":0,"visibility":3,"buffer":{"hasDynamicOffset":false,"minBindingSize":112,"type":"uniform"}}],"label":"SkinnedImp Params BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}}],"label":"WalkerFollowPatch BGL"},{"entries":[{"binding":0,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":720,"type":"uniform"}},{"binding":1,"visibility":4,"buffer":{"hasDynamicOffset":false,"minBindingSize":304,"type":"uniform"}},{"binding":2,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"read-only-storage"}},{"binding":3,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":4,"visibility":4,"buffer":{"hasDynamicOffset":false,"type":"storage"}},{"binding":5,"visibility":4,"texture":{"multisampled":false,"sampleType":"unfilterable-float","viewDimension":"2d"}}],"label":"SkinnedHizCull BGL"},{"entries":[{"binding":0,"visibility":2,"sampler":{"type":"filtering"}},{"binding":1,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":2,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":3,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}},{"binding":4,"visibility":2,"texture":{"multisampled":false,"sampleType":"depth","viewDimension":"2d"}},{"binding":5,"visibility":2,"buffer":{"hasDynamicOffset":false,"minBindingSize":48,"type":"uniform"}},{"binding":6,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"TAAU BGL"},{"entries":[{"binding":0,"visibility":2,"texture":{"multisampled":false,"sampleType":"float","viewDimension":"2d"}}],"label":"TAAU RCAS BGL"}],"layouts":[{"bindGroupLayouts":[{"__bgl":2}],"label":"BoneCompute PL"},{"bindGroupLayouts":[{"__bgl":5}],"label":"SkyGen Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":4},{"__bgl":6},{"__bgl":7},{"__bgl":8}],"label":"Skybox Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":9},{"__bgl":10}],"label":"Cluster Build PL"},{"bindGroupLayouts":[{"__bgl":12}],"label":"LocalShadow PL"},{"bindGroupLayouts":[{"__bgl":12},{"__bgl":1}],"label":"LocalShadow PL"},{"bindGroupLayouts":[],"label":"LocalShadow Clear PL"},{"bindGroupLayouts":[{"__bgl":14}],"label":"StaticLodDebug Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":15}],"label":"StaticLodGlobalPrefixClassify Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":16}],"label":"StaticLodGlobalPrefixWriteCounts Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":17}],"label":"StaticLodGlobalPrefixScatterPacked Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":18}],"label":"StaticLodScatterIndirect Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":19}],"label":"StaticLodShadowClassify Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":20}],"label":"StaticLodShadowScatter Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":21}],"label":"StaticLodShadowWriteIndirect Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":24},{"__bgl":22}],"label":"StaticLodShadow Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":24},{"__bgl":22},{"__bgl":0}],"label":"StaticLodShadowAlpha Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":23}],"label":"StaticLod DepthPrepass Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":25}],"label":"Prefix Sum Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":26}],"label":"Prefix Sum Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":27}],"label":"Shadow PL"},{"bindGroupLayouts":[{"__bgl":27},{"__bgl":0}],"label":"Shadow PL"},{"bindGroupLayouts":[{"__bgl":27},{"__bgl":1}],"label":"Shadow PL"},{"bindGroupLayouts":[{"__bgl":27},{"__bgl":1},{"__bgl":0}],"label":"Shadow PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":0}],"label":"GBuffer Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":0},{"__bgl":23}],"label":"GBuffer LOD Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":29},{"__bgl":0}],"label":"GBuffer Material16 Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":29},{"__bgl":1},{"__bgl":0}],"label":"GBuffer Material16 (Skinned) Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":30},{"__bgl":31}],"label":"Forward Material16 Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":30},{"__bgl":31},{"__bgl":1}],"label":"Forward Material16 (Skinned) Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":29},{"__bgl":0},{"__bgl":23}],"label":"GBuffer Material16 LOD Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":30},{"__bgl":31},{"__bgl":23}],"label":"Forward Material16 LOD Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":0},{"__bgl":1}],"label":"GBuffer Skinned Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":0}],"label":"Forward Unlit Blend Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":0},{"__bgl":1}],"label":"Forward Unlit Blend (Skinned) Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":32}],"label":"SMAA Edges PL"},{"bindGroupLayouts":[{"__bgl":33}],"label":"SMAA Blend PL"},{"bindGroupLayouts":[{"__bgl":34}],"label":"SMAA Neighborhood PL"},{"bindGroupLayouts":[{"__bgl":35}],"label":"SMAA DebugReadback PL"},{"bindGroupLayouts":[{"__bgl":36}],"label":"Upscale PL"},{"bindGroupLayouts":[{"__bgl":38}],"label":"Tonemap PL"},{"bindGroupLayouts":[{"__bgl":39}],"label":"SelectionOutline PipelineLayout"},{"bindGroupLayouts":[{"__bgl":40}],"label":"HighlightOutline PipelineLayout"},{"bindGroupLayouts":[{"__bgl":41}],"label":"Decal Global Cull PipelineLayout"},{"bindGroupLayouts":[{"__bgl":42}],"label":"Decal Bucket Ranges PipelineLayout"},{"bindGroupLayouts":[{"__bgl":43},{"__bgl":3}],"label":"Decal Global Texture PipelineLayout"},{"bindGroupLayouts":[{"__bgl":44},{"__bgl":3},{"__bgl":45}],"label":"Decal Material16 Global PipelineLayout"},{"bindGroupLayouts":[{"__bgl":47}],"label":"Terrain Tile Bake PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":46},{"__bgl":48}],"label":"Terrain Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":46},{"__bgl":48}],"label":"Terrain Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":46},{"__bgl":49}],"label":"Terrain Pick Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":50}],"label":"ring_heights"},{"bindGroupLayouts":[{"__bgl":51}],"label":"ring_normals"},{"bindGroupLayouts":[{"__bgl":52}],"label":"ring_weights_layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":46},{"__bgl":53}],"label":"Terrain Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":55}],"label":"GrassInteraction Debug PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":54},{"__bgl":56}],"label":"Grass GBuffer Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":54},{"__bgl":56}],"label":"Grass GBuffer Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":54},{"__bgl":56}],"label":"Grass GBuffer Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":58},{"__bgl":59}],"label":"GrassSpawn PL"},{"bindGroupLayouts":[{"__bgl":58},{"__bgl":59}],"label":"GrassSpawn PL"},{"bindGroupLayouts":[{"__bgl":60}],"label":"GrassCull PL"},{"bindGroupLayouts":[{"__bgl":61}],"label":"CompactGrass PL"},{"bindGroupLayouts":[{"__bgl":62}],"label":"GrassEnrich PL"},{"bindGroupLayouts":[{"__bgl":63}],"label":"GrassIndirectWrite PL"},{"bindGroupLayouts":[{"__bgl":64}],"label":"LeafCull"},{"bindGroupLayouts":[{"__bgl":65}],"label":"LeafCompact"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":66}],"label":"Leaf GBuffer PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":67}],"label":"Leaf Impostor PL"},{"bindGroupLayouts":[{"__bgl":71}],"label":"OceanRipples PL"},{"bindGroupLayouts":[{"__bgl":72}],"label":"Downsample Depth Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":73},{"__bgl":3}],"label":"SSAO Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":74}],"label":"Blur Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":75}],"label":"SSAO Upsample Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":76}],"label":"Motion Vector PL"},{"bindGroupLayouts":[{"__bgl":77}],"label":"SSR Downsample PL"},{"bindGroupLayouts":[{"__bgl":78}],"label":"SSR Hi-Z PL"},{"bindGroupLayouts":[{"__bgl":79},{"__bgl":3}],"label":"SSR Trace PL"},{"bindGroupLayouts":[{"__bgl":80}],"label":"SSR Filter PL"},{"bindGroupLayouts":[{"__bgl":81}],"label":"SSR Spatial Resolve PL"},{"bindGroupLayouts":[{"__bgl":82}],"label":"SSR History Copy PL"},{"bindGroupLayouts":[{"__bgl":83},{"__bgl":3}],"label":"SSR Resolve PL"},{"bindGroupLayouts":[{"__bgl":84}],"label":"Bloom Prefilter PL"},{"bindGroupLayouts":[{"__bgl":85}],"label":"Bloom Down PL"},{"bindGroupLayouts":[{"__bgl":86}],"label":"Bloom Up PL"},{"bindGroupLayouts":[{"__bgl":87},{"__bgl":28},{"__bgl":3},{"__bgl":88}],"label":"volumetric_march_pl"},{"bindGroupLayouts":[{"__bgl":89}],"label":"volumetric_composite_pl"},{"bindGroupLayouts":[{"__bgl":90}],"label":"Particle Composite PL"},{"bindGroupLayouts":[{"__bgl":70},{"__bgl":28},{"__bgl":3},{"__bgl":13}],"label":"Main Pass Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":91},{"__bgl":28}],"label":"Water Pipeline Layout (reload)"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":92},{"__bgl":28}],"label":"Ocean Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":93}],"label":"Ocean MV Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":94}],"label":"OceanFft Init"},{"bindGroupLayouts":[{"__bgl":94}],"label":"OceanFft Evolve"},{"bindGroupLayouts":[{"__bgl":95}],"label":"OceanFft H"},{"bindGroupLayouts":[{"__bgl":95}],"label":"OceanFft V"},{"bindGroupLayouts":[{"__bgl":96}],"label":"OceanFft Deriv"},{"bindGroupLayouts":[{"__bgl":97}],"label":"OceanFft Mip"},{"bindGroupLayouts":[{"__bgl":98}],"label":"StabilityProbe PL"},{"bindGroupLayouts":[{"__bgl":100},{"__bgl":101},{"__bgl":103}],"label":"particle_spawn_layout"},{"bindGroupLayouts":[{"__bgl":99},{"__bgl":101},{"__bgl":103}],"label":"particle_update_layout"},{"bindGroupLayouts":[{"__bgl":99},{"__bgl":103}],"label":"particle_dead_append_layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":99},{"__bgl":104},{"__bgl":102}],"label":"particle_pipeline_layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Custom Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Custom Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Custom Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Lines Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":105}],"label":"debug_texture_bind_group_layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Gizmo Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":106}],"label":"Gizmo blit Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":107},{"__bgl":108}],"label":"UiTextPipelineLayout"},{"bindGroupLayouts":[{"__bgl":109}],"label":"UiFramePipelineLayout"},{"bindGroupLayouts":[{"__bgl":109},{"__bgl":110},{"__bgl":111}],"label":"UiFrameMaterialPipelineLayout"},{"bindGroupLayouts":[{"__bgl":112},{"__bgl":113}],"label":"UiImagePipelineLayout"},{"bindGroupLayouts":[{"__bgl":114}],"label":"UiHsvPipelineLayout"},{"bindGroupLayouts":[{"__bgl":115}],"label":"UiVecIconPipelineLayout"},{"bindGroupLayouts":[{"__bgl":116}],"label":"UiImageResamplePipelineLayout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":117}],"label":"Pick Static Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":117},{"__bgl":1}],"label":"Pick Skinned Layout"},{"bindGroupLayouts":[{"__bgl":3}],"label":"Pick Proxy Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":117},{"__bgl":118}],"label":"Pick Static Filter Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":117},{"__bgl":1},{"__bgl":118}],"label":"Pick Skinned Filter Layout"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":118}],"label":"Pick Proxy Filter Layout"},{"bindGroupLayouts":[{"__bgl":120},{"__bgl":119},{"__bgl":121}],"label":"Pencil Cull PL"},{"bindGroupLayouts":[{"__bgl":120},{"__bgl":122},{"__bgl":123}],"label":"Skinned Pencil Cull PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":124}],"label":"MV Static PL"},{"bindGroupLayouts":[{"__bgl":125}],"label":"Downsample Depth Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":126},{"__bgl":3}],"label":"SSAO Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":127}],"label":"Blur Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":128}],"label":"SSAO Upsample Pipeline Layout"},{"bindGroupLayouts":[{"__bgl":129}],"label":"Motion Vector PL"},{"bindGroupLayouts":[{"__bgl":130}],"label":"SSR Downsample PL"},{"bindGroupLayouts":[{"__bgl":131}],"label":"SSR Hi-Z PL"},{"bindGroupLayouts":[{"__bgl":132},{"__bgl":3}],"label":"SSR Trace PL"},{"bindGroupLayouts":[{"__bgl":133}],"label":"SSR Filter PL"},{"bindGroupLayouts":[{"__bgl":134}],"label":"SSR Spatial Resolve PL"},{"bindGroupLayouts":[{"__bgl":135}],"label":"SSR History Copy PL"},{"bindGroupLayouts":[{"__bgl":136},{"__bgl":3}],"label":"SSR Resolve PL"},{"bindGroupLayouts":[{"__bgl":137}],"label":"Bloom Prefilter PL"},{"bindGroupLayouts":[{"__bgl":138}],"label":"Bloom Down PL"},{"bindGroupLayouts":[{"__bgl":139}],"label":"Bloom Up PL"},{"bindGroupLayouts":[{"__bgl":140},{"__bgl":0},{"__bgl":1}],"label":"SkinnedImp Bake PL"},{"bindGroupLayouts":[{"__bgl":3},{"__bgl":141},{"__bgl":142}],"label":"SkinnedImp Draw PL"},{"bindGroupLayouts":[{"__bgl":143}],"label":"WalkerFollowPatch PL"},{"bindGroupLayouts":[{"__bgl":144}],"label":"SkinnedHizCull PL"},{"bindGroupLayouts":[{"__bgl":145}],"label":"TAAU PL"},{"bindGroupLayouts":[{"__bgl":146}],"label":"TAAU RCAS PL"}],"_meta":{"pipelines":184,"modules":137,"bgls":147,"layouts":147,"source_url":"http://127.0.0.1:62043/game.html"}}