Camera Rays

Rye Terrell

February 10, 2019

Suppose you want to path (or ray) trace a scene using GLSL. You set up a full screen quad and pass it through your vertex shader into your fragment shader.
What’s next? Typically, you generate a camera ray, a ray that starts at your camera position and passes through the fragment being rendered. There are several
ways to skin this cat, and honestly, none of the ones I’ve come across feel particularly elegant. In this post, I’ll be presenting what I’ve settled on as an
efficient and reasonably intuitive method - my best of the bad, if you will.

Overview

We’ll be covering two ways to use this method - the perspective and orthographic projections. We’ll need a few things that are common
to both of these:

Full screen quad: A vertex attribute that defines the position of vertices composing two triangles that cover the entire rendering surface.

2D position vertices describing a full screen quad.

Camera position: The world-space position (x, y, z) of the camera.

Camera center: The world-space point (x, y, z) that the camera is looking at.

Up vector: The three dimensional vector that defines up. There is some overlap with this quantity and the vector defined by the camera position and
camera center. In this formulation, the up vector will be used to extract a roll, but not a pitch.

Aspect ratio: This is the rendering surface width divided by its height. Units don’t matter here, so pixels (or whatever else you want) for the width and
height are fine.

For both projections we’ll be creating a new vertex attribute of the same size and type as the full screen quad, but it will represent the vertices
defining a quad perpendicular to the view direction and intersecting the viewing frustum. For lack of a better term, I’ll dub this the ray quad. We’ll use
the graphics pipeline to interpolate across the ray quad in world space and use the fragment positions to construct a camera ray.

Symbols

Here’s a table of symbols you can reference while reading.

Symbol

Description

The normalized forward direction of the camera.

The normalized up direction of the camera. Used to define roll, but not pitch.

The normalized right direction of the camera.

, , ,

Each vertex of the full screen quad.

, , ,

Each vertex of the ray quad.

Camera position. The position of the camera in world space.

Camera center. The point in world space the camera is looking at.

,

The width and height of the rendering surface in any units.

The aspect ratio of the rendering surface.

The vertical field of view in radians. Used only in the perspective projection.

, , ,

The magnitude of the up, down, left, and right offsets from the center of the ray quad.

The scale of the orthographic projection.

Did I miss one? Hit me up on twitter and I’ll fill it in.

Perspective Projection

The first projection we’ll handle is the perspective projection. For this, we’ll need to define a vertical field of view, or . Once we have that,
the first step will be to calculate the normalized forward vector from the camera position and camera target.

Next we’ll calculate the normalized right vector from forward and up:

Then we’ll update the up vector so that it’s consistent with forward and right:

Next we need to calculate how far up, down, left, and right we need to move from the center of the ray quad to reach each of the four corners. If we assume
the ray quad intersects the view frustum one unit away from the camera position, the math boils down to this:

Let’s take a look at some code. First, a function that finds , , , and and
constructs a vertex attribute array from them:

functionrayQuad(Cp,Cc,up,fov,aspect){// Caluclate the normalized forward direction.constforward=vec3.normalize([],vec3.sub([],Cc,Cp));// Calculate the normalized right direction.constright=vec3.normalize([],vec3.cross([],forward,up));// Recalculate the normalized up direction.vec3.normalize(up,vec3.cross([],right,forward));// Calculate how far up, down, left, and right we need to move from the// center of the ray quad to find points a', b', c', and d'.constmu=Math.tan(fov/2);constmd=-mu;constmr=aspect*mu;constml=-mr;// Calculate Cp + forward to find the center of the ray quad.constZ=vec3.add([],Cp,forward);// Define vectors along up and right of lengths ml, mr, mu, and md.constmuUp=vec3.scale([],up,mu);constmdUp=vec3.scale([],up,md);constmrRight=vec3.scale([],right,mr);constmlRight=vec3.scale([],right,ml);// Find points a', b', c', and d'.consta=vec3.add([],Z,vec3.add([],mdUp,mlRight));constb=vec3.add([],Z,vec3.add([],mdUp,mrRight));constc=vec3.add([],Z,vec3.add([],muUp,mrRight));constd=vec3.add([],Z,vec3.add([],muUp,mlRight));// Construct an unindexed vertex attribute array from a', b', c', and d'.constuv=[];uv.push(...a);uv.push(...b);uv.push(...c);uv.push(...a);uv.push(...c);uv.push(...d);// Return the array.returnuv;}

Note that in addition to the ray quad vertex attribute, we also need to pass in the camera position in order to calculate the
ray direction.

Using the above code, we can perform ray-sphere intersection tests to ray trace some spheres:

Ray tracing spheres with perspective camera rays.

Orthographic Projection

Calculating the ray quad for the orthographic projection is very similar to the calculation for the perspective projection. We’ll need a scale quantity,
, that describes the extents of the orthographic projection. Once we have that, we can start by calculating the forward, up, and right unit vectors
in the same way we did for the perspective projections:

Then we’ll calculate , , , and , but we’ll use instead of the field of view:

The code for the orthographic projection is, again, very similar to that of the perspective projection. Let’s take a look.

functionrayQuad(Cp,Cc,up,scale,aspect){// Caluclate the normalized forward direction.constforward=vec3.normalize([],vec3.sub([],Cc,Cp));// Calculate the normalized right direction.constright=vec3.normalize([],vec3.cross([],forward,up));// Recalculate the normalized up direction.vec3.normalize(up,vec3.cross([],right,forward));// Calculate how far up, down, left, and right we need to move from the// center of the ray quad to find points a', b', c', and d'.constmu=scale;constmd=-scale;constmr=aspect*mu;constml=-mr;// Define vectors along up and right of lengths ml, mr, mu, and md.constmuUp=vec3.scale([],up,mu);constmdUp=vec3.scale([],up,md);constmrRight=vec3.scale([],right,mr);constmlRight=vec3.scale([],right,ml);// Find points a', b', c', and d'.consta=vec3.add([],Cp,vec3.add([],mdUp,mlRight));constb=vec3.add([],Cp,vec3.add([],mdUp,mrRight));constc=vec3.add([],Cp,vec3.add([],muUp,mrRight));constd=vec3.add([],Cp,vec3.add([],muUp,mlRight));// Construct an unindexed vertex attribute array from a', b', c', and d'.constuv=[];uv.push(...a);uv.push(...b);uv.push(...c);uv.push(...a);uv.push(...c);uv.push(...d);// Return the array.returnuv;}