@realitycollective/threejs-environment
The three.js adapter for the Reality Collective WebXR Environment Extensions. Re-exports the engine-free @realitycollective/webxr-environment core, so this is the only package you install.
npm install @realitycollective/threejs-environment three
Use
import {
createThreeAudio,
createThreeEnvironment,
STOCK_PRESETS,
} from "@realitycollective/threejs-environment";
const { director } = createThreeEnvironment(scene, {
presets: STOCK_PRESETS,
initial: STOCK_PRESETS.noon,
defaultTransition: { durationMs: 4000, easing: "easeInOut" },
});
const listener = new AudioListener();
camera.add(listener);
const { director: audio, port: audioPort } = createThreeAudio(listener, {
cues: [{ id: "hum", src: "/audio/hum.mp3", bus: "ambience", loop: true }],
});
renderer.setAnimationLoop(() => {
const deltaMs = clock.getDelta() * 1000;
director.update(deltaMs); // neither director ticks itself
audio.update(deltaMs);
renderer.render(scene, camera);
});
director.transition("dusk");
What the adapter does
- Sky - a solid colour becomes
scene.background; a gradient becomes a two-pixel-wide equirectangularDataTexture, regenerated in place across a transition rather than reallocated per frame. No shader, no canvas, and the ramp itself is a pure function (skyMix,gradientPixels) so it is unit tested with no renderer involved. - Fog -
FogandFogExp2, mutated in place while the kind is unchanged. - Lights - an
AmbientLightand aDirectionalLight, positioned from the direction light travels ([0, -1, 0]is overhead). - Image-based lighting -
scene.environment,environmentIntensityandenvironmentRotation. A gradient reuses the generated ramp; an image is loaded through the injectable loader (pass anRGBELoaderfor HDR);kind: "room"uses three.js'sRoomEnvironmentwhen you supply aprefilter, and a neutral ramp when you do not, because prefiltering needs a renderer this port deliberately does not hold. - Depth occlusion - three.js's OWN depth sensing, which has drawn an occlusion mesh before the scene since r158. Pass
rendererin the options; the port then decides whether the request can be served (a session,depth-sensingenabled, gpu-optimized depth, a scope three.js can honour) and reports the reason when it cannot. There is no hand-rolled depth pass here on purpose: duplicating the renderer's GL state handling is how caches start lying. - Light estimation - a WebXR light probe, read from the current
XRFrameand converted to ordinary ambient and key specs. NotXREstimatedLight, so the estimate is data the app can read, blend or ignore rather than two lights in aGrouponly the renderer can see. Pass areflectionhook - two lines aroundXRWebGLBinding.getReflectionCubeMap- and the measured cube map arrives asibl: { kind: "estimated" }too; without one, ambient and key still work and the report says the reflections were not measured. - The room -
createThreeWorldSensing(renderer)reads planes, meshes, anchors and hit-test results straight from theXRFrame, in the reference space three.js draws in, so a pose can be handed toobject.position.set(...)unchanged. A mesh is measured once perlastChangedTime, because the alternative is walking a room scan every frame. A hit test withspace: "left"or"right"is cast from that hand's owntargetRaySpace- it waits while there is no such input source and unbinds when the hand goes - and every hit carries the distance from where the ray started. - Audio -
Audio/PositionalAudioover the listener's Web Audio context, with per-cue reference distance, rolloff, maximum distance, distance model and cone. A cue with aconeand a play with afacingbecomesetDirectionalConeplus a turned holder: radians in the contract, degrees at the panner, +Z pointed the way the sound travels.
Everything the port creates is parented under one named Group (port.root), and dispose() gives all of it back. It creates no geometry - every one of those is a three.js facility with no app-side equivalent. A floor is a mesh, so a floor is yours; demos/playground builds its own in four lines.
Two things worth knowing
The first press is not silent. A play that arrives before its buffer has decoded is held and started when the decode lands, unless it was stopped in the meantime. Dropping it instead is the reason the first press of every button in a session so often makes no sound.
Autoplay. Browsers refuse to start an AudioContext outside a user gesture. Call audioPort.resume() from the same handler that enters XR (or from your Enter-VR button) - a suspended context makes every voice silently succeed.
Peer dependency
three >= 0.170.0. Meta's super-three fork satisfies this and is what the workspace develops against.
Licence
MIT.
Classes
| Class | Description |
|---|---|
| AudioDirector | - |
| EnvironmentDirector | - |
| ThreeAudioPort | Plays sounds. |
| ThreeEnvironmentPort | Applies an environment to a scene. |
| ThreeWorldSensingPort | What an adapter implements. Everything except observe is optional, because every host has a different subset and a missing method is reported rather than thrown. |
| WorldSensingDirector | - |
Interfaces
| Interface | Description |
|---|---|
| AmbientLightSpec | Uniform illumination from every direction. |
| AudioCone | A directional sound, in this package's own terms. |
| AudioCue | A sound the app knows how to make. Registered once, played by id. |
| AudioDirectorOptions | - |
| AudioPort | Plays sounds. |
| AudioSpatial | How a positional voice gets quieter with distance. |
| AudioVoice | A sounding voice. Opaque to the app apart from the fields shown. |
| AudioVoiceRequest | A play request with every decision already made: which file, how loud in absolute terms, looping or not, and where from. An adapter implementing AudioPort has no mixing left to do. |
| EnvironmentDirectorOptions | - |
| EnvironmentPort | Applies an environment to a scene. |
| EnvironmentPortHost | What a port is handed so it can talk back. Implemented by the director. |
| EnvironmentSpec | A PARTIAL description of the environment. |
| EquirectLoader | Anything that can turn a URL into a texture: TextureLoader, RGBELoader. |
| EstimatedLighting | Lighting the host measured from the real world. |
| FogExponential | Fog that thickens exponentially with distance. |
| FogLinear | Fog that ramps between two distances. |
| HitTestRequest | A standing question: "where does this ray meet the room". |
| IblEstimated | The reflections the HOST measured from the real room. |
| IblGradient | Image-based lighting: what the world reflects and how it lights everything that is not lit by the key light. |
| IblRoom | The host's built-in neutral room probe. Neither authored nor measured. |
| IblTexture | @realitycollective/webxr-environment - the engine-free core. |
| KeyLightSpec | The one directional light most scenes need: a sun, a moon, a work lamp. |
| LightEstimationSpec | Which slots a live estimate may take over. |
| OcclusionSource | How the app would like the depth sensor configured. |
| OcclusionSpec | - |
| PlayOptions | Per-play overrides. Everything here beats the cue's own value. |
| ResolvedEnvironment | Every slot decided. This is what the director holds and adapters receive. |
| ResolvedLightEstimation | spec with every default filled in, which is what a port receives. |
| ResolvedWorldDetection | - |
| SensingReport | - |
| SkyGradient | A vertical gradient sky. top sits at the zenith and bottom at the nadir; horizon is where they meet, as a fraction of the way up the sphere. |
| SkySolid | A single flat colour behind everything. The cheapest sky there is. |
| SkyTexture | An authored sky: an equirectangular image wrapped around the world. |
| ThreeAudioPortOptions | - |
| ThreeAudioSetup | - |
| ThreeEnvironmentPortOptions | - |
| ThreeEnvironmentSetup | - |
| ThreeWorldSensingPortOptions | - |
| ThreeWorldSensingSetup | - |
| TransitionOptions | - |
| WorldAnchor | A point in the real world the runtime promises to keep track of. |
| WorldChange | What moved since the last flush, per feature. Ids only; read the lists. |
| WorldDetectionSpec | Which of them to turn on. Everything defaults OFF: each one costs. |
| WorldHit | Where a ray met the world. |
| WorldMesh | Scanned geometry: furniture, walls, whatever the room-scan produced. |
| WorldPlane | A flat surface the host found: a floor, a wall, a table top. |
| WorldPose | Where something is, in whatever reference space the app is using. |
| WorldSensingDirectorOptions | - |
| WorldSensingPort | What an adapter implements. Everything except observe is optional, because every host has a different subset and a missing method is reported rather than thrown. |
| WorldSensingPortHost | What a port is handed so it can report what the room looks like. |
| XrAnchorLike | One XRAnchor. |
| XrFrameLike | - |
| XrHitTestResultLike | - |
| XrLightEstimateLike | One XRLightEstimate. The three products WebXR's light estimation makes. |
| XrManagerLike | The part of renderer.xr this port uses. |
| XrMeshLike | One XRMesh. The buffers are the runtime's and are never copied here. |
| XrPlaneLike | One XRPlane. polygon points are in the plane's own space, metres. |
| XrPoseLike | What frame.getPose hands back: a transform, or null when tracking is lost. |
| XrRendererLike | A WebGLRenderer, seen through the keyhole this port looks through. |
| XrSessionLike | - |
Type Aliases
| Type Alias | Description |
|---|---|
| AudioBus | A mix group. Any string works; the conventional set is below. |
| CuePolicy | What a second play request does while a cue is already sounding. |
| EasingFunction | t in 0..1 -> eased t in 0..1. |
| EasingName | The named easing curves. A transition may also carry its own function. |
| EnvironmentBlendMode | How the host composites what is rendered over the real world. |
| EnvironmentListener | Fired whenever what the port holds, or what the app asked for, changes. |
| EnvironmentSlot | @realitycollective/webxr-environment - the engine-free core. |
| FogSpec | @realitycollective/webxr-environment - the engine-free core. |
| IblSpec | @realitycollective/webxr-environment - the engine-free core. |
| OcclusionMode | How hard the edge between real and virtual is drawn. |
| OcclusionScope | Which content the real world is allowed to hide. |
| Quat | A rotation as [x, y, z, w]. Normalised; the adapters guarantee that. |
| RampGradient | A gradient with or without a horizon stop, from either slot. |
| Rgb | An sRGB colour, one channel per component, each in 0..1. |
| SensingFeature | - |
| SensingListener | - |
| SensingState | How a feature is doing, in the order things usually go wrong: |
| SkySpec | @realitycollective/webxr-environment - the engine-free core. |
| Vec3 | A direction or position in world space, metres. Need not be normalised. |
| WorldChangeListener | - |
| WorldFeature | - |
| WorldPlaneOrientation | Roughly which way a surface faces. |
| XrLightProbeLike | An XRLightProbe. Opaque: it is only ever handed back to getLightEstimate. |
Variables
| Variable | Description |
|---|---|
| ALL_WORLD_DETECTION | Everything on: what setDetection(true) means. |
| DAWN | - |
| DEFAULT_BUS | The bus a cue lands on when it does not name one. |
| DEFAULT_BUSES | The buses a director starts with, each at unity gain. Any other name works too - a bus springs into existence at unity the first time it is named. master is NOT in here: it is a separate scalar over all buses, so that "duck everything" and "turn the music down" never fight over one number. |
| DEFAULT_LIGHT_ESTIMATION | - |
| DEFAULT_OCCLUSION | Soft edges over everything, from a GPU texture. |
| DUSK | - |
| EMPTY_ENVIRONMENT | An environment with nothing in it: a black void with no light. |
| ENVIRONMENT_SLOTS | The slot names, in the order the director pushes them to the port. |
| NIGHT | - |
| NOON | - |
| OVERCAST | - |
| SENSING_FEATURES | The sensor-backed features an adapter can report on. |
| SH_DC_TO_IRRADIANCE | The L0 band of a spherical-harmonic probe, scaled to irradiance. |
| SKY_TEXTURE_HEIGHT | Default rows. Sixty-four is smooth to the eye and 512 bytes of texture. |
| SKY_TEXTURE_WIDTH | Columns in the generated texture. Two, because a 1px-wide equirect filters badly. |
| STOCK_PRESETS | All of the above, ready to hand to EnvironmentDirector's presets. |
| VOID | Nothing at all: black, unlit, empty. The starting point of a void scene. |
| WORLD_FEATURES | The world features an app can ask for. They are named separately from the environment's because a host can have any subset: Quest has planes, meshes and anchors; a phone AR runtime may have only planes and hit test. |
Functions
| Function | Description |
|---|---|
| ambientFromSphericalHarmonics | The ambient term of an estimate, or null when the runtime sent no probe. |
| clamp | Clamp to an arbitrary range. |
| clamp01 | Clamp to the unit interval. |
| clearedFog | The same fog, pushed far enough away to be invisible. |
| createSkyTexture | Build the equirectangular background texture for a gradient sky. |
| createThreeAudio | Wire an audio director to a three.js AudioListener (the one on your camera). Remember port.resume() from a user gesture. |
| createThreeEnvironment | Wire an environment director to a three.js scene. |
| createThreeWorldSensing | Wire a world-sensing director to a three.js renderer. |
| deepEquals | Structural equality for the plain-data specs. |
| gradientColourAt | The colour at a height up the sphere. |
| gradientPixels | RGBA bytes for the gradient, nadir row first. Pure - no three.js, no DOM. |
| interpolateEnvironment | Blend two resolved environments. t is the EASED progress in 0..1; the director has already applied the curve, so this stays linear per channel. |
| keyFromEstimate | The primary light of an estimate, or null when there is not one. |
| lerp | Linear interpolation. t is NOT clamped - the callers already clamp it. |
| lerpRgb | Component-wise colour interpolation. |
| resolveEasing | Resolve an easing name or function to a function. Unknown names are linear. |
| resolveLightEstimation | - |
| resolveWorldDetection | - |
| rgbEquals | True when two colours are identical channel for channel. |
| rgbFromHex | 0xff8800 -> [1, 0.533…, 0]. Convenience for authoring presets. |
| rgbToHex | [1, 0.533…, 0] -> 0xff8800. The inverse, for adapters that want a hex. |
| skyMix | The blend weight for bottom -> top at a given height up the sphere. |
| toEstimatedLighting | One estimate, as the slots it is allowed to take over. |
| unsupported | The report a feature has before any adapter has said otherwise. |
| worldEntryChanged | Has this entry changed since the last one with the same id? |