Back to Blog
Babylon.jsReact3DTypeScriptE-Commerce

Building a 3D Furniture Customizer with Babylon.js and React

Umut Korkmaz2025-04-0512 min read

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:

typescript
interface 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)

typescript
const 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:

typescript
const 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:

typescript
const 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:

typescript
export 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:

  1. Start with performance in mind - 3D on the web can quickly become sluggish without careful optimization
  2. Invest in good 3D models - The quality of your source models directly impacts the final experience
  3. Test on real devices - Mobile performance varies wildly across devices
  4. 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!