2024-08-22 15:53:15 +02:00

339 lines
11 KiB
Zig

const std = @import("std");
const print = std.debug.print;
const math = std.math;
const vec3 = @Vector(3, f64);
const Ray = @import("hittable.zig").Ray;
const utils = @import("utils.zig");
const toVec3 = utils.toVec3;
const Interval = utils.Interval;
const hit = @import("hittable.zig");
const HittableList = hit.HittableList;
const HitRecord = hit.HitRecord;
const mat_math = @import("mat_math.zig");
const unit_vector = mat_math.unit_vector;
const length = mat_math.length;
const cross = mat_math.cross;
const AtomicOrder = @import("std").builtin.AtomicOrder;
const Thread = std.Thread;
const Semaphore = std.Thread.Semaphore;
const Pixel = struct {
w: usize,
h: usize,
color: vec3,
};
const ThreadInfo = struct {
thread_index: usize,
semaphore_ptr: *Semaphore,
};
// Resolution
const aspect_ratio: f64 = 16.0 / 9.0;
const image_width: usize = 2560; // Possible 128, 256, 512, 1024, 1280, 1920, 2560, 3840, 7680
const image_height: usize = image_width / aspect_ratio;
// Ray precision
const samples_per_pixel = 500;
const max_depth = 50;
// Camera lenses
const defocus_angle = 0.6;
const focus_dist = 10;
const vfov = 20;
// Camera position
const lookfrom = vec3{ 13, 2, 3 };
const lookat = vec3{ 0, 0, 0 };
const vup = vec3{ 0, 1, 0 };
// Number of thread to use
const n_threads_to_spawn = 100;
// Global var for multi-thread
var completion_count: usize = 0;
var next_entry_to_do: usize = 0;
var entry_count: usize = 0;
var entry_buffer: [image_width * image_height]Pixel = undefined;
var _camera: *const Camera = undefined;
fn addWork(semaphore_ptr: *Semaphore) void {
const entry_index = @atomicLoad(usize, &entry_count, AtomicOrder.seq_cst);
// SeqCst guarantees that the msg write above is visible before the entry_count write below can be seen.
@atomicStore(usize, &entry_count, entry_index + 1, AtomicOrder.seq_cst);
semaphore_ptr.post(); // Wake up all threads
}
fn doWork(info: *ThreadInfo) void {
while (true) {
if (@atomicLoad(usize, &next_entry_to_do, AtomicOrder.seq_cst) < @atomicLoad(usize, &entry_count, AtomicOrder.seq_cst)) {
const entry_index = @atomicRmw(usize, &next_entry_to_do, .Add, 1, AtomicOrder.seq_cst);
const pixel = entry_buffer[entry_index];
var pixel_color = vec3{ 0, 0, 0 };
for (0.._camera.samples_per_pixel) |_| {
const r = _camera.get_ray(pixel.h, pixel.w);
pixel_color += ray_color(r, _camera.max_depth, _camera.world);
}
entry_buffer[entry_index].color = pixel_color;
_ = @atomicRmw(usize, &completion_count, .Add, 1, AtomicOrder.seq_cst);
} else {
info.semaphore_ptr.wait(); // Put all threads to sleep
}
}
}
pub const Camera = struct {
aspect_ratio: f64,
image_width: usize,
samples_per_pixel: usize,
max_depth: usize,
vfov: f64,
defocus_angle: f64,
focus_dist: f64,
world: *const HittableList,
image_height: usize,
center: vec3,
pixel_samples_scale: f64,
pixel00_loc: vec3,
pixel_delta_u: vec3,
pixel_delta_v: vec3,
defocus_disk_u: vec3,
defocus_disk_v: vec3,
u: vec3,
v: vec3,
w: vec3,
pub fn render(self: Camera, writer: anytype) !void {
_camera = &self;
var semaphore = Semaphore{};
var infos: [n_threads_to_spawn]ThreadInfo = undefined;
for (&infos, 0..) |*info, thread_index| {
info.thread_index = thread_index;
info.semaphore_ptr = &semaphore;
const handle = try Thread.spawn(.{}, doWork, .{info});
handle.detach();
}
// Write the PPM header
try writer.print("P3\n{} {}\n255\n", .{ self.image_width, self.image_height });
for (0..self.image_width) |w| {
for (0..self.image_height) |h| {
entry_buffer[w * self.image_height + h] = Pixel{ .w = w, .h = h, .color = vec3{ -1, -1, -1 } };
addWork(&semaphore);
}
}
print("\nStarting rendering using {d} threads\n", .{n_threads_to_spawn});
while (entry_count != @atomicLoad(usize, &completion_count, AtomicOrder.seq_cst)) {
pbar(@atomicLoad(usize, &completion_count, AtomicOrder.seq_cst), self.image_height * self.image_width);
}
pbar(100, 100);
for (0..self.image_height) |h| {
for (0..self.image_width) |w| {
try writeColor(self.get_pixel_color(w, h) * toVec3(self.pixel_samples_scale), writer);
}
}
}
fn get_pixel_color(self: Camera, w: usize, h: usize) vec3 {
const pixel = entry_buffer[w * self.image_height + h];
if ((pixel.w == w) and (pixel.h == h)) {
return pixel.color;
}
print("Pixel with different coordinates w: {}, h: {}, pixel.w: {}, pixel.h: {}", .{ w, h, pixel.w, pixel.h });
return vec3{ 0, 0, 0 };
}
fn get_ray(self: Camera, h: usize, w: usize) Ray {
// Construct a camera ray originating from the origin and directed at randomly sampled
// point around the pixel location i, j.
const offset = sample_square();
const pixel_sample = self.pixel00_loc +
(toVec3(h) + toVec3(offset[0])) * self.pixel_delta_v +
(toVec3(w) + toVec3(offset[1])) * self.pixel_delta_u;
const ray_origin = if (self.defocus_angle <= 0) self.center else self.defocus_disk_sample();
const ray_direction = pixel_sample - ray_origin;
return Ray{ .orig = ray_origin, .dir = ray_direction };
}
fn defocus_disk_sample(self: Camera) vec3 {
const p = random_in_unit_disk();
return self.center + (toVec3(p[0]) * self.defocus_disk_u) + (toVec3(p[1]) * self.defocus_disk_v);
}
pub fn new(world: *const HittableList) Camera {
const camera_center = lookfrom;
const pixel_samples_scale = 1.0 / @as(f64, @floatFromInt(samples_per_pixel));
// Camera
const theta = utils.degrees_to_radians(vfov);
const h = @tan(theta / 2);
const viewport_height: f64 = 2.0 * h * focus_dist;
const viewport_width: f64 = viewport_height * aspect_ratio;
const w = unit_vector(lookfrom - lookat);
const u = unit_vector(cross(vup, w));
const v = cross(w, u);
const viewport_u = toVec3(viewport_width) * u;
const viewport_v = toVec3(viewport_height) * -v;
const pixel_delta_u = viewport_u / toVec3(image_width);
const pixel_delta_v = viewport_v / toVec3(image_height);
const viewport_upper_left = camera_center - toVec3(focus_dist) * w - viewport_u / toVec3(2) - viewport_v / toVec3(2);
const pixel00_loc = viewport_upper_left + toVec3(0.5) * (pixel_delta_u + pixel_delta_v);
const defocus_radius = focus_dist * @tan(utils.degrees_to_radians(defocus_angle / 2.0));
const defocus_disk_u = u * toVec3(defocus_radius);
const defocus_disk_v = v * toVec3(defocus_radius);
return Camera{
.aspect_ratio = aspect_ratio,
.image_width = image_width,
.samples_per_pixel = samples_per_pixel,
.max_depth = max_depth,
.vfov = vfov,
.focus_dist = focus_dist,
.defocus_angle = defocus_angle,
.world = world,
.u = u,
.v = v,
.w = w,
.defocus_disk_v = defocus_disk_v,
.defocus_disk_u = defocus_disk_u,
.image_height = image_height,
.center = camera_center,
.pixel_samples_scale = pixel_samples_scale,
.pixel00_loc = pixel00_loc,
.pixel_delta_u = pixel_delta_u,
.pixel_delta_v = pixel_delta_v,
};
}
};
fn splitPixels(allocator: std.mem.Allocator, pixels: []Pixel, num_sublists: usize) ![][]Pixel {
const len = pixels.len;
if (num_sublists == 0 or num_sublists > len) {
return error.InvalidNumberOfSublists;
}
const sublist_size = len / num_sublists;
const remainder = len % num_sublists;
var result = try allocator.alloc([]Pixel, num_sublists);
var start: usize = 0;
for (0..num_sublists) |i| {
var end = start + sublist_size;
if (i < remainder) {
end += 1;
}
result[i] = pixels[start..end];
start = end;
}
return result;
}
fn random_in_unit_disk() vec3 {
while (true) {
const p = vec3{ utils.rand_mm(-1, 1), utils.rand_mm(-1, 1), 0 };
if (mat_math.length_squared(p) < 1)
return p;
}
}
fn ray_color(ray: Ray, depth: usize, world: *const HittableList) vec3 {
if (depth <= 0) {
return vec3{ 0, 0, 0 };
}
var rec = HitRecord.new();
if (world.hit(ray, Interval{ .min = 0.001, .max = math.inf(f64) }, &rec)) {
var ray_scattered = Ray{ .orig = vec3{ 0, 0, 0 }, .dir = vec3{ 0, 0, 0 } };
var attenuation = vec3{ 0, 0, 0 };
if (rec.material.scatter(ray, &rec, &attenuation, &ray_scattered)) {
return attenuation * ray_color(ray_scattered, depth - 1, world);
}
return vec3{ 0, 0, 0 };
}
const unit_direction = unit_vector(ray.dir);
const a = 0.5 * (unit_direction[1] + 1.0);
return toVec3(1.0 - a) * toVec3(1.0) + toVec3(a) * vec3{ 0.5, 0.7, 1.0 };
}
fn sample_square() vec3 {
return vec3{ utils.rand_01() - 0.5, utils.rand_01() - 0.5, 0 };
}
fn writeColor(color: vec3, writer: anytype) !void {
var r_float = color[0];
var g_float = color[1];
var b_float = color[2];
if ((r_float < -0.999) or (g_float < -0.999) or (b_float < -0.999)) {
@panic("-1 color detected");
}
r_float = utils.linear_to_gamma(r_float);
g_float = utils.linear_to_gamma(g_float);
b_float = utils.linear_to_gamma(b_float);
const intensity = Interval{ .min = 0, .max = 0.99 };
const r: u8 = @intFromFloat(256 * intensity.clamp(r_float));
const g: u8 = @intFromFloat(256 * intensity.clamp(g_float));
const b: u8 = @intFromFloat(256 * intensity.clamp(b_float));
try writer.print("{} {} {}\n", .{ r, g, b });
}
fn pbar(value: usize, max: usize) void {
const used_char = "-";
const number_of_char = 60;
const percent_done: usize = if (value == max - 1) 100 else @divFloor(value * 100, max);
const full_char: usize = @divFloor(number_of_char * percent_done, 100);
print("\r|", .{});
var i: usize = 0;
while (i < number_of_char) : (i += 1) {
if (i < full_char) {
print("{s}", .{used_char});
} else {
print(" ", .{});
}
}
print("| {}% |", .{percent_done});
print(" {} ", .{value});
print("/ {}", .{max});
if (percent_done == 100) {
print("\n", .{});
}
}