Blender Normal Maps in Three.js: Realistic Web Materials Guide
Normal maps are the secret weapon for creating visually stunning 3D web experiences without crushing your users' devices. Instead of modeling every surface detail with thousands of polygons, normal maps simulate surface complexity through clever lighting calculations. This technique lets you achieve photorealistic materials in Three.js applications while maintaining smooth 60fps performance.
This guide walks you through the complete workflow: baking high-quality normal maps in Blender, exporting them with the correct settings, and implementing them in a Next.js Three.js application. You'll learn the critical technical details that separate amateur-looking 3D web apps from professional-grade experiences.
Prerequisites
Before starting, ensure you have:
- Blender 3.0 or newer installed
- Node.js 18+ for Next.js development
- Basic familiarity with Blender's interface
- A Next.js project with Three.js already configured
Step 1: Prepare Your High and Low Poly Models
Start by creating two versions of your 3D model in Blender. The high-poly version contains all the surface detail you want to capture, while the low-poly version serves as your game-ready mesh.
Create your high-poly model first with subdivision surface modifiers, displacement maps, or detailed sculpting. This model can have 50,000+ polygons since it's only used for baking. Focus on getting the surface details exactly right.
Next, create the low-poly version by either manually retopologizing or using Blender's decimate modifier. Target 500-5000 polygons depending on your performance requirements. The low-poly mesh should follow the same general shape but without fine details.
Name your objects clearly: ObjectName_High and ObjectName_Low. This naming convention helps during the baking process and keeps your project organized.
Step 2: UV Unwrap the Low-Poly Model
Select your low-poly model and enter Edit mode. Proper UV unwrapping is crucial for normal map quality, so take time to get this right.
Mark seams strategically by selecting edges and pressing Ctrl+E, then "Mark Seam". Place seams in areas that will be less visible or along natural boundaries like clothing edges or panel lines.
Select all faces (A key) and unwrap with U > "Unwrap". In the UV editor, arrange your UV islands efficiently. Larger islands get more texture resolution, so prioritize important surfaces.
Check for overlapping UVs and ensure adequate spacing between islands. Overlaps cause baking errors, while insufficient spacing creates bleeding artifacts.
Step 3: Set Up Materials for Baking
Switch to the Shading workspace and select your low-poly model. Create a new material and add an Image Texture node, but don't connect it to anything yet.
Create a new image in the Image Texture node: click "New" and set dimensions to 1024x1024 or higher. Choose "Non-Color" for the Color Space since normal maps contain directional data, not color information.
Ensure this Image Texture node is selected (highlighted with a white border). Blender bakes to the selected Image Texture node, so this selection is critical.
For the high-poly model, you can keep existing materials or create simple ones. The high-poly materials won't affect the normal map baking process.
Step 4: Configure Baking Settings
Open the Render Properties panel and scroll to the Bake section. Set the Bake Type to "Normal" and ensure "Selected to Active" is checked since you're baking from high-poly to low-poly.
Set the Ray Distance to a value slightly larger than the maximum distance between your high and low-poly models. Start with 0.1 and adjust if you see missing details or unwanted projections.
The Extrusion value should be small, around 0.01. This prevents the baking rays from starting exactly on the surface, which can cause artifacts.
Under Influence, ensure only "Direct" and "Indirect" are checked. For normal maps, you typically want both to capture all surface details.
Step 5: Bake the Normal Map
Select your high-poly model first, then add the low-poly model to the selection (Shift+click). The low-poly model should be the active object (highlighted in a lighter orange).
Click the "Bake" button and wait for the process to complete. Baking time depends on your model complexity and image resolution. Watch for any error messages in the system console.
After baking, check the result in the Image Editor. A proper normal map appears predominantly blue with purple and green variations representing surface details. Pure black or white areas usually indicate problems.
Save the normal map as a PNG or EXR file. PNG works well for most web applications, while EXR preserves more data if you need maximum quality.
Step 6: Export Models with Correct Settings
Export your low-poly model as GLTF for optimal Three.js compatibility. Select File > Export > glTF 2.0 and choose these settings:
- Format: GLB (binary is more efficient for web)
- Include: Selected Objects only
- Transform: Apply modifiers
- Geometry: Export UVs and normals
- Materials: Export materials and images
Ensure your normal map is included in the export by checking the materials panel. The GLTF exporter should automatically include textures referenced by materials.
Test the exported file by importing it back into Blender or viewing it in a GLTF viewer to confirm everything exported correctly.
Step 7: Implement Normal Maps in Three.js
In your Next.js application, load the model and normal map using Three.js loaders. Here's the essential code structure:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { TextureLoader } from 'three'
const gltfLoader = new GLTFLoader()
const textureLoader = new TextureLoader()
// Load normal map texture
const normalMap = textureLoader.load('/models/your-normal-map.png')
// Load GLTF model
gltfLoader.load('/models/your-model.glb', (gltf) => {
const model = gltf.scene
// Apply normal map to materials
model.traverse((child) => {
if (child.isMesh) {
child.material.normalMap = normalMap
child.material.normalScale.set(1, 1) // Adjust intensity
child.material.needsUpdate = true
}
})
scene.add(model)
})
The normalScale property controls the normal map intensity. Values above 1 increase the effect, while values below 1 reduce it. Start with (1, 1) and adjust based on your visual needs.
Step 8: Optimize Lighting for Normal Maps
Normal maps only work with dynamic lighting, so ensure your Three.js scene has appropriate light sources. Directional lights work well for outdoor scenes, while point lights suit indoor environments.
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 5, 5)
scene.add(directionalLight)
// Add ambient light for fill lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.3)
scene.add(ambientLight)
Position lights to highlight the surface details captured in your normal map. Moving lights around your model should reveal the surface complexity even on the low-poly geometry.
Common Mistakes and Troubleshooting
Inverted Normal Maps: If your surface details appear inverted (bumps look like dents), flip the green channel of your normal map. In Blender, add a Separate RGB node and invert the green channel before recombining.
Seam Artifacts: Visible seams in your normal map usually indicate UV unwrapping issues. Ensure adequate spacing between UV islands and avoid stretching UVs too much.
Performance Issues: Large normal map textures impact loading times and memory usage. Start with 1024x1024 resolution and only increase if you need more detail. Consider using texture compression for production deployments.
For Next.js applications specifically, implement proper 3D loading states to handle the additional texture loading time gracefully.
Next Steps
With normal maps working in your Three.js application, consider adding other material maps like roughness and metallic maps for even more realistic materials. You can bake these using similar techniques in Blender.
Explore advanced Three.js features like environment mapping and post-processing effects that work beautifully with normal-mapped materials. The realistic surface details from your normal maps will make these effects much more convincing.
For production applications, implement texture streaming and level-of-detail systems to maintain performance across different devices. Consider how normal maps fit into your overall Next.js image optimization strategy for the best user experience.