Phase 6.3: RT ambient occlusion with bilateral blur

- 8 cosine-weighted hemisphere rays per pixel (inline ray queries, SM 6.5)
- Distance-weighted AO: quadratic falloff (1-hitT/aoRadius)² instead of binary hit/miss
- World-space hash seed: voxel coord + tangent-plane frac position (stable, no flicker)
- Bilateral blur pipeline: 2-pass separable (H+V), radius 6, depth+normal edge-stopping
- 4-pass dispatch: shadow+rawAO → blur H → blur V → apply
- AO written to separate R8_UNORM texture, blurred, then applied to color buffer
- Debug mode (F5 x3): grayscale AO visualization
This commit is contained in:
Samuel Bouchet 2026-03-29 09:31:19 +02:00
parent 6b41da0932
commit 9de53e5293
7 changed files with 449 additions and 123 deletions

View file

@ -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]

View file

@ -67,6 +67,8 @@ add_custom_command(TARGET BVLEVoxels POST_BUILD
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelSmoothCS.cso
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelBLASExtractCS.cso
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelShadowCS.cso
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelAOBlurCS.cso
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelAOApplyCS.cso
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelCommon.hlsli.cso
COMMENT "Clearing stale voxel shader cache (forces recompilation from current .hlsl sources)"
)

View file

@ -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<float> aoBlurred : register(t0);
RWTexture2D<float4> 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<ApplyPush> 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;
}
}

View file

@ -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<float> aoInput : register(t0);
// Depth + normals for edge-stopping
Texture2D<float> depthTexture : register(t1);
Texture2D<float4> normalTexture : register(t2);
// Output blurred AO
RWTexture2D<float> 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<BlurPush> 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;
}

View file

@ -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<float> depthTexture : register(t0); // voxelDepth_ (D32_FLOAT as R3
Texture2D<float4> 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<float4> colorOutput : register(u0);
// UAV outputs
RWTexture2D<float4> colorOutput : register(u0); // voxelRT_ (shadow applied in-place)
RWTexture2D<float> 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<ShadowPush> 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;
// ── Shadow ray toward sun ──────────────────────────────────
float3 L = normalize(-sunDirection.xyz);
float NdotL = dot(N, L);
float shadowFactor = 1.0;
if (NdotL <= 0.0) {
shadowFactor = 0.3; // back-facing = fully in shadow
} else {
RayDesc ray;
ray.Origin = origin;
ray.Direction = L;
ray.TMin = 0.01;
ray.TMax = push.maxDistance;
ray.TMax = push.shadowMaxDist;
// Inline ray query: accept first hit (binary shadow, don't need closest)
RayQuery<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH> q;
q.TraceRayInline(tlas, 0, 0xFF, ray);
// With FLAG_OPAQUE geometry + ACCEPT_FIRST_HIT, Proceed() handles everything
while (q.Proceed()) {}
[loop] 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!)
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<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH> 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 *= 0.3;
color.rgb *= shadowFactor;
colorOutput[DTid.xy] = color;
}
} 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<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH> 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)
}
}
}
}

View file

@ -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
// ── Pass 1: Shadow + raw AO ────────────────────────────────────
{
GPUBarrier preBarriers[] = {
GPUBarrier::Image(&const_cast<Texture&>(depthBuffer),
ResourceState::DEPTHSTENCIL, ResourceState::SHADER_RESOURCE),
GPUBarrier::Image(&const_cast<Texture&>(renderTarget),
ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
GPUBarrier::Image(&aoRawTexture_,
ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
};
dev->Barrier(preBarriers, 2, cmd);
dev->Barrier(preBarriers, 3, cmd);
dev->BindComputeShader(&shadowShader_, 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
dev->BindUAV(&renderTarget, 0, cmd); // u0 = color
dev->BindUAV(&aoRawTexture_, 1, cmd); // u1 = raw AO output
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
// Push constants
struct ShadowPush {
uint32_t width;
uint32_t height;
float normalBias;
float maxDistance;
uint32_t width, height;
float normalBias, shadowMaxDist;
uint32_t debugMode;
uint32_t pad[7];
float aoRadius;
uint32_t aoRayCount;
float aoStrength;
uint32_t pad[4];
} 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;
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);
}
// Dispatch: 8×8 thread groups covering the screen
dev->Dispatch((w + 7) / 8, (h + 7) / 8, 1, 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);
// Post-barriers: restore states for Compose()
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
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<Texture&>(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);
}

View file

@ -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,