From c945817dcfb3bcba6e3d5fdff7d3cee5160e0efa Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 18 Jul 2026 20:39:14 -0700 Subject: [PATCH] feat: realistic clouds and fix frozen distant birds Rewrite cloud shader with domain-warped FBM, cirrus layer, self-shadowing and forward scattering. Detect flight-path doodads from bone translation range so unclassified birds get sky-bird treatment, exempt them from the LOD3 bone freeze, and raise their render cap from 140 to 320 units. --- assets/shaders/clouds.frag.glsl | 82 +++++++++++++++---------- assets/shaders/clouds.frag.spv | Bin 8476 -> 15196 bytes assets/shaders/lens_flare.frag.glsl | 8 ++- assets/shaders/lens_flare.frag.spv | Bin 1768 -> 1240 bytes include/rendering/render_constants.hpp | 9 +-- src/rendering/m2_renderer.cpp | 33 +++++++++- src/rendering/m2_renderer_render.cpp | 4 +- 7 files changed, 94 insertions(+), 42 deletions(-) diff --git a/assets/shaders/clouds.frag.glsl b/assets/shaders/clouds.frag.glsl index 1e443a2f..964578ab 100644 --- a/assets/shaders/clouds.frag.glsl +++ b/assets/shaders/clouds.frag.glsl @@ -35,9 +35,11 @@ float gradientNoise(vec2 p) { float fbm(vec2 p) { float val = 0.0; float amp = 0.5; - for (int i = 0; i < 6; i++) { + // Rotate between octaves so ridge artifacts don't align to the axes + const mat2 rot = mat2(0.8, 0.6, -0.6, 0.8); + for (int i = 0; i < 5; i++) { val += amp * gradientNoise(p); - p *= 2.0; + p = rot * p * 2.02; amp *= 0.5; } return val; @@ -48,56 +50,68 @@ void main() { float altitude = dir.z; if (altitude < 0.0) discard; - vec3 sunDir = push.sunDirDensity.xyz; + vec3 sunDir = normalize(push.sunDirDensity.xyz); float density = push.sunDirDensity.w; float windOffset = push.windAndLight.x; float sunIntensity = push.windAndLight.y; float ambient = push.windAndLight.z; - vec2 uv = dir.xy / (altitude + 0.001); - uv += windOffset; + // Project the view ray onto a flat cloud layer. The +0.08 bias tempers the + // extreme UV stretching right at the horizon. + vec2 uv = dir.xy / (altitude + 0.08); + vec2 wind = vec2(windOffset, windOffset * 0.6); - // --- 6-octave FBM for cloud shape --- - float cloud1 = fbm(uv * 0.8); - float cloud2 = fbm(uv * 1.6 + 5.0); - float cloud = cloud1 * 0.7 + cloud2 * 0.3; + // --- Cumulus layer: domain-warped FBM for billowy, irregular shapes --- + vec2 p = uv * 0.8 + wind; + vec2 q = vec2(fbm(p), fbm(p + vec2(5.2, 1.3))); + float shape = fbm(p + q * 1.4); - // Coverage control: base coverage with detail erosion - float baseCoverage = smoothstep(0.30, 0.55, cloud); - float detailErosion = gradientNoise(uv * 4.0); - cloud = baseCoverage * smoothstep(0.2, 0.5, detailErosion); - cloud *= density; + // Coverage: density opens the threshold; erosion breaks up the edges + float coverage = smoothstep(0.42 - density * 0.22, 0.74 - density * 0.10, shape); + float erosion = fbm(uv * 3.1 + wind * 1.6 + q); + float cumulus = coverage * smoothstep(0.22, 0.55, erosion + coverage * 0.4); + + // --- Cirrus layer: thin, stretched, faster-drifting streaks --- + vec2 cuv = vec2(uv.x * 0.32, uv.y * 1.5) + wind * 1.8 + vec2(3.7, 9.1); + float cirrus = fbm(cuv) * fbm(cuv * 2.3 + 4.0); + cirrus = smoothstep(0.16, 0.5, cirrus) * 0.30 * (0.35 + 0.65 * density); + + float cloud = clamp(cumulus + cirrus * (1.0 - cumulus), 0.0, 1.0); + + // Overall visibility still follows DBC density (0 = clear sky) + cloud *= clamp(density * 1.6, 0.0, 1.0); // Horizon fade - float horizonFade = smoothstep(0.0, 0.15, altitude); - cloud *= horizonFade; - + cloud *= smoothstep(0.0, 0.15, altitude); if (cloud < 0.01) discard; - // --- Sun lighting on clouds --- - // Sun dot product for view-relative brightness - float sunDot = max(dot(vec3(0.0, 0.0, 1.0), sunDir), 0.0); + // --- Lighting --- + float sunUp = clamp(sunDir.z, 0.0, 1.0); // day factor + float sunView = max(dot(dir, sunDir), 0.0); // view alignment with the sun - // Self-shadowing: sample noise offset toward sun direction, darken if occluded - float lightSample = fbm((uv + sunDir.xy * 0.05) * 0.8); - float shadow = smoothstep(0.3, 0.7, lightSample); + // Self-shadowing: re-sample the shape a step toward the sun; if the cloud + // is denser upstream, this point sits in its own shadow. + float towardSun = fbm(p + q * 1.4 + sunDir.xy * 0.35); + float shadow = clamp(1.0 - (towardSun - shape) * 2.2, 0.35, 1.0); + + // Thick cores read darker — sunlight doesn't penetrate deep cloud + float coreDarken = mix(1.0, 0.55, cumulus * cumulus); - // Base lit color: mix dark (shadow) and bright (sunlit) based on shadow and sun vec3 baseColor = push.cloudColor.rgb; - vec3 shadowColor = baseColor * (ambient * 0.8); - vec3 litColor = baseColor * (ambient + sunIntensity * 0.6); - vec3 cloudRgb = mix(shadowColor, litColor, shadow * sunDot); + vec3 shadowColor = baseColor * ambient * 0.75; + vec3 litColor = baseColor * (ambient + sunIntensity * 0.85 * sunUp); + vec3 cloudRgb = mix(shadowColor, litColor, shadow) * coreDarken; - // Add ambient fill so clouds aren't too dark - cloudRgb = mix(baseColor * ambient, cloudRgb, 0.7 + 0.3 * sunIntensity); + // Forward scattering: thin cloud near the sun glows warm + float scatter = pow(sunView, 6.0) * sunIntensity * sunUp; + cloudRgb += vec3(1.0, 0.92, 0.82) * (1.0 - cumulus) * scatter * 0.9; - // --- Silver lining effect at cloud edges --- - float edgeLight = smoothstep(0.0, 0.3, cloud) * (1.0 - smoothstep(0.3, 0.8, cloud)); - cloudRgb += vec3(1.0, 0.95, 0.9) * edgeLight * sunDot * sunIntensity * 0.4; + // Silver lining on sunlit cloud edges + float edge = smoothstep(0.0, 0.35, cloud) * (1.0 - smoothstep(0.35, 0.85, cloud)); + cloudRgb += vec3(1.0, 0.95, 0.88) * edge * scatter * 0.6; // --- Edge softness for alpha --- - float edgeSoftness = smoothstep(0.0, 0.3, cloud); - float alpha = cloud * edgeSoftness; + float alpha = cloud * smoothstep(0.0, 0.25, cloud); if (alpha < 0.01) discard; outColor = vec4(cloudRgb, alpha); diff --git a/assets/shaders/clouds.frag.spv b/assets/shaders/clouds.frag.spv index 70b318db25fe1f5010d293d77dcd5466ce533839..89836f399e7b31f900c66c69d3155a8b4c546f94 100644 GIT binary patch literal 15196 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 literal 8476 zcmZ9Q33yyp8HR70q$#BfElXKTN)-x}0!?Y5rAxY{KxjhCP9aQ_Nit=UiP;cTETFPg zTtKnK1<~gL+;Mq8K`p2#+PW(uE+C?)2#U%kqVGF%|LHZC{LlHm?_bVx&pl^K&DdG> zNirtcGnth1j!DYT)MPA5f;O()Hn(hOnL9YtId}dE#~Cm=sVT=9GbO1{YH79JT%k|L zC$TP^dl9Ej2cR0NCdq^7waJ8}xuw0iwY9lrb$dsDfBWFjWyO4ZvCx-q?<@@DJBA7) zdE>@s;tG8o#o^9uKphd;e(RDt#+)QQxxt=!vq!q-ElyBs=q*Y5u{uUFg|>Sj*ICH- z4XrB`2J@M?$U09VFZo7^%g=Po!) zPF!92y^Wkx8Yp(ILUC?&$!BPD#i7CwOAkb;P41x`9PTRz$-fM58y@V@KaT(8J{`r< zaOcWWu|%rjolV-#1tox(psZ_s6A$zRsq;&Xz)V&k(fS-+i?GX?^?K0q;z+ z9pks)=CyWp4d%(JqvFG8^m9tzP_>(Kt)1w(-c7!Kcun#H`0z-jpU1%EF&|&yzlHC9 zXSK-_;h$IWKRwF-=kU`yhUaNFU@tDD(cN;ET$&-&FP|NsMy^TuqCZHIm@Yx^=DKl}#F$G+sAsXoE40FOzz>wFc)v&{VK01cRa=XX7=^HVq9b}Qx+{3ft< z_Tg>coY8LwTgUxy2dy#g7wg_hYh88uCuoh8FHX68SuMu>S@2lQdiT;=&pT{Jy3f_Z ztoaqV`^`hcM6c$oURfpE>StdE|T-9648j zBj@f-i206MdE520KT0)qAJ-!K{xR71b0ur* zyZkAp<~iDiX8zAGb?w?z`#IRU`W%Dbrdrf}1nm6f*7yZxOw@c7Y>Z~##{3d9PG8hi ziyFTITjRzw{x@JX&v@hg{?$&x?_3_oreW&(AESL7i+aBUXY2hQuJ$Ar_3sztwl0^6^;{wHYFVr~8cb}VmC$N5*V+A~?tj z`s~N^Of72s6Rb9oZ^HHX7v{S^9jnK9-*Ppd!H=0$LJZC-|}y@VO>S*R9s{x^6E zrmo+!Q!U2wD%d`PzXmoY#`a%ujLkdrD$JOezfNly7jspMdSk#l@C6?WckYp216MOH z^3@`K9N6z5##3)B+_h@LyvL0(w|wq{BX>MlE%+YbY~K^$YQ{z1Y7xID*!VajC&AVH zt>76sh1SP2Qr~1u&2v#4XQY}n?;v(>@IACQr+aK4xW6^jC-SwqANB?Fm(0fWH_wa6tnAnDxB3^~d_m2CK!`js$x~$6h=NuI5~wzwhg4xOw`l>D{juHID&jYtDsRGx|6d z?%ff691mCXcVnCv^T6imi+&ry*4Agg{>D&?eop{9mf#D(#zemh!LDcU6T$AE7{^Iq zW7ON&blroI@Bzho|EzxHLU2g^HNia>)nm<8fS*Cr7vo4y+bub_dwUGh5##OwBV}9A~zg zHJxuaIJ-Z4;P!Dw`c4aAV|+r}oNCIUU6N{^Pi>swo@Yfg^NrVUZRgcRtM5Oo+=-a~ z?yDz$Pt0czY$0*uX(wQge|TyCKj=?z{Z-d|25{I7W4HkoPzmG!eYLY!OnLt zTK|pf8XS(+6f$#o$G$c@CL-GG?s4 z$Xx<9>^)<9HzEi+!Q;WHs z3O80?^nV)Ib&Hr5u$ukHw|Y9*Tz!toy{Z;9TEW>loB>yB!;E*Ys>K{OfSrSS%;8M1 zdUg(H!AH;GY&2u_MgQl3vvW8Xu4ex+hx5SZ>T^u%Xw{;|1>o!)E`+OX#EkcQQj0mX zgPntV%;93NdUg&u_~ItWA#P)X3SOPwxYc))z!S8 zn=r9?jzw(FR_tZ^ zdMB-Y+P=WMeHZP!F(3W!q1A8e`dml*Ud$SIGyj`u-=vx`?%o|7<@@8Avrip`YMcZE;0dVmqkVol z^SI{PzFbKUcl->*d}q7w=b3+%-?^PRObYHr=6q{#JDKyp Zc**y$*DmnB_2$*vL2HRMcY>yM@E2YvJAD8E literal 1768 zcmYk6+fGwa5QdkW<&2y>Vi6S)#Ss;uXh$2&HaVNrTa1l zLwib;GZeZ*2U>YwN)8S5AYt@XxpLU79EH$HO?!FsR=XpNVDBf5 zCMxyE(4}hqpp^ON(eJ9YkEk?$1HE_LIMDCnN8eMgHIimhOW)cBQ`}iJlFxK~%Jsda zMxE!pD#0hi3D@bBYt%OR8S{l5D0a`@I3Q}lb$_NBj) z`MyVN9^bnI2xmjSJ;j_cKAwh8`BzT-y!EeRo5Nk0!tyQgzPNGo-bA|}#_dCYHp9m8 zZ(-+<*q8ns;;f9{M&}TZed*65#?5~RZGLOUz8_%s{+wAIV>%D9oJ@NbOkyU>X z@%t!WGi(T3p0Rbhu%=kdaSmIopO^TSQN+1!A;!F8U*g`8Gl7Wp^9ub*#5>N=+U|fH z@A?Mg@$Lm|?|#X0UBniP@k?3!8~vO6G9pJjV(z@SyA}Jsifs?_j7M$`+n9aXk2B06 zvEDScJx2c;ww!(@YW*3+yzZD><9Q kFlightPathRange || + std::abs(v.y) > kFlightPathRange || + std::abs(v.z) > kFlightPathRange) { + flightPathDoodad = true; + break; + } + } + if (flightPathDoodad) break; + } + if (flightPathDoodad) break; + } + if (flightPathDoodad) { + LOG_DEBUG("Flight-path doodad detected (unnamed sky bird): ", model.name); + } + } + } + gpuModel.isSkyBird = flightPathDoodad; gpuModel.ambientEmitterType = cls.ambientEmitterType; gpuModel.boundMin = tightMin; gpuModel.boundMax = tightMax; diff --git a/src/rendering/m2_renderer_render.cpp b/src/rendering/m2_renderer_render.cpp index 379ded66..480112f6 100644 --- a/src/rendering/m2_renderer_render.cpp +++ b/src/rendering/m2_renderer_render.cpp @@ -476,8 +476,10 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: // LOD 3 skip: models beyond 150 units use the lowest LOD mesh which has // no visible skeletal animation. Keep their last-computed bone matrices // (always valid — seeded on spawn) and avoid the expensive per-bone work. + // Sky birds are exempt: their flight path is baked into bone animation, + // so freezing bones freezes the whole bird mid-air. constexpr float kLOD3DistSq = rendering::M2_LOD3_DISTANCE * rendering::M2_LOD3_DISTANCE; - if (distSq > kLOD3DistSq) continue; + if (distSq > kLOD3DistSq && !instance.cachedIsSkyBird) continue; // Distance-based frame skipping: update distant bones less frequently uint32_t boneInterval = 1;