I’ve been learning from this forum for about a year and a half; it’s been an incredible resource. The depth of knowledge shared here is inspiring, and I wanted to contribute something back that might help others.
I’d like to share something I haven’t seen implemented quite this way before: a custom fog system for Three.js that works only on the XZ plane.
What is XZFog?
XZFog is a custom fog implementation that calculates fog distance based on the XZ plane only (horizontal distance), completely ignoring the Y axis (height). Unlike Three.js’s standard fog which calculates full 3D Euclidean distance from the camera, XZFog measures distance on the ground plane from a configurable center point (typically the player position).
The Problem It Solves
In standard Three.js fog, moving your camera vertically dramatically changes fog density because it calculates full 3D distance. If you raise your camera high above the scene, objects that are close horizontally but far away vertically become foggier even though they’re right below you. This breaks immersion in:
-
Top-down games (RTS, city builders, tactical games)
-
Isometric games (RPGs, strategy games)
-
Third-person games where the camera height varies but you want consistent visibility
-
Open world games where you want fog relative to the player’s ground position, not camera position
With XZFog, objects at the same horizontal distance maintain consistent fog density regardless of camera height.
Use Cases
1. Top-Down Strategy Games
Keep fog density consistent as players zoom in/out or adjust camera angle. Fog represents the player character’s visibility range, not the camera’s.
2. Isometric RPGs
Maintain atmospheric fog that follows the player character on the ground, not the elevated isometric camera.
3. Terrain-Based Games
Objects should fade based on how far away they are across the terrain, not through 3D space. A tower on a distant hill shouldn’t appear clearer just because its top is at the same elevation as your camera.
4. Flight-Restricted Games
Any game where the camera can move up/down but you want fog to represent horizontal distance (like a drone camera over a battlefield).
Features
-
Two modes: Color fog (blends to fog color) or Alpha fog (fades to transparent)
-
Player-centered: Fog calculated from player position, not camera
-
Automatic patching: Works with all existing meshes and future meshes added to scene
-
Smart material handling: Automatically clones shared materials to avoid side effects
-
Instanced mesh support: Works with InstancedMesh out of the box
-
Preserves existing shaders: Chains with existing
onBeforeCompilecallbacks -
Drop-in replacement: Extends Three.js Fog class, uses standard fog uniforms
Installation & Usage
javascript
import { XZFog } from './XZFog.js';
// Create XZ fog (color, near, far, mode)
const fog = new XZFog(0x87ceeb, 50, 150, 'color'); // or 'alpha'
scene.fog = fog;
fog.attachScene(scene);
// In your game loop:
fog.setPlayerPosition(player.x, player.z);
fog.updateUniforms();
That’s it! All existing and future meshes automatically use XZ fog.
API
javascript
// Change fog parameters
fog.setNear(30);
fog.setFar(200);
fog.setColor(0x334455);
// Switch between color and alpha modes
fog.setMode('alpha'); // Fade to transparent
fog.setMode('color'); // Blend with fog color
// Update player position (call every frame)
fog.setPlayerPosition(playerX, playerZ);
fog.updateUniforms();
How It Works
XZFog patches materials using onBeforeCompile to inject custom shader code that:
-
Passes player XZ position as a uniform
-
Calculates world position of each vertex
-
Measures distance on the XZ plane only:
length(worldPos.xz - playerPosXZ) -
Applies fog based on this horizontal distance
The shader modification happens automatically for all materials in the scene, and intelligently handles shared materials by cloning them.
Code
Full implementation available here: XZFog: Horizontal Distance Fog for Three.js (Top-Down/Isometric Games)
javascript
// XZFog.js - Complete implementation
import { Fog } from "three";
export class XZFog extends Fog {
constructor(color, near = 1, far = 1000, mode = "color") {
super(color, near, far);
this.isXZFog = true;
this.mode = mode;
this.scene = null;
this.patchedMaterials = new WeakSet();
this.materialCloneMap = new WeakMap();
this.materialUsageMap = new Map();
this.playerX = 0;
this.playerZ = 0;
this._shaderUniforms = [];
}
setPlayerPosition(x, z) {
this.playerX = x;
this.playerZ = z;
}
updateUniforms() {
this._shaderUniforms.forEach((uniform) => {
uniform.value.x = this.playerX;
uniform.value.y = this.playerZ;
});
}
attachScene(scene) {
// ... (see full implementation)
}
// ... (see full implementation for complete code)
}
Performance
XZFog has minimal performance impact:
-
Shader modifications happen once per material during compilation
-
Uniform updates are a simple array iteration per frame
-
No additional draw calls or geometry processing
Comparison with Standard Fog
Try the demo and adjust the camera height slider:
-
XZ Fog: Objects maintain consistent visibility regardless of camera height. A cube 100 units away horizontally always has the same fog, whether you’re at ground level or 200 units up.
-
Standard Fog: Raising the camera increases 3D distance to ground objects, making them foggier. Lowering the camera decreases distance, making them clearer.
Limitations
-
Doesn’t affect
LineBasicMaterialorLineDashedMaterial, since these materials don’t support fog in Three.js by default. -
Requires materials to have
fog: true(default behavior) -
Shader patching happens at material compile time
Future Enhancements
Potential additions I’m considering:
-
Exponential fog falloff option
-
Per-object fog override
-
Multiple fog zones
-
Height-based fog density modifier
Feedback Welcome
I’d love to hear if this is useful for your projects, or if you have suggestions for improvements. Has anyone else tackled this problem differently?
Demo: XZFog: Horizontal Distance Fog for Three.js (Top-Down/Isometric Games)
License: MIT
Hope this helps someone out there!