| 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 | **********************************************************************/ |
| 27 | import os |
| 28 | import math |
| 29 | import rand |
| 30 | import time |
| 31 | import term |
| 32 | import math.vec |
| 33 | |
| 34 | const inf = 1e+10 |
| 35 | const eps = 1e-4 |
| 36 | |
| 37 | type Vec = vec.Vec3[f64] |
| 38 | |
| 39 | @[inline] |
| 40 | fn (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************************************** |
| 46 | struct Image { |
| 47 | width int |
| 48 | height int |
| 49 | data &Vec = unsafe { nil } |
| 50 | } |
| 51 | |
| 52 | fn 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 |
| 62 | fn (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 ************************************ |
| 78 | struct Ray { |
| 79 | o Vec |
| 80 | d Vec |
| 81 | } |
| 82 | |
| 83 | // material types, used in radiance() |
| 84 | enum Refl_t { |
| 85 | diff |
| 86 | spec |
| 87 | refr |
| 88 | } |
| 89 | |
| 90 | //******************************** Sphere *********************************** |
| 91 | struct 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 | |
| 99 | fn (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 | ******************************************************************************/ |
| 128 | const cen = Vec{50, 40.8, -860} // used by scene 1 |
| 129 | |
| 130 | const 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] |
| 288 | fn 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] |
| 299 | fn 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 | |
| 304 | fn 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 |
| 319 | fn rand_f64() f64 { |
| 320 | x := rand.u32() & 0x3FFF_FFFF |
| 321 | return f64(x) / f64(0x3FFF_FFFF) |
| 322 | } |
| 323 | |
| 324 | const cache_len = 65536 // the 2*pi angle will be split in 2^16 parts |
| 325 | |
| 326 | const cache_mask = cache_len - 1 |
| 327 | |
| 328 | struct Cache { |
| 329 | mut: |
| 330 | sin_tab [65536]f64 |
| 331 | cos_tab [65536]f64 |
| 332 | } |
| 333 | |
| 334 | fn 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 ********** |
| 346 | const tabs = new_tabs() |
| 347 | |
| 348 | //****************** main function for the radiance calculation ************* |
| 349 | fn 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 ********************************* |
| 475 | fn 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 | |
| 528 | fn 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 | |