In this MP you will
This MP is core, with no elective components. It assumes you have already completed the Terrain MP.
You will submit a webpage that has
Submit an HTML file and any number of js, glsl, json, css, and image files. Do not include spaces in file names as the submission server does not process them well.
You are welcome to use a JavaScript math library, such as the one used in in-class examples or others you might know.
Do not have any <input ...>
elements in your HTML.
Instead, as soon as the page loads fill a full-screen canvas with a
randomly-generated fractal terrain, such as you did as part of the Terrain MP. You are welcome to use one of the
terrain add-ons such as weathering, cliffs, height map,
textures, or shine
map or other variants of that type.
When the user is holding down certain keys, the camera should move smoothly until those keys are released.
Key | Translation |
---|---|
w |
Forward – the direction the camera is currently looking |
s |
Backward – the opposite of w |
a |
Leftward – without changing the direction the camera is looking, move so the things it views pass before it left-to-right as the camera moves to its left |
d |
Rightward – the opposite of a |
Key | Rotation |
---|---|
ArrowUp |
Look up – pitch the camera to look closer to the sky |
ArrowDown |
Look down – pitch the camera to look closer to the ground |
ArrowLeft |
Turn left – pivot along the up-down axis to look to the left |
ArrowRight |
Turn right – pivot along the up-down axis to look to the right |
The JavaScript event system does not directly provide a way of discovering what keys are being held down. Instead, you’ll need to listen to when keys are pressed and when they are released and keep track of which have been pressed but not released. One way to do this is
window.keysBeingPressed = {}
window.addEventListener('keydown', event => keysBeingPressed[event.key] = true)
window.addEventListener('keyup', event => keysBeingPressed[event.key] = false)
after which you can add something like if (keysBeingPressed['ArrowUp']) { /* ... */ }
to your every-frame code.
As noted in the listen to keys section, the camera should move relative to its current view. There are two basic approaches to this: the easy one and the nicely-behaving one.
The view matrix stores the camera’s position and orientation, so moving the camera can be adjusting the view matrix.
Recall that matrices change the coordinate system. Thus if I have a rotation matrix R and do something like V := V R then R is applied before V changes the coordinate system, meaning V will have be rotated in the world coordinate system around the world’s origin. If I instead do something like V := R V then R is applied after V’s change of coordinate system meaning V will be rotated in its coordinate system (i.e., relative to the on-screen axes) and around its origin (i.e. the eye location). Thus, this second approach can implement the flight required of this task.
This is easy, but not very nice because it looses track of the fact
that humans always want down
to point in roughly the same
direction. With this approach that is not done; if look down, then left,
then up, then right you can end up pointing in the same direction as
before but with the camera no longer right-side-up.
This approach also makes it much harder to add limits on the camera’s motion, such as the elective follow-on Drive MP does.
In principle a camera has three degrees of freedom: a 3D location and 3 aspects of orientation. But we only want 2 aspects of orientation to be controlled by the user because the camera should always be right-side-up. Conveniently, we can get those 5 degrees of freedom by storing two 3D vectors with direct meaning: the eye location and the forward direction, where forward is always a unit vector.
From the camera’s forward direction and the global up direction, we can get the camera’s right direction with a (normalized) cross-product; from the camera’s forward and right directions we can get the camera’s up direction with another cross product.
Translating the camera means adding (a scalar multiple of) the camera’s forward or right direction to its eye location.
Rotating the camera means rotating its forward vector around its right vector (to pitch up or down) or the global up direction (to turn left or right).
The view matrix can be constructed from these vectors as V = R T where T translates the eye to the origin and R puts the camera’s right direction on the x axis, its up direction on the y axis, and its forward direction on the -z axis like so: R = \begin{bmatrix}r_x&r_y&r_z&0\\u_x&u_y&u_z&0\\-f_x&-f_y&-f_z&0\\0&0&0&1\end{bmatrix}
This means that the matrix
\begin{bmatrix}a&b&c&d\\e&f&g&h\\i&j&k&l\\m&n&o&p\end{bmatrix}
is provided in code as
, e, i, m
[ a, b, f, j, n
, c, g, k, o
, d, h, l, p
]
This process is a bit more work than the easier approach, but it results in two benefits. First, we can easily rotate around the world up axis but the camera’s location, thus avoiding the camera ever going upside down. Second, we have the camera’s location in world coordinates which we can use to constrain its motion, such as we do in the elective Drive MP.
On both your development machine and when submitted to the submission server and then viewed by clicking the HTML link, the resulting page should show a random terrain. When keys are held down the camera should fly smoothly through the scene. One example might be the following:
This MP focuses on view matrices. The Orbits MP focused on model matrices. Together, those two make up the model-view matrix used in many vertex shaders.
Can you put these together yourself? Try the following:
If you can do this you are probably ready to handle whatever scene arrangement and camera motion you want.