feat: add ghost mode grayscale screen effect

- FXAA path: repurpose _pad field as 'desaturate' push constant; when
  ghostMode_ is true, convert final pixel to grayscale with slight cool
  blue tint using luma(0.299,0.587,0.114) mix
- Non-FXAA path: apply a high-opacity gray overlay (rgba 0.5,0.5,0.55,0.82)
  over the scene for a washed-out look
- Both parallel (SEC_POST) and single-threaded render paths covered
- ghostMode_ flag set each frame from gameHandler->isPlayerGhost()
This commit is contained in:
Kelsi 2026-03-13 00:59:36 -07:00
parent d3159791de
commit acf99354b3
4 changed files with 26 additions and 5 deletions

View file

@ -2,7 +2,7 @@
// FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass.
// Reads the resolved scene color and outputs a smoothed result.
// Push constant: rcpFrame = vec2(1/width, 1/height), sharpness (0=off, 2=max), unused.
// Push constant: rcpFrame = vec2(1/width, 1/height), sharpness (0=off, 2=max), desaturate (1=ghost grayscale).
layout(set = 0, binding = 0) uniform sampler2D uScene;
@ -11,8 +11,8 @@ layout(location = 0) out vec4 outColor;
layout(push_constant) uniform PC {
vec2 rcpFrame;
float sharpness; // 0 = no sharpen, 2 = max (matches FSR2 RCAS range)
float _pad;
float sharpness; // 0 = no sharpen, 2 = max (matches FSR2 RCAS range)
float desaturate; // 1 = full grayscale (ghost mode), 0 = normal color
} pc;
// Quality tuning
@ -145,5 +145,11 @@ void main() {
fxaaResult = clamp(fxaaResult + s * (fxaaResult - blur), 0.0, 1.0);
}
// Ghost mode: desaturate to grayscale (with a slight cool blue tint).
if (pc.desaturate > 0.5) {
float gray = dot(fxaaResult, vec3(0.299, 0.587, 0.114));
fxaaResult = mix(fxaaResult, vec3(gray, gray, gray * 1.05), pc.desaturate);
}
outColor = vec4(fxaaResult, 1.0);
}

Binary file not shown.

View file

@ -638,6 +638,8 @@ private:
bool terrainEnabled = true;
bool terrainLoaded = false;
bool ghostMode_ = false; // set each frame from gameHandler->isPlayerGhost()
// CPU timing stats (last frame/update).
double lastUpdateMs = 0.0;
double lastRenderMs = 0.0;

View file

@ -5044,7 +5044,7 @@ void Renderer::renderFXAAPass() {
vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
// Pass rcpFrame + sharpness (vec4, 16 bytes).
// Pass rcpFrame + sharpness + desaturate (vec4, 16 bytes).
// When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the
// post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes.
float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f;
@ -5052,7 +5052,7 @@ void Renderer::renderFXAAPass() {
1.0f / static_cast<float>(ext.width),
1.0f / static_cast<float>(ext.height),
sharpness,
0.0f
ghostMode_ ? 1.0f : 0.0f // desaturate: 1=ghost grayscale, 0=normal
};
vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc);
@ -5092,6 +5092,9 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
lastWMORenderMs = 0.0;
lastM2RenderMs = 0.0;
// Cache ghost state for use in overlay and FXAA passes this frame.
ghostMode_ = (gameHandler && gameHandler->isPlayerGhost());
uint32_t frameIdx = vkCtx->getCurrentFrame();
VkDescriptorSet perFrameSet = perFrameDescSets[frameIdx];
const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f);
@ -5237,6 +5240,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
renderOverlay(tint, cmd);
}
}
// Ghost mode desaturation overlay (non-FXAA path approximation).
// When FXAA is active the FXAA shader applies true per-pixel desaturation;
// otherwise a high-opacity gray overlay gives a similar washed-out effect.
if (ghostMode_ && overlayPipeline && !fxaa_.enabled) {
renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f), cmd);
}
if (minimap && minimap->isEnabled() && camera && window) {
glm::vec3 minimapCenter = camera->getPosition();
if (cameraController && cameraController->isThirdPerson())
@ -5369,6 +5378,10 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
renderOverlay(tint);
}
}
// Ghost mode desaturation overlay (non-FXAA path approximation).
if (ghostMode_ && overlayPipeline && !fxaa_.enabled) {
renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f));
}
if (minimap && minimap->isEnabled() && camera && window) {
glm::vec3 minimapCenter = camera->getPosition();
if (cameraController && cameraController->isThirdPerson())