feat: play around with the raytracer
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
foreign fn ppm_start(width: i32, height: i32);
|
||||
foreign fn ppm_pixel(red: i32, green: i32, blue: i32);
|
||||
foreign fn ppm_finish();
|
||||
|
||||
foreign fn sqrtf(num: f32) -> f32;
|
||||
foreign fn tanf(num: f32) -> f32;
|
||||
foreign fn randf() -> f32;
|
||||
|
||||
struct Vec3 {
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32
|
||||
}
|
||||
|
||||
struct Ray {
|
||||
origin: Vec3,
|
||||
dir: Vec3
|
||||
}
|
||||
|
||||
// Added material type (0=Diffuse/Normal, 1=Glass, 2=Metal) and Refractive Index
|
||||
struct Sphere {
|
||||
center: Vec3,
|
||||
radius: f32,
|
||||
color: Vec3,
|
||||
mat_type: i32,
|
||||
ref_idx: f32,
|
||||
fuzz: f32
|
||||
}
|
||||
|
||||
struct Hit {
|
||||
hit: bool,
|
||||
t: f32,
|
||||
normal: Vec3,
|
||||
color: Vec3,
|
||||
mat_type: i32,
|
||||
ref_idx: f32,
|
||||
fuzz: f32
|
||||
}
|
||||
|
||||
// Wrapper for refraction results
|
||||
struct RefractResult {
|
||||
success: bool,
|
||||
dir: Vec3
|
||||
}
|
||||
|
||||
fn vec3_add(a: Vec3, b: Vec3) -> Vec3 {
|
||||
return Vec3 { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
||||
}
|
||||
|
||||
fn vec3_sub(a: Vec3, b: Vec3) -> Vec3 {
|
||||
return Vec3 { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
||||
}
|
||||
|
||||
fn vec3_mul(a: Vec3, t: f32) -> Vec3 {
|
||||
return Vec3 { x: a.x * t, y: a.y * t, z: a.z * t };
|
||||
}
|
||||
|
||||
fn vec3_dot(a: Vec3, b: Vec3) -> f32 {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
|
||||
fn vec3_normalize(v: Vec3) -> Vec3 {
|
||||
let len = sqrtf(vec3_dot(v, v));
|
||||
if len == 0.0 {
|
||||
return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
}
|
||||
return vec3_mul(v, 1.0 / len);
|
||||
}
|
||||
|
||||
// V - 2 * dot(V, N) * N
|
||||
fn vec3_reflect(v: Vec3, n: Vec3) -> Vec3 {
|
||||
return vec3_sub(v, vec3_mul(n, 2.0 * vec3_dot(v, n)));
|
||||
}
|
||||
|
||||
// Snell's Law for calculating the refracted ray direction
|
||||
fn vec3_refract(v: Vec3, n: Vec3, ni_over_nt: f32) -> RefractResult {
|
||||
let uv = vec3_normalize(v);
|
||||
let dt = vec3_dot(uv, n);
|
||||
let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt);
|
||||
|
||||
if discriminant > 0.0 {
|
||||
let temp1 = vec3_mul(vec3_sub(uv, vec3_mul(n, dt)), ni_over_nt);
|
||||
let temp2 = vec3_mul(n, sqrtf(discriminant));
|
||||
return RefractResult {
|
||||
success: true,
|
||||
dir: vec3_sub(temp1, temp2)
|
||||
};
|
||||
}
|
||||
|
||||
// Total internal reflection occurred
|
||||
return RefractResult {
|
||||
success: false,
|
||||
dir: Vec3 { x: 0.0, y: 0.0, z: 0.0 }
|
||||
};
|
||||
}
|
||||
|
||||
// The Cross Product generates a perpendicular vector to a and b
|
||||
fn vec3_cross(a: Vec3, b: Vec3) -> Vec3 {
|
||||
return Vec3 {
|
||||
x: a.y * b.z - a.z * b.y,
|
||||
y: a.z * b.x - a.x * b.z,
|
||||
z: a.x * b.y - a.y * b.x
|
||||
};
|
||||
}
|
||||
|
||||
// Generates a random point inside a 2D unit disk (used for depth of field blur)
|
||||
fn random_in_unit_disk() -> Vec3 {
|
||||
let p = Vec3 { x: 1.0, y: 1.0, z: 0.0 };
|
||||
let found = false;
|
||||
while !found {
|
||||
p = Vec3 {
|
||||
x: randf() * 2.0 - 1.0,
|
||||
y: randf() * 2.0 - 1.0,
|
||||
z: 0.0
|
||||
};
|
||||
// Check if the point is inside the radius of 1.0
|
||||
if vec3_dot(p, p) < 1.0 {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
fn intersect_sphere(ray: *Ray, sphere: *Sphere, t_min: f32, t_max: f32) -> Hit {
|
||||
let oc = vec3_sub((*sphere).center, (*ray).origin);
|
||||
let a = vec3_dot((*ray).dir, (*ray).dir);
|
||||
let h = vec3_dot((*ray).dir, oc);
|
||||
let c = vec3_dot(oc, oc) - (*sphere).radius * (*sphere).radius;
|
||||
let discriminant = h * h - a * c;
|
||||
|
||||
let miss = Hit {
|
||||
hit: false, t: 0.0, normal: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
|
||||
color: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, mat_type: 0, ref_idx: 0.0,
|
||||
fuzz: 0.0 // Ensure this is present
|
||||
};
|
||||
|
||||
if discriminant < 0.0 { return miss; }
|
||||
|
||||
let sqrtd = sqrtf(discriminant);
|
||||
let root = (h - sqrtd) / a;
|
||||
|
||||
if root <= t_min {
|
||||
root = (h + sqrtd) / a;
|
||||
if root <= t_min { return miss; }
|
||||
}
|
||||
if root >= t_max { return miss; }
|
||||
|
||||
let hit_point = vec3_add((*ray).origin, vec3_mul((*ray).dir, root));
|
||||
let normal = vec3_mul(vec3_sub(hit_point, (*sphere).center), 1.0 / (*sphere).radius);
|
||||
|
||||
return Hit {
|
||||
hit: true, t: root, normal: normal,
|
||||
color: (*sphere).color, mat_type: (*sphere).mat_type,
|
||||
ref_idx: (*sphere).ref_idx,
|
||||
fuzz: (*sphere).fuzz // Copy fuzz from the sphere
|
||||
};
|
||||
}
|
||||
|
||||
fn schlick(cosine: f32, ref_idx: f32) -> f32 {
|
||||
let r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
|
||||
let r0_sq = r0 * r0;
|
||||
let comp = 1.0 - cosine;
|
||||
// Approximating (1 - cosine)^5
|
||||
return r0_sq + (1.0 - r0_sq) * comp * comp * comp * comp * comp;
|
||||
}
|
||||
|
||||
fn random_unit_vector() -> Vec3 {
|
||||
// Generate a random vector in a cube
|
||||
let v = Vec3 {
|
||||
x: randf() * 2.0 - 1.0,
|
||||
y: randf() * 2.0 - 1.0,
|
||||
z: randf() * 2.0 - 1.0
|
||||
};
|
||||
// Normalize it to project it onto the unit sphere
|
||||
return vec3_normalize(v);
|
||||
}
|
||||
|
||||
fn ray_color(ray: *Ray, spheres: *[Sphere; 500], sphere_count: i32, depth: i32) -> Vec3 {
|
||||
if depth <= 0 {
|
||||
return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
}
|
||||
|
||||
let closest_so_far = 100000.0; // Increased to ensure far spheres render
|
||||
let hit_anything = false;
|
||||
let final_hit = Hit {
|
||||
hit: false, t: 0.0, normal: Vec3 { x: 0.0, y: 0.0, z: 0.0 },
|
||||
color: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
|
||||
};
|
||||
|
||||
// Iterate up to the active sphere_count, not a hardcoded 4
|
||||
let i = 0;
|
||||
while i < sphere_count {
|
||||
let hit = intersect_sphere(ray, &(*spheres)[i], 0.001, closest_so_far);
|
||||
if hit.hit {
|
||||
hit_anything = true;
|
||||
closest_so_far = hit.t;
|
||||
final_hit = hit;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
if hit_anything {
|
||||
let hit_point = vec3_add((*ray).origin, vec3_mul((*ray).dir, final_hit.t));
|
||||
let unit_dir = vec3_normalize((*ray).dir);
|
||||
|
||||
if final_hit.mat_type == 1 { // Glass
|
||||
let outward_normal: Vec3;
|
||||
let ni_over_nt: f32;
|
||||
let dir_dot_normal = vec3_dot(unit_dir, final_hit.normal);
|
||||
|
||||
if dir_dot_normal > 0.0 {
|
||||
outward_normal = vec3_mul(final_hit.normal, -1.0);
|
||||
ni_over_nt = final_hit.ref_idx;
|
||||
} else {
|
||||
outward_normal = final_hit.normal;
|
||||
ni_over_nt = 1.0 / final_hit.ref_idx;
|
||||
}
|
||||
|
||||
let cos_theta = vec3_dot(vec3_mul(unit_dir, -1.0), outward_normal);
|
||||
let refract_result = vec3_refract((*ray).dir, outward_normal, ni_over_nt);
|
||||
|
||||
let reflect_prob = 1.0;
|
||||
if refract_result.success {
|
||||
reflect_prob = schlick(cos_theta, final_hit.ref_idx);
|
||||
}
|
||||
|
||||
let scattered_ray: Ray;
|
||||
if randf() < reflect_prob {
|
||||
// Use normalized unit_dir to prevent self-intersection
|
||||
let reflected = vec3_reflect(unit_dir, final_hit.normal);
|
||||
scattered_ray = Ray { origin: hit_point, dir: reflected };
|
||||
} else {
|
||||
scattered_ray = Ray { origin: hit_point, dir: refract_result.dir };
|
||||
}
|
||||
|
||||
let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);
|
||||
return Vec3 {
|
||||
x: final_hit.color.x * bounce_color.x,
|
||||
y: final_hit.color.y * bounce_color.y,
|
||||
z: final_hit.color.z * bounce_color.z
|
||||
};
|
||||
}
|
||||
|
||||
if final_hit.mat_type == 2 { // Metal
|
||||
let reflected = vec3_reflect(unit_dir, final_hit.normal);
|
||||
|
||||
// Apply fuzz by randomly perturbing the reflected vector
|
||||
let fuzzed_dir = vec3_add(reflected, vec3_mul(random_unit_vector(), final_hit.fuzz));
|
||||
let scattered_ray = Ray { origin: hit_point, dir: vec3_normalize(fuzzed_dir) };
|
||||
|
||||
// Only scatter if the fuzzed ray doesn't bounce inward
|
||||
if vec3_dot(scattered_ray.dir, final_hit.normal) > 0.0 {
|
||||
let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);
|
||||
return Vec3 {
|
||||
x: final_hit.color.x * bounce_color.x,
|
||||
y: final_hit.color.y * bounce_color.y,
|
||||
z: final_hit.color.z * bounce_color.z
|
||||
};
|
||||
}
|
||||
return Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
}
|
||||
|
||||
// True Lambertian Diffuse
|
||||
let scatter_dir = vec3_add(final_hit.normal, random_unit_vector());
|
||||
|
||||
// Catch degenerate scatter direction (if random vector exactly opposes normal)
|
||||
if vec3_dot(scatter_dir, scatter_dir) < 0.001 {
|
||||
scatter_dir = final_hit.normal;
|
||||
}
|
||||
|
||||
// Ensure the outbound ray is perfectly normalized
|
||||
let scattered_ray = Ray { origin: hit_point, dir: vec3_normalize(scatter_dir) };
|
||||
let bounce_color = ray_color(&scattered_ray, spheres, sphere_count, depth - 1);
|
||||
|
||||
return Vec3 {
|
||||
x: final_hit.color.x * bounce_color.x,
|
||||
y: final_hit.color.y * bounce_color.y,
|
||||
z: final_hit.color.z * bounce_color.z
|
||||
};
|
||||
}
|
||||
|
||||
// Skybox gradient
|
||||
let unit_dir = vec3_normalize((*ray).dir);
|
||||
let t = 0.5 * (unit_dir.y + 1.0);
|
||||
return vec3_add(
|
||||
vec3_mul(Vec3 { x: 1.0, y: 1.0, z: 1.0 }, 1.0 - t),
|
||||
vec3_mul(Vec3 { x: 0.5, y: 0.7, z: 1.0 }, t)
|
||||
);
|
||||
}
|
||||
|
||||
fn main() -> i32 {
|
||||
let aspect_ratio = 16.0 / 9.0;
|
||||
let image_width = 1920;
|
||||
let image_height = ((image_width as f32) / aspect_ratio) as i32;
|
||||
|
||||
let samples_per_pixel = 10; // Increase this for higher quality
|
||||
let max_depth = 32;
|
||||
|
||||
// Allocate our static maximum array and track how many we've placed
|
||||
let spheres: [Sphere; 500];
|
||||
let sphere_count = 0;
|
||||
|
||||
// Ground sphere
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: Vec3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0,
|
||||
color: Vec3 { x: 0.5, y: 0.5, z: 0.5 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
|
||||
};
|
||||
sphere_count = sphere_count + 1;
|
||||
|
||||
// Generate random tiny spheres
|
||||
let a = -11;
|
||||
while a < 11 {
|
||||
let b = -11;
|
||||
while b < 11 {
|
||||
let choose_mat = randf();
|
||||
let center = Vec3 {
|
||||
x: (a as f32) + 0.9 * randf(),
|
||||
y: 0.2,
|
||||
z: (b as f32) + 0.9 * randf()
|
||||
};
|
||||
|
||||
// Avoid spawning inside the giant main spheres
|
||||
let center_offset = vec3_sub(center, Vec3 { x: 4.0, y: 0.2, z: 0.0 });
|
||||
let dist_sq = vec3_dot(center_offset, center_offset);
|
||||
|
||||
if dist_sq > (0.9 * 0.9) {
|
||||
if choose_mat < 0.8 {
|
||||
// Diffuse (albedo = color::random() * color::random())
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: center, radius: 0.2,
|
||||
color: Vec3 { x: randf() * randf(), y: randf() * randf(), z: randf() * randf() },
|
||||
mat_type: 0, ref_idx: 0.0, fuzz: 0.0
|
||||
};
|
||||
} else if choose_mat < 0.95 {
|
||||
// Metal (albedo = random(0.5, 1), fuzz = random(0, 0.5))
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: center, radius: 0.2,
|
||||
color: Vec3 { x: 0.5 + 0.5 * randf(), y: 0.5 + 0.5 * randf(), z: 0.5 + 0.5 * randf() },
|
||||
mat_type: 2, ref_idx: 0.0, fuzz: randf() * 0.5
|
||||
};
|
||||
} else {
|
||||
// Glass
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: center, radius: 0.2,
|
||||
color: Vec3 { x: 1.0, y: 1.0, z: 1.0 }, // Clear
|
||||
mat_type: 1, ref_idx: 1.5, fuzz: 0.0
|
||||
};
|
||||
}
|
||||
sphere_count = sphere_count + 1;
|
||||
}
|
||||
b = b + 1;
|
||||
}
|
||||
a = a + 1;
|
||||
}
|
||||
|
||||
// Material 1: Giant Central Glass Sphere
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: Vec3 { x: 0.0, y: 1.0, z: 0.0 }, radius: 1.0,
|
||||
color: Vec3 { x: 1.0, y: 1.0, z: 1.0 }, mat_type: 1, ref_idx: 1.5, fuzz: 0.0
|
||||
};
|
||||
sphere_count = sphere_count + 1;
|
||||
|
||||
// Material 2: Giant Left Diffuse Sphere
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: Vec3 { x: -4.0, y: 1.0, z: 0.0 }, radius: 1.0,
|
||||
color: Vec3 { x: 0.4, y: 0.2, z: 0.1 }, mat_type: 0, ref_idx: 0.0, fuzz: 0.0
|
||||
};
|
||||
sphere_count = sphere_count + 1;
|
||||
|
||||
// Material 3: Giant Right Metal Sphere
|
||||
spheres[sphere_count] = Sphere {
|
||||
center: Vec3 { x: 4.0, y: 1.0, z: 0.0 }, radius: 1.0,
|
||||
color: Vec3 { x: 0.7, y: 0.6, z: 0.5 }, mat_type: 2, ref_idx: 0.0, fuzz: 0.0
|
||||
};
|
||||
sphere_count = sphere_count + 1;
|
||||
|
||||
ppm_start(image_width, image_height);
|
||||
|
||||
// --- Camera Settings ---
|
||||
let vfov = 20.0;
|
||||
let lookfrom = Vec3 { x: 13.0, y: 2.0, z: 3.0 };
|
||||
let lookat = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
let vup = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
|
||||
let defocus_angle = 0.6;
|
||||
let focus_dist = 10.0;
|
||||
|
||||
let pi = 3.14159265;
|
||||
let theta = vfov * pi / 180.0;
|
||||
let h = tanf(theta / 2.0);
|
||||
|
||||
let viewport_height = 2.0 * h * focus_dist;
|
||||
let viewport_width = viewport_height * ((image_width as f32) / (image_height as f32));
|
||||
|
||||
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
|
||||
let w = vec3_normalize(vec3_sub(lookfrom, lookat));
|
||||
let u = vec3_normalize(vec3_cross(vup, w));
|
||||
let v = vec3_cross(w, u);
|
||||
|
||||
// Calculate the vectors across the horizontal and down the vertical viewport edges.
|
||||
let viewport_u = vec3_mul(u, viewport_width);
|
||||
let viewport_v = vec3_mul(v, -viewport_height); // Negative to point down the image
|
||||
|
||||
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
|
||||
let pixel_delta_u = vec3_mul(viewport_u, 1.0 / (image_width as f32));
|
||||
let pixel_delta_v = vec3_mul(viewport_v, 1.0 / (image_height as f32));
|
||||
|
||||
// Calculate the location of the upper left pixel.
|
||||
let half_u = vec3_mul(viewport_u, 0.5);
|
||||
let half_v = vec3_mul(viewport_v, 0.5);
|
||||
let focus_w = vec3_mul(w, focus_dist);
|
||||
let viewport_upper_left = vec3_sub(vec3_sub(vec3_sub(lookfrom, focus_w), half_u), half_v);
|
||||
|
||||
let pixel00_loc = vec3_add(viewport_upper_left, vec3_mul(vec3_add(pixel_delta_u, pixel_delta_v), 0.5));
|
||||
|
||||
// Calculate the camera defocus disk basis vectors.
|
||||
let defocus_radius = focus_dist * tanf((defocus_angle / 2.0) * pi / 180.0);
|
||||
let defocus_disk_u = vec3_mul(u, defocus_radius);
|
||||
let defocus_disk_v = vec3_mul(v, defocus_radius);
|
||||
|
||||
// --- Render ---
|
||||
let j = 0;
|
||||
while j < image_height {
|
||||
let i = 0;
|
||||
while i < image_width {
|
||||
let pixel_color = Vec3 { x: 0.0, y: 0.0, z: 0.0 };
|
||||
|
||||
let s = 0;
|
||||
while s < samples_per_pixel {
|
||||
let px = -0.5 + randf();
|
||||
let py = -0.5 + randf();
|
||||
|
||||
let pixel_center = vec3_add(pixel00_loc, vec3_add(vec3_mul(pixel_delta_u, i as f32), vec3_mul(pixel_delta_v, j as f32)));
|
||||
let sample_offset = vec3_add(vec3_mul(pixel_delta_u, px), vec3_mul(pixel_delta_v, py));
|
||||
let sample_loc = vec3_add(pixel_center, sample_offset);
|
||||
|
||||
// Apply Depth of Field logic
|
||||
let ray_origin = lookfrom;
|
||||
if defocus_angle > 0.0 {
|
||||
let p = random_in_unit_disk();
|
||||
let defocus_offset = vec3_add(vec3_mul(defocus_disk_u, p.x), vec3_mul(defocus_disk_v, p.y));
|
||||
ray_origin = vec3_add(lookfrom, defocus_offset);
|
||||
}
|
||||
|
||||
let ray_dir = vec3_sub(sample_loc, ray_origin);
|
||||
let ray = Ray { origin: ray_origin, dir: ray_dir };
|
||||
|
||||
let sample_color = ray_color(&ray, &spheres, sphere_count, max_depth);
|
||||
pixel_color = vec3_add(pixel_color, sample_color);
|
||||
|
||||
s = s + 1;
|
||||
}
|
||||
|
||||
let scale = 1.0 / (samples_per_pixel as f32);
|
||||
pixel_color = vec3_mul(pixel_color, scale);
|
||||
|
||||
pixel_color.x = sqrtf(pixel_color.x);
|
||||
pixel_color.y = sqrtf(pixel_color.y);
|
||||
pixel_color.z = sqrtf(pixel_color.z);
|
||||
|
||||
let ir = (255.999 * pixel_color.x) as i32;
|
||||
let ig = (255.999 * pixel_color.y) as i32;
|
||||
let ib = (255.999 * pixel_color.z) as i32;
|
||||
|
||||
ppm_pixel(ir, ig, ib);
|
||||
|
||||
i = i + 1;
|
||||
}
|
||||
j = j + 1;
|
||||
}
|
||||
|
||||
ppm_finish();
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user