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.