2
votes

Starting from this lesson here: WebGL 3D Perspective I am triyng to implement Back-Face culling no magic.

I am computing on the fly the face normals in object space. After that, I am setting the fudgeFactor inside m[2][3] to get the perspective divide by Z.

To check if the shear matrix works, I expanded my snippet using directly the vertex positions projected out of the WebGL vertex shader, and added the "projected position" flag.

Now, I am triyng to use the z-component inside the transformed normals to hide the faces with normal.z <0. This technique works well using orthogonal projection - or when fudgeFactor is 0. Why this doesn't work also for perspective projection, when I set the fudgeFactor by my self?

To see what's happen, I wrote the minimal example below to visualize the normal vectors (thanks to: Geeks3D) and colored it. Green normal: face is visible, red normal: face is culled.

I followed the hints of gman and implemented the basic polygon rasterization (out of the WebGL vertex shader) as a proof of concept . Try to drag the fudgeFactor slider and check that works correctly.

'use strict';

function main() {
  function setupUI() {
    webglLessonsUI.setupUI(document.getElementById('ui'), settings, [
      { type: 'checkbox', key: 'projectedP', name: 'projected position', change: draw },
      { type: 'option', key: 'visible', options: settings.visibleOptions, name: 'visibility buffer', change: draw },
      { type: 'slider', key: 'fudgeFactor', change: draw, max: 2, step: 0.001, precision: 3 },
      { type: 'slider', key: 'tX', change: draw, min: -0.5 * gl.canvas.width, max: 0.5 * gl.canvas.width },
      { type: 'slider', key: 'tY', change: draw, min: -0.5 * gl.canvas.height, max: 0.5 * gl.canvas.height },
      { type: 'slider', key: 'tZ', change: draw, min: -gl.canvas.height, max: gl.canvas.height },
      { type: 'slider', key: 'rX', change: draw, max: 360 },
      { type: 'slider', key: 'rY', change: draw, max: 360 },
      { type: 'slider', key: 'rZ', change: draw, max: 360 },
      { type: 'slider', key: 'scale', change: draw, min: -5, max: 5, step: 0.01, precision: 2 },
    ]);
  }

  function draw() {
    // assignZToWMatrix
    perspMatrix[11] = settings.fudgeFactor;
    var w = gl.canvas.clientWidth, h = gl.canvas.clientHeight, d = 400;
    var projMatrix = m4.multiply(perspMatrix, m4.projector(w, h, d));
    var worldMatrix = m4.translation(settings.tX, settings.tY, settings.tZ);
    m4.xRotate(worldMatrix, (settings.rX * Math.PI) / 180, worldMatrix);
    m4.yRotate(worldMatrix, (settings.rY * Math.PI) / 180, worldMatrix);
    m4.zRotate(worldMatrix, (settings.rZ * Math.PI) / 180, worldMatrix);
    m4.scale(worldMatrix, settings.scale, settings.scale, settings.scale, worldMatrix);
    var matrix = m4.multiply(projMatrix, worldMatrix);
    
    // set the visibility flag using polygon area
    transformVertices(matrix);
    if(settings.visible == 1) {
      // try to set the visibility flag using an early check
      transformNormals(matrix);
    }
    
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.enable(gl.CULL_FACE);
    gl.enable(gl.DEPTH_TEST);

    gl.useProgram(solidInfo.program);
    gl.bindBuffer(gl.ARRAY_BUFFER, solidBufferInfo.attribs.a_projectedP.buffer);
    gl.bufferData(gl.ARRAY_BUFFER, projectedVertices, gl.STATIC_DRAW);
    webglUtils.setBuffersAndAttributes(gl, solidInfo, solidBufferInfo);
    webglUtils.setUniforms(solidInfo, { u_matrix: matrix,
                                        u_projectedP: [settings.projectedP],
                                        u_fudgeFactor: settings.fudgeFactor });

    var count = 32 * 3; // <= geometries x points-each
    webglUtils.drawBufferInfo(gl, solidBufferInfo, gl.TRIANGLES);

    gl.useProgram(normalInfo.program);
    gl.bindBuffer(gl.ARRAY_BUFFER, normalBufferInfo.attribs.a_visible.buffer);
    gl.bufferData(gl.ARRAY_BUFFER, visibility, gl.STATIC_DRAW);
    webglUtils.setBuffersAndAttributes(gl, normalInfo, normalBufferInfo);
    webglUtils.setUniforms(normalInfo, { u_matrix: matrix, 
                                         u_visible: [settings.visible] });
    webglUtils.drawBufferInfo(gl, normalBufferInfo, gl.LINES);
  }

  function calcFaceNormals(size) {
    var v = vertices, l = v.length;
    var nl = normalLines, n = normals;
    var i = 0, j = 0, k = 0;

    while (i < l) {
      var x0 = v[i++]; var y0 = v[i++]; var z0 = v[i++];
      var x1 = v[i++]; var y1 = v[i++]; var z1 = v[i++];
      var x2 = v[i++]; var y2 = v[i++]; var z2 = v[i++];

      var dx1 = x1 - x0; var dy1 = y1 - y0; var dz1 = z1 - z0;
      var dx2 = x2 - x0; var dy2 = y2 - y0; var dz2 = z2 - z0;

      var nx = dy1*dz2-dz1*dy2; var ny = dz1*dx2-dx1*dz2; var nz = dx1*dy2-dy1*dx2;

      // Normalize
      var len = Math.sqrt(nx * nx + ny * ny + nz * nz);
      nx /= len; ny /= len; nz /= len;

      // Center of the geometry
      var cx = (x0+x1+x2)/3; var cy = (y0+y1+y2)/3; var cz = (z0+z1+z2)/3;

      // Vector start point
      nl[j++] = cx; nl[j++] = cy; nl[j++] = cz;

      // Normal vector
      n[k++] = nx; n[k++] = ny; n[k++] = nz;

      // Assign a drawing length (scale) & end-point (translate)
      nl[j++] = nx*size+cx; nl[j++] = ny*size+cy; nl[j++] = nz*size+cz;
      
      // doublette needed by WebGL for single vertex processing
      n[k++] = nx; n[k++] = ny; n[k++] = nz;
    }
  }

  function transformVertices(m) {
    // same as m4.transformPoint
    function transformVertex(m,src,dst) {
      var x = src[0], y = src[1], z = src[2];
      var d = x * m[3] + y * m[7] + z * m[11] + m[15];
      dst[0] = (x * m[0] + y * m[4] + z * m[8]  + m[12]) / d;
      dst[1] = (x * m[1] + y * m[5] + z * m[9]  + m[13]) / d;
      dst[2] = (x * m[2] + y * m[6] + z * m[10] + m[14]) / d;
      return dst;
    }  

    // here we already have fudgeFactor inside m[11]
    var mv = vertices, l = mv.length, pv = projectedVertices;
    var mp0 = [0,0,0], mp1 = [0,0,0], mp2 = [0,0,0];
    var pp0 = [0,0,0], pp1 = [0,0,0], pp2 = [0,0,0];
    
    // "visible" flag inside the vertex shader visibility buffer
    var vb = visibility, j = 0;

    for(var i=0; i<l; i+=9) {
      mp0[0] = mv[i    ]; mp0[1] = mv[i + 1]; mp0[2] = mv[i + 2];
      mp1[0] = mv[i + 3]; mp1[1] = mv[i + 4]; mp1[2] = mv[i + 5];
      mp2[0] = mv[i + 6]; mp2[1] = mv[i + 7]; mp2[2] = mv[i + 8];
      // Project vertex coords
      pp0 = transformVertex(m,mp0,pp0);
      pp1 = transformVertex(m,mp1,pp1);
      pp2 = transformVertex(m,mp2,pp2);
      // Assign 
      pv[i    ] = pp0[0]; pv[i + 1] = pp0[1]; pv[i + 2] = pp0[2];
      pv[i + 3] = pp1[0]; pv[i + 4] = pp1[1]; pv[i + 5] = pp1[2]; 
      pv[i + 6] = pp2[0]; pv[i + 7] = pp2[1]; pv[i + 8] = pp2[2];

     // we need just only the sign, no need to divde as in 3.5.1
     var da = (pp0[0]*pp1[1] - pp1[0]*pp0[1]) + 
              (pp1[0]*pp2[1] - pp2[0]*pp1[1]) + 
              (pp2[0]*pp0[1] - pp0[0]*pp2[1]);
        
     // swap the sign because we inverted the Y-axis inside the projector matrix
     vb[j++] = -da;
     // doublette needed by WebGL for single vertex processing
     vb[j++] = -da;
    }
  }

  function transformNormals(m) {
    var n = normals, l = n.length;
    var x, y, z, w, tx, ty, tz, tw;
    var vb = visibility, j = 0; // Visibility buffer
    
    var t = transformedNormals, k = 0; // debug
    for(var i=0; i<l; i+=6) {
      // normal buffer doublettes are needed for WebGL single
      //  vertex processing, here we take just only one entry
      x = n[i]; y = n[i + 1]; z = n[i + 2]; w = 1;

      // fudgeFactor is inside m[11] - any way to shear normal?
      tx = x * m[0] + y * m[4] + z * m[8]  + w * m[12];
      ty = x * m[1] + y * m[5] + z * m[9]  + w * m[13];
      tz = x * m[2] + y * m[6] + z * m[10] + w * m[14];
      tw = x * m[3] + y * m[7] + z * m[11] + w * m[15]; 
      
      var idx =~ ~i/6; // triangle index
      // debug: index 21 & 22 are the top side of the middle rung
      // debug: keep the transformed vector to check what's happen
      t[k++] = tx; t[k++] = ty; t[k++] = tz; t[k++] = tw;

      // set the visibility flag
      vb[j++] = tz;
      // doublette for WebGL single vertex processing
      vb[j++] = tz;
    }
  }
  
  var canvas = document.querySelector('#canvas'),
    gl = canvas.getContext('webgl');
  var perspMatrix = m4.identity(), drawSize = 10;

  var settings = {
    visibleOptions:  [ '', 'T. NORMAL', 'POLY AREA' ],
    visible: 1,
    projectedP: true,
    fudgeFactor: 1.5,
    tX: -54, tY: -67, tZ: 0, 
    rX: 12, rY: 33, rZ: 8,
    scale: 1
  };

  var solidArrays = {
    position: { numComponents: 3, data: vertices },
    projectedP: { numComponents: 3, data: projectedVertices },
    color: { numComponents: 3, data: vertexColors }
  };

  var solidInfo = webglUtils.createProgramInfo(gl, ['vs-solid', 'fs-solid']);
  var solidBufferInfo = webglUtils.createBufferInfoFromArrays(gl, solidArrays);

  var normalArrays = {
    position: { numComponents: 3, data: normalLines },
    visible: { numComponents: 1, data: visibility },
    normal: { numComponents: 3, data: normals }
  };

  calcFaceNormals(drawSize);

  var normalInfo = webglUtils.createProgramInfo(gl, ['vs-normal', 'fs-normal']);
  var normalBufferInfo = webglUtils.createBufferInfoFromArrays(gl, normalArrays);

  draw();

  setupUI();
}

