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:
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.
<!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>