• No results found

Throwing Shadows: Light Source Shading

Now, we have control over solid, visible figures that can move around anywhere in the space. However, illumination is one important aspect that we have overlooked. We can present the three dimensional worlds much more impressively by introducing a light source.

Today's PC's do not yet have the ability to perform for real time ray tracing. These types of pictures require a few minutes to generate. So, we use a faster method that does not consider every ray of light but produces very beautiful effects by using simpler methods.

If you move away from a continuous yet distant light source, all the rays of light appear to be parallel on the surface of the object. This is why you can perform ray tracing calculations using a single light vector instead of calculating each point on the surface individually. This "homogenous" illumination makes it possible to estimate the light covering each surface.

The following model demonstrates how the illumination on a surface is calculated. The "flatter" the light that strikes the surface, the darker is the illumination. The illumination is at its maximum when the light is perpendicular to the surface. This is because an equally large mass of energy is distributed over a larger surface of flat angles and is, therefore, not as dense:

d n l f ß ' = sin (ß - 90) = -cosß d f ß ' = ß - 90 d f = sinß ß

Relationship of the angle to the brightness

The relationship d/f is proportional to the illumination of the surface (as shown above). This relationship is the same as the negative cosine of the angle between the light vector and the normal vector for the surface. The normal vector is a vector that stands perpendicular to the surface. It's easily determined using the intersecting product of two vectors that lie in the plane. Because the cosine of the angle is needed here, and not the angle itself, simplifies the calculation greatly since the angle determination (through the scalar product) indicates the cosine of the angle.

The procedure for determining the illumination is as follows:

Ø Find two vectors appearing on the surface. The easiest would be two margin vectors (from the first to the second and to the last point).

Ø Form normal vector (intersecting product of the surface vectors).

Ø Through the scalar product, angle between (form constant light vector and normal vector). Ø Add the result to the color.

The result of the angle calculation is negative in the example. If the surface is turned away from the light (ß < 90 degrees), the result will be positive. Only the basic

surface color can be used because it lies in the shadow and, therefore, only receives diffused light. The 3D_LIGHT.PAS program demonstrates the capabilities of the shading routine:

Uses Crt,ModeXLib,Gif,var_3d; Const worldlen=8*3; {Point-Array} Worldconst:Array[0..worldlen-1] of Integer = (-200,-200,-200, -200,-200,200, -200,200,-200, -200,200,200, PC PCunderground

You can find 3D_LIGHT.PAS on the companion CD-ROM

200,-200,-200, 200,-200,200, 200,200,-200, 200,200,200); surfclen=38; {Surface-Array} surfcconst:Array[0..surfclen-1] of Word= ($fee0,4, 0,2,6,4, $fec0,4, 0,1,3,2, $fec0,4, 4,6,7,5, $fee0,4, 1,5,7,3, $fec0,4, 2,3,7,6, $fec0,4, 0,4,5,1,0,0);

{ $fe = use light source, primary color in the low-byte} Var

i,j:Word;

Procedure Shad_Pal; {prepare palette for shading} Begin

For j:=192 to 223 do Begin {prepare colors 192 - 223 and 224 - 255} i:=trunc((j/32)*43); {determine brightness}

Fillchar(Palette[j*3],3,i+20); {colors 192-223 to gray tones}

Palette[(j+32)*3]:=i+20; {colors 224-255 to red tones} Palette[(j+32)*3+1]:=0;

Palette[(j+32)*3+2]:=0; End;

Setpal; {set this palette} End;

procedure drawworld;external; {draws the world on current video page} {$l 3dasm.obj}

{$l poly.obj} {$l bres.obj} {$l root.obj} Begin

vz:=1000; {solid is located at 1000 unit depth} vpage:=0; {start with page 0}

LoadGif('logor.gif'); {load wallpaper} init_modex; {enable ModeX}

Shad_Pal; {calculate shading palette} rotx:=0; {initial values for rotation} roty:=0;

rotz:=0;

Fill:=true; {SurfaceFill on} sf_sort:=true; {SurfaceSort on}

sf_shift:=true; {SurfaceShift suppression on} Glass:=false; {glass surfaces off}

p13_2_modex(16000*2,16000); {wallpaper to VGA page 2} repeat

CopyScreen(vpage,16000*2); {wallpaper to current page} DrawWorld; {draw world}

switch; {switch to finished picture} WaitRetrace; {wait for next retrace} Inc(rotx); {continue rotating ... } If rotx=120 Then rotx:=0;

Inc(rotz);

If rotz=120 Then rotz:=0; inc(roty);

if roty=120 Then roty:=0;

Until KeyPressed; { ... until key} TextMode(3);

The palette is prepared before the shading can be established. Colors 192 to 223 and 224 to 255 are filled with two color displays. The first one contains the colors ranging from gray to white. The second one contains the colors ranging from dark red to red. Now the cosine can simply be added to the basic color when filling so the correct degree of shading is maintained.

The basic color is classified in the low byte of the color information ( 0C0h and 0E0h). The high byte is tested against this with the first directional value. The light source shading for this surface is turned on with 0FEh. This is why it's now possible to mix shaded surfaces (directional byte 0FEh) with fixed value surfaces (directional byte < 0FEh) or with textures later.

An additional change to the 3D_SOLID program involves the background picture. A picture is loaded at the beginning, which is then calculated by a raytracer and contains a similar light vector as the rotating object, which reinforces the impression of a light source. Instead of erasing the screen before creating every picture, the background picture is simply copied each time to the corresponding page.

