How To Make a Fast Screen-Space Snow

Let It Snow! How to Make a Fast screen-space snow accumulation shader

What I tried to do, it covered the entire screen instead of covering the surface of the object.

three.js webgl - STL
<body>
	<div id="info">
		<a href="https://threejs.org" target="\_blank" rel="noopener">three.js</a>
		- STL loader test by
		<a href="https://github.com/aleeper" target="\_blank" rel="noopener"
			>aleeper</a
		>.<br />
		PR2 head from
		<a href="http://www.ros.org/wiki/pr2_description">www.ros.org</a>
	</div>

	<script type="importmap">
		{
			"imports": {
				"three": "../build/three.module.js",
				"three/addons/": "./jsm/"
			}
		}
	</script>

	<script type="module">
		import \* as THREE from "three";

		import Stats from "three/addons/libs/stats.module.js";
		import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
		import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
		import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
		import { STLLoader } from "three/addons/loaders/STLLoader.js";
		import { OrbitControls } from "three/addons/controls/OrbitControls.js";
		let container, stats;

		let camera, cameraTarget, scene, renderer;

		init();

		function init() {
			// 1. Basic scene setup
			const scene = new THREE.Scene();
			const camera = new THREE.PerspectiveCamera(
				75,
				window.innerWidth / window.innerHeight,
				0.1,
				1000
			);
			const renderer = new THREE.WebGLRenderer({ antialias: true });
			renderer.setSize(window.innerWidth, window.innerHeight);
			renderer.shadowMap.enabled = true;
			renderer.outputDepth = true; // Ensure depth texture output
			document.body.appendChild(renderer.domElement);

			// 2. Add scene objects
			const cubeGeo = new THREE.BoxGeometry(2, 2, 2);
			const cubeMat = new THREE.MeshStandardMaterial({ color: 0x888888 });
			const cube = new THREE.Mesh(cubeGeo, cubeMat);
			cube.position.y = 1;
			cube.castShadow = true;
			cube.receiveShadow = true;
			scene.add(cube);

			const planeGeo = new THREE.PlaneGeometry(10, 10);
			const planeMat = new THREE.MeshStandardMaterial({ color: 0x444444 });
			const plane = new THREE.Mesh(planeGeo, planeMat);
			plane.rotation.x = -Math.PI / 2;
			plane.receiveShadow = true;
			scene.add(plane);

			// 3. Lighting setup
			const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
			directionalLight.position.set(5, 5, 5);
			directionalLight.castShadow = true;
			scene.add(directionalLight);
			scene.add(new THREE.AmbientLight(0x404040));

			// 4. Camera position
			camera.position.set(3, 3, 5);
			camera.lookAt(0, 0, 0);

			// 5. Core: Snow accumulation post-processing Shader
			const SnowShader = {
				uniforms: {
					tDiffuse: { value: null },
					tDepth: { value: null }, // Depth texture
					tNoise: { value: null },
					snowColor: { value: new THREE.Color(0xffffff) },
					snowThickness: { value: 0.3 }, // Increase thickness for more visible snow
					edgeSensitivity: { value: 5.0 }, // Decrease sensitivity
					snowCoverage: { value: 0.7 }, // Adjust coverage range
				},
				vertexShader: \`
    varying vec2 vUv;
    void main() {
        vUv = uv;
        gl_Position = projectionMatrix \* modelViewMatrix \* vec4(position, 1.0);
    }
\`,
				fragmentShader: \`
    uniform sampler2D tDiffuse;
    uniform sampler2D tDepth;
    uniform sampler2D tNoise;
    uniform vec3 snowColor;
    uniform float snowThickness;
    uniform float edgeSensitivity;
    uniform float snowCoverage;

    varying vec2 vUv;

    // Calculate linear depth from depth texture
    float getLinearDepth(vec2 uv) {
        float depth = texture2D(tDepth, uv).r;
        float near = 0.1;       // Camera default near = 0.1
        float far = 2000.0;     // Camera default far = 2000 (not 1000!)
        return (near \* far) / (far - depth \* (far - near)); // ✅ Corrected formula
    }

    void main() {
        vec4 originalColor = texture2D(tDiffuse, vUv);
        float depth = getLinearDepth(vUv);
        
        // Calculate depth gradient (edge detection)
        vec2 texelSize = 1.0 / vec2(textureSize(tDepth, 0));
        float depthRight = getLinearDepth(vUv + vec2(texelSize.x, 0.0));
        float depthLeft = getLinearDepth(vUv - vec2(texelSize.x, 0.0));
        float depthUp = getLinearDepth(vUv + vec2(0.0, texelSize.y));
        float depthDown = getLinearDepth(vUv - vec2(0.0, texelSize.y));
        
        float depthGradient = max(
            abs(depthRight - depthLeft),
            abs(depthUp - depthDown)
        );
        float isEdge = smoothstep(0.0, 1.0 / edgeSensitivity, depthGradient);

        // Sample noise texture for natural snow distribution
        float noise = texture2D(tNoise, vUv \* 5.0).r;
        
        // Calculate snow mask (only accumulate on object edges/top)
        float snowMask = smoothstep(1.0 - snowCoverage, 1.0, noise);
        snowMask = max(snowMask, isEdge \* 0.7); // Enhance snow on edges
        
        // Blend original color with snow color
        vec3 finalColor = mix(originalColor.rgb, snowColor, snowMask \* snowThickness);
        gl_FragColor = vec4(finalColor, originalColor.a);
    }
\`,
			};

			// 6. Generate noise texture for natural snow distribution
			function createNoiseTexture() {
				const canvas = document.createElement("canvas");
				canvas.width = 256;
				canvas.height = 256;
				const ctx = canvas.getContext("2d");
				const imageData = ctx.createImageData(256, 256);

				for (let i = 0; i < imageData.data.length; i += 4) {
					const gray = Math.floor(Math.random() \* 255);
					imageData.data\[i\] = gray; // R
					imageData.data\[i + 1\] = gray; // G
					imageData.data\[i + 2\] = gray; // B
					imageData.data\[i + 3\] = 255; // A
				}

				ctx.putImageData(imageData, 0, 0);
				const texture = new THREE.CanvasTexture(canvas);
				texture.wrapS = THREE.RepeatWrapping;
				texture.wrapT = THREE.RepeatWrapping;
				return texture;
			}

			// 7. Initialize post-processing pipeline
			const composer = new EffectComposer(renderer);
			const renderPass = new RenderPass(scene, camera);
			composer.addPass(renderPass);

			// Create depth texture render target
			const depthRenderTarget = new THREE.WebGLRenderTarget(
				window.innerWidth,
				window.innerHeight,
				{
					minFilter: THREE.NearestFilter,
					magFilter: THREE.NearestFilter,
					depthBuffer: true,
					depthTexture: new THREE.DepthTexture(), // Key: Create depth texture
				}
			);

			// Snow accumulation pass
			const snowPass = new ShaderPass(SnowShader);
			snowPass.uniforms.tNoise.value = createNoiseTexture();
			snowPass.uniforms.tDepth.value = depthRenderTarget.depthTexture; // Bind depth texture
			composer.addPass(snowPass);

			// 8. Render pipeline control
			renderPass.renderToScreen = false;
			snowPass.renderToScreen = true;

			// 9. Window resize handler
			window.addEventListener("resize", () => {
				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();
				renderer.setSize(window.innerWidth, window.innerHeight);
				composer.setSize(window.innerWidth, window.innerHeight);

				// Update depth texture size
				depthRenderTarget.setSize(window.innerWidth, window.innerHeight);
			});

			// 10. Animation loop (Key fix: Render depth texture first)
			function animate() {
				requestAnimationFrame(animate);

				// Rotate the cube
				cube.rotation.x += 0.01;
				cube.rotation.y += 0.01;

				// Step 1: Render scene to depth texture
				renderer.setRenderTarget(depthRenderTarget);
				renderer.render(scene, camera);
				renderer.setRenderTarget(null);

				// Step 2: Render final image with post-processing
				composer.render();
			}
			animate();
		}
	</script>
</body>