p5.projection.js
is a library
that hopes makes it easy to map a p5js sketch onto a projected
surface to correct for 3D shapes, projector mis-alignment or
even moving surfaces. It requires four uv
coordinates and four
xy
coordinates to create the sixteen values for applyMatrix()
that will make the projected image line up with the real world.
For many applications it is enough to click on the four corners
of the projected image and map those to the canvas coordinates
(0,0), (1920,0), (1920,1080) and (0,1080), then call into the
update()
function to compute the forward and inverse matrices.
Inspired by OpenCV perspectiveTransform()
Include math.js
and p5.projection.js
in your code and create ProjectionMatrix
object.
In your setup()
function create a WEBGL
canvas and in your draw()
function,
call mat.apply()
to skew the drawing to the projected frame. You can add a mouseClicked()
to update the mat.outPts
array with the correct corners.
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/9.5.1/math.js"></script>
<script src="https://cdn.jsdelivr.net/gh/osresearch/p5.projection@0.2.1/library/p5.projection.js"></script>
</head>
<body>
<script>
let mat = new ProjectionMatrix();
function setup()
{
createCanvas(windowWidth-10, windowHeight-10, WEBGL);
mat.edit = true;
}
function draw()
{
background(0);
mat.apply();
fill(80);
stroke(150);
strokeWeight(2);
rect(400, 200, 200, 200);
rect(600, 200, 200, 200);
rect(800, 200, 200, 200);
rect(800, 400, 200, 200);
rect(600, 600, 200, 200);
}
</script>
</body>
</html>
Chessboard example shows how to translate screen coordinates into canvas coordinates and back, as well as demonstrates line drawing and fonts in the projection mapping mode.
Multiple projected surfaces on a cube shows how multiple
ProjectionMatrix
objects can be created to allow mapping to multiple real world
surfaces, such as onto the three visible faces of a cube.
Mondrian art rear-projected on glass doors.
This was the art installation that started this project.
Halloween trees and eyeballs, on the same glass doors.
Just the fractal trees, which slowly wave in a creepy way, blown by the GPU fan that spins up to draw all those line segments.
You don’t need to understand this part to use the library, although you might find it interesting to see how it works under the covers.
The XY drawing coordinates are translated into UV screen coordinates
using a normal perspective transform matrix C that maps from (x,y)
to (u,v,z)
space through C xy = uvz
:
/ c00 c01 c02 \ / x \ / u \
| c10 c11 c12 | * | y | = | v |
\ c20 c21 1 / \ 1 / \ z /
Multiplying the matrix and applying the perspective transform to make u
and v
shrink towards the center as they get further away from the “camera”:
zi = c20*xi + c21*yi + 1
ui = (c00*xi + c01*yi + c02) / zi
vi = (c10*xi + c11*yi + c12) / zi
The problem is how to find the eight variable entries in the C
matrix
so that the xy
points correctly map to output real-world coordinates
in uv
space. Since the library takes in four xy
points and four corresponding
uv
points, this is eight knowns which should uniquely map to the eight
unknowns.
To make it easier to solve, it is best to rewrite the uv
equations and
expand them out so that the variables are isolated.
ui * zi = c00*xi + c01*yi + c02
vi * zi = c10*xi + c11*yi + c12
Each of these can be expanded
ui * (c20*xi + c21*yi + 1) = c00*xi + c01*yi + c02
c20*(ui*xi) + c21*(ui*yi) + ui = c00*xi + c01*yi + c02
ui = c00*xi + c01*yi + c02 - c20*(ui*xi) - c21*(ui*yi)
and
vi * (c20*xi + c21*yi + 1) = c10*xi + c11*yi + c12
c20*(vi*xi) + c21*(vi*yi) + vi = c10*xi + c11*yi + c12
vi = c10*xi + c11*yi + c12 - c20*(vi*xi) - c21*(vi*yi)
These eight equations can now be written as:
u0 = c00*x0 + c01*y0 + c02 - c20*(u0*x0) - c21*(u0*y0)
u1 = c00*x1 + c01*y1 + c02 - c20*(u1*x1) - c21*(u1*y1)
u2 = c00*x2 + c01*y2 + c02 - c20*(u2*x2) - c21*(u2*y2)
u3 = c00*x3 + c01*y3 + c02 - c20*(u3*x3) - c21*(u3*y3)
v0 = c10*x0 + c11*y0 + c12 - c20*(v0*x0) - c21*(v0*y0)
v1 = c10*x1 + c11*y1 + c12 - c20*(v1*x1) - c21*(v1*y1)
v2 = c10*x2 + c11*y2 + c12 - c20*(v2*x2) - c21*(v2*y2)
v3 = c10*x3 + c11*y3 + c12 - c20*(v3*x3) - c21*(v3*y3)
This is the linear system:
/ x0 y0 1 0 0 0 -x0*u0 -y0*u0 \ /c00\ /u0\
| x1 y1 1 0 0 0 -x1*u1 -y1*u1 | |c01| |u1|
| x2 y2 1 0 0 0 -x2*u2 -y2*u2 | |c02| |u2|
| x3 y3 1 0 0 0 -x3*u3 -y3*u3 |.|c10|=|u3|
| 0 0 0 x0 y0 1 -x0*v0 -y0*v0 | |c11| |v0|
| 0 0 0 x1 y1 1 -x1*v1 -y1*v1 | |c12| |v1|
| 0 0 0 x2 y2 1 -x2*v2 -y2*v2 | |c20| |v2|
\ 0 0 0 x3 y3 1 -x3*v3 -y3*v3 / \c21/ \v3/
The variables c_ij
can be computed by inverting the 8x8 matrix,
or using math.js function lusolve()
.
The p5 applyMatrix()
takes either 6 elements, in which case only affine transforms are supported,
or 16 elements, which allows arbitrary XYZ to UVW projections.
Since the 2D values have z=0
, the equation that the matrix should apply is:
ui = c00*xi + c01*yi + 0*zi + c02
vi = c10*xi + c11*yi + 0*zi + c12
zi = c20*xi + c21*yi + 0*zi + 1
This means that the columns of the arguments to applyMatrix()
must be c00, c01, 0, c02
, c10, c11, 0, c12
, and c20, c21, 0, 1
:
applyMatrix(
mat[0], mat[3], 0, mat[6],
mat[1], mat[4], 0, mat[7],
0, 0, 1, 0,
mat[2], mat[5], 0, mat[8]);
The helper function applies this matrix for the caller.