Skip to content

Blender Workflow and Baking

A Three.js guide for coding agents. Also covers blender to three.js, baking lightmaps, baked texture, bake render into texture, gltf export settings, glb export blender, and 17 more.

Show all 23 aliases

blender to three.js, baking lightmaps, baked texture, bake render into texture, gltf export settings, glb export blender, uv unwrapping, mark seam, smart uv project, flipY texture upside down, texture looks mirrored, washed out colors gltf, baked scene looks black, flipped normals black faces, apply scale before unwrap, merge geometries draw calls, emission material export, MeshBasicMaterial baked map, fireflies particles shader, portal shader perlin, gl_PointSize pixel ratio, how do I bake a blender scene for three.js, how do I import a baked scene

Why bake

  • Ray traced renders (Cycles) simulate light bounces, indirect lighting, and soft shadows. WebGL cannot afford that per frame, so real-time renders never match a Blender render
  • Baking saves the ray traced result into a texture applied to the geometry in Three.js. No lights, no real-time shadows, just a texture on meshes, so it looks like the offline render and runs fast
  • Costs: baking is slow to produce, textures add load time and a GPU upload freeze at startup, and lights are frozen. Any lighting change means re-baking in Blender
  • Mixing baked and dynamic materials is possible but keeping them visually homogeneous is hard
  • The pipeline: model, optimize geometry, UV unwrap everything, bake to texture(s), export model + texture, import in Three.js and apply the texture with a lightless material

Blender essentials

  • Blender's up axis is Z. Three.js uses Y. The glTF exporter's +Y Up option handles the conversion, never rotate the scene to compensate
  • 1 Blender unit exports as 1 Three.js unit. Decide a unit scale up front, the grid squares are 1 unit each
  • Shortcuts are hover-area sensitive and mode sensitive: the cursor must be over the 3D Viewport for viewport shortcuts. On Mac, CTRL means CTRL, not CMD
  • Core viewport: MIDDLE MOUSE orbit, SHIFT + MIDDLE MOUSE truck/pedestal, WHEEL dolly (gets stuck at the view-point, use SHIFT + CTRL + MIDDLE MOUSE to truly move forward), NUMPAD 1/3/7 axis views, NUMPAD 0 camera view, SHIFT + C reset, NUMPAD , focus selection, NUMPAD / isolate selection
  • Core editing: SHIFT + A add object, X delete, G/R/S move/rotate/scale (then X/Y/Z to constrain axis), TAB toggles Edit Mode, 1/2/3 switch vertex/edge/face select, SHIFT + D duplicate, ALT + D linked duplicate, A select all, F3 search any action by name
  • F9 reopens the creation parameters popup (vertex count, size) right after adding a primitive; it is gone once you edit the geometry
  • Rendering engines: Eevee is real-time and roughly matches what WebGL can do; Cycles is the ray tracer you bake with. Baking requires Cycles, the Bake panel does not exist under Eevee
  • Principled BSDF is PBR and maps to MeshStandardMaterial on export. Emission surfaces are for glowing objects. Change only Base Color and Roughness for a flat stylized look
  • If the interface is wrecked, close and reopen Blender. File > Defaults > Save Startup File persists a layout you like

Modeling recipes

  • Buns/pebbles: cube, Subdivision Surface modifier, raise subdivisions, right click Shade Smooth, then in Edit Mode add loop cuts (CTRL + R) near top and bottom edges to control roundness. Modifiers are non-destructive, the base cube stays editable
  • Melted flat shapes (cheese): plane, Subdivide with ~10 cuts, enable Proportional Editing (O, falloff Sharp, WHEEL adjusts radius) and drag corner vertices down, add a Solidify modifier for thickness
  • Sharp-plus-smooth surfaces: Shade Smooth alone smears hard edges. Use Shade Auto Smooth (Blender 4+) or Object Data Properties > Normals > Auto Smooth (pre-4) so flat areas stay crisp
  • Low poly cylinders: create with ~10 vertices in the creation popup. Triangle Fan cap fill lets you delete wedge faces to make split logs, then select 4 vertices and press F to fill missing faces
  • Rocks: cube, then repeated Bisect cuts (find it via F3) with Clear Outer + Fill. Do not hand-tweak vertices afterward or you break face flatness. Keep the origin at floor level so scaling stays grounded. Reuse the same few rocks everywhere, nobody notices
  • Insets and extrudes (lamp recesses): select faces, I for inset, click immediately, then set Individual + thickness in the operator panel. For per-face extrusion set Transform Pivot Point to Individual Origins first, E, orientation Normal. Separate faces into their own object with P > Selection when they need a different material later
  • Repeated identical objects (portal bricks, pole lights): duplicate with ALT + D (linked duplicate) so editing one edits all. Rotate during the duplicate move, then F3 > Repeat Last stamps out a circle of bricks
  • Organize with Collections (M to move selection into one). They are folders, not Three.js groups: they do not transform children. Keep the scene graph flat, deep parenting breaks the later import code. Make helper collections unselectable via the Outliner filter arrows
  • Share one material across objects: select targets, SHIFT + click the object that has the material last (active object), CTRL + L > Materials
  • Emissive objects use an Emission surface with high Strength (lamps ~80, portal ~40). One large Area Light gives diffuse ambient lighting and soft shadows

