Phase 7.1: stylized lighting — hemisphere ambient, colored shadows, rim light, tone mapping

Wonderbox-inspired lighting overhaul across all 3 pixel shaders:
- Hemisphere ambient (sky blue above, warm brown below) replaces flat ambient
- RT shadows lerp toward blue-violet tint instead of plain darkening (factor 0.55)
- Rim light (fresnel) with warm golden color on silhouettes (30% on vegetation)
- Soft exponential tone mapping + saturation boost in final post-process pass
- CB parameters for all lighting values (skyAmbient, groundAmbient, shadowTint, etc.)
- Fog color/density centralized from CB instead of hardcoded per-shader
- Screenshot mode (CLI "screenshot"): fixed camera, AO convergence, auto-capture
- AO noise stability: world-space hash using voxel center + tangent-axis frac position
- AO distance-weighted falloff: continuous occlusion values instead of binary hit/miss
This commit is contained in:
Samuel Bouchet 2026-03-29 15:00:12 +02:00
parent 40560c25ef
commit 55c67686f2
10 changed files with 162 additions and 36 deletions

View file

@ -35,7 +35,7 @@ bvle-voxels/
│ ├── voxelBLASExtractCS.hlsl # Compute shader BLAS position extraction (Phase 6.1) │ ├── voxelBLASExtractCS.hlsl # Compute shader BLAS position extraction (Phase 6.1)
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3) │ ├── 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) │ ├── 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) │ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
└── CLAUDE.md └── CLAUDE.md
``` ```
@ -615,6 +615,18 @@ Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour
- Shadow maps + SSAO when RT not available - Shadow maps + SSAO when RT not available
- `CheckCapability(RAYTRACING)` gating - `CheckCapability(RAYTRACING)` gating
### Phase 7 - Stylized Lighting (Wonderbox-inspired) [EN COURS]
#### Phase 7.1 - Hemisphere Ambient + Colored Shadows + Rim Light + Tone Mapping [FAIT]
- **Hemisphere ambient** : `lerp(groundAmbient, skyAmbient, N.y * 0.5 + 0.5)` — warm brown below, cool blue above. Applied in all 3 pixel shaders (voxelPS, voxelSmoothPS, voxelTopingPS). Vegetation gets 1.5× ambient for inter-reflection
- **Colored shadows** : RT shadows lerp toward `shadowTint` (blue-violet) instead of just darkening. `shadowFactor=0.55` (softer than 0.3)
- **Rim light** : `pow(1 - NdotV, exponent) * intensity * rimColor`. Warm golden rim on silhouettes. Reduced to 30% on vegetation (thin geometry causes halos)
- **Tone mapping + saturation** : soft exponential tone mapping (`1 - exp(-c)`) + saturation boost in `voxelAOApplyCS.hlsl` (final post-process pass)
- **CB paramètres** : `skyAmbient`, `groundAmbient`, `shadowTint`, `fogColor`, `fogParams`, `rimColor`, `rimParams`, `toneMapParams` added to VoxelCB
- **Fog centralisé** : fog density + color from CB instead of hardcoded per-shader
- **Screenshot mode** : CLI argument `screenshot` → fixed camera, 60 frames AO convergence, save `bvle_screenshot.png`, quit. Small non-intrusive window (`SW_SHOWNOACTIVATE`). No HUD.
## Métriques cibles et résultats ## Métriques cibles et résultats
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) | | Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |

View file

