javascriptwebglwebgl2

Are OpenGL vertices ordered as viewed by an internal or external viewer?


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);
});


Solution

  • 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