|====================================| | | | TELEMACHOS proudly presents : | | | | Part 4 of the PXD trainers - | | | | 3D Vector engine | | Differnt poly-fills | | | |====================================| ___---__--> The Peroxide Programming Tips <--__---___ <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> Intoduction ----------- As promised in my last tuturial this one will be on different types of fills that we can add to our basic 3D engine. If you have not read my previous tuturial (pxdtut3.zip) I suggest you get a copy of it as we'll use some code we discussed in it. This tuturial is about one (two) week late. This is because some work came up - involving a trip to a danish island with about 60 kids in the age 4-6 years, arranging a local sailing tournement and eating lots of sweets while drinking softdrinks and watching video. So as you see I'm a busy man 8) But well.. better late than never - here goes : - Z-shading : Bad type of flatshading... but I'll tell you anyway... - Flat shading according to moving lightsources - Gouraud shading according to moving lightsources - Texturemapping - Environmentmapping / Fake Phong.... Z-SHADING ----------- This one you can implement in about 2 minutes in the 3D object engine we build in the last tuturial. The theory behind Z-shading is that a face usually darkens when moving farther away from the light. As we store the center Z-values for all our faces in a variable called Centers we can easily determine the distance from the light to the center of the face. Now we convert this Z-value into a value that lies in the colorspan we use for shades. As you have probably noticed this means that the lightsource is placed at a fixed position somewhere outside the screen pointing straight into the screen. Take a look at this piece of code to see how it could be implemented : Procedure BadFlatShade(where : word; minZ, maxZ, Num_of_shades : integer); {********************************************************************} {** MinZ, MaxZ : What is the minimum and maximum Z-values of the **} {** faces that is to be drawn ? You COULD set theese **} {** values so that minZ is the minimum Z-val of the **} {** entire object and MaxZ the maximum value. However**} {** consider the fact that half of the objects faces **} {** is removed by hidden face removal. So, if you **} {** want to have bigger diference on the shown faces **} {** just set minZ to minimum object Z-value and MaxZ **} {** to the Z-value of the CENTER of the object. **} {** Experiment!! **} {** Num_of_shades : shades used = color 0 to Num_of_shades **} {********************************************************************} var taeller : integer; X1,Y1,X2,Y2,X3,Y3,X4,Y4 : integer; color : byte; polynr : integer; normal,span : integer; shade : real; begin for taeller := 1 to Num_of_faces do begin polynr := OrderTable[taeller]; X1 := translated[faces[polynr].P1].X; Y1 := translated[faces[polynr].P1].Y; X2 := translated[faces[polynr].P2].X; Y2 := translated[faces[polynr].P2].Y; X3 := translated[faces[polynr].P3].X; Y3 := translated[faces[polynr].P3].Y; X4 := translated[faces[polynr].P4].X; Y4 := translated[faces[polynr].P4].Y; {***************** Z-shading *****************} span := ABS (minZ-maxZ); {Z span of object} shade := (centers[taeller] div 4 + ABS(minZ)) / span; color := Num_of_shades - round(Num_of_shades*shade); {*******************************************************} {******* HIDDEN FACE REMOVAL - YES, THAT EASY ;) *******} {*******************************************************} {Z-Comp of normal to 2d-polygon} normal := (Y1-Y3)*(X2-X1) - (X1-X3)*(Y2-Y1); if (normal < 0) then {pointing towards us} Polygon(X1,Y1,X2,Y2,X3,Y3,X4,Y4,color,where); {*******************************************************} {*******************************************************} {*******************************************************} end; end; NICE FLATSHADING - THE THEORY BEHIND THE LIGHTVECTOR / DOT-PRODUCT ------------------------------------------------------------------- Now... while the above shown piece of code works quite allright on simple objects as fx. cubes it will not produce an acceptable shading for more complex objects. And the situation with the fixed lightsource is'nt to good either. So we'll have to find some better way of shading our objects. Remember the facenormals we discussed in the last tuturial - I told you they could be used for shading and I did not lie :) To refresh your memory I'll just show you the formulas again : Xnormal=(P2.Y-P1.Y)(P1.Z-P3.Z)-(P2.Z-P1.Z)(P1.Y-P3.Y) Ynormal=(P2.Z-P1.Z)(P1.X-P3.X)-(P2.X-P1.X)(P1.Z-P3.Z) Znormal=(P2.X-P1.X)(P1.Y-P3.Y)-(P2.Y-P1.Y)(P1.X-P3.X) Now, if we define the lightsource as a vector also : LightVect : RealPointT; then the direction of the lightsource can be set by defining LightVect.X, LightVect.Y and LightVect.Z Now, the amount of light that shines on an object is determined by the angle between the lightsource and the facenormal. Take a look at this picture.. normal | / <-- the lightsource | / |_ A / | \/ | / | --------------------- <-- a face in the object If the light shines directly on the face a maximum amount of light should be shown, and the angle A would be 0 degree. The greater the angle, the darker shade. Now is the time for another new term - UNITVECTOR. A unitvector is simply a vector with the length 1. Any vector can easily be made a unitvector by dividing all three vectorcomponents by the entire vector length. The length of a vector is calculated by : length = SQRT(X*X + Y*Y + Z*Z); Obviously it's WAY to slow to : 1) Calculate the facenormal. 2) Calculate the length of the normal (SQRT is SLOOOW) 3) Divide all three components by length So what we'll do is to calculate the facenormals and make them unitvectors ONE time when setting up the object. Then we rotate these unitvectors for each frame. This is of cause not entirely true. If we made our facenormals unit- vectors during setup we would have to store all vectorcomponents as reals. And then our rotation routine could not handle them. So we store the unit- vectors as fx. 8.8 fixed point values. As all vectorcomponents range from 0 to 1 we could easily store them as 1.15 fixed point values... but if we store them as 8.8 it'll make environmentmapping easier... :) As with the object itself, store the original normalvectors in a buffer and rotate the buffer into another buffer from which you do all the calculations on the rotated normals - this way you'll not lose precision during rotation. Now, if we define both the facenormal and the lightvector as UNITVECTORS we can use a new formula called the dot-product to calculate the angle between them. Actually the dot-product returns the cosinus values of the angle - but this is PERFECT! As cos ranges from 0 to 1 and being 1 at 0 degrees and 0 at 90 degrees we can just multiply the cosinus value by the numbers of shades we want in our object. The dot-product : dot := (Normal.X*Lightvect.X) + (Normal.Y*Lightvect.Y) + (Normal.Z*Lightvect.Z); Now go implement this in your engine.... It COULD look like this : Procedure NiceFlatShade(where : word; Num_of_shades : integer); var taeller : integer; X1,Y1,X2,Y2,X3,Y3,X4,Y4 : integer; color : byte; polynr : integer; normal : integer; shade : real; Nx,Ny,Nz : real; dot : real; begin for taeller := 1 to Num_of_faces do begin polynr := order[taeller]; X1 := translated[faces[polynr].P1].X; Y1 := translated[faces[polynr].P1].Y; X2 := translated[faces[polynr].P2].X; Y2 := translated[faces[polynr].P2].Y; X3 := translated[faces[polynr].P3].X; Y3 := translated[faces[polynr].P3].Y; X4 := translated[faces[polynr].P4].X; Y4 := translated[faces[polynr].P4].Y; {*******************************************************} {******* HIDDEN FACE REMOVAL - YES, THAT EASY ;) *******} {*******************************************************} {Z-Comp of normal to 2d-polygon} normal := (Y1-Y3)*(X2-X1) - (X1-X3)*(Y2-Y1); if (normal < 0) then {pointing towards us} begin {************************************************************} {** LAMBERTS FLATSHADING ACCORDING TO MOVING LIGHTSOURCE **} {************************************************************} Nx := RotNormals[polynr].X / 256; Ny := RotNormals[polynr].Y / 256; Nz := RotNormals[polynr].Z / 256; dot := (Nx*Lightvect.X) + (Ny*Lightvect.Y) + (Nz*Lightvect.Z); if (dot > 1) or (dot < 0) then dot := 0; color := Round(dot * Num_of_shades); Polygon(X1,Y1,X2,Y2,X3,Y3,X4,Y4,color,where); end; {*******************************************************} {*******************************************************} {*******************************************************} end; end; GOURAUD SHADING ---------------- After Flatshading comes Gouraud shading. Gouraud shading is the first shading type which does not have a constant color for a face in the object. First thing to say is, that gouraud shading is based on linear interpolation of colorvalues/lightintensities. We still draw the polygon scanline pr scanline, but now we need more than just two X-values to draw the line. We also needs two colors per line. Namely the starting color and the ending color. When we draw our horizontal line, we use fixed point math to step through the colorspan the line consist of. Take a look at this gouraud line drawer : PROCEDURE GouraudHorline(xbeg,xend,y:integer; c1,c2:byte;where : word); var coloradd : integer; begin if (Xend-Xbeg) <> 0 then coloradd := ((c2-c1) shl 8) div (Xend-Xbeg); asm mov bx,[xbeg] mov cx,[Xend] inc cx sub cx,bx { length of line in cx } mov es,Where { segment to draw in } mov ax,[y] { Ypos of the line } shl ax,6 mov di,ax shl ax,2 add di,ax { y*320 in di (offset) } add di,bx { add x-begin } xor ax,ax mov al,[C1] shl ax,8 {colorstart fixed-p 8.8 } @again: mov es:[di],ah {ah = real value of fixed-p color (ah = ax shr 8 ) } inc di dec cx add ax,[coloradd] cmp cx,0 jne @again end; end; Thats all well and good..... but how do we calculate C1 and C2 for each horizontal line ? Well.. for a start we'll calculate the color for each of the 4 points in our polygon. This is done in a way similar to the way we calculated the face color in flat shading - by calculating the dot-product of the lightvector and the POINT-normal. Now, as you all know, one can't calculate a normal to a point. It has to be a plane. So we'll have to think of our own way of defining the term POINT-normal. It has been decided that the normal to a point is calculated by taking the average of the FACE-normals in which the point is included. You could calculate the 4 normals pr. face each frame - or you could calculate them once during setup and then rotate them like the face normals. Suit yourself. Remember! The POINT-normals has to be unitvectors. The fact that the FACE-normals are unitvectors does NOT mean that the calculated POINT-normals will be too. So, you'll have to make them unitvectors for each frame. All this means that the solution with the POINT-normals being rotated is probably the best/fastest :) When you got the 4 color values for the 4 points of the polygon you scan the edges as with the normal polygon routine I showed you in the last tut. But as you scan along the edges you shade them at the same time - much like the GouraudHorLine procedure.... the difference is just that the line you shade is'nt horizontal. In the end, you'll have two variables filled with the info needed to draw our gouraud shaded polygon : polygon[1..200,1..2] : the X-values to draw the lines between color[1..200,1..2] : the starting and ending colors for each HorLine I'll just show you the new ScanPolySide procedure : Procedure ScanPolySide(x1,y1,x2,y2:integer;c1,c2 : byte); { This scans the side of a polygon and updates the poly variable } {updates the colors variable for gouraud shading} VAR temp:integer; xfixed,xinc,x:integer; loop1:integer; dcol : integer; color : integer; BEGIN if y1=y2 then exit; if y2(ytopclip)) and (loop1<(ybotclip)) then BEGIN x := xfixed shr 7; if (xpolygon[loop1,2]) then begin polygon[loop1,2]:=x; colors[loop1,2] := color shr 8; end; END; xfixed:=xfixed+xinc; color := color + dcol; END; END; Now... that was'nt too hard ehh ?? Put this new ScanPolySide into your polygon routine and change the call to HorLine to GouraudHorLine with the parameters : GouraudHorline(polygon[loop,1],polygon[loop,2],loop, colors[loop,1], colors[loop,2]); That should do the trick - a nice Gouraud shaded polygon. Check out the sample program if you have any trouble coding this effect. TEXTUREMAPPING --------------- Now, the first thing I would like to say in this section is that the texture mapping we'll do here differs ALOT from the one I wrote about in tuturial 1. The difference is that while the texturemapping in tuturial 1 had correct perspective this type won't. The reason we could do perspectively correct texturemapping in tut 1 was that all the polygons we mapped had constant Z-values for each VERTICAL scanline. That means that all the perspective calculations only needs to be calculated ONCE pr scanline. We could do the same thing with polygons with constant Z-values for each HORIZONTAL scanline. But the polygon we're mapping today is often rotated so there is NO constant Z-values. Therefor heavy calculation is needed for each pixel in the polygon to calculate the u,v coordinate in the texture. So, what we'll do is called a linear texturemapping. It works fine on the kind of objects that is seen in demos 'cause they often move to fast for the viewer to see the perspective errors. We'll use the same polygon drawing routine as for all the other fills. The only diffence lies in the ScanPolySide and in the horizontal line drawer - U probably allready guessed that :) When calling the TextureMappedPolygon routine we assign 4 texture coordinates - one to each point in the polygon. We call these coordinates : U1,V2, U2, ... , V4 When scanning the four sides in the polygon we also store two texture coordinates for each horizontal line. The implemention of this is VERY much like the one in gouraud shading - only with one more value to increment. Check out the sample program if you have any trouble. Now for the TextureMappedHorline routine ;) It has to scan through the texturemap while drawing the horizontal line. It is quite easy to do, using fixed point math : PROCEDURE TextureMapHorline(xbeg,xend,y,u1,v1,u2,v2:integer;source,dest : word); var DeltaX : integer; DeltaY : integer; begin If (Xend-Xbeg) <> 0 then begin DeltaX := ((u2-u1) shl 7) div (Xend-Xbeg); DeltaY := ((v2-v1) shl 7) div (Xend-Xbeg); { 9.7 fixed-p} DeltaX := DeltaX + DeltaX; DeltaY := DeltaY + DeltaY; {now 8.8 fixed-p :) } end else begin DeltaX := 0; DeltaY := 0; end; asm push ds mov ax, [source] mov ds,ax mov bx,[xbeg] mov cx,[Xend] inc cx sub cx,bx {cx = length of line} mov es,dest mov ax,[y] shl ax,6 mov di,ax shl ax,2 add di,ax add di,bx {es:[di] start of line} mov ah,byte[v1] {8.8 fixed-p value of YTexturePos - for easy ofs calc} mov al,byte[u1] mov si,ax {si = starting offset in texture } mov dh,al {8.8 fixed-p value of XTexturePos - for easy ofs calc} @again: movsb {draw byte} add ax,[DeltaY] {advance in texturemap} add dx,[DeltaX] {advance in texturemap} mov bh,ah {bh = Ypos * 256 } mov bl,dh {bl = Xpos_fixed / 256 = Xpos_real} mov si,bx {BX = Ypos_real * 256 + Xpos_real = offset} dec cx cmp cx,0 jne @again {are we finished ?? } pop ds end; end; As you see our Texture has to be placed in a 256X256 orientated coordinate system. This does not mean that the texture HAS to be 256X256, but it must be stored with 256 bytes pr line. This means you probably has to rewrite your image loader a little - and if you normally use a virtuel screen with the size 320X200 you have to allocate a little more memory. 320X200 = 64000 bytes while 256X256 = 64Kb... NOT the same :) ENVIRONMENT MAPPING / PHONG SHADING ------------------------------------ What is environement mapping ? Well.. environment mapping is an effect where an image is reflected on a shiny surface. The implementation is a straight texturemap - the only thing there is to environment mapping is calculating the 4 texturecoordinates needed for our polygon drawer. In ordinary texturemapping we allways set these to the corners of the image. This is not the case in environmentmapping. How DO we calculate the coordinated then ?? Well... it's allmost TOO easy. We simply use the POINT-normals again. As we have stored these in 8.8 fixed point we know all vectorcomponents ranges from -256 to 256. We just use the X and Y vectorcomponents for U and V TextureCoodinates needed for the point, so the only problem is to translate the value so it lies in the range 0 to 256. The solution is simple - divide the values by 2 and add 128 to the result. Voila! U and V coordinates for the Texturemapping routine is found.. What about this phong shading then ? Well.. for a start I'll just briefly explain the working of REAL phong shading. As with all other types of shading the light-intensity is calculated by the dot-product. In Flatshading we did ONE dot-calculation to find the color. In gouraud we did FOUR dot-calculations to find the color at each of the 4 points. In Phong shading you do a dot-calculation ON EVERY SINGLE PIXEL IN THE POLYGON! Ie. the light-intensity is calculated PRECISELY for every single pixel. So for each pixel we have to 1) Calculate the Normal to the point by making a plane of the neighbour pixels. 2) Make this normal a unitvector 3) Take the dot-product of the lightsource and the normal 4) Plot the pixel. Well.. this don't sound like real time to me :) Numerous approximations to this routine has been made, but the easiest one is to simple do an environmentmap of a Phong-map. A phong map is simply a picture of a lightsource that we calculate. By using the environment mapping methode discussed above it is possible to do something that looks pretty much as phong shading. Check the sample program for the routine for calculating such a map. This is BTW not mine - took it somewhere but can't remember where.. Thank you whoever you are :) MULTIPLY LIGHTSOURCES ----------------------- Oh yeah.. before I forget. I think I promised to tell you how to implement multiple moving lightsources. And I'm not a man who breaks my word :) First thing first : multiple lightsources. This is incredibly easy to implement. Instead of just having ONE lightvector you make an array with as many as you wish. When calculating the color you just add the lightintensities for all the lightsources together before multiplying with the number of shades. Of cause you has to make sure the intensity does not get bigger than 1. As for moving lightsources - as the routines just use the lightvector in color-calculation you can freely move it around by assigning new values to it for each frame. Just remember to make it a unitvector. OPTIMIZATIONS -------------- Now you got all the formulas you need to make nice fills/shadings. But it is up to you to optimize them. The goal of this tuturial is to make clear code that is easy to understand - in a tuturial I think that is more important than some highly optimized assembler code being thrown at you. Some people has mailed me telling me that my code was unoptimized... that I needed lots of stuff... like clipping, and more direct camera control. That is correct - but as mentioned before... this is not meant to be an example of my coding skills :) I have left many things out for the sake of easy understanding. So now you know HOW and WHY the things work... it is then up to you to optimize them - to make that engine that is just a LITTLE bit faster than everyone elses.. To see if you can beat Karl/Noone :) I suggest the following optimizations : 1) Clipping : easy in the normal polygon routine. In Gouraud and texturemap you'll have to calculate new starting u,v / color values.. 2) Speed : More assembler, draw two bytes at a time, use another polygon drawer. Rotate the point-normals instead of calculating them. 3) Triangles : Make triangle versions of ALL the drawing routines.. often used in more complex 3d meshes. LAST REMARKS ------------- Well, that's about all for now. Hope you found this doc useful - and BTW : If you DO make anything public using these techniques please mention me in your greets or where ever you se fit. I DO love to see my name in a greeting :=) This completes to 3D tuturial serie. Hope I explained it well enough for you to get the grasp of it :) I myself is very pleased with these last two tuturials as I think they assembles all the most nessesary 3D theory in only two textfiles. Also they give the reader examples and explenations of things that I myself has never seen. Fx. gouraud and environmentmapping. I have never seen any good docs on these subjects - somehow people allways stopped writing tuturials when reaching these subjects. But what now ?? If you have any good ideas for a subject you wish to see a tuturial on please mail me. If I like the idea (and know anything about it :) ) I'll write a tut on it. In near future I might write a small tuturial on how to use interrups for various programming problems. But, well.. after that I don't know. Keep on coding... Telemachos - June '97.