Building a 3D Furniture Customizer with Babylon.js and React
When I started working on Kapsül Mobilya, the challenge was clear: create an immersive 3D furniture shopping experience that lets customers customize products in real-time. After evaluating several 3D libraries, I chose Babylon.js for its robust feature set, excellent TypeScript support, and active community.
Why Babylon.js Over Three.js?
Both are excellent choices, but for this e-commerce project, Babylon.js offered several advantages:
- Built-in GUI system for creating in-scene UI elements
- Better physics engine integration for realistic product interactions
- Native TypeScript support from the ground up
- Inspector and debugging tools that significantly speed up development
- Excellent glTF support for importing 3D furniture models
Architecture Overview
The application follows a modular architecture that separates concerns effectively:
typescript// Scene setup with React integration
import { Engine, Scene } from '@babylonjs/core';
import { useEffect, useRef } from 'react';
export const useBabylonScene = (canvasRef: RefObject<HTMLCanvasElement>) => {
const engineRef = useRef<Engine | null>(null);
const sceneRef = useRef<Scene | null>(null);
useEffect(() => {
if (!canvasRef.current) return;
const engine = new Engine(canvasRef.current, true, {
preserveDrawingBuffer: true,
stencil: true,
});
const scene = new Scene(engine);
// Configure scene for furniture visualization
scene.clearColor = new Color4(0.95, 0.95, 0.95, 1);
engineRef.current = engine;
sceneRef.current = scene;
engine.runRenderLoop(() => {
scene.render();
});
return () => {
engine.dispose();
};
}, [canvasRef]);
return { engine: engineRef, scene: sceneRef };
};
Real-Time Material Customization
One of the most requested features was the ability to change furniture materials in real-time. Here's how we implemented the material switching system:
typescriptinterface MaterialConfig {
name: string;
albedoTexture: string;
normalTexture?: string;
roughness: number;
metallic: number;
}
const applyMaterial = async (
mesh: Mesh,
config: MaterialConfig,
scene: Scene
) => {
const material = new PBRMaterial(config.name, scene);
// Load textures asynchronously
material.albedoTexture = new Texture(config.albedoTexture, scene);
if (config.normalTexture) {
material.bumpTexture = new Texture(config.normalTexture, scene);
}
material.roughness = config.roughness;
material.metallic = config.metallic;
// Smooth transition
await animateMaterialTransition(mesh, material);
mesh.material = material;
};
Performance Optimizations
Working with 3D on the web requires careful attention to performance. Here are the key optimizations we implemented:
1. Level of Detail (LOD)
typescriptconst setupLOD = (mesh: Mesh, scene: Scene) => {
const lod0 = mesh; // High detail
const lod1 = createSimplifiedMesh(mesh, 0.5); // 50% detail
const lod2 = createSimplifiedMesh(mesh, 0.25); // 25% detail
lod0.addLODLevel(15, lod1);
lod0.addLODLevel(30, lod2);
};
2. Texture Compression
We used Basis Universal textures for significant file size reduction without visible quality loss:
typescriptconst loadCompressedTexture = async (url: string, scene: Scene) => {
const texture = new Texture(url, scene, false, true,
Texture.TRILINEAR_SAMPLINGMODE,
null, null, null, true // Enable Basis transcoding
);
return texture;
};
3. Instance Rendering
For scenes with multiple identical objects (like chair legs), we use instancing:
typescriptconst createFurnitureInstances = (
baseMesh: Mesh,
positions: Vector3[]
) => {
positions.forEach((pos, index) => {
const instance = baseMesh.createInstance(`instance_${index}`);
instance.position = pos;
});
};
Integration with React State
Connecting Babylon.js with React's state management required a custom hook system:
typescriptexport const useFurnitureCustomizer = (scene: Scene | null) => {
const [selectedPart, setSelectedPart] = useState<string | null>(null);
const [currentMaterial, setCurrentMaterial] = useState<MaterialConfig>(
defaultMaterial
);
const handleMaterialChange = useCallback(async (config: MaterialConfig) => {
if (!scene || !selectedPart) return;
const mesh = scene.getMeshByName(selectedPart);
if (mesh) {
await applyMaterial(mesh, config, scene);
setCurrentMaterial(config);
}
}, [scene, selectedPart]);
return {
selectedPart,
setSelectedPart,
currentMaterial,
handleMaterialChange,
};
};
Lessons Learned
Building Kapsül Mobilya taught me several valuable lessons:
- Start with performance in mind - 3D on the web can quickly become sluggish without careful optimization
- Invest in good 3D models - The quality of your source models directly impacts the final experience
- Test on real devices - Mobile performance varies wildly across devices
- Progressive enhancement - Have fallback experiences for devices that can't handle 3D
What's Next?
We're currently exploring WebXR integration to allow customers to visualize furniture in their actual spaces using AR. The combination of Babylon.js's excellent XR support and modern smartphone capabilities makes this increasingly practical.
The project is live at kapsulmobilya.com.tr - feel free to check it out and customize some furniture!