m4.projector = function (width, height, depth) {
  // Note: set 0,0 at canvas center
  return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
    0, 0, 0, 1,
  ];
};

var vertices = new Float32Array([
  // left column front
  0,0,0,0,150,0,30,0,0,
  0,150,0,30,150,0,30,0,0,
  // top rung front
  30,0,0,30,30,0,100,0,0,
  30,30,0,100,30,0,100,0,0,
  // middle rung front
  30,60,0,30,90,0,67,60,0,
  30,90,0,67,90,0,67,60,0,
  // left column back
  0,0,30,30,0,30,0,150,30,
  0,150,30,30,0,30,30,150,30,
  // top rung back
  30,0,30,100,0,30,30,30,30,
  30,30,30,100,0,30,100,30,30,
  // middle rung back
  30,60,30,67,60,30,30,90,30,
  30,90,30,67,60,30,67,90,30,
  // top
  0,0,0,100,0,0,100,0,30,
  0,0,0,100,0,30,0,0,30,
  // top rung right
  100,0,0,100,30,0,100,30,30,
  100,0,0,100,30,30,100,0,30,
  // under top rung
  30,30,0,30,30,30,100,30,30,
  30,30,0,100,30,30,100,30,0,
  // between top rung and middle
  30,30,0,30,60,30,30,30,30,
  30,30,0,30,60,0,30,60,30,
  // top of middle rung
  30,60,0,67,60,30,30,60,30,
  30,60,0,67,60,0,67,60,30,
  // right of middle rung
  67,60,0,67,90,30,67,60,30,
  67,60,0,67,90,0,67,90,30,
  // bottom of middle rung.
  30,90,0,30,90,30,67,90,30,
  30,90,0,67,90,30,67,90,0,
  // right of bottom
  30,90,0,30,150,30,30,90,30,
  30,90,0,30,150,0,30,150,30,
  // bottom
  0,150,0,0,150,30,30,150,30,
  0,150,0,30,150,30,30,150,0,
  // left side
  0,0,0,0,0,30,0,150,30,
  0,0,0,0,150,30,0,150,0
]);

