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.
This commit is contained in:
Kelsi
2026-07-18 20:39:14 -07:00
parent 5085493374
commit c945817dcf
7 changed files with 94 additions and 42 deletions

View File

@@ -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);

View File

Binary file not shown.

View File

@@ -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);
}

View File

Binary file not shown.

View File

@@ -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

View File

@@ -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;

View File

@@ -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;