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.
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.
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
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.
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.
Stay up to date! Get all the latest & greatest posts delivered straight to your inbox