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
.
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.
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>
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.:
(x,y,z ∈ [–1,1])
are clipped. This clip space is left-handed because the projection matrix introduces a –Z component, which reverses the direction of the Z axis.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
...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