#ifdef USE_FOG // Check runtime fog enabled uniform - allows toggling without shader recompilation if (fogEnabled) { // Fog disabled at runtime, skip all fog calculations } else { float dist = vFogDepth; // Discard fragments at or beyond visible distance - matches Torque's behavior // where objects beyond visibleDistance are not rendered at all. // This prevents fully-fogged geometry from showing as silhouettes against // the sky's fog-to-sky gradient. if (dist >= fogFar) { discard; } // Step 1: Calculate distance-based haze (quadratic falloff) // Since we discard at fogFar, haze never reaches 1.0 here float haze = 0.0; if (dist > fogNear) { float fogScale = 1.0 / (fogFar - fogNear); float distFactor = (dist - fogNear) * fogScale - 1.0; haze = 1.0 - distFactor * distFactor; } // Step 2: Calculate fog volume contributions // Note: Per-volume colors are NOT used in Tribes 2 ($specialFog defaults to false) // All fog uses the global fogColor - see Tribes2_Fog_System.md for details float volumeFog = 0.0; #ifdef USE_VOLUMETRIC_FOG { #ifdef USE_FOG_WORLD_POSITION float fragmentHeight = vFogWorldPosition.y; #else float fragmentHeight = cameraHeight; #endif float deltaY = fragmentHeight - cameraHeight; float absDeltaY = abs(deltaY); // Determine if we're going up (positive) or down (negative) if (absDeltaY > 0.01) { // Non-horizontal ray: ray-march through fog volumes for (int i = 0; i < 3; i++) { int offset = i * 4; float volVisDist = allFogVolumes[offset + 0]; float volMinH = allFogVolumes[offset + 1]; float volMaxH = allFogVolumes[offset + 2]; float volPct = allFogVolumes[offset + 3]; // Skip inactive volumes (visibleDistance = 0) if (volVisDist <= 0.0) continue; // Calculate fog factor for this volume // From Torque: factor = (1 / (volumeVisDist * visFactor)) * percentage // where visFactor is smVisibleDistanceMod (a user quality pref, default 1.0) // Since we don't have quality settings, we use visFactor = 1.0 float factor = (1.0 / volVisDist) * volPct; // Find ray intersection with this volume's height range float rayMinY = min(cameraHeight, fragmentHeight); float rayMaxY = max(cameraHeight, fragmentHeight); // Check if ray intersects volume height range if (rayMinY < volMaxH && rayMaxY > volMinH) { float intersectMin = max(rayMinY, volMinH); float intersectMax = min(rayMaxY, volMaxH); float intersectHeight = intersectMax - intersectMin; // Calculate distance traveled through this volume using similar triangles: // subDist / dist = intersectHeight / absDeltaY float subDist = dist * (intersectHeight / absDeltaY); // Accumulate fog: fog += subDist * factor volumeFog += subDist * factor; } } } else { // Near-horizontal ray: if camera is inside a volume, apply full fog for that volume for (int i = 0; i < 3; i++) { int offset = i * 4; float volVisDist = allFogVolumes[offset + 0]; float volMinH = allFogVolumes[offset + 1]; float volMaxH = allFogVolumes[offset + 2]; float volPct = allFogVolumes[offset + 3]; if (volVisDist <= 0.0) continue; // If camera is inside this volume, apply fog for full distance if (cameraHeight >= volMinH && cameraHeight <= volMaxH) { float factor = (1.0 / volVisDist) * volPct; volumeFog += dist * factor; } } } } #endif // Step 3: Combine haze and volume fog // Torque's clamping: if (bandPct + hazePct > 1) hazePct = 1 - bandPct // This gives fog volumes priority over haze float volPct = min(volumeFog, 1.0); float hazePct = haze; if (volPct + hazePct > 1.0) { hazePct = 1.0 - volPct; } float fogFactor = hazePct + volPct; // Apply fog using global fogColor (per-volume colors not used in Tribes 2) gl_FragColor.rgb = mix(gl_FragColor.rgb, fogColor, fogFactor); } #endif