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 70b318db..89836f39 100644 Binary files a/assets/shaders/clouds.frag.spv and b/assets/shaders/clouds.frag.spv differ diff --git a/assets/shaders/lens_flare.frag.glsl b/assets/shaders/lens_flare.frag.glsl index 98b99b5c..4af7cdb1 100644 --- a/assets/shaders/lens_flare.frag.glsl +++ b/assets/shaders/lens_flare.frag.glsl @@ -16,7 +16,11 @@ void main() { float dist = length(center); float alpha = smoothstep(0.5, 0.0, dist); float glow = exp(-dist * dist * 8.0) * 0.5; - alpha = max(alpha, glow) * push.color.w; - if (alpha < 0.01) discard; + // Fade to zero before the quad boundary — the glow term alone stays + // visibly nonzero at dist 0.5, which draws the billboard as a bright + // square behind the sun. + float edgeFade = 1.0 - smoothstep(0.30, 0.48, dist); + alpha = max(alpha, glow) * edgeFade * push.color.w; + if (alpha < 0.004) discard; outColor = vec4(push.color.rgb, alpha); } diff --git a/assets/shaders/lens_flare.frag.spv b/assets/shaders/lens_flare.frag.spv index 1b19ee3a..b3e16db2 100644 Binary files a/assets/shaders/lens_flare.frag.spv and b/assets/shaders/lens_flare.frag.spv differ diff --git a/include/rendering/render_constants.hpp b/include/rendering/render_constants.hpp index b4f5084d..7ebc128e 100644 --- a/include/rendering/render_constants.hpp +++ b/include/rendering/render_constants.hpp @@ -20,10 +20,11 @@ constexpr float M2_MAX_RENDER_DISTANCE_LOW_DENSITY = 2800.0f; constexpr float M2_LOD3_DISTANCE = 150.0f; // Beyond this: no bone updates constexpr float M2_BONE_SKIP_DIST_FAR = 100.0f; // Beyond this: every 4th frame constexpr float M2_BONE_SKIP_DIST_MID = 50.0f; // Beyond this: every 2nd frame -// Flying ambient models have obvious, rapid wing motion. Keep them animated at -// full rate while visible and finish their distance fade before the generic -// no-bone LOD, so a bird can never remain on screen in a frozen pose. -constexpr float M2_SKY_BIRD_MAX_RENDER_DISTANCE = 140.0f; +// Flying ambient models have obvious, rapid wing motion. They are exempt from +// bone frame-skipping and the LOD3 bone freeze (their flight path is baked into +// bone animation), so they stay fully animated out to this range instead of +// despawning at the generic no-bone LOD boundary. +constexpr float M2_SKY_BIRD_MAX_RENDER_DISTANCE = 320.0f; // --------------------------------------------------------------------------- // M2 culling geometry diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 23303fcc..4bf93c04 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1395,7 +1395,38 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.isBrazierOrFire = cls.isBrazierOrFire; gpuModel.isGroundFire = cls.isGroundFire; gpuModel.isTorch = cls.isTorch; - gpuModel.isSkyBird = cls.isSkyBird; + // Data-driven flight-path detection: name tokens miss many flying doodads + // (buzzards, swallows, bird swarms, ...). A small mesh whose bone animation + // translates it tens of units is a flight-path doodad — it visibly freezes + // mid-air whenever distance culling stops its bone updates, so give it the + // same treatment as named sky birds. + bool flightPathDoodad = cls.isSkyBird; + if (!flightPathDoodad && !cls.disableAnimation) { + glm::vec3 meshExtent = tightMax - tightMin; + const bool smallMesh = meshExtent.x < 6.0f && meshExtent.y < 6.0f && + meshExtent.z < 6.0f; + if (smallMesh) { + constexpr float kFlightPathRange = 15.0f; + for (const auto& bone : model.bones) { + for (const auto& seq : bone.translation.sequences) { + for (const auto& v : seq.vec3Values) { + if (std::abs(v.x) > 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;