Skip to content

Instantly share code, notes, and snippets.

@jesterKing
Last active July 13, 2024 17:34
Show Gist options
  • Save jesterKing/bd0b1272e576a9d08dc6327078dee76d to your computer and use it in GitHub Desktop.
Save jesterKing/bd0b1272e576a9d08dc6327078dee76d to your computer and use it in GitHub Desktop.
Using 0.14.0-dev.23
Run on command-line:
zig build && ./zig-out/bin/raytrace > img.ppm
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "raytrace",
.target = target,
.optimize = optimize,
.root_source_file = b.path("raytrace.zig"),
});
b.installArtifact(exe);
}
const std = @import("std");
// set logging level to info.
pub const std_options = .{
.log_level = .info,
.logFn = myLogFn,
};
const infinity = std.math.inf(f64);
// log function that writes to stderr
pub fn myLogFn(
comptime _: std.log.Level,
comptime _: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
const stderr = std.io.getStdErr().writer();
nosuspend stderr.print(format, args) catch return;
}
const stdout = std.io.getStdOut().writer();
var _rng = std.Random.DefaultPrng.init(42);
const _random = _rng.random();
fn next_float() f64 {
return next_float_range(std.math.floatMin(f64), std.math.floatMax(f64));
}
fn next_float_range(min: f64, max: f64) f64 {
return min + (max - min) * _random.float(f64);
}
var mem: std.mem.Allocator = std.heap.page_allocator;
pub fn main() !void {
var arena_allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_allocator.deinit();
mem = arena_allocator.allocator();
var _world = HittableList.init(mem);
defer _world.deinit();
var world = Hittable{ .hittable_list = &_world };
const material_ground = Lambertian.createMaterial(Color.init(0.5, 0.5, 0.5));
try world.append(Sphere.createHittable(Point3.init(0, -1000, -1), 1000.0, material_ground));
const mat1 = Dielectric.createMaterial(1.5);
try world.append(Sphere.createHittable(Point3.init(0, 1, 0), 1.0, mat1));
const mat2 = Lambertian.createMaterial(Color.init(0.4, 0.2, 0.1));
try world.append(Sphere.createHittable(Point3.init(-4, 1, 0), 1.0, mat2));
const mat3 = Metal.createMaterial(Color.init(0.7, 0.6, 0.5), 0.0);
try world.append(Sphere.createHittable(Point3.init(4, 1, 0), 1.0, mat3));
const p = Point3.init(4, 0.2, 0);
var a: i32 = -11;
while (a < 11) : (a += 1) {
var b: i32 = -11;
while (b < 11) : (b += 1) {
const choose_mat = _random.float(f64);
const center = Point3.init(@as(f64, @floatFromInt(a)) + 0.9 * _random.float(f64), 0.2, @as(f64, @floatFromInt(b)) + 0.9 * _random.float(f64));
if (center.subtract(p).length() > 0.9) {
if (choose_mat < 0.8) {
const albedo = Color.random().multiply_compwise(Color.random());
const sphere_material = Lambertian.createMaterial(albedo);
try world.append(Sphere.createHittable(center, 0.2, sphere_material));
} else if (choose_mat < 0.95) {
const albedo = Color.random_range(0.5, 1.0);
const fuzz = next_float_range(0.0, 0.5);
const sphere_material = Metal.createMaterial(albedo, fuzz);
try world.append(Sphere.createHittable(center, 0.2, sphere_material));
} else {
const sphere_material = Dielectric.createMaterial(1.5);
try world.append(Sphere.createHittable(center, 0.2, sphere_material));
}
}
}
}
var cam = Camera.create(
16.0 / 9.0,
1920,
42,
100,
10,
30.0,
);
cam.lookfrom = Point3.init(13, 2, 3);
cam.lookat = Point3.init(0, 0, 0);
cam.vup = Vec3.init(0, 1, 0);
cam.defocus_angle = 0.6;
cam.focus_distance = 10.0;
try cam.render(&world);
}
const intensity = Interval.init(0.000, 0.999);
const Camera = struct {
aspect_ratio: f64,
image_width: usize,
seed: u64,
samples_per_pixel: u64,
max_ray_depth: usize,
vfov: f64,
defocus_angle: f64 = 0.0,
focus_distance: f64 = 10.0,
_image_height: usize = 0,
_pixel_samples_scale: f64 = 1.0,
_center: Point3 = Point3.initZero(),
_pixel00_loc: Point3 = Point3.initZero(),
_pixel_delta_u: Vec3 = Vec3.initZero(),
_pixel_delta_v: Vec3 = Vec3.initZero(),
lookfrom: Point3 = Point3.init(0, 0, 0),
lookat: Point3 = Point3.init(0, 0, -1),
vup: Vec3 = Vec3.init(0, 1, 0),
_u: Vec3 = Vec3.initZero(),
_v: Vec3 = Vec3.initZero(),
_w: Vec3 = Vec3.initZero(),
_defocus_disk_u: Vec3 = Vec3.initZero(),
_defocus_disk_v: Vec3 = Vec3.initZero(),
_rng: std.Random.Xoshiro256,
_random: std.Random,
pub fn create(aspect_ratio: f64, image_width: usize, seed: u64, samples: u64, max_ray_depth: usize, vfov: f64) Camera {
var rng = std.Random.DefaultPrng.init(seed);
const cam = Camera{
.aspect_ratio = aspect_ratio,
.image_width = image_width,
.vfov = vfov,
.seed = seed,
.samples_per_pixel = samples,
.max_ray_depth = max_ray_depth,
._rng = std.Random.DefaultPrng.init(seed),
._random = rng.random(),
};
return cam;
}
pub fn render(self: *Camera, world: *Hittable) !void {
self.initialize();
const ppm_writer_log = std.log.scoped(.ppm_write);
ppm_writer_log.info("Starting to write PPM image ...\n\n", .{});
try stdout.print("P3\n{} {}\n255\n", .{ self.image_width, self._image_height });
for (0..self._image_height) |h| {
ppm_writer_log.info("\rScanlines remaining: {} ", .{self._image_height - h});
var pixel_color = Color.initZero();
for (0..self.image_width) |w| {
for (0..self.samples_per_pixel) |_| {
var ray = self.get_ray(w, h);
pixel_color = pixel_color.add(ray_color(&ray, self.max_ray_depth, world));
}
pixel_color = pixel_color.multiply(f64, self._pixel_samples_scale);
writeColor(pixel_color);
}
try stdout.print("\n", .{});
}
ppm_writer_log.info("\rDone. \n", .{});
}
fn initialize(self: *Camera) void {
// set up random number generator
self._random = self._rng.random();
// determine image size
self._image_height = @as(usize, @intFromFloat(@as(f64, @floatFromInt(self.image_width)) / self.aspect_ratio));
self._image_height = if (self._image_height < 1) 1 else self._image_height;
self._pixel_samples_scale = 1.0 / @as(f64, @floatFromInt(self.samples_per_pixel));
self._center = self.lookfrom;
// set up viewport dimensions
const theta = std.math.degreesToRadians(self.vfov);
const h = @tan(theta / 2.0);
const viewport_height = 2.0 * h * self.focus_distance;
const viewport_width = viewport_height * (@as(f64, @floatFromInt(self.image_width))) / (@as(f64, @floatFromInt(self._image_height)));
self._w = self.lookfrom.subtract(self.lookat).unitized();
self._u = self.vup.cross(self._w).unitized();
self._v = self._w.cross(self._u);
const viewport_u = self._u.multiply(f64, viewport_width);
const viewport_v = self._v.neg().multiply(f64, viewport_height);
self._pixel_delta_u = viewport_u.divide(usize, self.image_width);
self._pixel_delta_v = viewport_v.divide(usize, self._image_height);
// calculate the location of the upper left pixel
const viewport_upper_left =
self._center
.subtract(self._w.multiply(f64, self.focus_distance))
.subtract(viewport_u.multiply(f64, 0.5))
.subtract(viewport_v.multiply(f64, 0.5));
self._pixel00_loc =
viewport_upper_left
.add(self._pixel_delta_u
.add(self._pixel_delta_v)
.multiply(f64, 0.5));
// defocus disk vectors
const defocus_radius = self.focus_distance * @tan(std.math.degreesToRadians(self.defocus_angle / 2.0));
self._defocus_disk_u = self._u.multiply(f64, defocus_radius);
self._defocus_disk_v = self._v.multiply(f64, defocus_radius);
std.log.debug("pixel_samples_scale: {d}\n", .{self._pixel_samples_scale});
}
fn linear_to_gamma(linear_component: f64) f64 {
return if (linear_component > 0) std.math.sqrt(linear_component) else 0;
}
fn writeColor(color: Color) void {
const r = linear_to_gamma(color.x);
const g = linear_to_gamma(color.y);
const b = linear_to_gamma(color.z);
const ir = @as(u8, @intFromFloat(255.999 * intensity.clamp(r)));
const ig = @as(u8, @intFromFloat(255.999 * intensity.clamp(g)));
const ib = @as(u8, @intFromFloat(255.999 * intensity.clamp(b)));
nosuspend stdout.print("{} {} {} ", .{ ir, ig, ib }) catch return;
}
fn get_ray(self: *Camera, x: usize, y: usize) Ray {
const offset = self.sample_square();
const pixel_sample = self._pixel00_loc
.add(self._pixel_delta_u.multiply(f64, @as(f64, @floatFromInt(x)) + offset.x))
.add(self._pixel_delta_v.multiply(f64, @as(f64, @floatFromInt(y)) + offset.y));
const ray_origin = if (self.defocus_angle <= 0) self._center else self.defocus_disk_sample();
const ray_direction = pixel_sample.subtract(ray_origin);
return Ray{
.origin = ray_origin,
.direction = ray_direction,
};
}
fn sample_square(self: *Camera) Vec3 {
const x = self._random.float(f64);
const y = self._random.float(f64);
return Vec3.init(x - 0.5, y - 0.5, 0.0);
}
fn defocus_disk_sample(self: *Camera) Point3 {
const disk_sample = Vec3.random_in_unit_disk();
return self._center
.add(self._defocus_disk_u.multiply(f64, disk_sample.x))
.add(self._defocus_disk_v.multiply(f64, disk_sample.y));
}
fn ray_color(r: *Ray, depth: usize, world: *Hittable) Color {
if (depth <= 0)
return Color.initZero();
var hit_record = HitRecord{};
if (world.hit(r, Interval.init(0.001, infinity), &hit_record)) {
var scattered = Ray.init(Point3.initZero(), Vec3.initZero());
var attenuation = Color.initZero();
if (hit_record.m) |mat| {
if (mat.scatter(r, &hit_record, &attenuation, &scattered)) {
return attenuation.multiply_compwise(ray_color(&scattered, depth - 1, world));
}
}
return Color.initZero();
}
const unit_direction = r.direction.unitized();
const a: f64 = 0.5 * (unit_direction.y + 1.0);
const blue = Color.init(0.2, 0.4, 0.7);
const white = Color.init(1.0, 1.0, 1.0);
return white
.multiply(f64, 1.0 - a)
.add(blue
.multiply(f64, a));
}
};
const Interval = struct {
min: f64,
max: f64,
pub fn initInfinity() Interval {
return Interval{
.min = -infinity,
.max = infinity,
};
}
pub fn init(min: f64, max: f64) Interval {
return Interval{
.min = min,
.max = max,
};
}
pub fn contains(self: Interval, x: f64) bool {
return (self.min <= x) and (x <= self.max);
}
pub fn surrounds(self: Interval, x: f64) bool {
return (self.min < x) and (x < self.max);
}
pub fn clamp(self: Interval, x: f64) f64 {
return if (x < self.min) self.min else if (x > self.max) self.max else x;
}
};
const empty = Interval.init(infinity, -infinity);
const universe = Interval.initInfinity();
var _magentaLambertian = Lambertian.init(Color.init(1, 0, 1));
var _magentaMaterial = Material{ .lambertian = &_magentaLambertian };
var _magentaSphere = Sphere.init(Point3.initZero(), 0.0, &_magentaMaterial);
var _magentaHittable = Hittable{ .sphere = &_magentaSphere };
const HitRecord = struct {
p: Point3 = Point3.initZero(),
n: Vec3 = Vec3.initZero(),
t: f64 = 0.0,
f: bool = false,
m: ?*Material = null,
pub fn set_face_normal(self: *HitRecord, r: *Ray, outward_normal: Vec3) void {
self.f = r.direction.dot(outward_normal) < 0;
self.n = if (self.f) outward_normal else outward_normal.neg();
}
};
const HittableList = struct {
objects: std.ArrayList(Hittable),
pub fn init(_mem: std.mem.Allocator) HittableList {
return HittableList{
.objects = std.ArrayList(Hittable).init(_mem),
};
}
pub fn deinit(self: *HittableList) void {
self.objects.deinit();
}
pub fn append(self: *HittableList, object: *Hittable) !void {
try self.objects.append(object.*);
}
pub fn hit(self: HittableList, r: *Ray, ray_t: Interval, rec: *HitRecord) bool {
var temp_rec = HitRecord{};
var hit_anything = false;
var closest_so_far = ray_t.max;
for (self.objects.items) |object| {
if (object.hit(r, Interval.init(ray_t.min, closest_so_far), &temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec.t = temp_rec.t;
rec.p = temp_rec.p;
rec.n = temp_rec.n;
rec.f = temp_rec.f;
rec.m = temp_rec.m;
}
}
return hit_anything;
}
};
const Sphere = struct {
center: Point3,
radius: f64,
material: *Material,
pub fn createHittable(center: Point3, radius: f64, material: *Material) *Hittable {
const sphere = mem.create(Sphere) catch return &_magentaHittable;
sphere.* = .{ .center = center, .radius = @max(0, radius), .material = material };
const hittable = mem.create(Hittable) catch return &_magentaHittable;
hittable.* = .{ .sphere = sphere };
return hittable;
}
pub fn init(center: Point3, radius: f64, material: *Material) Sphere {
return Sphere{
.center = center,
.radius = @max(0, radius),
.material = material,
};
}
pub fn hit(self: *Sphere, r: *Ray, ray_t: Interval, rec: *HitRecord) bool {
const oc = self.center.subtract(r.origin);
const a = r.direction.length_squared();
const h = r.direction.dot(oc);
const c = oc.length_squared() - self.radius * self.radius;
const discriminant = h * h - a * c;
if (discriminant < 0) {
return false;
}
const sqrtd = std.math.sqrt(discriminant);
var root = (h - sqrtd) / a;
if (!ray_t.surrounds(root)) {
root = (h + sqrtd) / a;
if (!ray_t.surrounds(root)) {
return false;
}
}
rec.t = root;
rec.p = r.at(rec.t);
const outward_normal = rec.p.subtract(self.center).divide(f64, self.radius).unitized();
rec.set_face_normal(r, outward_normal);
rec.m = self.material;
return true;
}
};
const Hittable = union(enum) {
sphere: *Sphere,
hittable_list: *HittableList,
pub fn hit(self: Hittable, r: *Ray, ray_t: Interval, rec: *HitRecord) bool {
return switch (self) {
inline else => |s| s.hit(r, ray_t, rec),
};
}
pub fn append(self: Hittable, object: *Hittable) !void {
switch (self) {
.hittable_list => |l| try l.append(object),
else => return,
}
}
};
const Lambertian = struct {
albedo: Color,
pub fn createMaterial(albedo: Color) *Material {
const mat: *Material = mem.create(Material) catch return &_magentaMaterial;
const lambertian = mem.create(Lambertian) catch return &_magentaMaterial;
lambertian.* = .{ .albedo = albedo };
mat.* = .{ .lambertian = lambertian };
return mat;
}
pub fn init(albedo: Color) Lambertian {
return Lambertian{
.albedo = albedo,
};
}
pub fn scatter(self: *Lambertian, r_in: *Ray, rec: *HitRecord, attenuation: *Color, scattered: *Ray) bool {
_ = r_in;
const scatter_direction = rec.n.add(Vec3.random_unit_vector());
scattered.origin = rec.p;
scattered.direction = scatter_direction;
if (scatter_direction.near_zero()) {
scattered.direction = rec.n;
}
attenuation.x = self.albedo.x;
attenuation.y = self.albedo.y;
attenuation.z = self.albedo.z;
return true;
}
};
const Metal = struct {
albedo: Color,
fuzz: f64,
pub fn createMaterial(albedo: Color, fuzz: f64) *Material {
const mat: *Material = mem.create(Material) catch return &_magentaMaterial;
const metal = mem.create(Metal) catch return &_magentaMaterial;
metal.* = .{ .albedo = albedo, .fuzz = @min(1.0, fuzz) };
mat.* = .{ .metal = metal };
return mat;
}
pub fn init(albedo: Color, fuzz: f64) Metal {
return Metal{
.albedo = albedo,
.fuzz = @min(1.0, fuzz),
};
}
pub fn scatter(self: *Metal, r_in: *Ray, rec: *HitRecord, attenuation: *Color, scattered: *Ray) bool {
const _reflected = Vec3.reflect(r_in.direction, rec.n).unitized();
const reflected = _reflected.add(Vec3.random_unit_vector().multiply(f64, self.fuzz));
scattered.origin = rec.p;
scattered.direction = reflected;
attenuation.x = self.albedo.x;
attenuation.y = self.albedo.y;
attenuation.z = self.albedo.z;
return scattered.direction.dot(rec.n) > 0;
}
};
const Dielectric = struct {
refraction_index: f64,
pub fn createMaterial(refraction_index: f64) *Material {
const mat: *Material = mem.create(Material) catch return &_magentaMaterial;
const dielectric = mem.create(Dielectric) catch return &_magentaMaterial;
dielectric.* = .{ .refraction_index = refraction_index };
mat.* = .{ .dielectric = dielectric };
return mat;
}
pub fn init(refraction_index: f64) Dielectric {
return Dielectric{
.refraction_index = refraction_index,
};
}
pub fn scatter(self: *Dielectric, r_in: *Ray, rec: *HitRecord, attenuation: *Color, scattered: *Ray) bool {
attenuation.x = 1.0;
attenuation.y = 1.0;
attenuation.z = 1.0;
const ri = if (rec.f) 1.0 / self.refraction_index else self.refraction_index;
const unit_direction = r_in.direction.unitized();
const cos_theta = @min(unit_direction.neg().dot(rec.n), 1.0);
const sin_theta = std.math.sqrt(1.0 - cos_theta * cos_theta);
const cannot_refract = ri * sin_theta > 1.0;
const reflect = cannot_refract or (reflectance(cos_theta, ri) > _random.float(f64));
const direction = if (reflect) Vec3.reflect(unit_direction, rec.n) else Vec3.refract(unit_direction, rec.n, ri);
scattered.origin = rec.p;
scattered.direction = direction;
return true;
}
fn reflectance(cosine: f64, refraction_index: f64) f64 {
var r0 = (1.0 - refraction_index) / (1.0 + refraction_index);
r0 = r0 * r0;
return r0 + (1.0 - r0) * std.math.pow(f64, 1.0 - cosine, 5.0);
}
};
const Material = union(enum) {
lambertian: *Lambertian,
metal: *Metal,
dielectric: *Dielectric,
pub fn scatter(self: Material, r_in: *Ray, rec: *HitRecord, attenuation: *Color, scattered: *Ray) bool {
return switch (self) {
inline else => |s| s.scatter(r_in, rec, attenuation, scattered),
};
}
};
const Color = Vec3;
const Point3 = Vec3;
const Vec3 = struct {
x: f64 = 0.0,
y: f64 = 0.0,
z: f64 = 0.0,
pub fn initZero() Vec3 {
return Vec3{};
}
pub fn init(e0: f64, e1: f64, e2: f64) Vec3 {
return Vec3{
.x = e0,
.y = e1,
.z = e2,
};
}
pub fn neg(self: Vec3) Vec3 {
return Vec3{
.x = -self.x,
.y = -self.y,
.z = -self.z,
};
}
pub fn add(self: Vec3, other: Vec3) Vec3 {
return Vec3{
.x = self.x + other.x,
.y = self.y + other.y,
.z = self.z + other.z,
};
}
pub fn subtract(self: Vec3, other: Vec3) Vec3 {
return self.add(other.neg());
}
pub fn multiply(self: Vec3, comptime T: type, t: T) Vec3 {
const _t = if (@TypeOf(t) == f64) t else @as(f64, @floatFromInt(t));
return Vec3{
.x = self.x * _t,
.y = self.y * _t,
.z = self.z * _t,
};
}
pub fn multiply_compwise(self: Vec3, other: Vec3) Vec3 {
return Vec3{
.x = self.x * other.x,
.y = self.y * other.y,
.z = self.z * other.z,
};
}
pub fn divide(self: Vec3, comptime T: type, t: T) Vec3 {
const _t = if (@TypeOf(t) == f64) t else @as(f64, @floatFromInt(t));
return self.multiply(f64, 1.0 / _t);
}
pub fn length(self: Vec3) f64 {
return std.math.sqrt(self.length_squared());
}
pub fn length_squared(self: Vec3) f64 {
return self.x * self.x + self.y * self.y + self.z * self.z;
}
pub fn dot(self: Vec3, other: Vec3) f64 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
pub fn cross(self: Vec3, other: Vec3) Vec3 {
return Vec3.init(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
);
}
pub fn unitized(self: Vec3) Vec3 {
return self.divide(f64, self.length());
}
pub fn near_zero(self: Vec3) bool {
const s = 1e-8;
return (@abs(self.x) < s) and (@abs(self.y) < s) and (@abs(self.z) < s);
}
pub fn random() Vec3 {
return Vec3.init(_random.float(f64), _random.float(f64), _random.float(f64));
}
pub fn randomfull() Vec3 {
return Vec3.init(next_float(), next_float(), next_float());
}
pub fn random_range(min: f64, max: f64) Vec3 {
return Vec3.init(next_float_range(min, max), next_float_range(min, max), next_float_range(min, max));
}
pub fn random_in_unit_sphere() Vec3 {
while (true) {
const p = Vec3.random_range(-1.0, 1.0);
if (p.length_squared() < 1.0) {
return p;
}
}
unreachable;
}
pub fn random_in_unit_disk() Vec3 {
while (true) {
const p = Vec3.init(next_float_range(-1.0, 1.0), next_float_range(-1.0, 1.0), 0.0);
if (p.length_squared() < 1.0) {
return p;
}
}
unreachable;
}
pub fn random_unit_vector() Vec3 {
return Vec3.random_in_unit_sphere().unitized();
}
pub fn random_on_hemisphere(normal: Vec3) Vec3 {
const in_unit_sphere = Vec3.random_in_unit_sphere();
return if (in_unit_sphere.dot(normal) > 0.0) in_unit_sphere else in_unit_sphere.neg();
}
pub fn reflect(v: Vec3, n: Vec3) Vec3 {
return v.subtract(n.multiply(f64, 2.0 * v.dot(n)));
}
pub fn refract(uv: Vec3, n: Vec3, etai_over_etat: f64) Vec3 {
const cos_theta = @min(uv.neg().dot(n), 1.0);
const r_out_perp = uv.add(n.multiply(f64, cos_theta)).multiply(f64, etai_over_etat);
const negsqrt: f64 = -std.math.sqrt(1.0 - r_out_perp.length_squared());
const r_out_parallel = n.multiply(f64, negsqrt);
return r_out_perp.add(r_out_parallel);
}
};
const Ray = struct {
origin: Point3 = Point3{},
direction: Vec3 = Vec3{},
pub fn init(origin: Point3, direction: Vec3) Ray {
return Ray{
.origin = origin,
.direction = direction,
};
}
pub fn at(self: Ray, t: f64) Point3 {
return self.origin.add(self.direction.multiply(f64, t));
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment