Start · Sprachen · JavaScript · Referenz · GPURenderPassEncoder.endOcclusionQuery()

GPURenderPassEncoder.endOcclusionQuery()

Methode

Beendet die aktive Okklusionsabfrage, die zuvor mit <code>beginOcclusionQuery()</code> im selben Render-Pass gestartet wurde.

seit JavaScript WebGPU – Chrome/Edge 113+, Firefox 141+ (Windows), Kategorie: webapi

Signatur

GPURenderPassEncoder.prototype.endOcclusionQuery()

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

Typ
undefined
Beschreibung
Kein Rückgabewert. Das Ergebnis wird in das 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'}`);
});
Objekt 0: sichtbar Objekt 1: verdeckt Objekt 2: sichtbar

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 einen GPUValidationError (abfangbar über device.pushErrorScope('validation') oder das Event uncapturederror).
  • Abfragen können nicht verschachtelt werden: Pro Render-Pass darf immer nur eine Okklusionsabfrage gleichzeitig aktiv sein. Vor dem nächsten beginOcclusionQuery() muss endOcclusionQuery() aufgerufen werden, ebenso vor pass.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 GPURenderBundleEncoder gibt es diese Methode nicht; Render-Bundles, die per executeBundles() 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.