Code Structuring Best Practices for Three.js and WebXR Projects

This article outlines a scalable and maintainable code architecture for building WebXR experiences using Three.js. Proper structuring enhances team collaboration, improves performance, simplifies debugging, and allows smooth expansion of features and scenes over time.

Why Code Structuring Matters

Unstructured or monolithic code can quickly become unmanageable, especially in complex XR projects. By modularizing and organizing code components, developers can:

  • Reduce cognitive load
  • Enable code reuse
  • Improve debugging and maintenance
  • Separate concerns (e.g., UI logic vs. rendering logic)
  • Facilitate asset and scene management

Recommended Folder Structure

A scalable file structure typically looks like this:

bash

Copy

Edit
/src
  ├── core/               # Renderer, scene, camera, loop, XR
  ├── components/         # Reusable UI or interaction components
  ├── systems/            # Game/scene logic, input, animation systems
  ├── assets/
  │   ├── models/
  │   ├── textures/
  │   └── shaders/
  ├── scenes/             # Scene modules (Scene1, Scene2, Lobby, etc.)
  ├── utils/              # Helpers, math, physics, loaders
  ├── managers/           # State, event, or UI managers
  ├── main.js             # App entry point
  └── config.js           # Global config and constants

Code Responsibility Breakdown

1. Core Initialization (/core)

Handles essential setup logic:

javascript

Copy

Edit
// core/renderer.js
export const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
javascript

Copy

Edit
// core/camera.js
export const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
javascript

Copy

Edit
// core/loop.js
export class Loop {
  constructor(camera, scene, renderer) { ... }
  start() { renderer.setAnimationLoop(() => this.tick()); }
  tick() { ... }
}

2. Scene Modules (/scenes)

Each scene (e.g., Lobby, Garden, Futuristic City) lives in its own module. Scene modules encapsulate:

  • Geometry
  • Materials
  • Lighting
  • Scene-specific logic

Example:

javascript

Copy

Edit
// scenes/Scene1.js
export default class Scene1 {
  constructor() {
    this.scene = new THREE.Scene();
    this.loadEnvironment();
    this.addObjects();
  }
}

3. Systems (/systems)

Contains logic systems (inspired by ECS pattern) such as:

  • AnimationSystem
  • InputSystem
  • PhysicsSystem
  • XRSystem
  • InteractionSystem

Example:

javascript

Copy

Edit
// systems/AnimationSystem.js
export default class AnimationSystem {
  constructor(mixer) { this.mixer = mixer; }
  update(delta) { this.mixer.update(delta); }
}

4. Managers (/managers)

Encapsulates application-level logic:

  • StateManager.js: Tracks user progress or current scene
  • UIManager.js: Manages on-screen controls
  • XRManager.js: Handles XR session start/stop logic

5. Utils (/utils)

Reusable functions and wrappers:

  • loadModel.js: Loads GLTFs
  • createControls.js: Handles VR or desktop controls
  • createUI.js: Mesh UI wrappers
  • helpers.js: Math and general utility functions

Scene Switching Strategy

Use a SceneManager to dynamically load or unload scenes:

javascript

Copy

Edit
class SceneManager {
  constructor(renderer) { this.renderer = renderer; }
  load(sceneModule) {
    if (this.current) this.cleanup();
    this.current = new sceneModule();
    this.renderer.setAnimationLoop(() => this.current.update());
  }
}

Config Management

Use a single config.js file for centralized constants:

javascript

Copy

Edit
export const CONFIG = {
  XR_ENABLED: true,
  SHADOWS: true,
  FOV: 75,
  DEBUG_MODE: false,
};

Benefits of Modular Structure

FeatureBenefitScene SeparationEasier scene transitions & testingReusable ComponentsReduces code duplicationSystem Logic IsolationBetter debugging and readabilityAsset ModularityFast swapping of models, shadersConfig CentralizationOne point to control global behavior

Tips for Production Code

  • Use ES6 modules: Enables cleaner imports and tree-shaking
  • Implement Lazy Loading: For scenes and assets to reduce initial load time
  • Enable Hot Reload: For faster development with tools like Vite or Webpack
  • Track VRAM Usage: Optimize assets before use in scenes
  • Document each module: Describe purpose and usage clearly

Conclusion

A modular codebase is essential for the successful development of performant, extensible WebXR experiences. It ensures your team can collaborate efficiently, track performance issues, and expand the project with confidence as it grows in complexity.

Leave a comment

Your email address will not be published. Required fields are marked *