Queries

Query Objects and Conditional Rendering

A query object asks the GPU about the draws between beginQuery() and endQuery(). Occlusion queries (ANY_SAMPLES_PASSED and its cheaper _CONSERVATIVE form) report whether any fragment passed the depth test, which tells you whether a detailed object hidden behind its cheap proxy box is worth drawing. Answers come back through the same poll-per-frame pattern as Pixel Buffer Objects's fence:

The shape of an occlusion query, polled once a frameJavaScript
const query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, query);
drawProxyBox(book);                                   // depth test on, color writes off
gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
// in a later frame:
if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
  book.visible = gl.getQueryParameter(query, gl.QUERY_RESULT) === 1;
}

A test page drew a wall, then one quad behind it and one in front under separate queries: they returned 0 and 1 (numbers, not booleans), 3 to 6 frames later. The specification forbids a result in the task that asked, so you decide with answers a few frames old. OpenGL's conditional rendering, where the GPU skips a draw on a query's result by itself, is not in WebGL 2 (gl.beginConditionalRender is undefined). Profiling and Debugging uses the other query type, GPU timers.

Occlusion queries on six books behind a sliding panel: each proxy's ANY_SAMPLES_PASSED answer arrives a few frames laterHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  #status { display: flex; gap: 6px; max-width: 600px; margin-top: 4px; font: 11px monospace; }
  #status div { flex: 1; padding: 4px; text-align: center; border-radius: 4px; color: #fff; }
</style>
<canvas id="c" width="600" height="240"></canvas>
<div id="status"></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform vec3 uBox;                                               // x, y, depth of a quad
uniform vec2 uSize;
void main() {
  vec2 p = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0 - 1.0;
  gl_Position = vec4(uBox.xy + p * uSize, uBox.z, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; out vec4 c; void main() { c = vec4(uColor, 1.0); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const u = (n) => gl.getUniformLocation(program, n);
function quad([x, y, z], [w, h], color) {
  gl.uniform3f(u('uBox'), x, y, z); gl.uniform2f(u('uSize'), w, h); gl.uniform3fv(u('uColor'), color);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
const colors = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
const books = colors.map((color, i) => ({ color, x: -0.78 + i * 0.31, query: null, visible: null, age: 0 }));
const status = document.getElementById('status');
status.innerHTML = books.map((_, i) => `<div id="s${i}">book ${i + 1}</div>`).join('');
gl.enable(gl.DEPTH_TEST);
function frame(ms) {
  gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  const panelX = 1.1 * Math.sin(ms * 0.0006);
  quad([panelX, 0, -0.5], [0.55, 0.8], [0.54, 0.38, 0.25]);      // the occluder: a wooden panel in front
  for (const b of books) {
    // Collect an old answer, if it has arrived: never in the frame that asked.
    if (b.query && gl.getQueryParameter(b.query, gl.QUERY_RESULT_AVAILABLE)) {
      b.visible = gl.getQueryParameter(b.query, gl.QUERY_RESULT) === 1;
      gl.deleteQuery(b.query); b.query = null;
      const el = document.getElementById('s' + books.indexOf(b));
      el.style.background = b.visible ? '#3f7d3a' : '#b5452f';
      el.textContent = `book ${books.indexOf(b) + 1}: ${b.visible ? 'visible' : 'hidden'} (${b.age} fr)`;
    }
    if (!b.query) {                                                // ask again with the cheap proxy, colour writes off
      b.query = gl.createQuery(); b.age = 0;
      gl.colorMask(false, false, false, false); gl.depthMask(false);
      gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, b.query);
      quad([b.x, 0, 0.2], [0.12, 0.6], [0, 0, 0]);
      gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
      gl.colorMask(true, true, true, true); gl.depthMask(true);
    } else b.age++;
    // Draw the "detailed" book only if the last answer said it can be seen.
    if (b.visible !== false) {
      quad([b.x, 0, 0.2], [0.12, 0.6], b.color);
      quad([b.x - 0.1, 0, 0.19], [0.02, 0.6], b.color.map((c) => c * 0.6));   // spine shade
    }
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>