Reference renders in Blender

Do a real Cycles render before baking: the bake is supposed to reproduce it, and it is your ground truth for colors and lighting.

  • A camera is required to render (F12). NUMPAD 0 enters camera view; move the camera from a second viewport until the scene fills the frame
  • Render Properties > Sampling drives quality vs time: 128 is decent with visible noise, 256 is mostly clean, 512 is safely clean, 1024 only if the machine can take it. Keep viewport samples low (~32) for interactivity
  • Check Denoise for final presentation renders to remove sampling noise (but never for bakes, see below)
  • Output resolution lives in Output Properties (1920x1080 at 100% for a shareable render)
  • After the render finishes, ALT + S over the render window saves the image
  • Iterate lighting here: white strong lights first to judge material colors neutrally, then tint the Area Light (warm orange for sunrise, blue plus dimmer emissions for night). Every lighting decision is final once baked

Optimize before unwrapping

Do all of this before any UV work, removing faces after unwrapping invalidates the layout.

  • Delete every hidden face: bottoms of trunks resting on the floor, faces pressed against other faces (stair backs, brick faces inside stairs). Toggle Wireframe/Solid shading to see them
  • Unlink all duplicates: select everything, F3 > Make Single User > Object & Data. Linked duplicates share geometry, so both copies would unwrap to the same UV coordinates and bake twice into the same texel region
  • Fix face orientation: overlay menu, check Face Orientation. Blue is correct, red is flipped. Flipped normals bake black and are invisible in Three.js (backface culling). Select red faces in Edit Mode, F3 > flip > Mesh > Normals > Flip
  • Apply object scales: select all in Object Mode, CTRL + A > Scale. Automatic unwrapping sizes islands from geometry, not object scale, so an object scaled in Object Mode gets a wrongly sized island (a shrunken axe would still claim half the texture)
  • Move objects that will get their own Three.js material (emissions: lamps, portal disc) into their own collection and make it unselectable. Baking them wastes texture space; they will be flat colors or shaders in Three.js

UV unwrapping

  • Goal: unfold every baked geometry into one shared 0-1 square. One texture is enough for a small scene; split into multiple textures only for large scenes needing more resolution
  • Change an area to UV Editor. UV shortcuts mirror the viewport: A select all, G move, S scale, while hovering the editor
  • Boxes need seams before unwrap or the unfold is garbage: in edge select, choose the cut edges, U > Mark Seam (red), then select the island (CTRL + L selects connected faces) and U > Unwrap. Wrong seams are removed with U > Clear Seam. The test of a good unwrap is undistorted faces
  • Increase the unwrap Margin (operator panel, e.g. 0.05 to 0.15) so islands do not touch, baking bleeds past island edges
  • Save effort on repeated objects: unwrap one fence, delete the others, duplicate the unwrapped one into their places, then select all copies and unwrap together
  • Organic shapes (rocks): U > Smart UV Project with an island margin (~0.025). Automatic unwraps are decent but produce more islands and seams than manual work; manual unwrapping keeps groups compact and separable
  • Check proportions with Display Stretch > Area in the UV Editor overlay: scale island groups until colors match, so no object gets more or less texel density than it needs. Deliberately upscale surfaces seen up close (the floor)
  • Packing tips: arrange groups into a rough square first, then scale the whole thing into the UV square; keep spacing between islands; keep leftover room for forgotten objects; use a group of many small islands (rocks) as filler

Baking

  • Create the target image in the UV Editor: + New, name baked, 4096 x 4096 (power of 2; 8192 is not safe in every browser), white base color (mip mapping bleeds the background color at edges in Three.js, white reads as a reflection), no alpha, Generated Type: Blank, 32-bit Float checked, not tiled
  • 32-bit Float makes the bake an HDR intermediate. Bake into HDR to keep full precision, tone map later. Save it as Radiance HDR (Image > Save As, baked.hdr) next to the .blend
  • Tell each material where to bake: in the Shader Editor, add a Texture > Image Texture node, pick the baked image, leave it unconnected, and make sure it is the active (outlined) node. The active image texture node is the bake target. Keep its Color Space linear, this is the HDR intermediate, not the final texture
  • Bake settings (Render Properties > Bake, Cycles only): Bake Type: Combined; View From: Active Camera (affects reflections); Margin: 16 px, type Extend (cleaner edges in WebGL than adjacent-face pixels); uncheck Clear Image so successive per-object bakes accumulate into one texture
  • Sampling: 128 samples is enough for a 4096 bake. Turn Denoise OFF for baking (it is applied later in the compositor and can artifact during bakes)
  • Select the object(s), confirm the texture node is active in every involved material, hit Bake. Objects sharing a material already share the node. You can bake several objects at once if all their materials have the node set
  • If the bake finishes but the image looks unchanged, it is a display bug: save the image (ALT + S over the UV Editor) and hit refresh in the side panel. Save the image after every bake, it is a separate file from the .blend
  • Troubleshooting: overlapping bakes mean overlapping islands, separate them and re-bake those parts on top; unusually black islands mean flipped normals, flip and re-bake

