惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
J
Java Code Geeks
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
量子位
T
Tailwind CSS Blog
Vercel News
Vercel News
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
U
Unit 42
Engineering at Meta
Engineering at Meta
L
LangChain Blog
大猫的无限游戏
大猫的无限游戏
D
Docker
博客园_首页
P
Proofpoint News Feed
月光博客
月光博客
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Martin Fowler
Martin Fowler
腾讯CDC
N
Netflix TechBlog - Medium
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Codrops

Drawing With Light: An Exploration of Lit GPU Tubes with TSL and WebGPU | Codrops Building a Real-Time 3D Face Mask with MediaPipe, Threlte and Three.js | Codrops Building an Infinite Loom: Unravelling Images into Threads with Three.js | Codrops Beyond the Luminance Ramp: A Shape-Aware ASCII Renderer in Three.js | Codrops From Rays to Meshes: Building Vercel’s Prism with vgpu | Codrops More Three.js Speakers, More Ideas from Paris | Codrops Breaking the Frame: Building a Real-Time Datamosh Effect with Three.js | Codrops Bend, Aim, Fling: Turning the Eiffel Tower into a Catapult with Three.js | Codrops Volatile Nexus: Tinkering with Glass, Caustics, Cubes and Sound in Three.js | Codrops Goodgrowth: Boot Sequences, Spinning Discs, and the Art of the Portfolio | Codrops Building a Mouse-Following Square Lens Effect with Three.js and GLSL | Codrops Blender to Three.js and Back: 10 Tips for a Better Workflow | Codrops Sixty Frames for the Record: A Three.js Game, Seven Fly-Throughs, and a Wall of CRTs | Codrops Run Rob Run: Building a Music-Reactive Goo with Three.js and WebGPU | Codrops Building an Animated Testimonial Hero Using the GSAP Timeline and Dynamic CMS Data | Codrops Creativity at Enterprise-Scale, Without Compromise: The OFF+BRAND. Story | Codrops Inside HAOQI.DESIGN: Letting DOM and WebGL Share a Retro-Futurist Stage | Codrops From Motion to Meaning: Denis Avramenko’s Approach to Interactive Design | Codrops Creating an Interactive 3D Cluster with Three.js, TSL and Three Start | Codrops Exploring Procedural Geometry with Three.js and WebGPU | Codrops From Brand Systems to Cultural Worlds: Inside Antinomy and 27b | Codrops Designing a Flexible Digital Archive for Chems.Studio’s Creative Practice | Codrops The Department Is Open: Building the PX PUSH Website | Codrops Garden Anomaly: A Tiny WebGPU and TSL Experiment | Codrops A Canvas for Individuality: Creating Websites That Feel Unmistakably Human with Readymag | Codrops Building an Endless Interactive Glass Xylophone with Three.js | Codrops The Story Is in the Interaction: Bonhomme’s Digital Experiences for Luxury Brands | Codrops Building an Infinite GSAP Scroll Gallery with Parallax and Flip Transitions | Codrops Studio Freight: Moving Missions Forward | Codrops Between Print and Digital: The Making of MERSI’s Website | Codrops
Relighting Images with Depth Maps and Three.js | Codrops
By Dominik Fojcik · 2026-08-19 · via Codrops

Explore how depth maps, TSL, and WebGPU can be combined to turn flat 2D images into dynamic surfaces with realistic lighting, surface detail, and self-shadowing.

3D Three.js TSL webgpu

Editor’s note: And we are continuing our celebration of the Three.js community as we count down to the first Three.js Conference in Paris this September. In this new tutorial, Dominik Fojcik shares a little magic trick for making 2D images come alive with depth, light, and shadow, using Three.js, TSL, and WebGPU.

🇫🇷 Is Paris on your mind? As part of our partnership with the first Three.js Conference, Codrops readers can use the code CODROPS to get 15% off. Get your ticket

I’ve seen many cool image effects on the web, but most of them stay on the surface. What if you could take it further and make the effect dive into the image? Something that actually gets inside the picture like a light.

The key for that is a depth map. Something that before seemed to be magic is now doable thanks to depth estimation models, which have become really good at estimating depth from 2D images.

Depth Map

This is the main ingredient of our effect. To get one, we need to feed a depth estimation model with our image and use it to generate a depth map. You can install a model like Depth Anything 3 yourself or use a Depth Generation Tool I created for this article.

