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