Signatur
Beschreibung
Das redraw-Ereignis gehört zur WebXR Layers API. Ein XRCylinderLayer ist eine Kompositionsebene, die auf die Innenseite eines Zylinderausschnitts abgebildet wird. Typische Einsatzfälle sind gebogene Menüs, Video- oder Textpanels in VR. Der XR-Compositor stellt solche Ebenen direkt und mit hoher Qualität dar und projiziert sie bei Kopfbewegungen selbst neu. Deshalb muss die Anwendung den Inhalt nicht in jedem Frame zeichnen.
In zwei Fällen muss die Anwendung neu zeichnen, und dann wird redraw ausgelöst:
- Die zugrunde liegenden Ressourcen (z. B. die Texturen der Swapchain) gehen verloren, etwa nach einem Kontextverlust oder wenn das System Speicher freigibt.
- Der Compositor kann den zuletzt gelieferten Inhalt nicht mehr sinnvoll neu projizieren.
Besonders wichtig ist das Ereignis bei statischen Ebenen (isStatic: true). In so eine Ebene darf nur einmal nach dem Erzeugen und danach nur nach einem redraw gezeichnet werden. Bei dynamischen Ebenen, die ohnehin in jedem Frame neu gezeichnet werden, ist das Ereignis meist unkritisch.
Das Ereignisobjekt ist ein XRLayerEvent. Die Eigenschaft event.layer verweist auf die betroffene Ebene. Das Ereignis wird nicht weitergereicht (bubbelt nicht) und kann nicht abgebrochen werden. Du kannst den Handler mit addEventListener("redraw", …) oder über die Eigenschaft onredraw registrieren. Ergänzend gibt es das Flag layer.needsRedraw, das du im Frame-Loop abfragen kannst.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $type Pflicht | string | Der Ereignisname, hier immer "redraw". |
|
| $handler Pflicht | (event: XRLayerEvent) => void | Callback, der das Ereignis erhält. event.layer ist das XRCylinderLayer, das neu gezeichnet werden muss. |
|
| $options | boolean|AddEventListenerOptions | false | Optionale Listener-Optionen wie once oder signal (zum Entfernen per AbortController). |
Rückgabewert
addEventListener gibt nichts zurück. Der Handler erhält ein XRLayerEvent.Beispiele
Statische Zylinderebene anlegen und bei redraw neu zeichnen
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2', { xrCompatible: true });
const session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['layers']
});
const refSpace = await session.requestReferenceSpace('local');
const xrGlBinding = new XRWebGLBinding(session, gl);
const cylinderLayer = xrGlBinding.createCylinderLayer({
space: refSpace,
viewPixelWidth: 1024,
viewPixelHeight: 512,
radius: 2,
centralAngle: Math.PI / 2,
aspectRatio: 2,
isStatic: true // darf nur nach Erzeugung oder redraw beschrieben werden
});
session.updateRenderState({ layers: [cylinderLayer] });
let pendingRedraw = true; // erstes Zeichnen nach der Erzeugung
cylinderLayer.addEventListener('redraw', (event) => {
console.log('redraw für', event.layer.constructor.name);
pendingRedraw = true;
});
const fb = gl.createFramebuffer();
const onFrame = (time, frame) => {
session.requestAnimationFrame(onFrame);
if (!pendingRedraw) return;
const subImage = xrGlBinding.getSubImage(cylinderLayer, frame);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D, subImage.colorTexture, 0);
const { x, y, width, height } = subImage.viewport;
gl.viewport(x, y, width, height);
gl.clearColor(0.1, 0.3, 0.8, 1.0); // Panel-Inhalt (vereinfacht)
gl.clear(gl.COLOR_BUFFER_BIT);
pendingRedraw = false;
};
session.requestAnimationFrame(onFrame);
Handler über die onredraw-Eigenschaft setzen und wieder entfernen
// cylinderLayer wurde wie oben per xrGlBinding.createCylinderLayer() erzeugt
cylinderLayer.onredraw = (event) => {
console.log('Zylinderebene muss neu gezeichnet werden');
redrawPanel(event.layer);
};
// Später, z. B. beim Beenden der Sitzung:
session.addEventListener('end', () => {
cylinderLayer.onredraw = null;
});
Listener mit AbortController entfernen und needsRedraw prüfen
const controller = new AbortController();
const handleRedraw = () => {
console.log('needsRedraw:', cylinderLayer.needsRedraw);
};
cylinderLayer.addEventListener('redraw', handleRedraw, {
signal: controller.signal
});
// Alternativ im Frame-Loop direkt abfragen:
const onFrame = (time, frame) => {
session.requestAnimationFrame(onFrame);
if (cylinderLayer.needsRedraw) {
// Inhalt neu in getSubImage(...).colorTexture zeichnen
}
};
session.requestAnimationFrame(onFrame);
// Listener entfernen (gleichwertig zu removeEventListener('redraw', handleRedraw))
session.addEventListener('end', () => controller.abort());
// Wichtig · Fallstricke
Kompatibilität: Die WebXR Layers API ist experimentell. Sie wird im Wesentlichen nur in Chromium-basierten XR-Browsern unterstützt, etwa im Meta Quest Browser. Die Sitzung muss mit dem Feature "layers" angefordert werden. Das Feature solltest du immer prüfen (typeof XRWebGLBinding.prototype.createCylinderLayer === "function") und dann auf eine normale Projektionsebene zurückfallen, wenn es fehlt. Die WebXR-Features stehen außerdem nur in sicheren Kontexten (HTTPS) zur Verfügung.
- Statische Ebenen: Wenn du bei
isStatic: trueaufredrawnicht reagierst, bleibt die Ebene leer oder zeigt veralteten Inhalt. Zeichnen außerhalb der erlaubten Zeitpunkte ist nicht vorgesehen. - Zeichne nicht direkt im Handler. Setze dort nur ein Flag und führe das eigentliche Rendering im nächsten
XRSession.requestAnimationFrame()-Callback aus, denn nur dort steht ein gültigesXRFramefürgetSubImage()zur Verfügung. - Nach einem WebGL-Kontextverlust musst du auch eigene GPU-Ressourcen (Texturen, Shader, Buffer) neu anlegen, bevor du neu zeichnest.
- Das Ereignis bubbelt nicht und kann nicht abgebrochen werden.
preventDefault()hat also keine Wirkung.