var vertexColors = new Uint8Array([
  // left column front
  100,35,60,100,35,60,100,35,60,
  200,70,120,200,70,120,200,70,120,
  // top rung front
  100,35,60,100,35,60,100,35,60,
  200,70,120,200,70,120,200,70,120,
  // middle rung front
  100,35,60,100,35,60,100,35,60,
  200,70,120,200,70,120,200,70,120,
  // left column back
  40,35,100,40,35,100,40,35,100,
  80,70,200,80,70,200,80,70,200,
  // top rung back
  40,35,100,40,35,100,40,35,100,
  80,70,200,80,70,200,80,70,200,
  // middle rung back
  40,35,100,40,35,100,40,35,100,
  80,70,200,80,70,200,80,70,200,
  // top
  35,100,105,35,100,105,35,100,105,
  70,200,210,70,200,210,70,200,210,
  // top rung right
  100,100,35,100,100,35,100,100,35,
  200,200,70,200,200,70,200,200,70,
  // under top rung
  105,50,35,105,50,35,105,50,35,
  210,100,70,210,100,70,210,100,70,
  // between top rung and middle
  105,80,35,105,80,35,105,80,35,
  210,160,70,210,160,70,210,160,70,
  // top of middle rung
  35,90,105,35,90,105,35,90,105,
  70,180,210,70,180,210,70,180,210,
  // right of middle rung
  50,35,105,50,35,105,50,35,105,
  100,70,210,100,70,210,100,70,210,
  // bottom of middle rung.
  38,105,50,38,105,50,38,105,50,
  76,210,100,76,210,100,76,210,100,
  // right of bottom
  70,105,40,70,105,40,70,105,40,
  140,210,80,140,210,80,140,210,80,
  // bottom
  45,65,55,45,65,55,45,65,55,
  90,130,110,90,130,110,90,130,110,
  // left side
  80,80,110,80,80,110,80,80,110,
  160,160,220,160,160,220,160,160,220
]);

