bvle-voxels/shaders/voxelShadowCS.hlsl

106 lines
4 KiB
HLSL
Raw Normal View History

// 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.
#include "voxelCommon.hlsli"
// SRV bindings
Texture2D<float> depthTexture : register(t0); // voxelDepth_ (D32_FLOAT as R32_FLOAT SRV)
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);
// Push constants
struct ShadowPush {
uint width;
uint height;
float normalBias;
float maxDistance;
uint debugMode; // 0=normal, 1=debug visualization
uint pad[7];
};
[[vk::push_constant]] ConstantBuffer<ShadowPush> 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 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
return;
}
// Reconstruct world position from depth via inverse VP
float2 uv = (float2(DTid.xy) + 0.5) / float2(push.width, push.height);
float2 ndc = float2(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);
float4 clipPos = float4(ndc, depth, 1.0);
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;
// 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;
}
} 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)
}
}
}
}