Building a Next-Generation 3D Web Viewer with Three.js: Lightweight, Modular, and Fully Interactive

4/26/2025UI/UX Design3 min read
Featured image for article: Building a Next-Generation 3D Web Viewer with Three.js: Lightweight, Modular, and Fully Interactive

Today, web-based 3D applications are no longer limited to static visualizations. A real 3D experience demands dynamic interaction, professional-level architecture, and lightweight performance. In this article, I explain how I built a high-quality 3D Viewer by using only Three.js — without relying on heavy external libraries like React-Three-Fiber — integrating essential features like model loading (Loader), object selection (Raycaster), a window management system, and a layered scene manager.

The Story: Why I Built This Infrastructure

I was tasked with creating a web application where users could select cities, draw on top of them, and manage these drawings through multiple layers. The goal was to allow users to build detailed 3D city-based models, add and remove objects, and fully control their scenes.

Existing solutions were either too heavy or lacked the necessary customization capabilities. Thus, I decided to build my own infrastructure:

  • Lightweight and independent,

  • Easily extendable for future needs,

  • Offering maximum real-time interaction,

  • Enabling city and layer-based scene control,

  • Providing complete ownership and customization flexibility.

To achieve this, I meticulously designed a system based on Loader, Raycaster, Window Manager, Layer Manager, and a centralized Viewer Store.

Why I Chose Zustand

I opted for Zustand for global state management because it offers a minimal and modular approach compared to heavier alternatives like Redux. With Zustand, I achieved:

  • Clear and readable code structures,

  • Minimal external dependencies,

  • High-performance state sharing for critical objects like scene, camera, and renderer,

  • A scalable store design split into manageable pieces.

This helped me maintain a clean, maintainable, and high-performing application even as the project grew in complexity.

Targeted Architecture

My goal was to establish an infrastructure with:

  • Modular and readable code organization

  • Visual feedback during loading (progress tracking)

  • Real-time object interaction (hover, click, etc.)

  • Dynamic UI control through a Window Manager

  • Visibility management through a Layer Manager

  • Centralized global state management with Viewer Store

  • Minimal dependencies and maximum control

Development: Code Principles, Structure, and Quality

Key coding principles I applied in this project include:

  • Single Responsibility Principle: Each module handles its own task only.

  • Asynchronous Management: Users receive real-time loading feedback and proper error handling.

  • Real-Time Interaction: Raycaster dynamically detects objects under the mouse on every frame.

  • Window Manager: Each window (e.g., Layer Manager, Debug Panel) can register and be opened/closed dynamically.

Example Window Manager snippet:

class WindowManager {
  static windows = [];

  static openWindow(window) {
    if (!this.windows.includes(window)) {
      this.windows.push(window);
    }
  }

  static closeWindow(window) {
    this.windows = this.windows.filter(w => w !== window);
  }
}
  • Layer Manager: Objects are assigned to layers, and users can toggle their visibility through the UI.

Example Layer Manager snippet:

function toggleLayerVisibility(layerNumber, scene) {
  scene.children.forEach(object => {
    if (object.layers.test(new THREE.Layers().set(layerNumber))) {
      object.visible = !object.visible;
    }
  });
}
  • Viewer Store: Global objects like scene, camera, and renderer are centrally managed using Zustand.

Example Viewer Store snippet:

import create from 'zustand';

export const useViewerStore = create((set) => ({
  scene: null,
  camera: null,
  renderer: null,
  setScene: (scene) => set({ scene }),
  setCamera: (camera) => set({ camera }),
  setRenderer: (renderer) => set({ renderer }),
}));
  • Performance-First Rendering: Updates occur only when absolutely necessary.

  • Memory Management: Unused objects are properly disposed to prevent memory leaks.

Conclusion

This Canvas + Three.js architecture offers a rock-solid foundation for anyone aiming to build a modern 3D web viewer. By keeping the code clean and modular while integrating critical systems like Loader, Raycaster, Window Manager, Layer Manager, and Zustand-based Viewer Store, I enabled future-proof scalability and professional-grade interaction.

In the next stages, potential improvements could include:

  • Zooming the camera to an object upon click,

  • Enhancing loading animations,

  • Introducing multi-layer and multi-scene support,

  • Adding window minimization and restoration features,

  • Saving and loading scene settings through the Viewer Store.

Comments (0)

Newsletter

Stay updated! Get all the latest and greatest posts delivered straight to your inbox

© 2026 Kuray Karaaslan. All rights reserved.