In procedure drawworld (in 3DASM.ASM), the directional byte is deciphered right at the beginning and the global variables Lightsrc and Texture are set, in this case it only refers to Lightsrc.

Additionally, arrays Points3D and Poly3D are factored. The first array receives three dimensional coordinates which were completely rotated and calculated in the macro zrot. These are then carried over to loop npoint in array Poly3D and ultimately formed into a closed line, in which the first corner is copied onto the last one. This process completely corresponds to the

one executed for the two dimensional coordinates.

If the surface is now filled, the macro getdelta is called. It determines both surface vectors:

getdelta macro ;calculates the two surface vectors mov ax,poly3d[0] ;x: original corner

mov delta2[0],ax ;store temporarily in delta2 sub ax,poly3d[8] ;obtain difference to first point mov delta1[0],ax ;and delta1 finished

mov ax,poly3d[2] ;y: original corner

mov delta2[2],ax ;store temporarily in delta2 sub ax,poly3d[10d] ;obtain difference to first point mov delta1[2],ax ;and delta1 finished

mov ax,poly3d[4] ;z: original corner

mov delta2[4],ax ;store temporarily in delta2 sub ax,poly3d[12d] ;obtain difference to first point mov delta1[4],ax ;and delta1 finished

mov bp,polyn ;select last point dec bp

shl bp,3 ;8 bytes at a time mov ax,poly3d[bp] ;get x

sub delta2[0],ax ;obtain difference mov ax,poly3d[bp+2] ;get y

sub delta2[2],ax ;obtain difference mov ax,poly3d[bp+4] ;get z

sub delta2[4],ax ;obtain difference endm

delta1 is loaded here with the difference between the first and second polygon points and delta2 is loaded with the difference between the first and last points. If no illogical surfaces are defined, both vectors will always be independently linear (not parallel) and, therefore, available.

PC

PCunderground

The getdelta macro is part of the 3DASM.ASM file on the companion CD-ROM

Otherwise, the program is stopped with a Division by Zero error. If global variable lightsrc is TRUE here, both get_normal

and light macros are called. The first one determines the normal surface vector from the two surface vectors, delta1 and

delta2. The second macro determines the lightness of the surface from the angle.

get_normal macro ;calculates normal vector of an area mov ax,delta1[2] ;a2*b3

imul delta2[4] shrd ax,dx,4 mov n[0],ax

mov ax,delta1[4] ;a3*b2 imul delta2[2]

shrd ax,dx,4 sub n[0],ax

mov ax,delta1[4] ;a3*b1 imul delta2[0]

shrd ax,dx,4 mov n[2],ax

mov ax,delta1[0] ;a1*b3 imul delta2[4]

shrd ax,dx,4 sub n[2],ax

mov ax,delta1[0] ;a1*b2 imul delta2[2] shrd ax,dx,4 mov n[4],ax mov ax,delta1[2] imul delta2[0] shrd ax,dx,4

sub n[4],ax ;cross product (=normal vector) finished mov ax,n[0] ;x1 ^ 2 imul ax mov bx,ax mov cx,dx mov ax,n[2] ;+x2 ^ 2 imul ax add bx,ax adc cx,dx mov ax,n[4] ;+x3 ^ 2 imul ax add ax,bx

adc dx,cx ;sum in dx:ax push si

call root ;root in ax pop si

mov n_amnt,ax ;amount of normal vector finished endm

The first part of this macro calculates the normal vector itself. Then, every individual vector component from the difference of two products is formed. This must follow the definition of the intersecting product and is stored in array n. The second part next calculates the

amount of the normal vector in which all the components are squared and then added. This sum is reduced and the result is saved in n_amt. The last step executes the light macro:

PCPCunderground

The get_normal macro is part of the 3DASM.ASM file on the companion CD-ROM

PCPCunderground

The light macro is part of the 3DASM.ASM file on the companion CD-ROM

light macro ;determines brightness of an area mov ax,n[0]

imul l[0] ;light vector * normal vector mov bx,ax ;form sum in cx:bx

mov cx,dx mov ax,n[2] imul l[2] add bx,ax adc cx,dx mov ax,n[4] imul l[4]

add ax,bx ;scalar product finished in dx:ax adc dx,cx

idiv l_amnt ;divide by l_amnt mov bx,n_amnt ;and by n_amnt cwd

shld dx,ax,5 ;values from -32 bis +32 shl ax,5d

mov bp,startpoly ;prepare addressing of surface color idiv bx ;division by denominator

inc ax or ax,ax

js turned_toward ;if cos à positive -> turned away from the light xor ax,ax ;thus, no light

turned_toward:

sub b polycol,al ;cos<0 -> add to primary color endm

After the scalar product is formed between the normal vector and the (constant) light vector (in l), the amount of the (equally constant) light vector is divided by the amount of the normal vector and the angle between both of the vectors is determined. The result of this calculation would normally be between plus and minus one. However, since whole numbers are used here, the interim result is multiplied by 32 before the second division so a value range of -32 to +32 is reached.

If the result is positive, AX is set to zero and the basic color is retained. If the result is negative, this value is subtracted from the basic color and, as a result, the amount is added. The completely calculated lightness factor is then located in variable PolyCol, which is used as a filling color by FillPol.