v / examples / path_tracing.v
568 lines · 505 sloc · 13.3 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1/**********************************************************************
2* path tracing demo
3*
4* Copyright (c) 2019-2024 Dario Deledda. All rights reserved.
5* Use of this source code is governed by an MIT license
6* that can be found in the LICENSE file.
7*
8* This file contains a path tracer example in less of 500 line of codes
9* 3 demo scenes included
10*
11* This code is inspired by:
12* - "Realistic Ray Tracing" by Peter Shirley 2000 ISBN-13: 978-1568814612
13* - https://www.kevinbeason.com/smallpt/
14*
15* Known limitations:
16* - there are some approximation errors in the calculations
17* - to speed-up the code a cos/sin table is used
18* - the full precision code is present but commented, can be restored very easily
19* - an higher number of samples ( > 60) can block the program on higher resolutions
20* without a stack size increase
21* - as a recursive program this code depend on the stack size,
22* for higher number of samples increase the stack size
23* in linux: ulimit -s byte_size_of_the_stack
24* example: ulimit -s 16000000
25* - No OpenMP support
26**********************************************************************/
27import os
28import math
29import rand
30import time
31import term
32import math.vec
33
34const inf = 1e+10
35const eps = 1e-4
36
37type Vec = vec.Vec3[f64]
38
39@[inline]
40fn (v Vec) norm() Vec {
41 tmp_norm := 1.0 / math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
42 return Vec{v.x * tmp_norm, v.y * tmp_norm, v.z * tmp_norm}
43}
44
45//********************************Image**************************************
46struct Image {
47 width int
48 height int
49 data &Vec = unsafe { nil }
50}
51
52fn new_image(w int, h int) Image {
53 vecsize := int(sizeof(Vec))
54 return Image{
55 width: w
56 height: h
57 data: unsafe { &Vec(vcalloc(vecsize * w * h)) }
58 }
59}
60
61// write out a .ppm file
62fn (image Image) save_as_ppm(file_name string) {
63 npixels := image.width * image.height
64 mut f_out := os.create(file_name) or { panic(err) }
65 f_out.writeln('P3') or { panic(err) }
66 f_out.writeln('${image.width} ${image.height}') or { panic(err) }
67 f_out.writeln('255') or { panic(err) }
68 for i in 0 .. npixels {
69 c_r := to_int(unsafe { image.data[i] }.x)
70 c_g := to_int(unsafe { image.data[i] }.y)
71 c_b := to_int(unsafe { image.data[i] }.z)
72 f_out.write_string('${c_r} ${c_g} ${c_b} ') or { panic(err) }
73 }
74 f_out.close()
75}
76
77//********************************** Ray ************************************
78struct Ray {
79 o Vec
80 d Vec
81}
82
83// material types, used in radiance()
84enum Refl_t {
85 diff
86 spec
87 refr
88}
89
90//******************************** Sphere ***********************************
91struct Sphere {
92 rad f64 = 0.0 // radius
93 p Vec // position
94 e Vec // emission
95 c Vec // color
96 refl Refl_t // reflection type => [diffuse, specular, refractive]
97}
98
99fn (sp Sphere) intersect(r Ray) f64 {
100 op := sp.p - r.o // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0
101 b := op.dot(r.d)
102 mut det := b * b - op.dot(op) + sp.rad * sp.rad
103
104 if det < 0 {
105 return 0
106 }
107
108 det = math.sqrt(det)
109
110 mut t := b - det
111 if t > eps {
112 return t
113 }
114
115 t = b + det
116 if t > eps {
117 return t
118 }
119 return 0
120}
121
122/*********************************** Scenes **********************************
123* 0) Cornell Box with 2 spheres
124* 1) Sunset
125* 2) Psychedelic
126* The sphere fields are: Sphere{radius, position, emission, color, material}
127******************************************************************************/
128const cen = Vec{50, 40.8, -860} // used by scene 1
129
130const spheres = [
131 [// scene 0 cornnel box
132 Sphere{
133 rad: 1e+5
134 p: Vec{1e+5 + 1, 40.8, 81.6}
135 e: Vec{}
136 c: Vec{.75, .25, .25}
137 refl: .diff
138 }, // Left
139 Sphere{
140 rad: 1e+5
141 p: Vec{-1e+5 + 99, 40.8, 81.6}
142 e: Vec{}
143 c: Vec{.25, .25, .75}
144 refl: .diff
145 }, // Rght
146 Sphere{
147 rad: 1e+5
148 p: Vec{50, 40.8, 1e+5}
149 e: Vec{}
150 c: Vec{.75, .75, .75}
151 refl: .diff
152 }, // Back
153 Sphere{
154 rad: 1e+5
155 p: Vec{50, 40.8, -1e+5 + 170}
156 e: Vec{}
157 c: Vec{}
158 refl: .diff
159 }, // Frnt
160 Sphere{
161 rad: 1e+5
162 p: Vec{50, 1e+5, 81.6}
163 e: Vec{}
164 c: Vec{.75, .75, .75}
165 refl: .diff
166 }, // Botm
167 Sphere{
168 rad: 1e+5
169 p: Vec{50, -1e+5 + 81.6, 81.6}
170 e: Vec{}
171 c: Vec{.75, .75, .75}
172 refl: .diff
173 }, // Top
174 Sphere{
175 rad: 16.5
176 p: Vec{27, 16.5, 47}
177 e: Vec{}
178 c: Vec{1, 1, 1}.mul_scalar(.999)
179 refl: .spec
180 }, // Mirr
181 Sphere{
182 rad: 16.5
183 p: Vec{73, 16.5, 78}
184 e: Vec{}
185 c: Vec{1, 1, 1}.mul_scalar(.999)
186 refl: .refr
187 }, // Glas
188 Sphere{
189 rad: 600
190 p: Vec{50, 681.6 - .27, 81.6}
191 e: Vec{12, 12, 12}
192 c: Vec{}
193 refl: .diff
194 }, // Lite
195 ],
196 [// scene 1 sunset
197 Sphere{
198 rad: 1600
199 p: Vec{1.0, 0.0, 2.0}.mul_scalar(3000)
200 e: Vec{1.0, .9, .8}.mul_scalar(1.2e+1 * 1.56 * 2)
201 c: Vec{}
202 refl: .diff
203 }, // sun
204 Sphere{
205 rad: 1560
206 p: Vec{1, 0, 2}.mul_scalar(3500)
207 e: Vec{1.0, .5, .05}.mul_scalar(4.8e+1 * 1.56 * 2)
208 c: Vec{}
209 refl: .diff
210 }, // horizon sun2
211 Sphere{
212 rad: 10000
213 p: cen + Vec{0, 0, -200}
214 e: Vec{0.00063842, 0.02001478, 0.28923243}.mul_scalar(6e-2 * 8)
215 c: Vec{.7, .7, 1}.mul_scalar(.25)
216 refl: .diff
217 }, // sky
218 Sphere{
219 rad: 100000
220 p: Vec{50, -100000, 0}
221 e: Vec{}
222 c: Vec{.3, .3, .3}
223 refl: .diff
224 }, // grnd
225 Sphere{
226 rad: 110000
227 p: Vec{50, -110048.5, 0}
228 e: Vec{.9, .5, .05}.mul_scalar(4)
229 c: Vec{}
230 refl: .diff
231 }, // horizon brightener
232 Sphere{
233 rad: 4e+4
234 p: Vec{50, -4e+4 - 30, -3000}
235 e: Vec{}
236 c: Vec{.2, .2, .2}
237 refl: .diff
238 }, // mountains
239 Sphere{
240 rad: 26.5
241 p: Vec{22, 26.5, 42}
242 e: Vec{}
243 c: Vec{1, 1, 1}.mul_scalar(.596)
244 refl: .spec
245 }, // white Mirr
246 Sphere{
247 rad: 13
248 p: Vec{75, 13, 82}
249 e: Vec{}
250 c: Vec{.96, .96, .96}.mul_scalar(.96)
251 refl: .refr
252 }, // Glas
253 Sphere{
254 rad: 22
255 p: Vec{87, 22, 24}
256 e: Vec{}
257 c: Vec{.6, .6, .6}.mul_scalar(.696)
258 refl: .refr
259 }, // Glas2
260 ],
261 [// scene 3 Psychedelic
262 Sphere{
263 rad: 150
264 p: Vec{50 + 75, 28, 62}
265 e: Vec{1, 1, 1}.mul_scalar(0e-3)
266 c: Vec{1, .9, .8}.mul_scalar(.93)
267 refl: .refr
268 },
269 Sphere{
270 rad: 28
271 p: Vec{50 + 5, -28, 62}
272 e: Vec{1, 1, 1}.mul_scalar(1e+1)
273 c: Vec{1, 1, 1}.mul_scalar(0)
274 refl: .diff
275 },
276 Sphere{
277 rad: 300
278 p: Vec{50, 28, 62}
279 e: Vec{1, 1, 1}.mul_scalar(0e-3)
280 c: Vec{1, 1, 1}.mul_scalar(.93)
281 refl: .spec
282 },
283 ],
284]
285
286//********************************** Utilities ******************************
287@[inline]
288fn clamp(x f64) f64 {
289 if x < 0 {
290 return 0
291 }
292 if x > 1 {
293 return 1
294 }
295 return x
296}
297
298@[inline]
299fn to_int(x f64) int {
300 p := math.pow(clamp(x), 1.0 / 2.2)
301 return int(p * 255.0 + 0.5)
302}
303
304fn intersect(r Ray, spheres &Sphere, nspheres int) (bool, f64, int) {
305 mut d := 0.0
306 mut t := inf
307 mut id := 0
308 for i := nspheres - 1; i >= 0; i-- {
309 d = unsafe { spheres[i] }.intersect(r)
310 if d > 0 && d < t {
311 t = d
312 id = i
313 }
314 }
315 return t < inf, t, id
316}
317
318// some casual random function, try to avoid the 0
319fn rand_f64() f64 {
320 x := rand.u32() & 0x3FFF_FFFF
321 return f64(x) / f64(0x3FFF_FFFF)
322}
323
324const cache_len = 65536 // the 2*pi angle will be split in 2^16 parts
325
326const cache_mask = cache_len - 1
327
328struct Cache {
329mut:
330 sin_tab [65536]f64
331 cos_tab [65536]f64
332}
333
334fn new_tabs() Cache {
335 mut c := Cache{}
336 inv_len := 1.0 / f64(cache_len)
337 for i in 0 .. cache_len {
338 x := f64(i) * math.pi * 2.0 * inv_len
339 c.sin_tab[i] = math.sin(x)
340 c.cos_tab[i] = math.cos(x)
341 }
342 return c
343}
344
345//************ Cache for sin/cos speed-up table and scene selector **********
346const tabs = new_tabs()
347
348//****************** main function for the radiance calculation *************
349fn radiance(r Ray, depthi int, scene_id int) Vec {
350 if depthi > 1024 {
351 eprintln('depthi: ${depthi}')
352 eprintln('')
353 return Vec{}
354 }
355 mut depth := depthi // actual depth in the reflection tree
356 mut t := 0.0 // distance to intersection
357 mut id := 0 // id of intersected object
358 mut res := false // result of intersect
359
360 v_1 := 1.0
361 // v_2 := f64(2.0)
362
363 scene := spheres[scene_id]
364 // res, t, id = intersect(r, id, tb.scene)
365 res, t, id = intersect(r, scene.data, scene.len)
366 if !res {
367 return Vec{}
368 }
369 // if miss, return black
370
371 obj := scene[id] // the hit object
372
373 x := r.o + r.d.mul_scalar(t)
374 n := Vec(x - obj.p).norm()
375
376 nl := if n.dot(r.d) < 0.0 { n } else { n.mul_scalar(-1) }
377
378 mut f := obj.c
379
380 // max reflection
381 mut p := f.z
382 if f.x > f.y && f.x > f.z {
383 p = f.x
384 } else {
385 if f.y > f.z {
386 p = f.y
387 }
388 }
389
390 depth++
391 if depth > 5 {
392 if rand_f64() < p {
393 f = f.mul_scalar(f64(1.0) / p)
394 } else {
395 return obj.e // R.R.
396 }
397 }
398
399 if obj.refl == .diff { // Ideal DIFFUSE reflection
400 // **Full Precision**
401 // r1 := f64(2.0 * math.pi) * rand_f64()
402
403 // tabbed speed-up
404 r1 := rand.u32() & cache_mask
405
406 r2 := rand_f64()
407 r2s := math.sqrt(r2)
408
409 w := nl
410
411 mut u := if math.abs(w.x) > f64(0.1) { Vec{0, 1, 0} } else { Vec{1, 0, 0} }
412 u = Vec(u.cross(w)).norm()
413
414 v := w.cross(u)
415
416 // **Full Precision**
417 // d := (u.mul_scalar(math.cos(r1) * r2s) + v.mul_scalar(math.sin(r1) * r2s) + w.mul_scalar(1.0 - r2)).norm()
418
419 // tabbed speed-up
420 d := Vec(u.mul_scalar(tabs.cos_tab[r1] * r2s) + v.mul_scalar(tabs.sin_tab[r1] * r2s) +
421 (w.mul_scalar(math.sqrt(f64(1.0) - r2)))).norm()
422
423 return obj.e + f * radiance(Ray{x, d}, depth, scene_id)
424 } else {
425 if obj.refl == .spec { // Ideal SPECULAR reflection
426 return obj.e + f * radiance(Ray{x, r.d -
427 n.mul_scalar(2.0 * n.dot(r.d))}, depth, scene_id)
428 }
429 }
430
431 refl_ray := Ray{x, r.d - n.mul_scalar(2.0 * n.dot(r.d))} // Ideal dielectric REFRACTION
432 into := n.dot(nl) > 0 // Ray from outside going in?
433
434 nc := f64(1.0)
435 nt := f64(1.5)
436
437 nnt := if into { nc / nt } else { nt / nc }
438
439 ddn := r.d.dot(nl)
440 cos2t := v_1 - nnt * nnt * (v_1 - ddn * ddn)
441 if cos2t < 0.0 { // Total internal reflection
442 return obj.e + f * radiance(refl_ray, depth, scene_id)
443 }
444
445 dirc := if into { f64(1) } else { f64(-1) }
446 tdir := Vec(r.d.mul_scalar(nnt) - n.mul_scalar(dirc * (ddn * nnt + math.sqrt(cos2t)))).norm()
447
448 a := nt - nc
449 b := nt + nc
450 r0 := a * a / (b * b)
451 c := if into { v_1 + ddn } else { v_1 - tdir.dot(n) }
452
453 re := r0 + (v_1 - r0) * c * c * c * c * c
454 tr := v_1 - re
455 pp := f64(.25) + f64(.5) * re
456 rp := re / pp
457 tp := tr / (v_1 - pp)
458
459 mut tmp := Vec{}
460 if depth > 2 {
461 // Russian roulette
462 tmp = if rand_f64() < pp {
463 radiance(refl_ray, depth, scene_id).mul_scalar(rp)
464 } else {
465 radiance(Ray{x, tdir}, depth, scene_id).mul_scalar(tp)
466 }
467 } else {
468 tmp = (radiance(refl_ray, depth, scene_id).mul_scalar(re)) +
469 (radiance(Ray{x, tdir}, depth, scene_id).mul_scalar(tr))
470 }
471 return obj.e + (f * tmp)
472}
473
474//*********************** beam scan routine *********************************
475fn ray_trace(w int, h int, samps int, scene_id int) Image {
476 image := new_image(w, h)
477
478 // inverse costants
479 w1 := f64(1.0 / f64(w))
480 h1 := f64(1.0 / f64(h))
481 samps1 := f64(1.0 / f64(samps))
482
483 cam := Ray{Vec{50, 52, 295.6}, Vec{0, -0.042612, -1}.norm()} // cam position, direction
484 cx := Vec{f64(w) * 0.5135 / f64(h), 0, 0}
485 cy := Vec(cx.cross(cam.d)).norm().mul_scalar(0.5135)
486 mut r := Vec{}
487
488 // speed-up constants
489 v_1 := f64(1.0)
490 v_2 := f64(2.0)
491
492 // OpenMP injection point! #pragma omp parallel for schedule(dynamic, 1) shared(c)
493 for y := 0; y < h; y++ {
494 if y & 7 == 0 || y + 1 == h {
495 term.cursor_up(1)
496 eprintln('Rendering (${samps * 4} spp) ${(100.0 * f64(y)) / (f64(h) - 1.0):5.2f}%')
497 }
498 for x in 0 .. w {
499 i := (h - y - 1) * w + x
500 mut ivec := unsafe { &image.data[i] }
501 // we use sx and sy to perform a square subsampling of 4 samples
502 for sy := 0; sy < 2; sy++ {
503 for sx := 0; sx < 2; sx++ {
504 r = Vec{0, 0, 0}
505 for _ in 0 .. samps {
506 r1 := v_2 * rand_f64()
507 dx := if r1 < v_1 { math.sqrt(r1) - v_1 } else { v_1 - math.sqrt(v_2 - r1) }
508
509 r2 := v_2 * rand_f64()
510 dy := if r2 < v_1 { math.sqrt(r2) - v_1 } else { v_1 - math.sqrt(v_2 - r2) }
511
512 d := cx.mul_scalar(((f64(sx) + 0.5 + dx) * 0.5 + f64(x)) * w1 - .5) +
513 cy.mul_scalar(((f64(sy) + 0.5 + dy) * 0.5 + f64(y)) * h1 - .5) + cam.d
514 r = r + radiance(Ray{cam.o +
515 d.mul_scalar(140.0), Vec(d).norm()}, 0, scene_id).mul_scalar(samps1)
516 }
517 tmp_vec := Vec{clamp(r.x), clamp(r.y), clamp(r.z)}.mul_scalar(.25)
518 unsafe {
519 (*ivec) = *ivec + tmp_vec
520 }
521 }
522 }
523 }
524 }
525 return image
526}
527
528fn main() {
529 if os.args.len > 6 {
530 eprintln('Usage:\n path_tracing [samples] [image.ppm] [scene_n] [width] [height]')
531 exit(1)
532 }
533 mut width := 320 // width of the rendering in pixels
534 mut height := 200 // height of the rendering in pixels
535 mut samples := 4 // number of samples per pixel, increase for better quality
536 mut scene_id := 0 // scene to render [0 cornell box,1 sunset,2 psyco]
537 mut file_name := 'image.ppm' // name of the output file in .ppm format
538
539 if os.args.len >= 2 {
540 samples = os.args[1].int() / 4
541 }
542 if os.args.len >= 3 {
543 file_name = os.args[2]
544 }
545 if os.args.len >= 4 {
546 scene_id = os.args[3].int()
547 }
548 if os.args.len >= 5 {
549 width = os.args[4].int()
550 }
551 if os.args.len == 6 {
552 height = os.args[5].int()
553 }
554 // change the seed for a different result
555 rand.seed([u32(2020), 0])
556
557 eprintln('Path tracing samples: ${samples}, file_name: ${file_name}, scene_id: ${scene_id}, width: ${width}, height: ${height}')
558 eprintln('')
559
560 t1 := time.ticks()
561 image := ray_trace(width, height, samples, scene_id)
562 t2 := time.ticks()
563 eprintln('Rendering finished. Took: ${(t2 - t1):5}ms')
564
565 image.save_as_ppm(file_name)
566 t3 := time.ticks()
567 eprintln('Image saved as [${file_name}]. Took: ${(t3 - t2):5}ms')
568}
569