Matrices and ray tracing to transformed surfaces
For your fun and convenience, with this week's package we've included information about the mouse cursor. You can use this to create work which responds interactively to the user's mouse.
You can access it from your fragment shader,
via uniform vec3 variable
uniform vec3 uCursor; x: -1.0 ≤ uCursor.x ≤ 1.0 // left to right y: -1.0 ≤ uCursor.y ≤ 1.0 // bottom to top z: uCursor.z == 0.0 if mouse is up, 1.0 if mouse is down
USING MATRICES TO TRANSFORM POINTS
Our major use of 4x4 matrices will be to perform linear transformations. These are any geometric transformations that always keep straight lines straight.
Internally, the 16 numerical values in a matrix can be stored in either column major order, as shown below, or row major order. For this course, you will be storing matrices in column major order, because that is consistent with how matrices are stored in shader programs on the GPU.
The basic operation is multiplying a column vector on the left by a 4x4 matrix, thereby transforming that column vector. The operation itself is essentially four dot products, one for each row of the matrix:
There are a few primitive matrix generating functions. The matrices returned by these functions can be used to generate all other matrices. Each of these matrices represents a simple geometric intuition:
In the chart below, "c" represents cos(θ) and "s" represents sin(θ), where θ is angle of rotation.
[ 1 0 0 0 0 1 0 0 0 0 1 0 x y z 1 ]
There are some basic operations for matrices:
In the course notes I will provide you with an
BUILDING MATRIX HIERARCHIES
Not this week, but soon, you will be using 4x4 matrices to build hierarchical movement models.
Any hierarchical structure with moving parts, such as an electric motor or a human body, is essentially a tree structure. Traversing such a structure therefore requires some sort of internal data stack.
In our case, each element of that stack is a 4x4 matrix:
save() stack[top+1] = copy(stack[top]); ++top; restore() --top;
SENDING MATRIX DATA FROM THE CPU TO THE GPU
Eventually you will be using matrices on the CPU to model geometry that you create there. But for now, you will just be using them in our ray tracer on the GPU.
In order to do this, you need to be able to send the 16 numerical values of a matrix from the CPU down to the GPU. You can do this as follows:
ACCESSING A MATRIX ON THE GPU
You can directly specify the 16 values of a matrix variable in a shader program as follows:
mat4 M = mat4(1.,0.,0.,0., 0.,1.,0.,0., 0.,0.,1.,0., 2.,3.,4.,1.);When you do the above, you get the following column major matrix:
1 0 0 2 0 1 0 3 0 0 1 4 0 0 0 1You can then access a single column of this matrix as follows:
M // == vec4(2.,3.,4.,1.)You can access a single element of this matrix in two different ways:
M.y // == 3. M // == 3. (this line and the previous line are equivalent)Accessing a row of this matrix is a little more awkward:
vec4(M.y,M.y,M.y,M.y) // == vec4(0.,1.,0.,3.)
RAY TRACING: TRANSFORMING A PLANE SURFACE
To use a transformation matrix for ray tracing, you will really want to use its corresponding inverse matrix.
I suggest that in your definition of
gl.uniformMatrix4fv(state.uShapesLoc.matrix , false, someArray); gl.uniformMatrix4fv(state.uShapesLoc.imatrix, false, inverse(someArray));
To use a 4x4 matrix to transform a ray traced polyhedron, in your fragment shader you can simply transform each of the planes that define one of its component halfspaces. For example, to ray trace to a transformed cube, you would transform each of its 6 defining planes as part of your fragment shader computation.
Because the goal is to preserve the relationship between any halfspace S and any point V:
S•V ≤ 0we need to transform
S'•(M•V) ≤ 0produces the same result.
It is easy to see that this will be the case when:
S' = S•M-1because:
(S•M-1)•(M•V) == S•(M-1•M)•V == S•VSo if you have a function that computes ray/halfspace intersection in your fragment shader program, you can just insert the following assignment right at the beginning of the function:
S = S * uShapes[i].imatrix;
RAY TRACING: TRANSFORMING A QUADRATIC SURFACE
In the following section, we show how to do the same kind of transformation when ray tracing in your fragment shader to second order surfaces, such as spheres.
In the data you pass down to your fragment shader, you can represent any general quadratic surface (eg: sphere, cylindrical tube, cone, parabaloid, etc) as a 4x4 matrix, where you actually use only 10 of the 16 available matrix slots:
mat4 S = mat4(a,b,c,d, 0.,e,f,g, 0.,0.,h,i, 0.,0.,0.,j);A key observation, which I learned from the great computer graphics pioneer Jim Blinn, is that the above matrix can be used to represent a general quadratic equation in three variables by expanding the following inequality:
VT S V ≤ 0Where:
This means you now need to find a different
(MV)T S' MV == VT S VNote also, by the way, that when you take the transpose, you need to reverse the order of the matrix/vector multiplication:
(M V)T == VT MTFollowing the same approach you used for half spaces, you will want to modify S as follows in your fragment shader:
VT MT M-1T S M-1 M V == VT S VYou can implement this in your shader via the following code:
S ⇐ transpose(M-1) * S * M-1;After doing that, you need to rearrange the values in S to ensure all of the non-zero values end up in just those 10 slots:
S = mat4( S.x, S.y+S.x, S.z+S.x, S.w+S.x, 0., S.y , S.z+S.y, S.w+S.y, ... );You are really trying to intersect S with a ray V+tW, and your goal is to find the value of t where the resulting point is on the surface of S.
Expanding out the expression (V+tW)T S (V+tW):
(Note that the six numbers
a (vx + t wx) (vx + t wx) + b (vx + t wx) (vy + t wy) + c (vx + t wx) (vz + t wz) + d (vx + t wx) + e (vy + t wy) (vy + t wy) + f (vy + t wy) (vz + t wz) + g (vy + t wy) + h (vz + t wz) (vz + t wz) + i (vz + t wz) + jYour goal is to end up with a quadratic equation in t that you can then solve:
A t2 + B t + C ≤ 0You can multiply out the large expression above and then regroup all of the terms to get the three coefficients of the quadratic equation:
A = a wx wx + b wx wy + c wx wz + e wy wy + f wy wz + h wz wz B = a (vx wx + vx wx) + b (vx wy + vy wx) + c (vx wz + vz wx) + d wx + e (vy wy + vy wy) + f (vy wz + vz wy) + g wy + h (vz wz + vz wz) + i wz C = a vx vx + b vx vy + c vx vz + d vx + e vy vy + f vy vz + g vy + h vz vz + i vz + jNow that you have A, B and C, you can just solve quadratic equation for t, to find where the ray intersects the surface.
To sum up the above steps -- all of which should be done in your fragment shader:
FINDING THE SURFACE NORMAL
You can now easily find the surface normal, because it's just the gradient of the polynomial.
That means you can just take the partial derivatives in x,y,z and normalize the result:
(x,y,z) = V + t W normal = normalize( [ 2ax + by + cz + d, 2ey + fz + g, 2hz + i ] )The next few sections show some convenient coefficient values for the original untransformed S, to make some standard useful shapes.
xx + yy + zz ≤ 1 ⟶ [ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,-1 ]
CYLINDRICAL TUBE COEFFICIENTS
xx + yy ≤ 1 ⟶ [ 1,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,-1 ]
INTERSECT YOUR CYLINDRICAL TUBE WITH TWO HALFSPACES TO MAKE A UNIT CYLINDER
z ≥ -1 ⟶ z + 1 ≥ 0 ⟶ -z - 1 ≤ 0 ⟶ [ 0, 0,-1,-1 ] z ≤ 1 ⟶ z - 1 ≤ 0 ⟶ [ 0, 0, 1,-1 ]
COOL VIDEO WE WATCHED
At the end of class we watched World Builder.
For your homework, which is due by class time on Monday October 7, do the following:
An updated library package that you can use, which also contains
As a reference, the image on the right was created using the below code.
I set CYLINDER, SPHERE, OCTAHEDRON and CUBE to constant values
that correspond to matching constants in my fragment shader,
to choose the type of ray tracing logic to use.