The raw depth map has a problem you can’t see, but the light can. It’s an 8-bit image, so it only has 256 possible depth values, and smooth surfaces get quantized into flat steps. The lighting reads the slope from this map, and at every step edge the slope suddenly spikes, so shading that should flow across smooth stone breaks up into gritty, blotchy noise. To fix it, we convert the depth map to floats and blur it to smooth out those steps.

First, we convert the 8-bit depth values into floating-point values, blur them to remove the visible steps, and then store the result as half-float data so we can preserve smoother depth information.

const { data } = context.getImageData(0, 0, width, height)
const values = new Float32Array(width * height)
for (let i = 0; i < values.length; i++) {
  values[i] = data[i * 4] / 255
}

// blur just enough to melt the 8-bit steps together
smoothBands({ values, width, height }, radius)

const halfFloats = new Uint16Array(values.length)
for (let i = 0; i < halfFloats.length; i++) {
  halfFloats[i] = DataUtils.toHalfFloat(values[i])
}

Faking the Surface with a Normal Map

How do we make light act like our image is 3D? The answer is a normal map.

Lighting doesn’t actually care about the shape itself, it cares about normals: the direction each point on a surface faces. A point facing the light is bright, a point tilted away is dark. That’s the entire trick of this effect.

So instead of building real geometry, we can give each pixel a normal that makes the plane appear three-dimensional to the light.

To create the normal map, we’re going to use our depth map. The depthGradient function samples the depth map texture and calculates the surface slope that we can use to create our normal map.

const depthGradient = Fn(([vUv, step]) => {
  const left = smoothDepthNode.sample(vUv.sub(alongX)).r
  const right = smoothDepthNode.sample(vUv.add(alongX)).r
  const bottom = smoothDepthNode.sample(vUv.sub(alongY)).r
  const top = smoothDepthNode.sample(vUv.add(alongY)).r

  return vec2(right.sub(left), top.sub(bottom)).mul(0.5)
})

When visualized as colors, the normal map looks like this:

To add even more detail, I run the same trick on the photo itself, using its brightness instead of depth. It’s a cheat—a dark painted stripe tilts the normal in the same way a real groove would—but under a moving light it reads as surface detail. The two gradients are simply added together:

const shape = vec3(slope.x.negate(), slope.y.negate(), float(1))
return shape.add(vec3(detail.x.negate(), detail.y.negate(), 0)).normalize()

Here is the normal before and after adding the details.

Shadows

Normals make the image react to light like a 3D surface, but they can’t make one part cast a shadow on another. To find shadows, each pixel traces a line toward the light through the depth map. If it detects a bump along the way, the pixel is in shadow.

The depth map is sampled at several points along this path, and the amount of occlusion is accumulated to create a soft shadow.

const occlusion = float(0).toVar()

Loop(SHADOW_STEPS, ({ i }) => {
  const travel = float(i).add(1).div(SHADOW_STEPS)
  const rayDepth = surfaceDepth.add(headroom.mul(travel))
  const blockerDepth = smoothDepthNode.sample(vUv.add(sweep.mul(travel))).r
  const softness = uShadowSoftness.mul(travel.mul(SOFTNESS_GROWTH).add(1))
  const blocked = blockerDepth.sub(rayDepth).div(softness).clamp(0, 1)

  occlusion.assign(occlusion.max(blocked))
})

Material

All three ingredients are passed to MeshPhongNodeMaterial, which handles the actual lighting calculation for us:

const material = new MeshPhongNodeMaterial({ specular: 0x000000 })
material.colorNode = diffuseNode(vUv, depth)   // our image
material.normalNode = normalNode(vUv)          // fake normals from depth-map
material.aoNode = shadowNode(vUv, depth)       // shadows

Here, the image becomes the material’s color, our generated normals control how the surface reacts to light, and the depth-based shadows are added as ambient occlusion.

Final Words

I hope this little magic trick gives you some inspiration to create your own interesting effects with depth maps. There is still a lot of unexplored potential in this field.

Wishing you all the best at the upcoming Three.js conference! I couldn’t make it this year, but hopefully next time!

Dominik Fojcik

Crafting unique websites for founders and artists. Creative Technologist, UI & Motion Designer.

Creative Spotlights

Inside the journeys and portfolios of today's most inspiring designers and developers.

Studio Stories

Discover how studios & agencies started, how they work, and what they've built.

Case Studies

Discover the ideas, design, and craft behind today’s most inspiring web experiences.