Add debug tools

This commit is contained in:
Samuel Bouchet 2026-04-01 18:12:58 +02:00
parent 4c50727cb6
commit d5bf499375
4 changed files with 123 additions and 19 deletions

View file

@ -106,7 +106,7 @@ float3 triplanarWeights(float3 normal, float sharpness) {
// Triplanar sampling — RGB only (non-blended path)
float3 sampleTriplanar(float3 worldPos, float3 normal, uint texIndex, float tiling) {
float3 w = triplanarWeights(normal, 4.0);
float3 colX = materialTextures.Sample(materialSampler, float3(worldPos.yz * tiling, (float)texIndex)).rgb;
float3 colX = materialTextures.Sample(materialSampler, float3(worldPos.zy * tiling, (float)texIndex)).rgb;
float3 colY = materialTextures.Sample(materialSampler, float3(worldPos.xz * tiling, (float)texIndex)).rgb;
float3 colZ = materialTextures.Sample(materialSampler, float3(worldPos.xy * tiling, (float)texIndex)).rgb;
return colX * w.x + colY * w.y + colZ * w.z;
@ -115,7 +115,7 @@ float3 sampleTriplanar(float3 worldPos, float3 normal, uint texIndex, float tili
// Triplanar sampling — RGBA (includes heightmap in alpha)
float4 sampleTriplanarRGBA(float3 worldPos, float3 normal, uint texIndex, float tiling) {
float3 w = triplanarWeights(normal, 4.0);
float4 colX = materialTextures.Sample(materialSampler, float3(worldPos.yz * tiling, (float)texIndex));
float4 colX = materialTextures.Sample(materialSampler, float3(worldPos.zy * tiling, (float)texIndex));
float4 colY = materialTextures.Sample(materialSampler, float3(worldPos.xz * tiling, (float)texIndex));
float4 colZ = materialTextures.Sample(materialSampler, float3(worldPos.xy * tiling, (float)texIndex));
return colX * w.x + colY * w.y + colZ * w.z;
@ -340,22 +340,27 @@ PSOutput main(PSInput input)
}
// ── Normal map perturbation ──
float3 flatN = N; // preserve flat face normal for ambient + side-darkening
float nmStrength = toneMapParams.z; // 0 = off (F9 toggle)
if (nmStrength > 0.0) {
float3 perturbedN = sampleTriplanarNormal(input.worldPos, N, texIndex, tiling);
// Blend between flat and perturbed normal (strength control)
N = normalize(lerp(N, perturbedN, 0.7));
N = normalize(lerp(N, perturbedN, nmStrength));
}
// ── Lighting ──
// Use FLAT normal for hemisphere ambient + side-darkening (consistent per face)
// Use PERTURBED normal for NdotL only (organic detail variation)
float3 L = normalize(-sunDirection.xyz);
float NdotL = max(dot(N, L), 0.0);
float hemiLerp = N.y * 0.5 + 0.5; // 0=down, 1=up
float hemiLerp = flatN.y * 0.5 + 0.5; // flat: consistent per face orientation
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
float3 diffuse = sunColor.rgb * NdotL;
// Grass-specific shading (Wonderbox style)
bool isGrass = (texIndex == 0); // material 1 = grass = texture layer 0
if (isGrass) {
// Vertical face darkening: grass sides are darker green (not black)
float verticalDarken = saturate(abs(N.y)); // 1=top, 0=side
// Vertical face darkening: use FLAT normal for consistency
float verticalDarken = saturate(abs(flatN.y)); // 1=top, 0=side
float sideFactor = lerp(0.60, 1.0, verticalDarken); // sides at 60% brightness
albedo *= sideFactor;
@ -369,6 +374,39 @@ PSOutput main(PSInput input)
ambient *= 1.15;
}
// ── Debug lighting modes (F9 cycle) ──
uint dbgLight = (uint)toneMapParams.w;
if (dbgLight == 2) {
// FLAT: uniform color per face, no texture, no blend, no normal map
// Pure lighting with flat face normal. If two +X faces differ here, it's a VS/mesher bug.
float flatNdotL = max(dot(flatN, normalize(-sunDirection.xyz)), 0.0);
float flatHemi = flatN.y * 0.5 + 0.5;
float3 flatAmb = lerp(groundAmbient.rgb, skyAmbient.rgb, flatHemi);
float3 flatColor = float3(0.5, 0.5, 0.5) * (flatAmb + sunColor.rgb * flatNdotL);
output.color = float4(flatColor, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 3) {
// ALBEDO only: texture + blend, no lighting
output.color = float4(albedo, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 4) {
// NdotL only: grayscale NdotL with flat normal (no normal map)
float flatNdotL = max(dot(flatN, normalize(-sunDirection.xyz)), 0.0);
output.color = float4(flatNdotL, flatNdotL, flatNdotL, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 5) {
// NORMAL viz: geometric normal mapped to RGB (XYZ → [0,1])
output.color = float4(flatN * 0.5 + 0.5, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
float3 color = albedo * (ambient + diffuse);
// ── Rim light ──

View file

@ -77,7 +77,7 @@ float3 triplanarWeights(float3 n, float sharpness) {
float3 sampleTriplanar(float3 wp, float3 n, uint texIdx, float tiling) {
float3 w = triplanarWeights(n, 4.0);
float3 cx = materialTextures.Sample(texSampler, float3(wp.yz * tiling, (float)texIdx)).rgb;
float3 cx = materialTextures.Sample(texSampler, float3(wp.zy * tiling, (float)texIdx)).rgb;
float3 cy = materialTextures.Sample(texSampler, float3(wp.xz * tiling, (float)texIdx)).rgb;
float3 cz = materialTextures.Sample(texSampler, float3(wp.xy * tiling, (float)texIdx)).rgb;
return cx * w.x + cy * w.y + cz * w.z;
@ -85,7 +85,7 @@ float3 sampleTriplanar(float3 wp, float3 n, uint texIdx, float tiling) {
float4 sampleTriplanarRGBA(float3 wp, float3 n, uint texIdx, float tiling) {
float3 w = triplanarWeights(n, 4.0);
float4 cx = materialTextures.Sample(texSampler, float3(wp.yz * tiling, (float)texIdx));
float4 cx = materialTextures.Sample(texSampler, float3(wp.zy * tiling, (float)texIdx));
float4 cy = materialTextures.Sample(texSampler, float3(wp.xz * tiling, (float)texIdx));
float4 cz = materialTextures.Sample(texSampler, float3(wp.xy * tiling, (float)texIdx));
return cx * w.x + cy * w.y + cz * w.z;
@ -249,13 +249,49 @@ PSOutput main(PSInput input) {
}
// ── Normal map perturbation ──
float3 flatN = N; // preserve for ambient
float nmStrength = toneMapParams.z;
if (nmStrength > 0.0) {
float3 perturbedN = sampleTriplanarNormal(input.worldPos, geoN, selfTexIdx, tiling);
N = normalize(lerp(N, perturbedN, 0.5)); // lighter strength on smooth surfaces
N = normalize(lerp(N, perturbedN, nmStrength * 0.7)); // lighter on smooth
}
// Lighting
// ── Debug lighting modes (F9 cycle) ──
uint dbgLight = (uint)toneMapParams.w;
if (dbgLight == 2) {
// FLAT: uniform gray, no texture, no normal map — pure lighting with geometric normal
float flatNdotL = max(dot(flatN, normalize(-sunDirection.xyz)), 0.0);
float flatHemi = flatN.y * 0.5 + 0.5;
float3 flatAmb = lerp(groundAmbient.rgb, skyAmbient.rgb, flatHemi);
float3 flatColor = float3(0.5, 0.5, 0.5) * (flatAmb + sunColor.rgb * flatNdotL);
output.color = float4(flatColor, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 3) {
// ALBEDO only: texture + blend, no lighting
output.color = float4(albedo, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 4) {
// NdotL only: grayscale NdotL with geometric normal (no normal map)
float flatNdotL = max(dot(flatN, normalize(-sunDirection.xyz)), 0.0);
output.color = float4(flatNdotL, flatNdotL, flatNdotL, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
if (dbgLight == 5) {
// NORMAL viz: geometric normal mapped to RGB (XYZ → [0,1])
output.color = float4(flatN * 0.5 + 0.5, 1.0);
output.normal = float4(flatN, 0.0);
return output;
}
// Lighting: flat normal for ambient (consistent), perturbed for NdotL (detail)
float3 L = normalize(-sunDirection.xyz);
float NdotL = max(dot(N, L), 0.0);
float hemiLerp = N.y * 0.5 + 0.5;
float hemiLerp = flatN.y * 0.5 + 0.5;
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
float3 color = albedo * (sunColor.rgb * NdotL + ambient);

View file

@ -785,7 +785,7 @@ void VoxelRenderer::render(
cb.fogParams = XMFLOAT4(0.004f, 0.0f, 0.0f, 0.0f); // fog density
cb.rimColor = XMFLOAT4(0.90f, 0.80f, 0.55f, 0.0f); // warm golden rim
cb.rimParams = XMFLOAT4(2.5f, 0.45f, 0.0f, 0.0f); // exponent, intensity
cb.toneMapParams = XMFLOAT4(1.10f, 1.8f, 0.0f, 0.0f); // natural saturation, balanced exposure
cb.toneMapParams = XMFLOAT4(1.10f, 1.8f, normalStrength_, (float)debugLighting_); // saturation, exposure, normalStrength, debugLighting
dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
// Save current VP for next frame's temporal reprojection
XMStoreFloat4x4(&rt_.prevViewProjection, vpMatrix);
@ -1519,6 +1519,18 @@ void VoxelRenderPath::Update(float dt) {
anim_.showCrosshair = !anim_.showCrosshair;
wi::backlog::post(anim_.showCrosshair ? "Crosshair + debug: ON" : "Crosshair + debug: OFF");
}
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F9)) {
anim_.debugLighting = (anim_.debugLighting + 1) % AnimationState::DEBUG_LIGHTING_MODES;
static const char* modeNames[] = {
"F9: ALL ON (normals+lighting+blend)",
"F9: Normal maps OFF",
"F9: FLAT lighting (no tex, no blend, no nmap)",
"F9: ALBEDO only (texture, no lighting)",
"F9: NdotL only (grayscale, flat normal)",
"F9: NORMAL viz (RGB = XYZ geometric normal)"
};
wi::backlog::post(modeNames[anim_.debugLighting]);
}
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_F5)) {
if (!renderer.rt_.isShadowsEnabled()) {
renderer.rt_.setShadowsEnabled(true); renderer.rt_.setShadowDebug(0);
@ -1538,6 +1550,8 @@ void VoxelRenderPath::Update(float dt) {
if (camera) camera_.handleInput(dt, camera);
anim_.windTime += dt;
renderer.windTime_ = anim_.windTime;
renderer.normalStrength_ = (anim_.debugLighting == 0) ? 0.7f : 0.0f;
renderer.debugLighting_ = anim_.debugLighting;
// Sun direction: fixed or orbiting (F7)
if (anim_.sunOrbit) {
@ -1757,8 +1771,8 @@ void VoxelRenderPath::Render() const {
renderer.renderSmooth(cmd, voxelDepth_, voxelRT_, voxelNormalRT_);
device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_DRAW_END, cmd);
// Phase 6.2: RT Shadows + AO
if (renderer.isRTShadowsEnabled() && renderer.isRTReady()) {
// Phase 6.2: RT Shadows + AO (skip in F9 debug modes 2-4 for raw diagnostic output)
if (renderer.isRTShadowsEnabled() && renderer.isRTReady() && renderer.debugLighting_ < 2) {
device->QueryEnd(&renderer.timestampHeap_, VoxelRenderer::TS_RT_SHADOWS_BEGIN, cmd);
renderer.rt_.dispatchShadows(cmd, voxelDepth_, voxelRT_, voxelNormalRT_,
renderer.constantBuffer_);
@ -1934,7 +1948,13 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF")
+ "] | F5: shd+ao [" + std::string(renderer.rt_.getShadowDebug() == 1 ? "SHD" : (renderer.rt_.getShadowDebug() == 2 ? "AO" : (renderer.isRTShadowsEnabled() ? "ON" : "OFF")))
+ "] | F7: sun [" + std::string(anim_.sunOrbit ? "ORBIT" : "FIXED")
+ "] | F8: xhair [" + std::string(anim_.showCrosshair ? "ON" : "OFF") + "]";
+ "] | F8: xhair [" + std::string(anim_.showCrosshair ? "ON" : "OFF")
+ "] | F9: " + std::string(
anim_.debugLighting == 0 ? "ALL" :
anim_.debugLighting == 1 ? "noNMAP" :
anim_.debugLighting == 2 ? "FLAT" :
anim_.debugLighting == 3 ? "ALBEDO" :
anim_.debugLighting == 4 ? "NdotL" : "NORMAL");
wi::font::Draw(stats, fp, cmd);
@ -2088,7 +2108,7 @@ std::string VoxelRenderPath::buildDebugLog() const {
"FPS: %.1f (%.2f ms)\n"
"Chunks: %u/%u Quads: %u GPU Mesh: %u\n"
"Smooth verts: %u Toping instances: %zu\n"
"Animation: %s | Blend debug: %s | Sun orbit: %s | Crosshair: %s\n"
"Animation: %s | Blend debug: %s | Sun orbit: %s | Crosshair: %s | F9: %s\n"
"RT available: %s | RT shadows+AO: %s | RT debug: %s\n"
"Debug face mode: %s | Debug smooth: %s\n",
smoothFps_, lastDt_ * 1000.0f,
@ -2099,6 +2119,11 @@ std::string VoxelRenderPath::buildDebugLog() const {
renderer.debugBlend_ ? "ON" : "OFF",
anim_.sunOrbit ? "ORBIT" : "FIXED",
anim_.showCrosshair ? "ON" : "OFF",
anim_.debugLighting == 0 ? "ALL" :
anim_.debugLighting == 1 ? "noNMAP" :
anim_.debugLighting == 2 ? "FLAT" :
anim_.debugLighting == 3 ? "ALBEDO" :
anim_.debugLighting == 4 ? "NdotL" : "NORMAL",
renderer.isRTAvailable() ? "yes" : "no",
renderer.isRTShadowsEnabled() ? "ON" : "OFF",
renderer.rt_.getShadowDebug() == 1 ? "SHADOWS" :

View file

@ -64,6 +64,8 @@ public:
bool debugFaceColors_ = false;
bool debugBlend_ = false;
float windTime_ = 0.0f; // set by VoxelRenderPath::Update each frame
float normalStrength_ = 0.7f; // normal map strength (0=off)
int debugLighting_ = 0; // 0=all, 1=no nmap, 2=flat, 3=albedo, 4=NdotL
XMFLOAT4 sunDirection_ = { -0.7f, -0.4f, -0.3f, 0.0f }; // set by VoxelRenderPath::Update
private:
@ -303,6 +305,9 @@ struct AnimationState {
bool terrainAnimated = false; // toggled with F3
bool sunOrbit = false; // toggled with F7: sun orbits in ~10s cycle
bool showCrosshair = true; // toggled with F8: crosshair + face debug info
// F9 debug cycle: 0=all ON, 1=normals OFF, 2=flat lighting, 3=albedo only, 4=NdotL only, 5=normal viz
int debugLighting = 0;
static constexpr int DEBUG_LIGHTING_MODES = 6;
float time = 0.0f; // current animation time offset
float accum = 0.0f; // accumulator for 30 Hz timer
static constexpr float INTERVAL = 1.0f / 30.0f; // ~33.3ms = 30 Hz