GravityLens
A starfield that warps around black holes, as real gravity bends light.
What it does
GravityLens: {} on the world entity draws a nebula-and-stars sky and, each frame, bends it around each GravityMass marked lenses. Drag a black hole and the stars behind it smear into arcs that follow it. Raise its mass and the arcs close into a ring, with a thin bright ring hugging the hole's black shadow.
The bend happens in the sky's own shader, not in a post-process: it turns each pixel back into a direction, bends that around each hole, then looks up the sky there. A post-process would warp the rocks too; this bends only what lies behind them. The same shader draws the stars from the bent direction, so they stay sharp points where stars baked into a texture would turn into squares.
A hole's bend is its Einstein angle, θE, which grows as √(mass ÷ distance from the camera):
A pixel at angle θ from a hole shows the sky from angle β = θ − θE²/θ. Inside θE, β goes below zero: that light came from the far side of the hole, so a second, flipped image hugs the shadow. A white hole's mass is below zero too, which flips the bend: it pushes stars out of a disc, with no shadow and no ring.
Setup
The engine work ships with the component, in adapter/register.ts. A project made with bjs create installs it for you: src/main.ts calls every adapter/register.ts that bjs download puts in the project, before the scene loads. If you boot the game from your own code, call it once, right after building the adapter:
Without it there is no sky and the System warns once.
Use it in a scene
A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.
Build it from scratch with the bjs CLI:
- 1Scaffold a project
World, renderer, and dev server — ready to run.
- 2Install dependencies
- 3Add GravityLens
Copies its source into src/ so the scene resolves.
First time? Run bjs login once.
- 4Paste the scene into src/scenes/arcade-room.ts and run
SceneLoader builds the world from the JSON; reload to rebuild.
This bare white hole needs lenses: true. The flag defaults to absorbs, which is true for black holes and false for rocks and white holes. The WhiteHole and BlackHole components set it for you.
Props
size(number, default512) — face size of the sky cube, in texels.seed(number, default0x5ca1e) — seeds the nebula noise.starCount(number, default0) — stars baked into the sky texture. They turn square when magnified, so they suit only a wide camera.starDensity(number, default1) — how many shader stars. More stars show the bend better.starBrightness(number, default1) — how bright the shader stars are.starSeed(number, default0xbadc0de) — seeds the baked stars.galaxyCount(number, default26) /galaxySeed(default0xfeed5).nebulaScale(number, default1.4) — nebula noise frequency. Higher means smaller clouds.densityThreshold(number, default0.42) — nebula thinner than this reads as empty space.intensity(number, default0.7) — peak nebula brightness, 0 to 1.skyboxSize(number, default2000) — world size of the sky cube.strength(number, default0.022) — scales every Einstein angle. Raise it to make the bend read from further away.ringIntensity(number, default0.9) — brightness of the ring at the shadow's edge.ringColor(RGB, default[1, 0.78, 0.52]) — colour of that ring.
Events
camera.active.changed(listens) — the camera to measure from. Until one arrives, the sky is unbent.
Emits nothing.
Dependencies
GravityMass— each hole's mass, radius andlensesflag. The radius sizes the shadow.MeshPrimitive— where each hole is. A mass with no mesh is skipped.NebulaSky— its generator paints the sky. No NebulaSky entity is needed.- A camera that sends
camera.active.changed, such asAxisViewCamera.
Notes
- At most 8 holes reach the shader, ranked by how far they bend, white holes included. A heavy far hole can outrank a light near one.
- Only
strengthandringIntensityare read each frame. The rest are read once, when the sky is built. skyboxSizemust fit inside the camera's far plane. The default 2000 fits AxisViewCamera's 4000.- The plugin is Babylon only. Under Three.js or Babylon Lite there is no sky.












