From HDR bake to final JPEG

Raw bakes look burned out and noisy: baking bypasses both the color management (Filmic/AgX view transform) and the Denoise render option. Fix all of it in the Compositor:

  • Open a Compositor area, check Use Nodes
  • Add an Image node pointing at baked, a Denoise node (defaults), and wire Image > Denoise > Composite
  • Mute the default Render Layers node with M so the scene itself is not rendered
  • The view transform (Filmic or AgX, in Render Properties > Color Management) is applied automatically during this composite render
  • Set the output resolution to 4096 x 4096 in Output Properties, press F12, then ALT + S over the render window
  • Save as JPEG, ~75% quality, baked.jpg. JPEG is fine because no alpha is needed; sRGB encoding comes with it

glTF export

  • Select only what should ship (everything except camera, lights, and helper collections), File > Export > glTF 2.0
  • Settings: format glTF Binary (.glb); Remember Export Settings; include Selected Objects only; +Y Up; Apply Modifiers (otherwise a subdivided cube exports as the raw cube); geometry data limited to what you use. For a baked scene export UVs only, no normals, no materials, the texture carries everything; Compression on (Draco)
  • Draco shrinks the file substantially but Three.js must ship the decoder. For tiny uncompressed models the decoder can cost more than it saves; decide per project
  • Object names survive export. In Blender, rename each object needing special treatment in Three.js (poleLightA, poleLightB, portalLight, baked). Blender names are unique per object, so name-based lookup is reliable

Importing into Three.js

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

const bakedTexture = textureLoader.load('baked.jpg')
bakedTexture.flipY = false
bakedTexture.colorSpace = THREE.SRGBColorSpace

const bakedMaterial = new THREE.MeshBasicMaterial({ map: bakedTexture })
  • Exported PBR materials import as MeshStandardMaterial, which needs lights, so the scene appears as black silhouettes. Replace with MeshBasicMaterial carrying the baked map, lighting is already in the texture
  • flipY = false is mandatory: glTF and Three.js disagree on the texture Y direction, without it the texture is vertically mirrored and lands on the wrong faces
  • colorSpace = THREE.SRGBColorSpace on the baked texture, or colors look washed out compared to the Blender render
  • Find named objects and give emissives their own lightless materials:
gltfLoader.load('portal.glb', (gltf) => {
    scene.add(gltf.scene)
    const bakedMesh = gltf.scene.children.find((c) => c.name === 'baked')
    const portalLightMesh = gltf.scene.children.find((c) => c.name === 'portalLight')
    bakedMesh.material = bakedMaterial
    portalLightMesh.material = portalLightMaterial   // MeshBasicMaterial or ShaderMaterial
})
  • Before merging (below), applying one material to everything is done with gltf.scene.traverse((child) => { child.material = bakedMaterial })

Merging for draw calls

  • Dozens of separate meshes means dozens of draw calls. A fully baked static scene needs one mesh: shadows are painted into the texture anyway, so individual objects can never move without looking broken
  • In Blender: make the emissions collection unselectable, select all baked objects, duplicate with SHIFT + D (never merge the originals, you lose editability), move duplicates to a merged collection (M), join with CTRL + J (CMD + J on macOS), rename the result baked
  • Keep the original per-object collections deactivated but present for future edits and re-bakes
  • The merged object keeps multiple materials in Blender; irrelevant since materials are not exported
  • Export the baked mesh plus the emission objects. Result: 4 draw calls for the whole scene (baked, portal, two pole lights). Verify with Spector.js or renderer.info

Animated details on top of a baked scene

Baked scenes are static; shader-driven details bring them back to life.

Fireflies (custom Points shader)

  • BufferGeometry with a random position attribute (constrain to the scene bounds, above the floor) and a per-particle aScale attribute (itemSize 1), rendered as THREE.Points with a ShaderMaterial
  • Vertex shader essentials:
