javascripthtml5-canvaswebglwebgl2

Ambiguous information about WebGL clip space


I am reading about WebGL clip space, I think it basically should mean the same thing as coordinate system. The same site specifies ambiguous, information.
In WebGL basics is specified left hand system, where z aux goes from -1 to 1:
WebGL basics clip space enter image description here.

But in WebGL game development basics the coordinate system is specified as right handed, like in OpenGL where z axis goes in opposite direction, from 1 to -1:
WegGL game development coordinate system. enter image description here
Which variant is the correct one? What I've practically seen so far, it was consistent with left handed. My question ends here, follows a demo Update 1.

Update 1 The squares intersecting the cone are added one on top of the other in direction of negative Z

"use strict";
{


function buildGeometryPolar (shape)
{
   let dfi = 2 * Math.PI / shape.sectors;

   let [base, norms] = [[], []];
   let sec2  =   shape.sectors; // / 2;
   sec2 /=  2;

   //transform radius to coordinate
   let [xfi, yfi, zfi]  = [a => shape.func(a) * Math.cos(a), a => shape.func(a) * Math.sin(a), a => 0.0];
   //transform radius to normals
    let  [nxfi, nyfi, nzfi] = [xfi, yfi, a =>  1];
    if (shape.d) [nxfi, nyfi, nzfi] = [shape.d.x, shape.d.y, shape.d.z];
   if (!shape.scaleVert) shape.scaleVert = a => a ;
   if (!shape.scaleNorm) shape.scaleNorm = a => a ;
   // Calculate for fi = [0 .. PI]
   for (let i = 0, fi = 0; i <= sec2; i++, fi += dfi)
   {
      base  [i]  =  shape.scaleVert ([ xfi(fi),  yfi(fi),   zfi(fi)]);
      norms [i]  =  shape.scaleNorm ([nxfi(fi), nyfi(fi),  nzfi(fi)]);
      norms [i]  =  normv(norms[i]); //normalize
   }
   return {base:base, norms:norms};
}

function buildSides (geometry, slices, sectors) 
{
   let sec2 = sectors / 2;
   let dh = 1.0 / slices;

   let sides = [[], []];

   //tip of the cone
   sides[0][0] = [];
   for (let i = 0; i <= sec2; i++)
      sides[0][0][i]  = [0, 0, 0, 0, 0, 0];
   //slices
   for (let j = 1, s = 1, z = dh; j <= slices; j++, s++, z += dh) //z = z axix
   {
      sides[0][j] = [];
      for (let i = 0; i <= sec2; i++)
      {
         let bs = geometry.base[i], ns = geometry.norms[i];
         let sd = [s * bs[0] / slices,    s * bs[1] / slices, z,     ns[0], ns[1], ns[2]];
         sides[0][j][i] = [sd[0], sd[1], sd[2],    sd[3],  sd[4], sd[5]];
      }
   }

   mirrorSide (sides[0], sides[1], [1, -1, 1]);
   return sides;
}
function mirrorSide (side1, side2, mirror)
{
   const cuts = side1.length;
   for (let j = 0; j < cuts; j++)
   {
      side2[j] = [];
      const sls = side1[j]; //slice
      const len = sls.length;  //sectors
      for (let i = 0, i2 = len - 1; i < len; i++, i2--)
         side2[j][i2] = mulv (sls[i], [... mirror, ...  mirror]);  //mirroring segments
   }
}


function buildConePolar (shape)
{
   let slices = shape.slices, sectors = shape.sectors;
   let revealInvisibles = shape.revealInvisibles;
   let nfi = 0;
   if (sectors &  3) throw "Number of sectors must be a multiple of 4: "    + sectors;
   if (sectors <  8) throw "Must have no less than 8 sectors: "             + sectors;
   if (slices  <  1) throw "Must have no less than 1 slices: "              + slices;


   let geo = buildGeometryPolar (shape);
   if (revealInvisibles)
      geo.base[0] = [0.5, 0.0, 0.0];

   console.log ("slices;" + slices +  " sectors:" + sectors);
    //all needed vertices and normals
   let sides = buildSides(geo, slices, sectors);
   let sec2  = sectors / 2;

   //order vertices in triangles
   let [verts, norms] = [[],[]]; //vertices

   let [i0, i1, i2]   = [0, 1, 2];
   for (let side of sides)
      for (let j = 1; j <= slices; j++)
         for (let i = 0; i < sec2; i++)
         {
            let s11, s12; // s11   \      // s11 <- s12
            let s21, s22; // s21 -> s22   //    \   s22

            s11 = side [j-1][i];   s12 = side [j-1][i+1];
            s21 = side [j]  [i];   s22 = side [j]  [i+1];
            {
               [verts [i0],     verts [i1],     verts [i2]]       =   [ s21[0],   s21[1],   s21[2] ];
               [norms [i0],     norms [i1],     norms [i2]]       =   [ s21[3],   s21[4],   s21[5] ];
               
               [verts [i0 + 3], verts [i1 + 3], verts [i2 + 3]]   =   [ s22[0],   s22[1],   s22[2] ];
               [norms [i0 + 3], norms [i1 + 3], norms [i2 + 3]]   =   [ s22[3],   s22[4],   s22[5] ];
               
               [verts [i0 + 6], verts [i1 + 6], verts [i2 + 6]]   =   [ s11[0],   s11[1],   s11[2] ];
               [norms [i0 + 6], norms [i1 + 6], norms [i2 + 6]]   =   [ s11[3],   s11[4],   s11[5] ];

               i0  +=  9; i1  += 9;  i2  += 9;
            }
            if (j > 1) // when j == 1 we do not double triangles on the tip of the cone
            {
               [verts [i0],     verts [i1],     verts [i2]]     = [ s22[0],   s22[1],   s22[2] ];
               [norms [i0],     norms [i1],     norms [i2]]     = [ s22[3],   s22[4],   s22[5] ];
            
               [verts [i0 + 3], verts [i1 + 3], verts [i2 + 3]] = [ s12[0],   s12[1],   s12[2] ];
               [norms [i0 + 3], norms [i1 + 3], norms [i2 + 3]] = [ s12[3],   s12[4],   s12[5] ];
            
               [verts [i0 + 6], verts [i1 + 6], verts [i2 + 6]] = [ s11[0],   s11[1],   s11[2] ];
               [norms [i0 + 6], norms [i1 + 6], norms [i2 + 6]] = [ s11[3],   s11[4],   s11[5] ];
               i0 += 9;  i1 += 9;  i2 += 9;
            }
         }


   let nverts      = []; //build spykes
   let [ni0, ni1, ni2] = [0, 1, 2];

   for (let side of sides)
      for (let j = 0; j <= slices; j++)
         for (let i = 0; i < sec2; i++)
         {
            let s = side [j][i]; //[...verts.xyz, ...norms.xyz];
            [nverts [ni0],     nverts [ni1],     nverts [ni2]]     = [s[0], s[1], s[2]];
            [nverts [ni0 + 3], nverts [ni1 + 3], nverts [ni2 + 3]] = [s[0] + s[3]/10,   s[1] + s[4]/10,   s[5] ] ;//- 0.5];
            [nverts [ni0 + 3], nverts [ni1 + 3], nverts [ni2 + 3]] = [s[0] + s[3]/10,   s[1] + s[4]/10,   s[2] ] ;//- s[5] / 10];
            ni0 += 6;  ni1 += 6;  ni2 += 6;
         }

   let cone =  {verts:verts, norms:norms, nverts:nverts};

   let expected;
   expected =     3 * (sectors * 3 + sectors * 6 * (slices - 1));
   console.assert ( expected ==  verts.length,  "wrong number of vertices, expected %d, calculated %d, vertices %d", expected, verts.length, verts.length / 3);
   expected = (slices * sectors)  *  (2 * 3); //each segment two points of x,y,z
   //expected = (slices * sectors)  *  (2 * 3) + slices; //each segment two points of x,y,z
   expected = ((slices  + 1)* sectors)  *  (2 * 3); //each segment two points of x,y,z
   console.assert ( expected ==  nverts.length, "wrong number of vertices, expected %d, calculated %d", expected, nverts.length);

   return cone;

}

function buildSquare (len, z)
{
   return   [ len, -len, z,    len, len, z,   -len,   len, z, 
              len, -len, z,   -len, len, z,   -len,  -len, z  ];
}
function buildNorms (nm)
{
   let nv =  normv(nm); //normalize
   return [ ...nv,  ...nv,  ...nv,   ...nv,  ...nv,  ...nv ];
}

let func = () =>
{
   let canvas = document.getElementById("cone1 heart geometry 2");
   let glCanvas = new GlCanvas(canvas);

   let gl = glCanvas.gl;
   glCanvas.useProgram ();
   let program = glCanvas.program;

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

    let hearth = {
        func: a => a, //hearth double sided
      d: //gradient
      {
         x : a =>   Math.sin(a) + hearth.func(a) * Math.cos(a),
         y : a => -(Math.cos(a) - hearth.func(a) * Math.sin(a)),
         z : a =>  -hearth.func(a), //Z negative, dorected toward
        },
        scaleVert: a =>  mulv(a, [1.0 / Math.PI,  1.0 / Math.PI,   1.0]),
        scaleNorm: a =>  mulv(a, [1.0,            1.0,   1.0 / Math.PI]),
        //sectors:16, slices:3, revealInvisibles: false
        sectors:40, slices:1, revealInvisibles: false
    };

   let obj;
   try
   {
        let shape = hearth;
        //shape = circle;
      obj = buildConePolar (shape);

      ////slice object with squares
      obj.verts.push( ... buildSquare (   0.40,  0.90));
      obj.norms.push( ... buildNorms  ([  1.00,  0.00,  1.00 ]));
      obj.verts.push( ... buildSquare (   0.35,  0.70));
      obj.norms.push( ... buildNorms  ([ -1.00, -1.00, -1.00]));
      obj.verts.push( ... buildSquare (   0.30,  0.50,));
      obj.norms.push( ... buildNorms  ([ -1.00, -0.40, -1.00 ]));
      obj.verts.push( ... buildSquare (   0.25,  0.30));
      obj.norms.push( ... buildNorms  ([ -1.00, -0.60, -1.00 ]));
   }
   catch(err)
   {
      alert(err);
   }
   let verts = obj.verts;
   let norms = obj.norms;
   ////////////////////////////////////

   let vertex_buffer = gl.createBuffer();
   gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
   gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);

   program = glCanvas.program;
   //gl.useProgram(program);
   glCanvas.useProgram ();
   let coord = gl.getAttribLocation (program, "coordinates");
   gl.vertexAttribPointer     (coord, 3, gl.FLOAT, false, 0, 0);
   gl.enableVertexAttribArray (coord);

   let normalBuffer = gl.createBuffer();
   gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
   gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(norms), gl.STATIC_DRAW);
   let noord = gl.getAttribLocation (program, "inputNormal");
   gl.vertexAttribPointer     (noord, 3, gl.FLOAT, false, 0, 0);
   gl.enableVertexAttribArray (noord);

   gl.clearColor(0.5, 0.5, 0.5, 0.9);
   gl.drawArrays(gl.TRIANGLES,  0, verts.length / 3);

   gl.useProgram (null);
   gl.bindBuffer(gl.ARRAY_BUFFER, null);
   gl.disableVertexAttribArray (coord);

}