// projection buffer: same as vertices
var projectedVertices = new Float32Array(vertices.length);
// normal buffer: 16 faces 2 triangles each, total 32 triangles => 128 elements + 128 doublettes => 64 components x 4 floats
var normalLines = new Float32Array((2 * vertices.length) / 3);
var normals = new Float32Array(2 * vertices.length / 3);
// visible buffer: 16 faces 2 triangles each, total 32 triangles => 64 components x 1 float
var visibility = new Float32Array(2 * vertices.length / 9);
// debug: normal transformation buffer has 4 components, to check also how the w is transformed
// 16 faces 2 triangles each, total 32 normals x 4 floats
var transformedNormals = new Float32Array(vertices.length / 9 * 4);

document.addEventListener('DOMContentLoaded', function (event) {
  setTimeout(main,100)
});
<!DOCTYPE html>
<html>
  <head>
    <link href="https://webglfundamentals.org/webgl/resources/webgl-tutorials.css" type="text/css" rel="stylesheet"/>
    <script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
    <script src="https://webglfundamentals.org/webgl/resources/m4.js"></script>
    <script src="https://webglfundamentals.org/webgl/resources/webgl-lessons-ui.js"></script>
  </head>
  <body style="background-color: #fff;">
    <canvas id="canvas"></canvas>
    <div id="uiContainer">
      <div id="ui"></div>
    </div>

    <script id="vs-solid" type="x-shader/x-vertex">
      attribute vec3 a_position;
      attribute vec3 a_projectedP;
      attribute vec3 a_color;
      uniform bool u_projectedP;
      uniform mat4 u_matrix;
      uniform float u_fudgeFactor;
      varying vec4 v_color;
      void main() {
        vec4 pos;
        float zToDivideBy;
        if(u_projectedP) {
          pos = vec4(a_projectedP, 1.0);
          zToDivideBy = 1.0;
        } else {
          pos = u_matrix * vec4(a_position, 1.0);
          zToDivideBy = 1.0 + pos.z * u_fudgeFactor;
        }
        gl_Position = vec4(pos.xyz, zToDivideBy);
        v_color = vec4(a_color, 1.0);
      }
    </script>

    <script id="fs-solid" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_color;
      void main() {
         gl_FragColor = v_color;
      }
    </script>

    <script id="vs-normal" type="x-shader/x-vertex">
      attribute vec3 a_position;
      attribute vec3 a_normal;
      attribute float a_visible;
      uniform int u_visible;
      uniform mat4 u_matrix;
      varying vec4 v_color;
      void main() {
        // The x,y,z,w value we assign to gl_Position in our
        // vertex shader will be divided by w automatically.
        gl_Position = u_matrix * vec4(a_position, 1.0);
        float eps = -0.000001;
        bool vis;
        vec4 nrm = u_matrix * vec4(a_normal, 0.0);
        if(u_visible == 0) {
          // this is obviously wrong for fudgeFactor > 0 
          vis = nrm.z < eps;
        } else {
          // option 1: transformed normal.z
          // option 2: sign of polygon area
          vis = a_visible < eps;
        }
        // visible => green, hidden => red 
        v_color = vis ? vec4(0., 255., 0., 1.) : vec4(255., 0., 0., 1.);
      }
    </script>

    <script id="fs-normal" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_color;
      void main() {
         gl_FragColor = v_color;
      }
    </script>
  </body>
