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:
parent
6b41da0932
commit
9de53e5293
7 changed files with 449 additions and 123 deletions
28
CLAUDE.md
28
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
|||
32
shaders/voxelAOApplyCS.hlsl
Normal file
32
shaders/voxelAOApplyCS.hlsl
Normal 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;
|
||||
}
|
||||
}
|
||||
83
shaders/voxelAOBlurCS.hlsl
Normal file
83
shaders/voxelAOBlurCS.hlsl
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
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<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()) {}
|
||||
|
||||
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<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)
|
||||
}
|
||||
RayDesc ray;
|
||||
ray.Origin = origin;
|
||||
ray.Direction = L;
|
||||
ray.TMin = 0.01;
|
||||
ray.TMax = push.shadowMaxDist;
|
||||
|
||||
RayQuery<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH> 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<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 *= shadowFactor;
|
||||
colorOutput[DTid.xy] = color;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Texture&>(depthBuffer),
|
||||
ResourceState::DEPTHSTENCIL, ResourceState::SHADER_RESOURCE),
|
||||
GPUBarrier::Image(&const_cast<Texture&>(renderTarget),
|
||||
ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS),
|
||||
};
|
||||
dev->Barrier(preBarriers, 2, cmd);
|
||||
// ── 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, 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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue