From ef1e5abe8e3b1ab5513ef06aabae61f8ed2dad7f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 03:53:50 -0800 Subject: [PATCH] Add shader-driven tree beautification: wind sway, SSS, color variation, AO - Vertex wind animation: 3-layer displacement (trunk/branch/leaf) with quadratic height scaling so bases stay grounded - Shadow pass: matching vertex displacement split into foliage/non-foliage passes, removed UV-wiggle approach - Leaf subsurface scattering: warm backlit glow when looking toward sun - Per-instance color variation: hue/brightness from position hash via flat varying to avoid interpolation flicker - Canopy ambient occlusion: height-based darkening of tree interiors - Detail normal perturbation: UV-only procedural normals to break flat cards - Bayer 4x4 ordered dither replacing sin-hash noise for alpha edges - Foliage skips shadow map sampling and specular to prevent flicker from swaying geometry sampling unstable shadow/highlight values --- assets/shaders/m2.frag.glsl | 94 ++++++++++++++++++++++++------ assets/shaders/m2.frag.spv | Bin 7868 -> 15880 bytes assets/shaders/m2.vert.glsl | 32 ++++++++++ assets/shaders/m2.vert.spv | Bin 5684 -> 8936 bytes assets/shaders/shadow.frag.glsl | 17 +----- assets/shaders/shadow.frag.spv | Bin 3516 -> 1644 bytes assets/shaders/shadow.vert.glsl | 29 ++++++++- assets/shaders/shadow.vert.spv | Bin 4348 -> 5292 bytes include/rendering/m2_renderer.hpp | 2 +- src/rendering/m2_renderer.cpp | 92 +++++++++++++++++++---------- src/rendering/renderer.cpp | 2 +- 11 files changed, 199 insertions(+), 69 deletions(-) diff --git a/assets/shaders/m2.frag.glsl b/assets/shaders/m2.frag.glsl index bd91744f..d85455c1 100644 --- a/assets/shaders/m2.frag.glsl +++ b/assets/shaders/m2.frag.glsl @@ -32,12 +32,28 @@ layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap; layout(location = 0) in vec3 FragPos; layout(location = 1) in vec3 Normal; layout(location = 2) in vec2 TexCoord; +layout(location = 3) flat in vec3 InstanceOrigin; +layout(location = 4) in float ModelHeight; layout(location = 0) out vec4 outColor; +// 4x4 Bayer dither matrix (normalized to 0..1) +float bayerDither4x4(ivec2 p) { + int idx = (p.x & 3) + (p.y & 3) * 4; + float m[16] = float[16]( + 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0, + 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0, + 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0, + 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 + ); + return m[idx]; +} + void main() { vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0); + bool isFoliage = (alphaTest == 2); + float alphaCutoff = 0.5; if (alphaTest == 2) { // Vegetation cutout: lower threshold to preserve leaf coverage at grazing angles. @@ -50,13 +66,13 @@ void main() { } if (alphaTest == 2) { float alpha = texColor.a; - float softBand = 0.12; + float softBand = 0.15; if (alpha < (alphaCutoff - softBand)) discard; if (alpha < alphaCutoff) { - vec2 p = floor(gl_FragCoord.xy); - float n = fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); + ivec2 p = ivec2(gl_FragCoord.xy); + float threshold = bayerDither4x4(p); float keep = clamp((alpha - (alphaCutoff - softBand)) / softBand, 0.0, 1.0); - if (n > keep) discard; + if (threshold > keep) discard; } } else if (alphaTest != 0 && texColor.a < alphaCutoff) { discard; @@ -67,10 +83,26 @@ void main() { } if (blendMode == 1 && texColor.a < 0.004) discard; + // Per-instance color variation (foliage only) + if (isFoliage) { + float hash = fract(sin(dot(InstanceOrigin.xy, vec2(127.1, 311.7))) * 43758.5453); + float hueShiftR = 1.0 + (hash - 0.5) * 0.16; // ±8% red + float hueShiftB = 1.0 + (fract(hash * 7.13) - 0.5) * 0.16; // ±8% blue + float brightness = 0.85 + hash * 0.30; // 85–115% + texColor.rgb *= vec3(hueShiftR, 1.0, hueShiftB) * brightness; + } + vec3 norm = normalize(Normal); bool foliageTwoSided = (alphaTest == 2); if (!foliageTwoSided && !gl_FrontFacing) norm = -norm; + // Detail normal perturbation (foliage only) — UV-based only so wind doesn't cause flicker + if (isFoliage) { + float nx = sin(TexCoord.x * 12.0 + TexCoord.y * 5.3) * 0.10; + float ny = sin(TexCoord.y * 14.0 + TexCoord.x * 4.7) * 0.10; + norm = normalize(norm + vec3(nx, ny, 0.0)); + } + vec3 ldir = normalize(-lightDir.xyz); float nDotL = dot(norm, ldir); float diff = foliageTwoSided ? abs(nDotL) : max(nDotL, 0.0); @@ -80,32 +112,58 @@ void main() { result = texColor.rgb; } else { vec3 viewDir = normalize(viewPos.xyz - FragPos); - vec3 halfDir = normalize(ldir + viewDir); - float spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity; + // Foliage: no specular, no shadow map — both flicker on swaying thin cards + float spec = 0.0; float shadow = 1.0; - if (shadowParams.x > 0.5) { - vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); - vec3 proj = lsPos.xyz / lsPos.w; - proj.xy = proj.xy * 0.5 + 0.5; - if (proj.x >= 0.0 && proj.x <= 1.0 && - proj.y >= 0.0 && proj.y <= 1.0 && - proj.z >= 0.0 && proj.z <= 1.0) { - float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005); - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + if (isFoliage) { + // Use a fixed gentle shadow from the shadow system strength + if (shadowParams.x > 0.5) { + shadow = mix(1.0, 0.75, shadowParams.y); } - shadow = mix(1.0, shadow, shadowParams.y); - if (foliageTwoSided) shadow = max(shadow, 0.45); + } else { + vec3 halfDir = normalize(ldir + viewDir); + spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity; + + if (shadowParams.x > 0.5) { + vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + vec3 proj = lsPos.xyz / lsPos.w; + proj.xy = proj.xy * 0.5 + 0.5; + if (proj.x >= 0.0 && proj.x <= 1.0 && + proj.y >= 0.0 && proj.y <= 1.0 && + proj.z >= 0.0 && proj.z <= 1.0) { + float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005); + shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + } + shadow = mix(1.0, shadow, shadowParams.y); + } + } + + // Leaf subsurface scattering (foliage only) — uses stable normal, no FragPos dependency + vec3 sss = vec3(0.0); + if (isFoliage) { + float backLit = max(-nDotL, 0.0); + float viewDotLight = max(dot(viewDir, -ldir), 0.0); + float sssAmount = backLit * pow(viewDotLight, 4.0) * 0.35 * texColor.a; + sss = sssAmount * vec3(1.0, 0.9, 0.5) * lightColor.rgb; } result = ambientColor.rgb * texColor.rgb - + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb); + + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb) + + sss; if (interiorDarken > 0.0) { result *= mix(1.0, 0.5, interiorDarken); } } + // Canopy ambient occlusion (foliage only) + if (isFoliage) { + float normalizedHeight = clamp(ModelHeight / 18.0, 0.0, 1.0); + float aoFactor = mix(0.55, 1.0, smoothstep(0.0, 0.6, normalizedHeight)); + result *= aoFactor; + } + float dist = length(viewPos.xyz - FragPos); float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0); result = mix(fogColor.rgb, result, fogFactor); diff --git a/assets/shaders/m2.frag.spv b/assets/shaders/m2.frag.spv index 9b67de76c5a62645301df0d8d687eef586da6378..273a749132f4c0c50af75d340fe53639795250da 100644 GIT binary patch literal 15880 zcmZ{q2bh%Qwa16DfQTKj>n=7F!~!aUz_L`O3!-AdVRx1t8Ftn!Gz(xi_GnDN6t8KP zBsYn%#27W2t0`bit|g`zO-zg>YBbiU$?x~gcjkU~o_pVC&h!4y|D5-{=dIs-v)iZl zzWsB#Ub(Gv+vG0kolD9Nx!y22xPD1Jeb$0mBfGj=Mvgx6Fdeqd^-28n*^YL7sg)J^ zV!MVp$Q8)5$W}P*jI<%6h`$cmp7#3FF{lr{<#G>W@0%N#n?7sF^tp4V&zicVxuauA zSNEDyVM(djURcsn>?|~Q7grYa8<6oUwl|l0TCxsxcx3abqo+aiu{6J?&^e{p-B#!v zy?XSpmBk~*6 z?>ZFQ9YwiwO1^V>fmQ6s9Glw!q^qOQ+*8VT9*0kRSFw8yoO3mv+TvUsPu)`;afNdq zKus=}J54$9TSQ&#npQ3q^UKW5wx>}i^D?=oyWH9e(>J#=xYTDYbyvByyD8tE#_E&1 z9Imo%{gVB*th8iWXMWlL>hBtSx-;`;>}ze8@61=$wr}ow^yP(uq3F9$?k?(5&x$0E zXTYpwTgBEd+0)0hcXj96n+tP0i_6^cj^Rc9dI}5LimlxzfaPAva7`)q7WGovwRN`_ zy1MjZH?uL?%bli^=$@H5=r(Yh}h3t|~7mwiH?t-gA|Vx!m48E#F*hU#5Sb zT)pvDSMW$N3uZr!;1cJk(obEk34LCnlQ~=AQqWU{6R#{5R#muui65@Hj?VH~p22du zKWy*bEjgS89r@-0XQ8u5&y_u=-Tj-`r&Rj&EXccW6Fxmro&|aLZhpnmVxhgede$N@ z_pf=)D|Zp4634yUT4v4Da~Zt*7Ji<~%rxU&iN6)Nt1aJBUR5U z_pDT(i{4UXW2X8NbWUPVi9@GM0M{vH==o}m(V>!(SC(`l0&8aIp}TqQfsuo z2EEi}>xz9Nx^o}xH={2t<~0+)F83^Y&w?c2>^!^JJ@5)#7aPp}-_W0HWfr%D`{`}; zBwvQ;(nXvPDs3jMDEG8WdT$=V0=qZ!rQ+H`%Zx(S-}~Gvrj*j=G6pVRW;b@nJ=r%m z39hA>TzPoZHdQCNQ{l=z-ElP~em*PUs%tv~);r0vZ>7owN9qaZz6y@!3Flr2 z&i!axp9jI&MsE8I=RTAhoZ;Mma=T_Y_n+LJ8P5LX>XGTJMcnbzSqtyXp@{j0-T*dN zb??!vU0u6HyLal|Ncj7l_4(y<%f5W(Zkyn~3D&nys(Y`fN8jEbw%q}lL2S>T#j!g# zp*t2!mUG;d`Kq+DX||oZ?HZA2Cs*6fab(-6+ipD)?d1NaT_rEaq~AkG_{BOtk>O$; zpU-fyj&EkTSVt!(;>9{{pW(be?6)4y`F4)X&$aOm849+9-cV5!-Fw0Lw`lj=X8qOm zr%LReFZ~^t{+@?uzZ1Qp;_o-8N)9{Y4wR7v7x=+TW*tOAq47GOoy}2d4!(6`*+hwu3V%uY> z-EX!Tm+Iboa?XqGCLk95CsJ#Z9}H@u-i>I}sg2nS$(GRfV5iN%pr3J$OP%EGSKa&5 z;uw9N8_&M9 z#`!q7yyJ!A^#3*3JdJ0b>OQk#o{aN5WYnlnnxg&h(KkJN)8r9Eb*-LDedNRE1@M)x zA3Qzum(T}3vu|~5uc5y??8U~gzm2|W<0;j#zLV&cxp1!gl91yv=l)>VHk-50Qa*`& zaEhDI9h17xRz5k~H>5b`$a+7GxyDJ6&oJ~&8*i)THv)aj=FQc6sW1P{^~x2YT#(Zo6mv`(lHuNXace!W&81IFr!N#UageocO7Vm{; zQ1mnYuTz6uk}tK-T1)KhSJ4~LL!#f062JA;b?Hl@t*x!x2bD8Xhwfb<}zc!<5zrIHIeLMWM z`_7%!U#!tzsnOrc=$_R9NVIo+zCVX<{Q4T*cjU0!zA>Zg@B3ib)qNk#>hm+Y?R`HC zySnd(q1)c~!L084VCee$J{Y>|<@;dh+Bar&v@@n=p#RYxbA(q4~SzQ#xri%7S!1EF}AT5 zBDUApGo>D5JQ?g5edfm)PXWu{o{sTUu-szA7@j*hv1e{567fz4*Ty>oPCnu-0m~WB zIm=U9oHK1_BF%_n6h}Wx!H!>lZTdvNE#TUI3vlw$Z!1{N@A~%JMs2ZQZOf3e5Mzs@ z-y+z4_1C6P^t&8f+iwX@KK958uw3ksGT36j+S(C0V~eBT4zT@(%{rTh`DEwAd5OHv z2G{1*2`3-((FK-^b?5>S^So}J^*qU$3c^Zq%o zakNF;jbPhdmgcbute^ZKz7@G%HzAI3IHJ7)IRP=x>#4Ozyw8KpJ@hYt^|>!y=P!cw zk#`Mlrna~S+P;L`jJO8kSZ6uUMXZmty8ByS_jH_-FN4P;+57J+=<;!XzY3O{fSAYE zsV(NA?Q4jfvBhytZUv7;vghPBbom(b?cnUby8~Sxd2^1q*6*-0ny9~l*stTz-#*Rj zThtcw(&w9qoOy|3e&n+A;k?9rd>ib1EKcgmGw(ZK`IwLIg5_d9z6ZA0ueLi8Ir|km zR?o&gi1RU*y>d78y(uSeao_YtZb5_WIeCj~AM5@Hu=eMW zL451liu!p(pKTEB7pHsVkKpH#7m)D%6IkDYh<5kRGt~02cm52Pi~H^`V7Z}4#QrN- z{v~8X+TY9I6A*32eUVx&;=T$tZp8Q-Snf3>@_QW|`Du?F-T-UM=I|!Ed^U%_qnm>^ zb9jYXE^_z>IGe+}=yLBMF^>1ZZzI}_@fNjQ#CRX<*h2pRY(Mi@57+ZUL?83e7JmN( z8|!N3OTUj0edYBTL;W!_7KwY{la!OUI5)i!=g=7cLi90KoC}?du>&$cU7KET`dS>5 zK625fciLtM_Gr^5Z9|o`(MK-Y)Pe1%FLku(2euz`v>$!sJbO>Wx%aIdulWze=I<`9 zzxvi-KFRxlHe>kiCvU$Kv26qPPB{rNPCx2x;pF2^*$ynX7@IMCUzF3o3C`dDliY3Eqx~UZ zZ6grJUr#L;IUEYE&EYUO`G|1@SnhBn+KmF&j(Ieke6%|XEa&fak^9kL<7tcB$AI}H zxoeO1$AGm(?*3LG7wbG0>^jGsj{`ft(2oV%ChnW@V148p=+B(o9~01xr9IBqL@=M^ zd})upBk$gcePHdJOr@TNOhE?I&K$g_rl)+|Q!~)z6pNL2KA713rM zjnwin53|5A4~}Oxx<2tNoCDTJ-u5%8)uy^3@i2mMzr^3m{chS?paucvQ2WNmS&Vjbmk)??Fi;d&jo(c9|kG0LC z%PAJCxZjtcXm?iHcSu?e~xJ2E6UAzM9v)TCWlXmn))Xq@{*y0??myxxI;}TC|rXR%K33gm( zBiZ}03td~hr*(tnhSJ9PE5R1yYwJPej4zJ(tH2R|2>#~k{ks}oTf9@Q0rN@z7U-Dv zPkmyX+FhII!&=>O+NWcSc|I51n9-$cx|z^SAykYZLbDfTw85dA#%>M*c`8=z78?I-%XsON#IYX ze9YPP=_WEmgpIk>S+CGPF-}*Qg*HFuu zhxScixj3sgf#np7m3=w3QPiJ@(0>)!7_Q|P(BTI2XOK+ zANPahVm=~{b>#6wu>F{u{`O%W4^l@S`aFQhnTI&?cnEABeUX@tAA#j#J{|_kH4xDG zh&a}f$0K0-F*p6~!#w_rI`Yux$B3MHh$D|j!L@lj1}7i$@e{CI%tyqrjy!$}wjXoT z-#*Oa=hTshK0ia`%tIV!`f>1pjQ$I7BNESsC&2p1$9J+{g88uGxt#X+jo?>cbJZSq z{eOeEAll;j@f4U(@@&!mB;uLgOs&n_o}vB?@@%SmKm8W$oW=d~JFuK$v5NWqJxcBT zJ_jcs>;4C@eMbJzgY}V*JLCnh{aAwgW6DX#o%$yfeYF`Ud|w1>^Nv1+?=IfQe@1s~ z^4cBy3Tk=xTdb3HQ@o-@m_olyolPp_Bz<&9U%WVM9z5P7{i<3nTYQs&colqaF3et~c1W^3KTz zV7bVv4_GdK+v&s1#v!S0!OXYu>R066*hez6r;PO;!_8c80`{Xi6B>lgb% z&OH-rZXJDYjm#nUl5gnuvWU zwnfyA<8ZLWamXKr$k|35^D!Fi|2W0pG>-zy`F~C^j-$bjV>qI{f!b%VIr={*+9TdL zuw1k~7Hr$Y5!?CyQTl{^d}<$w&Htq`u5FC%|F39|*b~4Bm+ps&=-K&eM3>Vq=1hjNdJ$q1#42zROGp+pY=G zZf=vP<>Og<9N1hgL58I9eSRGeuPx?arfrb;J~0cdPr|3aTg*oPzhcdS*A{)w&G=>C zf#<<%i$3OqZ7U!9%jQFE|Tq-<~P`?ZZ4u z)E4v5XE`Eg9^#m{72w)=YloAMIa0U(Xm1_kD1&R~tOHIy{zh~*SkAG=oOPnhn`<|< z#a!jP5IJ)dM_)bQ+P+r8$wyzSz;gbEZq5cK`PggcfjxKfF^_WQ6fv#i9IXdu&(Q{S_h-CcTmaU`65NF;=Q#@QqLkA$ zewVr!#rFDZH?}#gqt^B@Cvgno|HFE21|b&zc6~H{1E{w~oCp8*?f=GZOT8V^Kh^i4 zu1|I6N&Ak7?c!c@kMET7kKv=fbIQ41(QX&8?cxsBZ&yS<+U=Hdw!0{ewFg*!cVvCa r?FqIIZI0QT<;3;WJ`>~o9Rb#E9^N~nk$OZv?zbbs@}DplV(0DOFA!oK literal 7868 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 E2kfx{jQ{`u diff --git a/assets/shaders/shadow.frag.glsl b/assets/shaders/shadow.frag.glsl index 3ae16e38..986c5166 100644 --- a/assets/shaders/shadow.frag.glsl +++ b/assets/shaders/shadow.frag.glsl @@ -16,22 +16,7 @@ layout(location = 1) in vec3 WorldPos; void main() { if (useTexture != 0) { - vec2 uv = TexCoord; - if (foliageSway != 0) { - float sway = sin(windTime + WorldPos.x * 0.5) * 0.02 * foliageMotionDamp; - uv += vec2(sway, sway * 0.5); - } - vec4 texColor = textureLod(uTexture, uv, 0.0); + vec4 texColor = textureLod(uTexture, TexCoord, 0.0); if (alphaTest != 0 && texColor.a < 0.5) discard; - - if (foliageSway != 0) { - vec2 uv2 = TexCoord + vec2( - sin(windTime * 1.3 + WorldPos.z * 0.7) * 0.015 * foliageMotionDamp, - sin(windTime * 0.9 + WorldPos.x * 0.6) * 0.01 * foliageMotionDamp - ); - vec4 texColor2 = textureLod(uTexture, uv2, 0.0); - float blended = (texColor.a + texColor2.a) * 0.5; - if (alphaTest != 0 && blended < 0.5) discard; - } } } diff --git a/assets/shaders/shadow.frag.spv b/assets/shaders/shadow.frag.spv index a57b04018aa0b6246204a716b2b2b80cbd5bfdfa..b14825fc3ff3b2a680b2928971c64fff8f138a82 100644 GIT binary patch literal 1644 zcmY+D*>2NN5QYy;(uS69bfXkX(y|vq5p9Kp06{_KBB+&83GN~*Hq?@1M~*|>m03RJO&L{zDuF?nl2hbWhK4dKm_hcNBD$1%7qZPvsx~ zZJY*i^vdn_gjKYYNr;k)e#<&j|71T7d_ngt2)UA4j~Hz7B> zxhDnxlF#4BG2Z2;?%O<^-*kf=J&;m|`QcN8T|Lx^&Y4>6owBZ0SvRu;?N|Hpb-fm$`k_FsKG=N3sf{T>27H4bjNfj0VhDjS<&)J zZ>`b`U%+c#f+w*YN~`?7?z7q>yXvpK*1x8`*4g`XE$&~MB#V;e$;#xrq`lTAi(wMD zC9OR_GCnfUXch-{@7bxxs-&yssL$$TY0}LurVEvthRxt%um(SQ3LtfnHA!v~)SdJu z`H{)|=xBcA_~g{g%w(f^sal$>R%)fmVr8~8)vU~y^jntktJJ2dbH%Jf4<6aNJ)CzL zHO4O#iuHxD!fau>fePE^mFF6z;d-s~Ki)pzCrTHabF`EL3X6iOMut^k3FGPtSk;C+p2hy>_fHJpDQ{$-n^*2> z&5#2ft=DIZZF_gJ4WVH{mOjq-RDHHu9ILnbksE?*8dt5)BDVaFvCnn7Yu7p>H@~Z8 zzlujT@XI0)njwS3lV2+Jg zyvyqMax%}RJqzt^pYG%iWb^2^pIqwpAh#pUy&T!xIdo^E&3s{BjqF;F#W`)l&)HZ* zU*k5Uy1e=(c5|v*SKZmjMc%uR?Nk38dz?G$eQ0|u|Q#s-vLN~X(_0*lc zoHKlcJqKLoSKr>!+WB`N>uY}P?gM~zICSr2@2wvX#QAokdsjTqA$DWpyoXa=ehX7S zlIqsTuF$tKiD3--+3(TRNzQ)My(_LbPrj{o=J_nTGtzEK1_uW_{Z`O#+?;FmtKI)axm@npo9I7$zonyJMDM-Te_$_DF|V_5d4@k=oRz(J zSNzYIQ$5GNEu%ZTp^UD-|BCR}?*Ah69P+hPKSCAn@Jir4_ii7ghWB|DkpBfA@A2A{ z^M3!%Dc7;P{=#3|oj}gBE1Spryajld4-sP@>QQ$qvUR=NQTJ|S`KWsjvRu@?57}j1 zZT>IhtgCG9e)ju;x%+^*?f(HFua9^5QQ*DzZimlfDSsUwV;%=`-tVg^w-ec1+Ey?v z{hk1efHTt`@q@_Pd=GK9J;<7kaGfwUR@VnTX`oWabH@=zNpUgMr>~8!p`*Gk~i+s-@o9{GJ@jaga`pA3F z^T51A?An}(cknsjOnjH-@(w-^0 z;~a>%V#-Hc8C|Xft`g^L&ja&mi}!H>c@gk^Xpi_ek+u2%jPt&Q?0I*Cs9QyrkGN@M zW91{RhJ1UUbvXHmdmCA921K8;$o9D%M7;*Ge8kNm%QZptIge~UZPDifa`dS^;x8d< zy9naE?;zja=euz75%(UlvGVbb-$%B8ZJuR4`v<`9uFdy#CgtpB82JjwzSFDd`ouf^ z5cvv_UkviVJeS$EdFCAZM_>qK-_rGz51)_Gjm!Fcf-diQm$H8fjFXREKTG)mc57c_ zmox7Q_RoRy%Kjt1Ncng(U!u#c2JtPwLN-^t^{;wM-@eLL= diff --git a/assets/shaders/shadow.vert.glsl b/assets/shaders/shadow.vert.glsl index d3801129..f8c64618 100644 --- a/assets/shaders/shadow.vert.glsl +++ b/assets/shaders/shadow.vert.glsl @@ -23,7 +23,34 @@ layout(location = 0) out vec2 TexCoord; layout(location = 1) out vec3 WorldPos; void main() { - vec4 worldPos = push.model * vec4(aPos, 1.0); + vec4 pos = vec4(aPos, 1.0); + + // Wind vertex displacement for foliage (matches m2.vert.glsl) + if (foliageSway != 0) { + vec3 worldRef = push.model[3].xyz; + float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0); + heightFactor *= heightFactor; + + // Layer 1: Trunk sway + float trunkPhase = windTime * 0.8 + dot(worldRef.xy, vec2(0.1, 0.13)); + float trunkSwayX = sin(trunkPhase) * 0.35 * heightFactor; + float trunkSwayY = cos(trunkPhase * 0.7) * 0.25 * heightFactor; + + // Layer 2: Branch sway + float branchPhase = windTime * 1.7 + dot(worldRef.xy, vec2(0.37, 0.71)); + float branchSwayX = sin(branchPhase + pos.y * 0.4) * 0.15 * heightFactor; + float branchSwayY = cos(branchPhase * 1.1 + pos.x * 0.3) * 0.12 * heightFactor; + + // Layer 3: Leaf flutter + float leafPhase = windTime * 4.5 + dot(aPos, vec3(1.7, 2.3, 0.9)); + float leafFlutterX = sin(leafPhase) * 0.06 * heightFactor; + float leafFlutterY = cos(leafPhase * 1.3) * 0.05 * heightFactor; + + pos.x += trunkSwayX + branchSwayX + leafFlutterX; + pos.y += trunkSwayY + branchSwayY + leafFlutterY; + } + + vec4 worldPos = push.model * pos; WorldPos = worldPos.xyz; TexCoord = aTexCoord; gl_Position = push.lightSpaceMatrix * worldPos; diff --git a/assets/shaders/shadow.vert.spv b/assets/shaders/shadow.vert.spv index f972e12ecf4bda55ef9a5b9db835a7c193bf7493..3399c4ccd99a30d364714b1b6e1fdd0deeab2fda 100644 GIT binary patch literal 5292 zcmZ9O`Z4x&`PvS zBMnirpqrK1MeIhUR+3?9KfoW*AJXcwR%`Y7?DIay?QGUF&-490*Y~pbp=aL81zC1Z zc3yUV_V=vwT%65=$>915JvcNnw64`2Uw7U5YmK-d>nQ?_xiDK)*z@^2RjJkmGAEH2 zk(ZHE$m_@(NDu$!p-+=MKRY)Y92yCsmEU@af5RqOfac(s|2wX65! z#w{$xRqJE5nelQ!FA?Sbz4Uk+J*FG2OrP^qh5+g0ZFb-2B7cQq^ZvB^%dxno_flPu@DJ8^RRiXQjF)$++a%IA@geeTz7A!@O0 zIrHVz5oZs%o)V`m=N>wXxiO1pZLh$dvDlo&u=2}6V(rVY`q0ae%c3;8O5ntYwJ)rRf_dR~B;=>tBj?PVa*C#oh%u z>zj8u67yVYFzdZt_VN$|wPbKSjo8qE1@^nSd6?fqJTh<}6bLE^m? z_iLCA_INvDzVZ6~&Te8C-7|e#=;8P9O2lub-%00KjD80q|2#2%6W^J*CFp-q<7%|e ze~H)UZ-|`dqrHXcuR*^XS%s`dqQ0}e2hq2iQ*_<$O`N=sYjZ8m>zV3zUeDzHh->hB z=)CUJwTS#9Y2QAWIKPpL=wZxth;^-@zaNP`@t)|9dh4;}0$-o-L#h6Uz;ebpqp@<% z=(qL;#69&J95p_g_^7c7EVnVmxDT5V*Snh&^xfQoY)*nI=6@>9a1&U+-{A)mw;kKM z`kc%AE*CTGz?NIYj<_c`Bkqa!NngzNF>HOeAdx$S?K&f7C$@3&_8LUXMf`4Tdqw=M z*mA>2^uG<;{+@&Wh~0y&Zv=^YAIC2Dza4Cxy#05f<>CzPz@B>s`{3lG&nL0v{65>q zGx!v;9EmgdbmHWFoHx#(g025fB+lS2Z0qWCuDxivSjRZFoO5~?ClK#*jzrA<#K)Xf zu-qgPeLjnAEq$?{cVj!B=czyPr?Bm6U_|IWGchqcQ%QcZ$ z%M7+_X(N$$54L>d9l(~GMPe-nv8|^s)^ablYw>RAkNnSL>$?w$9$&yNujPwiW&=l@9J9!AGnzKU(GK70F{AQ!WK4O=eO z`6zarnZD@%b!>f)A(49oyS&cF!N$qk>o8g_;-A2_SJZq8Tkc6D`hNpE_E~?#eiK{Y z(@50&7IwM+F|cv+_CJc2i}>T%eis6N8{2yod-WY`W8@>|yV$NHV!nrMoV@3^2K{|x z?mc@Z@salfu$=jk_e1Q+b6&A=(fdc(yGr<3Z0}Cg{4usM@{#i#w(E|VpI{p&AANs{ zZQt@c{xh(AKvp5E5$9Wh{v{$WUV#1; zV$GxEh<}Y3BX5rriHrE(g5~13`ghoJijQ|^9%AlEw0?63(0@Q)M)bXyaLoE=u(K{f nV%Ar&<)imsuA50L=j8?32HFGpt#2&(~y?Q%s4Y4fGFUG3xbLZ z`U<{gqWnQKu~#Cw-Y ztvORUoVGjQ;C-QEjapVYo=(nJPA~A*IiJ<5hqGCBF@8y2Pv7 zT?VqmL+GPf_a44+#Y$hSSF`D~y+4oXO*W(3C$jp%N{5`jAJ$sXn;U?zVGpPJN+OC?Jlg-L>N+qrA zl$>Yl_dRDD)wG7TN9WMZVh?L}ZjRh=PG3GpG+=L|(W>GmsxO}_-hP3!_8;&auT4#) zt=H35CoRtH^5*Glv@>6s=*4-w_2hh3?@VTAir#d6_88yZTGrf`wL6vibh?mdpY_ev zY70K!@+!P*w3xqvu9+O5X6Mgk|6IXYgPb!GoM+1|DRG`D=UhjeIdZp^IA==E^G2yj z?S9teOsz*eGwh*~ZJssv6#fU%{m3wKfP81lSSGFZXd-XXNmMlh^hN-sL=PYaK=+PcHJT)xDxN=hIq@ zv9?Qvjmm9*@51kX&ZO(+uFB7lGha>}ah@UPy96hev(MIIu5qjQs~&zr;C^)M_iVw1?GL+qgk1&O&jIqCTlZGn?-TS=MBekX{r#0Q-@48tasCCc zzR3R)?A|dS+7}DXzI+4jN36&5zAb$6o~P~i67%*Q*t5c~?e`rFzneqsYOS{x9BP5{_u4q`8gVNN&iW?U(LHZR%r{=Y-`{Skwnlv;oZ)wJ zH{!SC_hTLQ@*YI~4`Tdw))t&Gf3b(ZQ?7rB*S8Ll^F74wV-5Es8<0A--`E2MXC1zc zwskn)k0WxQkT&miW> zM-4l{*5KSk4bOq)qlR7Js6k)Uusg@sYj_^p8e%VN7)Ioyh7quN!-&1~&GsPH<_c~v z*jU$vB5ohpSbY)q0@&P}O5A?1xpI*^3N}_>Hiv;G-X}Vn;vpML#CN`rONz zIE>hj*vo#rR`Ah}*Rjo$k9!;eTcazuH^9ca;%;w(jnx-%Z-LGA?Sp$8Y_447z5_N^ zU&Or&Hn-f5_pr?^_hSm%SbdTEKG@uHKPuSfmiuuO`+Y=T^kW+AU98EyjfsAw;31@n zL_dy!<)a_R!O;(W(T^FhzUW5=wjZ&V{rCWpkA9p0nWjD**xYhI+Suln`_aKRR$t`Kfz9nHaVNp%%Eh_!U}N<~ zKTd(Yi#55oG5&sTM(d0BJq?!AUW`72EJc1OelI=(8zUe856JyP^#f>syVdsR0{T0-6TEDs9psyg8 z5q*~mJ8Jz2+gev6QR~lO`8fL*uxH16{0cTk-hHm2e?zV!?&CSy#{EZSJCOeYNrO}N diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 88edb24f..79931692 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -251,7 +251,7 @@ public: /** * Render depth-only pass for shadow casting */ - void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix); + void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, float globalTime = 0.0f); /** * Render M2 particle emitters (point sprites) diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 12e11ed5..8d599ce1 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -401,7 +401,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout VkPushConstantRange pushRange{}; pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pushRange.offset = 0; - pushRange.size = 80; // mat4(64) + vec2(8) + int(4) + int(4) + pushRange.size = 84; // mat4(64) + vec2(8) + int(4) + int(4) + int(4) VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; ci.setLayoutCount = 3; @@ -2109,6 +2109,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const glm::vec2 uvOffset; int texCoordSet; int useBones; + int isFoliage; }; // Bind per-frame descriptor set (set 0) — shared across all draws @@ -2382,6 +2383,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const pc.uvOffset = uvOffset; pc.texCoordSet = static_cast(batch.textureUnit); pc.useBones = useBones ? 1 : 0; + pc.isFoliage = model.shadowWindFoliage ? 1 : 0; vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); @@ -2625,46 +2627,72 @@ bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) { return true; } -void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix) { +void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, float globalTime) { if (!shadowPipeline_ || !shadowParamsSet_) return; if (instances.empty() || models.empty()) return; - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, - 0, 1, &shadowParamsSet_, 0, nullptr); - struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; }; - uint32_t currentModelId = UINT32_MAX; - const M2ModelGPU* currentModel = nullptr; + // Helper lambda to draw instances with a given foliageSway setting + auto drawPass = [&](bool foliagePass) { + // Update ShadowParams UBO for this pass + struct ShadowParamsUBO { + int32_t useBones = 0; + int32_t useTexture = 0; + int32_t alphaTest = 0; + int32_t foliageSway = 0; + float windTime = 0.0f; + float foliageMotionDamp = 1.0f; + }; + ShadowParamsUBO params{}; + params.foliageSway = foliagePass ? 1 : 0; + params.windTime = globalTime; + params.foliageMotionDamp = 1.0f; - for (const auto& instance : instances) { - auto modelIt = models.find(instance.modelId); - if (modelIt == models.end()) continue; - const M2ModelGPU& model = modelIt->second; - if (!model.isValid() || model.isSmoke || model.isInvisibleTrap) continue; + VmaAllocationInfo allocInfo{}; + vmaGetAllocationInfo(vkCtx_->getAllocator(), shadowParamsAlloc_, &allocInfo); + std::memcpy(allocInfo.pMappedData, ¶ms, sizeof(params)); - // Bind vertex/index buffers when model changes - if (instance.modelId != currentModelId) { - currentModelId = instance.modelId; - currentModel = &model; - VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); - vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &shadowParamsSet_, 0, nullptr); + + uint32_t currentModelId = UINT32_MAX; + const M2ModelGPU* currentModel = nullptr; + + for (const auto& instance : instances) { + auto modelIt = models.find(instance.modelId); + if (modelIt == models.end()) continue; + const M2ModelGPU& model = modelIt->second; + if (!model.isValid() || model.isSmoke || model.isInvisibleTrap) continue; + + // Filter: only draw foliage models in foliage pass, non-foliage in non-foliage pass + if (model.shadowWindFoliage != foliagePass) continue; + + // Bind vertex/index buffers when model changes + if (instance.modelId != currentModelId) { + currentModelId = instance.modelId; + currentModel = &model; + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); + } + + ShadowPush push{lightSpaceMatrix, instance.modelMatrix}; + vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, 128, &push); + + for (const auto& batch : model.batches) { + if (batch.submeshLevel > 0) continue; + vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); + } } + }; - ShadowPush push{lightSpaceMatrix, instance.modelMatrix}; - vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, - 0, 128, &push); - - // Draw all batches in shadow pass. - // Blend-mode filtering was excluding many valid world casters after - // Vulkan material path changes (trees/buildings losing shadows). - for (const auto& batch : model.batches) { - if (batch.submeshLevel > 0) continue; // skip LOD submeshes - vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); - } - } + // Pass 1: non-foliage (no wind displacement) + drawPass(false); + // Pass 2: foliage (wind displacement enabled) + drawPass(true); } // --- M2 Particle Emitter Helpers --- diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 035f1b12..8ebc31bb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3975,7 +3975,7 @@ void Renderer::renderShadowPass() { wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix); } if (m2Renderer) { - m2Renderer->renderShadow(currentCmd, lightSpaceMatrix); + m2Renderer->renderShadow(currentCmd, lightSpaceMatrix, globalTime); } if (characterRenderer) { characterRenderer->renderShadow(currentCmd, lightSpaceMatrix);