# WebGPU Fog

Procedural height fog with animated noise, written in TSL. Scoped useUniforms read from Leva, and useLocalNodes installs the fogNode and backgroundNode on the scene. Based on three.js webgpu_custom_fog.

Demo: https://pmndrs.github.io/examples/examples/webgpu-fog
Source: https://github.com/pmndrs/examples/tree/main/examples/webgpu-fog
Scaffold: npx degit pmndrs/examples/examples/webgpu-fog
Authors: Dennis Smolek, Victor Zakharov
Tags: webgpu, tsl, fog, noise
Ported from: https://github.com/pmndrs/react-three-fiber/tree/v10/example/src/demos/webgpu/WebGPUFog.tsx
Dependencies: @react-three/drei@11.0.0-alpha.7, @react-three/fiber@10.0.0-alpha.6, @react-three/tsl@10.0.0-alpha.6, leva@^0.10.1, react@19.2.9, react-dom@19.2.9, three@0.186.1

## src/App.tsx

```tsx
/**
 * WebGPU Fog
 *
 * A ground plane and a field of blocks receding into a procedural TSL fog. The fog itself is
 * `<Fog />`: scoped `useUniforms` driven by Leva, composed into `fogNode` and `backgroundNode`
 * through `useLocalNodes` with a typed `uniforms.scope<FogUniformSchema>('fog')` read.
 *
 * Based on three.js example: webgpu_custom_fog.html
 */

import { useMemo } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { Fog } from "./Fog";

//* Scene ==============================

/** A deterministic scatter so Fast Refresh and reloads keep the same layout. */
function scatter(count: number) {
  const blocks: {
    position: [number, number, number];
    scale: [number, number, number];
  }[] = [];
  let seed = 7;
  const next = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
  for (let i = 0; i < count; i++) {
    const height = 1 + next() * 5;
    blocks.push({
      position: [(next() - 0.5) * 90, height / 2, (next() - 0.5) * 90],
      scale: [1 + next() * 2, height, 1 + next() * 2],
    });
  }
  return blocks;
}

function Blocks() {
  const blocks = useMemo(() => scatter(120), []);
  return (
    <>
      {blocks.map((block, index) => (
        <mesh key={index} position={block.position} scale={block.scale}>
          <boxGeometry />
          <meshStandardNodeMaterial color="#c8b9a6" roughness={0.9} />
        </mesh>
      ))}
    </>
  );
}

function Scene() {
  return (
    <>
      <Fog />
      <hemisphereLight args={["#f0f5f5", "#4a5d5d", 2]} />
      <directionalLight position={[20, 30, 10]} intensity={2} />
      <mesh rotation-x={-Math.PI / 2}>
        <planeGeometry args={[300, 300]} />
        <meshStandardNodeMaterial color="#5e6d6b" roughness={1} />
      </mesh>
      <Blocks />
    </>
  );
}

//* App ==============================

export default function App() {
  return (
    <Canvas camera={{ position: [0, 8, 28], fov: 60, far: 300 }}>
      <Scene />
      <OrbitControls maxPolarAngle={Math.PI / 2 - 0.05} />
    </Canvas>
  );
}
```

## src/Fog.tsx