</html>

I would expect my normals being sheared (rotated) by my transformation matrix, but they don't.

Please note:

  • I am aware that I can compute again the perspective-corrected normals using the screen-space vertex coords (see my transformVertices function), so I am not asking for that .
  • Moreover: I am aware that normals are usually used for shading computation, for my question this doesn't matter.

I am triyng to understand how to transform normals to implement Back-Face culling under the hood, using a matrix multiplication when I set divide by Z by my self.


EDIT - here are some references:

Backface Culling in Object Space

A Compact Method for Backface Culling

Back Face Culling - Dot/Cross Products

Another look at 3D graphics: fast hidden faces removal (back-face culling)

1
What do you mean by "back face culling"? Normally, back face culling refers to whether vertices of a triangle appear in clockwise or counter clockwise order in clip space. That has nothing to do with normals. It happens purely in 2D. The GPU will do it for you by turning on face culling with gl.enable(gl.CULL_FACE) and by default it will cull back facing triangles, defined as triangles whose vertices end up in clockwise order.gman
I also don't understand why you'd expect the normals to get sheared. They're using the same math as the triangles so they do exactly the same thing as the triangles.gman

1 Answers

1
votes

I'm sorry if this is not an answer to your question but "back face culling" has nothing to do with normals and you don't generally do it manually. You let WebGL do it for you

From the spec

3.5.1 Basic Polygon Rasterization

The first step of polygon rasterization is to determine if the polygon is back facing or front facing. This determination is made based on the sign of the (clipped or unclipped) polygon’s area computed in window coordinates. One way to compute this area is

enter image description here

where xiw and yiw are the x and y window coordinates of the ith vertex of the n-vertex polygon (vertices are numbered starting at zero for purposes of this computation) and i⊕1 is (i+1) mod n. The interpretation of the sign of this value is controlled with

void FrontFace( enum dir );

Setting dir to CCW (corresponding to counter-clockwise orientation of the projected polygon in window coordinates) indicates that the sign of a should be reversed prior to use. Setting dir to CW (corresponding to clockwise orientation) uses the sign of a is as computed above. Front face determination requires one bit of state, and is initially set to CCW.