document.addEventListener('DOMContentLoaded', func);
}
<html>
<head>
   <title>webgl test</title>
   <!-- script src="./gljs/glcanvas.js"></script -->
   <script language="javascript">
       //math functions
       function normv   (norm)   { let len = Math.hypot ( ... norm); return norm.map(a => a / len); }
      function mulv    (v,  f)  { return v.map  ( (a, i) => a * f[i] ); }
   </script>
   <script language="javascript">
       //gl functions

      class GlShader
      {
         static shaderMap = new Map(
         [
            ["vertex-shader",                        "vertex-shader"],
            ["x-vertex",                             "vertex-shader"],
            ["x-shader/x-vertex",                    "vertex-shader"],
            ["fragment-shader",                      "fragment-shader"],
            ["x-fragment",                           "fragment-shader"],
            ["x-shader/x-fragment",                  "fragment-shader"]
         ]);
         
         static translateType (str, gl)
         {
            if (typeof(str) != 'string' && (str == gl.VERTEX_SHADER || str == gl.FRAGMENT_SHADER))
               return str;
            if (!GlShader.shaderMap.has(str)) throw "Unknown shader type: " + str;
            switch ( GlShader.shaderMap.get(str) )
            {
            case "vertex-shader":       return gl.VERTEX_SHADER;
            case "fragment-shader":     return gl.FRAGMENT_SHADER;
            }
            throw "Unknown shader type: " + str;
         }
         //TODO: not used isAsynk
         isAsynk = false;
         #glType = null;
         constructor (gl, obj, type)
         {
            this.source = "";
            this.gl     = gl;
            this.shader = null;
            if (type) this.type = type;
            if (!obj) return;
            if (typeof obj == "string") this.#setString (obj);
            else if (obj instanceof Element) this.#setScriptElement (obj);
            else if (obj instanceof WebGLShader) this.shader = obj;
         }
      
         //internal automatic full compile
         #compile ()
         {
            this.shader = this.gl.createShader (this.#glType);
            this.gl.shaderSource  (this.shader, this.source);
            this.compileShader();
         }
         //private:
         #setString (str) //type = go.VERTEX_SHADER/gl.VERTEX_SHADER
         {
            if (!str) {console.log ("no string for shader to compile"); return;}
            this.source = str.trimStart ();
            if (!this.type) return;
            if (this.type.length <= 0) return;
            this.#compile ();
         }
         #setScriptElement (obj)
         {
            this.script = obj;
            let type = obj.dataset.glType; //same as getAttribute("data-gl-type");
            if (type) { if (type.length > 0) this.type = type; }
            let text = obj.innerText;
            if (!text) text = "";
            text = text.trimStart ();
            this.#setString (text);
         }
         #showResult ()
         {
            let shader = this.shader, gl = this.gl;  //shortcuts
            if (gl.getShaderParameter (shader, gl.COMPILE_STATUS)) return;
            let msg = gl.getShaderInfoLog (shader);
            console.log ("SHADER ERROR: " + msg);
         }
       
         get type () { return this.#glType; }
         set type (str)
         {
            this.#glType = GlShader.translateType (str, this.gl);
         }
         compileShader ()
         {
            this.gl.compileShader (this.shader);
            this.#showResult ();
         }
         deleteShader ()
         {
            this.gl.deleteShader (this.shader);
            this.#glType = null;
            this.source = "";
            this.shader = null;
         }
      }

      class GlApi
      {
         constructor (gl){this.gl = gl;}
         arrayBuffer (buffer, glDrawType)
         {
            let gl = this.gl;
            let glBuffer = new GlBuffer (gl, gl.ARRAY_BUFFER, this.program);
            glBuffer.arrayBuffer (buffer, glDrawType);
            return glBuffer;
         }
         indexBuffer (buffer, glDrawType)
         {
            let gl = this.gl;
            let glBuffer = new GlBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, this.program, buffer, glDrawType);
            return glBuffer;
         }
         useProgram (){this.gl.useProgram (this.program);}
         uniformMatrix4fv (name)
         {
            this.useProgram ();
            //this.bindBuffer ();
            let id = this.gl.getUniformLocation (this.program, name);
            //this.gl.uniformMatrix4fv     (id, size, transpose, value);
            this.gl.enableVertexAttribArray (id);
            return id;
         }
      }
      class GlProgram extends GlApi
      {
         #shaders = [];
         constructor (gl)
         {
            super(gl);
            this.program = this.gl.createProgram ();
         }
         add (shader, type)
         {
            if (!shader)
            {
               console.log("null shader");
               return;
            }
            if (shader instanceof GlShader)         this.#shaders.push (shader);
            else if (shader instanceof WebGLShader) this.#shaders.push (new GlShader (this.gl, shader));
            else if (shader instanceof Element)     this.#shaders.push (new GlShader (this.gl, shader, type));
            else if (typeof shader == "string")     this.#shaders.push (new GlShader (this.gl, shader, type));
         }
         #showResult ()
         {
            let program = this.program; //shortcut
            let gl = this.gl;           //shortcut
            if (gl.getProgramParameter (program, gl.LINK_STATUS)) return;
            let msg = gl.getProgramInfoLog (program);
            if (this.name)
               console.log ("PROGRAM ERROR [" + this.name + "]: " + msg);
            else
               console.log ("CPROGRAM ERROR: " + msg);
         }
         linkProgram ()
         {
            let program = this.program;
            let gl = this.gl; //shortcut
            for (let shader of this.#shaders) gl.attachShader (program, shader.shader);
            gl.linkProgram (program);
            this.#showResult (program);
            for (let shader of this.#shaders) shader.deleteShader ();
         }
         useProgram (){this.gl.useProgram (this.program);}
      }
      class GlCanvas
      {
         #glObj                = null;
         #canvasObj            = null;
         #defaultProgramName   = "___DEFAULT_PROGRAM___";
         #programMap           = new Map();
      
         constructor (canvasVar, elementVars)
         {
            this.#canvas = canvasVar;
            this.#programMap.set (this.#defaultProgramName, new GlProgram (this.gl));
            //if (canvasVar instanceof  GlInfo) throw "GlInfo to be handled by offscrfeen";
            this.#extractShaderCodes ();
            this.#extractElementCodes (elementVars);
            
            for (let program of this.programs)
               program[1].linkProgram ();
         }
      
         #prepareElement (el)
         {
            if (typeof el == "string") el = document.getElementById (el);
            if (!el.hasAttribute ("data-gl-type"))
               if (el.hasAttribute ("type") && (el.getAttribute ("type") != "text/glsl-shader")  )
               {
                  let type = GlShader.translateType (el.getAttribute("type"), this.gl);
                  if (type) el.setAttribute ("data-gl-type", type);
               }
            if (el.hasAttribute ("data-gl-type")) return el;
            return null; //since now a recognisable attribute "data-gl-type" is a must, otherwise it is not gl shader
         }
         #extractProgramInfo (el)
         {
            el = this.#prepareElement (el);
            let programName = this.#defaultProgramName;
            if (el.hasAttribute ("data-gl-program"))
                programName = el.getAttribute ("data-gl-program");
            return {id: programName, shader: new GlShader (this.gl, el)};
         }
         #addScriptShader (el)
         {
            let info = this.#extractProgramInfo (el);
            if (! this.programs.has (info.id) )
                  this.programs.set (info.id, new GlProgram (this.gl));
            if (info.shader)
                this.programs.get (info.id).add (info.shader);
      
            //console.log(info.id + ": " + el.innerText);
         }
         
         #extractElementCodes (elementVars)
         {
            if (!elementVars) return;
            for (let el of elementVars)
               this.#addScriptShader (el);
         }
         #extractShaderCodes ()
         {
            const shaderElements = document.evaluate ("./script[@type='text/glsl-shader']", this.canvas);
            //const shaderElements = document.evaluate("./script[@type='text/glsl-shader']", this.canvas); //Add one more condition, for external script
         
            for (let el = shaderElements.iterateNext (); el; el = shaderElements.iterateNext ())
               this.#addScriptShader (el);
         
         }
      
         set #gl (glObj) {this.#glObj = glObj;}
         set #canvas (canvasVar)
         {
            if (typeof canvasVar == "string")
                this.#canvasObj = document.getElementById (canvasVar);
            else if (typeof canvasVar == "object")
                this.#canvasObj = canvasVar;
            this.#gl = this.canvas.getContext ('webgl2');
            this.gl.viewport (0, 0, this.canvas.width, this.canvas.height);
         }
      
         //publig getter private setter
         get gl (){return this.#glObj;}
         get canvas   () { return this.#canvasObj;  }
         get programs () { return this.#programMap; }
         //default context and default program
      
         #ProgName (progName) {if (progName) return progName; return this.#defaultProgramName;}
         get glProgram  () { return this.programs.get( this.#ProgName () ); }
         get program    () { return this.glProgram.program; }
         getGlProgram   (progName) { return this.programs.get (this.#ProgName (progName)); } //defaults to
         getProgram     (progName) { return this.getGlProgram (progName).program; } //defaults to
         useProgram     (progName) { this.getGlProgram (progName).useProgram(); } //defaults to
      
      }

   </script>
</head>
<body>
   <div class="sample">
      <canvas id = "cone1 heart geometry 2" width = "400" height = "400"  class="yellow">
         <!--   Program default  -->
         <script  type="text/glsl-shader" data-gl-type="vertex-shader">
             attribute vec3 coordinates;
             attribute vec3 inputNormal;

             varying vec3 nm;
             void main()
             {
                gl_Position = gl_Position = vec4(coordinates, 1.0); //coordinates;
                nm = inputNormal;
             }
         </script>
         <script type="text/glsl-shader" data-gl-type="fragment-shader">
            precision mediump float;
            varying vec3 nm; //varying mediump

            const vec4  greenColor     = vec4( 0.0,   1.0,   0.0, 1.0);
            const vec3 lightDirection  = normalize(vec3(1.0,  -1.0,  1.0)); //right down far
            void main()
            {
               gl_FragColor = vec4(greenColor.rgb * -dot(lightDirection, normalize(nm)), 1.0);
            }
         </script>

      </canvas>
   </div>

</body>
</html>


Solution

  • Which variant is the correct one?

    Therefore, the“Explaining basic 3D theory” page is describing the right‐handed world/view coordinate system, while the “WebGL model view projection” page is referring to the left‐handed clip/NDC system — both descriptions are correct, they are just talking about different stages of the pipeline ▼ e.g.:

    After applying a standard projection matrix (glOrtho, glFrustum, gluPerspective, etc.), a −Z factor is introduced, which flips the Z axis, so that the clip space (and, when divided by W, also the NDC) becomes left-handed, with +Z pointing into the scene and all coordinates in the cube.

    Model/World Space: The local grid (model) coordinates follow a right-handed convention: +X → right, +Y → top, +Z → outside the screen.

    Clip Space: When multiplied by the projection matrix, the Z coordinate indicator is scaled by −1, which reverses its direction - effectively +Z points into the scene.

    In general, right-h is valid until a projection is applied (model, world, view), and left-h starts from clip space (after projection) up to the final screen coordinates.

    EDIT

    Generally speaking, as I wrote, there is no enforced coordinator system. The documentation also mentions this, whether it is a right-handed or left-handed system depends on the convention.

    ...7. OpenGL does not force left- or right-handedness on any of its coordinates systems... - p. 384

    https://registry.khronos.org/OpenGL/specs/gl/glspec40.core.pdf#:~:text=6.%20Because%20rasterization%20of%20non,383

    ...The code, it has no projections, it is practically left handed from the very beginning. What I need is to know what specifications say explicitly about that...

    See section 2.14 v. 4.0 p. 123

    Clip coordinates for a vertex result from vertex or, if active, geometry shader execution, which yields a vertex coordinate gl_Position. Perspective division on clip coordinates yields normalized device coordinates, followed by a viewport transformation to convert these coordinates into window coordinates.

    By setting directly:

    gl_Position = vec4(x, y, z, 1.0);

    then the values ​​(x,y,z) are immediately clip coordinates, and after dividing by 1 we get NDC=(x,y,z) in the same system.

    As a result, without projection, +Z in the application space goes unchanged to +Z in NDC, which causes us to build the scene in a left-handed coordinate system from the very beginning.

    Take look at here as well:

    https://github.com/toji/gl-matrix/issues/185

    https://learnopengl.com/Getting-started/Coordinate-Systems

    About CPU in pipline I meant:

    Building and combining the world and camera matrices, culling, dynamic meshes, instantiation, logical drawing calls