Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 38 additions & 79 deletions src/rendering/tsl/effects/EffectPasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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)));
Comment on lines +913 to +914

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip 2D cloud overlay when volumetric fog is enabled

For high/ultra quality you now always apply the flat unexplored cloud overlay before the volumetric mix, but the volumetric path is intended to replace the 2D overlay (per the existing comments and prior behavior). With useVolumetric true, finalColor already has the 2D cloud darkening baked in, and then you mix in volumetric fog on top, so unexplored areas get double-darkened/washed compared to low/medium. This is a regression in the high/ultra visual output; consider gating the overlay with useVolumetric or moving it into the low/medium branch.

Useful? React with 👍 / 👎.


// 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)
Expand Down Expand Up @@ -1053,8 +1015,6 @@ export function createFogOfWarPass(
volFogHeightFalloff: uVolFogHeightFalloff,
volMaxDistance: uVolMaxDistance,
volSteps: uVolSteps,
volScattering: uVolScattering,
volLightDir: uVolLightDir,
volFogColor: uVolFogColor,
};

Expand Down Expand Up @@ -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<string, number> = {
low: 16,
medium: 24,
Expand Down
Loading