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