In which you extend the mouse position into a ray shooting from the eye through the pixel and into the 3D scene.
The mouse is not just a single point on the screen. It is the accumulation of the many 3D positions that project onto that pixel. To figure out what the mouse is over, you must turn the mouse position not into a single 3D position but rather a ray or line segment that passes through those many 3D positions. This technique is called raycasting.
Explore the idea of shooting a ray through the mouse into the scene by clicking on this renderer:
Each time you click, a cylinder is added to the scene. It starts at the position on the near clipping plane that projects to the mouse cursor. It ends at the position on the far clipping plane that projects to the mouse cursor. Rotate the scene by dragging to see the complete cylinders.
The mouse listener in this renderer walks the mouse position backward through the transformation pipeline exactly as you read about earlier. It first turns the mouse's pixel space coordinates into normalized device coordinates:
function onMouseUp(event) {
const normalizedPosition = new Vector4(
event.clientX / canvas.width * 2 - 1,
(canvas.height - event.clientY) / canvas.height * 2 - 1,
-1,
1,
);
// ...
}
The mouse doesn't have a z-coordinate of its own. This code sets it to -1, whichs means that normalized position is on the near plane of the unit cube of normalized space. The homogeneous coordinate is set to 1 in order to treat the coordinates as a position rather than a vector.
Next the normalized position is untransformed by the inverses of the matrices:
function onMouseUp(event) {
// ...
let eyePosition = eyeFromClip.multiplyVector(normalizedPosition);
eyePosition = eyePosition.scalarDivide(eyePosition.w);
let worldPosition = worldFromEye.multiplyVector(eyePosition);
let modelPosition = modelFromWorld.multiplyVector(worldPosition);
from = modelPosition;
}
Remember how you skipped over clip space earlier? To correct for that skipping, you must divide the eye space position by its homogeneous coordinate.
The ray starts at from
. It heads to the to
position on the far clipping plane that projects to the mouse position. You find this position just as you did from
. The only difference is that the z-coordinate of the normalized position is 1:
function onMouseUp(event) {
// ...
normalizedPosition.z = 1;
eyePosition = eyeFromClip.multiplyVector(normalizedPosition);
eyePosition = eyePosition.scalarDivide(eyePosition.w);
worldPosition = worldFromEye.multiplyVector(eyePosition);
modelPosition = modelFromWorld.multiplyVector(worldPosition);
const to = modelPosition.scalarDivide(modelPosition.w);
// add in a cylinder spanning from -> to
}
This renderer fits a cylinder between the from
and to
positions. Some applications might use it to figure out what object is being clicked on. The two positions are used to form a ray, and then objects in the scene are tested to see if they intersect the ray. Other applications might use the ray to direct a projectile launched by the user.