mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-01 02:11:51 -04:00
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:
@@ -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);
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user