From 5a227c0376b860b3024ac3313e47c90685f1c4c1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 19:15:34 -0800 Subject: [PATCH] Add water refraction toggle with per-frame scene history Fix VK_ERROR_DEVICE_LOST crash by allocating per-frame scene history images (color + depth) instead of a single shared image that raced between frames in flight. Water refraction can now be toggled via Settings > Video > Water Refraction. Without refraction: richer blue base colors, animated caustic shimmer, and normal-based color shifts give the water visible life. With refraction: clean screen-space refraction with Beer-Lambert absorption. Disabling clears scene history to black for immediate fallback. --- assets/shaders/water.frag.glsl | 29 +- assets/shaders/water.frag.spv | Bin 32584 -> 34440 bytes include/rendering/renderer.hpp | 3 + include/rendering/water_renderer.hpp | 28 +- include/ui/game_screen.hpp | 2 + src/rendering/renderer.cpp | 33 ++- src/rendering/water_renderer.cpp | 402 ++++++++++++++++----------- src/ui/game_screen.cpp | 17 ++ 8 files changed, 323 insertions(+), 191 deletions(-) diff --git a/assets/shaders/water.frag.glsl b/assets/shaders/water.frag.glsl index ecd7ee1d..c7dbc5b4 100644 --- a/assets/shaders/water.frag.glsl +++ b/assets/shaders/water.frag.glsl @@ -226,11 +226,32 @@ void main() { float depthFade = 1.0 - exp(-verticalDepth * 0.15); vec3 waterBody = mix(shallowColor, deepColor, depthFade); - vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7); + // Detect if scene history is available (scene data captured for refraction) + float sceneBrightness = dot(sceneRefract, vec3(0.299, 0.587, 0.114)); + bool hasSceneData = (sceneBrightness > 0.003); - if (verticalDepth < 0.01) { - float opticalDepth = 1.0 - exp(-dist * 0.004); - refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6); + // Animated caustic shimmer — only without refraction (refraction already provides movement) + if (!hasSceneData) { + float caustic1 = noiseValue(FragPos.xy * 1.8 + time * vec2(0.3, 0.15)); + float caustic2 = noiseValue(FragPos.xy * 3.2 - time * vec2(0.2, 0.35)); + float causticPattern = caustic1 * 0.6 + caustic2 * 0.4; + vec3 causticTint = vec3(0.08, 0.18, 0.28) * smoothstep(0.35, 0.75, causticPattern); + waterBody += causticTint; + } + + vec3 refractedColor; + if (hasSceneData) { + refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7); + if (verticalDepth < 0.01) { + float opticalDepth = 1.0 - exp(-dist * 0.004); + refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6); + } + } else { + // No refraction data — use lit water body with animated variation + vec3 litWater = waterBody * (ambientColor.rgb * 0.8 + NdotL * lightColor.rgb * 0.6); + float normalShift = dot(detailNorm.xy, vec2(0.5, 0.5)); + litWater += vec3(0.02, 0.06, 0.10) * normalShift; + refractedColor = litWater; } vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5); diff --git a/assets/shaders/water.frag.spv b/assets/shaders/water.frag.spv index 5f1d56b1b98de9dc2b06e9828729bd0a7d61ea76..6fe7f2a6449a2a30df1299673bfdd8b027c567bb 100644 GIT binary patch literal 34440 zcmZ{t2b^71)xHm8CiLDxdXX*=s~8u2FEPD`7%|tRJB62 zQgvBxRUfNWOQTek^)A#aRAXyCb@~z0cN|^NzvHgE?WE(%)flVVwo0{P)dRN7>l>J_ zzRc29wGL$i$}t!g5&vD3ODU_;R$n>?jiINidVo%Qs^zPx(@&my=%G`mPd<6p@bJl_ z3(g*Fojf=&zjbo|z({M>f`Kzy`Yl`d4a}c4xUj!;=*6SVrI)xX(?|cpzQH4AjSLMA z9@BS5>yV+5d3}RhpD}xvy=U*dH|DCBTvntWu5k~uQdJ8E=Fy|J<*K!)4;UC-FfuT6 z;evso`BSGJOY?DiTMS|Q)$uu`_8(KNK|QU;z15o3BSQ=4%$?sF9j&!xs|~29wua`l z7L1&I#Jqt8bGIf+`;S;X)w#24HFJA9utG(c} zT7!cN2m3~*4E4?HZo5a(c2DrB0}B?k7-=s%d!PT+7R^5GygkM-)qddqo!k0EF(;iq zQ^X`EehYYZXMtxmKIe%WpG(Az&*k9$X=l~>f2PJ~*Z488o6T|kNL;t=sh$M8*W2eyPxW+o zV}BOj-p&n`QE7YgexTpFT^_)3pk)3{A2bQWiQQGY$!v`8X4eS_=cNDm< z#y!>1;NiZJzVvk*d}d=ismpdUd{$$d)n#kJa~=$I+0KEV(X_i@30qHfWf%YG5`0W` zBRpfc1>7D(PjxGJ-oSh>QT3Q^ho4>dF{Zi)?7Dg0d99t_8kjS8=8#i}(o;Q%=3YOL zgyj#zhld8{FKFzK$UAe=Q~h8GKBjsKp0U2TgsrD~xr_e=zW6-rtzLo8XpKx^#_MbC zSTa!aIAfr7ruA6<-pqm4`~{6gTRX3@*vxCj&}b*V_p;eTa|+*fzRR}t z8BX0OIPsSQkIwDuA3C#RBKnx>?+%c|b@~r@X5Z+*tRv4Jc4AzY*U@Gy9G$C=`N%!y z4fVGMsiP^M$z5*3yx}@W{hTAUDI=}ZfgRnsQ9ok1)p8BZQUCVboW5|Ne_|&Kb1=|w zlY@Eo!bfL~v|96z@)CnW1hhGWC$px^$J9N9<`uOy7pBfl!dzpvN7Ixq|$lm}j_vpr5d^7m`P9MG1HrfaK&Tfqup`G{6 zXd?r|!-K6O7L2s!&so4syDqz;I|%cAbH``${PsEATTNB#>mTSFY;Q30Im|ZG`a9>? zFx-8khxW7KgFM${%!}aV{r=*Hx9|5o)n)L0PwqO8p6Y6NbFU}f*ye0P z&YnHmYM;Bk)h8G0^#bX83)-mbaCo~xJ7#ZnTlD(g-X8DmX!V+Pw4UmVXytwVOYru6 zeQb4CVoc{^`{ULG&>#kr5Io<#4PIXW~l>~&|-P>V?Jg%_gF ztWO=3WvZ9Z7S8W)jnv6c>0A|z|1!2?921wQ)(G2$x$#=0?ol`CT#MRz`&`C-!r<`S zzV`X4zfDHx4$Pa^8tH7-#PXc()BOkr$i>4w{O6_*b9@cFM2=JX`rFqSt|{2Kmd$DP z*Q-w3u~iQNxn2zp4xQP##yS4w(feDi;fQLh)*AC Rlh9^1sB{`Ng-Y_*{=-GdWH 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 literal 32584 zcmZ{s2b^71^|lXWCP3)DNiWg`LJutglAwWv4uZlYbCV2AGQ&(l6D3FyQ3MoJiVA`> zX^Iq4s)&Ffhzcs8pwbjjQ4lQfJMl8JEB2~3e zwRp8;wR3mXJeI8%MyV?6t*IBUMmG6`Ne4{YVt7vP7TavQrH)HgBdnUXrK=^XF0f@* zPv30yMHa5Al_{%J7QwJJ@pqz(p)5mNed!!Df}X1Cemd={MpY9g9XVn0YF-uPT#=n2@?*b`RFkgL)d;feD-bnkEoWXp4i0Q)e6)@19NB0oZT26Zfc8Gt5Z*C z49sfG89MQRS$%V6ZbX#U53#zcRj}{dJ22-MPZHVT2^bGZ# z)Y!8zxIog_oP+hKdz-kcTAx~Ps-|rN>giD9Zvvj#Gdy$at(vRQv~P^on*Wj2=CN;m zLdyYe{LKm?M)?GE6h`{vAPFw$;z_RjxTTQvK$^VS$gRJ($Ew`%F* z#GG{cj2Dxf_`~4owU28*z4mdRO|N~XfQM_J{l&G*PCTKR|8-4#ViP|OcC$IIUx=G+yQ-(b?)BFB z(p5bdUfW-Q_cmq^%%ZmKo76)!evA5;Chn^KLp@{031p`q*MWtaoG7h!d%^o^JQ3^| zjdu{Zr-{3&gTaG6Lp|y1F!g}=_dGz%|1p{cY<9v&pWTRvm1RgW=xz+lg`#y&kY7O&J(&$M;?~ePBl6 z+sb#*mOjO)8wMx-V&LJKJ-q|Rw@pMJQT@vSa=1?a15fQ4?wfYtiGxmz>+&Ajl)1w* z^)Vm0$E<R@_jh!{v%uzq*NNxO3<5*x@cW%@V7;H3LLvz%>H8;o3?du)a z&cYlFG+5_gp55@_X+w?1?1Q|-pb!CVM*oqlDRVL~WLQ0`CD9L<)|lO>XG(2Fv{{Ye znck6GeqGIHht?gs)owL>dbv;a^|!pt!)FUSa2@0yfR}r8tq#5}e0IB!?rLN0{XHi( zhK$h4dn>e|zQMu%#sPDN8nb82VWwS|ZO|Qr`R>&A*=u&|9PX|rsP**r_4KzknEC8y zn~A;cb8HaqzR^SbiST}&Ycl3}@bZ3tM$KFI`>yJ2c&{gSGmoz7e0Y7YC*H{FBKS~a z`cTibIg_VPA8xeH-R|o0`FeAK^t~2s*mc;y)u0u#ySg!Y^WNSX?@egUHEU~K)%VcK z`}(c$)_r|sb$en=;$q|@LY?m&=;iv{*};Ff03TW113$ipTWUv~dl&HQsvhXz4=%vF ztB2ve=CNnr^wza_nns~3w?g>Bde#;d!`N#3=MkS*<+wV zB=^G0(Wf>~9h60?*U{$A?rjV;lONx{Dj5F_Y=<}|E>VpkwhMFPwMgBg*`$3fYU!bGk?O0~jC|5BKoj>ORcz9qJhU1PYW zVB=agqtV-3b=r=sx(LYis=t5W`1Uo<@sC39Z8QcWsx8yhnC~Ly%4p58jT`7~-IGRE zYoOKpwb9$Y9*nFuK_6hA$vjIy)UIj=^!~m%T#Op% zRj+pNzszq(9PXX+;o*A!jHtS~mBE|y>1R|w73RX}nl9P0XKOceSrN^>!R9)|N2t~{ zwyRnVeOl`gqS@C5az2N(>?5n4;M4gG(cIAc^bEJ2SG%j-(2wsK8l2y6S2Yo>T&sgR z_~CV|*52!?j)ME_)_VT$s%CchA6@(VQ0zPm#DD7+Kfc5N#M&Qo)BlwCH=pghsueoo4LH}%+)|qJ z;k%E4x$~bRog?3c9N0KvfoCJ%jqrY={TwyX9K859WBV>){&VqqEqMXWXV7IRX=hVH zwteKb+r2~UZ0BvFY$vzfJW6sP(_k5xGOs)2}LNmUa z5B=%a=V`TN3e9I}wYAYEaG%TPfeFl?&(rH-^9Wz3MVs@K=jV(!yxA{)_8YAnuNpti zes?eYGFN*R+KAe3VxeVTe6}-Z zuS09wi&p}9)7LhxiEY%^!FyS1 z+o)UgQ(u`{U7n)82Hdu4^ZmQ3jo|v*mwkH=2-kO0`j+oTIcBRPb_?Gfe$)jE@3t9j z%xf$~pWP|(*#o@Rwtv4n+;imS$9vjs_JZr99-j%|YX+~_HGCp`$4iFpNk4nT^-+({ zN5Ds%JFeYkANboFJ=^A!;jittLp%5V;G;(mxA_6^D+ixz^Ml|QU$pA(iGK+E#tA*` zc!$9sd-Ty=Vn4jeC$;%hcva2YeMRQ(T+rH54*lilHQ9WAJ$#|s?p#08w9jkj^GuV^ zAMY#hXO`Z(o!{&53-5Vqbnd>8m*leTZi*cGmidVKZ;vqRc>Om6aO_w;)>+_5{}Xgz3h z^=LC{KefdE6xziXUDRHq)8QQLIs9ys-`no{{3f5*_WyE|@7|uXi{PV2k8ZdBM$=yH z>YCmFUYp=bo|`uUJO|Xx$NFZ<`yAo7!};Mlx^Q&|AY*w7Y_1ls1^=LS4|uIvh5!E( zt|niTZ!5glsikQ9{}pq48@tyb+l{HY_eizm_72?hU+(^Vx3=5<-!(R;H5sM%E&G<2 zMc?<)y=LfRUvfLjb;NmD2<}+)U%0jzM?Mm66USqFx%Wo3Rf#eRtgpO0!dK@Cx;Q@i z8E*+{4>jY-y_b4q>`T|&xnCA+j!L#)31I9|6x++a_o{_^@0A>iZ|Cp>TmE;29}Fkk-R1UoEIji$ z1lH_)#(ok)+5Z>eqn^H^9sf)4i7%Yg-uIWn$#3WE*Kfhi(UKgl0v~_(Dedd})o?XS z=G|u!kF>c>e$=1GJJ0ulokR1E{nuz~o$;6U`S~08Yx~{No~Pf!&%J-{E}PNH^?3$v z8~wB;&hNohb#Hq-e}a!5eM>v;i*V+*9p@#uKE}|tCh>i~aSqKd#k~KD&QH_sIORU? zsHOk6AdfvdY1icWHr%-0L$t+jQ5J>k7jxJe&r)!&X=f>eBpZ2iaQ1F^)$tC?1$#{%Q3FW?(kX5b6nejVA5=Aj#2LOmzws2Ypj2A zn1jaJwD;7>@QW`xti1+j!v~fdF(!NJb8t0FVtgL_#>w;d)H!f9OZJq{bslN+75HaX zntu=YD*T&!t<>)KBKWUPc(uLO7sG9%pSHxg63qR*J;ztWd46cG;r(#Wo0QD$v*_N_ zQWEd4=+mda)Qsmj`X=1@a~$q{pGT3~YqS*naQFt;+>41b2Aw$V@$3ob+-;u|`@*@y zjLW=EhO1c;<1jE|YsZ)h=R_QvHVwFY%-CsjY}4oYw$B{6*CC&a;&T$(#z&mn&gIl* zn~CjbjML%tdsq571MWRoJu$8Ye`19#+PPf^U*X0HZT@XI&tdIz8xiYTwuyRn1&{YYu|`;n6S z9ZAXkZlvUQc5uHNDeZnYQu2E{_yZmM!4B?sB4zuZb?_$(?s)zFBmVN2JNWAbcRu`n zq4f7Vg_3u%X-ht;;I?0;;P&VD2c^H?9fa$@Nx}8sq2Sv6t|0!}{hlD)`<>qrguC9o z1-JbP1-HHL_T#VJ_xC0Dy?x1jUmtFJ-_?iP-gouk+I?SN@|z2;zwhf~m;1iHOS|vw!?pX)KHT~AU46KA-_@7gcl9OrU46;B zx!8r9kMHKgjpuv$aP7X257)jy2lst^?B?ft_>%h$zU01xFS+mF!;ODb!HwrT_|on> z_>vD4Tz}uemv-O5m)v*o;rjayKHUC&2Ve4Y3$DNK-%Gpi-%IZM_i+7v_g-?}yO-Se z?&0?Dd-rhLzgTeXzH^UV?)&y|{e9OSuHBni$$igWa^JJd$MPKOXVU*r{2c3NS3j@r z!hO$Y^Z$ai?aH0jd!l@-_fM`zBPiZT7ox#H2r`7rF0M=jKd2!$G2)4~Qigx?-nM*z68Ur?u z@SVZ>oI*dw*afV12K~r)1FQL*oPKr(+t2E?J-%bnjj!FleZEsmU*o`PcM{w8UweYp ze2?%i{Kr#!{F_{~?L|@ZIah2P_s2wVId6Nz)hr(6ynPt0oVSm_^;b_`lfcfw63nz? z-WP0)jVRjop=2LS25YzEoSgzTt~SR#fm$u`_Xj)f%+mp2HRm)w2Z7BYK8JwS4yMHC zP_XA?d=7)F`Cij~VeTJ=+g4lF>u|8+S+cgL{SoK~QnaU!kAcfPJ`Pta^EeV-=5Z8U zt<0kbZd+~1V=CA@yl!Yu9@D_a*M4d}j$W|&X^US2?A&Q{p7*0xGk5pJbnv=0_nesl zb}ii7@|j?@jI|GJj4#lq_M^f2H1#@1tvQxl%VweJtIhW+j>DX0!_7;( z^Pn~WwyoDM?T*E~)!${WXU|)E-x{JGrg(ihmfHNiH_fT_$@O(}E?mvxVIRWG&v6jX z8~2KN91pgQdU87foZPgV+aR@ia`^<Q4lglT;Y8H>=;#%1Er_l9Dzo&rH zuQvNVky<_Zp9;?U8spP&bGx8km(#%dsOMZh9c*0BW$k-Vj6IK9TVkIHE@OWNZvTmW z7FZwk#QrQev9-JI#y$hAE%Ws`aOc{69!)*-bq-j~;*t4EeqTT@*X~@nZPasqod>qv zIEr?2Ih$JDI9?wv06X?vFD?W>QtKJlm(h&l-x_FN1Cn#_EATH-^l^_H^Q+)5QncCU z`P6F3{bI0lo9B;9z-kr``$~RaL$iJQ`#ShiihBC{2H5_z<=njtY+P;Ta1phdaXq)c z3GOcV6<~dgsr^c@KI%S)s9gg#w@qvNx8PS(jDI<`cH=KjeH~@Vn%nl!?TLRg_@pYr?I+;m^B`EBd>#UukMV4$-T3!VYfFqrz~&hK zQ?Ncce;x(vqn`8UF|cdtHAj1{3lD>}IaWXKJPsbDlw*AYuAX!H=U`*;QK`~qjz6PT zPfou88$0}$V11I$ufWMio9mwQ>nXVH^)dF7)N10Vsh^=F?z3P&ALaS&Ik?*CXz}@7 z?Q;iX^cnAYxZ3Y2SJ&DLVB2c*x$lqE9zOSJ`vXPI=RR?Ac?s0R@{>|+Tu_4KhMSj|%Q;XJ!eOQY-K zcXRgPJ506A&9dNfotA^EWt~<4tNHy()@em>)=7KTX(h1hq}{%JKdP2>S_SMF!&e2H zPjXoeY@4jp>R`2$tkVb3vQEY{j=uKkJ6P@MV@? zoV(F*+obPp!D`!3GI!g7)l)Ke+oNUf?8n&pX703mea#$LC$AmB=8~d=J)!p zi_bnc96`Ft4oVBs6_evYz|Y?YzF4hkfgt_0%rjmpacNlfhFc&hNq0hfofp z=_Ml^=zxc|<$B)*MH)BWlexpTcvXYxglU+v~60bK!h)(e`n; zed}Z1`%|l#tItbEg4H&!_kweG6kI*eBt2j?f49POd>Yupb6ne0ikj!R*fH9718nSk zMwt#)vv@eJUmGp2UfG>J<-u%<7&(2mSe!`t@Unw7U{2f_NLGKv(WU_ z=G^pBt0kw|U^Rb_FnezRuAXbpAlUg({v30&=BivAZ; zt7Xo=23E`5d>x#*IU9U9G4+Z6H^9zE*5ER*ZPbl_DYaVixg2ahxmSD>tmgTs|75UQ z;$HzaM&f@9tac?u|9z>|ythuH_S$2u?JDZ4De3!K@QL;H&AGV_J_tXRxsiVxu8(>? zw_Fc){Ju}rZrp3A)f4w7u;;P18^Q9}z5~`@+s)MSjO`Y%IcobZwLG7pzX$f;$1tAl zwCi^RwYJ2#9Xzk@`!=vVw(o>q)(i65Z8pORc31Si)YgXQLG4nF~} zLv4H8X*b4Q)Y=l`VX$)-{s`FdWMBOhtdDxe^C;Lj*{hF%)$~oApMjnK#CaS(m3-9W z^90y+h|kaA`l-A3yv95UR^J1^Je&LiuAbt(&N2TI&0Ms5og1RfufUzpa!;YD=eqGU zSk2<$I?%M4mvMiMZfyOM`)|PJo;mm}Tt9W^`5|hx%-J(wHP_pi&w=e{1B$k1DQTxI z?S2pTIVktS=izD=4`bwB_yU^tsc6ab5AgDPlRv`MysqTA;ZJaNi}UXF_C>Jqv?bO{ z;4;?Ba5dkvnENYW+lgPL*6w$=uH&D<{`)BUXw&C+)ar@<8dxpQ4zGjNHXwe^lQ-aw z+4DrZ=ZWiYAAf^u|0^Z_e+TRDdu8qZEr5H&@ACfv_p|w%)XCwWVD;?Dx4_0wkM=fL z-RC{W@fWaKV*CrNmOc0mxN{G_i>97E_;0Y9#lxIQt-ZhggRYOU?Z-XzU$A@1HIx4j ztfsGdX;X{;`(Up_;e=^@-%-a~$QOQ;T&ETW+eSU}IRb2M&cAl&Ken#g=3MJHmD)Vs zBZjuj&q%OZ_@Z!s7d7*{7+B4hQ>Us896>L8~M`?FH{A_1@e?LjP@$G+g@EY}8%Rd00T+IC%aNFeCx~Bd` zu}|Brg=U|A?lY!w^tDfa2TFVTTpR4(3ttEBxU)~!1?!_8pY_1TiO>3AHGR{En*Pqy zhG6$h=H!E5^Yi)qbigCEPv&tW?E1XOTpQ2d(^5;kjlpV}hfTn0DamtFv@*}l;A;9O zPqoC|9PIn+ybsy}uI4o}J|BYn&M2QJw}h+t&M5D%w}RVNo1Za9Q+xOsQ`^=QHJ|gu z?%{=LzYW+8c{@3zzirXf)8BSrHTRLvi-}|X7Iy>Rsc#RqpJ}YC{`O%WJ5qa?hdw({ z)XYPiK6eH?#;n&aaKg4fPmO`=lWYI3VAoJT=hfe%Q!}3Xad&WeUmOcpvv?Rg_r-B& z+7G7xOR}p$I)Q5jCBxPj&%sGc5LnA z-W&#(W1S11Ls8FIj|1CQoBbS5t(LK#0CueE+6SoBGS*Lk)iTzT!R1&_g6os9eiH0h z^~+d41y;{J?L;vpW1R;s$NFiw+NrgVV?7OAj`d9V85H%5^)q1GYI7`SQLAOFp9MQs zb?v86t7WX81FL1Mp9hy?J)E}sWUS|a9jksB>leW4SJ&%$HdxI*yw05mF3*qi;c6BS z`^(<_BARxeo08*~;N>;p0=U}p^q2GYLb$pmbMs}e@w7SiuTZOHZoUfknxL-zTxzxS zdokEO8h#1bbu_-~b17IK_4s@Z?D*sJb+CTw@%aXLCrak;GO&K?@wpsqK3SJ67NcIdGGobJn_6|`Z++K_+JG!M&e%$RQyt27|t>5js2~D3A+Zp%fx}DE#_HjK$UvscO?cVQqr1pMo zoiTg|Tz>EVUAWrmBod!nYM(sYd=IYXvrV2`ZiU-co6ifkQ+xQlpzSt_n*EE%vOj!o zxdV)}z;ny@(bV(Yawk|V&n<~#o#&PxfbGYz>2DwAaTm3RdFb;)ikf+dGsb(si%_x_ z_ky$b&V}c*KF;ag)Y`MI_k+uG_eXFwi-&P??mmE~eGDai{}^1JyAQ(E+;2H|e*#yx zn|^)R(s&fQ1Ap1bO~HvJT=p7nMAJPI%Ke+;f>@hJ2E8Jc$UPaZ!9t3N@> zn4SceWBLVL?IcRZ^h>b1C9!@5Hl8-e^c1yP#`HAUF{x)vzXq#2ro{dY+_7rQ-2WD= z?%z)u`#!K**6~?zISM=Z$^88p?Ecm-G5!K} zpXb{48dyJd`+S*NO@H^$8{l&8{t8#Kc-VjDDZm^oy{=Bmp2{*3|DDESDGGB{;mtntUeiyBM z9fSSbRv*Ves+IP%9R*ekU%ckYWl6Z2e^;AamI9Z#=#%zKgM9~+T$ZVQ&BeZKt54=* zS+MU7PogBoa%k$wVR^7|)E$F=SELsImB9XfNBUeDZhoGN{{63i^Q%w%R{!E9l-};5$dE}TrHbB>wJ~jl~Rz2^1KM1yOZ5i)I;EY$h&j{{8$Lrs8 zX?H#Sj$jk8Ic`jCzkWBrDOf%4`8ETqSv-tQ)26@Ixy{kb^LY!n{p5Yphrntn=4rbv z(TwF@vk&9wYo5l_Zch4+2A6$o19y${yARuf_3?Gyw)sD?cauL-<9IEIec6PA75}| ze7J*8>frl!@F^X9{|20;MdjMb#U&+QpC>NUNv^k_O7w>^^qDoSNqi1d74~f=V-qgJ3sy#RK|2}{P(BC z&Wr!Xg4j99_Z-g0;S~3y>r)@~Xmi2pWp4hP(Z<%6 z+>Qs^zczDo&#EQ26T!}9_(@>bG~Wq*0<4dE@;w>s-b*f@1nZ}s-%b1!*lW;I6z%ry z`JtY^=7F87@KeFAS^EAocpgPPeV+!NL`mPLgY{ES-)Df+w|4vXTvShAp8-1;;b(ze zll1*rus-VP`)si1R{H)NSU>gj{dsWu)^6WtQmZG<7r@Si_oKzA&!;%&=TYMG#ad5a zUxM3CJw6wJ%XPRAuAh2*z6|#KPMoiR^;3_}SHaFt^0^4?I+Xcb3|CK_OTe~MPn=7^ z<=T7=uAh2*z79T=k~rT0>!?^^xQ%{_4 zfy*_#3a+1ee69w6l#)2tfb~;Pj@N=OqiA!D&!JXJjBkU>HNK(gx8U>DjcDrW=O(c2 z)D!1saJk0cf$OIppYMXpHNFL|pL%@02QJt6R=8_?ec^K(SUvgN4z`_o;@kl)*ZBKz z{nX=gC%9bWAHelf&;0!mY@XWE?k;e-#&^SA<1)v4;OfcoUa;-d6X!l~xyJXy@1>~6 z=SSdjjURyPr=A>t3^q?~uJLu$YKie6*mckEZ2ttT7W+eB+r<7bSS`tBZhPnIc}o2M z09FhCBiQ`&8Rt)6HTOVbz6iFRw(Q53!0P(hk87ZoxG#f^8~!R-pTzt#_!WwJeEtG1 zeO`m>r=EV_0IR=F$!D;?g6&V6`%_--&(+}`D^uK`?!#3IylR101G_&zK%M>hCb-<6 zZ*}nhbnq?~QvZzWA7JBV4F3eHW$uix7XP=wYT^H?dFJjNxIXHcgMWk7-=(-7=BSn! z{{=hd@c(u2_u#h4K6oE&e(LGh4Qe}WiL(%z?J~B7;rgj3w-I3b*Jf_cms(+$hwG=F+?D{_zczDo?bH%uNwE2bF9p{p{VomG zN8PdCjqn_V}&>*4KXYT@|jsdU9C}>{yb^>Tq*W&%2Nhfca^@ z`_cXkSf5WwLmdsAw2_FANmT(<{n+l~_79l-oF*G#*)sg=2Hj@_foZ8NyJZ9$#f#?VeZx$Op5+m(|3 zb_es*%uRc88w=J}<~9z_Pct{|=B8HWwk39tGPe)G&21~{yhqxLcDXk=w-XEQedeHo zYd^T)+7BzZ_Ky`@`;i6LKCR%|8wJc({2xPv$uR&QEh6X!Dxq7}V2dZ*bY> z!*G6@^QFx;j#E8tJ_1$?pHy?#!G8CF>!Ti@DYZ}Dwd@C1^SiKozp_8r?_?~V2gW)8 ztge6J9|*P&ZOKn9eH{XJoZ4IidA^T16zqATuH7|LOWTivZR<72{*M5w9Zu2THB(Ex zkAsb;uH7|OOT44NWxT0ywH}K8uCZF;^@5G3uH8MMmUz>_WxSbiwHXxs-4kkwcQn{| z>e?IBYR2;#dJNb(2=50whOvy_JvR&NTAv4w&uqAQuKNREewyd8@r4w#>2AI39=K8Z0GY>exRJdcB`C&uw$ews1N*_irxP0&_e6Sl?A zV{1yTrK4-?ylg}9`mh~!o*hpjhI=O0i%)m(b36FgYVNw&*88P%WQDC%>%1jvZqf48&8{ko2Od#%xPe+iRy`c23Y-cO8P$&Y~IFo zFMS5ArfT9k1;+6F30qFxSIWDOy_{rExAs70c<>N_U&A&WlZOR z9g}*-^hL1x`IL<5OW<-$7r@o@&6w0OrVGL4n7#s6`*Q8$n7#@w$8<4V?IKFXbO~79 zl6ATiY&>oD?S4_qn7$5nOzIiaWnlGhP%@^=!R45~30KoMV^WL%6=3~6FT~|}u_OI? zl;_3v@H}(v0QS7ti8|-ORm3RIi)#z+xpaNOZv@}e!S5`%_r3c&_(L81(GLDt2Y;gA z_Ww-Hv!++W9aHY3*TDH{-bb}LXO2ldZLR~CZN3fXr+FXMW*f(?E_Te1U>7^yn<;*G z{vB%P&-!hy1-DRtkK&>I*4l1u-#1X-Mlr@MoQHQ%f4|mlr}p=beVtG@u|r}@rT zyM24Usb@@o0DC@!{}HTDJ~#de%un;PnZDYMV;gOr7w%Q}n6+^{FJ7d$$L4Wf$X}{8 z_owZ(so6gJ-@PjLoHL$v;=K%ZPbS_gV70{4re^z`$DVT;*Q;Q2_PQ!Aud6#VK97wk zdEOWUHiuoPb1i+1cDa^%?Rul;_M!ed+;)ynU%6O6_euUv?yqpK<@#tdCpB~Wf4AHJ AqyPW_ diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 8953a78f..d29ada83 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -256,6 +256,9 @@ public: bool areShadowsEnabled() const { return shadowsEnabled; } void setMsaaSamples(VkSampleCountFlagBits samples); + void setWaterRefractionEnabled(bool enabled); + bool isWaterRefractionEnabled() const; + private: void applyMsaaChange(); VkSampleCountFlagBits pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index 99767782..edc15e6b 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -93,12 +93,13 @@ public: bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; } VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; } - void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false, uint32_t frameIndex = 0); void captureSceneHistory(VkCommandBuffer cmd, VkImage srcColorImage, VkImage srcDepthImage, VkExtent2D srcExtent, - bool srcDepthIsMsaa); + bool srcDepthIsMsaa, + uint32_t frameIndex = 0); // --- Planar reflection pass --- // Call sequence: beginReflectionPass → [render scene] → endReflectionPass @@ -124,6 +125,9 @@ public: void setEnabled(bool enabled) { renderingEnabled = enabled; } bool isEnabled() const { return renderingEnabled; } + void setRefractionEnabled(bool enabled); + bool isRefractionEnabled() const { return refractionEnabled; } + std::optional getWaterHeightAt(float glX, float glY) const; /// Like getWaterHeightAt but only returns water surfaces whose height is /// close to the query Z (within maxAbove units above). Avoids false @@ -159,17 +163,22 @@ private: VkDescriptorPool materialDescPool = VK_NULL_HANDLE; VkDescriptorSetLayout sceneSetLayout = VK_NULL_HANDLE; VkDescriptorPool sceneDescPool = VK_NULL_HANDLE; - VkDescriptorSet sceneSet = VK_NULL_HANDLE; static constexpr uint32_t MAX_WATER_SETS = 16384; VkSampler sceneColorSampler = VK_NULL_HANDLE; VkSampler sceneDepthSampler = VK_NULL_HANDLE; - VkImage sceneColorImage = VK_NULL_HANDLE; - VmaAllocation sceneColorAlloc = VK_NULL_HANDLE; - VkImageView sceneColorView = VK_NULL_HANDLE; - VkImage sceneDepthImage = VK_NULL_HANDLE; - VmaAllocation sceneDepthAlloc = VK_NULL_HANDLE; - VkImageView sceneDepthView = VK_NULL_HANDLE; + // Per-frame scene history to avoid race between frames in flight + static constexpr uint32_t SCENE_HISTORY_FRAMES = 2; + struct PerFrameSceneHistory { + VkImage colorImage = VK_NULL_HANDLE; + VmaAllocation colorAlloc = VK_NULL_HANDLE; + VkImageView colorView = VK_NULL_HANDLE; + VkImage depthImage = VK_NULL_HANDLE; + VmaAllocation depthAlloc = VK_NULL_HANDLE; + VkImageView depthView = VK_NULL_HANDLE; + VkDescriptorSet sceneSet = VK_NULL_HANDLE; + }; + PerFrameSceneHistory sceneHistory[SCENE_HISTORY_FRAMES]; VkExtent2D sceneHistoryExtent = {0, 0}; bool sceneHistoryReady = false; mutable uint32_t renderDiagCounter_ = 0; @@ -200,6 +209,7 @@ private: std::vector surfaces; bool renderingEnabled = true; + bool refractionEnabled = false; }; } // namespace rendering diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index cc1bd4ab..0a2f72f8 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -87,6 +87,7 @@ private: bool pendingVsync = false; int pendingResIndex = 0; bool pendingShadows = true; + bool pendingWaterRefraction = false; int pendingMasterVolume = 100; int pendingMusicVolume = 30; int pendingAmbientVolume = 100; @@ -123,6 +124,7 @@ private: bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer + bool waterRefractionApplied_ = false; bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied // Mute state: mute bypasses master volume without touching slider values diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1570057f..0f1d7593 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -855,6 +855,14 @@ void Renderer::unregisterPreview(CharacterPreview* preview) { } } +void Renderer::setWaterRefractionEnabled(bool enabled) { + if (waterRenderer) waterRenderer->setRefractionEnabled(enabled); +} + +bool Renderer::isWaterRefractionEnabled() const { + return waterRenderer && waterRenderer->isRefractionEnabled(); +} + void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { if (!vkCtx) return; @@ -1054,20 +1062,27 @@ void Renderer::endFrame() { vkCmdEndRenderPass(currentCmd); - // Scene-history capture is disabled: with MAX_FRAMES_IN_FLIGHT=2, the single - // sceneColorImage can race between frame N-1's water shader read and frame N's - // transfer write, eventually causing VK_ERROR_DEVICE_LOST. Water renders - // without refraction until per-frame scene-history images are implemented. - // TODO: allocate per-frame sceneColor/Depth images to fix the race. + uint32_t frame = vkCtx->getCurrentFrame(); - // Render water in separate 1x pass (without scene refraction for now) + // Capture scene color/depth into per-frame history images for water refraction + if (waterRenderer && waterRenderer->isRefractionEnabled() && waterRenderer->hasSurfaces() + && currentImageIndex < vkCtx->getSwapchainImages().size()) { + waterRenderer->captureSceneHistory( + currentCmd, + vkCtx->getSwapchainImages()[currentImageIndex], + vkCtx->getDepthCopySourceImage(), + vkCtx->getSwapchainExtent(), + vkCtx->isDepthCopySourceMsaa(), + frame); + } + + // Render water in separate 1x pass after MSAA resolve + scene capture bool waterDeferred = waterRenderer && waterRenderer->hasSurfaces() && waterRenderer->hasWater1xPass() && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; if (waterDeferred && camera) { VkExtent2D ext = vkCtx->getSwapchainExtent(); - uint32_t frame = vkCtx->getCurrentFrame(); if (waterRenderer->beginWater1xPass(currentCmd, currentImageIndex, ext)) { - waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true); + waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true, frame); waterRenderer->endWater1xPass(currentCmd); } } @@ -3268,7 +3283,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass() && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; if (waterRenderer && camera && !waterDeferred) { - waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime); + waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime, false, vkCtx->getCurrentFrame()); } // Weather particles diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index fd3c3981..452d6dc2 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -118,15 +118,15 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay return false; } - // Pool needs 3 combined image samplers + 1 uniform buffer + // Pool needs 3 combined image samplers + 1 uniform buffer per frame std::array scenePoolSizes{}; scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - scenePoolSizes[0].descriptorCount = 3; + scenePoolSizes[0].descriptorCount = 3 * SCENE_HISTORY_FRAMES; scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - scenePoolSizes[1].descriptorCount = 1; + scenePoolSizes[1].descriptorCount = SCENE_HISTORY_FRAMES; VkDescriptorPoolCreateInfo scenePoolInfo{}; scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - scenePoolInfo.maxSets = 1; + scenePoolInfo.maxSets = SCENE_HISTORY_FRAMES; scenePoolInfo.poolSizeCount = static_cast(scenePoolSizes.size()); scenePoolInfo.pPoolSizes = scenePoolSizes.data(); if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) { @@ -267,6 +267,47 @@ void WaterRenderer::recreatePipelines() { } } +void WaterRenderer::setRefractionEnabled(bool enabled) { + if (refractionEnabled == enabled) return; + refractionEnabled = enabled; + + // When turning off, clear scene history images to black so the shader + // detects "no data" and uses the non-refraction path. + if (!enabled && vkCtx) { + vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { + for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) { + auto& sh = sceneHistory[f]; + if (!sh.colorImage) continue; + + VkImageMemoryBarrier toTransfer{}; + toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + toTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = sh.colorImage; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toTransfer); + + VkClearColorValue clearColor = {{0.0f, 0.0f, 0.0f, 0.0f}}; + VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(cmd, sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range); + + VkImageMemoryBarrier toRead = toTransfer; + toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toRead); + } + }); + } +} + void WaterRenderer::shutdown() { clear(); @@ -304,13 +345,15 @@ VkDescriptorSet WaterRenderer::allocateMaterialSet() { void WaterRenderer::destroySceneHistoryResources() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); - if (sceneColorView) { vkDestroyImageView(device, sceneColorView, nullptr); sceneColorView = VK_NULL_HANDLE; } - if (sceneDepthView) { vkDestroyImageView(device, sceneDepthView, nullptr); sceneDepthView = VK_NULL_HANDLE; } - if (sceneColorImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneColorImage, sceneColorAlloc); sceneColorImage = VK_NULL_HANDLE; sceneColorAlloc = VK_NULL_HANDLE; } - if (sceneDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneDepthImage, sceneDepthAlloc); sceneDepthImage = VK_NULL_HANDLE; sceneDepthAlloc = VK_NULL_HANDLE; } + for (auto& sh : sceneHistory) { + if (sh.colorView) { vkDestroyImageView(device, sh.colorView, nullptr); sh.colorView = VK_NULL_HANDLE; } + if (sh.depthView) { vkDestroyImageView(device, sh.depthView, nullptr); sh.depthView = VK_NULL_HANDLE; } + if (sh.colorImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.colorImage, sh.colorAlloc); sh.colorImage = VK_NULL_HANDLE; sh.colorAlloc = VK_NULL_HANDLE; } + if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; } + sh.sceneSet = VK_NULL_HANDLE; + } if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; } if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; } - sceneSet = VK_NULL_HANDLE; sceneHistoryExtent = {0, 0}; sceneHistoryReady = false; } @@ -323,54 +366,7 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo vkResetDescriptorPool(device, sceneDescPool, 0); sceneHistoryExtent = extent; - VkImageCreateInfo colorImgInfo{}; - colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - colorImgInfo.imageType = VK_IMAGE_TYPE_2D; - colorImgInfo.format = colorFormat; - colorImgInfo.extent = {extent.width, extent.height, 1}; - colorImgInfo.mipLevels = 1; - colorImgInfo.arrayLayers = 1; - colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT; - colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - - VmaAllocationCreateInfo allocCI{}; - allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; - if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sceneColorImage, &sceneColorAlloc, nullptr) != VK_SUCCESS) { - LOG_ERROR("WaterRenderer: failed to create scene color history image"); - return; - } - - VkImageCreateInfo depthImgInfo = colorImgInfo; - depthImgInfo.format = depthFormat; - if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sceneDepthImage, &sceneDepthAlloc, nullptr) != VK_SUCCESS) { - LOG_ERROR("WaterRenderer: failed to create scene depth history image"); - return; - } - - VkImageViewCreateInfo colorViewInfo{}; - colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - colorViewInfo.image = sceneColorImage; - colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - colorViewInfo.format = colorFormat; - colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - colorViewInfo.subresourceRange.levelCount = 1; - colorViewInfo.subresourceRange.layerCount = 1; - if (vkCreateImageView(device, &colorViewInfo, nullptr, &sceneColorView) != VK_SUCCESS) { - LOG_ERROR("WaterRenderer: failed to create scene color history view"); - return; - } - - VkImageViewCreateInfo depthViewInfo = colorViewInfo; - depthViewInfo.image = sceneDepthImage; - depthViewInfo.format = depthFormat; - depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - if (vkCreateImageView(device, &depthViewInfo, nullptr, &sceneDepthView) != VK_SUCCESS) { - LOG_ERROR("WaterRenderer: failed to create scene depth history view"); - return; - } - + // Create shared samplers VkSamplerCreateInfo sampCI{}; sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; sampCI.magFilter = VK_FILTER_LINEAR; @@ -389,99 +385,155 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo return; } - VkDescriptorSetAllocateInfo ai{}; - ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - ai.descriptorPool = sceneDescPool; - ai.descriptorSetCount = 1; - ai.pSetLayouts = &sceneSetLayout; - if (vkAllocateDescriptorSets(device, &ai, &sceneSet) != VK_SUCCESS) { - LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set"); - sceneSet = VK_NULL_HANDLE; - return; + VkImageCreateInfo colorImgInfo{}; + colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + colorImgInfo.imageType = VK_IMAGE_TYPE_2D; + colorImgInfo.format = colorFormat; + colorImgInfo.extent = {extent.width, extent.height, 1}; + colorImgInfo.mipLevels = 1; + colorImgInfo.arrayLayers = 1; + colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT; + colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImageCreateInfo depthImgInfo = colorImgInfo; + depthImgInfo.format = depthFormat; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + // Create per-frame images, views, and descriptor sets + for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) { + auto& sh = sceneHistory[f]; + + if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sh.colorImage, &sh.colorAlloc, nullptr) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create scene color history image [", f, "]"); + return; + } + if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sh.depthImage, &sh.depthAlloc, nullptr) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create scene depth history image [", f, "]"); + return; + } + + VkImageViewCreateInfo colorViewInfo{}; + colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + colorViewInfo.image = sh.colorImage; + colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + colorViewInfo.format = colorFormat; + colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + colorViewInfo.subresourceRange.levelCount = 1; + colorViewInfo.subresourceRange.layerCount = 1; + if (vkCreateImageView(device, &colorViewInfo, nullptr, &sh.colorView) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create scene color history view [", f, "]"); + return; + } + + VkImageViewCreateInfo depthViewInfo = colorViewInfo; + depthViewInfo.image = sh.depthImage; + depthViewInfo.format = depthFormat; + depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + if (vkCreateImageView(device, &depthViewInfo, nullptr, &sh.depthView) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create scene depth history view [", f, "]"); + return; + } + + // Allocate descriptor set for this frame + VkDescriptorSetAllocateInfo ai{}; + ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ai.descriptorPool = sceneDescPool; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &sceneSetLayout; + if (vkAllocateDescriptorSets(device, &ai, &sh.sceneSet) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set [", f, "]"); + sh.sceneSet = VK_NULL_HANDLE; + return; + } + + VkDescriptorImageInfo colorInfo{}; + colorInfo.sampler = sceneColorSampler; + colorInfo.imageView = sh.colorView; + colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkDescriptorImageInfo depthInfo{}; + depthInfo.sampler = sceneDepthSampler; + depthInfo.imageView = sh.depthView; + depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkDescriptorImageInfo reflColorInfo{}; + reflColorInfo.sampler = sceneColorSampler; + reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sh.colorView; + reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkDescriptorBufferInfo reflUBOInfo{}; + reflUBOInfo.buffer = reflectionUBO; + reflUBOInfo.offset = 0; + reflUBOInfo.range = sizeof(ReflectionUBOData); + + std::vector writes; + + VkWriteDescriptorSet w0{}; + w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w0.dstSet = sh.sceneSet; + w0.dstBinding = 0; + w0.descriptorCount = 1; + w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w0.pImageInfo = &colorInfo; + writes.push_back(w0); + + VkWriteDescriptorSet w1{}; + w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w1.dstSet = sh.sceneSet; + w1.dstBinding = 1; + w1.descriptorCount = 1; + w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w1.pImageInfo = &depthInfo; + writes.push_back(w1); + + VkWriteDescriptorSet w2{}; + w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w2.dstSet = sh.sceneSet; + w2.dstBinding = 2; + w2.descriptorCount = 1; + w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w2.pImageInfo = &reflColorInfo; + writes.push_back(w2); + + if (reflectionUBO) { + VkWriteDescriptorSet w3{}; + w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w3.dstSet = sh.sceneSet; + w3.dstBinding = 3; + w3.descriptorCount = 1; + w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + w3.pBufferInfo = &reflUBOInfo; + writes.push_back(w3); + } + + vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); } - VkDescriptorImageInfo colorInfo{}; - colorInfo.sampler = sceneColorSampler; - colorInfo.imageView = sceneColorView; - colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkDescriptorImageInfo depthInfo{}; - depthInfo.sampler = sceneDepthSampler; - depthInfo.imageView = sceneDepthView; - depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - // Reflection color texture (binding 2) — use scene color as placeholder until reflection is created - VkDescriptorImageInfo reflColorInfo{}; - reflColorInfo.sampler = sceneColorSampler; - reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sceneColorView; - reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - // Reflection UBO (binding 3) - VkDescriptorBufferInfo reflUBOInfo{}; - reflUBOInfo.buffer = reflectionUBO; - reflUBOInfo.offset = 0; - reflUBOInfo.range = sizeof(ReflectionUBOData); - - // Write bindings 0,1 always; write 2,3 only if reflection resources exist - std::vector writes; - - VkWriteDescriptorSet w0{}; - w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w0.dstSet = sceneSet; - w0.dstBinding = 0; - w0.descriptorCount = 1; - w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - w0.pImageInfo = &colorInfo; - writes.push_back(w0); - - VkWriteDescriptorSet w1{}; - w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w1.dstSet = sceneSet; - w1.dstBinding = 1; - w1.descriptorCount = 1; - w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - w1.pImageInfo = &depthInfo; - writes.push_back(w1); - - VkWriteDescriptorSet w2{}; - w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w2.dstSet = sceneSet; - w2.dstBinding = 2; - w2.descriptorCount = 1; - w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - w2.pImageInfo = &reflColorInfo; - writes.push_back(w2); - - if (reflectionUBO) { - VkWriteDescriptorSet w3{}; - w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w3.dstSet = sceneSet; - w3.dstBinding = 3; - w3.descriptorCount = 1; - w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - w3.pBufferInfo = &reflUBOInfo; - writes.push_back(w3); - } - - vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); - - // Initialize history images to shader-read layout so first frame samples are defined. + // Initialize all per-frame history images to shader-read layout vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { - VkImageMemoryBarrier barriers[2]{}; - barriers[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barriers[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barriers[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barriers[0].image = sceneColorImage; - barriers[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - barriers[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - barriers[1] = barriers[0]; - barriers[1].image = sceneDepthImage; - barriers[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + std::vector barriers; + for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) { + VkImageMemoryBarrier b{}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = sceneHistory[f].colorImage; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barriers.push_back(b); + b.image = sceneHistory[f].depthImage; + b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + barriers.push_back(b); + } vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 2, barriers); + 0, 0, nullptr, 0, nullptr, static_cast(barriers.size()), barriers.data()); }); } @@ -986,7 +1038,7 @@ void WaterRenderer::clear() { // ============================================================== void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, - const Camera& /*camera*/, float /*time*/, bool use1x) { + const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) { VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline; if (!renderingEnabled || surfaces.empty() || !pipeline) { if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) { @@ -997,7 +1049,9 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, } return; } - if (!sceneSet) { + uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES; + VkDescriptorSet activeSceneSet = sceneHistory[fi].sceneSet; + if (!activeSceneSet) { if (renderDiagCounter_++ % 300 == 0) { LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size()); } @@ -1009,7 +1063,7 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perFrameSet, 0, nullptr); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, - 2, 1, &sceneSet, 0, nullptr); + 2, 1, &activeSceneSet, 0, nullptr); for (const auto& surface : surfaces) { if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; @@ -1050,8 +1104,11 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, VkImage srcColorImage, VkImage srcDepthImage, VkExtent2D srcExtent, - bool srcDepthIsMsaa) { - if (!vkCtx || !cmd || !sceneColorImage || !sceneDepthImage || srcExtent.width == 0 || srcExtent.height == 0) { + bool srcDepthIsMsaa, + uint32_t frameIndex) { + uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES; + auto& sh = sceneHistory[fi]; + if (!vkCtx || !cmd || !sh.colorImage || !sh.depthImage || srcExtent.width == 0 || srcExtent.height == 0) { return; } @@ -1091,7 +1148,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT, + barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1101,9 +1158,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, colorCopy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; colorCopy.extent = {copyExtent.width, copyExtent.height, 1}; vkCmdCopyImage(cmd, srcColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - sceneColorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy); + sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy); - barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT, + barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); @@ -1118,7 +1175,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); - barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT, + barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1128,9 +1185,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, depthCopy.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; depthCopy.extent = {copyExtent.width, copyExtent.height, 1}; vkCmdCopyImage(cmd, srcDepthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - sceneDepthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy); + sh.depthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy); - barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT, + barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); @@ -1543,11 +1600,11 @@ bool WaterRenderer::isWmoWaterAt(float glX, float glY) const { glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 0: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); // inland: blue-green - case 1: return glm::vec4(0.04f, 0.14f, 0.30f, 1.0f); // ocean: deep blue + case 0: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); // inland: richer blue + case 1: return glm::vec4(0.04f, 0.16f, 0.38f, 1.0f); // ocean: deep blue case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime - default: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); + default: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); } } @@ -1815,21 +1872,28 @@ void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) { vkCmdEndRenderPass(cmd); reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - // Update scene descriptor set with the freshly rendered reflection texture - if (sceneSet && reflectionColorView && reflectionSampler) { + // Update all per-frame scene descriptor sets with the freshly rendered reflection texture + if (reflectionColorView && reflectionSampler) { VkDescriptorImageInfo reflInfo{}; reflInfo.sampler = reflectionSampler; reflInfo.imageView = reflectionColorView; reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = sceneSet; - write.dstBinding = 2; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &reflInfo; - vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); + std::vector writes; + for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) { + if (!sceneHistory[f].sceneSet) continue; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = sceneHistory[f].sceneSet; + write.dstBinding = 2; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &reflInfo; + writes.push_back(write); + } + if (!writes.empty()) { + vkUpdateDescriptorSets(vkCtx->getDevice(), static_cast(writes.size()), writes.data(), 0, nullptr); + } } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 5a232e23..98c2f4fb 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -288,6 +288,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { msaaSettingsApplied_ = true; } + // Apply saved water refraction setting once when renderer is available + if (!waterRefractionApplied_) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + renderer->setWaterRefractionEnabled(pendingWaterRefraction); + waterRefractionApplied_ = true; + } + } + // Apply saved normal mapping / POM settings once when WMO renderer is available if (!normalMapSettingsApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); @@ -6237,6 +6246,10 @@ void GameScreen::renderSettingsWindow() { if (renderer) renderer->setShadowsEnabled(pendingShadows); saveSettings(); } + if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) { + if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction); + saveSettings(); + } { const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) { @@ -6336,7 +6349,9 @@ void GameScreen::renderSettingsWindow() { window->setFullscreen(pendingFullscreen); window->setVsync(pendingVsync); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); + pendingWaterRefraction = false; if (renderer) renderer->setShadowsEnabled(pendingShadows); + if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction); if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT); if (renderer) { if (auto* tm = renderer->getTerrainManager()) { @@ -7349,6 +7364,7 @@ void GameScreen::saveSettings() { out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n"; out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "shadows=" << (pendingShadows ? 1 : 0) << "\n"; + out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n"; out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n"; out << "normal_map_strength=" << pendingNormalMapStrength << "\n"; @@ -7433,6 +7449,7 @@ void GameScreen::loadSettings() { else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0); else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); else if (key == "shadows") pendingShadows = (std::stoi(val) != 0); + else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0); else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);