@ -1,5 +1,5 @@
// BVLE Voxels - AO Apply Compute Shader (Phase 6.3) // BVLE Voxels - AO Apply + Tone Mapping Compute Shader (Phase 6.3 + 7)
// Multiplies the blurred AO factor onto the color buffer. // Final post-process pass: applies AO, saturation boost, and tone mapping.
#include "voxelCommon.hlsli" #include "voxelCommon.hlsli"
@ -14,6 +14,19 @@ struct ApplyPush {
}; };
[[vk::push_constant]] ConstantBuffer<ApplyPush> push : register(b999); [[vk::push_constant]] ConstantBuffer<ApplyPush> push : register(b999);
// Soft clamp tone mapping: linear up to shoulder, gentle roll-off to prevent harsh clipping
float3 softClampToneMap(float3 color, float exposure) {
color *= exposure;
// Soft exponential curve: gentler than Reinhard, preserves bright midtones
return 1.0 - exp(-color); // natural 1-e^-x curve
}
// Saturation adjustment in linear space
float3 adjustSaturation(float3 color, float saturation) {
float luma = dot(color, float3(0.2126, 0.7152, 0.0722));
return lerp(float3(luma, luma, luma), color, saturation);
}
[RootSignature(VOXEL_ROOTSIG)] [RootSignature(VOXEL_ROOTSIG)]
[numthreads(8, 8, 1)] [numthreads(8, 8, 1)]
void main(uint3 DTid : SV_DispatchThreadID) { void main(uint3 DTid : SV_DispatchThreadID) {
@ -26,7 +39,16 @@ void main(uint3 DTid : SV_DispatchThreadID) {
colorOutput[DTid.xy] = float4(ao, ao, ao, 1); colorOutput[DTid.xy] = float4(ao, ao, ao, 1);
} else { } else {
float4 color = colorOutput[DTid.xy]; float4 color = colorOutput[DTid.xy];
// Apply AO
color.rgb *= ao; color.rgb *= ao;
// Saturation boost (toneMapParams.x)
color.rgb = adjustSaturation(color.rgb, toneMapParams.x);
// Tone mapping (toneMapParams.y = exposure)
color.rgb = softClampToneMap(color.rgb, toneMapParams.y);
colorOutput[DTid.xy] = color; colorOutput[DTid.xy] = color;
} }
} }

View file

@ -53,6 +53,15 @@ cbuffer VoxelCB : register(b0) {
uint bleedMask; // bit N set = material N can bleed onto neighbors uint bleedMask; // bit N set = material N can bleed onto neighbors
uint resistBleedMask; // bit N set = material N resists bleed from neighbors uint resistBleedMask; // bit N set = material N resists bleed from neighbors
float windTime; // elapsed time for wind animation (seconds) float windTime; // elapsed time for wind animation (seconds)
// ── Stylized lighting (Phase 7) ──
float4 skyAmbient; // hemisphere ambient: sky (top) color
float4 groundAmbient; // hemisphere ambient: ground (bottom) color
float4 shadowTint; // colored shadow tint (blue-violet)
float4 fogColor; // atmospheric fog color
float4 fogParams; // x=density, y=unused, z=unused, w=unused
float4 rimColor; // rim/fresnel light color
float4 rimParams; // x=exponent, y=intensity, z=unused, w=unused
float4 toneMapParams; // x=saturation boost, y=exposure, z=unused, w=unused
}; };
// ── Indirect draw args (must match C++ IndirectDrawArgs, 20 bytes) ── // ── Indirect draw args (must match C++ IndirectDrawArgs, 20 bytes) ──

View file

@ -293,15 +293,22 @@ PSOutput main(PSInput input)
} }
// ── Lighting ── // ── Lighting ──
float3 ambient = float3(0.15, 0.18, 0.25); float hemiLerp = N.y * 0.5 + 0.5; // 0=down, 1=up
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
float3 diffuse = sunColor.rgb * NdotL; float3 diffuse = sunColor.rgb * NdotL;
float3 color = albedo * (ambient + diffuse); float3 color = albedo * (ambient + diffuse);
// ── Rim light ──
float3 V = normalize(cameraPosition.xyz - input.worldPos);
float NdotV = saturate(dot(N, V));
float rim = pow(1.0 - NdotV, rimParams.x) * rimParams.y;
color += rimColor.rgb * rim;
// ── Distance fog ── // ── Distance fog ──
float dist = length(input.worldPos - cameraPosition.xyz); float dist = length(input.worldPos - cameraPosition.xyz);
float fog = 1.0 - exp(-dist * 0.003); float fogDensity = fogParams.x;
float3 fogColor = float3(0.55, 0.70, 0.90); float fog = 1.0 - exp(-dist * fogDensity);
color = lerp(color, fogColor, saturate(fog)); color = lerp(color, fogColor.rgb, saturate(fog));
output.color = float4(color, 1.0); output.color = float4(color, 1.0);
output.normal = float4(N, 0.0); output.normal = float4(N, 0.0);

