Add raytracer example (wip)
This commit is contained in:
parent
07a50e5f7a
commit
c83a5be119
2 changed files with 829 additions and 0 deletions
BIN
examples/raytracer.png
Normal file
BIN
examples/raytracer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 563 KiB |
829
examples/raytracer.psl
Normal file
829
examples/raytracer.psl
Normal file
|
|
@ -0,0 +1,829 @@
|
|||
// ===== Dynamic memory =====
|
||||
|
||||
func allocate(size: u64) -> unit mut*:
|
||||
foreign func malloc(size: u64) -> unit mut *
|
||||
return malloc(size)
|
||||
|
||||
func deallocate(ptr: unit*):
|
||||
foreign func free(ptr: unit*)
|
||||
free(ptr)
|
||||
|
||||
// ===== Debug IO =====
|
||||
|
||||
func floor(x: f32) -> i32:
|
||||
foreign func floorf(x: f32) -> f32
|
||||
return floorf(x) as i32
|
||||
|
||||
// No function overloading yet...
|
||||
func print_byte(c: u8):
|
||||
foreign func putchar(c: i32) -> i32
|
||||
putchar(c as i32)
|
||||
|
||||
func print_str(str: u8*):
|
||||
// Don't use puts() because it adds a newline
|
||||
mut p = str
|
||||
while *p != 0ub:
|
||||
print_byte(*p)
|
||||
p += 1
|
||||
|
||||
func print_i32(x: i32):
|
||||
if x < 0:
|
||||
print_byte('-')
|
||||
print_i32(-x)
|
||||
return
|
||||
if x >= 10:
|
||||
print_i32(x / 10)
|
||||
print_byte('0' + (x % 10 as u8))
|
||||
|
||||
func print_f32(x: f32):
|
||||
if x < 0.0:
|
||||
print_byte('-')
|
||||
print_f32(-x)
|
||||
return
|
||||
// Print integer part
|
||||
let xfloor = floor(x)
|
||||
print_i32(xfloor)
|
||||
print_byte('.')
|
||||
mut y = x - (xfloor as f32)
|
||||
// Print fractional part
|
||||
mut i = 0
|
||||
while i < 3:
|
||||
y = y * 10.0
|
||||
let yfloor = floor(y)
|
||||
print_byte('0' + (yfloor as u8))
|
||||
y = y - (yfloor as f32)
|
||||
i = i + 1
|
||||
|
||||
func print_vec3(v: vec3):
|
||||
print_byte('(')
|
||||
print_f32(v.x)
|
||||
print_byte(',')
|
||||
print_f32(v.y)
|
||||
print_byte(',')
|
||||
print_f32(v.z)
|
||||
print_byte(')')
|
||||
|
||||
func print_time(t: f32):
|
||||
if t >= 3600.0:
|
||||
let hours = floor(t / 3600.0)
|
||||
print_i32(hours)
|
||||
print_byte('h')
|
||||
print_byte(' ')
|
||||
let minutes = floor((t - (hours as f32) * 3600.0) / 60.0)
|
||||
print_i32(minutes)
|
||||
print_byte('m')
|
||||
else if t >= 60.0:
|
||||
let minutes = floor(t / 60.0)
|
||||
print_i32(minutes)
|
||||
print_byte('m')
|
||||
print_byte(' ')
|
||||
let seconds = t - (minutes as f32) * 60.0
|
||||
print_f32(seconds)
|
||||
print_byte('s')
|
||||
else if t >= 1.0:
|
||||
print_f32(t)
|
||||
print_byte('s')
|
||||
else if t >= 0.001:
|
||||
print_f32(t * 1000.0)
|
||||
print_byte('m')
|
||||
print_byte('s')
|
||||
else if t >= 0.000001:
|
||||
print_f32(t * 1000000.0)
|
||||
print_byte('u')
|
||||
print_byte('s')
|
||||
else:
|
||||
print_f32(t * 1000000000.0)
|
||||
print_byte('n')
|
||||
print_byte('s')
|
||||
|
||||
func flush():
|
||||
foreign func fflush(stream: unit*) -> i32
|
||||
fflush(0ul as unit*)
|
||||
|
||||
// ===== File IO =====
|
||||
|
||||
// Opaque empty struct for type safety
|
||||
struct file:
|
||||
|
||||
func open(path: u8*, mode: u8) -> file*:
|
||||
foreign func fopen(path: u8*, mode: u8*) -> file*
|
||||
// A hack to turn a single u8 into a zero-terminated array
|
||||
let mode_wide = mode as u32
|
||||
return fopen(path, &mode as u8*)
|
||||
|
||||
func close(file: file*):
|
||||
foreign func fclose(file: file*) -> i32
|
||||
fclose(file)
|
||||
|
||||
func write(file: file*, data: u8*, size: u64):
|
||||
foreign func fwrite(data: u8*, size: u64, count: u64, file: file*) -> u64
|
||||
fwrite(data, size, 1ul, file)
|
||||
|
||||
func write_byte(file: file*, value: u8):
|
||||
foreign func fputc(ch: i32, file: file*) -> i32
|
||||
fputc(value as i32, file)
|
||||
|
||||
func write_u64(file: file*, value: u64):
|
||||
if value >= 10ul:
|
||||
write_u64(file, value / 10ul)
|
||||
write_byte(file, '0' + (value % 10ul as u8))
|
||||
|
||||
// ===== Time =====
|
||||
|
||||
// Platform-dependent!
|
||||
struct timespec:
|
||||
seconds: i64
|
||||
nanoseconds: i64
|
||||
|
||||
const TIME_UTC = 1
|
||||
|
||||
func get_time() -> timespec:
|
||||
foreign func timespec_get(ts: timespec mut*, base: i32) -> i32
|
||||
mut result = timespec()
|
||||
timespec_get(&mut result, TIME_UTC)
|
||||
return result
|
||||
|
||||
func time_delta(x: timespec, y: timespec) -> f32:
|
||||
return ((x.seconds - y.seconds) as f32) + ((x.nanoseconds - y.nanoseconds) as f32) / 1000000000.0
|
||||
|
||||
// ===== Image =====
|
||||
|
||||
struct image:
|
||||
width: u64
|
||||
height: u64
|
||||
data: u8[3] mut*
|
||||
|
||||
func create_image(width: u64, height: u64) -> image:
|
||||
return image(width, height, allocate(width * height * 3ul) as u8[3] mut *)
|
||||
|
||||
// ===== PPM format helpers =====
|
||||
|
||||
func write_ppm(path: u8*, image: image):
|
||||
let file = open(path, 'w')
|
||||
write_byte(file, 'P')
|
||||
write_byte(file, '6')
|
||||
write_byte(file, '\n')
|
||||
write_u64(file, image.width)
|
||||
write_byte(file, ' ')
|
||||
write_u64(file, image.height)
|
||||
write_byte(file, '\n')
|
||||
write_u64(file, 255ul)
|
||||
write_byte(file, '\n')
|
||||
write(file, image.data as u8*, image.width * image.height * 3ul)
|
||||
close(file)
|
||||
|
||||
// ===== Math =====
|
||||
|
||||
const pi = 3.141592653589793
|
||||
|
||||
func sin(x: f32) -> f32:
|
||||
foreign func sinf(x: f32) -> f32
|
||||
return sinf(x)
|
||||
|
||||
func cos(x: f32) -> f32:
|
||||
foreign func cosf(x: f32) -> f32
|
||||
return cosf(x)
|
||||
|
||||
func sqrt(x: f32) -> f32:
|
||||
foreign func sqrtf(x: f32) -> f32
|
||||
return sqrtf(x)
|
||||
|
||||
func tan(x: f32) -> f32:
|
||||
foreign func tanf(x: f32) -> f32
|
||||
return tanf(x)
|
||||
|
||||
func atan(x: f32) -> f32:
|
||||
foreign func atanf(x: f32) -> f32
|
||||
return atanf(x)
|
||||
|
||||
func pow(x: f32, y: f32) -> f32:
|
||||
foreign func powf(x: f32, y: f32) -> f32
|
||||
return powf(x, y)
|
||||
|
||||
func log(x: f32) -> f32:
|
||||
foreign func logf(x: f32) -> f32
|
||||
return logf(x)
|
||||
|
||||
func abs(x: f32) -> f32:
|
||||
if x < 0.0:
|
||||
return -x
|
||||
return x
|
||||
|
||||
func min(x: f32, y: f32) -> f32:
|
||||
if x < y:
|
||||
return x
|
||||
return y
|
||||
|
||||
func max(x: f32, y: f32) -> f32:
|
||||
if x > y:
|
||||
return x
|
||||
return y
|
||||
|
||||
func clamp(x: f32) -> f32:
|
||||
return max(0.0, min(1.0, x))
|
||||
|
||||
struct vec3:
|
||||
x: f32
|
||||
y: f32
|
||||
z: f32
|
||||
|
||||
// No operator overloading yet...
|
||||
func add(v: vec3, u: vec3) -> vec3:
|
||||
return vec3(v.x + u.x, v.y + u.y, v.z + u.z)
|
||||
|
||||
func sub(v: vec3, u: vec3) -> vec3:
|
||||
return vec3(v.x - u.x, v.y - u.y, v.z - u.z)
|
||||
|
||||
// Multiply scalar
|
||||
// No function overloading yet...
|
||||
func mults(v: vec3, s: f32) -> vec3:
|
||||
return vec3(v.x * s, v.y * s, v.z * s)
|
||||
|
||||
// Multiply vector (component-wise)
|
||||
func multv(v: vec3, u: vec3) -> vec3:
|
||||
return vec3(v.x * u.x, v.y * u.y, v.z * u.z)
|
||||
|
||||
// Divide vector (component-wise)
|
||||
func divv(v: vec3, u: vec3) -> vec3:
|
||||
return vec3(v.x / u.x, v.y / u.y, v.z / u.z)
|
||||
|
||||
func lerp(v: vec3, u: vec3, t: f32) -> vec3:
|
||||
return add(v, mults(sub(u, v), t))
|
||||
|
||||
func dot(v: vec3, u: vec3) -> f32:
|
||||
return v.x * u.x + v.y * u.y + v.z * u.z
|
||||
|
||||
func cross(v: vec3, u: vec3) -> vec3:
|
||||
return vec3(v.y * u.z - v.z * u.y, v.z * u.x - v.x * u.z, v.x * u.y - v.y * u.x)
|
||||
|
||||
func length(v: vec3) -> f32:
|
||||
return sqrt(dot(v, v))
|
||||
|
||||
func normalized(v: vec3) -> vec3:
|
||||
return mults(v, 1.0 / length(v))
|
||||
|
||||
struct ray:
|
||||
origin: vec3
|
||||
direction: vec3
|
||||
|
||||
struct quaternion:
|
||||
x: f32
|
||||
y: f32
|
||||
z: f32
|
||||
w: f32
|
||||
|
||||
const identity = quaternion(0.0, 0.0, 0.0, 1.0)
|
||||
|
||||
func inverse(q: quaternion) -> quaternion:
|
||||
return quaternion(-q.x, -q.y, -q.z, q.w)
|
||||
|
||||
func rotation(axis: vec3, angle: f32) -> quaternion:
|
||||
let s = sin(angle * 0.5)
|
||||
let c = cos(angle * 0.5)
|
||||
return quaternion(axis.x * s, axis.y * s, axis.z * s, c)
|
||||
|
||||
func rotate(q: quaternion, v: vec3) -> vec3:
|
||||
let qv = vec3(q.x, q.y, q.z)
|
||||
let t = mults(cross(qv, v), 2.0)
|
||||
return add(add(v, mults(t, q.w)), cross(qv, t))
|
||||
|
||||
// ===== Random number generation =====
|
||||
|
||||
struct rng:
|
||||
state: u64[2]
|
||||
|
||||
func splitmix64(x: u64) -> u64:
|
||||
mut result = x
|
||||
result += 11400714819323198485ul
|
||||
result = (result ^ (result >> 30u)) * 13787848793156543929ul
|
||||
result = (result ^ (result >> 27u)) * 10723151780598845931ul
|
||||
return result ^ (result >> 31u)
|
||||
|
||||
func next_u64(rng: rng mut*) -> u64:
|
||||
func rotate_left(x: u64, k: u32) -> u64:
|
||||
return (x << k) | (x >> (64u - k))
|
||||
// No struct-field-by-pointer (rng->state) access yet
|
||||
let result = rotate_left((*rng).state[0] * 5ul, 7u) * 9ul
|
||||
(*rng).state[1] ^= (*rng).state[0]
|
||||
(*rng).state[0] = rotate_left((*rng).state[0], 24u) ^ (*rng).state[1] ^ ((*rng).state[1] << 16u)
|
||||
(*rng).state[1] = rotate_left((*rng).state[1], 37u)
|
||||
return result
|
||||
|
||||
// Uniform in [0..1)
|
||||
func next_f32(rng: rng mut*) -> f32:
|
||||
return (next_u64(rng) as f32) / 18446744073709551615.0
|
||||
|
||||
func next_normal(rng: rng mut*) -> f32:
|
||||
let u = next_f32(rng)
|
||||
let v = next_f32(rng)
|
||||
return sqrt(- 2.0 * log(u)) * cos(2.0 * pi * v)
|
||||
|
||||
// Uniform on unit sphere
|
||||
func next_vec3(rng: rng mut*) -> vec3:
|
||||
let theta = 2.0 * pi * next_f32(rng)
|
||||
let cosphi = 2.0 * next_f32(rng) - 1.0
|
||||
let sinphi = sqrt(max(0.0, 1.0 - cosphi * cosphi))
|
||||
return vec3(cos(theta) * sinphi, sin(theta) * sinphi, cosphi)
|
||||
|
||||
func next_vec3_normal(rng: rng mut*) -> vec3:
|
||||
return vec3(next_normal(rng), next_normal(rng), next_normal(rng))
|
||||
|
||||
// ===== Camera =====
|
||||
|
||||
// Default (non-rotated) camera uses standard OpenGL conventions:
|
||||
// X right
|
||||
// Y up
|
||||
// -Z forward
|
||||
struct camera:
|
||||
position: vec3
|
||||
rotation: quaternion
|
||||
fovx: f32
|
||||
fovy: f32
|
||||
|
||||
func compute_fovx(fovy: f32, aspect_ratio: f32) -> f32:
|
||||
return 2.0 * atan(tan(fovy * 0.5) * aspect_ratio)
|
||||
|
||||
// x, y are in [-1..1]
|
||||
func camera_direction(camera: camera, x: f32, y: f32) -> vec3:
|
||||
let dir = vec3(x * tan(camera.fovx * 0.5), y * tan(camera.fovy * 0.5), -1.0)
|
||||
return rotate(camera.rotation, normalized(dir))
|
||||
|
||||
func camera_ray(camera: camera, x: f32, y: f32) -> ray:
|
||||
return ray(camera.position, camera_direction(camera, x, y))
|
||||
|
||||
// ===== Scene description =====
|
||||
|
||||
const diffuse_tag = 1u
|
||||
const metallic_tag = 2u
|
||||
const glass_tag = 3u
|
||||
|
||||
struct material:
|
||||
// albedo for diffuse, reflection color for metallic
|
||||
color: vec3
|
||||
emission: vec3
|
||||
type: u32
|
||||
roughness: f32
|
||||
ior: f32
|
||||
|
||||
struct plane:
|
||||
// Plane normal is defined as vec3(0,0,1) rotated by object rotation
|
||||
// Plane origin is defined as object position
|
||||
|
||||
struct sphere:
|
||||
radius: f32
|
||||
|
||||
struct box:
|
||||
// NB: box size is 2 * extent
|
||||
// I.e. box boundary is center +/- extent
|
||||
extent: vec3
|
||||
|
||||
const plane_tag = 1u
|
||||
const sphere_tag = 2u
|
||||
const box_tag = 3u
|
||||
|
||||
// Poor man's union
|
||||
struct shape:
|
||||
tag: u32
|
||||
data: f32[3]
|
||||
|
||||
struct object:
|
||||
position: vec3
|
||||
rotation: quaternion
|
||||
shape: shape
|
||||
material: material
|
||||
|
||||
func set_plane(object: object mut*, shape: plane):
|
||||
(*object).shape.tag = plane_tag
|
||||
|
||||
func set_sphere(object: object mut*, shape: sphere):
|
||||
(*object).shape.tag = sphere_tag
|
||||
*((*object).shape.data as f32 mut* as sphere mut*) = shape
|
||||
|
||||
func set_box(object: object mut*, shape: box):
|
||||
(*object).shape.tag = box_tag
|
||||
*((*object).shape.data as f32 mut* as box mut*) = shape
|
||||
|
||||
struct object_array:
|
||||
size: u64
|
||||
data: object mut*
|
||||
|
||||
struct scene:
|
||||
background: vec3
|
||||
objects: object_array
|
||||
|
||||
// ===== Color utilities =====
|
||||
|
||||
func gamma_correct(v: vec3) -> vec3:
|
||||
const gamma = 1.0 / 2.2
|
||||
return vec3(pow(v.x, gamma), pow(v.y, gamma), pow(v.z, gamma))
|
||||
|
||||
func aces(x: vec3) -> vec3:
|
||||
let a = 2.51
|
||||
let b = 0.03
|
||||
let c = 2.43
|
||||
let d = 0.59
|
||||
let e = 0.14
|
||||
// This is why operator overloading is a must
|
||||
let nom = multv(x, add(mults(x, a), vec3(b, b, b)))
|
||||
let den = add(multv(x, add(mults(x, c), vec3(d, d, d))), vec3(e, e, e))
|
||||
return divv(nom, den)
|
||||
|
||||
func to_bytes(v: vec3) -> u8[3]:
|
||||
func to_byte(x: f32) -> u8:
|
||||
return clamp(x) * 255.0 as u8
|
||||
return [to_byte(v.x), to_byte(v.y), to_byte(v.z)]
|
||||
|
||||
// ===== Raytracing core =====
|
||||
|
||||
struct intersection:
|
||||
intersected: bool
|
||||
distance: f32
|
||||
normal: vec3
|
||||
material: material*
|
||||
|
||||
const max_distance = 1000000.0
|
||||
const no_intersection = intersection(false, max_distance, vec3(0.0, 0.0, 1.0), 0ul as material*)
|
||||
|
||||
func intersect_plane(ray: ray, object: object*) -> intersection:
|
||||
let normal = rotate((*object).rotation, vec3(0.0, 0.0, 1.0))
|
||||
// dot(o + t * d - p, n) = 0
|
||||
// dot(o - p, n) + t * dot(d, n) = 0
|
||||
// t = - dot(o - p, n) / dot(d, n)
|
||||
let t = - dot(sub(ray.origin, (*object).position), normal) / dot(ray.direction, normal)
|
||||
return intersection(t > 0.0, t, normal, &(*object).material)
|
||||
|
||||
func intersect_sphere(ray: ray, object: object*) -> intersection:
|
||||
let shape = &(*object).shape.data as sphere*
|
||||
// |o + t * d - p|^2 = r^2
|
||||
// dot(o - p, o - p) + 2 * t * dot(o - p, d) + t^2 * dot(d, d) = r * r
|
||||
let delta = sub(ray.origin, (*object).position)
|
||||
// Solve quadratic
|
||||
let a = 1.0 // assume ray.direction is normalized
|
||||
let b = 2.0 * dot(delta, ray.direction)
|
||||
let c = dot(delta, delta) - (*shape).radius * (*shape).radius
|
||||
let D = b * b - 4.0 * a * c
|
||||
if D < 0.0:
|
||||
return no_intersection
|
||||
let t1 = (- b - sqrt(D)) / (2.0 * a)
|
||||
let t2 = (- b + sqrt(D)) / (2.0 * a)
|
||||
if t2 < 0.0:
|
||||
return no_intersection
|
||||
mut t = t1
|
||||
if t1 < 0.0:
|
||||
t = t2
|
||||
let normal = normalized(add(delta, mults(ray.direction, t)))
|
||||
return intersection(true, t, normal, &(*object).material)
|
||||
|
||||
func intersect_box(ray: ray, object: object*) -> intersection:
|
||||
func swap(x: f32 mut*, y: f32 mut*):
|
||||
let temp = *x
|
||||
*x = *y
|
||||
*y = temp
|
||||
|
||||
let shape = &(*object).shape.data as box*
|
||||
let inverse_rotation = inverse((*object).rotation)
|
||||
let local_delta = rotate(inverse_rotation, sub(ray.origin, (*object).position))
|
||||
let local_direction = rotate(inverse_rotation, ray.direction)
|
||||
|
||||
// (o + t * d).x = +/- e.x
|
||||
mut txmin = (- (*shape).extent.x - local_delta.x) / local_direction.x
|
||||
mut txmax = ( (*shape).extent.x - local_delta.x) / local_direction.x
|
||||
mut tymin = (- (*shape).extent.y - local_delta.y) / local_direction.y
|
||||
mut tymax = ( (*shape).extent.y - local_delta.y) / local_direction.y
|
||||
mut tzmin = (- (*shape).extent.z - local_delta.z) / local_direction.z
|
||||
mut tzmax = ( (*shape).extent.z - local_delta.z) / local_direction.z
|
||||
|
||||
if txmin > txmax:
|
||||
swap(&txmin, &txmax)
|
||||
if tymin > tymax:
|
||||
swap(&tymin, &tymax)
|
||||
if tzmin > tzmax:
|
||||
swap(&tzmin, &tzmax)
|
||||
|
||||
let tmin = max(txmin, max(tymin, tzmin))
|
||||
let tmax = min(txmax, min(tymax, tzmax))
|
||||
|
||||
if tmin > tmax || tmax < 0.0:
|
||||
return no_intersection
|
||||
|
||||
let inside = tmin < 0.0
|
||||
mut t = tmin
|
||||
mut tt = vec3(txmin, tymin, tzmin)
|
||||
// No ternary if yet...
|
||||
if inside:
|
||||
t = tmax
|
||||
tt = vec3(txmax, tymax, tzmax)
|
||||
|
||||
mut normal = vec3(0.0, 0.0, 0.0)
|
||||
|
||||
if t == tt.x:
|
||||
if local_direction.x > 0.0:
|
||||
normal = vec3(-1.0, 0.0, 0.0)
|
||||
else:
|
||||
normal = vec3( 1.0, 0.0, 0.0)
|
||||
else if t == tt.y:
|
||||
if local_direction.y > 0.0:
|
||||
normal = vec3(0.0, -1.0, 0.0)
|
||||
else:
|
||||
normal = vec3(0.0, 1.0, 0.0)
|
||||
else:
|
||||
if local_direction.z > 0.0:
|
||||
normal = vec3(0.0, 0.0, -1.0)
|
||||
else:
|
||||
normal = vec3(0.0, 0.0, 1.0)
|
||||
|
||||
if inside:
|
||||
normal = mults(normal, -1.0)
|
||||
|
||||
return intersection(true, tmin, rotate((*object).rotation, normal), &(*object).material)
|
||||
|
||||
func intersect_scene(scene: scene*, ray: ray) -> intersection:
|
||||
mut intersection = no_intersection
|
||||
mut i = 0ul
|
||||
while i < (*scene).objects.size:
|
||||
mut current_intersection = no_intersection
|
||||
|
||||
if (*scene).objects.data[i].shape.tag == plane_tag:
|
||||
current_intersection = intersect_plane(ray, &(*scene).objects.data[i])
|
||||
else if (*scene).objects.data[i].shape.tag == sphere_tag:
|
||||
current_intersection = intersect_sphere(ray, &(*scene).objects.data[i])
|
||||
else if (*scene).objects.data[i].shape.tag == box_tag:
|
||||
current_intersection = intersect_box(ray, &(*scene).objects.data[i])
|
||||
|
||||
if current_intersection.intersected && current_intersection.distance < intersection.distance:
|
||||
intersection = current_intersection
|
||||
|
||||
i += 1ul
|
||||
|
||||
return intersection
|
||||
|
||||
func raytrace(scene: scene*, camera_ray: ray, rng: rng mut*) -> vec3:
|
||||
mut current_ray = camera_ray
|
||||
mut result = vec3(0.0, 0.0, 0.0)
|
||||
mut factor = vec3(1.0, 1.0, 1.0)
|
||||
let termination_probability = 0.25
|
||||
|
||||
mut running = true
|
||||
while running:
|
||||
let intersection = intersect_scene(scene, current_ray)
|
||||
|
||||
if intersection.intersected:
|
||||
// Uncomment to debug normals
|
||||
// return mults(add(intersection.normal, vec3(1.0, 1.0, 1.0)), 0.5)
|
||||
|
||||
let cosine = - dot(intersection.normal, current_ray.direction)
|
||||
let inside = cosine < 0.0
|
||||
|
||||
result = add(result, multv(factor, (*intersection.material).emission))
|
||||
|
||||
// Russian roulette ray termination
|
||||
if next_f32(rng) < termination_probability:
|
||||
// No break operator yet...
|
||||
running = false
|
||||
else:
|
||||
mut new_direction = vec3(0.0, 0.0, 0.0)
|
||||
|
||||
if (*intersection.material).type == diffuse_tag:
|
||||
// NB: albedo is assumed to be premultiplied by pi to be in [0..1] range
|
||||
// This should also contain multiplication by cos(new_dir, normal), division by direction pdf (cos / pi)
|
||||
// and division by pi (because of albedo normalization), but these all cancel out
|
||||
factor = multv(factor, (*intersection.material).color)
|
||||
|
||||
// Cosine-weighted hemisphere direction
|
||||
new_direction = normalized(add(next_vec3(rng), intersection.normal))
|
||||
|
||||
else if (*intersection.material).type == metallic_tag:
|
||||
// This should also contain multiplication by brdf and division by direction pdf,
|
||||
// but we'll just pretend that the random reflected ray pdf coincides with brdf and thus cancels out
|
||||
factor = multv(factor, (*intersection.material).color)
|
||||
|
||||
// Compute perfect-mirror reflected direction
|
||||
new_direction = add(current_ray.direction, mults(intersection.normal, 2.0 * cosine))
|
||||
|
||||
// Alter the direction based on roughness
|
||||
new_direction = normalized(add(new_direction, mults(next_vec3_normal(rng), cosine * (*intersection.material).roughness)))
|
||||
|
||||
else if (*intersection.material).type == glass_tag:
|
||||
// This should also contain multiplication by brdf and division by direction pdf,
|
||||
// but we'll just pretend that the random refracted ray pdf coincides with brdf and thus cancels out
|
||||
factor = multv(factor, (*intersection.material).color)
|
||||
|
||||
mut ior = (*intersection.material).ior
|
||||
if inside:
|
||||
ior = 1.0 / ior
|
||||
|
||||
// Compute perfect refracted ray
|
||||
let k = 1.0 - ior * ior * (1.0 - cosine * cosine)
|
||||
if k >= 0.0:
|
||||
new_direction = add(mults(current_ray.direction, ior), mults(intersection.normal, ior * abs(cosine) - sqrt(k)))
|
||||
else:
|
||||
// Total internal reflection
|
||||
new_direction = add(current_ray.direction, mults(intersection.normal, 2.0 * cosine))
|
||||
|
||||
// Alter the direction based on roughness
|
||||
new_direction = normalized(add(new_direction, mults(next_vec3_normal(rng), (*intersection.material).roughness)))
|
||||
|
||||
// Compute the new ray, and offset its origin a bit along intersection normal
|
||||
let position = add(current_ray.origin, mults(current_ray.direction, intersection.distance))
|
||||
mut position_shift = 0.001
|
||||
if dot(new_direction, intersection.normal) < 0.0:
|
||||
position_shift = -position_shift
|
||||
current_ray = ray(add(position, mults(intersection.normal, position_shift)), new_direction)
|
||||
|
||||
// Account for Russian roulette
|
||||
factor = mults(factor, 1.0 / (1.0 - termination_probability))
|
||||
else:
|
||||
result = add(result, multv(factor, (*scene).background))
|
||||
running = false
|
||||
|
||||
return result
|
||||
|
||||
// ===== Main =====
|
||||
|
||||
func make_default_scene() -> scene:
|
||||
let object_count = 8ul
|
||||
// No sizeof yet...
|
||||
let objects = allocate(object_count * 20ul * 4ul) as object mut*
|
||||
|
||||
// Filling all objects in the same function overflows the max stack size
|
||||
// dictated by arm64 limitations (add immediate is bound by 12 bits => 4Kb)
|
||||
// The reasonable solution: fix the compiler!
|
||||
// The temporary solution: split into several separate functions!
|
||||
|
||||
func fill_walls(objects: object mut*):
|
||||
// Floor
|
||||
objects[0].position = vec3(0.0, -5.0, 0.0)
|
||||
objects[0].rotation = rotation(vec3(1.0, 0.0, 0.0), - pi * 0.5)
|
||||
objects[0].material.color = vec3(0.6, 0.6, 0.6)
|
||||
objects[0].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[0].material.type = diffuse_tag
|
||||
set_plane(&objects[0], plane())
|
||||
|
||||
// Ceiling
|
||||
objects[1].position = vec3(0.0, 5.0, 0.0)
|
||||
objects[1].rotation = rotation(vec3(1.0, 0.0, 0.0), pi * 0.5)
|
||||
objects[1].material.color = vec3(0.6, 0.6, 0.6)
|
||||
objects[1].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[1].material.type = diffuse_tag
|
||||
set_plane(&objects[1], plane())
|
||||
|
||||
// Left wall
|
||||
objects[2].position = vec3(-5.0, 0.0, 0.0)
|
||||
objects[2].rotation = rotation(vec3(0.0, 1.0, 0.0), pi * 0.5)
|
||||
objects[2].material.color = vec3(1.0, 0.2, 0.2)
|
||||
objects[2].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[2].material.type = diffuse_tag
|
||||
set_plane(&objects[2], plane())
|
||||
|
||||
// Right wall
|
||||
objects[3].position = vec3(5.0, 0.0, 0.0)
|
||||
objects[3].rotation = rotation(vec3(0.0, 1.0, 0.0), - pi * 0.5)
|
||||
objects[3].material.color = vec3(0.2, 0.2, 1.0)
|
||||
objects[3].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[3].material.type = diffuse_tag
|
||||
set_plane(&objects[3], plane())
|
||||
|
||||
// Back wall
|
||||
objects[4].position = vec3(0.0, 0.0, -5.0)
|
||||
objects[4].rotation = identity
|
||||
objects[4].material.color = vec3(0.6, 0.6, 0.6)
|
||||
objects[4].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[4].material.type = diffuse_tag
|
||||
set_plane(&objects[4], plane())
|
||||
|
||||
func fill_objects(objects: object mut*):
|
||||
// Top lamp
|
||||
objects[5].position = vec3(0.0, 10.0, 0.0)
|
||||
objects[5].rotation = identity
|
||||
objects[5].material.color = vec3(0.0, 0.0, 0.0)
|
||||
objects[5].material.emission = vec3(20.0, 16.0, 12.0)
|
||||
objects[5].material.type = diffuse_tag
|
||||
set_sphere(&objects[5], sphere(5.25))
|
||||
|
||||
// Box
|
||||
objects[6].position = vec3(-2.25, -2.0, -1.0)
|
||||
objects[6].rotation = rotation(vec3(0.0, 1.0, 0.0), pi / 6.0)
|
||||
objects[6].material.color = vec3(1.0, 1.0, 1.0)
|
||||
objects[6].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[6].material.type = glass_tag
|
||||
objects[6].material.roughness = 0.0
|
||||
objects[6].material.ior = 1.333
|
||||
set_box(&objects[6], box(vec3(1.5, 3.0, 1.5)))
|
||||
|
||||
// Sphere
|
||||
objects[7].position = vec3(2.5, -3.0, -1.0)
|
||||
objects[7].rotation = rotation(vec3(0.0, 1.0, 0.0), - pi / 6.0)
|
||||
objects[7].material.color = vec3(1.0, 1.0, 1.0)
|
||||
objects[7].material.emission = vec3(0.0, 0.0, 0.0)
|
||||
objects[7].material.type = metallic_tag
|
||||
objects[7].material.roughness = 0.5
|
||||
set_sphere(&objects[7], sphere(2.0))
|
||||
|
||||
fill_walls(objects)
|
||||
fill_objects(objects)
|
||||
|
||||
let background = vec3(0.0, 0.0, 0.0)
|
||||
return scene(background, object_array(object_count, objects))
|
||||
|
||||
const default_fovy = 2.0 * atan(0.5)
|
||||
|
||||
func main():
|
||||
let scene = make_default_scene()
|
||||
|
||||
let image = create_image(512ul, 512ul)
|
||||
// let image = create_image(128ul, 128ul)
|
||||
|
||||
let aspect_ratio = (image.width as f32) / (image.height as f32)
|
||||
let camera = camera(vec3(0.0, 0.0, 15.0), identity, default_fovy, compute_fovx(default_fovy, aspect_ratio))
|
||||
|
||||
// let samples_per_pixel = 4096ul
|
||||
let samples_per_pixel = 2048ul
|
||||
// let samples_per_pixel = 1024ul
|
||||
// let samples_per_pixel = 512ul
|
||||
// let samples_per_pixel = 256ul
|
||||
// let samples_per_pixel = 16ul
|
||||
// let samples_per_pixel = 1ul
|
||||
|
||||
let start_time = get_time()
|
||||
mut last_report_time = start_time
|
||||
mut last_report_progress = 0.0
|
||||
mut average_speed = 0.0
|
||||
|
||||
func clear_line(spaces: u32):
|
||||
mut i = 0u
|
||||
print_byte('\r')
|
||||
while i < spaces:
|
||||
print_byte(' ')
|
||||
i += 1u
|
||||
print_byte('\r')
|
||||
|
||||
mut y = 0ul
|
||||
while y < image.height:
|
||||
mut x = 0ul
|
||||
while x < image.width:
|
||||
mut rng = rng([splitmix64(x | (y << 32u)), 10723151780598845931ul])
|
||||
mut sample = 0ul
|
||||
mut color = vec3(0.0, 0.0, 0.0)
|
||||
while sample < samples_per_pixel:
|
||||
let tx = - 1.0 + 2.0 * (x as f32 + next_f32(&mut rng)) / (image.width as f32)
|
||||
let ty = 1.0 - 2.0 * (y as f32 + next_f32(&mut rng)) / (image.height as f32)
|
||||
let ray = camera_ray(camera, tx, ty)
|
||||
color = add(color, raytrace(&scene, ray, &mut rng))
|
||||
sample += 1ul
|
||||
color = mults(color, 1.0 / (samples_per_pixel as f32))
|
||||
image.data[y * image.width + x] = to_bytes(gamma_correct(aces(color)))
|
||||
|
||||
x += 1ul
|
||||
|
||||
let time = get_time()
|
||||
let delta = time_delta(time, last_report_time)
|
||||
if delta > 0.125 || (y == 0ul && x == 1ul) || (y + 1ul == image.height && x == image.width):
|
||||
let done = y * image.width + x
|
||||
let total = image.width * image.height
|
||||
let progress = (done as f32) / (total as f32)
|
||||
let time_passed = time_delta(time, start_time)
|
||||
|
||||
// Running exponential-weighted average speed for
|
||||
// accurate remaining time estimate
|
||||
let speed_estimate = (progress - last_report_progress) / delta
|
||||
if average_speed == 0.0:
|
||||
average_speed = speed_estimate
|
||||
else:
|
||||
average_speed = average_speed + (speed_estimate - average_speed) * 0.125
|
||||
let time_remaining = (1.0 - progress) / average_speed
|
||||
|
||||
let str1 = ['%', ' ', 'd', 'o', 'n', 'e', ',', ' ', '\0']
|
||||
let str2 = [' ', 'l', 'e', 'f', 't', ' ', '\0']
|
||||
|
||||
clear_line(30u)
|
||||
print_f32(100.0 * progress)
|
||||
print_str(str1 as u8*)
|
||||
print_time(time_remaining)
|
||||
print_str(str2 as u8*)
|
||||
flush()
|
||||
last_report_time = time
|
||||
last_report_progress = progress
|
||||
|
||||
y += 1ul
|
||||
|
||||
let total_time = time_delta(get_time(), start_time)
|
||||
let total_samples = image.width * image.height * samples_per_pixel
|
||||
|
||||
let str1 = ['R', 'e', 'n', 'd', 'e', 'r', ' ', 't', 'o', 'o', 'k', ' ', '\0']
|
||||
let str2 = [' ', 'p', 'e', 'r', ' ', 's', 'a', 'm', 'p', 'l', 'e', '\0']
|
||||
clear_line(30u)
|
||||
print_str(str1 as u8*)
|
||||
print_time(total_time)
|
||||
print_byte('\n')
|
||||
print_time(total_time / (total_samples as f32))
|
||||
print_str(str2 as u8*)
|
||||
print_byte('\n')
|
||||
|
||||
// No string literal support yet...
|
||||
let path = ['o', 'u', 't', '.', 'p', 'p', 'm', '\0']
|
||||
write_ppm(path as u8*, image)
|
||||
deallocate(image.data as unit*)
|
||||
|
||||
deallocate(scene.objects.data as unit*)
|
||||
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue