I have written a WebGL program that displays a rotating cube. The indices are declared in counter-clockwise order as viewed from the outside. However, when I decide to enable back-facing polygon culling, the triangles that I understand to be the "front" are culled, and the cube appears to be hollowed-out. I can fix this issue by telling WebGL to cull "front" triangles instead.
I declared the cube's vertex positions, texture coordinates, and indices as follows: image or text.
My question is: am I supposed to declare points in counter-clockwise order as viewed from within the shape? If not, why are the indices that I am using backwards?
A minimum reproducible example is in the code snippet below.
// Vertex positions to make a cube.
const cubeVertexPositions = new Float32Array([
// Front
-1, 1, -1,
-1, -1, -1,
1, -1, -1,
1, 1, -1,
// Back
1, 1, 1,
1, -1, 1,
-1, -1, 1,
-1, 1, 1,
// Left
-1, 1, 1,
-1, -1, 1,
-1, -1, -1,
-1, 1, -1,
// Right
1, 1, -1,
1, -1, -1,
1, -1, 1,
1, 1, 1,
// Top
-1, 1, 1,
-1, 1, -1,
1, 1, -1,
1, 1, 1,
// Bottom
-1, -1, -1,
-1, -1, 1,
1, -1, 1,
1, -1, -1
]);
// Indices for the vertex positions above.
const cubeIndices = new Uint8Array([
// Front
0, 1, 2,
2, 3, 0,
// Back
4, 5, 6,
6, 7, 4,
// Left
8, 9, 10,
10, 11, 8,
// Right
12, 13, 14,
14, 15, 12,
// Top
16, 17, 18,
18, 19, 16,
// Bottom
20, 21, 22,
22, 23, 20
]);
// Vertex shader source code.
const vss = `\
#version 300 es
in vec4 a_position;
uniform mat4 u_matrix;
out vec4 v_color;
void main() {
gl_Position = u_matrix * a_position;
v_color = a_position;
}`;
// Fragment shader source code.
const fss = `\
#version 300 es
precision highp float;
in vec4 v_color;
out vec4 outColor;
void main() {
outColor = v_color;
}`;
// https://umath.lakuna.pw/
function perspective(fov, aspect, near, far, out) {
const f = 1 / Math.tan(fov / 2);
out[0] = f / aspect;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = f;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[11] = -1;
out[12] = 0;
out[13] = 0;
out[15] = 0;
if (far != null && far != Infinity) {
const nf = 1 / (near - far);
out[10] = (far + near) * nf;
out[14] = 2 * far * near * nf;
} else {
out[10] = -1;
out[14] = -2 * near;
}
return out;
}
// https://umath.lakuna.pw/
function rotateY(radians, out) {
const s = Math.sin(radians);
const c = Math.cos(radians);
const a00 = out[0];
const a01 = out[1];
const a02 = out[2];
const a03 = out[3];
const a20 = out[8];
const a21 = out[9];
const a22 = out[10];
const a23 = out[11];
out[0] = a00 * c - a20 * s;
out[1] = a01 * c - a21 * s;
out[2] = a02 * c - a22 * s;
out[3] = a03 * c - a23 * s;
out[8] = a00 * s + a20 * c;
out[9] = a01 * s + a21 * c;
out[10] = a02 * s + a22 * c;
out[11] = a03 * s + a23 * c;
return out;
}
// https://umath.lakuna.pw/
function translate(x, y, z, out) {
out[12] = out[0] * x + out[4] * y + out[8] * z + out[12];
out[13] = out[1] * x + out[5] * y + out[9] * z + out[13];
out[14] = out[2] * x + out[6] * y + out[10] * z + out[14];
out[15] = out[3] * x + out[7] * y + out[11] * z + out[15];
return out;
}
// https://umath.lakuna.pw/
function rotateZ(radians, out) {
const s = Math.sin(radians);
const c = Math.cos(radians);
const a00 = out[0];
const a01 = out[1];
const a02 = out[2];
const a03 = out[3];
const a10 = out[4];
const a11 = out[5];
const a12 = out[6];
const a13 = out[7];
out[0] = a00 * c + a10 * s;
out[1] = a01 * c + a11 * s;
out[2] = a02 * c + a12 * s;
out[3] = a03 * c + a13 * s;
out[4] = a10 * c - a00 * s;
out[5] = a11 * c - a01 * s;
out[6] = a12 * c - a02 * s;
out[7] = a13 * c - a03 * s;
return out;
}
// https://ugl.lakuna.pw/
function makeFullscreenCanvas() {
const canvas = document.createElement("canvas");
canvas.style.touchAction = "none";
document.body = document.createElement("body");
document.body.appendChild(canvas);
const recursiveFullscreen = (element) => {
element.style.width = "100%";
element.style.height = "100%";
element.style.margin = "0px";
element.style.padding = "0px";
element.style.display = "block";
if (element.parentElement) {
recursiveFullscreen(element.parentElement);
}
};
recursiveFullscreen(canvas);
return canvas;
}
// Initialization step.
window.addEventListener("load", () => {
// Get the rendering context.
const canvas = makeFullscreenCanvas();
const gl = canvas.getContext("webgl2");
// Create the vertex shader.
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vss);
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(vs));
}
// Create the fragment shader.
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fss);
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(fs));
}
// Create the shader program.
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
// Vertex position attribute location.
const posLoc = gl.getAttribLocation(program, "a_position");
// Matrix uniform location.
const matLoc = gl.getUniformLocation(program, "u_matrix");
// Vertex position attribute buffer.
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, cubeVertexPositions, gl.STATIC_DRAW);
// Cube vertex array object.
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0);
// Cube index buffer.
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, cubeIndices, gl.STATIC_DRAW);
// Animation properties.
const matrix = new Float32Array(16);
// Constant global values.
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
gl.cullFace(gl.BACK); // Change to `gl.FRONT` to "fix."
gl.useProgram(program);
// Rasterization step.
function render(now) {
requestAnimationFrame(render);
// Resize the canvas.
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width != displayWidth
|| canvas.height != displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
}
// Resize the viewport.
gl.viewport(0, 0, canvas.width, canvas.height);
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Make the transformation matrix.
perspective(45, canvas.width / canvas.height, 1, 10, matrix);
translate(0, 0, -5, matrix);
rotateY(now * 0.001, matrix);
rotateZ(now * 0.0005, matrix);
// Set the transformation matrix.
gl.uniformMatrix4fv(matLoc, false, matrix);
// Rasterize.
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_BYTE, 0);
}
requestAnimationFrame(render);
});
You said "The indices are in counter-clockwise order as viewed from the outside."
No they are not. The indices of the 1st triangle are 0, 1, 2 and the vertices are (-1, 1, -1), (-1, -1, -1), (1, -1, -1).
0
y +
^ | \
| | \
| +-----+
1 2
----> x
In a right handed system (Right-hand rule), the z-axis points against the view. Hence this is a triangle on the back.
When you look at the triangle from the back you can clearly see that the winding order is clock wise
0
+
/ |
/ |
+-----+
2 1