View file

@ -107,7 +107,7 @@ void main(uint3 DTid : SV_DispatchThreadID) {
float shadowFactor = 1.0; float shadowFactor = 1.0;
if (NdotL <= 0.0) { if (NdotL <= 0.0) {
shadowFactor = 0.3; // back-facing = fully in shadow shadowFactor = 0.55; // back-facing = fully in shadow
} else { } else {
RayDesc ray; RayDesc ray;
ray.Origin = origin; ray.Origin = origin;
@ -120,7 +120,7 @@ void main(uint3 DTid : SV_DispatchThreadID) {
[loop] while (q.Proceed()) {} [loop] while (q.Proceed()) {}
if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { if (q.CommittedStatus() == COMMITTED_TRIANGLE_HIT) {
shadowFactor = 0.3; shadowFactor = 0.55;
} }
} }
@ -205,7 +205,11 @@ void main(uint3 DTid : SV_DispatchThreadID) {
colorOutput[DTid.xy] = float4(1, 1, 1, 1); colorOutput[DTid.xy] = float4(1, 1, 1, 1);
} else { } else {
float4 color = colorOutput[DTid.xy]; float4 color = colorOutput[DTid.xy];
color.rgb *= shadowFactor; // Colored shadows: lerp toward shadow tint instead of just darkening
// shadowFactor=1 → no change, shadowFactor=0.3 → blend toward tinted shadow
float shadowAmount = 1.0 - shadowFactor; // 0=lit, 0.7=full shadow
float3 tintedColor = color.rgb * shadowTint.rgb; // shadow = original × tint color
color.rgb = lerp(color.rgb, tintedColor, shadowAmount);
colorOutput[DTid.xy] = color; colorOutput[DTid.xy] = color;
} }
} }

View file

