Notes, 98-02-23 --------------- Direct Lighting - last time Indirect Lighting: remarkably little code, amazingly lots of CPU time. Running the indirect Cornell box to noiselessness takes about a teraray. This lecture will be completely backward: code first, then intuition of why that's the way it is. In direct lighting, one sends out a ray, hits a point, there's a surface normal, some light source, and I send a ray to some random spot on that light source and do some direct computation based on cosines, etc. Am missing light that hits another object, bounces toward the object you're looking at then toward your eye. Ceiling tiles in this room are lit this way. Leap of faith: how bright that point is depends on how easy it is for light to get from the light source to the eye. If it's a very improbable path, you won't get much light. There are more ways for light to get from the source to the wall than under the table: the measure of pathspace to the wall is bigger than the measure under the table. Will get to this last lecture. Like a Feynman integral, except he didn't really have a notion of measure. If you're likely to get form the light to the eye, then if you put a flashlight at your eye, you probably end up with a bright spot where the light is. Will see this to be true later. So the way we do this is to send a ray out from the eye and see how likely it is to get back to the light source. Send a ray out, scatter it with a diffuse dist'n. It's a cosine term, cause the amount of intensity leaving at an angle gets less as the angle gets bigger (ie: you're looking at it more obliquely). If you graph the liklihood that light scatters off a diffuse surface in any particular direction: probability is proportional to cosine(theta), where theta is the angle between the scattered ray and the surface. Want: p(theta,phi) for a hemisphere: k cos(theta). Know that: Int_0^2Pi Int_0^Pi/2 kcos(theta) sin(theta)d(theta)d(phi) = 1, solve for k. k = Pi. Is that a dumb answer? Sure. So the magic density function for light outgoing a diffuse scattering is: p(theta,phi)=(1/Pi)cos(theta). Of course, the light could get absorbed with some probability (1-reflectance). Coord sys of reflectance frame: normal alight with hat(z): hat(v) is the scattered direction. cos(theta)=hat(v).hat(z) = v.z cos(phi)=hat(v).hat(x). p(phi)=(1/2Pi) - uniform. phi=2Pi*drand48 (or stratefy it). p(theta)=2sin(theta)cos(theta) once you normalize in range (0..Pi/2). So how do you generate a random number with respect to that thing? P(theta)=Int_0^theta 2sin(theta')cos(theta')d(theta') = sin^2(theta). Set (xi)=sin^2(theta), figure out theta. sin^2(theta)=1-cos^2(theta), 'cause we really want cos(theta) -> cos^2(theta)=1-xi, cos(theta)=sqrt(1-xi). But 1-xi (uniform 0..1) is swappable with xi. cos(theta)=sqrt(xi). But that cos(phi)=v.x should be sin(theta)cos(phi)=v.x. So we have: v_x = cos(phi)sin(theta) = cos(2PiXi_1)sqrt(Xi_2) v_y = sin(phi)sin(theta) = sin(2PiXi_1)sqrt(Xi_2) v_z = cos(theta) = sqrt(1-Xi_2) Their sqares sum to 1, so that's plausible. But, the normal doesn't usually align with z, so we do a coordinate tranformation. Maybe you have (u,v) as texture coordinates. You might want (u,v) for reflection to line up with texture coords if you want your texture map to interact with reflection. We still want a cos dist'n with respect to the w coordinate. There's a handout on the web on changing coordinates. Code: generate normal vectors, generate reflection vector, gives you hat(a) with a cosine desnity f'n. Wisdom: calculate everything on the fly; store nothing. Modellers exhaust resources, if you store lots of extra stuff, you're guaranteed to run out of memory and never get a picture. It's better to just take twice as long and actually get a picture at all. Code: Spectrum radiance(Ray r) { Spectrum s=0; if(miss) return background(r) - for Cornell box, background is black if(x emits) - x is the point the ray hits at. It emits if the viewing ray goes out and hits the light source directly. s+=x.emittedRadiance - if you see the light directly, you see its radiance reflectedRay=(junk with u,v,w) - scatter the ray w/cosine dist'n s+=reflectance*radiance(reflectedRay) return s } This code will never terminate. In the cornell box it works; light escapes the front. In a closed room it doesn't. Remember: you still reflect off the light when you hit it. This is the opposite of the program that emits random photons from the light; you get very lucky when they hit the camera. If your light is smaller than the aperture, you're better off reversing this whole process. This is calculating direct lighting implicitly. This process is called "naive path tracing." We'll have an efficient way to get the first bounce for direct lighting. Might use this for first bounces. Hack: have huge lights off screen - you're likely to hit 'em, and they needn't be very bright. Now we modify that code a bit: Spectrum radiance2(Ray r) { Spectrum s=0 if(miss) return background(r) s+=directLighting reflectedRay ... s+=reflectance*radiance2(reflectedRay) return s } Catch: this will produce incorrect image - it missed the light when your viewing ray hits the light source first: there's this amazingly ugly thing that shows up in code like this: if(depth==0) s+=emittedRadiance This code is ugly. This is not surprising: naive methods are always pretty and clean. Another problem: if you want mirrors to coexist peacefully, mirrors don't have direct lighting. If you bounce off a mirror and hit a light source, you have to have depth==1. Pete hopes there's a clean way to do this. A mirror is direct lighting with a delta function involved; you have to hard- code delta handling. Perhaps we're doomed to ugly code. How do you debug this mess? Want a scene with an analytic solution. Suppose one is in a scene, sends out a viewing ray, bounce once into ray L' from point x1' with refl. R1. Eye sees: L = E(x1') + R1*L' L = E(x1) + R1* [ E(x2) + R2* L2 ] ... = E(x1) + R1E(x2) + R1R2E(x3) + ... Notice there's no geometry in there. Geometry is implicitly in there. If all objects are emitters of the same color, then E(xi) = E0. Set all R's equal: Ri=R0. L=E0+R0E0+R0^2E0+... = E0 (1+R0+R0^2+...) = E0/(1-R0). So set up a scene where you're inside a tet and they're all illuminating each other. If E0=R0=1/2, then all the pixels should have value 1.0. Brute force RT'er has to be pretty dumb not to get this answer. But the radiance2() program probably won't work, 'cause it won't pass the test, 'cause we're all bad programmers. This is a good debugging case for this particular method. Another: point source and a plane; should be able to anaytically solve. [ Plan 10: One really would like to be able to write code once and have generated something that stores everything with everything like I do (eg: octants, normals stored with triangles, etc) or something that does it like Pete and computes everything on the fly. [ How about generating Cramer's rule code that reuses little 2x2's as much as possible? There's a cute test case. ] [ Papers on reusing stuff intersecting triangular meshes - super-ugly code, but factor of 2 speedup? - Pete knows the ref. ] Stopping indirect lighting: Way 1: put a threshold on attenuation. Stop when R0*R1*R2.. is sufficiently small. Might add an error estimate on the end. Way 2: Ray comes in, send reflected ray with probability p, terminate with probability 1-p. Doesn't guarantee termination for real random numbers, but drand48() hits all numbers in a finite amount of time, so you will terminate. But you have to return (RL/p) to be right on reflected rays: if(drand48