Ray Casting

This guide covers ray casting in Crater.rs, which allows you to test for intersections between rays and regions. Ray casting is essential for applications like rendering, collision detection, and path finding.

What is Ray Casting?

Ray casting is the process of shooting a ray (a line with an origin and direction) into a scene and finding where it intersects with geometry. In Crater.rs, rays are cast against regions to find surface intersections.

A ray is defined by:

  • Origin: Starting point of the ray
  • Direction: Normalized vector indicating the ray's direction
  • Extension: The point where the ray (may) intersect the surface

See the Ray Casting Gallery for examples of ray casting in action.

Creating Rays

The Rays struct represents a batch of rays that can be cast at once, leveraging the performance of Burn.

Ray creation from device memory

use crater::analysis::prelude::*;
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;

use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();

// Create ray origins and directions as tensors
let origins = Tensor::<Autodiff<Wgpu>, 2>::from_data(
    [
        [0.0, 0.0, 2.0],  // Ray 1: above origin
        [1.0, 0.0, 2.0],  // Ray 2: above (1,0,0)
        [0.0, 1.0, 2.0],  // Ray 3: above (0,1,0)
    ],
    &device
);

let directions = Tensor::<Autodiff<Wgpu>, 2>::from_data(
    [
        [0.0, 0.0, -1.0], // Ray 1: pointing down
        [0.0, 0.0, -1.0], // Ray 2: pointing down
        [0.0, 0.0, -1.0], // Ray 3: pointing down
    ],
    &device
);

let rays = Rays::<_, 3>::new(origins, directions);
}

Ray allocation from CPU memory

For testing and simple cases, you can use the new_from_slices method:

use crater::analysis::prelude::*;
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();

let rays = Rays::<Autodiff<Wgpu>, 3>::new_from_slices(
    &[
        [0.0, 0.0, 2.0],  // Origins
        [1.0, 0.0, 2.0],
        [0.0, 1.0, 2.0],
    ],
    &[
        [0.0, 0.0, -1.0], // Directions
        [0.0, 0.0, -1.0],
        [0.0, 0.0, -1.0],
    ],
    device
);
}

Ray Casting Algorithms

Crater.rs provides three ray casting algorithms, each with different trade-offs:

AlgorithmProsConsBest Used For
Analytical• Very fast and precise
• Direct mathematical solutions
• No iterative approximation
• Only works with analytically solvable shapesSimple primitive shapes (spheres, planes, etc.)
Bracket-and-Bisect• Works with any implicit surface
• Handles complex CSG operations
• Robust and predictable
• Slower than analytical methods
• Requires parameter tuning
Complex CSG regions, general-purpose ray casting
Newton's Method• Fast convergence for smooth surfaces
• Good for differentiable implicit functions
• Can fail on discontinuous surfaces
• Requires gradient information
• May not converge for all cases
Smooth, differentiable implicit surfaces

Usage

use crater::{analysis::prelude::*, csg::prelude::*, primitives::prelude::*};
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();
let rays = Rays::new_from_slices(&[[0.0, 0.0, 2.0]], &[[0.0, 0.0, -1.0]], device.clone());
let sphere = Field3D::<Autodiff<Wgpu>>::sphere(1.0, device.clone()).into_isosurface(0.0).region();
// Choose the appropriate algorithm for your use case
let results = sphere.ray_cast(
    rays,
    &Algebra::default(),
    RayCastAlgorithm::Analytical  // or BracketAndBisect or Newton
);
}

Interpreting Results

The RayCastResult struct contains comprehensive information about the ray casting operation:

use crater::{analysis::prelude::*, csg::prelude::*, primitives::prelude::*};
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();
let rays = Rays::new_from_slices(&[[0.0, 0.0, 2.0]], &[[0.0, 0.0, -1.0]], device.clone());
let sphere = Field3D::<Autodiff<Wgpu>>::sphere(1.0, device.clone()).into_isosurface(0.0).region();
let results = sphere.ray_cast(rays, &Algebra::default(), RayCastAlgorithm::Analytical);
// Access hit points (extensions)
let hit_points = results.extensions(); // [n_rays, 3], Float

