On Github VisualComputing / Transformations
Set your presentation theme: Black (default) - White - League - Sky - Beige - Simple Serif - Blood - Night - Moon - Solarized
Jean Pierre Charalambos
Vertex shader
Fragment shader
for (int i = 0; i < vertexCount; i++) { output = vertexShader(vertex[i]); } function vertexShader(vertex) { projPos = projection * modelview * vertex.position; litColor = lightColor * dot(vertex.normal, lightDirection); return (projPos, litColor); }
uniform mat4 transform; attribute vec4 vertex; attribute vec4 color; varying vec4 vertColor; void main() { gl_Position = transform * vertex; vertColor = color; }
uniform mat4 transform; attribute vec4 vertex; attribute vec4 color; varying vec4 vertColor; void main() { gl_Position = transform * vertex; vertColor = color; }transform = projection
uniform mat4 transform; attribute vec4 vertex; attribute vec4 color; varying vec4 vertColor; void main() { gl_Position = transform * vertex; vertColor = color; }transform = projection * modelview transform = projection * view * model
(goto matrix composition and then to eye transform)
for (int i = 0; i < fragmentCount; i++) { screenBuffer[fragment[i].xy] = fragmentShader(fragment[i]); } function fragmentShader(fragment) { return fragment.litColor; }
varying vec4 vertColor; void main() { gl_FragColor = vertColor; }
Class that encapsulates a GLSL shader program, including a vertex and a fragment shader
Loads a shader into the PShader object
Method signatures
loadShader(fragFilename) loadShader(fragFilename, vertFilename)
Example
PShader unalShader; void setup() { ... //when no path is specified it looks in the sketch 'data' folder unalShader = loadShader("unal_frag.glsl", "unal_vert.glsl"); }
Applies the specified shader
Method signature
shader(shader)
Example
PShader simpleShader, unalShader; void draw() { ... shader(simpleShader); simpleGeometry(); shader(unalShader); unalGeometry(); }
Restores the default shaders
Method signatures
resetShader()
Example
PShader simpleShader; void draw() { ... shader(simpleShader); simpleGeometry(); resetshader(); otherGeometry(); }
Sets the uniform variables inside the shader to modify the effect while the program is running
Method signatures for vector uniform variables vec2, vec3 or vec4:
.set(name, x) .set(name, x, y) .set(name, x, y, z) .set(name, x, y, z, w) .set(name, vec)
Sets the uniform variables inside the shader to modify the effect while the program is running
Method signatures for vector uniform variables boolean[], float[], int[]:
.set(name, x) .set(name, x, y) .set(name, x, y, z) .set(name, x, y, z, w) .set(name, vec)
Sets the uniform variables inside the shader to modify the effect while the program is running
Method signatures for mat3 and mat4 uniform variables:
.set(name, mat) // mat is PMatrix2D, or PMatrix3D
Sets the uniform variables inside the shader to modify the effect while the program is running
Method signatures for vector uniform variables:
.set(name, tex) // tex is a PImage
Sets the uniform variables inside the shader to modify the effect while the program is running
Example:
PShader unalShader; PMatrix3D projectionModelView1, projectionModelView2; void draw() { ... shader(unalShader); unalShader.set("unalMatrix", projectionModelView1); unalGeometry1(); unalShader.set("unalMatrix", projectionModelView2); unalGeometry2(); }
Property 1 $$F(a+b)= F(a)+ F(b)$$
Property 2 $$F(\lambda a) = \lambda F(a)\rightarrow F(0) = 0$$
Observation 1: Matrix multiplication is always linear
Observation 2: Translation is a nonlinear transformation
$x'= sx*x$
$y'= sy*y$
$\begin{bmatrix} x' \cr y' \cr \end{bmatrix} = \begin{bmatrix} sx & 0 \cr 0 & sy \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr \end{bmatrix} $
$P'= S(sx,sy) \bullet P$
$x'= sx*x$
$y'= sy*y$
$z'= sz*z$
$\begin{bmatrix} x' \cr y' \cr z' \cr \end{bmatrix} = \begin{bmatrix} sx & 0 & 0 \cr 0 & sy & 0 \cr 0 & 0 & sz \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr \end{bmatrix} $
$P'= S(sx,sy,sz) \bullet P$
$x = rcos \alpha$
$y= rsin \alpha$
$x'= rcos (\alpha+\beta)$ $x'= rcos \alpha cos \beta - rsin \alpha sin \beta$
$y'= rsin (\alpha+\beta)$ $y'= rcos \alpha sin \beta - rsin \alpha cos \beta$
$\begin{bmatrix} x' \cr y' \cr \end{bmatrix} = \begin{bmatrix} cos\beta & -sin \beta \cr sin\beta & cos \beta \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr \end{bmatrix} $
$P'= R(\beta) \bullet P$
$z' = z$
$\begin{bmatrix} x' \cr y' \cr z' \cr \end{bmatrix} = \begin{bmatrix} cos\beta & -sin \beta & 0 \cr sin\beta & cos \beta & 0 \cr 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr \end{bmatrix} $
$P'= R_z(\beta) \bullet P$
$x' = x$
$\begin{bmatrix} x' \cr y' \cr z' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \cr 0 & cos\beta & -sin \beta \cr 0 & sin\beta & cos \beta \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr \end{bmatrix} $
$P'= R_x(\beta) \bullet P$
$y' = y$
$\begin{bmatrix} x' \cr y' \cr z' \cr \end{bmatrix} = \begin{bmatrix} cos\beta & 0 & sin \beta \cr 0 & 1 & 0 \cr -sin\beta & 0 & cos \beta \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr \end{bmatrix} $
$P'= R_y(\beta) \bullet P$
$x'= x + h*y$
$y'=y$
$\begin{bmatrix} x' \cr y' \cr \end{bmatrix} = \begin{bmatrix} 1 & h \cr 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr \end{bmatrix} $
$P'= D_y(h) \bullet P$
$x'= x$
$y'=y + h*x$
$\begin{bmatrix} x' \cr y' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 \cr h & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr \end{bmatrix} $
$P'= D_x(h) \bullet P$
$x'=x+az$
$y'=y+bz$
$z'=z$
$\begin{bmatrix} x' \cr y' \cr z' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & a \cr 0 & 1 & b \cr 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr \end{bmatrix} $
$P'= D_z(a,b) \bullet P$ (goto 2d translation)
...don't forget $P'= D_x(a,b) \bullet P$ and $P'= D_y(a,b) \bullet P$
$x'= x + dx$
$y'=y + dy$
$\begin{bmatrix} x' \cr y' \cr \end{bmatrix} = \begin{bmatrix} dx \cr dy \cr \end{bmatrix} + \begin{bmatrix} x \cr y \cr \end{bmatrix} $
$P'= T(dx,dy) + P$
Linear transformations $+$ Translation $\rightarrow P' = M\ast P + T $
Homogeneous w-coordinate: $(x,y,w)$
Homogeneous space $\rightarrow$ 2d
$(x,y,1) \rightarrow (x,y)$, for $w=1$
In general: $(x,y,w) \rightarrow (x/w,y/w)$
$(x,y,z,1) \rightarrow (x,y,z)$, for $w=1$
In general: $(x,y,z,w) \rightarrow (x/w,y/w,z/w)$
$x'= x + dx$
$y'=y + dy$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr w' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & dx \cr 0 & 1 & dy \cr 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr w \cr \end{bmatrix} $
$P'= T(dx,dy) \bullet P$ (goto 3d shearing)
$x'= x + dx$
$y'=y + dy$
$z'=z + dz$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & dx \cr 0 & 1 & 0 & dy \cr 0 & 0 & 1 & dz \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= T(dx,dy,dz) \bullet P$
$x'=x+az$
$y'=y+bz$
$z'=z$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & a & 0 \cr 0 & 1 & b & 0 \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= D_z(a,b) \bullet P$
$x'= sx*x$
$y'= sy*y$
$z'= sz*z$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} sx & 0 & 0 & 0 \cr 0 & sy & 0 & 0 \cr 0 & 0 & sz & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= S(sx,sy,sz) \bullet P$
$z' = z$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} cos\beta & -sin \beta & 0 & 0 \cr sin\beta & cos \beta & 0 & 0 \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= R_z(\beta) \bullet P$
$x' = x$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & 0 \cr 0 & cos\beta & -sin \beta & 0 \cr 0 & sin\beta & cos \beta & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= R_x(\beta) \bullet P$
$y' = y$
$w'=w=1$
$\begin{bmatrix} x' \cr y' \cr z' \cr w' \cr \end{bmatrix} = \begin{bmatrix} cos\beta & 0 & sin \beta & 0 \cr 0 & 1 & 0 & 0 \cr -sin\beta & 0 & cos \beta & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x \cr y \cr z \cr w \cr \end{bmatrix} $
$P'= R_y(\beta) \bullet P$
A matrix $$M = \begin{bmatrix} m_{11} & m_{12} & m_{13} \cr m_{21} & m_{22} & m_{33} \cr m_{31} & m_{32} & m_{33} \cr \end{bmatrix}$$
is orthogonal iff:
$$MM^{T} = I$$
This is equivalent to:
$$M^{-1} = M^{T}$$
Let
$$r_{1} = \begin{bmatrix} m_{11} & m_{12} & m_{13} \end{bmatrix}$$ $$r_{2} = \begin{bmatrix} m_{21} & m_{22} & m_{23} \end{bmatrix}$$ $$r_{3} = \begin{bmatrix} m_{31} & m_{32} & m_{33} \end{bmatrix}$$
then
$$r_{1} \cdot r_{1} = r_{2} \cdot r_{2} = r_{3} \cdot r_{3} = 1 $$ $$r_{i} \cdot r_{j} = 0\ \ i=1,2,3 \ \ j=1,2,3 \ \ i\ne j$$
We can conclude that:
Note 1: that $r_1$, $r_2$ and $r_3$ form a non-canonical basis
Note 2: that a rotation matrix is always orthogonal
Let $M$ be an affine transformation matrix such that:
$$P'=MP$$
Let $M^{-1}$ be the inverse of $M$. Observe that:
$$M^{-1}P'=M^{-1}MP=(M^{-1}M)P=IP=P$$
Consider the following sequence of transformations:
$P_1=M_1P,$ $P_2=M_2P_1,$ $...,$ $P_n=M_nP_{n-1}$
which is the same as: $P_n=M_n^*P$, where $M_n^*=M_n...M_2M_1$
Mnemonic 1: The (right-to-left) $M_1M_2...M_n$ transformation sequence is performed respect to a world (fixed) coordinate system
Mnemonic 2: The (left-to-right) $M_n,...M_2M_1$ transformation sequence is performed respect to a local (mutable) coordinate system
$T(x_f,y_f)S(sx,sy)T(-x_f,-y_f)$ Processing implementation
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: default shader (applyMatrix())
float xr=500, yr=250; float beta = -QUARTER_PI; void draw() { background(0); // We do the rotation as: T(xr,yr)Rz(β)T(−xr,−yr) // 1. T(xr,yr) applyMatrix(1, 0, 0, xr, 0, 1, 0, yr, 0, 0, 1, 0, 0, 0, 0, 1); // 2. Rz(β) applyMatrix(cos(beta), -sin(beta), 0, 0, sin(beta), cos(beta), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); // 3. T(−xr,−yr) applyMatrix(1, 0, 0, -xr, 0, 1, 0, -yr, 0, 0, 1, 0, 0, 0, 0, 1); // drawing code follows }
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: default shader (translation() and rotation())
float xr=500, yr=250; float beta = -QUARTER_PI; void draw() { background(0); // 1. T(xr,yr) translate(xr, yr); // 2. Rz(β) rotate(beta); // 3. T(−xr,−yr) translate(-xr, -yr); // drawing code follows }
Hence translate(), rotate(), applies the transformation to the current modelview
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: simple shader
PShader simpleShader; void setup() { size(700, 700, P3D); // simple custom shader simpleShader = loadShader("simple_frag.glsl", "simple_vert.glsl"); shader(simpleShader); }
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: simple shader
uniform mat4 transform; attribute vec4 vertex; attribute vec4 color; varying vec4 vertColor; void main() { gl_Position = transform * vertex; vertColor = color; }
Since here we use the default uniforms (transform) and attributes (vertex, color) the rest of the sketch remains the same
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: unal shader
PShader unalShader; PMatrix3D modelview; void setup() { size(700, 700, P3D); // unal custom shader unalShader = loadShader("unal_frag.glsl", "unal_vert.glsl"); shader(unalShader); modelview = new PMatrix3D(); }
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: unal shader
void draw() { background(0); //load identity modelview.reset(); // 1. T(xr,yr) modelview.translate(xr, yr); // 2. Rz(β) modelview.rotate(beta); // 1. T(-xr,-yr) modelview.translate(-xr, -yr); emitUniforms(); // drawing code follows }
void emitUniforms() { //hack to retrieve the Processing projection matrix PMatrix3D projectionTimesModelview = new PMatrix3D(((PGraphics2D)g).projection); projectionTimesModelview.apply(modelview); //GLSL uses column major order, whereas processing uses row major order projectionTimesModelview.transpose(); unalShader.set("unalMatrix", projectionTimesModelview); }
$T(x_r,y_r)R_z(\beta)T(-x_r,-y_r)$ Processing implementation: unal shader
uniform mat4 unalMatrix; attribute vec4 vertex; attribute vec4 color; varying vec4 unalVertColor; void main() { gl_Position = unalMatrix * vertex; unalVertColor = color; }Note the use of the custom uniform unalMatrix instead of the default transform The Shader Programming for Computational Arts and Design paper discusses API simplicity and flexibility in shader programming
let $v = p_2 - p_1$
$u = v / |v| = (a, b, c)$
$a = (x_2 - x_1) / |v|$
$b = (y_2 - y_1) / |v|$
$c = (z_2 - z_1) / |v|$
$u = (a, b, c)$
$u'= (0,b,c)$
$\cos \alpha = (u' \bullet u_z) / ( |u'| |u_z| )$, note that $|u_z| = 1$
$\cos \alpha = c/d$, where $d = |u'| = \sqrt{(b^2 + c^2)}$
since $\cos ^ 2 \alpha + \sin ^ 2 \alpha = 1$
$\sin \alpha = b/d$
$u = (a, b, c)$
$u'= (0,b,c)$
$u''=(a,0,d)$, $d = sqrt{(b^2+c^2)}$
$\cos \lambda = (u'' \bullet u_z) / ( |u''| |u_z| )$
since $|u''|=|u_z|=1$
$\cos \lambda = d$
since $\cos ^ 2 \lambda + \sin ^ 2 \lambda = 1$ and $|u| = 1$
$\sin \lambda = a$, note that the actual angle we need is $-\lambda$
$\sin -\lambda = -a$ (unlike $\cos$, $\sin$ is an odd function)
$ R_y(\lambda) * R_x(\alpha) = \begin{bmatrix} u_{x'1} & u_{x'2} & u_{x'3} & 0 \cr u_{y'1} & u_{y'2} & u_{y'3} & 0 \cr u_{z'1} & u_{z'2} & u_{z'3} & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} $
where
$u_{z'}=u$
$u_{x'}$ is any orthogonal vector to $u_{z'}$
$u_{y'} = u \times u_{x'}$
missing:
Affine transformations: Rotation: use orthogonality to compute R_y(\lambda) * R_x(\alpha) Affine transformations: Rotation: Quaternions magic Affine transformations: Rotation: Rodrigues' rotation formulaMnemonic 2: The (left-to-right) $M_n,...M_2M_1$ transformation sequence is performed respect to a local (mutable) coordinate system
Local coordinate systems are commonly referred to as "frames"
Consider the function axes() which draws the X (horizontal) and Y vertical) axes:
void axes() { pushStyle(); // X-Axis strokeWeight(4); stroke(255, 0, 0); fill(255, 0, 0); line(0, 0, 100, 0);//horizontal red X-axis line text("X", 100 + 5, 0); // Y-Axis stroke(0, 0, 255); fill(0, 0, 255); line(0, 0, 0, 100);//vertical blue Y-axis line text("Y", 0, 100 + 15); popStyle(); }
let's first call the axes() function to see what it does:
void draw() { background(50); axes(); }
now let's call it again, but pre-translating it first:
void draw() { background(50); axes(); translate(300, 180);//translation axes();//2nd call }
let's add a rotation to the second axes() call:
void draw() { background(50); axes(); translate(300, 180); rotate(QUARTER_PI / 2);//rotation after translation axes();//2nd call }
let's do something similar with a third axes() call:
void draw() { background(50); axes(); translate(300, 180); rotate(QUARTER_PI/2); axes(); translate(260, -180); rotate(-QUARTER_PI); scale(1.5);//even scaling it axes();//3rd call }
see the result when we animate only the first rotation;
void draw() { background(50); axes(); translate(300, 180); rotate(QUARTER_PI/2 * p.frameCount);//animation line axes(); translate(260, -180); rotate(-QUARTER_PI); scale(1.5); axes(); }
and now see the result when we animate only the second rotation;
void draw() { background(50); axes(); translate(300, 180); rotate(QUARTER_PI/2); axes(); translate(260, -180); rotate(-QUARTER_PI * p.frameCount);//animation line scale(1.5); axes(); }
A frame is defined by an affine (composed) transform: $M_i^*, 1 \leq i \leq 3$ read in left-to-right order (goto mnemonic 2)
A scene-graph is a directed acyclic graph (DAG) of frames which root is the world coordinate system
World ^ | L1 ^ |\ L2 L3
void drawModel() { // define a local frame L1 (respect to the world) pushMatrix(); affineTransform1(); drawL1(); // define a local frame L2 respect to L1 pushMatrix(); affineTransform2(); drawL2(); // "return" to L1 popMatrix(); // define a local coordinate system L3 respect to L1 pushMatrix(); affineTransform3(); drawL3(); // return to L1 popMatrix(); // return to World popMatrix(); }
World ^ |\ ... Eye ^ |\ ... ...
Let the eye frame transform be defined, like it is with any other frame, as: $M_{eye}^*$
The eye transform is therefore: $\left.M_{eye}^{*}\right.^{-1}$ (goto vertex shader)
For example, for an eye frame transform: $M_{eye}^*=T(x,y,z)R(\beta)S(s)$
The eye transform would be: $\left.M_{eye}^{*}\right.^{-1}=S(1/s)R(-\beta)T(-x,-y,-z)$
$M_{eye}^*$ would position (orient, scale, ...) the eye frame in the world, but want it to be the other way around (i.e., draw the scene from the eye point-of-view)
World ^ |\ L1 Eye ^ |\ L2 L3
void draw() { // the following sequence would position (orient, scale, ...) the eye frame in the world: // translate(eyePosition.x, eyePosition.y); // rotate(eyeOrientation); // scale(eyeScaling) // drawEye(); scale(1/eyeScaling); rotate(-eyeOrientation); translate(-eyePosition.x, -eyePosition.y); drawModel(); }
Let $P_e$ be a point in eye space and $P_c$ a point in clip space, we seek:
$$P_e = [x_e,y_e,z_e]\xrightarrow{\text{map}}P_c = [x_c,y_c,z_c]$$
$$x_e \in [l,r] \rightarrow x_c \in [-1,1], y_e \in [b,t] \rightarrow y_c \in [-1,1], z_e \in [n,f] \rightarrow z_c \in [-1,1]$$
|---------------*---------| -> |-------------------*--------------| min u max min' u' max'
The linear conversion is given by:
$$u' = min'+(u-min)(\Delta u')/(\Delta u)$$
where $\Delta u=max-min$, and $\Delta u'=max'-min'$
which may be re-written as:
$$u' = uS_u + T_u$$ $$S_u=\Delta u'/\Delta u$$ $$T_u=(min'\Delta u - min\Delta u')/\Delta u$$
|---------------*---------| -> |-------------------*--------------| min u max -1 u' 1
$$u' = uS_u + T_u$$ $$S_u=2/(max-min)$$ $$T_u=-(max+min)/(max-min)$$
$$[x_e,y_e,z_e]\xrightarrow{\text{map}}[x_c,y_c,z_c]$$ $$x_e \in [l,r] \rightarrow x_c \in [-1,1], y_e \in [b,t] \rightarrow y_c \in [-1,1], z_e \in [n,f] \rightarrow z_c \in [-1,1]$$
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} S_{x_e} & 0 & 0 & T_{x_e} \cr 0 & S_{y_e} & 0 & T_{y_e} \cr 0 & 0 & S_{z_e} & T_{z_e} \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$P_c = Ortho(S_{x_e/y_e/z_e},T_{x_e/y_e/z_e}) \bullet P_e$
$$[x_e,y_e,z_e]\xrightarrow{\text{map}}[x_c,y_c,z_c]$$ $$x_e \in [l,r] \rightarrow x_c \in [-1,1], y_e \in [b,t] \rightarrow y_c \in [-1,1], z_e \in [n,f] \rightarrow z_c \in [-1,1]$$
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 2 \above 1pt (r-l) & 0 & 0 & -(r+l) \above 1pt (r-l) \cr 0 & 2 \above 1pt (t-b) & 0 & -(t+b) \above 1pt (t-b) \cr 0 & 0 & -2 \above 1pt (f-n) & -(f+n) \above 1pt (f-n) \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$P_c = Ortho(l,r,b,t,n,f) \bullet P_e$
$$[x_e,y_e,z_e]\xrightarrow{\text{map}}[x_c,y_c,z_c]$$ $$x_e \in [-r,r] \rightarrow x_c \in [-1,1], y_e \in [-t,t] \rightarrow y_c \in [-1,1], z_e \in [n,f] \rightarrow z_c \in [-1,1]$$
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 1 \above 1pt r & 0 & 0 & 0 \cr 0 & 1 \above 1pt t & 0 & 0 \cr 0 & 0 & -2 \above 1pt (f-n) & -(f+n) \above 1pt (f-n) \cr 0 & 0 & 0 & 1 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$P_c= Ortho(r,t,n,f) \bullet P_e$
Let $P_e$ be a point in eye space and $P_n$ a point in NDC, we seek:
$$P_e = [x_e,y_e,z_e,w_e(=1)]\xrightarrow{\text{map}}P_c = [x_c,y_c,z_c,w_c(\neq 1)]$$
$$P_c = [x_c,y_c,z_c,w_c(\neq 1)]\xrightarrow[\text{divide}]{\text{perspective}}P_n = [x_n(=x_c/w_c),y_n(=y_c/w_c),z_n(=z_c/w_c),1]$$
$${x_p\above 1pt x_e}= {-n\above 1pt z_e}$$ $$x_p= {nx_e\above 1pt -z_e}$$
$${y_p\above 1pt y_e}= {-n\above 1pt z_e}$$ $$y_p= {ny_e\above 1pt -z_e}$$
which means ${\color{red} {w_c}}=-z_e$
$$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} . & . & . & . \cr . & . & . & . \cr . & . & . & . \cr 0 & 0 & {\color{red} {-1}} & 0 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $$
$$\begin{bmatrix} {\color{blue} {x_n}} \cr {\color{blue} {y_n}} \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 2 \above 1pt (r-l) & 0 & 0 & -(r+l) \above 1pt (r-l) \cr 0 & 2 \above 1pt (t-b) & 0 & -(t+b) \above 1pt (t-b) \cr . & . & . & . \cr . & . & . & . \cr \end{bmatrix} \bullet \begin{bmatrix} {\color{green} {x_p}} \cr {\color{green} {y_p}} \cr z_e \cr w_e(=1) \cr \end{bmatrix} $$
solving for ${\color{blue} {x_n,y_n}}$ we get: ${\color{blue} {x_n}}= {2{\color{green} {x_p}}\above 1pt r-l}-{r+l\above 1pt r-l},{\color{blue} {y_n}} = {2{\color{green} {y_p}}\above 1pt t-b}-{t+b\above 1pt t-b}$
since ${\color{blue} {x_n}}={x_c\above 1pt w_c}$ and ${\color{blue} {y_n}}={y_c\above 1pt w_c}$ , solving for ${\color{red} {x_c,y_c}}$ in terms of $x_e,y_e,z_e$ , we get: ${\color{red} {x_c}}= {2nx_e\above 1pt r-l}+{(r+l)z_e\above 1pt r-l},{\color{red} {y_c}}= {2ny_e\above 1pt t-b}+{(t+b)z_e\above 1pt t-b}$
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 2n \above 1pt r-l & 0 & r+l \above 1pt r-l & 0 \cr 0 & 2n \above 1pt t-b & t+b \above 1pt t-b & 0 \cr . & . & . & . \cr 0 & 0 & -1 & 0 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 2n \above 1pt r-l & 0 & r+l \above 1pt r-l & 0 \cr 0 & 2n \above 1pt t-b & t+b \above 1pt t-b & 0 \cr 0 & 0 & {\color{green} A} & {\color{green} B} \cr 0 & 0 & -1 & 0 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$z_n=z_c/w_c={Az_e+Bw_e\above 1pt -z_e}={Az_e+B\above 1pt -z_e}$
To find $A$ and $B$, use the map relation $z_e \in [n,f] \rightarrow z_n \in [-1,1]$ and replace them above (twice)
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 2n \above 1pt r-l & 0 & r+l \above 1pt r-l & 0 \cr 0 & 2n \above 1pt t-b & t+b \above 1pt t-b & 0 \cr 0 & 0 & -(f+n) \above 1pt f-n & -2fn \above 1pt f-n \cr 0 & 0 & -1 & 0 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$P_c = Persp(l,r,b,t,n,f) \bullet P_e$
$\begin{bmatrix} x_c \cr y_c \cr z_c \cr w_c \cr \end{bmatrix} = \begin{bmatrix} 1 \above 1pt \tan (fovy/2)aspectRatio & 0 & 0 & 0 \cr 0 & \tan (fovy/2) & 0 & 0 \cr 0 & 0 & -(f+n) \above 1pt f-n & -2fn \above 1pt f-n \cr 0 & 0 & -1 & 0 \cr \end{bmatrix} \bullet \begin{bmatrix} x_e \cr y_e \cr z_e \cr w_e(=1) \cr \end{bmatrix} $
$P_c = Persp(fovy,aspectRatio,n,f) \bullet P_e$