Phong illumination on planes

Started by
4 comments, last by okonomiyaki 16 years, 6 months ago
I'm writing a ray tracer to learn more about various illumination algorithms. I recently implemented Phong illumination using the standard algorithm found here: http://www.gamedev.net/reference/articles/article667.asp It works great on spheres. However, when I added a plane to the scene things got interesting. The diffuse + specular illumination seem to work on the plane but only for the closer region. As the plane extends into infinity, I assumed that 1) there shouldn't be any specular highlights there, because the view and light rays are in the same direction, and 2) although I'm not doing any attenuation yet, the color would still gradually fade because of the increasing angle between the light ray and plane normal. However, the back of the plane becomes suddenly bright. A screenshot is surely worth a thousand words: image.png Basically there are three different colored lights, a couple spheres and a tilted plane. Here's the source code (Scheme) that calculates the lighting, essentially just looping over all the lights and adding in the light's affect to an accumulator.

; hitObj - the intersected object
; point - vector to hit point
; view - the view direction vector
(define (apply-lighting hitObj point v)
  (if (string=? (obj-type hitObj) "light")
      (obj-color hitObj)
      (let ((acc (make-vec3d 0 0 0)))
        (for-each (lambda (obj)
                    (when (string=? (obj-type obj) "light")
                          (let* ((lightToP (vec3d-sub (obj-pos obj) point))
                                 (l (vec3d-unit lightToP))
                                 (n (obj-normal hitObj point))
                                 (r (vec3d-sub (vec3d-scalar-mul n (* 2 (vec3d-dot n l))) l))
                                 (diff (vec3d-dot n l))
                                 (spec (expt (vec3d-dot r v) 20)))
                            (when debug
                                  (log "hit: " (obj-type hitObj))
                                  (log "hitColor:" (obj-color hitObj))
                                  (log "point: " point)
                                  (log "lightToP: " lightToP)
                                  (log "normal: " n)
                                  (log "spec:" spec))
                            (if (> diff 0) 
                                (set! acc (vec3d-add acc (vec3d-component-mul
                                                          (vec3d-add
                                                           (vec3d-scalar-mul (obj-color hitObj)
                                                                             diff)
                                                           (vec3d-scalar-mul (make-vec3d 1 1 1)
                                                                             spec))
                                                          (obj-color obj))))))))
                  scene)
        acc)))



Can anyone help explain this?
Advertisement
Don't forget to clamp your dots to >= 0, and make sure none of your colour components are < 0. I can't say exactly from your code but it's possibly the problem, with one light it would be easier to see if that was the problem (you'd get two highlights). This wouldn't show up on the spheres, but would on planes.
this is sometimes called self-shadowing term. A very simplified Pong equation that leaves out lots of details is:

Color = Ambient + N.L + Specular

you calculate something like

S = saturate(4.0f * N.L)

Then you apply this like this

Color = Ambient + S * (N.L + Specular)

You might also use the S in a branch. If you do bump mapping and your light vector is in texture space, you might also use something like

S= saturate(4.0f * LightDir.z);

The 4.0f is just a sensible value. You can also pick 10.0f :-)

- Wolf
Thanks guys, I forgot about the self-shadowing term. That seems to help in certain places, but it doesn't solve my problem. It also seems like it's more complex than sanitizing dot products and such (i.e. enforcing dot product to be positive).

I played around with it a bunch, and I'm still a little confused. There's a reason why primitives behind the lights have the specular component at all, and it has something to do with how I calculate the reflection vector.

Here it is without the specular component (1 light):



And here it is with unshadowed specular component:



Adding the self-shadowing helps, but there's still a problem in the calculations:



So, why do the faces behind the lights light up like that? I was thinking through the whole process in my head, and it actually makes sense why it would with my current implementation. It seems that there are two places which are strangely buggy to me. One is where I calculate the reflection vector, and the other is where I calculate the actual specular component. Code as it stand now is:

