A fragment shader may declare several outputs, each tagged layout(location = i). drawBuffers() maps output i to a color attachment, and in WebGL 2 entry i must be COLOR_ATTACHMENTi or NONE, which is why GLH.targets() attaches in order:
console.log('MAX_DRAW_BUFFERS', gl.getParameter(gl.MAX_DRAW_BUFFERS),
'MAX_COLOR_ATTACHMENTS', gl.getParameter(gl.MAX_COLOR_ATTACHMENTS));
const g = GLH.targets(gl, 1920, 1080, [gl.RGBA8, gl.RGBA16F, gl.RGBA16F]);
gl.bindFramebuffer(gl.FRAMEBUFFER, g.fbo);
gl.drawBuffers([gl.COLOR_ATTACHMENT1]); // output 0 may only go to attachment 0
console.log('error:', gl.getError() === gl.INVALID_OPERATION ? 'INVALID_OPERATION' : 'none');Output
MAX_DRAW_BUFFERS 8 MAX_COLOR_ATTACHMENTS 8 WebGL: INVALID_OPERATION: drawBuffers: COLOR_ATTACHMENTi_EXT or NONE error: INVALID_OPERATION
The GTX 1650 and SwiftShader 2,546 both report 8; WebGL 2 guarantees 4. At 1920 x 1080 this G-buffer and its depth take 1920 x 1080 x 24 bytes, 47.5 MiB. All targets share one size; clearBufferfv(gl.COLOR, i, color) clears one. WebGL 1 needed WEBGL_draw_buffers and gl_FragData[i].
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; font-family: monospace; font-size: 10px; }
.names div { flex: 1; padding: 2px; }
p { max-width: 600px; margin: 4px 0; font-family: monospace; }
</style>
<canvas id="c" width="600" height="220"></canvas>
<div class="names"><div>location 0 -> COLOR_ATTACHMENT0</div><div>location 1 -> COLOR_ATTACHMENT1</div><div>location 2 -> COLOR_ATTACHMENT2</div></div>
<p id="out"></p>
<script>
const gl = document.getElementById('c').getContext('webgl2');
function program(vs, fs) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(p, s);
}
gl.linkProgram(p);
return p;
}
const quadVS = `#version 300 es
out vec2 vUV;
void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`;
// A fragment shader with three outputs: the cover, its "height" and its uv coordinates.
const writer = program(quadVS, `#version 300 es
precision highp float; in vec2 vUV; uniform float uTime;
layout(location = 0) out vec4 outCover;
layout(location = 1) out vec4 outMask;
layout(location = 2) out vec4 outUV;
void main() {
vec2 p = vUV * 2.0 - 1.0;
float ring = smoothstep(0.03, 0.0, abs(length(p - vec2(0.0, 0.25)) - 0.35 - 0.05 * sin(uTime)));
float band = step(abs(p.y + 0.45), 0.08);
outCover = vec4(mix(vec3(0.12, 0.37, 0.55), vec3(0.95, 0.9, 0.8), max(ring, band)), 1.0);
outMask = vec4(vec3(max(ring, band)), 1.0);
outUV = vec4(vUV, 0.0, 1.0);
}`);
const viewer = program(quadVS, `#version 300 es
precision mediump float; in vec2 vUV; uniform sampler2D uTex; out vec4 c; void main() { c = texture(uTex, vUV); }`);
gl.bindVertexArray(gl.createVertexArray());
const limits = `MAX_DRAW_BUFFERS ${gl.getParameter(gl.MAX_DRAW_BUFFERS)}, MAX_COLOR_ATTACHMENTS ${gl.getParameter(gl.MAX_COLOR_ATTACHMENTS)}`;
const S = 180, fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
const targets = [0, 1, 2].map((i) => { // attached in order: attachment i for output i
const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, S, S);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, t, 0);
return t;
});
// The WebGL 2 rule: entry i of drawBuffers must be COLOR_ATTACHMENTi or NONE.
gl.drawBuffers([gl.COLOR_ATTACHMENT1]);
const ruleError = gl.getError() === gl.INVALID_OPERATION ? 'INVALID_OPERATION' : 'none';
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
document.getElementById('out').textContent = `${limits} | drawBuffers([COLOR_ATTACHMENT1]) -> ${ruleError}`;
function frame(ms) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0, 0, S, S);
gl.useProgram(writer);
gl.uniform1f(gl.getUniformLocation(writer, 'uTime'), ms * 0.002);
gl.drawArrays(gl.TRIANGLES, 0, 3); // one draw fills all three targets
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(viewer);
targets.forEach((t, i) => {
gl.bindTexture(gl.TEXTURE_2D, t);
gl.viewport(i * 200 + 10, 20, S, S);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>