Every triangle has two sides, and WebGL tells them apart by winding order, the direction its vertices run once projected to the screen. By default (frontFace(gl.CCW)) counterclockwise means the front face; from behind, the same vertices run clockwise and show the back:

Enabling CULL_FACE (off by default) discards triangles by facing before shading; cullFace() picks BACK (the default), FRONT or FRONT_AND_BACK, and frontFace(gl.CW) suits models wound the other way. Points and lines are never culled, and the fragment shader's gl_FrontFacing says which side it is shading (Two-Sided Lighting). Because winding is measured on screen, a negative scale that mirrors a model turns its front faces into back faces; set frontFace(gl.CW) for that draw. GLH.box() winds counterclockwise seen from outside, like glTF.
<!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; }
.names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="600" height="220"></canvas>
<div class="names"><div>CULL_FACE off</div><div>CULL_FACE, cullFace(BACK)</div><div>CULL_FACE, frontFace(CW)</div></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPosition;
uniform float uAngle;
out vec2 vUV;
void main() {
vUV = aPosition + 0.5;
float c = cos(uAngle), s = sin(uAngle);
vec3 p = vec3(aPosition.x * c, aPosition.y, aPosition.x * s); // spin about y
float w = 1.0 + p.z * 0.6; // a little perspective
gl_Position = vec4(p.x * 1.1, p.y * 1.4, 0.0, w);
gl_PointSize = 9.0;
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; uniform bool uPoints; out vec4 c;
void main() {
if (uPoints) { c = vec4(0.1, 0.1, 0.1, 1.0); return; }
// gl_FrontFacing: true when the triangle runs counterclockwise on screen (with frontFace(CCW)).
vec3 color = gl_FrontFacing ? vec3(0.12, 0.37, 0.55) : vec3(0.88, 0.6, 0.06);
c = vec4(color * (vUV.x < 0.12 ? 0.6 : 1.0), 1.0);
}`]]) {
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(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
// Two triangles wound counterclockwise as seen from the front (+z).
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-0.35, -0.5, 0.35, -0.5, 0.35, 0.5, -0.35, -0.5, 0.35, 0.5, -0.35, 0.5]), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
const uAngle = gl.getUniformLocation(program, 'uAngle');
const uPoints = gl.getUniformLocation(program, 'uPoints');
gl.enable(gl.SCISSOR_TEST);
function frame(ms) {
gl.uniform1f(uAngle, ms * 0.0012);
for (let pane = 0; pane < 3; pane++) {
gl.viewport(pane * 200, 0, 200, 220); gl.scissor(pane * 200 + 2, 0, 196, 220);
gl.clearColor(0.93, 0.91, 0.87, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
if (pane === 0) gl.disable(gl.CULL_FACE); else gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
gl.frontFace(pane === 2 ? gl.CW : gl.CCW); // CW: clockwise now counts as the front
gl.uniform1i(uPoints, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.uniform1i(uPoints, 1);
gl.drawArrays(gl.POINTS, 0, 6); // points are never culled
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>