Shading is the computation used in 3D graphics to determine how a surface appears to one's eye, given surface properties and an incident lighting environment. Here is a sketch of a simple shader. It handles specular and diffuse shading, texture mapping, and a checker-board effect.
Shade
computes how one point on a surface looks to an eye, given a
lighting environment. Surface
is a structure containing the
surface properties, typically color, transparency, shininess, and a
plastic/metal bit. S
, t
, and normal
give the position and
orientation of the surface point. Shade
sums the contributions
from each of the lights. Each light reflects proportionally to a
power of the dot product of the eye and the reflected light.
(define (pass-checks? freq s t) (odd? (xor (mod (* s freq) 1)) (mod (* t freq) 1)))(define (shade eye lights surface s t normal) (define (shade-one-light light) (let ((k (dot-product eye (reflect (direction light) normal))) (exp (roughness->exponent (roughness surface))) (diffuse-color (cond ((texture? surface) ...) ((and (checked? surface) (pass-checks? (check-frequency surface) s t)) (check-color surface)) (else (diffuse-color surface))))) (gen-* (color light) (gen-+ (gen-* (power exp k) (specular-color surface)) (gen-* k diffuse-color))))) (gen-+ (ambient surface) ; maybe inside lights? (reduce gen-+ (map shade-one-light lights))))
Though this code is concise and very abstract, it is cluttered with
vector arithmetic and structure references. The vector and color
arithmetic is done with generic procedures gen-*
and gen-+
that
dynamically test the types of their arguments.
When rendering animation, eye
, lights
, and surface
will be
fixed for at least each complete frame. If we hold these arguments
static and use RTCG, then a loop over the points on a surface can use
this program instead:
(define (shade-specialized s t normal) (let* ((t0 (reflect constant-direction0 normal)) (t1 (reflect constant-direction1 normal)) (k (+ (* eye_0 t0_0) (* eye_1 t0_1) (* eye_2 t0_2))) (k2 (* k k)) (k4 (* k2 k2)) (k8 (* k4 k4)) (k16 (* k8 k8)) (k20 (* k16 k4)) (m (+ (* eye_0 t1_0) (* eye_1 t1_1) (* eye_2 t1_2))) (m2 (* m m)) (m4 (* m2 m2)) (m8 (* m4 m4)) (m16 (* m8 m8)) (m20 (* m16 m4)))(color (+ a_0 (* Cl0_0 (+ (* k20 Cs_0) (* k d_0))) (* Cl1_0 (+ (* m20 Cs_0) (* m d_0)))) (+ a_1 (* Cl0_1 (+ (* k20 Cs_1) (* k d_1))) (* Cl1_1 (+ (* m20 Cs_1) (* m d_1)))) (+ a_2 (* Cl0_2 (+ (* k20 Cs_2) (* k d_2))) (* Cl1_2 (+ (* m20 Cs_2) (* m d_2)))))))
All tests, loops, and indirect calls have been eliminated; the remaining code can be scheduled more easily and will run faster. Thus we can more than win back the time spent compiling it in a wide range of pixel resolutions, scene complexities, and numbers of frames.
But a lisp-generating extension for this shader would be much harder to write than the interpreter given above. If this were C/DCG instead of lisp, then the generating extension would be quite tricky for the programmer.