@realitycollective/webxr-environment
The engine-free core of the Reality Collective WebXR Environment Extensions: the platform features that make up the world around the player, and the playback of sound.
You do not install this package directly. Install the adapter for the engine you already use - @realitycollective/threejs-environment or @realitycollective/iwsdk-environment - and it re-exports everything here.
What it is
Two directors and two ports.
EnvironmentDirectorowns a sky (gradient, solid colour or an authored image), fog, an ambient light, a key light and an environment map for image-based lighting, described as plain data. It interpolates between named presets, pushes only what changed to the adapter, and suppresses the sky and fog while passthrough is showing.EnvironmentDirectoralso owns the two sensor-backed features.setOcclusion(spec)asks for real-world depth and reaches the port only while passthrough is on;setLightEstimation(true)lets the host's measurement of the room take over the ambient, key and ibl slots while it is measuring, as a layer over what the app asked for rather than an edit to it.setPassthroughtakes a WebXR blend mode as well as a boolean, becauseadditivedisplays add what you draw to the real world and a dark fog is then invisible.- The sensing seam. A port reports back through
observe(host):unsupported,unavailable,pendingoractiveper feature, each with a sentence for a human, plus measured lighting. Read it withgetSensing(feature)andonSensing(listener). It exists because a sensor that is doing nothing looks exactly like a sensor that is working, and the rule that keeps it honest is that a report never changes what the app asked for - only what the app can be told. WorldSensingDirectoris the other half, and deliberately a separate object: it asks for planes, meshes and anchors, keeps the registries, runs standing hit tests, and emits what appeared, moved and went away. It reports what the host measured and builds nothing - the mesh you draw on a detected wall is yours.- Sound has a direction as well as a distance. A cue's
spatial.conesays how narrowly it points and a play'sfacingsays which way this one is turned, in radians and as a travel direction - the package's own terms, converted by each adapter to the degrees and the +Z forward its host happens to want. AudioDirectorowns a cue registry, a bus mix, the retrigger policy and the voices. The adapter is handed a resolved absolute gain and told to make a noise.
Every slot is a platform facility each host exposes differently - three.js has scene.background and Fog, IWSDK has DomeGradient and AmbientLightComponent, the next host will have something else. Presenting one description all of them can be driven from is the whole job.
Neither owns a loop. update(deltaMs) is called by whatever already runs per frame, which is what makes an eight-second dusk a five-line unit test rather than a stopwatch and a headset.
What it is not
- Not a session or capability layer. It never reads
navigator.xr. Passthrough arrives throughsetPassthrough(boolean), pushed in by whatever already tracks it - on this stack, the service framework. - Not content. No geometry, no meshes, no prefabs, no placement, no floors. If a thing could be built by the app out of a geometry and a material, it does not belong here. Art direction is the app's too: the stock presets are examples to copy, not an opinion about how your world should look.
- Not an event source. It plays sounds when asked; it has no notion of why.
One name to keep straight: @realitycollective/service-framework exports an EnvironmentDescriptor, which means the PLATFORM environment the app is running in - a name plus a set of capability strings. The EnvironmentSpec here means the VISUAL environment: the sky, the fog and the light. Unrelated concepts, and an app can hold both.
The architecture test asserts the first of those and the dependency rule behind them: this package has no runtime dependencies at all, and no sibling package is imported, named in a type, or asserted against in a test.
Playing a sound when something happens
play(cueId, options?) is the whole inbound surface, and the app does the binding:
const stop = someEmitter.on("thing", () => audio.play("click"));
That is the integration, in the app, where both halves are already in scope. Swap the emitter for an interaction event, a level change, a socket message - the line looks the same, and nothing here knows which it was. If the source is present it gets bound; if it is not, nothing happens.
A caller's gain is relative: the bus and master gains still apply over it, so whatever is bound is a peer and never an owner of the player's mix.
Specs are partial
director.apply({ fog: null }); // clears the fog, touches nothing else
director.apply({ sky: DUSK.sky }); // changes the sky, keeps the lighting
An omitted slot inherits; an explicit null turns the slot off. That is what makes presets composable.
The one interpolation rule
A slot interpolates only when both ends describe the same kind of thing. A slot appearing, a slot disappearing, or a linear fog becoming exponential takes the target value at t = 0 and holds it.
There is no honest halfway point between "fog" and "no fog". To ease fog in, make both ends fogs:
director.apply({ fog: clearedFog(DUSK.fog!) }); // present, but invisible
director.transition({ fog: DUSK.fog! }, { durationMs: 4000 }); // rolls in
Licence
MIT. Part of the Reality Collective WebXR stack.
Classes
| Class | Description |
|---|---|
| AudioDirector | - |
| EnvironmentDirector | - |
| 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. |
| 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. |
| 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. |
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. |
| 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. |
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. |
| 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 |
|---|---|
| clamp | Clamp to an arbitrary range. |
| clamp01 | Clamp to the unit interval. |
| clearedFog | The same fog, pushed far enough away to be invisible. |
| deepEquals | Structural equality for the plain-data specs. |
| 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. |
| 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. |
| 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? |