Let It Snow! How to Make a Fast screen-space snow accumulation shader
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>

