WebGL 1's core has no VAOs; the Khronos-ratified OES_vertex_array_object extension adds them with an OES suffix (createVertexArrayOES(), bindVertexArrayOES() and so on). WebGL 2 promoted it to core, so a WebGL 2 context returns null for it. A thin wrapper gives both the WebGL 2 names:
const gl1 = document.createElement('canvas').getContext('webgl');
const ext = gl1.getExtension('OES_vertex_array_object');
const vaoApi = gl1 instanceof WebGL2RenderingContext ? gl1 : {
createVertexArray: () => ext.createVertexArrayOES(),
bindVertexArray: (vao) => ext.bindVertexArrayOES(vao),
};
const vao = vaoApi.createVertexArray();
vaoApi.bindVertexArray(vao);
gl1.bindBuffer(gl1.ARRAY_BUFFER, gl1.createBuffer());
gl1.enableVertexAttribArray(0); // recorded in vao
vaoApi.bindVertexArray(null);
const enabled = () => gl1.getVertexAttrib(0, gl1.VERTEX_ATTRIB_ARRAY_ENABLED);
console.log('outside:', enabled());
vaoApi.bindVertexArray(vao);
console.log('inside:', enabled());Output
outside: false inside: true
Without the extension, re-issue the pointer and enable calls before each draw: the state a VAO would restore.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
pre { max-width: 600px; margin: 4px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="view" width="600" height="220"></canvas>
<pre id="out"></pre>
<script>
const gl1 = document.getElementById('view').getContext('webgl'); // a WebGL 1 context
const ext = gl1.getExtension('OES_vertex_array_object');
const vaoApi = gl1 instanceof WebGL2RenderingContext ? gl1 : {
createVertexArray: () => ext.createVertexArrayOES(),
bindVertexArray: (vao) => ext.bindVertexArrayOES(vao),
};
// GLSL ES 1.00 shaders: attribute/varying, gl_FragColor.
const program = gl1.createProgram();
for (const [type, src] of [[gl1.VERTEX_SHADER, `attribute vec2 aPos; attribute vec3 aColor;
varying vec3 vColor; uniform float uTime;
void main() { vColor = aColor; gl_Position = vec4(aPos.x, aPos.y + 0.04 * sin(uTime + aPos.x * 6.0), 0.0, 1.0); }`],
[gl1.FRAGMENT_SHADER, `precision mediump float; varying vec3 vColor;
void main() { gl_FragColor = vec4(vColor, 1.0); }`]]) {
const s = gl1.createShader(type);
gl1.shaderSource(s, src); gl1.compileShader(s);
gl1.attachShader(program, s);
}
gl1.bindAttribLocation(program, 0, 'aPos');
gl1.bindAttribLocation(program, 1, 'aColor');
gl1.linkProgram(program);
gl1.useProgram(program);
function mesh(data) { // interleaved x, y, r, g, b recorded in a VAO
const vao = vaoApi.createVertexArray();
vaoApi.bindVertexArray(vao);
gl1.bindBuffer(gl1.ARRAY_BUFFER, gl1.createBuffer());
gl1.bufferData(gl1.ARRAY_BUFFER, new Float32Array(data), gl1.STATIC_DRAW);
gl1.vertexAttribPointer(0, 2, gl1.FLOAT, false, 20, 0);
gl1.vertexAttribPointer(1, 3, gl1.FLOAT, false, 20, 8);
gl1.enableVertexAttribArray(0); // recorded in the VAO
gl1.enableVertexAttribArray(1);
vaoApi.bindVertexArray(null);
return { vao, count: data.length / 5 };
}
const shelf = mesh([-0.9, -0.75, 0.5, 0.36, 0.24, 0.9, -0.75, 0.5, 0.36, 0.24, 0.9, -0.6, 0.6, 0.45, 0.3,
-0.9, -0.75, 0.5, 0.36, 0.24, 0.9, -0.6, 0.6, 0.45, 0.3, -0.9, -0.6, 0.6, 0.45, 0.3]);
const spines = [];
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f', '#1f5f8b', '#e09a10'].forEach((hex, i) => {
const c = [1, 3, 5].map(k => parseInt(hex.slice(k, k + 2), 16) / 255);
const x0 = -0.85 + i * 0.21, x1 = x0 + 0.17, top = 0.35 + (i % 3) * 0.15;
for (const [x, y] of [[x0, -0.6], [x1, -0.6], [x1, top], [x0, -0.6], [x1, top], [x0, top]]) spines.push(x, y, ...c);
});
const books = mesh(spines);
// The book's check: attribute 0 is enabled only while the VAO is bound.
const enabled = () => gl1.getVertexAttrib(0, gl1.VERTEX_ATTRIB_ARRAY_ENABLED);
vaoApi.bindVertexArray(null);
const outside = enabled();
vaoApi.bindVertexArray(books.vao);
document.getElementById('out').textContent =
`${gl1.getParameter(gl1.VERSION)} | extension: ${ext ? 'OES_vertex_array_object' : 'missing'}\n` +
`VERTEX_ATTRIB_ARRAY_ENABLED outside the VAO: ${outside}, inside: ${enabled()}`;
const uTime = gl1.getUniformLocation(program, 'uTime');
function frame(ms) {
gl1.clearColor(0.97, 0.96, 0.93, 1);
gl1.clear(gl1.COLOR_BUFFER_BIT);
gl1.uniform1f(uTime, 0.0);
vaoApi.bindVertexArray(shelf.vao); gl1.drawArrays(gl1.TRIANGLES, 0, shelf.count);
gl1.uniform1f(uTime, ms * 0.003);
vaoApi.bindVertexArray(books.vao); gl1.drawArrays(gl1.TRIANGLES, 0, books.count);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>