Back to Blog

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

A deep dive into creating an interactive 3D furniture customization experience using Babylon.js, React, and TypeScript for Kapsül Mobilya.

Umut Korkmaz

Published: 12 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:

// 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:

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)

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:

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:

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:

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!

About the author

Umut Korkmaz

Lead Software Engineer

I'm a senior software engineer in Istanbul. Here I write about the tools and releases I've shipped to production myself, and what they taught me along the way.

Picked by shared topics.