Skip to content
AILinkDeepTech
Go back
Deep Learning Medium

Neural Radiance Fields (NeRF) Implementation in PyTorch

Abstract

A PyTorch implementation of Neural Radiance Fields (NeRF): positional encoding for positions and view directions, MLP with skip connections, density/color prediction, stratified sampling, and volumetric rendering.

Neural Radiance Fields (NeRF) Implementation in PyTorch

This implementation builds Neural Radiance Fields (NeRF) from scratch. It includes positional encoding for 3D positions and view directions, an MLP with skip connections that outputs density and color, stratified sampling with random perturbation, and volumetric rendering along rays.

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

class PositionalEncoding(nn.Module):
    def __init__(self, num_frequencies=10, include_input=True):
        super().__init__()
        self.num_frequencies = num_frequencies
        self.include_input = include_input

    def forward(self, x):
        """
        Args:
            x: Input coordinates (batch_size, ..., channel)
        """
        outputs = []
        if self.include_input:
            outputs.append(x)
            
        for i in range(self.num_frequencies):
            freq = 2.0 ** i
            for func in [torch.sin, torch.cos]:
                outputs.append(func(x * freq * np.pi))
                
        return torch.cat(outputs, dim=-1)
    
class NeRF(nn.Module):
    """
    Predicts RGB color and density 
    """
    def __init__(self, 
                 pos_dim=3,
                 dir_dim=3,
                 hidden_dim=256,
                 num_layers=8,
                 skip_layers=[4],
                 pos_frequencies=10,
                 dir_frequencies=4):
        super().__init__()

        self.skip_layers = skip_layers

        # Position and direction encoders
        self.pos_encoder = PositionalEncoding(num_frequencies=pos_frequencies)
        self.dir_encoder = PositionalEncoding(num_frequencies=dir_frequencies)
        
        pos_encoded_dim = pos_dim * (2 * pos_frequencies + 1)
        dir_encoded_dim = dir_dim * (2 * dir_frequencies + 1)

        self.pos_layers = nn.ModuleList()
        input_dim = pos_encoded_dim
        for i in range(num_layers):
            if i in skip_layers:
                input_dim += pos_encoded_dim
            self.pos_layers.append(nn.Linear(input_dim, hidden_dim))
            input_dim = hidden_dim

        # Output layers for density and feature vector
        self.density_layer = nn.Linear(hidden_dim, 1)
        self.feature_layer = nn.Linear(hidden_dim, hidden_dim)
        
        self.dir_layer = nn.Linear(hidden_dim + dir_encoded_dim, hidden_dim // 2)
        self.color_layer = nn.Linear(hidden_dim // 2, 3)

        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()

    def forward(self, pos, dir):
        """    
        Args:
            pos: Position coordinates 
            dir: View direction coordinates 
            
        Returns:
            rgb: Predicted colors 
            sigma: Predicted densities 
        """
        pos_encoded = self.pos_encoder(pos)
        dir_encoded = self.dir_encoder(dir)

        # Process position features with skip connections
        x = pos_encoded
        for i, layer in enumerate(self.pos_layers):
            if i in self.skip_layers:
                x = torch.cat([x, pos_encoded], dim=-1)
            x = self.relu(layer(x))

        # Compute density and features
        sigma = self.relu(self.density_layer(x))
        features = self.feature_layer(x)

        dir_features = torch.cat([features, dir_encoded], dim=-1)
        x = self.relu(self.dir_layer(dir_features))
        rgb = self.sigmoid(self.color_layer(x))

        return rgb, sigma
    
def render_rays(nerf_model, rays_o, rays_d, near, far, n_samples, rand=True):
    """  
    Args:
        nerf_model: NeRF model instance
        rays_o: Ray origins 
        rays_d: Ray directions 
        near: Near plane distance
        far: Far plane distance
        n_samples: Number of samples per ray
        rand: Whether to add random noise to sample positions
        
    Returns:
        rgb: Rendered colors 
        depth: Rendered depths
        weights: Sample weights 
    """
    batch_size = rays_o.shape[0]

    # Sample points along each ray
    t_vals = torch.linspace(0., 1., n_samples, device=rays_o.device)
    z_vals = near * (1. - t_vals) + far * t_vals
    z_vals = z_vals.expand(batch_size, n_samples)

    if rand:
        # Add random noise to sample positions
        mids = .5 * (z_vals[..., 1:] + z_vals[..., :-1])
        upper = torch.cat([mids, z_vals[..., -1:]], dim=-1)
        lower = torch.cat([z_vals[..., :1], mids], dim=-1)
        t_rand = torch.rand_like(z_vals)
        z_vals = lower + (upper - lower) * t_rand

    # Get sample positions along rays
    pts = rays_o[..., None, :] + rays_d[..., None, :] * z_vals[..., :, None]

    pts_flat = pts.reshape(-1, 3)
    dirs_flat = rays_d[:, None].expand(-1, n_samples, -1).reshape(-1, 3)
    rgb, sigma = nerf_model(pts_flat, dirs_flat)
    
    rgb = rgb.reshape(batch_size, n_samples, 3)
    sigma = sigma.reshape(batch_size, n_samples)

    # Compute weights for volume rendering
    dists = z_vals[..., 1:] - z_vals[..., :-1]
    dists = torch.cat([dists, torch.full_like(dists[..., :1], 1e10)], dim=-1)
    alpha = 1. - torch.exp(-sigma * dists)
    weights = alpha * torch.cumprod(
        torch.cat([torch.ones_like(alpha[..., :1]), 1. - alpha + 1e-10], dim=-1),
        dim=-1
    )[..., :-1]

    rgb_final = (weights[..., None] * rgb).sum(dim=1)
    depth = (weights * z_vals).sum(dim=1)

    return rgb_final, depth, weights

def test_nerf():
    model = NeRF()
    model.eval()

    print("forward pass")
    pos = torch.randn(4, 3)
    dir = torch.randn(4, 3)
    dir = dir / dir.norm(dim=-1, keepdim=True)
    rgb, sigma = model(pos, dir)
    print(f"RGB shape: {rgb.shape}, values range: [{rgb.min():.3f}, {rgb.max():.3f}]")
    print(f"Sigma shape: {sigma.shape}, values range: [{sigma.min():.3f}, {sigma.max():.3f}]")

    print("\nRay rendering")
    rays_o = torch.zeros(2, 3)  
    rays_d = torch.tensor([[0., 0., 1.], [0., 1., 0.]])  
    rays_d = rays_d / rays_d.norm(dim=-1, keepdim=True)
    
    rgb, depth, weights = render_rays(
        model, rays_o, rays_d,
        near=2., far=6.,
        n_samples=64, rand=True
    )
    
    print(f"Rendered RGB shape: {rgb.shape}")
    print(f"Rendered depth shape: {depth.shape}")
    print(f"Sample weights shape: {weights.shape}")

    # Multi-ray rendering
    print("\nMulti-ray rendering (simulated image)")
    n_rays = 32
    theta = torch.linspace(0, 2*np.pi, n_rays)
    rays_o = torch.zeros(n_rays, 3)
    rays_d = torch.stack([
        torch.cos(theta),
        torch.sin(theta),
        torch.ones_like(theta)
    ], dim=1)
    rays_d = rays_d / rays_d.norm(dim=-1, keepdim=True)
    
    rgb, depth, _ = render_rays(
        model, rays_o, rays_d,
        near=2., far=6.,
        n_samples=32, rand=True
    )

    # Visualize results
    plt.figure(figsize=(10, 4))
    
    plt.subplot(121)
    plt.scatter(theta.numpy(), depth.detach().numpy(), c=rgb.detach().numpy())
    plt.title('Depth vs Angle')
    plt.xlabel('Angle (radians)')
    plt.ylabel('Depth')
    
    plt.subplot(122)
    plt.scatter(rays_d[:, 0].numpy(), rays_d[:, 1].numpy(), c=rgb.detach().numpy())
    plt.title('Rendered Colors')
    plt.xlabel('Ray Direction X')
    plt.ylabel('Ray Direction Y')
    
    plt.tight_layout()
    # plt.show()

    return {
        'model': model,
        'test_rgb': rgb,
        'test_depth': depth,
        'test_rays_d': rays_d
    }

if __name__ == "__main__":
    test_results = test_nerf()


Cite this Explanation

@article{ailinkdeeptech2026nerfalgo,
  title={Neural Radiance Fields (NeRF) Implementation in PyTorch},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2026},
  url={https://ailinkdeeptech.com/research/nerf_algo}
}

Related Explanations