@ -217,14 +217,21 @@ PSOutput main(PSInput input) {
// Lighting // Lighting
float3 L = normalize(-sunDirection.xyz); float3 L = normalize(-sunDirection.xyz);
float NdotL = max(dot(N, L), 0.0); float NdotL = max(dot(N, L), 0.0);
float3 ambient = float3(0.15, 0.18, 0.25); float hemiLerp = N.y * 0.5 + 0.5;
float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
float3 color = albedo * (sunColor.rgb * NdotL + ambient); float3 color = albedo * (sunColor.rgb * NdotL + ambient);
// ── Rim light ──
float3 V = normalize(cameraPosition.xyz - input.worldPos);
float NdotV = saturate(dot(N, V));
float rim = pow(1.0 - NdotV, rimParams.x) * rimParams.y;
color += rimColor.rgb * rim;
// Distance fog // Distance fog
float dist = length(input.worldPos - cameraPosition.xyz); float dist = length(input.worldPos - cameraPosition.xyz);
float fog = 1.0 - exp(-dist * 0.003); float fogDensity = fogParams.x;
float3 fogColor = float3(0.55, 0.70, 0.90); float fog = 1.0 - exp(-dist * fogDensity);
color = lerp(color, fogColor, saturate(fog)); color = lerp(color, fogColor.rgb, saturate(fog));
output.color = float4(color, 1.0); output.color = float4(color, 1.0);
output.normal = float4(N, 0.0); output.normal = float4(N, 0.0);

View file

@ -47,10 +47,12 @@ PSOutput main(PSInput input) {
// inspired by Airborn Trees (simonschreibt.de/gat/airborn-trees/) // inspired by Airborn Trees (simonschreibt.de/gat/airborn-trees/)
float3 lit; float3 lit;
float hemiLerp = N.y * 0.5 + 0.5;
float3 V = normalize(cameraPosition.xyz - input.worldPos);
if (input.materialID == 3u) { if (input.materialID == 3u) {
// Stone: classic Lambert + cool ambient (matches voxel PS) // Stone: classic Lambert + hemisphere ambient
float NdotL = max(rawNdotL, 0.0); float NdotL = max(rawNdotL, 0.0);
float3 ambient = float3(0.15, 0.18, 0.25); float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp);
lit = texColor * (sunColor.rgb * NdotL + ambient); lit = texColor * (sunColor.rgb * NdotL + ambient);
} else { } else {
// ── Vegetation: soft wrap lighting ────────────────────── // ── Vegetation: soft wrap lighting ──────────────────────
@ -64,20 +66,24 @@ PSOutput main(PSInput input) {
// Translucency: thin blades let light through from behind // Translucency: thin blades let light through from behind
// Stronger effect to reduce contrast when orbiting around grass // Stronger effect to reduce contrast when orbiting around grass
float3 V = normalize(cameraPosition.xyz - input.worldPos);
float backLight = saturate(dot(V, L)); float backLight = saturate(dot(V, L));
float transAmount = (1.0 - saturate(rawNdotL)) * 0.6; float transAmount = (1.0 - saturate(rawNdotL)) * 0.6;
float translucency = backLight * transAmount; float translucency = backLight * transAmount;
// Higher ambient for vegetation: grass blades bounce light between each other // Hemisphere ambient for vegetation, scaled up 1.5x for inter-reflection
// (simplified inter-reflection / GI), brighter than stone ambient float3 ambient = lerp(groundAmbient.rgb, skyAmbient.rgb, hemiLerp) * 1.5;
float3 ambient = float3(0.30, 0.33, 0.35);
// Wrap + translucency for soft, low-contrast vegetation shading // Wrap + translucency for soft, low-contrast vegetation shading
float3 diffuse = sunColor.rgb * (wrap * 0.88 + translucency * 0.55); float3 diffuse = sunColor.rgb * (wrap * 0.88 + translucency * 0.55);
lit = texColor * (diffuse + ambient); lit = texColor * (diffuse + ambient);
} }
// ── Rim light (reduced on vegetation — thin geometry causes halos) ──
float NdotV = saturate(dot(N, V));
float rimScale = (input.materialID == 3u) ? 1.0 : 0.3; // stone full, grass 30%
float rim = pow(1.0 - NdotV, rimParams.x) * rimParams.y * rimScale;
lit += rimColor.rgb * rim;
output.color = float4(lit, 1.0); output.color = float4(lit, 1.0);
output.normal = float4(N, 0.0); output.normal = float4(N, 0.0);
return output; return output;

View file

@ -1,5 +1,6 @@
#include "WickedEngine.h" #include "WickedEngine.h"
#include "voxel/VoxelRenderer.h" #include "voxel/VoxelRenderer.h"
#include "wiHelper.h"
#include <fstream> #include <fstream>
#include <DbgHelp.h> #include <DbgHelp.h>
#pragma comment(lib, "dbghelp.lib") #pragma comment(lib, "dbghelp.lib")
@ -124,6 +125,10 @@ int APIENTRY wWinMain(
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
// Parse arguments early so we can adjust window creation
wi::arguments::Parse(lpCmdLine);
bool isScreenshot = wi::arguments::HasArgument("screenshot");
WNDCLASSEXW wcex = {}; WNDCLASSEXW wcex = {};
wcex.cbSize = sizeof(WNDCLASSEX); wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.style = CS_HREDRAW | CS_VREDRAW;
@ -134,32 +139,33 @@ int APIENTRY wWinMain(
wcex.lpszClassName = L"BVLEVoxels"; wcex.lpszClassName = L"BVLEVoxels";
RegisterClassExW(&wcex); RegisterClassExW(&wcex);
// Screenshot mode: small minimized window to avoid interrupting user
HWND hWnd = CreateWindowW( HWND hWnd = CreateWindowW(
wcex.lpszClassName, wcex.lpszClassName,
L"BVLE Voxels - Prototype", isScreenshot ? L"BVLE Screenshot" : L"BVLE Voxels - Prototype",
WS_OVERLAPPEDWINDOW, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, isScreenshot ? 0 : CW_USEDEFAULT,
1920, 1080, isScreenshot ? 0 : 0,
isScreenshot ? 640 : 1920,
isScreenshot ? 480 : 1080,
nullptr, nullptr, hInstance, nullptr nullptr, nullptr, hInstance, nullptr
); );
ShowWindow(hWnd, SW_SHOWMAXIMIZED); // SW_SHOWNOACTIVATE: visible but doesn't steal focus (minimized windows don't render)
ShowWindow(hWnd, isScreenshot ? SW_SHOWNOACTIVATE : SW_SHOWMAXIMIZED);
// Initialize Wicked Engine (selects DX12 by default on Windows, Vulkan on Linux) // Initialize Wicked Engine
// Pass "vulkan" as command line argument to force Vulkan backend
// Pass "debugdevice" for D3D debug layer, "gpuvalidation" for GPU-based validation
application.SetWindow(hWnd); application.SetWindow(hWnd);
wi::arguments::Parse(lpCmdLine);
// Redirect Wicked Engine log to file // Redirect Wicked Engine log to file
wi::backlog::SetLogFile("bvle_backlog.txt"); wi::backlog::SetLogFile("bvle_backlog.txt");
// Info display // Info display (disabled in screenshot mode)
application.infoDisplay.active = true; application.infoDisplay.active = !isScreenshot;
application.infoDisplay.watermark = false; application.infoDisplay.watermark = false;
application.infoDisplay.resolution = true; application.infoDisplay.resolution = !isScreenshot;
application.infoDisplay.fpsinfo = true; application.infoDisplay.fpsinfo = !isScreenshot;
application.infoDisplay.heap_allocation_counter = true; application.infoDisplay.heap_allocation_counter = !isScreenshot;
// Check for "debug" argument to enable face-color debug mode // Check for "debug" argument to enable face-color debug mode
if (wi::arguments::HasArgument("debug")) { if (wi::arguments::HasArgument("debug")) {
@ -169,12 +175,17 @@ int APIENTRY wWinMain(
if (wi::arguments::HasArgument("debugsmooth")) { if (wi::arguments::HasArgument("debugsmooth")) {
renderPath.debugSmooth = true; renderPath.debugSmooth = true;
} }
// Screenshot mode: auto-position camera, wait for AO convergence, capture, quit
if (wi::arguments::HasArgument("screenshot")) {
renderPath.screenshotMode = true;
}
// Activate our custom voxel render path // Activate our custom voxel render path
application.ActivatePath(&renderPath); application.ActivatePath(&renderPath);
// Main loop // Main loop
MSG msg = { 0 }; MSG msg = { 0 };
static int screenshotFrameCounter = 0;
while (msg.message != WM_QUIT) while (msg.message != WM_QUIT)
{ {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
@ -183,6 +194,23 @@ int APIENTRY wWinMain(
} }
else { else {
application.Run(); application.Run();
// Screenshot mode: wait for rendering + AO convergence, then capture and quit
if (renderPath.screenshotMode) {
screenshotFrameCounter++;
// Only start counting convergence frames once we have actual rendered quads
bool hasRendered = renderPath.renderer.getGpuMeshQuadCount() > 0;
static int convergenceFrames = 0;
if (hasRendered) convergenceFrames++;
// Wait 60 frames after first render for AO temporal convergence
if (convergenceFrames == 60) {
bool ok = wi::helper::saveTextureToFile(
renderPath.getVoxelRT(), "bvle_screenshot.png");
wi::backlog::post(ok ? "Screenshot saved: bvle_screenshot.png"
: "Screenshot FAILED");
PostQuitMessage(0);
}
}
} }
} }

View file

@ -1441,7 +1441,7 @@ void VoxelRenderer::render(
cb.prevViewProjection = prevViewProjection_; // from last frame cb.prevViewProjection = prevViewProjection_; // from last frame
cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f); cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
cb.sunDirection = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f); // lower sun = longer cast shadows cb.sunDirection = XMFLOAT4(-0.7f, -0.4f, -0.3f, 0.0f); // lower sun = longer cast shadows
cb.sunColor = XMFLOAT4(1.2f, 1.1f, 0.9f, 1.0f); cb.sunColor = XMFLOAT4(1.35f, 1.15f, 0.75f, 1.0f); // warm golden sun
cb.chunkSize = (float)CHUNK_SIZE; cb.chunkSize = (float)CHUNK_SIZE;
cb.textureTiling = 0.25f; cb.textureTiling = 0.25f;
cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path
@ -1450,6 +1450,15 @@ void VoxelRenderer::render(
cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5); cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5);
cb.resistBleedMask = (1u << 1); cb.resistBleedMask = (1u << 1);
cb.windTime = windTime_; cb.windTime = windTime_;
// Stylized lighting (Phase 7) — Wonderbox-inspired, iteration 4
cb.skyAmbient = XMFLOAT4(0.65f, 0.68f, 0.75f, 0.0f); // very high ambient fill
cb.groundAmbient = XMFLOAT4(0.40f, 0.33f, 0.22f, 0.0f); // warm brown, high fill
cb.shadowTint = XMFLOAT4(0.55f, 0.45f, 0.72f, 0.0f); // purple-blue shadows
cb.fogColor = XMFLOAT4(0.80f, 0.75f, 0.60f, 1.0f); // warm sandy-golden fog
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.40f, 2.2f, 0.0f, 0.0f); // vivid saturation, high exposure
dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb)); dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
// Save current VP for next frame's temporal reprojection // Save current VP for next frame's temporal reprojection
XMStoreFloat4x4(&prevViewProjection_, vpMatrix); XMStoreFloat4x4(&prevViewProjection_, vpMatrix);
@ -2268,6 +2277,13 @@ void VoxelRenderPath::Start() {
} else { } else {
world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4); world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4);
} }
// Screenshot mode: fixed camera with good framing of terrain
if (screenshotMode) {
cameraPos = { 270.0f, 50.0f, 240.0f }; // above terrain, below sky
cameraPitch = -0.25f; // slight downward look
cameraYaw = 0.6f; // angled view for depth
}
if (renderer.isInitialized()) { if (renderer.isInitialized()) {
renderer.updateMeshes(world); renderer.updateMeshes(world);
} }
@ -2693,6 +2709,9 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
wi::image::Draw(&voxelRT_, fx, cmd); wi::image::Draw(&voxelRT_, fx, cmd);
} }
// No HUD in screenshot mode
if (screenshotMode) return;
// HUD overlay // HUD overlay
wi::font::Params fp; wi::font::Params fp;
fp.posX = 10; fp.posY = 10; fp.size = 20; fp.posX = 10; fp.posY = 10; fp.size = 20;

