Signatur
Beschreibung
endOcclusionQuery() schließt eine mit GPURenderPassEncoder.beginOcclusionQuery(queryIndex) geöffnete Okklusionsabfrage ab. Alle Zeichenbefehle zwischen Beginn und Ende werden darauf geprüft, ob mindestens ein Fragment (Sample) den Tiefen- und Stencil-Test bestanden hat. Das Ergebnis landet als 64-Bit-Ganzzahl im GPUQuerySet, das beim Anlegen des Render-Passes über occlusionQuerySet übergeben wurde. Ein Wert ungleich 0 bedeutet „sichtbar“, 0 bedeutet „vollständig verdeckt“.
Typische Anwendung ist das Occlusion Culling: Man zeichnet zunächst eine einfache Bounding-Box eines teuren Objekts (oft ohne Farb- und Tiefenschreiben) innerhalb einer Abfrage und entscheidet in einem späteren Frame anhand des Ergebnisses, ob das eigentliche Modell überhaupt gerendert werden muss. Auch für Lens-Flare-Effekte oder Sichtbarkeitstests von Lichtquellen ist das nützlich.
Die Ergebnisse sind nicht direkt in JavaScript lesbar. Sie müssen mit GPUCommandEncoder.resolveQuerySet() in einen GPUBuffer kopiert und anschließend über einen Mapping-fähigen Buffer mit mapAsync() ausgelesen werden. Das geschieht asynchron, daher werden die Werte in der Praxis meist erst ein oder mehrere Frames später ausgewertet.
Rückgabewert
occlusionQuerySet des Render-Passes geschrieben.Beispiele
Grundgerüst: Abfrage um einen Zeichenbefehl legen
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
// QuerySet mit Platz für 1 Okklusionsabfrage
const querySet = device.createQuerySet({ type: 'occlusion', count: 1 });
// ... pipeline, colorView, depthView, vertexBuffer wurden zuvor erstellt
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{ view: colorView, loadOp: 'clear', storeOp: 'store' }],
depthStencilAttachment: {
view: depthView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store'
},
occlusionQuerySet: querySet
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.beginOcclusionQuery(0);
pass.draw(36); // z. B. Bounding-Box
pass.endOcclusionQuery();
pass.end();
device.queue.submit([encoder.finish()]);
Ergebnisse auflösen und auslesen
const count = 3;
const querySet = device.createQuerySet({ type: 'occlusion', count });
// Buffer für resolveQuerySet (8 Byte je Ergebnis)
const resolveBuffer = device.createBuffer({
size: count * 8,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
});
const readBuffer = device.createBuffer({
size: count * 8,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ ...passDescriptor, occlusionQuerySet: querySet });
pass.setPipeline(pipeline);
objects.forEach((obj, i) => {
pass.setVertexBuffer(0, obj.boxBuffer);
pass.beginOcclusionQuery(i);
pass.draw(obj.vertexCount);
pass.endOcclusionQuery();
});
pass.end();
encoder.resolveQuerySet(querySet, 0, count, resolveBuffer, 0);
encoder.copyBufferToBuffer(resolveBuffer, 0, readBuffer, 0, count * 8);
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
const results = new BigUint64Array(readBuffer.getMappedRange().slice(0));
readBuffer.unmap();
results.forEach((samples, i) => {
console.log(`Objekt ${i}: ${samples > 0n ? 'sichtbar' : 'verdeckt'}`);
});
Occlusion Culling über mehrere Frames
// Sichtbarkeit aus dem letzten Frame merken
const visible = new Array(objects.length).fill(true);
const renderFrame = (pass) => {
objects.forEach((obj, i) => {
// Test-Geometrie immer abfragen
pass.setPipeline(boxPipeline); // ohne Farbschreiben
pass.setVertexBuffer(0, obj.boxBuffer);
pass.beginOcclusionQuery(i);
pass.draw(36);
pass.endOcclusionQuery();
// Teures Modell nur zeichnen, wenn im Vorframe sichtbar
if (visible[i]) {
pass.setPipeline(meshPipeline);
pass.setVertexBuffer(0, obj.meshBuffer);
pass.draw(obj.meshVertexCount);
}
});
};
// Nach dem Auslesen (siehe vorheriges Beispiel):
const updateVisibility = (results) => {
results.forEach((s, i) => { visible[i] = s > 0n; });
};
// Wichtig · Fallstricke
- Validierungsfehler: Der Aufruf ist ungültig, wenn keine Okklusionsabfrage aktiv ist. Der Fehler wird nicht sofort geworfen, sondern macht den Encoder ungültig;
finish()bzw.submit()erzeugt dann einenGPUValidationError(abfangbar überdevice.pushErrorScope('validation')oder das Eventuncapturederror). - Abfragen können nicht verschachtelt werden: Pro Render-Pass darf immer nur eine Okklusionsabfrage gleichzeitig aktiv sein. Vor dem nächsten
beginOcclusionQuery()mussendOcclusionQuery()aufgerufen werden, ebenso vorpass.end(). - Jeder Query-Index darf innerhalb eines Render-Passes nur einmal verwendet werden.
- Der Ergebniswert ist nur als „0 oder nicht 0“ zuverlässig – die genaue Sample-Anzahl ist implementierungsabhängig und nicht garantiert.
- Innerhalb eines
GPURenderBundleEncodergibt es diese Methode nicht; Render-Bundles, die perexecuteBundles()während einer aktiven Abfrage ausgeführt werden, zählen aber mit. - Ohne Tiefen-/Stencil-Anhang ist das Ergebnis wenig aussagekräftig, da dann jedes gerasterte Fragment als sichtbar gilt.
- Kompatibilität: WebGPU ist nur in sicheren Kontexten (HTTPS) verfügbar und noch nicht in allen Browsern/Plattformen aktiviert. Vorher per
if (!navigator.gpu)prüfen.