There is a category of shadow that nobody should be paying frame budget for. The chair in a product viewer does not move. The lounger in a configurator swaps models, but each model sits exactly where the last one sat. Rendering a real-time shadow for these means running a shadow map pass every frame, on every device, forever, to produce an image that never changes. The alternative everyone knows is baking: render the shadow once, save it as a texture, put it on a plane. Cheaper at runtime, softer looking, and completely stable.
The problem is the workflow. Baking a shadow traditionally means opening Blender, setting up a plane and a sun, configuring a render, compositing the result, and remembering which of the seventeen export settings matter. For a full scene that is fine. For "I need a soft shadow under this chair by lunch" it is a disproportionate amount of ceremony, and I kept paying it on every product project. So I built a tool that does exactly this one job in the browser, and this post is a tour of it, partly because the tool is useful and partly because the design decisions inside it say a lot about handling assets for real client work.
What it does
ShadowExport is a browser app. You drop in a model, glTF or common CAD formats, it lands on a ground plane in a small studio scene, and you aim a light at it. The shadow you see is the shadow you get: an orthographic camera captures the ground from directly above, and the export is that capture as a texture, cropped, padded, and packaged. No render farm, no timeline, no other software. Aim, adjust, download.
Running in the browser is not just convenience, it is correctness. The shadow is rendered by the same engine that will display it, so the softness, falloff, and edge behavior you approve in the tool are exactly what the production scene shows. A Cycles shadow baked in Blender is beautiful and subtly different, different penumbra math, different gamma path, and that mismatch is why baked shadows from a DCC sometimes look pasted-on next to real-time content. Same renderer in, same renderer out closes that gap by construction. It also means anyone on a team can bake a shadow with a link, no installs, no license, no tutorial.
The light rig is deliberately small but honest. Directional, spot, or point light, each with position and target you can drag directly in the viewport with transform gizmos, plus a softness control that shapes the penumbra through the emitter size: a point emitter gives crisp edges, a disk or rectangle emitter gives the soft falloff of a big studio source. That one control covers the useful range between "harsh noon sun" and "diffused softbox," which in practice is all a product shadow needs.
Framing, and the mistakes the tool catches
The capture frame has the controls you would expect, resolution, padding in pixels or percent, rotation, offset, plus an auto-crop mode that tightens the frame to the actual shadow content. The part I am most glad I built is the diagnostics: the capture reports whether the shadow is empty, whether it clips the frame, and which edges it touches. A clipped shadow texture is the kind of defect you discover three days later in a different project, wondering why the shadow ends in a hard line under one side of the product. The tool refuses to let that pass silently.
There are also a few background modes because shadows get used differently: a transparent export with the shadow in the alpha channel for compositing onto anything, a multiply-style export for dropping onto existing surfaces, and a contact mode that concentrates the darkness where the model meets the ground, for that tight grounding pool that reads as weight. Which mode you want depends on the scene the shadow is going into, and switching between them is one click rather than a re-render.
The export formats, and why KTX2 is in the list
Exports come out as PNG, WebP, JPEG, AVIF, or KTX2. The first four are familiar. KTX2 is the one worth explaining, and the encoder is the actual Khronos KTX toolchain compiled for the browser, running in a worker so the interface stays responsive during encoding.
The case for KTX2 shadows is GPU memory, the same argument I made in the mobile performance post: a PNG decompresses to raw pixels on the GPU, while KTX2 stays compressed all the way through. A shadow is also the perfect candidate for aggressive compression, because it is smooth, low-frequency, and often effectively single-channel. The tool has a grayscale KTX2 option for exactly that case, plus two encoding modes, one tuned for size and one for edge crispness, because a soft shadow forgives compression in the body but shows artifacts at its edge gradient first.
The part that saves the most time: placement data
The first version of the tool exported an image, and I immediately rediscovered the annoying half of shadow baking: the image is easy, placing it is fiddly. What world-space size should the plane be? Where is its center relative to the model origin? Was the model in millimeters or meters? Guessing those numbers by eye is how shadows end up subtly too small or drifting off-center from the product.
So the export is optionally a small package: the texture plus a placement.json describing the capture in world units, size, center, rotation, and the anchor convention, with scene units configurable from millimeters to inches because CAD files lie about scale constantly. Loading it in three.js is a few honest lines:
const placement = await (await fetch('/shadows/chair.placement.json')).json();
const texture = await ktx2Loader.loadAsync('/shadows/chair.ktx2');
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(placement.size.width, placement.size.depth),
new THREE.MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false })
);
plane.rotation.x = -Math.PI / 2;
plane.position.set(placement.center.x, 0.001, placement.center.z);
The anchor convention matters more than it sounds. The tool can anchor the capture to the model origin, to the footprint center, or to the shadow center, and it records which one you chose. When a configurator swaps between product variants that share an origin, the same placement math keeps every baked shadow registered to its product without per-variant tweaking.
Small things that turned out to matter
- Quality tiers from draft to studio, with supersampling for the final pass. Draft mode keeps the viewport instant while you aim; the export can afford to take two seconds.
- Animated models can be scrubbed to a specific frame, so a character or a folding product can drop its shadow in exactly the pose the scene needs.
- Straight or premultiplied alpha on export, because compositing pipelines disagree and finding out inside a shader is the worst place to find out.
- A proof view that previews the exported texture composited on a neutral surface, which has caught more wrong-softness decisions than any setting.
When baked is right, and when it is not
Baked shadows are for things that do not move relative to their ground: product viewers, configurator variants, furniture in a room scene, architectural stills. The moment the object genuinely animates through space, you are back to real-time techniques or a hybrid, a baked base with a light real-time contact component, which is the setup I described in the lighting post. The point is not that baking replaces real-time shadows. The point is that a large share of the shadows on product sites never needed to be real-time in the first place, and every one of those is frame budget handed back to the things that actually move.
ShadowExport currently lives in my own pipeline, feeding the configurator projects. If there is interest I may clean it up for a public release, and if your team has the same recurring fifteen-minute Blender detour in its week, that interest is worth telling me about.