From acf99354b3c931bffe5d1e8b4b33df9ca622b665 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 00:59:36 -0700 Subject: [PATCH] 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() --- assets/shaders/fxaa.frag.glsl | 12 +++++++++--- assets/shaders/fxaa.frag.spv | Bin 7764 -> 14932 bytes include/rendering/renderer.hpp | 2 ++ src/rendering/renderer.cpp | 17 +++++++++++++++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/assets/shaders/fxaa.frag.glsl b/assets/shaders/fxaa.frag.glsl index 158f2f98..3da10854 100644 --- a/assets/shaders/fxaa.frag.glsl +++ b/assets/shaders/fxaa.frag.glsl @@ -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); } diff --git a/assets/shaders/fxaa.frag.spv b/assets/shaders/fxaa.frag.spv index 7803f3e27e4b92579b95e608e8653b07d921062a..b87b3dee4c2bf8247c4e11b0939b6ffa4a51c031 100644 GIT binary patch literal 14932 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 literal 7764 zcmZXZd#u)V9mjti&VkS)dW3V(NIY=DP>2M)u*EqXXo|PRO>226o%9E`oB}mBa=@5@ znD-hJO)awGc&Szy*-}%dscBThO3l)-%AkujHE&z5*YkYe&whLQj?ccI&--)ze!iFI zIS->c2F6s?d#VGggQ|(6s>bN4IzTG0(T#cW87t11wPx+gS;rlJtOnz%rlB?mS7WM9 zX6bVmth$(~IvlzhIs`j$5>$)f#3w_JQ>w1&{Y|zSD0495ioF*K)={u-=EcO-SIoH6 znWsXI$WxkPL$;RW9gmlOQ;6XzSrL!kApV%-82;WVB~L{)VCn= zA;^0!dwFms*82BA`e>)lxV_BAGG{;XVa%>o{3AuSM`Dvpf8x`uud_M=-B`%=>!a-2 z#CnmBWbTKIVQrBS!8=)ZY~81^)ih*dYhNFcC((tYP-v(B2aA!I`IYUz!!^!eHlF*Q z-pqz~M4r=-HaswsHM+jJkT&A6nGau88y3W75J?;HU?&$@JlGO=3hQSlqZ_mDG!y)`%kS^vo zkk+bXybbSqK98*4c@Qtp%KISi$U?lPFi(X%|NT&G2H?e&J*P)Qp3zU>y@ zmGQoQsNm`xq;J&Pjo!Q`6wH10yqZJ*#5@t+JlZAZJa}=*(H?WSV8--(x5r!vX3SwI zF&Dv$OOE!KrxwhZ-jDW}r-2z$yTm*lUR-jt$6Q`8V|u^ZW3B)*rgn*WCcL=hNX)OW z2cE5U9aUw2dZ1wI!CYS*+XQBPb!;=3@$1-CVD6zM5=I%8;?n3vad=F@gl>ET9r_11Jn!M;>5 z-?+rs3~wK-YZIi+F6@05u7r%Ejd$=WX2)CH9d%nFG4G-LvXbXo_}rCX*A>iM!LBbD zrB_SYml@1AK>BO1-uGgN+`hN!-XxcAfjP~cJGdR**mEG?;`VcRGnnU4yPU&Y;Ke0} z{??M1I|^pZ+oATD-vl$Jc8Pfhytw3Ok9k+YjJXqPk9jwkF||v~d*H<-M`CWI-@ZRT zf_!g`l{e>Jc;l!`oFBuBOOC|J9lMsr+mjwB*o|PWsg7*}v!*(BGnnzktjjz0B$Trr z?5To#&V&65Ox%06p0lOzgi)L=WcNaO4`jXeO?y8xwdqEy{uxNUpC8(8DK$I`uWloh zyYU>nxG{1Mo`=`pdoUZ)CimtAc=diR$Noil?WaM0K5G;Em*CZlr~bdeYj6E~A=hfZ z(}TZ*X=87*rhUlzxF+?9_3wfwmVL6%iS;UaZJKXdvp@eSKE_g?Sg#e_SocM!ZD- z-WU6vSO=ol#xgt0x zpZfbbx4ylT;O&<>-`;*^hi|XCBcW-K@1T6%)DOV>4lp&|O8vCaHa63Xjq!X>W8?f< z`Fwnkx{YOQ?e*VT?%FJPYjDl>H~8FwyNC8T_w7V*eVX^Vc^>B>^RIEX)yH-|ym9Kb zgUI}A*s4#DhT)B)-X49F*@k3JiYiiw)z-DKW)vo zggN=_$H$-*kn!Zx?`81riDPMbvzM!fd~3{WEdBJ4-&ye4%lLh~_^7M<>2JNg#CwPu zjc*Ow8}mE##B=pYcx$!(bC@0WOZ;p|%sS*#$NBJnzBqFJF973T<2|!K{!O9Xa?Xe~ zjDTqqKWj~F=hXTrcG_6KKIXNy&oDc3-d7@vS(|)nTLo_nrdDkiAt-xyQ=4|F?QZO~ ziJ!G4wsUHG3_ESCS0D4{&RzoVnf9#aT=nvKB%U6vhUZ_S4t4gZd9It^LBCM+)3M1p z_8!>3b?D6RUYb*EeGRkb&B*q{IcN0`*n6g2$M4{`)^xwE>3&nw8T)Bujm)^;;lu*Ds&*eI2}cJwrFZ$4C5nNK7C3d{_D^yx)~vOTH^@0~41VPN-Yoi*7^E z#_v0MyS5{XyYKE#aye&z?7RD5&DvW>YQF{EUR(RE@D9(v_}3sYYnM;$Ux&AL_b;{Y z027xSPN-Y8??BL|Ui-I@#Z!B7Ij45dljqCzXm1_XI>PL*R{OjY60=tMeE!`9&p)iH z%0A@#(mjCQ1%C_3+x}f-?bX@K#B;VD&!uO|y|zZ}&1F45WOk%?KR_0<9{JRBFFgMm z^|-dwvkS1jo}Ykeug-cB&pGvYH{3sK)ZSd*E@%2@@Ybu1H9o-Xuts(FLt@q_Z;qUA z@tl!|;EiL9N0<{s{9#DU81i{b9)tI6`<56tHqO@WJ@)QuV=nFWPtM2TQ}>hb$tnH> zBxX+eyj4%bdnS`JHqOcEJEM(tXs^HZZDlr|^9K6od*K`5o3Ojs)WZIZU@Wo3dJ#ceb@f=r%)0jBV@_+;-aM}Bugs3D>o3UvfUHG6wY&`P z9yo&S1It=d%YHCz)me-2jb+UE{R`e$OfBpcgse5OUIo)uT|JgDv#x&<$DG!xy?OGx z#2fIgTbq3MdUYqz?TVgw>wl3moW4-#AH=l#E;QbubCZB^J1{0SY)*!o(J@dEh*^s{v`=<7=#~anzp_l4uOg1yZi!pG0#Tc&0%Ew zuP$p_2=Cg|Wo?V##U)49me`BI%~g+m3bL`)CHATC##Wcur@@O$j>NV`e-A$$-2B>Q zy-VO-ueSL<^b%zCj_l*5 o1v5vm)dgdsx6R*a*C6Pxz54m+@;j|M>sia}y>Nb)s^>xf0~8U`L;wH) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 07d8091f..a198b0c7 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -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; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 8dcf724c..67426ff3 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -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(ext.width), 1.0f / static_cast(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())