A browser-based Three.js scene: a reflective sphere, a ring of procedural toruses on an elliptical path, a Preetham analytic sky dome, and real-time environment reflections. Originally part of globex; extracted here as a focused WebGL demo.
Live demo: rings-two.vercel.app
| Element | Role |
|---|---|
| Sky dome | Preetham atmospheric scattering shader; sun position drives lighting |
Reflective sphere (demo) |
Mirror material updated each frame from ping-pong cube cameras |
| Donut ring | 24 toruses placed on an ellipse; each gets a unique procedural MeshStandardMaterial |
| Gloss floor | Large plane with tiled metal texture, receives shadows |
| Magenta glow ball | PointLight + small sphere orbiting the path on a 10s loop |
| Sun & frontera | Spot lights from Scenario.json; sun tracks sky sun, casts shadows |
Interaction: OrbitControls — drag to orbit, scroll to zoom, right-drag to pan (damped).
Debug: stats.js FPS panel (top-left) when running locally or in dev builds.
index.htmlloadssrc/Main.js.Main.jsreads optional URL query params, builds aviewobject, and dynamically importsRings.Ringsconstructs the scene, renderer, cameras, lights, actors, and starts the animation loop.
Scene
├── Sky (shader dome)
├── fixed/
│ └── floor
├── lights/ ← from Scenario.json
├── cameras/
│ ├── PerspectiveCamera (main)
│ └── cubeCamera_a / cubeCamera_b
└── actors/
├── glowBall (PointLight on path)
├── 24× donut (TorusGeometry)
└── demo (reflective sphere)
Ellipse extends THREE.Curve: points on (xRadius·cos(2πt), 0, yRadius·sin(2πt)) with default radius 42. Utilities in Utils.js place objects on the curve and align their orientation to the tangent.
Two motion layers run in parallel:
- Path tween (90s loop) — Moves the main camera and both cube cameras along the ellipse (
animateObjectsAlongPath). The reflective sphere’s env-map sources follow this motion indirectly via cube camera updates. - Glow ball tween (10s loop) — Moves the magenta
PointLightaround the same path.
Each frame:
- Hide
demo, alternate updatingcubeCamera_a/cubeCamera_binto the sphere’senvMap, showdemo. tweenGroup.update(time).renderer.render(scene, camera).controls.update().
- Donuts: Canvas-generated textures via
Textures.js(noise,cowpatterns fromtooloud, Perlin FBM) with random UV scale/offset per instance. - Floor:
/images/metal2b.jpgfrompublic/images/(Phong, repeated). - Sphere:
MeshBasicMaterialwith dynamicenvMapfrom cube render targets (512³).
SkyShader.js implements the Preetham analytic skylight model. After the sky mesh is added, sky.update() syncs shader uniforms; the named sun spotlight in Scenario.json is positioned from sky.getSunPosition() and targets the reflective sphere.
The main camera uses setViewOffset(fullWidth, fullHeight, x, y, width, height) so one viewport can show a window into a larger virtual canvas — useful for tiled displays or video walls carried over from globex.
rings/
├── index.html # Full-viewport stage + stats.js CDN
├── public/images/ # Static textures (metal2b.jpg, uv.jpg)
├── src/
│ ├── Main.js # Entry: URL params → Rings
│ ├── Rings/
│ │ ├── Rings.js # Scene orchestration & render loop
│ │ ├── Actors.js # Meshes: donut, floor, glowBall, …
│ │ ├── Materials.js # Gloss, mirror, procedural donut mats
│ │ └── Scenario.json # Light (and legacy camera) definitions
│ ├── Shaders/
│ │ └── SkyShader.js # Preetham sky dome
│ └── common/ # Stage, Camera, Lights, Ellipse, Utils, …
├── tests/
│ └── webgl-smoke.mjs # Playwright screenshot smoke test
└── vite.config.js # `tween` → @tweenjs/tween.js compat alias
- Node.js 18+ (20+ recommended)
- A browser with WebGL (Chrome, Firefox, Safari, Edge)
npm install
npm run devOpen http://localhost:5173/.
npm run build # output → dist/
npm run preview # serve dist/ locally| Param | Meaning | Default |
|---|---|---|
fullWidth |
Virtual canvas width | window.innerWidth |
fullHeight |
Virtual canvas height | window.innerHeight |
x |
Viewport offset X | 0 |
y |
Viewport offset Y | 0 |
Example (top-left quarter of a 4K canvas):
http://localhost:5173/?fullWidth=3840&fullHeight=2160&x=0&y=0
Headless WebGL smoke test: loads the app in Chromium, captures a screenshot, and fails if too few non-black pixels (scene did not render).
Start the dev server first, then in another terminal:
npm run test:webglOptional environment variables:
| Variable | Default | Purpose |
|---|---|---|
WEBGL_TEST_URL |
http://localhost:5175/ |
Page URL (set to your dev server port, e.g. 5173) |
WEBGL_MIN_NON_BLACK_RATIO |
0.01 |
Minimum fraction of non-black pixels |
WEBGL_TEST_TIMEOUT_MS |
15000 |
Navigation timeout |
Example:
WEBGL_TEST_URL=http://localhost:5173/ npm run test:webglThe app is a static Vite site. Vercel detects vite build and serves dist/ automatically.
npx vercel # preview
npx vercel --prod # productionConnect the GitHub repo in the Vercel project settings for deploy-on-push.
| Layer | Choice |
|---|---|
| Bundler | Vite 7 |
| 3D | three.js 0.180 |
| Animation | @tweenjs/tween.js |
| Controls | OrbitControls (three/examples) |
| Procedural textures | tooloud, custom Perlin (Noise.js) |
| Smoke test | Playwright + pngjs |
Legacy tween imports are aliased to @tweenjs/tween.js via src/common/tweenCompat.js.
Ideas grounded in the current codebase — good starting points for contributions:
Performance & bundle
- Code-split
Rings.js(main chunk is ~760 KB minified) with dynamic imports or manual chunks. - Lower cube camera resolution on mobile /
prefers-reduced-motion. - Pause cube env updates when the tab is hidden (
document.visibilityState).
Rendering & assets
- Expose Preetham sky parameters (
turbidity,rayleigh,inclination, …) via URL or a small debug GUI. - Restore or document optional skybox paths in
Materials.skyBox()(public/images/skybox/). - Fix
animate()being invoked twice inRingsconstructor (constructor +Main.js).
Code quality
- Remove debug
console.loginanimateObjectsAlongPath. - Use
Scenario.jsoncamera templates or trim unusedlightsX/camerasentries. - Drop redundant
tweennpm package if everything uses the Vite alias. - Align smoke test default port with Vite (
5173).
Features
- Re-enable path visualization (
Actors.tube) behind a?debug=1flag. - Wire
Materials.simplifywireframe mode for shader debugging. - TypeScript types for scene/scenario JSON.
Ops
- GitHub → Vercel integration for continuous deploy.
- CI workflow:
npm run build+npm run test:webglon pull requests.
- Extracted from globex by maggiben.
- Sky shader: Preetham model via Three.js sky example (zz85 et al.); 2026 uniform naming fix in
SkyShader.js. - Atmospheric scattering references: Preetham et al., Simon Wallner, Martin Upitis.
MIT © 2026 Benjamin Maggi