// Access distances to hit points
let distances = results.distances(); // [n_rays], Float

// Access field values at hit points
let field_values = results.field_values(); // [n_rays], Float

// Check which rays hit the surface
let hit_mask = results.field_values().is_on_mask(); // [n_rays], Bool

// Get only the successful hits
let surface_hits = results.hits_on_surface(); // [n_rays, 3], Float

// Count successful hits
let hit_count = hit_mask.int().sum(); // [1], Int
}

Understanding Ray Failures

When a ray doesn't intersect with the surface, the result contains RAY_FAIL_VALUE = f32::MAX:

use crater::{analysis::prelude::*, csg::prelude::*, primitives::prelude::*};
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();
let sphere = Field3D::<Autodiff<Wgpu>>::sphere(1.0, device.clone())
    .into_isosurface(0.0)
    .region();

// Ray that misses the sphere
let rays = Rays::new_from_slices(
    &[[2.0, 2.0, 2.0]], // Origin outside sphere
    &[[1.0, 1.0, 1.0]], // Direction pointing away
    device.clone()
);

let results = sphere.ray_cast(
    rays,
    &Algebra::default(),
    RayCastAlgorithm::Analytical
);

// Check if ray hit
let hit_mask = results.field_values().is_on_mask();
let hit = hit_mask.clone().any().into_scalar();
if !hit {
    println!("Ray missed the sphere");
}
}

Ray Casting with Builtins

Field2D

use crater::{analysis::prelude::*, csg::prelude::*, primitives::prelude::*};
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();
// Circle
let circle = Field2D::<Autodiff<Wgpu>>::circle(1.0, device.clone())
    .into_isosurface(0.0)
    .region();

// Line (infinite line through origin)
let line = Field2D::<Autodiff<Wgpu>>::line([0.0, 1.0], device.clone())
    .into_isosurface(0.0)
    .region();

// Ray from below circle pointing up
let rays = Rays::new_from_slices(
    &[[0.0, -2.0]], // Origin below circle
    &[[0.0, 1.0]], // Direction upward
    device.clone()
);

// Basic halfspace regions
let circle_results = circle.ray_cast(
    rays.clone(),
    &Algebra::default(),
    RayCastAlgorithm::Analytical
);

// Composite CSG regions work just as well
// "Above the line and inside the circle"
let region = circle & -line;
let results = region.ray_cast(
    rays,
    &Algebra::default(),
    RayCastAlgorithm::Analytical
);
}

Field3D

use crater::{analysis::prelude::*, csg::prelude::*, primitives::prelude::*};
use burn::backend::wgpu::{Wgpu, WgpuDevice};
use burn::backend::Autodiff;
use burn::prelude::*;
fn main() {
let device = WgpuDevice::default();
// Sphere
let sphere = Field3D::<Autodiff<Wgpu>>::sphere(1.0, device.clone())
    .into_isosurface(0.0)
    .region();

// Plane
let plane = Field3D::<Autodiff<Wgpu>>::plane([0.0, 0.0, 1.0], device.clone())
    .into_isosurface(0.0)
    .region();

// Grid of rays from above
let rays = Rays::new_from_slices(
    &[
        [0.0, 0.0, 2.0],
        [0.5, 0.0, 2.0],
        [0.0, 0.5, 2.0],
    ],
    &[
        [0.0, 0.0, -1.0], // All pointing down
        [0.0, 0.0, -1.0],
        [0.0, 0.0, -1.0],
    ],
    device.clone()
);

// Basic halfspace regions
let sphere_results = sphere.ray_cast(
    rays.clone(),
    &Algebra::default(),
    RayCastAlgorithm::Analytical
);

// Composite CSG regions work just as well
// "Inside the sphere and above the plane"
let region = sphere & -plane;
let results = region.ray_cast(
    rays,
    &Algebra::default(),
    RayCastAlgorithm::Analytical
);
}

Next Steps

Now that you understand ray casting:

  1. Surface Analysis - Learn about surface normal calculation
  2. Volume Estimation - Understand volume computation methods
  3. Theory - Dive deeper into the mathematical foundations