forked from RayTracing/raytracing.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRayTracingTheRestOfYourLife.html
2134 lines (1693 loc) · 85.7 KB
/
RayTracingTheRestOfYourLife.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<meta charset="utf-8">
<!-- Markdeep: https://casual-effects.com/markdeep/ -->
**Ray Tracing: The Rest of Your Life**
Peter Shirley
Version 2.0.0, 2019-Oct-07
<br>Copyright 2018-2019. Peter Shirley. All rights reserved.
Overview
====================================================================================================
In _Ray Tracing in One Weekend_ and _Ray Tracing: the Next Week_, you built a “real” ray tracer.
In this volume, I assume you will be pursuing a career related to ray tracing and we will dive into
the math of creating a very serious ray tracer. When you are done you should be ready to start
messing with the many serious commercial ray tracers underlying the movie and product design
industries. There are many many things I do not cover in this short volume; I dive into only one of
many ways to write a Monte Carlo rendering program. I don’t do shadow rays (instead I make rays more
likely to go toward lights), bidirectional methods, Metropolis methods, or photon mapping. What I do
is speak in the language of the field that studies those methods. I think of this book as a deep
exposure that can be your first of many, and it will equip you with some of the concepts, math, and
terms you will need to study the others.
As before, https://in1weekend.blogspot.com/ will have further readings and references.
Acknowledgements
-----------------
Thanks to Dave Hart and Jean Buckley for help on the original manuscript.
Thanks to Paul Melis, Nakata Daisuke, Filipe Scur, Vahan Sosoyan, and Matthew Heimlich for finding
errors.
Thanks to Tatsuya Ogawa and Aaryaman Vasishta for code improvements.
Thanks to Steve Hollasch, Trevor David Black, Berna Kabadayı and Lorenzo Mancini for translating the
book to Markdeep and moving it to the web.
A Simple Monte Carlo Program
====================================================================================================
Let’s start with one of the simplest Monte Carlo (MC) programs. MC programs give a statistical
estimate of an answer, and this estimate gets more and more accurate the longer you run it. This
basic characteristic of simple programs producing noisy but ever-better answers is what MC is all
about, and it is especially good for applications like graphics where great accuracy is not needed.
<div class='together'>
As an example, let’s estimate $\pi$. There are many ways to do this, with the Buffon Needle
problem being a classic case study. We’ll do a variation inspired by that. Suppose you have a circle
inscribed inside a square:
![Figure 2-1](../images/fig-3-02-1.jpg)
</div>
<div class='together'>
Now, suppose you pick random points inside the square. The fraction of those random points that end
up inside the circle should be proportional to the area of the circle. The exact fraction should in
fact be the ratio of the circle area to the square area. Fraction:
$$ \frac{Pi \cdot R^2}{(2R)^2} = \frac{Pi}{4} $$
</div>
<div class='together'>
Since the *R* cancels out, we can pick whatever is computationally convenient. Let’s go with R=1
centered at the origin:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
#include <math.h>
#include <stdlib.h>
int main() {
int N = 1000;
int inside_circle = 0;
for (int i = 0; i < N; i++) {
float x = 2*random_double() - 1;
float y = 2*random_double() - 1;
if(x*x + y*y < 1)
inside_circle++;
}
std::cout << "Estimate of Pi = " << 4*float(inside_circle) / N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
This gives me the answer `Estimate of Pi = 3.196`
<div class='together'>
If we change the program to run forever and just print out a running estimate:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
#include <math.h>
#include <stdlib.h>
int main() {
int inside_circle = 0;
int runs = 0;
while (true) {
runs++;
float x = 2*random_double() - 1;
float y = 2*random_double() - 1;
if(x*x + y*y < 1)
inside_circle++;
if(runs% 100000 == 0)
std::cout << "Estimate of Pi = " << 4*float(inside_circle) / runs << "\n";
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
We get very quickly near $\pi$, and then more slowly zero in on it. This is an example of the *Law
of Diminishing Returns*, where each sample helps less than the last. This is the worst part of MC.
We can mitigate this diminishing return by *stratifying* the samples (often called *jittering*),
where instead of taking random samples, we take a grid and take one sample within each:
![Figure 2-2](../images/fig-3-02-2.jpg)
</div>
<div class='together'>
This changes the sample generation, but we need to know how many samples we are taking in advance
because we need to know the grid. Let’s take a hundred million and try it both ways:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
int main() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int sqrt_N = 10000;
for (int i = 0; i < sqrt_N; i++) {
for (int j = 0; j < sqrt_N; j++) {
float x = 2*random_double() - 1;
float y = 2*random_double() - 1;
if (x*x + y*y < 1)
inside_circle++;
x = 2*((i + random_double()) / sqrt_N) - 1;
y = 2*((j + random_double()) / sqrt_N) - 1;
if (x*x + y*y < 1)
inside_circle_stratified++;
}
}
std::cout << "Regular Estimate of Pi = " <<
4*float(inside_circle) / (sqrt_N*sqrt_N) << "\n";
std::cout << "Stratified Estimate of Pi = " <<
4*float(inside_circle_stratified) / (sqrt_N*sqrt_N) << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I get:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Regular Estimate of Pi = 3.1415
Stratified Estimate of Pi = 3.14159
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
Interestingly, the stratified method is not only better, it converges with a better asymptotic rate!
Unfortunately, this advantage decreases with the dimension of the problem (so for example, with the
3D sphere volume version the gap would be less). This is called the *Curse of Dimensionality*. We
are going to be very high dimensional (each reflection adds two dimensions), so I won't stratify in
this book. But if you are ever doing single-reflection or shadowing or some strictly 2D problem, you
definitely want to stratify.
One Dimensional MC Integration
====================================================================================================
Integration is all about computing areas and volumes, so we could have framed Chapter 2 in an
integral form if we wanted to make it maximally confusing. But sometimes integration is the most
natural and clean way to formulate things. Rendering is often such a problem. Let’s look at a
classic integral:
$$ I = \int_{0}^{2} x^2 dx $$
<div class='together'>
In computer sciency notation, we might write this as:
$$ I = area( x^2, 0, 2 ) $$
We could also write it as:
$$ I = 2 \cdot average(x^2, 0, 2) $$
</div>
<div class='together'>
This suggests a MC approach:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int N = 1000000;
float sum;
for (int i = 0; i < N; i++) {
float x = 2*random_double();
sum += x*x;
}
std::cout << "I =" << 2*sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
This, as expected, produces approximately the exact answer we get with algebra, $I = 8/3$. But we
could also do it for functions that we can’t analytically integrate like $log(sin(x))$. In graphics,
we often have functions we can evaluate but can’t write down explicitly, or functions we can only
probabilistically evaluate. That is in fact what the ray tracing `color()` function of the last two
books is -- we don’t know what color is seen in every direction, but we can statistically estimate
it in any given dimension.
One problem with the random program we wrote in the first two books is small light sources create
too much noise because our uniform sampling doesn’t sample the light often enough. We could lessen
that problem if we sent more random samples toward the light, but then we would need to downweight
those samples to adjust for the over-sampling. How we do that adjustment? To do that we will need
the concept of a _probability density function_.
<div class='together'>
First, what is a _density function_? It’s just a continuous form of a histogram. Here’s an example
from the histogram Wikipedia page:
![Figure 3-1](../images/fig-3-03-1.jpg)
</div>
<div class='together'>
If we added data for more trees, the histogram would get taller. If we divided the data into more
bins, it would get shorter. A discrete density function differs from a histogram in that it
normalizes the frequency y-axis to a fraction or percentage (just a fraction times 100). A
continuous histogram, where we take the number of bins to infinity, can’t be a fraction because the
height of all the bins would drop to zero. A density function is one where we take the bins and
adjust them so they don’t get shorter as we add more bins. For the case of the tree histogram above
we might try:
$$ \text{Bin-height} = \frac{(\text{Fraction of trees between height }H\text{ and }H’)}{(H-H’)} $$
</div>
<div class='together'>
That would work! We could interpret that as a statistical predictor of a tree’s height:
$$ \text{Probability a random tree is between } H \text{ and } H’ = \text{Bin-height}\cdot(H-H’)$$
</div>
If we wanted to know about the chances of being in a span of multiple bins, we would sum.
A _probability density function_, henceforth pdf, is that fractional histogram made continuous.
Let’s make a _pdf_ and use it a bit to understand it more. Suppose I want a random number $r$
between 0 and 2 whose probability is proportional to itself: $r$. We would expect the pdf $p(r)$
to look something like the figure below. But how high should it be?
![Figure 3-2](../images/fig-3-03-2.jpg)
<div class='together'>
The height is just $p(2)$. What should that be? We could reasonably make it anything by
convention, and we should pick something that is convenient. Just as with histograms we can sum up
(integrate) the region to figure out the probability that $r$ is in some interval $(x0,x1)$:
$$ \text{Probability } x0 < r < x1 = C \cdot area(p(r), x0, x1) $$
</div>
<div class='together'>
where $C$ is a scaling constant. We may as well make $C = 1$ for cleanliness, and that is exactly
what is done in probability. And we know the probability $r$ has the value 1 somewhere, so for
this case
$$ area(p(r), 0, 2) = 1 $$
</div>
<div class='together'>
Since $p(r)$ is proportional to $r$ , _i.e._, $p = C’ \cdot r$ for some other constant $C’$
$$
area(C’r, 0, 2) = \int_{0}^{2} C’ r dr
= \frac{C’r^2}{2}(2) - \frac{C’r^2}{2}(0)
= 2C’
$$
So $p(r) = r/2$.
</div>
How do we generate a random number with that pdf $p(r)$? For that we will need some more machinery.
Don’t worry this doesn’t go on forever!
<div class='together'>
Given a random number from `d = random_double()` that is uniform and between 0 and 1, we should be
able to find some function $f(d)$ that gives us what we want. Suppose $e = f(d) = d^2$. That is no
longer a uniform _pdf_ . The _pdf_ of $e$ will be bigger near 0 than it is near 1 (squaring a
number between 0 and 1 makes it smaller). To take this general observation to a function, we need
the cumulative probability distribution function $P(x)$:
$$ P(x) = area(p, -\infty, x) $$
</div>
<div class='together'>
Note that for x where we didn’t define $p(x)$, $p(x) = 0$, _i.e._, the probability of an $x$ there
is zero. For our example _pdf_ $p(r) = r/2$ , the $P(x)$ is:
$$ P(x) = 0 : x < 0 $$
$$ P(x) = \frac{x^2}{4} : 0 < x < 2 $$
$$ P(x) = 1 : x > 2 $$
</div>
<div class='together'>
One question is, what’s up with $x$ versus $r$? They are dummy variables -- analogous to the
function arguments in a program. If we evaluate $P$ at $x = 0.5$ , we get:
$$ P(1.0) = \frac{1}{4} $$
</div>
<div class='together'>
This says _the probability that a random variable with our pdf is less than one is 25%_ . This
gives rise to a clever observation that underlies many methods to generate non-uniform random
numbers. We want a function `f()` that when we call it as `f(random_double())` we get a return value
with a pdf $\frac{x^2}{4}$ . We don’t know what that is, but we do know that 25% of what it returns
should be less than 1, and 75% should be above one. If $f()$ is increasing, then we would expect
$f(0.25) = 1.0$. This can be generalized to figure out $f()$ for every possible input:
$$ f(P(x)) = x $$
</div>
<div class='together'>
That means $f$ just undoes whatever $P$ does. So,
$$ f(x) = P^-1 (x) $$
</div>
<div class='together'>
The -1 means “inverse function”. Ugly notation, but standard. For our purposes what this means is,
if we have pdf $p()$ and its cumulative distribution function $P()$ , then if we do this to a random
number we’ll get what we want:
$$ e = P^-1 (\text{random_double}()) $$
</div>
<div class='together'>
For our _pdf_ $p(x) = x/2$ , and corresponding $P(x)$ , we need to compute the inverse of $P$. If
we have
$$ y = \frac{x^2}{4} $$
we get the inverse by solving for $x$ in terms of $y$:
$$ x = \sqrt{4y} $$
Thus our random number with density $p$ we get by:
$$ e = \sqrt{4*\text{random_double}()} $$
</div>
Note that does range 0 to 2 as hoped, and if we send in $1/4$ for `random_double()` we get 1 as
desired.
<div class='together'>
We can now sample our old integral
$$ I = \int_{0}^{2} x^2 $$
</div>
<div class='together'>
We need to account for the non-uniformity of the _pdf_ of $x$. Where we sample too much we should
down-weight. The _pdf_ is a perfect measure of how much or little sampling is being done. So the
weighting function should be proportional to $1/pdf$ . In fact it is exactly $1/pdf$ :
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
#include <math.h>
#include <stdlib.h>
inline float pdf(float x) {
return 0.5*x;
}
int main() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int N = 1000000;
float sum;
for (int i = 0; i < N; i++) {
float x = sqrt(4*random_double());
sum += x*x / pdf(x);
}
std::cout << "I =" << sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
Since we are sampling more where the integrand is big, we might expect less noise and thus faster
convergence. This is why using a carefully chosen non-uniform pdf is usually called _importance
sampling_.
<div class='together'>
If we take that same code with uniform samples so the pdf = $1/2$ over the range [0,2] we can use
the machinery to get `x = 2*random_double()` and the code is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
#include <math.h>
#include <stdlib.h>
inline float pdf(float x) {
return 0.5;
}
int main() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int N = 1000000;
float sum;
for (int i = 0; i < N; i++) {
float x = 2*random_double();
sum += x*x / pdf(x);
}
std::cout << "I =" << sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
Note that we don’t need that 2 in the `2*sum/N` anymore -- that is handled by the _pdf_ , which is
2 when you divide by it. You’ll note that importance sampling helps a little, but not a ton. We
could make the pdf follow the integrand exactly:
$$ p(x) = \frac{3}{8}x^2 $$
And we get the corresponding
$$ P(x) = \frac{x^3}{8} $$
and
$$ P^{-1}(x) = 8x^\frac{1}{3} $$
</div>
<div class='together'>
This perfect importance sampling is only possible when we already know the answer (we got $P$ by
integrating $p$ analytically), but it’s a good exercise to make sure our code works. For just 1
sample we get:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
#include "random.h"
#include <iostream>
#include <math.h>
#include <stdlib.h>
inline float pdf(float x) {
return 3*x*x/8;
}
int main() {
int inside_circle = 0;
int inside_circle_stratified = 0;
int N = 1;
float sum;
for (int i = 0; i < N; i++) {
float x = pow(8*random_double(), 1./3.);
sum += x*x / pdf(x);
}
std::cout << "I =" << sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
Which always returns the exact answer.
<div class='together'>
Let’s review now because that was most of the concepts that underlie MC ray tracers.
1. You have an integral of $f(x)$ over some domain $[a,b]$
2. You pick a pdf $p$ that is non-zero over $[a,b]$
3. You average a whole ton of $f(r)/p(r)$ where $r$ is a random number $r$ with pdf $p$ .
This will always converge to the right answer. The more $p$ follows $f$, the faster it converges.
</div>
MC Integration on the Sphere of Directions
====================================================================================================
In our ray tracer we pick random directions, and directions can be represented as points on the
unit-sphere. The same methodology as before applies. But now we need to have a pdf defined over 2D.
Suppose we have this integral over all directions:
$$ \int cos^2(\theta) $$
<div class='together'>
By MC integration, we should just be able to sample $cos^2(\theta) / p(direction)$. But what is
direction in that context? We could make it based on polar coordinates, so $p$ would be in terms of
$(\theta, \phi)$ . However you do it, remember that a pdf has to integrate to 1 and represent the
relative probability of that direction being sampled. We have a method from the last books to take
uniform random samples in a unit sphere:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
vec3 random_in_unit_sphere() {
vec3 p;
do {
p = 2.0*vec3(random_double(),random_double(),random_double()) - vec3(1,1,1);
} while (dot(p,p) >= 1.0);
return p;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
To get points on a unit sphere we can just normalize the vector to those points:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
vec3 random_on_unit_sphere() {
vec3 p;
do {
p = 2.0*vec3(random_double(),random_double(),random_double()) - vec3(1,1,1);
} while (dot(p,p) >= 1.0);
return unit_vector(p);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
Now what is the pdf of these uniform points? As a density on the unit sphere, it is $1/area$ of the
sphere or $1/(4\pi)$. If the integrand is $cos^2(\theta)$ and $\theta$ is the angle with the z axis:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
inline float pdf(const vec3& p) {
return 1 / (4*M_PI);
}
int main() {
int N = 1000000;
float sum;
for (int i = 0; i < N; i++) {
vec3 d = random_on_unit_sphere();
float cosine_squared = d.z()*d.z();
sum += cosine_squared / pdf(d);
}
std::cout << "I =" << sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
The analytic answer (if you remember enough advanced calc, check me!) is $\frac{4}{3} \pi$, and the
code above produces that. Next, we are ready to apply that in ray tracing!
The key point here is that all the integrals and probability and all that are over the unit sphere.
The area on the unit sphere is how you measure the directions. Call it direction, solid angle, or
area -- it’s all the same thing. Solid angle is the term usually used. If you are comfortable with
that, great! If not, do what I do and imagine the area on the unit sphere that a set of directions
goes through. The solid angle $\omega$ and the projected area $A$ on the unit sphere are the same
thing.
![Figure 4-1](../images/fig-3-04-1.jpg)
Now let’s go on to the light transport equation we are solving.
Light Scattering
====================================================================================================
In this chapter we won't actually implement anything. We will set up for a big lighting change in
our program in the next chapter.
Our program from the last books already scatters rays from a surface or volume. This is the commonly
used model for light interacting with a surface. One natural way to model this is with probability.
First, is the light absorbed?
Probability of light scattering: $A$
Probability of light being absorbed: $1-A$
Here $A$ stands for _albedo_ (latin for _whiteness_). Albedo is a precise technical term in some
disciplines, but in all uses it means some form of fractional reflectance. As we implemented for
glass, the albedo may vary with incident direction, and it varies with color. In the most physically
based renderers, we would use a set of wavelengths for the light color rather than RGB. We can
almost always harness our intuition by thinking of R, G, and B as specific long, medium, and short
wavelengths.
If the light does scatter, it will have a directional distribution that we can describe as a pdf
over solid angle. I will refer to this as its _scattering pdf_: $s(direction)$. The scattering pdf
can also vary with incident direction, as you will notice when you look at reflections off a road --
they become mirror-like as your viewing angle approaches grazing.
<div class='together'>
The color of a surface in terms of these quantities is:
$$ Color = \int A \cdot s(direction) \cdot color(direction) $$
</div>
Note that $A$ and $s()$ might depend on the view direction, so of course color can vary with view
direction. Also $A$ and $s()$ may vary with position on the surface or within the volume.
<div class='together'>
If we apply the MC basic formula we get the following statistical estimate:
$$ Color = (A \cdot s(direction) \cdot color(direction)) / p(direction) $$
where $p(direction)$ is the pdf of whatever direction we randomly generate.
</div>
For a Lambertian surface we already implicitly implemented this formula for the special case where
$p()$ is a cosine density. The $s()$ of a Lambertian surface is proportional to $cos(\theta)$, where
$\theta$ is the angle relative to the surface normal. Remember that all pdf need to integrate to
one. For $cos(\theta) < 0$ we have $s(direction) = 0$, and the integral of cos over the hemisphere
is $\pi$.
<div class='together'>
To see that remember that in spherical coordinates remember that:
$$ \delta A = sin(\theta) \delta \theta \delta \phi $$
So:
$$ Area = \int_{0}^{2 \pi} \int_{0}^{\pi / 2} cos(\theta)sin(\theta) \delta \theta \delta \phi =
2 \pi \cdot \frac{1}{2} = \pi $$
</div>
<div class='together'>
So for a Lambertian surface the scattering pdf is:
$$ s(direction) = cos(\theta) / \pi $$
</div>
<div class='together'>
If we sample using the same pdf, so $p(direction) = cos(\theta) / \pi$, the numerator and
denominator cancel out and we get:
$$ Color = A * color(direction) $$
This is exactly what we had in our original color() function! But we need to generalize now so we
can send extra rays in important directions such as toward the lights.
The treatment above is slightly non-standard because I want the same math to work for surfaces and
volumes. To do otherwise will make some ugly code.
</div>
<div class='together'>
If you read the literature, you’ll see reflection described by the bidirectional reflectance
distribution function (BRDF). It relates pretty simply to our terms:
$$ BRDF = A * s(direction) / cos(\theta) $$
So for a Lambertian surface for example, $BRDF = A / \pi$. Translation between our terms and BRDF is
easy.
For participation media (volumes), our albedo is usually called _scattering albedo_, and our
scattering pdf is usually called _phase function_.
</div>
Importance Sampling Materials
====================================================================================================
<div class='together'>
Our goal over the next two chapters is to instrument our program to send a bunch of extra rays
toward light sources so that our picture is less noisy. Let’s assume we can send a bunch of rays
toward the light source using a pdf $pLight(direction)$ . Let’s also assume we have a pdf related to
$s$, and let’s call that $pSurface(direction)$ . A great thing about pdfs is that you can just use
linear mixtures of them to form mixture densities that are also pdfs. For example, the simplest
would be:
$$ p(direction) = 0.5 \cdot pLight(direction) + 0.5* \cdot pSurface(direction) $$
</div>
As long as the weights are positive and add up to one, any such mixture of pdfs is a pdf. And
remember, we can use any pdf -- it doesn’t change the answer we converge to! So, the game is to
figure out how to make the pdf larger where the product $s(direction)*color(direction)$ is large.
For diffuse surfaces, this is mainly a matter of guessing where $color(direction)$ is high.
For a mirror, $s()$ is huge only near one direction, so it matters a lot more. Most renderers in
fact make mirrors a special case and just make the $s/p$ implicit -- our code currently does that.
<div class='together'>
Let’s do simple refactoring and temporarily remove all materials that aren’t Lambertian. We can use
our Cornell Box scene again, and let’s generate the camera in the function that generates the model:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
void cornell_box(hittable **scene, camera **cam, float aspect) {
int i = 0;
hittable **list = new hittable*[8];
material *red = new lambertian( new constant_texture(vec3(0.65, 0.05, 0.05)) );
material *white = new lambertian( new constant_texture(vec3(0.73, 0.73, 0.73)) );
material *green = new lambertian( new constant_texture(vec3(0.12, 0.45, 0.15)) );
material *light = new diffuse_light( new constant_texture(vec3(15, 15, 15)) );
list[i++] = new flip_normals(new yz_rect(0, 555, 0, 555, 555, green));
list[i++] = new yz_rect(0, 555, 0, 555, 0, red);
list[i++] = new xz_rect(213, 343, 227, 332, 554, light);
list[i++] = new flip_normals(new xz_rect(0, 555, 0, 555, 555, white));
list[i++] = new xz_rect(0, 555, 0, 555, 0, white);
list[i++] = new flip_normals(new xy_rect(0, 555, 0, 555, 555, white));
list[i++] = new translate(new rotate_y(
new box(vec3(0, 0, 0), vec3(165, 165, 165), white), -18), vec3(130,0,65));
list[i++] = new translate(new rotate_y(
new box(vec3(0, 0, 0), vec3(165, 330, 165), white), 15), vec3(265,0,295));
*scene = new hittable_list(list,i);
vec3 lookfrom(278, 278, -800);
vec3 lookat(278, 278, 0);
float dist_to_focus = 10.0;
float aperture = 0.0;
float vfov = 40.0;
*cam = new camera(lookfrom, lookat, vec3(0,1,0),
vfov, aspect, aperture, dist_to_focus, 0.0, 1.0);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
At 500x500 my code produces this image in 10min on 1 core of my Macbook:
![Figure 6-1](../images/img-3-06-1.jpg)
Reducing that noise is our goal. We’ll do that by constructing a pdf that sends more rays to the
light.
</div>
First, let’s instrument the code so that it explicitly samples some pdf and then normalizes for
that. Remember MC basics: $\int f(x) \approx f(r)/p(r)$. For the Lambertian material,
let’s sample like we do now: $p(direction) = cos(\theta) / \pi$.
<div class='together'>
We modify the base-class _material_ to enable this importance sampling:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class material {
public:
virtual bool scatter(const ray& r_in,
const hit_record& rec, vec3& albedo, ray& scattered, float& pdf) const {
return false;
}
virtual float scattering_pdf(const ray& r_in, const hit_record& rec,
const ray& scattered) const {
return 0;
}
virtual vec3 emitted(float u, float v, const vec3& p) const {
return vec3(0,0,0);
}
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
And _Lambertian_ material becomes:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
class lambertian : public material {
public:
lambertian(texture *a) : albedo(a) {}
float scattering_pdf(const ray& r_in,
const hit_record& rec, const ray& scattered) const {
float cosine = dot(rec.normal, unit_vector(scattered.direction()));
if (cosine < 0)
return 0;
return cosine / M_PI;
}
bool scatter(const ray& r_in,
const hit_record& rec, vec3& alb, ray& scattered, float& pdf) const {
vec3 target = rec.p + rec.normal + random_in_unit_sphere();
scattered = ray(rec.p, unit_vector(target-rec.p), r_in.time());
alb = albedo->value(rec.u, rec.v, rec.p);
pdf = dot(rec.normal, scattered.direction()) / M_PI;
return true;
}
texture *albedo;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
And the color function gets a minor modification:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
vec3 color(const ray& r, hittable *world, int depth) {
hit_record rec;
if (world->hit(r, 0.001, MAXFLOAT, rec)) {
ray scattered;
vec3 attenuation;
vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p);
float pdf;
vec3 albedo;
if (depth < 50 && rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf)) {
return emitted + albedo*rec.mat_ptr->scattering_pdf(r, rec, scattered)
*color(scattered, world, depth+1) / pdf;
}
else
return emitted;
}
else
return vec3(0,0,0);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
You should get exactly the same picture.
<div class='together'>
Now, just for the experience, try a different sampling strategy. Let’s choose randomly from the
hemisphere that is above the surface. This would be $p(direction) = 1/(2* \pi)$ .
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
bool scatter(const ray& r_in,
const hit_record& rec, vec3& alb, ray& scattered, float& pdf) const {
vec3 direction;
do {
direction = random_in_unit_sphere();
} while (dot(direction, rec.normal) < 0);
scattered = ray(rec.p, unit_vector(direction), r_in.time());
alb = albedo->value(rec.u, rec.v, rec.p);
pdf = 0.5 / M_PI;
return true;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
And again I _should_ get the same picture except with different variance, but I don’t!
![Figure 6-2](../images/img-3-06-2.jpg)
</div>
It’s pretty close to our old picture, but there are differences that are not noise. The front of the
tall box is much more uniform in color. So I have the most difficult kind of bug to find in a Monte
Carlo program -- a bug that produces a reasonable looking image. And I don’t know if the bug is the
first version of the program or the second, or even in both!
Let’s build some infrastructure to address this.
Generating Random Directions
====================================================================================================
In this and the next two chapters let’s harden our understanding and tools and figure out which
Cornell Box is right. Let’s first figure out how to generate random directions, but to simplify
things let’s assume the z-axis is the surface normal and $\theta$ is the angle from the normal.
We’ll get them oriented to the surface normal vector in the next chapter. We will only deal with
distributions that are rotationally symmetric about $z$. So $p(direction) = f(\theta)$. If you
have had advanced calculus, you may recall that on the sphere in spherical coordinates
$dA = \sin(\theta) \cdot d\theta \cdot d\phi$. If you haven’t, you’ll have to take my word for the
next step, but you’ll get it when you take advanced calc.
<div class='together'>
Given a directional pdf, $p(direction) = f(\theta)$ on the sphere, the 1D pdfs on $\theta$ and
$\phi$ are:
$$ a(\phi) = \frac{1}{2\pi} $$
(uniform)
$$ b(\theta) = 2*\pi*f(\theta)\sin(\theta) $$
</div>
<div class='together'>
For uniform random numbers $r_1$ and $r_2$, the material presented in Chapter 3 leads to:
$$ r_1 = \int_{0}^{\phi} \frac{1}{2\pi} = \frac{\phi}{2\pi} $$
Solving for $\phi$ we get:
$$ \phi = 2 \pi \cdot r_1 $$
For $\theta$ we have:
$$ r_2 = \int_{0}^{\theta} 2 \pi f(t) \sin(t) $$
</div>
<div class='together'>
Here, $t$ is a dummy variable. Let’s try some different functions for $f()$. Let’s first try a
uniform density on the sphere. The area of the unit sphere is $4\pi$ , so a uniform $p(direction) =
\frac{1}{4\pi}$ on the unit sphere.
$$ r_2 = \frac{-\cos(\theta)}{2} - \frac{-\cos(0)}{2} = \frac{1 - \cos(\theta)}{2} $$
Solving for $\cos(\theta)$ gives:
$$ \cos(\theta) = 1 - 2 r_2 $$
We don’t solve for theta because we probably only need to know $\cos(\theta)$ anyway and don’t want
needless $\arccos()$ calls running around.
</div>
<div class='together'>
To generate a unit vector direction toward $(\theta,\phi)$ we convert to Cartesian coordinates:
$$ x = \cos(\phi) \cdot \sin(\theta) $$
$$ y = \sin(\phi) \cdot \sin(\theta) $$
$$ z = \cos(\theta) $$
And using the identity that $\cos^2 + \sin^2 = 1$, we get the following in terms of random
$(r_1,r_2)$:
$$ x = \cos(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$
$$ y = \sin(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$
$$ z = 1 - 2 r_2 $$
Simplifying a little, $(1 - 2 r_2)^2 = 1 - 4r_2 + 4r_2^2$, so:
$$ x = \cos(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$
$$ y = \sin(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$
$$ z = 1 - 2 r_2 $$
</div>
<div class='together'>
We can output some of these:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
for (int i = 0; i < 200; i++) {
float r1 = random_double();
float r2 = random_double();
float x = cos(2*M_PI*r1)*2*sqrt(r2*(1-r2));
float y = sin(2*M_PI*r1)*2*sqrt(r2*(1-r2));
float z = 1 - 2*r2;
std::cout << x << " " << y << " " << z << "\n";
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
And plot them for free on plot.ly (a great site with 3D scatterplot support):
![Image 7-1](../images/fig-3-07-1.jpg)
</div>
On the plot.ly website you can rotate that around and see that it appears uniform.
<div class='together'>
Now let’s derive uniform on the hemisphere. The density being uniform on the hemisphere means
$p(direction) = 1 / (2 \pi)$ and just changing the constant in the theta equations yields:
$$ \cos(\theta) = 1 - r_2 $$
</div>
<div class='together'>
It is comforting that $\cos(\theta)$ will vary from 1 to 0, and thus theta will vary from 0 to
$\pi/2$. Rather than plot it, let’s do a 2D integral with a known solution. Let’s integrate cosine
cubed over the hemisphere (just picking something arbitrary with a known solution). First let’s do
it by hand:
$$ \int \cos^3 = 2 \pi \int_{0}^{\pi/2} \cos^3 \sin = \frac{\pi}{2} $$
</div>
<div class='together'>
Now for integration with importance sampling. $p(direction) = 1/(2 \pi)$, so we average $f/p$
which is $\cos^3 / (1/(2 \pi))$, and we can test this:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++
int main() {
int N = 1000000;
float sum = 0.0;
for (int i = 0; i < N; i++) {
float r1 = random_double();
float r2 = random_double();
float x = cos(2*M_PI*r1)*2*sqrt(r2*(1-r2));
float y = sin(2*M_PI*r1)*2*sqrt(r2*(1-r2));
float z = 1 - r2;
sum += z*z*z / (1.0/(2.0*M_PI));
}
std::cout << "PI/2 = " << M_PI/2 << "\n";
std::cout << "Estimate = " << sum/N << "\n";
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</div>
<div class='together'>
Now let’s generate directions with $p(directions) = \cos(\theta) / \pi$.
$$ r_2 = \int_{0}^{\theta} 2 \pi \frac{\cos(t)}{\pi} \sin(t) = 1 - \cos^2(\theta) $$
So,