View file

@ -158,9 +158,18 @@ private:
float debugBlend; float debugBlend;
XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0 XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0
uint32_t chunkCount; uint32_t chunkCount;
uint32_t bleedMask; // bit N set = material N can bleed onto neighbors uint32_t bleedMask;
uint32_t resistBleedMask; // bit N set = material N resists bleed from neighbors uint32_t resistBleedMask;
float windTime; float windTime;
// Stylized lighting (Phase 7)
XMFLOAT4 skyAmbient;
XMFLOAT4 groundAmbient;
XMFLOAT4 shadowTint;
XMFLOAT4 fogColor;
XMFLOAT4 fogParams;
XMFLOAT4 rimColor;
XMFLOAT4 rimParams;
XMFLOAT4 toneMapParams;
}; };
wi::graphics::GPUBuffer constantBuffer_; wi::graphics::GPUBuffer constantBuffer_;
@ -307,6 +316,7 @@ public:
bool debugMode = false; bool debugMode = false;
bool debugSmooth = false; bool debugSmooth = false;
bool screenshotMode = false; // CLI "screenshot": auto-position camera, capture, quit
float cameraSpeed = 50.0f; float cameraSpeed = 50.0f;
float cameraSensitivity = 0.003f; float cameraSensitivity = 0.003f;
@ -315,6 +325,8 @@ public:
float cameraYaw = 0.0f; float cameraYaw = 0.0f;
bool mouseCaptured = false; bool mouseCaptured = false;
const wi::graphics::Texture& getVoxelRT() const { return voxelRT_; }
void Start() override; void Start() override;
void Update(float dt) override; void Update(float dt) override;
void Render() const override; void Render() const override;