(let ((reflect (vec3d-sub (vec3d-scalar-mul n (* 2 (vec3d-dot n l))) l))      ...      (spec (expt r.v 20)))


So, that code creates a reflection vector which on the far back of the plane will be closely parallel to the direction vector, resulting in a dot product close to 1.0. In fact, this means the the correct specular highlights like on the spheres are actually being calculated with a inversed reflection vector, or rather r.v is negative of what it should be. But because it's exponentiated by an even number, the negation of it is multiplied out. To test this out, I changed spec to be (expt r.v 19), and my theory was right. The "correct" specular highlights turned negative (and "saturated" to black):



So how come I saw so many examples of the exact same code as above? I did some more research and found a few examples that actually use a negated light direction vector in the calculation of the reflection vector. So, I figured if I did that and kept the exponentiation to an odd number, it would effectively cancel out faces behind the lights. So the new code is:

(let ((reflect (vec3d-sub (vec3d-scalar-mul n (* 2 (vec3d-dot n -l))) -l))      ...      (spec (expt r.v 19)))


And this generates a correct image:



So, is this normal? How come I haven't seen anyone mention this, as it seems pretty serious? I haven't read any books about phong illumination yet, but I've seen quite a few online examples using the standard code I had at first, which seems to be wrong...

Final picture turned out to work well:



EDIT: I am currently reading more theoretical papers about phong lighting, such as this one, and learning more about the formal theory, and I expect to find an answer to my own question there. It's still an interesting discussion though.

[Edited by - okonomiyaki on October 1, 2007 12:43:49 AM]
Hmm, tricky - could you post the whole new code again? Also an image with a constant diffuse might help, so we can see exactly the highlights. The perfect specular reflection vector should be:

R = 2*(N dot L)*N - L

(which you had, along with)
PhongS = (dot_clamp(V,R) pow n)

Are you sure your normals for the plane are correct?
Quote:Original post by JuNC
Hmm, tricky - could you post the whole new code again? Also an image with a constant diffuse might help, so we can see exactly the highlights. The perfect specular reflection vector should be:

R = 2*(N dot L)*N - L

(which you had, along with)
PhongS = (dot_clamp(V,R) pow n)

Are you sure your normals for the plane are correct?


Yes, the plane normal is correct. And that algorithm is right, but the terms need to be explained better. L is not the vector from the point to the light, but rather the light to the point, and then everything works smoothly. I can't find any good articles online that explain this. This is the one this tipped me off about the light vector, that it needs to be inverted because we are usually working with a point to light vector. As long as I saturate R.V the exponentiation can be whatever we want (I wasn't saturating it before because it took away the correct highlights, which I now understand why).

Here's the image you requested with constant diffuse, calculated with the original code:



And here is the new code:

(define (apply-lighting hitObj point v)  (if (string=? (obj-type hitObj) "light")      (obj-color hitObj)      (let ((acc (make-vec3d 0 0 0)))        (for-each (lambda (obj)                    (when (string=? (obj-type obj) "light")                          (let* ((pointToLight (vec3d-sub (light-pos obj) point))                                 (l (vec3d-unit pointToLight))                                 (-l (vec3d-scalar-mul l -1.0))                                 (n (obj-normal hitObj point))                                 (n.l (ceiling (saturate (vec3d-dot n l))))                                 (r (vec3d-sub (vec3d-scalar-mul n (* 2 (vec3d-dot n -l))) -l))                                 (r.v (saturate (vec3d-dot r v)))                                 (diff n.l)                                 (spec (expt r.v 20))                                 (shadow (saturate (* 4 diff))))                            (if (> diff 0)                                 (set! acc (vec3d-add acc (vec3d-component-mul                                                           (vec3d-scalar-mul                                                             (vec3d-add                                                               (vec3d-scalar-mul (obj-color hitObj)                                                                                 diff)                                                               (vec3d-scalar-mul (make-vec3d 1 1 1)                                                                                 spec))                                                             shadow)                                                           (obj-color obj))))))))                  scene)        acc)))


And the new image generated:

This topic is closed to new replies.

Advertisement