3D Shadow Mapping Renderer in JavaScript
Late last year I decided it would be fun to build a 3D renderer in JavaScript. Recently it got into some sort of finished state and decided to put it here. This isn't so much of a tutorial on how to get there, but rather more of a here's a fun thing to do with nice pictures. But it was interesting to do. So here's that.
Late last year I decided it would be fun to build a 3D renderer in JavaScript. Recently it got into some sort of “finished” state and decided to put it here. This isn't so much of a tutorial on how to get there, but rather more of a “here's a fun thing to do with nice pictures”. But it was interesting to do. So here's that.
START
To start out I really wanted to do this from first principles and didn't want to “cheat” by using any kind of libraries. Initially, that meant writing the rendering engine entirely with the 2d graphics library, doing all of the 3d math by hand.
Technically there's nothing terribly interesting going on with this initial renderer. It's just over 250 lines long and does the naive thing: for each triangle in the three dimensional space, the renderer projects it onto the two dimensional screen, drawing from the back to the front.
Smooth surface:
START
Once I actually got that working doing all of the math by hand in JavaScript I decided that using WebGL would probably be worth it, and actually probably wasn't cheating all that much. WebGL exposes access to the GPU-enabled rendering engine through JavaScript. While it does abstract away some of the rendering, it's less than I thought---it just supports the ability to do the necessary math efficiently---so I decided this wouldn't be cheating. And fortunately, it didn't take long to reproduce the initial renderer, but this time supporting much better (and more efficient) graphics.
Shadow map resolution:
256
START
Once the base renderer was ready, I started adding lighting with shadows. To do this I used shadow mapping: one of the most common approaches for generating real-time shadows. At a high level, shadow mapping works by rendering the scene twice every frame. First, from the perspective of the light source, we render the scene and record how far the closest object is to the light, for each pixel in the scene. This gives us the “shadow map” Then, we render the scene again, this time from the actual camera. In order to determine if any given pixel is in light or is in the shadow, we project it back onto the (pre-rendered) shadow map. If this pixel is the closest one to the light, then it's in light; if not, it's in the shadow.
Unfortunately, the light is fairly blocky.
Getting a high-quality shadow on my old laptop without a dedicated GPU wasn't possible: I had to increase the dimensions of the shadow camera to 2048 by 2048, and I was no longer getting a smooth 60 frames a second. Also, those hard edges on the shadows really don't look that realistic.
Shadow map resolution:
256
START
In order to get nicer-looking shadows efficiently, I decided to move to a slightly fancier method: variance shadow maps. At their core they do the same thing as standard shadow maps: first, render from the light, recording the distance to the light for each pixel; then, render from the camera. However, variance shadow maps introduce one key difference: on the first rendering pass, instead of just recording the distance from the object to the camera, also record the distance squared.
But first, let's back up---one of the main reason that standard shadow maps look bad is because of their hard edges. It would be possible to blur the edges slightly by taking a large number of random samples when rendering from the camera, but this doesn't look great and is very inefficient.
Variance shadow maps allow using Chebyshev's inequality to get a nice smooth blur by estimating the fraction of pixels that are occluded by comparing the squared expected distance to the expected distance squared. The math is somewhat complicated here, but when you do it right, you get a pretty picture.
At the same time, I also added some light bloom, the effect that extra-bright lights cause a fuzzy blur around the boundary. Given that I had already written a blur method for computing the shadow maps, adding a filter to the screen was straightforward.
And that's all there is.
Nicholas Carlini Blog
https://nicholas.carlini.com/writing/2019/3d-renderer-javascript.htmlSign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products

Nancy Guthrie Case Sparks Fury as 'Rookie' Detective With Just 2 Years' Experience Led Probe During Crucial Early Hours
The Nancy Guthrie disappearance case has sparked outrage due to the assignment of a rookie detective during its critical early phase, raising questions about law enforcement practices.

The 3-Prompt Rule: Why Limiting AI Turns Produces Better Code
<p>Here's a counterintuitive trick: the fewer prompts you send, the better your AI-generated code gets.</p> <p>I call it the 3-prompt rule. For any coding task, limit yourself to three interactions. If you can't get a good result in three turns, the problem isn't the AI — it's your approach.</p> <h2> Why 3? </h2> <p>Most AI coding sessions go wrong after turn 3:</p> <ul> <li> <strong>Turn 1:</strong> Clear instruction → good output</li> <li> <strong>Turn 2:</strong> Focused refinement → better output</li> <li> <strong>Turn 3:</strong> Edge case or final adjustment → done</li> <li> <strong>Turn 4+:</strong> "Actually, change this..." → context degradation, contradictions, regression</li> </ul> <p>By turn 5-6, you're often debugging problems the AI introduced while "fixing" earlier problems.

Async/Await in JavaScript: Writing Cleaner Asynchronous Code
<p>“If you write asynchronous code using promises, this blog is for you.<br> In this blog, we explore Async/Await in JavaScript.”</p> <p>What is Async/Await:</p> <p>Async/Await keywords are used to write asynchronous code that looks like synchronous code. It makes your code more readable and clean.<br> They are just syntactic sugar over JavaScript promises.</p> <p>Async keyword:</p> <p>We can use the async keyword with any function. This keyword ensures that the function always returns a promise.<br> If a function returns a non-promise value, JavaScript automatically wraps it in a resolved promise.</p> <p>Await keyword:</p> <p>We use the await keyword with code that takes time to resolve.<br> The await keyword pauses the execution of the function until the promise is either resolved or rej
React 20 Is Coming. Here's What Actually Matters (and What Doesn't).
<h1> React 20 Is Coming: Here's What Actually Matters (and What Doesn't) </h1> <p>Let's be honest. Every time a major framework version is on the horizon, a little knot forms in our stomachs. "Oh no, another paradigm shift? Am I going to have to re-learn everything?" We've all been there, staring at an announcement, wondering if our existing codebase is about to become a legacy nightmare overnight. It's a valid feeling in our fast-paced industry.</p> <p>But here’s the unvarnished truth about "React 20": For most professional developers and engineering teams, the impending updates are far less about a complete rewrite of your mental model, and far more about a profound, subtle evolution that will deliver tangible benefits in performance, developer experience, and maintainability. It’s not a
Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!