diff --git a/CLAUDE.md b/CLAUDE.md index 037f2e8..3a336ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,9 @@ bvle-voxels/ │ ├── voxelSmoothVS.hlsl # Vertex shader smooth Surface Nets (vertex pulling, t6) │ ├── voxelSmoothPS.hlsl # Pixel shader smooth (triplanar + material blending) │ ├── voxelBLASExtractCS.hlsl # Compute shader BLAS position extraction (Phase 6.1) -│ └── voxelShadowCS.hlsl # Compute shader RT shadows (inline ray queries, Phase 6.2) +│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3) +│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3) +│ └── voxelAOApplyCS.hlsl # Compute shader AO apply to color buffer (Phase 6.3) └── CLAUDE.md ``` @@ -583,15 +585,29 @@ Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour - Pre : `voxelDepth_` DEPTHSTENCIL→SHADER_RESOURCE + `voxelRT_` SHADER_RESOURCE→UAV - Post : `voxelDepth_` SHADER_RESOURCE→DEPTHSTENCIL + `voxelRT_` UAV→SHADER_RESOURCE - **Mode debug** (F5 × 2 = DBG) : rouge=shadow hit, vert=miss, bleu=back-facing, gris foncé=ciel -- **Toggle** : F5 cycle OFF→ON→DBG→OFF +- **Toggle** : F5 cycle OFF→ON→DBG_SHADOW→DBG_AO→OFF - **CB** : `inverseViewProjection` (float4x4) ajouté après `viewProjection` dans VoxelCB (HLSL + C++) - **Push constants** : width, height, normalBias, maxDistance, debugMode -#### Phase 6.3 - RT AO [A FAIRE] +#### Phase 6.3 - RT AO [FAIT] -- 4-8 hemisphere rays per pixel, short range -- Cosine-weighted random directions from normal -- Output: AO factor (R8_UNORM) +- **Intégré dans `voxelShadowCS.hlsl`** : 8 rayons hémisphère cosine-weighted par pixel + 1 rayon soleil +- **Distance-weighted AO** : `(1 - hitT/aoRadius)²` — falloff quadratique, valeurs continues au lieu de binaire hit/miss +- **World-space hash stable** : seed = `floor(worldPos - N*0.5)` (voxel solide derrière la surface) + `frac(dot(worldPos, T/B)) * 256` (position fractionnaire sur les axes tangents uniquement — l'axe normal est exclu car il oscille à cause de la précision du depth buffer) +- **Bilateral blur séparable** (`voxelAOBlurCS.hlsl`) : 2 passes H+V, rayon 6 (kernel 13×13), edge-stopping sur depth + normals +- **Pipeline 4 passes** : + 1. Shadow CS : shadow in-place sur `voxelRT_` + AO brut → `aoRawTexture_` (R8_UNORM, u1) + 2. Blur H : `aoRawTexture_` → `aoBlurredTexture_` (bilateral, depth/normal edge-stopping) + 3. Blur V : `aoBlurredTexture_` → `aoRawTexture_` (idem, direction verticale) + 4. Apply : `aoRawTexture_` × `voxelRT_` → modulation finale (ou debug AO grayscale si debugMode=2) +- **Frisvad orthonormal basis** : construction robuste de (T,B) depuis N pour le hemisphere sampling +- **Cosine-weighted hemisphere** : `sqrt(u1)` distribution pour importance sampling +- **Push constants** : width, height, normalBias, shadowMaxDist, debugMode, aoRadius, aoRayCount, aoStrength +- **Pièges résolus** : + - **Hash screen-space → suit la caméra** : résolu en utilisant uniquement des coordonnées world-space + - **Hash `asuint(worldPos)` → clignote** : trop sensible aux variations FP du depth buffer, résolu par quantification au voxel + tangent frac + - **Hash `frac(worldPos)` sur axe normal → clignote sur ~30% des faces** : l'axe normal est à une frontière entière (ex: face +Y à y=5.0000) où `frac()` oscille entre ~0 et ~1. Résolu en projetant sur T/B uniquement + - **`floor(worldPos + 0.5)` → artefact au milieu des faces** : la coordonnée traverse 0.5 au centre de la face. Résolu par offset `-N*0.5` pour atterrir dans le voxel solide #### Phase 6.4 - Fallback [A FAIRE] diff --git a/CMakeLists.txt b/CMakeLists.txt index 446744b..21e311d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,8 @@ add_custom_command(TARGET BVLEVoxels POST_BUILD $/shaders/hlsl6/voxel/voxelSmoothCS.cso $/shaders/hlsl6/voxel/voxelBLASExtractCS.cso $/shaders/hlsl6/voxel/voxelShadowCS.cso + $/shaders/hlsl6/voxel/voxelAOBlurCS.cso + $/shaders/hlsl6/voxel/voxelAOApplyCS.cso $/shaders/hlsl6/voxel/voxelCommon.hlsli.cso COMMENT "Clearing stale voxel shader cache (forces recompilation from current .hlsl sources)" ) diff --git a/shaders/voxelAOApplyCS.hlsl b/shaders/voxelAOApplyCS.hlsl new file mode 100644 index 0000000..80281dd --- /dev/null +++ b/shaders/voxelAOApplyCS.hlsl @@ -0,0 +1,32 @@ +// BVLE Voxels - AO Apply Compute Shader (Phase 6.3) +// Multiplies the blurred AO factor onto the color buffer. + +#include "voxelCommon.hlsli" + +Texture2D aoBlurred : register(t0); +RWTexture2D colorOutput : register(u0); + +struct ApplyPush { + uint width; + uint height; + uint debugMode; // 0=normal, 2=debug AO (show AO as grayscale) + uint pad[9]; +}; +[[vk::push_constant]] ConstantBuffer push : register(b999); + +[RootSignature(VOXEL_ROOTSIG)] +[numthreads(8, 8, 1)] +void main(uint3 DTid : SV_DispatchThreadID) { + if (DTid.x >= push.width || DTid.y >= push.height) return; + + float ao = aoBlurred[DTid.xy]; + + if (push.debugMode == 2) { + // Debug AO: grayscale visualization of blurred AO + colorOutput[DTid.xy] = float4(ao, ao, ao, 1); + } else { + float4 color = colorOutput[DTid.xy]; + color.rgb *= ao; + colorOutput[DTid.xy] = color; + } +} diff --git a/shaders/voxelAOBlurCS.hlsl b/shaders/voxelAOBlurCS.hlsl new file mode 100644 index 0000000..6e6a563 --- /dev/null +++ b/shaders/voxelAOBlurCS.hlsl @@ -0,0 +1,83 @@ +// BVLE Voxels - Bilateral AO Blur Compute Shader (Phase 6.3) +// Separable bilateral blur: preserves edges using depth + normal comparison. +// Run twice: horizontal (direction=0) then vertical (direction=1). + +#include "voxelCommon.hlsli" + +// Input AO (raw or partially blurred) +Texture2D aoInput : register(t0); + +// Depth + normals for edge-stopping +Texture2D depthTexture : register(t1); +Texture2D normalTexture : register(t2); + +// Output blurred AO +RWTexture2D aoOutput : register(u0); + +struct BlurPush { + uint width; + uint height; + uint direction; // 0 = horizontal, 1 = vertical + uint radius; // blur kernel radius (e.g. 4 = 9x1 kernel) + float depthThreshold; // edge-stopping depth sensitivity + float normalThreshold; // edge-stopping normal sensitivity (dot product) + uint pad[6]; +}; +[[vk::push_constant]] ConstantBuffer push : register(b999); + +[RootSignature(VOXEL_ROOTSIG)] +[numthreads(8, 8, 1)] +void main(uint3 DTid : SV_DispatchThreadID) { + if (DTid.x >= push.width || DTid.y >= push.height) return; + + float centerAO = aoInput[DTid.xy]; + float centerDepth = depthTexture[DTid.xy]; + float3 centerN = normalTexture[DTid.xy].xyz; + + // Skip sky pixels + if (centerDepth == 0.0) { + aoOutput[DTid.xy] = 1.0; + return; + } + + // Gaussian-ish weights (sigma ≈ radius/2) + float totalWeight = 1.0; + float totalAO = centerAO; + + int2 step = (push.direction == 0) ? int2(1, 0) : int2(0, 1); + int r = (int)push.radius; + + for (int i = -r; i <= r; i++) { + if (i == 0) continue; + + int2 coord = int2(DTid.xy) + step * i; + if (coord.x < 0 || coord.x >= (int)push.width || + coord.y < 0 || coord.y >= (int)push.height) + continue; + + float sampleAO = aoInput[coord]; + float sampleDepth = depthTexture[coord]; + float3 sampleN = normalTexture[coord].xyz; + + // Skip sky + if (sampleDepth == 0.0) continue; + + // Edge-stopping: depth difference + float depthDiff = abs(centerDepth - sampleDepth); + float depthWeight = exp(-depthDiff * depthDiff / (push.depthThreshold * push.depthThreshold)); + + // Edge-stopping: normal difference + float normalDot = max(0.0, dot(centerN, sampleN)); + float normalWeight = (normalDot > push.normalThreshold) ? normalDot : 0.0; + + // Spatial weight (Gaussian falloff) + float dist = float(abs(i)) / float(r + 1); + float spatialWeight = exp(-dist * dist * 2.0); + + float w = spatialWeight * depthWeight * normalWeight; + totalWeight += w; + totalAO += sampleAO * w; + } + + aoOutput[DTid.xy] = totalAO / totalWeight; +} diff --git a/shaders/voxelShadowCS.hlsl b/shaders/voxelShadowCS.hlsl index 33cd24f..2ed8cb9 100644 --- a/shaders/voxelShadowCS.hlsl +++ b/shaders/voxelShadowCS.hlsl @@ -1,6 +1,6 @@ -// BVLE Voxels - RT Shadow Compute Shader (Phase 6.2) -// Traces shadow rays from each pixel toward the sun using inline ray queries. -// Reads depth + normal to reconstruct world position, modulates voxelRT_ in-place. +// BVLE Voxels - RT Shadow + AO Compute Shader (Phase 6.2 + 6.3) +// Per-pixel: traces 1 shadow ray toward sun + N hemisphere rays for AO. +// Modulates voxelRT_ in-place via RWTexture2D. #include "voxelCommon.hlsli" @@ -9,29 +9,71 @@ Texture2D depthTexture : register(t0); // voxelDepth_ (D32_FLOAT as R3 Texture2D normalTexture : register(t1); // voxelNormalRT_ (R16G16B16A16_SNORM) RaytracingAccelerationStructure tlas : register(t2); // TLAS with blocky + smooth instances -// UAV: read-modify-write voxelRT_ (each thread handles exactly one pixel, no race) -RWTexture2D colorOutput : register(u0); +// UAV outputs +RWTexture2D colorOutput : register(u0); // voxelRT_ (shadow applied in-place) +RWTexture2D aoOutput : register(u1); // raw AO factor (blurred separately) // Push constants struct ShadowPush { uint width; uint height; float normalBias; - float maxDistance; - uint debugMode; // 0=normal, 1=debug visualization - uint pad[7]; + float shadowMaxDist; + uint debugMode; // 0=normal, 1=debug shadows, 2=debug AO + float aoRadius; // max distance for AO rays (e.g. 8.0 voxels) + uint aoRayCount; // number of hemisphere rays (e.g. 6) + float aoStrength; // how dark full occlusion is (e.g. 0.35 = 65% darkening) + uint pad[4]; }; [[vk::push_constant]] ConstantBuffer push : register(b999); +// ── Hash-based pseudo-random for AO ray directions ────────────── +// Golden ratio hash: deterministic, no texture lookup, good distribution +float hashF(uint x) { + x ^= x >> 16; + x *= 0x45d9f3bu; + x ^= x >> 16; + return float(x & 0xFFFFFF) / float(0xFFFFFF); +} + +uint hashU(uint a, uint b) { + a ^= b * 0x9E3779B9u; + a ^= a >> 16; + a *= 0x45d9f3bu; + return a; +} + +// Build orthonormal basis from normal (Frisvad's method, robust for all N) +void buildBasis(float3 N, out float3 T, out float3 B) { + if (N.z < -0.9999) { + T = float3(0, -1, 0); + B = float3(-1, 0, 0); + } else { + float a = 1.0 / (1.0 + N.z); + float b = -N.x * N.y * a; + T = float3(1.0 - N.x * N.x * a, b, -N.x); + B = float3(b, 1.0 - N.y * N.y * a, -N.y); + } +} + +// Cosine-weighted hemisphere sample (probability ∝ cos(θ)) +float3 cosineSampleHemisphere(float u1, float u2, float3 N, float3 T, float3 B) { + float r = sqrt(u1); + float phi = 6.28318530718 * u2; + float x = r * cos(phi); + float y = r * sin(phi); + float z = sqrt(max(0.0, 1.0 - u1)); + return normalize(x * T + y * B + z * N); +} + [RootSignature(VOXEL_ROOTSIG)] [numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { if (DTid.x >= push.width || DTid.y >= push.height) return; float depth = depthTexture[DTid.xy]; - // depth == 0 means sky (reverse-Z: 0 = far plane) if (depth == 0.0) { - if (push.debugMode > 0) colorOutput[DTid.xy] = float4(0.1, 0.1, 0.1, 1); // dark gray = sky + if (push.debugMode > 0) colorOutput[DTid.xy] = float4(0.1, 0.1, 0.1, 1); return; } @@ -42,64 +84,104 @@ void main(uint3 DTid : SV_DispatchThreadID) { float4 worldPos4 = mul(inverseViewProjection, clipPos); float3 worldPos = worldPos4.xyz / worldPos4.w; - // Read world-space normal float3 N = normalTexture[DTid.xy].xyz; - - // Light direction: sunDirection is the direction of travel, negate for "toward sun" - float3 L = normalize(-sunDirection.xyz); - - // Skip surfaces facing away from the light (self-shadowed by geometry) - float NdotL = dot(N, L); - if (NdotL <= 0.0) { - if (push.debugMode > 0) { - colorOutput[DTid.xy] = float4(0.0, 0.0, 0.5, 1); // blue = back-facing - } else { - float4 color = colorOutput[DTid.xy]; - color.rgb *= 0.3; - colorOutput[DTid.xy] = color; - } - return; - } - - // Offset ray origin along normal to avoid self-intersection float3 origin = worldPos + N * push.normalBias; - RayDesc ray; - ray.Origin = origin; - ray.Direction = L; - ray.TMin = 0.01; - ray.TMax = push.maxDistance; + // ── Shadow ray toward sun ────────────────────────────────── + float3 L = normalize(-sunDirection.xyz); + float NdotL = dot(N, L); - // Inline ray query: accept first hit (binary shadow, don't need closest) - RayQuery q; - q.TraceRayInline(tlas, 0, 0xFF, ray); - // With FLAG_OPAQUE geometry + ACCEPT_FIRST_HIT, Proceed() handles everything - while (q.Proceed()) {} - - if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { - if (push.debugMode > 0) { - colorOutput[DTid.xy] = float4(1.0, 0.0, 0.0, 1); // RED = shadow ray hit (cast shadow!) - } else { - float4 color = colorOutput[DTid.xy]; - color.rgb *= 0.3; - colorOutput[DTid.xy] = color; - } + float shadowFactor = 1.0; + if (NdotL <= 0.0) { + shadowFactor = 0.3; // back-facing = fully in shadow } else { - if (push.debugMode > 0) { - // Debug: trace downward ray from reconstructed worldPos to verify BLAS - RayDesc testRay; - testRay.Origin = worldPos + float3(0, 5, 0); // 5 units above surface - testRay.Direction = float3(0, -1, 0); // straight down - testRay.TMin = 0.01; - testRay.TMax = 100.0; - RayQuery testQ; - testQ.TraceRayInline(tlas, 0, 0xFF, testRay); - while (testQ.Proceed()) {} - if (testQ.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { - colorOutput[DTid.xy] = float4(1, 0, 0, 1); // RED = hit (BLAS works!) - } else { - colorOutput[DTid.xy] = float4(0, 1, 0, 1); // GREEN = miss (BLAS broken) - } + RayDesc ray; + ray.Origin = origin; + ray.Direction = L; + ray.TMin = 0.01; + ray.TMax = push.shadowMaxDist; + + RayQuery q; + q.TraceRayInline(tlas, 0, 0xFF, ray); + [loop] while (q.Proceed()) {} + + if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { + shadowFactor = 0.3; } } + + // ── AO: hemisphere rays ──────────────────────────────────── + float aoFactor = 1.0; + uint rayCount = push.aoRayCount; + if (rayCount > 0) { + float3 T, B; + buildBasis(N, T, B); + + // Fully world-space seed: solid voxel coord + tangent-plane frac position + // vc: offset by -N*0.5 → inside the solid voxel (stable per face) + // Sub-voxel: only use the 2 tangent axes (T,B), NOT the normal axis. + // The normal axis sits at an integer boundary (e.g. face +Y → y=5.0000) + // where frac() oscillates between ~0 and ~1 due to depth precision → flicker. + // Tangent axes vary smoothly across the face → always stable. + int3 vc = int3(floor(worldPos - N * 0.5)); + float tFrac = frac(dot(worldPos, T)); + float bFrac = frac(dot(worldPos, B)); + uint st = uint(tFrac * 256.0); + uint sb = uint(bFrac * 256.0); + uint baseSeed = hashU(hashU((uint)(vc.x + 32768), (uint)(vc.y + 32768)), (uint)(vc.z + 32768)); + uint pixelSeed = hashU(baseSeed, hashU(st, sb)); + float totalOcclusion = 0.0; + + [loop] + for (uint i = 0; i < rayCount; i++) { + // Per-ray random: hash(pixelSeed, rayIndex) + uint seed = hashU(pixelSeed, i); + float u1 = hashF(seed); + float u2 = hashF(seed ^ 0xA5A5A5A5u); + + float3 dir = cosineSampleHemisphere(u1, u2, N, T, B); + + RayDesc aoRay; + aoRay.Origin = origin; + aoRay.Direction = dir; + aoRay.TMin = 0.05; // larger TMin for AO to avoid edge self-intersection + aoRay.TMax = push.aoRadius; + + RayQuery aoQ; + aoQ.TraceRayInline(tlas, 0, 0xFF, aoRay); + [loop] while (aoQ.Proceed()) {} + + if (aoQ.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { + // Distance-weighted: close hits = strong occlusion, far hits = weak + float hitT = aoQ.CommittedRayT(); + float falloff = 1.0 - saturate(hitT / push.aoRadius); + totalOcclusion += falloff * falloff; // quadratic for natural falloff + } + } + + float occlusionRatio = totalOcclusion / float(rayCount); + aoFactor = 1.0 - occlusionRatio * push.aoStrength; + } + + // ── Write AO to separate buffer (will be blurred), apply shadow in-place ── + aoOutput[DTid.xy] = aoFactor; + + if (push.debugMode == 1) { + // Debug shadows: red=shadow, green=lit, blue=backface + if (NdotL <= 0.0) + colorOutput[DTid.xy] = float4(0, 0, 0.5, 1); + else if (shadowFactor < 1.0) + colorOutput[DTid.xy] = float4(1, 0, 0, 1); + else + colorOutput[DTid.xy] = float4(0, 1, 0, 1); + } else if (push.debugMode == 2) { + // Debug AO: raw AO written to aoOutput, will be visualized after blur + // Write white to color so blur apply pass shows AO only + colorOutput[DTid.xy] = float4(1, 1, 1, 1); + } else { + // Apply shadow only — AO applied after blur in a separate pass + float4 color = colorOutput[DTid.xy]; + color.rgb *= shadowFactor; + colorOutput[DTid.xy] = color; + } } diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index 4395405..ee1cf22 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -197,14 +197,16 @@ void VoxelRenderer::initialize(GraphicsDevice* dev) { rtAvailable_ = false; wi::backlog::post("VoxelRenderer: RT available but BLAS extraction shader failed", wi::backlog::LogLevel::Warning); } - // ── RT Shadows (Phase 6.2) ──────────────────────────────────── + // ── RT Shadows + AO (Phase 6.2 + 6.3) ──────────────────────── wi::renderer::LoadShader(ShaderStage::CS, shadowShader_, "voxel/voxelShadowCS.cso", wi::graphics::ShaderModel::SM_6_5); - if (shadowShader_.IsValid()) { + wi::renderer::LoadShader(ShaderStage::CS, aoBlurShader_, "voxel/voxelAOBlurCS.cso"); + wi::renderer::LoadShader(ShaderStage::CS, aoApplyShader_, "voxel/voxelAOApplyCS.cso"); + if (shadowShader_.IsValid() && aoBlurShader_.IsValid() && aoApplyShader_.IsValid()) { rtShadowsEnabled_ = true; - wi::backlog::post("VoxelRenderer: RT shadows available"); + wi::backlog::post("VoxelRenderer: RT shadows + AO blur available"); } else { - wi::backlog::post("VoxelRenderer: RT shadow shader failed to compile", + wi::backlog::post("VoxelRenderer: RT shadow/AO shader(s) failed to compile", wi::backlog::LogLevel::Warning); } } else { @@ -1200,7 +1202,7 @@ void VoxelRenderer::buildAccelerationStructures(CommandList cmd) const { rtDirty_ = false; } -// ── RT Shadow dispatch (Phase 6.2) ────────────────────────────── +// ── RT Shadow + AO dispatch (Phase 6.2 + 6.3) ────────────────── void VoxelRenderer::dispatchShadows(CommandList cmd, const Texture& depthBuffer, const Texture& renderTarget, @@ -1212,48 +1214,136 @@ void VoxelRenderer::dispatchShadows(CommandList cmd, auto* dev = device_; uint32_t w = renderTarget.GetDesc().width; uint32_t h = renderTarget.GetDesc().height; + uint32_t gx = (w + 7) / 8; + uint32_t gy = (h + 7) / 8; - // Pre-barriers: - // - voxelDepth_: DEPTHSTENCIL → SHADER_RESOURCE (for depth reads) - // - voxelRT_: SHADER_RESOURCE → UNORDERED_ACCESS (for in-place shadow modulation) - // - voxelNormalRT_ is already in SHADER_RESOURCE state from render pass - GPUBarrier preBarriers[] = { - GPUBarrier::Image(&const_cast(depthBuffer), - ResourceState::DEPTHSTENCIL, ResourceState::SHADER_RESOURCE), - GPUBarrier::Image(&const_cast(renderTarget), - ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS), - }; - dev->Barrier(preBarriers, 2, cmd); + // ── Pass 1: Shadow + raw AO ──────────────────────────────────── + { + GPUBarrier preBarriers[] = { + GPUBarrier::Image(&const_cast(depthBuffer), + ResourceState::DEPTHSTENCIL, ResourceState::SHADER_RESOURCE), + GPUBarrier::Image(&const_cast(renderTarget), + ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS), + GPUBarrier::Image(&aoRawTexture_, + ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS), + }; + dev->Barrier(preBarriers, 3, cmd); - dev->BindComputeShader(&shadowShader_, cmd); + dev->BindComputeShader(&shadowShader_, cmd); + dev->BindResource(&depthBuffer, 0, cmd); // t0 = depth + dev->BindResource(&normalTarget, 1, cmd); // t1 = normals + dev->BindResource(&tlas_, 2, cmd); // t2 = TLAS + dev->BindUAV(&renderTarget, 0, cmd); // u0 = color + dev->BindUAV(&aoRawTexture_, 1, cmd); // u1 = raw AO output + dev->BindConstantBuffer(&constantBuffer_, 0, cmd); - // Bind resources - dev->BindResource(&depthBuffer, 0, cmd); // t0 = depth - dev->BindResource(&normalTarget, 1, cmd); // t1 = normals - dev->BindResource(&tlas_, 2, cmd); // t2 = TLAS - dev->BindUAV(&renderTarget, 0, cmd); // u0 = color (read-modify-write) - dev->BindConstantBuffer(&constantBuffer_, 0, cmd); // b0 = VoxelCB + struct ShadowPush { + uint32_t width, height; + float normalBias, shadowMaxDist; + uint32_t debugMode; + float aoRadius; + uint32_t aoRayCount; + float aoStrength; + uint32_t pad[4]; + } pushData = {}; + pushData.width = w; + pushData.height = h; + pushData.normalBias = 0.15f; + pushData.shadowMaxDist = 512.0f; + pushData.debugMode = rtShadowDebug_; + pushData.aoRadius = 8.0f; + pushData.aoRayCount = 8; + pushData.aoStrength = 0.7f; + dev->PushConstants(&pushData, sizeof(pushData), cmd); + dev->Dispatch(gx, gy, 1, cmd); + } - // Push constants - struct ShadowPush { - uint32_t width; - uint32_t height; - float normalBias; - float maxDistance; - uint32_t debugMode; - uint32_t pad[7]; - } pushData = {}; - pushData.width = w; - pushData.height = h; - pushData.normalBias = 0.15f; // offset along normal to avoid self-intersection - pushData.maxDistance = 512.0f; // max shadow ray distance - pushData.debugMode = rtShadowDebug_ ? 1 : 0; - dev->PushConstants(&pushData, sizeof(pushData), cmd); + // ── Pass 2: Bilateral blur horizontal (aoRaw → aoBlurred) ────── + { + GPUBarrier barriers[] = { + GPUBarrier::Image(&aoRawTexture_, + ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE), + GPUBarrier::Image(&aoBlurredTexture_, + ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS), + }; + dev->Barrier(barriers, 2, cmd); - // Dispatch: 8×8 thread groups covering the screen - dev->Dispatch((w + 7) / 8, (h + 7) / 8, 1, cmd); + dev->BindComputeShader(&aoBlurShader_, cmd); + dev->BindResource(&aoRawTexture_, 0, cmd); // t0 = AO input + dev->BindResource(&depthBuffer, 1, cmd); // t1 = depth + dev->BindResource(&normalTarget, 2, cmd); // t2 = normals + dev->BindUAV(&aoBlurredTexture_, 0, cmd); // u0 = AO output - // Post-barriers: restore states for Compose() + struct BlurPush { + uint32_t width, height, direction, radius; + float depthThreshold, normalThreshold; + uint32_t pad[6]; + } blurPush = {}; + blurPush.width = w; + blurPush.height = h; + blurPush.direction = 0; // horizontal + blurPush.radius = 6; + blurPush.depthThreshold = 0.001f; + blurPush.normalThreshold = 0.9f; + dev->PushConstants(&blurPush, sizeof(blurPush), cmd); + dev->Dispatch(gx, gy, 1, cmd); + } + + // ── Pass 3: Bilateral blur vertical (aoBlurred → aoRaw) ──────── + { + GPUBarrier barriers[] = { + GPUBarrier::Image(&aoBlurredTexture_, + ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE), + GPUBarrier::Image(&aoRawTexture_, + ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS), + }; + dev->Barrier(barriers, 2, cmd); + + dev->BindComputeShader(&aoBlurShader_, cmd); + dev->BindResource(&aoBlurredTexture_, 0, cmd); // t0 = AO input (H-blurred) + dev->BindResource(&depthBuffer, 1, cmd); + dev->BindResource(&normalTarget, 2, cmd); + dev->BindUAV(&aoRawTexture_, 0, cmd); // u0 = AO output (fully blurred) + + struct BlurPush { + uint32_t width, height, direction, radius; + float depthThreshold, normalThreshold; + uint32_t pad[6]; + } blurPush = {}; + blurPush.width = w; + blurPush.height = h; + blurPush.direction = 1; // vertical + blurPush.radius = 6; + blurPush.depthThreshold = 0.001f; + blurPush.normalThreshold = 0.9f; + dev->PushConstants(&blurPush, sizeof(blurPush), cmd); + dev->Dispatch(gx, gy, 1, cmd); + } + + // ── Pass 4: Apply blurred AO to color ────────────────────────── + { + GPUBarrier barriers[] = { + GPUBarrier::Image(&aoRawTexture_, + ResourceState::UNORDERED_ACCESS, ResourceState::SHADER_RESOURCE), + }; + dev->Barrier(barriers, 1, cmd); + + dev->BindComputeShader(&aoApplyShader_, cmd); + dev->BindResource(&aoRawTexture_, 0, cmd); // t0 = blurred AO + dev->BindUAV(&renderTarget, 0, cmd); // u0 = color + + struct ApplyPush { + uint32_t width, height, debugMode; + uint32_t pad[9]; + } applyPush = {}; + applyPush.width = w; + applyPush.height = h; + applyPush.debugMode = rtShadowDebug_; + dev->PushConstants(&applyPush, sizeof(applyPush), cmd); + dev->Dispatch(gx, gy, 1, cmd); + } + + // ── Restore resource states ──────────────────────────────────── GPUBarrier postBarriers[] = { GPUBarrier::Image(&const_cast(depthBuffer), ResourceState::SHADER_RESOURCE, ResourceState::DEPTHSTENCIL), @@ -2263,7 +2353,21 @@ void VoxelRenderPath::createRenderTargets() { depthDesc.layout = wi::graphics::ResourceState::DEPTHSTENCIL; device->CreateTexture(&depthDesc, nullptr, &voxelDepth_); - rtCreated_ = voxelRT_.IsValid() && voxelNormalRT_.IsValid() && voxelDepth_.IsValid(); + // AO textures (R8_UNORM) for bilateral blur pipeline + wi::graphics::TextureDesc aoDesc; + aoDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D; + aoDesc.width = w; + aoDesc.height = h; + aoDesc.format = wi::graphics::Format::R8_UNORM; + aoDesc.bind_flags = wi::graphics::BindFlag::SHADER_RESOURCE | wi::graphics::BindFlag::UNORDERED_ACCESS; + aoDesc.mip_levels = 1; + aoDesc.sample_count = 1; + aoDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE; + device->CreateTexture(&aoDesc, nullptr, &renderer.aoRawTexture_); + device->CreateTexture(&aoDesc, nullptr, &renderer.aoBlurredTexture_); + + rtCreated_ = voxelRT_.IsValid() && voxelNormalRT_.IsValid() && voxelDepth_.IsValid() + && renderer.aoRawTexture_.IsValid() && renderer.aoBlurredTexture_.IsValid(); wi::backlog::post("VoxelRenderPath: render targets " + std::string(rtCreated_ ? "OK" : "FAILED") + " (" + std::to_string(w) + "x" + std::to_string(h) + ")"); } @@ -2291,18 +2395,21 @@ void VoxelRenderPath::handleInput(float dt) { wi::backlog::post(renderer.debugBlend_ ? "Blend debug: ON" : "Blend debug: OFF"); } if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F5)) { - // Cycle: OFF → ON → DEBUG → OFF + // Cycle: OFF → ON → DBG_SHADOW → DBG_AO → OFF if (!renderer.rtShadowsEnabled_) { renderer.rtShadowsEnabled_ = true; - renderer.rtShadowDebug_ = false; - wi::backlog::post("RT Shadows: ON"); - } else if (!renderer.rtShadowDebug_) { - renderer.rtShadowDebug_ = true; - wi::backlog::post("RT Shadows: DEBUG (red=shadow, green=lit, blue=backface)"); + renderer.rtShadowDebug_ = 0; + wi::backlog::post("RT Shadows+AO: ON"); + } else if (renderer.rtShadowDebug_ == 0) { + renderer.rtShadowDebug_ = 1; + wi::backlog::post("RT Debug: SHADOWS (red=shadow, green=lit, blue=backface)"); + } else if (renderer.rtShadowDebug_ == 1) { + renderer.rtShadowDebug_ = 2; + wi::backlog::post("RT Debug: AO (white=open, black=occluded)"); } else { renderer.rtShadowsEnabled_ = false; - renderer.rtShadowDebug_ = false; - wi::backlog::post("RT Shadows: OFF"); + renderer.rtShadowDebug_ = 0; + wi::backlog::post("RT Shadows+AO: OFF"); } } if (wi::input::Press(wi::input::MOUSE_BUTTON_RIGHT)) { @@ -2613,7 +2720,7 @@ void VoxelRenderPath::Compose(CommandList cmd) const { stats += "RT: TLAS ready | Blocky " + std::to_string(renderer.getRTBlockyTriCount()) + " tris | Smooth " + std::to_string(renderer.getRTSmoothTriCount()) + " tris" - + " | Shadows " + std::string(renderer.rtShadowDebug_ ? "DEBUG" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF")) + "\n"; + + " | Shadows+AO " + std::string(renderer.rtShadowDebug_ == 1 ? "DBG_SHD" : (renderer.rtShadowDebug_ == 2 ? "DBG_AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF"))) + "\n"; } else { stats += "RT: building...\n"; } @@ -2623,7 +2730,7 @@ void VoxelRenderPath::Compose(CommandList cmd) const { stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n"; stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF") + "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") - + "] | F5: shadows [" + std::string(renderer.rtShadowDebug_ ? "DBG" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF")) + "]"; + + "] | F5: shd+ao [" + std::string(renderer.rtShadowDebug_ == 1 ? "SHD" : (renderer.rtShadowDebug_ == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF"))) + "]"; wi::font::Draw(stats, fp, cmd); } diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index 675e1f7..3a681f3 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -206,10 +206,14 @@ private: void dispatchBLASExtract(wi::graphics::CommandList cmd) const; void buildAccelerationStructures(wi::graphics::CommandList cmd) const; - // ── RT Shadows (Phase 6.2) ───────────────────────────────────── + // ── RT Shadows + AO (Phase 6.2 + 6.3) ────────────────────────── wi::graphics::Shader shadowShader_; // voxelShadowCS compute shader + wi::graphics::Shader aoBlurShader_; // voxelAOBlurCS compute shader + wi::graphics::Shader aoApplyShader_; // voxelAOApplyCS compute shader + mutable wi::graphics::Texture aoRawTexture_; // R8_UNORM: raw AO from shadow CS + mutable wi::graphics::Texture aoBlurredTexture_; // R8_UNORM: after bilateral blur mutable bool rtShadowsEnabled_ = false; // true when shader + TLAS ready - mutable bool rtShadowDebug_ = false; // debug visualization mode + mutable uint32_t rtShadowDebug_ = 0; // 0=off, 1=debug shadows, 2=debug AO void dispatchShadows(wi::graphics::CommandList cmd, const wi::graphics::Texture& depthBuffer,