uniform float uPixelRatio;
uniform float uSize;
uniform float uTime;
attribute float aScale;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    modelPosition.y += sin(uTime + modelPosition.x * 100.0) * aScale * 0.2;
    vec4 viewPosition = viewMatrix * modelPosition;
    gl_Position = projectionMatrix * viewPosition;
    gl_PointSize = uSize * aScale * uPixelRatio;
    gl_PointSize *= (1.0 / - viewPosition.z);
}
  • Multiply gl_PointSize by a uPixelRatio uniform (Math.min(window.devicePixelRatio, 2)) or particles shrink on retina screens; update the uniform in the resize handler
  • gl_PointSize *= (1.0 / - viewPosition.z) is manual size attenuation, shader points do not get sizeAttenuation for free
  • Desynchronize the float by offsetting sin() with the particle's own x position instead of adding another attribute; scale amplitude by aScale so small fireflies drift less
  • Fragment shader glow, drawn from gl_PointCoord (a texture is cheaper, a formula is more controllable):
float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
float strength = 0.05 / distanceToCenter - 0.1;
gl_FragColor = vec4(1.0, 1.0, 1.0, strength);
  • The - 0.1 pushes the falloff below zero before the square's edge; without it the formula never reaches 0 and the quad edges show
  • Material flags: transparent: true (alpha does nothing without it), blending: THREE.AdditiveBlending for the light-like glow, depthWrite: false to stop particles clipping each other
  • Drive uTime from clock.getElapsedTime() in the tick

Portal surface (Perlin shader)

  • The portal disc gets its own ShaderMaterial. Its UVs must fill the whole 0-1 square: in Blender select the disc's faces and U > Unwrap on its own (rotation does not matter for a circular pattern), re-export
  • Verify UVs by outputting gl_FragColor = vec4(vUv, 1.0, 1.0); no gradient means broken unwrap
  • Pattern recipe, using Stefan Gustavson's classic 3D Perlin cnoise (3D so time animates the third axis):
vec2 displacedUv = vUv + cnoise(vec3(vUv * 5.0, uTime * 0.1));
float strength = cnoise(vec3(displacedUv * 5.0, uTime * 0.2));
float outerGlow = distance(vUv, vec2(0.5)) * 5.0 - 1.4;
strength += outerGlow;
strength += step(- 0.2, strength) * 0.8;
strength = clamp(strength, 0.0, 1.0);
vec3 color = mix(uColorStart, uColorEnd, strength);
gl_FragColor = vec4(color, 1.0);
  • Displacing the UV with one noise before sampling a second gives the swirling look; different small time multipliers per noise keep it organic and slow
  • The outer glow (scaled, offset distance-to-center) forces the rim to full strength so the disc meets the bricks in solid color
  • Adding a dimmed step() on top of the smooth noise mixes sharp shapes into the smoothness instead of replacing it
  • clamp(strength, 0.0, 1.0) before mix() is required: Perlin output exceeds the 0-1 range, and unclamped values extrapolate colors (deep blue to orange turns yellow at the rim)
  • Expose uColorStart/uColorEnd (THREE.Color) and the renderer clear color as lil-gui tweaks; setClearColor must be re-called in onChange, changing the stored string does nothing

Common mistakes

  • Baking with linked duplicates still linked, so both copies unwrap to identical UVs and bake on top of each other
  • Skipping CTRL + A > Apply Scale, so auto unwrap sizes islands from the un-scaled geometry
  • Removing hidden faces after unwrapping, invalidating the layout
  • Not marking seams before unwrapping boxes, producing distorted spaghetti islands
  • Zero unwrap margin, so bake bleed overlaps neighboring islands
  • Flipped normals: black baked islands in the texture, invisible faces in Three.js
  • Baking with Eevee selected (no Bake panel) or with Clear Image checked, wiping previous per-object bakes
  • Forgetting to make the Image Texture node active in a material, baking into the wrong image
  • Leaving Denoise on during the bake instead of denoising in the compositor
  • Exporting the raw bake without the compositor pass, shipping burned-out colors and noise (the view transform and denoise are lost in baking)
  • Forgetting to save the baked image file, the .blend save does not include it
  • Exporting materials/normals with a baked scene, or forgetting Apply Modifiers so subdivided objects export as their base cubes
  • Leaving the imported glTF on MeshStandardMaterial with no lights and reporting a black scene as a loader bug
  • Forgetting bakedTexture.flipY = false (mirrored texture) or SRGBColorSpace (washed out colors)
  • Merging the original objects instead of duplicates, losing the editable source scene
  • Shipping dozens of draw calls for a static scene instead of one merged baked mesh
  • Points shader without the pixel-ratio multiplier, so particles look tiny on retina displays
  • Glow shader without transparent: true, or with depthWrite left on so particles clip each other
  • Not clamping Perlin strength before mix(), extrapolating rim colors

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-three-js-guide({ topic: "blender-workflow-and-baking" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems