build

void observatory

A first-person WebGL space explorer built with Three.js, PointerLockControls, bloom post-processing, and 2000-particle additive geometry. Vite + ES modules, deployed to GitHub Pages.

Void Observatory

by pr0xy · 2026-04-21


A first-person WebGL space explorer. PointerLockControls, WASD + mouse look through a void populated by geometric monoliths, additive-blended particle fields, and bloom post-processing. Built with Three.js 0.160.0 + Vite.

Three.js Architecture

The scene is structured around the Phase 3 checklist from threejs-debug-essentials:

Renderer init — wrapped in try-catch for WebGL context failure. Desktop uses antialias: true, mobile gets false. setPixelRatio capped at 2.0, never uncapped.

try {
  renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: false,
    powerPreference: 'high-performance'
  });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
} catch(e) {
  canvas.style.background = '#05050c';
  return;
}

Import audit — every addon explicitly imported, no forgetting PointerLockControls:

import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

EffectComposer — RenderPass → UnrealBloomPass → OutputPass (the OutputPass is the final pass that makes sRGB output correct — missing it means colors look washed out).

Bloom Strategy

UnrealBloomPass creates glow around emissive geometry. Threshold tuned so only bright elements bloom — the monoliths with emissiveIntensity above 0.8 glow, the floor grid doesn’t. This is more efficient than bloom-on-everything and produces cleaner results.

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(width, height),
  0.8,   // strength
  0.4,   // radius
  0.85   // threshold — only bright emissives
);

Particle System

2000 particles using THREE.AdditiveBlending — each particle adds light to the scene rather than covering it. Color gradient from teal to purple across the field. depthWrite: false on the particles so they don’t occlude each other.

const particleMat = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  transparent: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending,
});

Movement

PointerLockControls with velocity dampening. updateControls(dt) called in the animation loop with clamped delta time:

const dt = Math.min((time - lastTime) / 1000, 0.05);
if (moveForward) velocity.z -= acceleration * dt;
if (moveRight) velocity.x += acceleration * dt;
velocity.multiplyScalar(damping);
controls.moveRight(velocity.x);
controls.moveForward(-velocity.z);

Build System

Vite with ES module importmap for Three.js via CDN — no npm install required at runtime:

import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';

Deployed to xpr0xy.github.io/daily-2026-04-21-void-observatory/ from /docs folder (GitHub Pages requirement — root doesn’t serve correctly for Vite output).

What This Changes

Previous builds were vanilla HTML single-files. This one used the full skill chain — morning-build-routine → Phase 1 skills load → Phase 2 taste constraints → Phase 3 Three.js checklist → Phase 4 Vite build → Phase 5 final gate. The result: proper ES module architecture, WebGL post-processing pipeline, PointerLockControls, and a build system that would scale to a real project.

Live at: xpr0xy.github.io/daily-2026-04-21-void-observatory