Ray Tracing with Raycore

Ray Tracing in one Hour

Analougus to the famous Ray Tracing in one Weekend, this tutorial uses Raycore to do the hard work of performant ray triangle intersection and therefore get a high performing ray tracer in a much shorter time. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows, materials, and reflections.

Setup

Ready to go! We have:

  • Raycore for fast ray-triangle intersections

  • GeometryBasics for geometry primitives

  • Colors and ImageShow for displaying rendered images

  • MeshIO for loading the cat data

Part 1: Our Scene, The Makie Cat

Let's create a fun scene that we'll use throughout this tutorial.

BVH built with 19866 triangles

Part 2: Helper Functions - Building Blocks

Let's define reusable helper functions we'll use throughout:

to_rgb (generic function with 1 method)

Part 3: The Simplest Ray Tracer - Depth Visualization

We're using one main function to shoot rays for each pixel. For simplicity, we already added multisampling and simple multi threading, to enjoy smoother images and faster rendering times throughout the tutorial. Read the GPU tutorial how to further improve the performance of this simple, not yet optimal kernel.

depth_kernel (generic function with 1 method)

First render! Depth visualization shows distance to surfaces. Much faster with threading and smoother with multi-sampling!

Part 5: Lighting with Hard Shadows

Let's add lighting and shadows using a reusable lighting function:

Hard shadows working! Scene has realistic lighting with sharp shadow edges.

Part 6: Soft Shadows

Now let's make shadows more realistic by sampling the light as an area light:

Soft shadows! Much more realistic with smooth penumbra edges.

Part 7: Materials and Multiple Lights

Time to add color and multiple lights! To associate materials with geometry, we need to rebuild the BVH with metadata that links each triangle to its material.

Triangle Metadata

When building a BVH, you can pass a metadata_fn(mesh_idx, tri_idx) that assigns custom data to each triangle. This metadata is stored in triangle.metadata and returned with every ray hit. For materials, we use the mesh index as metadata:

Now when a ray hits a triangle, we can look up its material using triangle.metadata:

Colorful scene with soft shadows from multiple lights! Each object has its own material.

Part 8: Reflections

Add simple reflections for metallic surfaces:

For performance type stability is a must! We can use JET to test if a function is completely type stable, which we also test in the Raycore tests for all functions.

Summary

We built a complete ray tracer with:

Core Features:

  • BVH acceleration for fast ray-scene intersections

  • Perspective camera with configurable FOV

  • Smooth shading from interpolated normals

  • Multi-light system with distance attenuation

  • Soft shadows using area light sampling (via compute_light with shadow_samples)

  • Material system (base color, metallic, roughness)

  • Reflections with optional roughness

  • ACES tone mapping for HDR

Performance:

  • Multi-threading for parallel rendering (introduced early!)

  • Multi-sampling for anti-aliasing (introduced early!)

  • Type-stable kernels for optimal performance

  • Modular, reusable compute_light function - works for both hard and soft shadows

Key Raycore Functions:

  • Raycore.BVH(meshes) - Build acceleration structure (default metadata = primitive index)

  • Raycore.BVH(meshes, metadata_fn) - Build with custom per-triangle metadata

  • Raycore.Ray(o=origin, d=direction) - Create ray

  • Raycore.closest_hit(bvh, ray) - Find nearest intersection, returns (hit, triangle, distance, bary_coords)

  • Raycore.any_hit(bvh, ray) - Test for any intersection (fast shadow test)

  • Raycore.reflect(wo, normal) - Compute reflection direction

  • triangle.metadata - Access custom data stored per-triangle

Key Patterns:

  1. Material Scene Pattern - Associate materials with geometry using metadata:

    :start => "2"
  1. Reusable Lighting - The compute_light function handles both hard and soft shadows:

  • shadow_samples=1 → hard shadows

  • shadow_samples>1 → soft shadows

This shows how well-designed functions can handle multiple use cases cleanly!