Hello! Welcome to the next lesson in our journey to build expertise in 3D generative models.
In the last few lessons, we've built a strong conceptual foundation, comparing different 3D representations and diving into the theory behind both Neural Radiance Fields (NeRFs) and Signed Distance Functions (SDFs). As we concluded last time, it's now time to pivot back to practical implementation and start building the components of a NeRF-style renderer from the ground up.
Today's learning outcome is to implement ray generation for a pinhole camera model. This is the first and most fundamental step in any ray-based rendering technique. We will explore the geometry of a virtual camera and implement the logic that translates a 2D pixel coordinate into a 3D ray, defined by an origin and a direction, in world space. Mastering this step is essential, as the correctness of every subsequent stage—from sampling points to querying the neural network—depends on generating these rays accurately.
1. The Geometry of Ray Casting
At its heart, rendering an image of a 3D scene is about figuring out what color each pixel of the image should be. With techniques like NeRF, we do this by casting rays. Imagine your screen is a window looking into a 3D world. For each pixel on that window, we trace a straight line from your eye (the camera), through that pixel, and out into the world. This line is a ray.

Mathematically, a 3D ray is defined by its origin and a direction vector. We can parameterize any point along the ray using a distance :
Where:
- is the ray origin: the 3D position of the camera in world coordinates.
- is the ray direction: a unit vector indicating the direction the ray travels.
- is the distance from the origin along the ray.
Our goal for this lesson is to compute the specific origin and direction for the ray corresponding to every single pixel in our desired output image.
To do this, we need two key pieces of information about our virtual camera:
- Extrinsics: The camera's pose (position and orientation) in the 3D world. This is typically represented by a camera-to-world (c2w) transformation matrix. This matrix directly gives us the ray origin and tells us how to orient the ray directions .
- Intrinsics: The camera's internal properties, such as focal length and sensor size. These are captured in an intrinsic matrix (K). This matrix allows us to determine the initial direction of the ray in the camera's own local coordinate system before we transform it into the world.
2. The Inverse Transformation: From Pixels to World Space
In a typical graphics pipeline, you start with a 3D point in world space and project it onto a 2D screen. For our task, we need to do the exact opposite: we start with a 2D pixel coordinate and un-project it to find the corresponding 3D ray in world space.
This "un-projection" involves a two-step transformation:
- Pixel to Camera Space: We use the camera's intrinsic properties to convert the 2D pixel coordinates
(i, j)into a 3D direction vector in the camera's local coordinate system. - Camera to World Space: We use the camera's extrinsic properties (the
c2wmatrix) to transform this local direction vector and the camera's origin into the global world coordinate system.
The following video provides an excellent, albeit C++ focused, explanation of this inverse transformation. The underlying mathematics are universal and directly applicable to our goal.
Adding an Interactive 3D Camera System // Ray Tracing series
This video from 'Adding an Interactive 3D Camera System' by The Cherno explains the core logic of calculating ray directions from camera matrices. It clearly explains the concept of going from normalized device coordinates (which relate to our pixel coordinates) back to world space.
Watch the segment from 16:34 to 19:31. The key insight is how inverse projection and inverse view matrices are used to transform a 2D coordinate back into a 3D world-space ray. Don't worry about the C++ specifics; focus on the description of the transformation pipeline.
As the video explains, the core idea is to reverse the standard rendering projection. We use the camera's intrinsic parameters to determine where a pixel (i, j) lies on the camera's imaginary image plane, and then use the camera's extrinsic pose to place that ray correctly in the 3D world.
3. A Practical Implementation with NumPy
Now let's translate this theory into a practical Python implementation. The NeRF ecosystem has largely standardized this process, and a clear, concise function is all we need.
The following article provides a well-annotated NumPy implementation named get_rays_np that perfectly captures this logic.
The Annotated NeRF – Training on Custom Dataset ...
To see how this all comes together in code, let's study the 'Ray Batching' section from 'The Annotated NeRF – Training on Custom Dataset ...' by Soumya Ranjan Mohanty. This section contains the get_rays_np function, a canonical implementation for ray generation in NeRFs.
Read the 'Ray Batching' section, focusing on the get_rays_np function and its line-by-line explanation. Pay close attention to how it uses the intrinsic matrix K and the camera-to-world matrix c2w to generate rays_o and rays_d.
Let's break down the get_rays_np function from the article. It's the embodiment of the two-step process we discussed.
import numpy as np
def get_rays_np(H, W, K, c2w):
# 1. Create a grid of pixel coordinates
i, j = np.meshgrid(np.arange(W, dtype=np.float32),
np.arange(H, dtype=np.float32),
indexing='xy')
# 2. Convert pixel coordinates to camera space directions
# This uses the intrinsic matrix K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
fx, fy = K[0][0], K[1][1]
cx, cy = K[0][2], K[1][2]
dirs = np.stack([(i - cx) / fx,
-(j - cy) / fy,
-np.ones_like(i)], -1)
# 3. Transform camera space directions to world space
# This uses the rotation part of the c2w matrix
# c2w is a 3x4 matrix: [R | t] where R is 3x3 rotation, t is 3x1 translation
rotation_matrix = c2w[:3, :3]
rays_d = np.sum(dirs[..., np.newaxis, :] * rotation_matrix, -1)
# 4. The ray origin is the camera's position in world space
# This is the translation part of the c2w matrix
translation_vector = c2w[:3, -1]
rays_o = np.broadcast_to(translation_vector, np.shape(rays_d))
return rays_o, rays_d
Dissecting the Code:
- Step 1 (
np.meshgrid): This simply creates two 2D arrays,iandj, that hold the x and y coordinates of every pixel in an image of sizeW x H. - Step 2 (
dirs = ...): This is the Pixel to Camera Space transformation.(i - cx) / fxand(j - cy) / fyconvert pixel locations into coordinates on the image plane, which is positioned at a distancez=-1from the camera center.- The
-(j - cy) / fyis a common convention because pixel coordinates usually start from the top-left (y increases downwards), whereas in a typical 3D camera coordinate system, the y-axis points upwards. - The z-component is set to
-1, establishing that we are looking down the negative z-axis in the camera's local frame.
- Step 3 (
rays_d = ...): This is the Camera to World Space transformation for the directions. Thenp.sum(...)line is a vectorized way to multiply every direction vector indirsby the camera's rotation matrix. This rotates the rays to point in the correct direction in the global 3D world. - Step 4 (
rays_o = ...): This sets the origin for all rays. Since all rays for a single image originate from the same point (the camera's center), we simply take the translation vector from thec2wmatrix and broadcast it to match the shape of ourrays_darray.
Test your understanding!
Suppose you have two c2w matrices, c2w_1 and c2w_2. c2w_1 and c2w_2 have the same rotation part but different translation parts. If you generate rays for both, how will the resulting rays_d and rays_o differ between the two calls?
Show answer
- The
rays_d(ray directions) will be identical for both. The direction of each ray relative to the camera is determined by the camera's rotation and the pixel coordinates, which are the same in both cases. - The
rays_o(ray origins) will be different. They will correspond to the different translation parts ofc2w_1andc2w_2. This represents taking two pictures of the same scene from two different locations but with the camera pointed in the same direction.
You can test this implementation with a sample set of parameters:
# Example Usage
H, W = 400, 400
focal_length = 555.55
# Camera Intrinsic Matrix K
K = np.array([
[focal_length, 0, W/2],
[0, focal_length, H/2],
[0, 0, 1]
])
# Camera-to-World Matrix (c2w) - Identity, camera at origin looking down Z
c2w = np.array([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0]
])
# Note: NeRF datasets often use a different convention where Z is up and camera looks forward.
# For example, [0, -1, 0] for Y, [1, 0, 0] for X would make the camera look down +Y.
# We stick to the simpler case here.
rays_o, rays_d = get_rays_np(H, W, K, c2w)
print(f"rays_o shape: {rays_o.shape}")
print(f"rays_d shape: {rays_d.shape}")
# Direction of the ray for the center pixel (200, 200)
# Should be close to [0, 0, -1] since cx,cy = W/2,H/2
center_pixel_direction = rays_d[H//2, W//2]
print(f"Center pixel ray direction: {center_pixel_direction}")
# Direction of the ray for the top-left pixel (0, 0)
top_left_pixel_direction = rays_d[0, 0]
print(f"Top-left pixel ray direction (un-normalized): {top_left_pixel_direction}")
Conclusion
In this lesson, we transitioned from theory to practice, establishing the critical first step of our renderer. We have defined the pinhole camera model, understood the process of un-projecting 2D pixels into 3D space, and analyzed a concrete implementation for generating rays.
Key Takeaways:
- Ray generation is the process of computing a 3D ray, defined by an origin and direction , for each pixel of a target image.
- This process requires two matrices: the intrinsic matrix
K(for pixel-to-camera-space mapping) and the extrinsicc2wmatrix (for camera-to-world-space transformation). - The ray origin is the camera's position in the world, derived from the translation part of the
c2wmatrix. - The ray direction is calculated by first finding the direction in the camera's local coordinate system (using
K) and then rotating it into the world coordinate system (using the rotation part ofc2w).
Next Lesson Preview:
We now know how to generate a grid of rays pointing out into our 3D scene. The next logical step in the volumetric rendering pipeline is to determine the 3D points along these rays that we will evaluate with our neural network. In the next lesson, we will implement stratified sampling of points along a camera ray, taking the rays_o and rays_d we've just created as our starting point.