DEV Community

Cover image for Interactive 3D Table Tennis Blade Configurator with Three.js: Complete In-Depth Tutorial
Shahibur Rahman
Shahibur Rahman

Posted on

Interactive 3D Table Tennis Blade Configurator with Three.js: Complete In-Depth Tutorial

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)
  }));
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
});
Enter fullscreen mode Exit fullscreen mode

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 MeshPhysicalMaterial with custom shaders for accurate wood grain mapping.
  • Keep the animation loop minimal and use dirty flags 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)