If the sign of the area computed by equation 3.4 (including the possible reversal of this sign as indicated by the last call to FrontFace) is positive, the polygon is front facing; otherwise, it is back facing. This determination is used in conjunction with the CullFace enable bit and mode value to decide whether or not a particular polygon is rasterized. The CullFace mode is set by calling

void CullFace( enum mode );

mode is a symbolic constant: one of FRONT, BACK or FRONT_AND_BACK. Culling is enabled or disabled with Enable or Disable using the symbolic constant CULL_FACE. Front facing polygons are rasterized if either culling is disabled or the CullFace mode is BACK while back facing polygons are rasterized only if either culling is disabled or the CullFace mode is FRONT. The initial setting of the CullFace mode is BACK. Initially, culling is disabled.

Notice normals are never mentioned, nor is z mentioned. It happens entirely in 2D. Further, though it's not impossible, it would be difficult to back face cull using face normals (the normals shown in your example code) as each vertex is processed independently. Vertex shaders don't process triangles, they process individual vertices. So, how would they compute a normal for the triangle in order to cull it? I suppose you could pass in a face normal with each vertex or pass in all 3 vertices every time by duplicating them in a separate buffers but with their order rotated per triangle.

The easiest way to see back face culling at work is probably to draw something but separate the faces so you can see the back faces

'use strict';

function main() {
  const settings = {
    enabled: false,
    frontFace: 0,
    cullFace: 0,
    fudgeFactor: 1.42,
    tX: -34,
    tY: -47,
    tZ: 20,
    rX: 43,
    rY: 33,
    rZ: 8,
    scale: 1.77
  };
  const frontFaceOptions = [ "CCW", "CW" ];
  const cullFaceOptions = [ "BACK", "FRONT", "FRONT_AND_BACK" ];

  function setupUI() {
   
    webglLessonsUI.setupUI(document.querySelector('#ui'), settings, [
      { type: "checkbox", key: "enabled", name: "culling enabled", },
      { type: "option",   key: "frontFace", options: frontFaceOptions, },
      { type: "option",   key: "cullFace", options: cullFaceOptions, },
    ]);
  }

  function draw(time) {
    settings.rY = time * 0.01;
    // assignZToWMatrix
    perspMatrix[11] = settings.fudgeFactor;
    var projMatrix = m4.multiply(perspMatrix, m4.projector(gl.canvas.clientWidth, gl.canvas.clientHeight, 400));
    var worldMatrix = m4.translation(settings.tX, settings.tY, settings.tZ);
    m4.translate(worldMatrix, settings.tX, settings.tY, settings.tZ, worldMatrix);
    m4.xRotate(worldMatrix, (settings.rX * Math.PI) / 180, worldMatrix);
    m4.yRotate(worldMatrix, (settings.rY * Math.PI) / 180, worldMatrix);
    m4.zRotate(worldMatrix, (settings.rZ * Math.PI) / 180, worldMatrix);
    m4.scale(worldMatrix, settings.scale, settings.scale, settings.scale, worldMatrix);
    var matrix = m4.multiply(projMatrix, worldMatrix);

    webglUtils.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    if (settings.enabled) {
      gl.enable(gl.CULL_FACE);
    } else {
      gl.disable(gl.CULL_FACE);
    }
    gl.cullFace(gl[cullFaceOptions[settings.cullFace]]);
    gl.frontFace(gl[frontFaceOptions[settings.frontFace]]);
    
    gl.enable(gl.DEPTH_TEST);

    gl.useProgram(solidInfo.program);
    webglUtils.setBuffersAndAttributes(gl, solidInfo, solidBufferInfo);
    webglUtils.setUniforms(solidInfo, {
      u_matrix: matrix
    });

    var count = 32 * 3; // <= geometries x points-each
    webglUtils.drawBufferInfo(gl, solidBufferInfo, gl.TRIANGLES);
    
    requestAnimationFrame(draw);
  }

  var canvas = document.querySelector('#canvas'),
    gl = canvas.getContext('webgl');
  var perspMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

  var solidArrays = {
    position: {
      numComponents: 3,
      data: vertices
    },
    color: {
      numComponents: 3,
      data: vertexColors
    },
  };

  var solidInfo = webglUtils.createProgramInfo(gl, ['vs-solid', 'fs-solid']);
  var solidBufferInfo = webglUtils.createBufferInfoFromArrays(gl, solidArrays);

  setupUI();
  requestAnimationFrame(draw);
}

