Use /environment with a host-provided room scope to change the local user's
background. Query getCapabilities().localKinds for the integrating host's
supported background kinds; the SDK schema alone does not install a renderer. Lighting, reflection/environment maps, fog, and room geometry
remain unchanged. Other participants do not receive these local changes.
import { getCapabilities, setLocalOverride, onChanged } from "@urth/metatell-sdk/environment";
import { waitUntilReady } from "@urth/metatell-sdk/runtime";
import type { PluginRuntimeContext } from "@urth/metatell-sdk/lifecycle";
async function showSky(sdk: PluginRuntimeContext) {
const scope = sdk.getRoomScope();
const capabilities = await getCapabilities({ scope });
if (!capabilities.localKinds.includes("procedural-sky")) return;
await waitUntilReady("environment", { scope });
return setLocalOverride({
mode: "custom",
background: {
kind: "procedural-sky",
parameters: { azimuth: 0.495 },
lighting: "preserve",
},
}, { scope });
}
function observeView(sdk: PluginRuntimeContext) {
return onChanged((state) => {
console.log(state.local, state.effective, state.render);
}, { scope: sdk.getRoomScope() });
}
Handle rejection at your application boundary. observeView returns the UI's
unsubscribe function: call it when that view closes, without disposing the
plugin's room scope. The background continues until the owner explicitly releases
it or the host invalidates that room/plugin lifetime. On a room change, subscribe
using the new scope from sdk.onRoomChanged rather than reusing the old one.
custom or scene-default receives CONFLICT; there is no automatic takeover.custom loads and applies a background. scene-default displays the current
scene default while retaining the owner's slot. follow-room releases only
the calling scope's slot; a non-owner cannot clear another owner's background.follow-room displays the newest accepted shared snapshot, or the scene default
if no shared background exists. A personal custom or scene-default slot
remains unchanged while shared snapshots update behind it.capabilities.shared. Older servers
continue reporting false; installing a local renderer does not enable sharing.
Never use the host-local scene counter as an authoritative backend revision.ABORTED or STALE_SCENE and cannot clear new owners.await setLocalOverride({ mode: "scene-default" }, { scope });
await setLocalOverride({ mode: "follow-room" }, { scope });
scope above is the current PluginRoomScope.
getState({ scope }) and onChanged(callback, { scope }) return copied,
owner-relative snapshots. local.requested and local.ownsSlot describe the
calling owner, while effective describes the currently applied visual.
render.status is loading, ready, or failed, with a fixed error code.
Here ready means preparation and local scene activation succeeded; it is not
an acknowledgement of the first GPU frame, context recovery, or another client.
Other owners do not receive the holder's private owner ID or requested override.
The effective background is room-local visible state, not secret data.
Preparation leaves the preceding visual displayed. Only a successful preparation
and activation replace it. Failure preserves the preceding visual and reports
ASSET_FAILED; the owner retains its slot to retry or release it. A newer request
by the same owner cancels older preparation. A cancelled or timed-out preparation
cannot later activate, but cancellation does not undo an already accepted visual.
Cancellation is not a rollback guarantee for accepted server writes.
getCapabilities is available once the driver is installed, including before
connection/readiness. State reads and acquisitions require a connected, ready
room. Release remains available while disconnected or loading. An accepted local
background stays visible during a temporary disconnect in the same room.
onChanged coalesces state reads and rejects old epochs/revisions; use getState
when an explicit error is needed, since observer read failures are not callbacks.
Errors include UNSUPPORTED, NOT_READY, DISCONNECTED, CONFLICT, ABORTED,
TIMEOUT, INVALID_ARGUMENT, STALE_SCENE, ASSET_FAILED, and INTERNAL.
An older host rejects immediately with UNSUPPORTED. Request timeoutMs defaults
to 5000; override it explicitly for larger image loads. No automatic retry occurs.
import { getState, updateShared } from "@urth/metatell-sdk/environment";
const current = await getState({ scope });
if (current.shared.supported && current.shared.snapshot) {
const accepted = await updateShared({
requestId: crypto.randomUUID(),
expectedSceneRevision: current.shared.snapshot.sceneRevision,
expectedRevision: current.shared.snapshot.revision,
background: { kind: "color", color: "#223344", lighting: "preserve" },
}, { scope });
console.log(accepted.revision); // Server acceptance, not render completion.
}
The authenticated server checks current update_hub permission and atomically
compares the explicit server scene/revision with persisted room state. A scene
mismatch rejects with STALE_SCENE; a competing write rejects with CONFLICT.
Pass background: null to clear the durable room setting. Scope disposal releases
only personal resources; it never deletes the server's persisted setting.
requestId is caller-owned, separate from the SDK's RPC correlation ID. The server
stores an idempotency receipt keyed by room, authenticated actor and request ID.
An explicit retry must use the same request ID and identical payload, including
expected revisions, to retrieve that acceptance. Reusing it for different content
returns CONFLICT; current authorization is checked even for a replay. A replay
may return an older accepted snapshot while the current room has advanced. The
SDK returns that ACK without rolling current shared state back. There are no
automatic mutation retries. If a write times out or is aborted, it may already
have committed; inspect a fresh snapshot and apply the server's receipt policy
before deciding to retry.
Request IDs are 1–128 UTF-8 bytes, expected scene revisions 1–256 UTF-8 bytes; NUL and invalid Unicode are rejected. Revisions are nonnegative safe integers. Shared URLs are normalized first, then restricted to the backend-compatible credentialless HTTPS subset. HTTP localhost exceptions are personal only.
state.shared.snapshot is null until the host binds a fresh server snapshot to
the currently rendered scene, including after reconnect. During that interval,
updateShared rejects with NOT_READY. Server scene revisions are opaque IDs,
independent from scope.room.sceneRevision. Older/out-of-order snapshots and
assets finishing after scene invalidation cannot replace newer visuals.
A successful updateShared returns the server's SharedEnvironmentSnapshot as
soon as it is acknowledged; rendering may still be loading or failed. Observe
state.render separately. A personal override can intentionally hide the shared
setting. Render failures retain the preceding visual without rolling back the
durable server setting. This SDK contract does not prove deployment, database
migration, another client's display, or delivery of a first GPU frame.
All kinds require lighting: "preserve". from-background is unsupported.
| Kind | Fields and units |
|---|---|
color |
color: "#RRGGBB" in sRGB. No named colors or alpha. |
equirectangular |
Absolute url, projection: "equirectangular", colorSpace: "srgb"; raster image with exactly 2:1 dimensions. |
procedural-sky |
Optional parameters use the defaults and ranges below. Values are finite; out-of-range inputs are rejected, not clamped. |
model |
URL and position/rotation/positive scale vectors. Position uses host world units; rotation uses XYZ Euler angles in radians. Requires model in localKinds; hosts without that renderer reject it as UNSUPPORTED. Asset format and complexity limits depend on the installed host driver. |
| Sky parameter | Default | Inclusive range |
|---|---|---|
| turbidity | 10 | 0 to 20 |
| rayleigh | 2 | 0 to 4 |
| luminance | 1 | 0 to 2 |
| mieCoefficient | 0.005 | 0 to 0.1 |
| mieDirectionalG | 0.8 | 0 to 0.999 |
| inclination | 0 | -0.5 to 0.5 |
| azimuth | 0.15 | 0 to 1 |
| distance | 8000 | 1 to 10000 world units (sky cube edge length) |
The normalized angle parameters follow the existing shader convention:
theta = pi * (inclination - 0.5), phi = 2*pi * (azimuth - 0.5);
the sun direction is normalized (cos(phi), sin(phi)*sin(theta), sin(phi)*cos(theta)).
They are not geographic latitude/longitude or degrees. The SDK-owned mesh uses
the specified distance without the existing A-Frame component's internal clamp.
The original component is unchanged.
URLs must use HTTPS and contain no credentials; HTTP is accepted only for
localhost or 127.0.0.1 development. The URL limit is 4096 characters.
data:, blob:, file:, and SVG are rejected. Images must have PNG/JPEG/WebP
MIME and matching byte signatures, be accessible by CORS, and require no host
credentials. Fetch uses credentials: "omit"; the SDK never attaches user tokens.
Use a suitable public or explicitly signed asset URL.
The driver streams at most 16 MiB of compressed bytes and checks image dimensions
before creating the GPU texture. The maximum is 8192x4096, further bounded by the
renderer texture limit; without that information it uses 4096x2048. Query the
actual advertised limits. These are implementation bounds, not a claim of
performance across every supported device. Browser decoding allocates image
memory before the decoded-size check; this is not an untrusted-image sandbox.
DEFAULT_SKY_PARAMETERS and DEFAULT_ENVIRONMENT_LIMITS expose the defaults
above. The copy… and validateEnvironmentRequest functions are the validators
the SDK uses at the bus boundary; host integrations can reuse them. They throw
SdkError on invalid input.
The host hides only existing skybox meshes while an override is active, preserving their associated lighting. Procedural sky uses the pure shader object without A-Frame light-probe/ambient generation. Only owned textures, ImageBitmaps, materials, and cloned geometry are disposed. Scene-owned resources are retained. Scene invalidation releases the old visual before the next default is applied.
This API uses the trusted same-window plugin bus. Ownership supports predictable cleanup; it is not isolation from arbitrary JavaScript on the host page.