Skip to main content

@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.

  • EnvironmentDirector owns 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.
  • EnvironmentDirector also 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. setPassthrough takes a WebXR blend mode as well as a boolean, because additive displays 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, pending or active per feature, each with a sentence for a human, plus measured lighting. Read it with getSensing(feature) and onSensing(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.
  • WorldSensingDirector is 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.cone says how narrowly it points and a play's facing says 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.
  • AudioDirector owns 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 through setPassthrough(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

ClassDescription
AudioDirector-
EnvironmentDirector-
WorldSensingDirector-

Interfaces

InterfaceDescription
AmbientLightSpecUniform illumination from every direction.
AudioConeA directional sound, in this package's own terms.
AudioCueA sound the app knows how to make. Registered once, played by id.
AudioDirectorOptions-
AudioPortPlays sounds.
AudioSpatialHow a positional voice gets quieter with distance.
AudioVoiceA sounding voice. Opaque to the app apart from the fields shown.
AudioVoiceRequestA 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-
EnvironmentPortApplies an environment to a scene.
EnvironmentPortHostWhat a port is handed so it can talk back. Implemented by the director.
EnvironmentSpecA PARTIAL description of the environment.
EstimatedLightingLighting the host measured from the real world.
FogExponentialFog that thickens exponentially with distance.
FogLinearFog that ramps between two distances.
HitTestRequestA standing question: "where does this ray meet the room".
IblEstimatedThe reflections the HOST measured from the real room.
IblGradientImage-based lighting: what the world reflects and how it lights everything that is not lit by the key light.
IblRoomThe host's built-in neutral room probe. Neither authored nor measured.
IblTexture@realitycollective/webxr-environment - the engine-free core.
KeyLightSpecThe one directional light most scenes need: a sun, a moon, a work lamp.
LightEstimationSpecWhich slots a live estimate may take over.
OcclusionSourceHow the app would like the depth sensor configured.
OcclusionSpec-
PlayOptionsPer-play overrides. Everything here beats the cue's own value.
ResolvedEnvironmentEvery slot decided. This is what the director holds and adapters receive.
ResolvedLightEstimationspec with every default filled in, which is what a port receives.
ResolvedWorldDetection-
SensingReport-
SkyGradientA 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.
SkySolidA single flat colour behind everything. The cheapest sky there is.
SkyTextureAn authored sky: an equirectangular image wrapped around the world.
TransitionOptions-
WorldAnchorA point in the real world the runtime promises to keep track of.
WorldChangeWhat moved since the last flush, per feature. Ids only; read the lists.
WorldDetectionSpecWhich of them to turn on. Everything defaults OFF: each one costs.
WorldHitWhere a ray met the world.
WorldMeshScanned geometry: furniture, walls, whatever the room-scan produced.
WorldPlaneA flat surface the host found: a floor, a wall, a table top.
WorldPoseWhere something is, in whatever reference space the app is using.
WorldSensingDirectorOptions-
WorldSensingPortWhat an adapter implements. Everything except observe is optional, because every host has a different subset and a missing method is reported rather than thrown.
WorldSensingPortHostWhat a port is handed so it can report what the room looks like.

Type Aliases

Type AliasDescription
AudioBusA mix group. Any string works; the conventional set is below.
CuePolicyWhat a second play request does while a cue is already sounding.
EasingFunctiont in 0..1 -> eased t in 0..1.
EasingNameThe named easing curves. A transition may also carry its own function.
EnvironmentBlendModeHow the host composites what is rendered over the real world.
EnvironmentListenerFired 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.
OcclusionModeHow hard the edge between real and virtual is drawn.
OcclusionScopeWhich content the real world is allowed to hide.
QuatA rotation as [x, y, z, w]. Normalised; the adapters guarantee that.
RgbAn sRGB colour, one channel per component, each in 0..1.
SensingFeature-
SensingListener-
SensingStateHow a feature is doing, in the order things usually go wrong:
SkySpec@realitycollective/webxr-environment - the engine-free core.
Vec3A direction or position in world space, metres. Need not be normalised.
WorldChangeListener-
WorldFeature-
WorldPlaneOrientationRoughly which way a surface faces.

Variables

VariableDescription
ALL_WORLD_DETECTIONEverything on: what setDetection(true) means.
DAWN-
DEFAULT_BUSThe bus a cue lands on when it does not name one.
DEFAULT_BUSESThe 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_OCCLUSIONSoft edges over everything, from a GPU texture.
DUSK-
EMPTY_ENVIRONMENTAn environment with nothing in it: a black void with no light.
ENVIRONMENT_SLOTSThe slot names, in the order the director pushes them to the port.
NIGHT-
NOON-
OVERCAST-
SENSING_FEATURESThe sensor-backed features an adapter can report on.
STOCK_PRESETSAll of the above, ready to hand to EnvironmentDirector's presets.
VOIDNothing at all: black, unlit, empty. The starting point of a void scene.
WORLD_FEATURESThe 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

FunctionDescription
clampClamp to an arbitrary range.
clamp01Clamp to the unit interval.
clearedFogThe same fog, pushed far enough away to be invisible.
deepEqualsStructural equality for the plain-data specs.
interpolateEnvironmentBlend two resolved environments. t is the EASED progress in 0..1; the director has already applied the curve, so this stays linear per channel.
lerpLinear interpolation. t is NOT clamped - the callers already clamp it.
lerpRgbComponent-wise colour interpolation.
resolveEasingResolve an easing name or function to a function. Unknown names are linear.
resolveLightEstimation-
resolveWorldDetection-
rgbEqualsTrue when two colours are identical channel for channel.
rgbFromHex0xff8800 -> [1, 0.533…, 0]. Convenience for authoring presets.
rgbToHex[1, 0.533…, 0] -> 0xff8800. The inverse, for adapters that want a hex.
unsupportedThe report a feature has before any adapter has said otherwise.
worldEntryChangedHas this entry changed since the last one with the same id?