diff --git a/src/rendering/tsl/effects/EffectPasses.ts b/src/rendering/tsl/effects/EffectPasses.ts index 93915404..001ef7f6 100644 --- a/src/rendering/tsl/effects/EffectPasses.ts +++ b/src/rendering/tsl/effects/EffectPasses.ts @@ -713,13 +713,11 @@ export function createFogOfWarPass( const uCoolShift = uniform(new THREE.Color(0.85, 0.9, 1.0)); // Volumetric fog parameters (for high/ultra quality) - const uVolFogDensity = uniform(0.015); // Base fog density - const uVolFogHeightFalloff = uniform(0.08); // Height-based density falloff - const uVolMaxDistance = uniform(80.0); // Max raymarch distance + const uVolFogDensity = uniform(0.02); // Base fog density + const uVolFogHeightFalloff = uniform(0.05); // Height-based density falloff + const uVolMaxDistance = uniform(100.0); // Max raymarch distance const uVolSteps = uniform(32); // Raymarch steps (set by quality) - const uVolScattering = uniform(0.8); // Light scattering intensity - const uVolLightDir = uniform(new THREE.Vector3(0.3, 0.8, 0.4).normalize()); - const uVolFogColor = uniform(new THREE.Color(0.15, 0.18, 0.25)); // Dark blue-gray + const uVolFogColor = uniform(new THREE.Color(0.12, 0.14, 0.2)); // Dark blue-gray // Map configuration const uMapDimensions = uniform(new THREE.Vector2(256, 256)); @@ -908,107 +906,71 @@ export function createFogOfWarPass( // Start with scene color for visible areas const finalColor = sceneColor.toVar(); - // Apply explored effect (desaturation + darkening) - same for all quality levels + // Apply explored effect (desaturation + darkening) const exploredAmount = isExplored.mul(float(1.0).sub(isVisible)); finalColor.assign(mix(finalColor, exploredColor, exploredAmount.mul(heightFactor))); + // Apply unexplored effect (cloud overlay) + finalColor.assign(mix(finalColor, cloudColor, isUnexplored.mul(heightFactor))); + + // Add rim glow at visibility edges + const rimContribution = uRimColor.mul(rimGlow).mul(isVisible); + finalColor.addAssign(rimContribution); + // ============================================ // VOLUMETRIC FOG (high/ultra quality only) // ============================================ - // For quality >= 2 (high/ultra), use raymarched volumetric fog - // For quality < 2 (low/medium), use flat 2D cloud overlay + // For quality >= 2, add raymarched volumetric fog depth const useVolumetric = uQuality.greaterThanEqual(2); - // Calculate ray direction for volumetric (camera to world position) + // Camera position from world matrix const cameraPos = uCameraWorldMatrix.mul(vec4(0, 0, 0, 1)).xyz; const pixelWorldPos = vec3(worldX, worldY, worldZ); const rayDir = normalize(pixelWorldPos.sub(cameraPos)); - // Calculate distance to surface for raymarch + // Distance to surface const surfaceDistance = length(pixelWorldPos.sub(cameraPos)); const maxDist = min(surfaceDistance, uVolMaxDistance); const stepSize = maxDist.div(uVolSteps); - // Base fog density modulated by visibility state (sampled once at surface) - // Unexplored = full density, explored = reduced, visible = minimal - const baseFogDensity = isUnexplored.mul(1.0).add(exploredAmount.mul(0.3)).mul(uVolFogDensity); - - // Light direction for scattering - const cosTheta = dot(rayDir, uVolLightDir); - // Henyey-Greenstein phase function (g=0.3 for slight forward scatter) - const g = float(0.3); - const g2 = g.mul(g); - const hgPhase = float(1.0) - .sub(g2) - .div( - float(1.0) - .add(g2) - .sub(g.mul(2.0).mul(cosTheta)) - .pow(1.5) - .mul(4.0 * Math.PI) - ); + // Base fog density from visibility (sampled ONCE at surface - no loop sampling) + // Unexplored = full fog, explored = partial, visible = none + const baseDensity = isUnexplored.add(exploredAmount.mul(0.4)).mul(uVolFogDensity); - // Volumetric fog accumulation + // Volumetric accumulation const volTransmittance = float(1.0).toVar(); - const volInScatter = vec3(0.0).toVar(); + const volScatter = vec3(0.0).toVar(); - // Raymarch loop (max 64 steps for ultra, fewer for high) - // NOTE: No texture sampling inside loop - TSL limitation - const loopIdx = int(0).toVar(); + // Raymarch loop - ONLY simple math operations (no fbm3, no texture sampling) + const idx = int(0).toVar(); Loop(64, () => { - // Early exit when we've done enough steps for current quality - If(loopIdx.greaterThanEqual(uVolSteps), () => { + If(idx.greaterThanEqual(uVolSteps), () => { Break(); }); - // Current position along ray (offset by 0.5 for better sampling) - const t = float(loopIdx).add(0.5).mul(stepSize); - const samplePos = cameraPos.add(rayDir.mul(t)); - - // Height-based density falloff (fog denser at low heights) - const heightDensityFactor = exp(samplePos.y.negate().mul(uVolFogHeightFalloff)); - - // Animated noise for cloud variation (computed from position) - const noiseX = samplePos.x.mul(uCloudScale); - const noiseZ = samplePos.z.mul(uCloudScale); - const noiseT = uTime.mul(uCloudSpeed); - const cloudNoise = fbm3(vec3(noiseX, noiseZ, noiseT)) - .mul(0.5) - .add(0.5); + // Position along ray + const t = float(idx).add(0.5).mul(stepSize); + const pos = cameraPos.add(rayDir.mul(t)); - // Local density with all factors - const localDensity = baseFogDensity - .mul(heightDensityFactor) - .mul(cloudNoise.mul(0.5).add(0.75)); + // Height-based density falloff (simple exp - no complex functions) + const heightFactor2 = exp(pos.y.negate().mul(uVolFogHeightFalloff)); + const localDensity = baseDensity.mul(heightFactor2); - // Accumulate light scattering - const scatterAmount = localDensity.mul(hgPhase).mul(uVolScattering).mul(stepSize); - volInScatter.addAssign(uVolFogColor.mul(scatterAmount).mul(volTransmittance)); + // Simple in-scattering (no phase function complexity) + volScatter.addAssign(uVolFogColor.mul(localDensity).mul(stepSize).mul(volTransmittance)); // Beer-Lambert absorption volTransmittance.mulAssign(exp(localDensity.mul(stepSize).negate())); - loopIdx.addAssign(1); + idx.addAssign(1); }); - // Combine volumetric fog result - const volFogContribution = uVolFogColor.mul(float(1.0).sub(volTransmittance)).add(volInScatter); + // Combine volumetric with scene + const volFogAmount = float(1.0).sub(volTransmittance); + const volResult = mix(finalColor, uVolFogColor.add(volScatter), volFogAmount); - // Blend fog with scene based on transmittance - const volFinalColor = mix(volFogContribution, sceneColor, volTransmittance); - - // Apply explored darkening to volumetric result - const volWithExplored = mix(volFinalColor, exploredColor, exploredAmount.mul(0.5)); - - // For 2D (low/medium): apply flat cloud overlay - finalColor.assign(mix(finalColor, cloudColor, isUnexplored.mul(heightFactor))); - - // For volumetric (high/ultra): use raymarched fog instead - finalColor.assign(useVolumetric.select(volWithExplored, finalColor)); - - // Add rim glow at visibility edges - const rimContribution = uRimColor.mul(rimGlow).mul(isVisible); - finalColor.addAssign(rimContribution); + // Apply volumetric only for high/ultra quality + finalColor.assign(useVolumetric.select(volResult, finalColor)); // DEBUG: Show world position as color to verify reconstruction // Red = X position, Blue = Z position (normalized to map dimensions) @@ -1053,8 +1015,6 @@ export function createFogOfWarPass( volFogHeightFalloff: uVolFogHeightFalloff, volMaxDistance: uVolMaxDistance, volSteps: uVolSteps, - volScattering: uVolScattering, - volLightDir: uVolLightDir, volFogColor: uVolFogColor, }; @@ -1099,8 +1059,7 @@ export function createFogOfWarPass( uQuality.value = qualityIndex >= 0 ? qualityIndex : 2; // Set volumetric raymarch steps based on quality - // low/medium: doesn't use volumetric (but set anyway for consistency) - // high: 32 steps, ultra: 48 steps + // high: 32 steps, ultra: 48 steps (low/medium don't use volumetric) const volStepsMap: Record = { low: 16, medium: 24,