m4.projector = function(width, height, depth) {
  // Note: set 0,0 at canvas center
  return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
    0, 0, 0, 1,
  ];
};

var vertices = new Float32Array([
  // left column front
  0, 0, 0, 0, 150, 0, 30, 0, 0,
  0, 150, 0, 30, 150, 0, 30, 0, 0,
  // top rung front
  30, 0, 0, 30, 30, 0, 100, 0, 0,
  30, 30, 0, 100, 30, 0, 100, 0, 0,
  // middle rung front
  30, 60, 0, 30, 90, 0, 67, 60, 0,
  30, 90, 0, 67, 90, 0, 67, 60, 0,
  // left column back
  0, 0, 30, 30, 0, 30, 0, 150, 30,
  0, 150, 30, 30, 0, 30, 30, 150, 30,
  // top rung back
  30, 0, 30, 100, 0, 30, 30, 30, 30,
  30, 30, 30, 100, 0, 30, 100, 30, 30,
  // middle rung back
  30, 60, 30, 67, 60, 30, 30, 90, 30,
  30, 90, 30, 67, 60, 30, 67, 90, 30,
  // top
  0, 0, 0, 100, 0, 0, 100, 0, 30,
  0, 0, 0, 100, 0, 30, 0, 0, 30,
  // top rung right
  100, 0, 0, 100, 30, 0, 100, 30, 30,
  100, 0, 0, 100, 30, 30, 100, 0, 30,
  // under top rung
  30, 30, 0, 30, 30, 30, 100, 30, 30,
  30, 30, 0, 100, 30, 30, 100, 30, 0,
  // between top rung and middle
  30, 30, 0, 30, 60, 30, 30, 30, 30,
  30, 30, 0, 30, 60, 0, 30, 60, 30,
  // top of middle rung
  30, 60, 0, 67, 60, 30, 30, 60, 30,
  30, 60, 0, 67, 60, 0, 67, 60, 30,
  // right of middle rung
  67, 60, 0, 67, 90, 30, 67, 60, 30,
  67, 60, 0, 67, 90, 0, 67, 90, 30,
  // bottom of middle rung.
  30, 90, 0, 30, 90, 30, 67, 90, 30,
  30, 90, 0, 67, 90, 30, 67, 90, 0,
  // right of bottom
  30, 90, 0, 30, 150, 30, 30, 90, 30,
  30, 90, 0, 30, 150, 0, 30, 150, 30,
  // bottom
  0, 150, 0, 0, 150, 30, 30, 150, 30,
  0, 150, 0, 30, 150, 30, 30, 150, 0,
  // left side
  0, 0, 0, 0, 0, 30, 0, 150, 30,
  0, 0, 0, 0, 150, 30, 0, 150, 0
]);

// spread the faces in the direction of their normals
for (let i = 0; i < vertices.length; i += 9) {
  // get a view of each position in place (works because it's a typedarray)
  const p0 = vertices.subarray(i + 0, i + 3);
  const p1 = vertices.subarray(i + 3, i + 6);
  const p2 = vertices.subarray(i + 6, i + 9);
  
  // compute the normal
  const n = m4.normalize(m4.cross(
     m4.subtractVectors(p1, p0),
     m4.subtractVectors(p2, p1)));
     
  // scale by 10
  m4.scaleVector(n, 10, n);
  
  // add the normal to each vertex of the triangle
  // this will move it in the direction of the normal
  m4.addVectors(p0, n, p0);
  m4.addVectors(p1, n, p1);
  m4.addVectors(p2, n, p2);
}

