
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 updated! Get all the latest and greatest posts delivered straight to your inbox