```tsx
import { useControls } from "leva";
import {
  uniform,
  fog,
  color,
  float,
  positionView,
  triNoise3D,
  positionWorld,
  normalWorld,
} from "three/tsl";
import { useLocalNodes, useUniforms } from "@react-three/tsl";

export const Fog = () => {
  //* Leva Controls ==============================
  const levaUniforms = useControls("Fog", {
    // Colors - Sky gradient and fog color
    skyColor: { value: "#f0f5f5", label: "Sky Color" },
    groundColor: { value: "#d0dee7", label: "Fog Color" },

    // Distance Fade - How fog appears based on camera distance
    fogStart: {
      value: 0,
      min: 0,
      max: 100,
      step: 1,
      label: "Fog Start Distance",
    },
    fogEnd: {
      value: 50,
      min: 10,
      max: 200,
      step: 1,
      label: "Fog End Distance",
    },

    // Height Fog - Controls vertical fog distribution
    fogHeight: {
      value: 0,
      min: -20,
      max: 20,
      step: 0.5,
      label: "Fog Base Height",
    },
    fogDensity: {
      value: 0.85,
      min: 0,
      max: 2,
      step: 0.01,
      label: "Fog Density",
    },
    fogFalloff: {
      value: 2,
      min: 0.5,
      max: 5,
      step: 0.1,
      label: "Height Falloff",
    },
    fogThickness: {
      value: 20,
      min: 1,
      max: 100,
      step: 1,
      label: "Fog Layer Thickness",
    },

    // Animated Noise - Adds moving/shifting fog effect
    noiseSpeed: {
      value: 1.2,
      min: 0,
      max: 5,
      step: 0.1,
      label: "Noise Animation Speed",
    },
    noiseScale: {
      value: 0.01,
      min: 0.001,
      max: 0.1,
      step: 0.001,
      label: "Noise Detail",
    },
    noiseStrength: {
      value: 0.2,
      min: 0,
      max: 1,
      step: 0.01,
      label: "Noise Strength",
    },
  });

  //* Apply Uniforms ==============================
  useUniforms(levaUniforms, "fog");
  type FogUniformSchema = UniformNodesFor<typeof levaUniforms>;

  //* Build Fog Nodes, Install on the Scene ==============================
  // Built during render; the returned function installs them after commit and removes them on unmount.
  useLocalNodes(({ scene, uniforms }) => {
    const u = uniforms.scope<FogUniformSchema>("fog");

    // Timer for animated noise
    const timer = uniform(0).onFrameUpdate((frame) => frame.time);

    // Colors
    const skyColor = color(u.skyColor);
    const groundColor = color(u.groundColor);

    // Distance-based fog fade (closer = less fog, farther = more fog)
    const distanceFade = positionView.z
      .negate()
      .smoothstep(u.fogStart, u.fogEnd);

    // Height-based fog calculation
    // Fog is strongest at fogHeight, falls off above and below
    const heightAboveFog = positionWorld.y.sub(u.fogHeight);
    const heightFogAmount = float(u.fogThickness)
      .sub(heightAboveFog)
      .div(u.fogThickness)
      .pow(u.fogFalloff)
      .saturate()
      .mul(u.fogDensity);

    // Animated noise for fog variation (makes it look more natural/volumetric)
    const noiseScaleA = u.noiseScale;
    const noiseScaleB = u.noiseScale.mul(2); // Second layer is 2x frequency
    const fogNoiseA = triNoise3D(
      positionWorld.mul(noiseScaleA),
      u.noiseStrength,
      timer,
    );
    const fogNoiseB = triNoise3D(
      positionWorld.mul(noiseScaleB),
      u.noiseStrength,
      timer.mul(u.noiseSpeed),
    );

    const fogNoise = fogNoiseA.add(fogNoiseB).mul(groundColor);

    // Combine distance fade + height fog for final fog amount
    const finalFogAmount = distanceFade.mul(heightFogAmount);

    // Final fog node (color + opacity)
    const fogNode = fog(
      distanceFade.oneMinus().mix(groundColor, fogNoise),
      finalFogAmount,
    );

    // Background gradient (horizon to sky)
    const backgroundNode = normalWorld.y.max(0).mix(groundColor, skyColor);

    return () => {
      scene.fogNode = fogNode;
      scene.backgroundNode = backgroundNode;
      return () => {
        scene.fogNode = null;
        scene.backgroundNode = null;
      };
    };
  }, []);

  return null;
};
```

## src/index.tsx

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);
```

## src/styles.css

```css
* {
  box-sizing: border-box;
}

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  background: #dedddf;
  overscroll-behavior-y: none;
}
```