var vertexColors = new Uint8Array([
  // left column front
  100, 35, 60, 100, 35, 60, 100, 35, 60,
  200, 70, 120, 200, 70, 120, 200, 70, 120,
  // top rung front
  100, 35, 60, 100, 35, 60, 100, 35, 60,
  200, 70, 120, 200, 70, 120, 200, 70, 120,
  // middle rung front
  100, 35, 60, 100, 35, 60, 100, 35, 60,
  200, 70, 120, 200, 70, 120, 200, 70, 120,
  // left column back
  40, 35, 100, 40, 35, 100, 40, 35, 100,
  80, 70, 200, 80, 70, 200, 80, 70, 200,
  // top rung back
  40, 35, 100, 40, 35, 100, 40, 35, 100,
  80, 70, 200, 80, 70, 200, 80, 70, 200,
  // middle rung back
  40, 35, 100, 40, 35, 100, 40, 35, 100,
  80, 70, 200, 80, 70, 200, 80, 70, 200,
  // top
  35, 100, 105, 35, 100, 105, 35, 100, 105,
  70, 200, 210, 70, 200, 210, 70, 200, 210,
  // top rung right
  100, 100, 35, 100, 100, 35, 100, 100, 35,
  200, 200, 70, 200, 200, 70, 200, 200, 70,
  // under top rung
  105, 50, 35, 105, 50, 35, 105, 50, 35,
  210, 100, 70, 210, 100, 70, 210, 100, 70,
  // between top rung and middle
  105, 80, 35, 105, 80, 35, 105, 80, 35,
  210, 160, 70, 210, 160, 70, 210, 160, 70,
  // top of middle rung
  35, 90, 105, 35, 90, 105, 35, 90, 105,
  70, 180, 210, 70, 180, 210, 70, 180, 210,
  // right of middle rung
  50, 35, 105, 50, 35, 105, 50, 35, 105,
  100, 70, 210, 100, 70, 210, 100, 70, 210,
  // bottom of middle rung.
  38, 105, 50, 38, 105, 50, 38, 105, 50,
  76, 210, 100, 76, 210, 100, 76, 210, 100,
  // right of bottom
  70, 105, 40, 70, 105, 40, 70, 105, 40,
  140, 210, 80, 140, 210, 80, 140, 210, 80,
  // bottom
  45, 65, 55, 45, 65, 55, 45, 65, 55,
  90, 130, 110, 90, 130, 110, 90, 130, 110,
  // left side
  80, 80, 110, 80, 80, 110, 80, 80, 110,
  160, 160, 220, 160, 160, 220, 160, 160, 220
]);

main();
body {
  margin: 0;
}

canvas {
  border: none;
  width: 100vw;
  height: 100vh;
  display: block;
}
<link href="https://webglfundamentals.org/webgl/resources/webgl-tutorials.css" type="text/css" rel="stylesheet"/>
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m4.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/webgl-lessons-ui.js"></script>
<canvas id="canvas"></canvas>
<div id="uiContainer" style="top: 40px;">
  <div id="ui"></div>
</div>

<script id="vs-solid" type="x-shader/x-vertex">
  attribute vec4 a_position;
  attribute vec3 a_color;
  uniform mat4 u_matrix;
  varying vec4 v_color;
  void main() {
    gl_Position = u_matrix * a_position;
    v_color = vec4(a_color, 1.0);
  }
</script>

<script id="fs-solid" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_color;
  void main() {
     gl_FragColor = v_color;
  }
</script>

In the comments you linked to software renderer on the Commodore 64. It's kind of hard to emulate a software renderer in WebGL because it's whole point is to replace that renderer. That code is doing things with triangles but in WebGL we don't do things with triangles, we do things with vertices and WebGL itself handles the triangles.

In any case, as for why your code is not working, the code is checking if the normal is facing the Z plane (z < 0), not the camera. To check if the normal is facing the camera you need to compare the transformed normal to the direction from the camera to that point in space. This article calls that the surfaceToView vector.

So, if you wanted to draw a wireframe box and do fake hidden line removal like the example you'd have to pass in a face normal for each vertex representing what direction that vertex points. You'd then need a modelView matrix (everything except the projection matrix). Compute the view position of the point and use that to compute an eye to point vector, you also orient the normal through that same matrix and compute the angle between them with the dot product. < 0 vs > 0 will tell you if that normal is facing away or toward. example

That technique will not work for the 3D F though. It will only work for convex shapes like a cube, sphere, pyramid. The F is not convex so inner lines will still be drawn.

The easiest way to do hidden line removal in WebGL is to draw the object twice. Once with triangles (to fill in the depth buffer) and then again with lines. You might switch the depth test from LESS to LEQUAL when drawing with lines