This in-depth tutorial walks you through every line of a production-ready 3D product configurator built with Three.js. You will learn exactly how the code loads STEP geometry via OCCT, falls back to GLTF, processes scene graphs, swaps PBR materials in real time, and keeps the UI in sync with live weight and price values.
Why This Project Matters for Beginners
Building an interactive 3D configurator teaches you the complete pipeline developers use in real e-commerce sites: loading complex CAD data, handling user input, updating materials without reloading assets, and maintaining 60 fps performance. The example uses beginner-friendly patterns while still showing professional techniques.
Core Libraries and Their Roles
- Three.js r160 – WebGL renderer, scene graph, materials, lights, and animation loop.
- GLTFLoader – Loads the optimized .gltf fallback model.
- OrbitControls – Mouse/touch camera interaction with damping.
- occt-import-js – Converts STEP CAD files into triangle meshes directly in the browser.
All assets live in an assets/ folder. The configurator first tries the STEP file for maximum fidelity, then silently falls back to GLTF if OCCT fails.
HTML Structure and Canvas Setup
<div id="stageContainer">
<div class="stage-loader" id="stageLoader">
<div class="spinner"></div>
<span>Loading 3D Model...</span>
</div>
<canvas id="three-canvas"></canvas>
</div>
The JavaScript selects the container, creates the renderer, and appends the canvas immediately after DOM ready to prevent layout shift.
Renderer, Scene, Camera, and Controls Initialization
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const stageEl = document.getElementById('stageContainer');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(stageEl.clientWidth, stageEl.clientHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
stageEl.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf1f5f9);
const camera = new THREE.PerspectiveCamera(35, stageEl.clientWidth / stageEl.clientHeight, 0.01, 100);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
The renderer uses ACES tone mapping for realistic wood colors. OrbitControls adds smooth inertia and prevents the camera from going below the horizon.
Lighting for Realistic Wood Rendering
const key = new THREE.DirectionalLight(0xffffff, 1.0);
key.position.set(1, 2, 3);
scene.add(key);
scene.environment = new THREE.PMREMGenerator(renderer)
.fromScene(new RoomEnvironment(), 0.04).texture;
A single directional key light plus an environment map gives believable reflections on the Limba veneer without extra fill lights.
Loading STEP Geometry with OCCT (The Critical Step)
async function readStep() {
const occt = await occtimportjs({
locateFile: n => `https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/${n}`
});
const buf = await (await fetch('assets/Flared.step')).arrayBuffer();
const result = occt.ReadStepFile(new Uint8Array(buf), {
linearUnit: 'millimeter',
linearDeflectionType: 'bounding_box_ratio',
linearDeflection: 0.0005,
angularDeflection: 0.3
});
if (!result.success) throw new Error('STEP parse failed');
return result.meshes.map((m, i) => ({
name: m.name || `mesh_${i}`,
pos: Float32Array.from(m.attributes.position.array),
nor: m.attributes.normal ? Float32Array.from(m.attributes.normal.array) : null,
idx: Uint32Array.from(m.index.array)
}));
}
OCCT parses the STEP file on the main thread, returning raw vertex and index arrays. The code then normalizes the coordinate system so the blade always faces the camera.
Fallback GLTF Loading
async function readGltf() {
const gltf = await new GLTFLoader().loadAsync('assets/flared-blade.gltf');
// traverse and extract geometry exactly like the STEP path
}
If OCCT fails (CORS, file missing, or browser limitation), the app silently loads the GLTF version instead.
Building the Scene Graph and Material Assignment
After loading, buildModel() creates BufferGeometry objects, computes normals when missing, and assigns either bladeMat or one of the handle materials based on mesh name heuristics.
const mesh = new THREE.Mesh(geometry, isHandle ? handleMats[current] : bladeMat);
modelGroup.add(mesh);
if (isHandle) handleMeshes.push(mesh);
Real-Time Handle Swapping
function setHandle(n) {
current = n;
handleMeshes.forEach(m => m.material = handleMats[n]);
decals[1].visible = (n === 1);
decals[2].visible = (n === 2);
redraw();
}
Only the material reference changes; the geometry stays in GPU memory. This keeps the configurator responsive.
Animation Loop and Performance
renderer.setAnimationLoop(() => {
const moved = controls.update();
if (moved || autoRotate || dirty) {
renderer.render(scene, camera);
dirty = false;
}
});
requestAnimationFrame via setAnimationLoop guarantees GPU synchronization. Dirty flags prevent unnecessary renders when the camera is still.
The full source is available on GitHub:
Key Takeaways
- OCCT gives you native CAD precision; always provide a GLTF fallback.
- Traverse the scene graph once at load time, then only swap materials.
- Use
MeshPhysicalMaterialwith custom shaders for accurate wood grain mapping. - Keep the animation loop minimal and use
dirtyflags for performance.
Try extending the project by adding more veneer layers or exporting the final configuration as JSON for your backend.
What challenges have you faced when loading CAD data in the browser? Share your experiences in the comments!

Top comments (0)