From 3f81104c99295563492559a55b23549febe09ff8 Mon Sep 17 00:00:00 2001 From: Jens Pitkanen Date: Mon, 28 Jul 2025 18:41:14 +0300 Subject: [PATCH] Add part of Ray Tracing in One Weekend as an example --- examples/cpu_raytracer.reid | 466 ++++++++++++++++++++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 examples/cpu_raytracer.reid diff --git a/examples/cpu_raytracer.reid b/examples/cpu_raytracer.reid new file mode 100644 index 0000000..a6f97a8 --- /dev/null +++ b/examples/cpu_raytracer.reid @@ -0,0 +1,466 @@ +// First half of Ray Tracing in One Weekend, rendered to a SDL3 window rather +// than an image file. Needs to be linked against SDL3, i.e. +// `./cli cpu_raytracer.reid SDL3` + +import std::print; +import std::String; + +/////////////////// +/// SDL externs /// +/////////////////// + +// Helper struct for stack allocated const sized strings, because structs are +// easier to create uninit than arrays. +struct SDL_Window {} +struct SDL_Renderer {} +struct SDL_Texture {} +struct SDL_Event { type: u32, reserved: [u8; 124] } +struct SDL_FRect { x: f32, y: f32, w: f32, h: f32 } +struct SDL_Rect { x: i32, y: i32, w: i32, h: i32 } +extern fn SDL_malloc(size: u64) -> *u8; +extern fn SDL_Init(flags: u32) -> bool; +extern fn SDL_Quit(); +extern fn SDL_CreateWindowAndRenderer(title: *char, width: i32, height: i32, flags: i32, + window_out: &mut *SDL_Window, renderer_out: &mut *SDL_Renderer) -> bool; +extern fn SDL_Delay(ms: u32); +extern fn SDL_SetRenderDrawColor(renderer: *SDL_Renderer, r: u8, g: u8, b: u8, a: u8); +extern fn SDL_RenderClear(renderer: *SDL_Renderer); +extern fn SDL_RenderPresent(renderer: *SDL_Renderer); +extern fn SDL_HasEvent(event_type: u32) -> bool; +extern fn SDL_PollEvent(event: &mut SDL_Event) -> bool; +extern fn SDL_PumpEvents(); +extern fn SDL_FlushEvents(min_type: u32, max_type: u32); +extern fn SDL_GetTicks() -> u64; +extern fn SDL_SetWindowTitle(window: *SDL_Window, title: *char) -> bool; +extern fn SDL_CreateTexture(renderer: *SDL_Renderer, + pixel_format: u32, texture_access: u32, width: u32, height: u32) -> *SDL_Texture; +extern fn SDL_RenderTexture(renderer: *SDL_Renderer, + texture: *SDL_Texture, srcfrect: &SDL_FRect, dstfrect: &SDL_FRect) -> bool; +extern fn SDL_UpdateTexture(texture: *SDL_Texture, rect: &SDL_Rect, pixels: *u8, pitch: u32) -> bool; +extern fn SDL_GetError() -> *char; +extern fn SDL_GetWindowSize(window: *SDL_Window, w: &mut i32, h: &mut i32) -> bool; +extern fn SDL_rand(max_exclusive: u32) -> u32; +extern fn SDL_SetTextureScaleMode(texture: *SDL_Texture, scale_mode: i32) -> bool; +extern fn SDL_sqrtf(value: f32) -> f32; +extern fn SDL_randf() -> f32; +extern fn SDL_powf(value: f32, power: f32) -> f32; + +// SDL error reporting helper +fn print_sdl_error(context: *char) { + let mut message = String::new(); + message = message + context + ": " + SDL_GetError(); + print(message); + message.free(); +} + +///////////////////////////////// +/// Main setup and frame loop /// +///////////////////////////////// + +struct GameState { + renderer: *SDL_Renderer, + window: *SDL_Window, + render_texture: *SDL_Texture, + frame_counter: u32, + last_fps_reset: u64, + pixels: *u8, + pixels_w: u32, + pixels_h: u32, + pixels_bpp: u32, +} + +fn main() -> i32 { + let SDL_INIT_VIDEO = 32; + let SDL_WINDOW_RESIZABLE = 32; + let SDL_PIXELFORMAT_RGBA8888 = 373694468; + let SDL_PIXELFORMAT_ABGR8888 = 376840196; + let SDL_PIXELFORMAT_RGB24 = 386930691; + let SDL_PIXELFORMAT_BGR24 = 390076419; + let SDL_PIXELFORMAT_RGB96_FLOAT = 454057996; + let SDL_PIXELFORMAT_BGR96_FLOAT = 457203724; + let SDL_TEXTUREACCESS_STREAMING = 1; + let SDL_SCALEMODE_NEAREST = 0; + let SDL_SCALEMODE_LINEAR = 1; + let SDL_SCALEMODE_PIXELART = 2; + + let init_success = SDL_Init(SDL_INIT_VIDEO); + if init_success == false { + print_sdl_error("SDL init failed"); + return 1; + } + + let mut window = SDL_Window::null(); + let mut renderer = SDL_Renderer::null(); + let gfx_init_success = SDL_CreateWindowAndRenderer( + "cpu raytracer", 640, 480, SDL_WINDOW_RESIZABLE, + &mut window, &mut renderer + ); + if gfx_init_success == false { + print_sdl_error("SDL renderer and window creation failed"); + return 1; + } + + let width = 128; + let height = 64; + let bpp = 4; + let render_texture = SDL_CreateTexture(renderer, + SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, width, height); + SDL_SetTextureScaleMode(render_texture, SDL_SCALEMODE_NEAREST); + + let pixels_len = (width * height * bpp) as u64; + let pixels = SDL_malloc(pixels_len); + let mut game_state = GameState { + renderer: renderer, + window: window, + render_texture: render_texture, + frame_counter: 0, + last_fps_reset: 0, + pixels: pixels, + pixels_w: width, + pixels_h: height, + pixels_bpp: bpp, + }; + + while frame_loop(&mut game_state) {} + + SDL_Quit(); + return 0; +} + +fn frame_loop(game_state: &mut GameState) -> bool { + let mut event = SDL_Event { type: 0, reserved: [0; 124] }; + while (SDL_PollEvent(&mut event)) { + if event.type == 256 { // SDL_EVENT_QUIT + return false; + } + } + + let mut screen_width = 0; + let mut screen_height = 0; + SDL_GetWindowSize(*game_state.window, &mut screen_width, &mut screen_height); + + let renderer = *game_state.renderer; + SDL_SetRenderDrawColor(renderer, 0, 50, 90, 255); + SDL_RenderClear(renderer); + + let w = *game_state.pixels_w; + let h = *game_state.pixels_h; + let bpp = *game_state.pixels_bpp; + for y in 0..h { + for x in 0..w { + render_pixel(x, y, game_state); + } + } + + let texture_area = SDL_Rect { x: 0, y: 0, w: w as i32, h: h as i32 }; + if SDL_UpdateTexture(*game_state.render_texture, &texture_area, *game_state.pixels as *u8, bpp * w) == false { + print_sdl_error("UpdateTexture error"); + } + let src = SDL_FRect { x: 0.0, y: 0.0, w: w as f32, h: h as f32 }; + let aspect_ratio = src.w / src.h; + let scaled_width = screen_height as f32 * aspect_ratio; + let dst = SDL_FRect { x: (screen_width as f32 - scaled_width) / 2.0, y: 0.0, w: scaled_width, h: screen_height as f32 }; + if SDL_RenderTexture(renderer, *game_state.render_texture, &src, &dst) == false { + print_sdl_error("RenderTexture error"); + } + + SDL_RenderPresent(renderer); + SDL_Delay(1); + + *game_state.frame_counter = *game_state.frame_counter + 1; + let t = SDL_GetTicks(); + if (t - *game_state.last_fps_reset) >= 1000 { + let mut title = String::new(); + title = title + "cpu raytracer (" + *game_state.frame_counter as u64 + " fps)"; + SDL_SetWindowTitle(*game_state.window, title.inner); + title.free(); + *game_state.frame_counter = 0; + *game_state.last_fps_reset = t; + } + + return true; +} + +fn render_pixel(x: u32, y: u32, game_state: &mut GameState) { + let w = *game_state.pixels_w; + let h = *game_state.pixels_h; + let bpp = *game_state.pixels_bpp; + + let samples = 8; + let old_sample_weight = 0.9; + let new_sample_weight = 0.1 / samples as f32; + let mut rgb = vec_mul_scalar(old_sample_weight, [ + srgb_to_linear(*game_state.pixels[(x + y * w) * bpp + 0]), + srgb_to_linear(*game_state.pixels[(x + y * w) * bpp + 1]), + srgb_to_linear(*game_state.pixels[(x + y * w) * bpp + 2]) + ]); + for sample in 0..samples { + rgb = vec_add(rgb, vec_mul_scalar(new_sample_weight, shade(x, y, *game_state.frame_counter, w, h))); + } + *game_state.pixels[(x + y * w) * bpp + 0] = linear_to_srgb(rgb[0]); + *game_state.pixels[(x + y * w) * bpp + 1] = linear_to_srgb(rgb[1]); + *game_state.pixels[(x + y * w) * bpp + 2] = linear_to_srgb(rgb[2]); + *game_state.pixels[(x + y * w) * bpp + 3] = 255; +} + + +///////////////// +/// Rendering /// +///////////////// + +struct Ray { + origin: [f32; 3], + direction: [f32; 3], +} + +struct Material { + // 0 = lambertian diffuse + // 1 = mirror + type: u32, + // Generally the "color" of the surface (linear factors of how much of each + // color channel this surface does not absorb), but the idea is that the + // type governs what this means. + linear_color: [f32; 3], +} + +struct Hit { + hit: bool, + front_face: bool, + distance: f32, + normal: [f32; 3], + position: [f32; 3], + material: Material, +} +impl Hit { + fn none() -> Hit { + Hit { + hit: false, front_face: true, distance: 0.0, normal: [0.0; 3], position: [0.0; 3], + material: Material { type: 0, linear_color: [0.0; 3] }, + } + } +} + +struct Sphere { + center: [f32; 3], + radius: f32, + material: Material, +} + +fn shade(x: u32, y: u32, t: u32, w: u32, h: u32) -> [f32; 3] { + let jitter_x = SDL_randf() - 0.5; + let jitter_y = SDL_randf() - 0.5; + + let pixel_scale = 1.0 / h as f32; + let pixel_pos = [ + (x as f32 + jitter_x) * pixel_scale, + 1.0 - (y as f32 + jitter_y) * pixel_scale, + -1.0 + ]; + let camera_pos = [w as f32 * 0.5f32 * pixel_scale, h as f32 * 0.5f32 * pixel_scale, 0.0f32]; + let dir = vec_normalize(vec_sub(pixel_pos, camera_pos)); + let ray = Ray { origin: camera_pos, direction: dir }; + let beige_lambertian = Material { type: 0, linear_color: [0.3, 0.2, 0.1] }; + let green_lambertian = Material { type: 0, linear_color: [0.1, 0.5, 0.06] }; + let greenish_mirror = Material { type: 1, linear_color: [0.9, 1.0, 0.95] }; + let spheres = [ + // Ground + Sphere { center: vec_sub(camera_pos, [0.0, 100001.0, 0.0]), radius: 100000.0, material: beige_lambertian }, + // Centered unit sphere + Sphere { center: vec_add(camera_pos, [0.0, 0.0, 0.0 - 5.0]), radius: 1.0, material: green_lambertian }, + // The unit sphere on the right + Sphere { center: vec_add(camera_pos, [2.0, 0.0, 0.0 - 6.0]), radius: 1.0, material: greenish_mirror } + ]; + return shade_world(ray, &spheres, 3); +} + +fn shade_world(ray: Ray, spheres: &[Sphere; 3], bounces_left: u8) -> [f32; 3] { + if bounces_left == 0 { + return [0.0, 0.0, 0.0]; + } + + let mut closest_hit = Hit::none(); + closest_hit.distance = 100.0; + for i in 0..3 { + let sphere_hit = ray_sphere_closest_hit(ray, *spheres[i], [0.001, closest_hit.distance]); + if sphere_hit.hit { + closest_hit = sphere_hit; + } + } + + if closest_hit.hit { + //return vec_mul_scalar(0.5, vec_add(closest_hit.normal, [1.0, 1.0, 1.0])); // normal + //return vec_mul_scalar(closest_hit.distance / 10.0, [1.0, 1.0, 1.0]); // depth + if closest_hit.material.type == 0 { + let bounce_dir = vec_normalize(vec_add(closest_hit.normal, random_unit_vec())); + let bounce_ray = Ray { origin: closest_hit.position, direction: bounce_dir }; + return vec_mul_componentwise( + closest_hit.material.linear_color, + shade_world(bounce_ray, spheres, bounces_left - 1) + ); + } else if closest_hit.material.type == 1 { + let bounce_dir = vec_reflect(ray.direction, closest_hit.normal); + let bounce_ray = Ray { origin: closest_hit.position, direction: bounce_dir }; + return vec_mul_componentwise( + closest_hit.material.linear_color, + shade_world(bounce_ray, spheres, bounces_left - 1) + ); + } else { + return [1.0, 0.0, 1.0]; + } + } + + return shade_sky(ray); +} + +fn shade_sky(ray: Ray) -> [f32; 3] { + let a = 0.5 * (ray.direction[1] + 1.0); + return vec_add( + vec_mul_scalar(1.0 - a, [1.0, 1.0, 1.0]), + vec_mul_scalar(a, [0.5, 0.7, 1.0]) + ); +} + +// Returns the distance from the ray origin to the sphere, or -1.0 if the ray doesn't hit. +fn ray_sphere_closest_hit(ray: Ray, sphere: Sphere, interval: [f32; 2]) -> Hit { + let to_sphere = vec_sub(sphere.center, ray.origin); + let h = vec_dot(ray.direction, to_sphere); + let c = vec_length_squared(to_sphere) - sphere.radius * sphere.radius; + let discriminant = h * h - c; + if discriminant < 0.0 { + return Hit::none(); + } + + let discriminant_sqrt = SDL_sqrtf(discriminant); + let mut distance = h - discriminant_sqrt; + if interval_surrounds(interval, distance) == false { + distance = h + discriminant_sqrt; + if interval_surrounds(interval, distance) == false { + return Hit::none(); + } + } + let hit_position = vec_add(ray.origin, vec_mul_scalar(distance, ray.direction)); + let mut front_face = true; + let mut normal = vec_normalize(vec_sub(hit_position, sphere.center)); + if vec_dot(normal, ray.direction) > 0.0 { + normal = vec_mul_scalar(-1.0, normal); + front_face = false; + } + + return Hit { + hit: true, + front_face: front_face, + distance: distance, + normal: normal, + position: hit_position, + material: sphere.material, + }; +} + + +////////////////// +/// Other math /// +////////////////// + +fn clamp(min: f32, max: f32, value: f32) -> f32 { + if value > max { + return max; + } + if value < min { + return min; + } + return value; +} + +fn abs(f: f32) -> f32 { + if f < 0.0 { + return f * -1.0; + } + return f; +} + +fn vec_add(lhs: [f32; 3], rhs: [f32; 3]) -> [f32; 3] { + return [lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2]]; +} + +fn vec_sub(lhs: [f32; 3], rhs: [f32; 3]) -> [f32; 3] { + return [lhs[0] - rhs[0], lhs[1] - rhs[1], lhs[2] - rhs[2]]; +} + +fn vec_dot(lhs: [f32; 3], rhs: [f32; 3]) -> f32 { + return lhs[0] * rhs[0] + lhs[1] * rhs[1] + lhs[2] * rhs[2]; +} + +fn vec_mul_componentwise(lhs: [f32; 3], rhs: [f32; 3]) -> [f32; 3] { + return [lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2]]; +} + +fn vec_mul_scalar(lhs: f32, rhs: [f32; 3]) -> [f32; 3] { + return [lhs * rhs[0], lhs * rhs[1], lhs * rhs[2]]; +} + +fn vec_normalize(v: [f32; 3]) -> [f32; 3] { + let len_reciprocal = 1.0f32 / SDL_sqrtf(vec_length_squared(v)); + return vec_mul_scalar(len_reciprocal, v); +} + +fn vec_length_squared(v: [f32; 3]) -> f32 { + return v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; +} + +fn vec_abs(v: [f32; 3]) -> [f32; 3] { + return [abs(v[0]), abs(v[1]), abs(v[2])]; +} + +fn vec_reflect(direction: [f32; 3], normal: [f32; 3]) -> [f32; 3] { + return vec_sub(direction, vec_mul_scalar(2.0f32 * vec_dot(direction, normal), normal)); +} + +fn interval_surrounds(interval: [f32; 2], value: f32) -> bool { + return (interval[0] < value) && (value < interval[1]); +} + +fn random_unit_vec() -> [f32; 3] { + let mut point = [ + SDL_randf() * 2.0f32 - 1.0f32, + SDL_randf() * 2.0f32 - 1.0f32, + SDL_randf() * 2.0f32 - 1.0f32 + ]; + let mut lensq = vec_length_squared(point); + while lensq > 1.0 { + point = [ + SDL_randf() * 2.0f32 - 1.0f32, + SDL_randf() * 2.0f32 - 1.0f32, + SDL_randf() * 2.0f32 - 1.0f32 + ]; + lensq = vec_length_squared(point); + } + let len_reciprocal = 1.0f32 / SDL_sqrtf(lensq); + return vec_mul_scalar(len_reciprocal, point); +} + +fn random_unit_vec_on_hemi(normal: [f32; 3]) -> [f32; 3] { + let rand_vec = random_unit_vec(); + if vec_dot(rand_vec, normal) < 0.0f32 { + return vec_mul_scalar(0.0f32 - 1.0f32, rand_vec); + } + return rand_vec; +} + +fn linear_to_srgb(linear: f32) -> u8 { + let mut floating_srgb = 0.0; + if linear <= 0.0031308f32 { + floating_srgb = 12.92f32 * linear; + } else { + floating_srgb = SDL_powf(linear as f32, 1.0 / 2.4) * 1.055f32 - 0.055f32; + } + let clamped = clamp(0.0, 1.0, floating_srgb); + return (clamped * 255.999) as u8; +} + +fn srgb_to_linear(srgb: u8) -> f32 { + let floating_srgb = srgb as f32 / 255.0; + if floating_srgb <= 0.04045f32 { + return floating_srgb / 12.92f32; + } + return SDL_powf((floating_srgb as f32 + 0.055) / 1.055, 2.4); +}