-Writing a Texture class- |
OK. You want to load textures, and and do cool effects with them. But each time you want to do some project, you find yourself recoding the texture loader over and over again. And even this would be permissible for simple demos. What about when you want to multitexture? What about state sorting? After finishing this texture class, you will simply find yourself writing extensions, and not reinventing the wheel. OpenGL extensions, etc. will be easier to use and code. Without furthur ado,let us launch into the code. We will be supporting three formats, BMP, TGA and the Quake2 WAL. |
As before, we will dicuss briefly why we are doing this, because if this wasn't of much use, you should probably not read the rest of this tutorial. We do this for several reasons, a few of them listed below. |
|
OK. By now we all agree that this class is a good thing to have around. |
We will now take a look at the declarations of the texture class. |
//This will be the structure that will hold the basic image data that //we will give to OpenGL TTextureImage=record ImageData:array of GLubyte; BPP:GLuint; Width,Height:GLuint; TexID:GLuint; end; |
We next declare the header that is used in uncompressed TGAs. We will declare it as a const. |
const TGAHeader:TTGAHeader=(0,0,2,0,0,0,0,0,0,0,0,0); //Come on, are you telling you dont want to multitexture ? MultiTextureAvailable:boolean=true; WAL_Brightness:integer=0; |
The use of the other two constants will become clear when I write the respective loading routines. |
TTexture=class private protected fTextureData:TTextureImage; fMIPMapping:boolean; fName:string; function GetID:GLuint; public function LoadTexture(aFilename:string):boolean; function LoadTGA(aFileName:string):boolean; function LoadBitmap(aFilename:string):boolean; function LoadWAL(aFilename,PaletteFilename:string):boolean; function LoadJPEG(aFilename:string):Boolean; procedure Enable;virtual; procedure Disable;virtual; procedure Transparency; procedure Translucency; procedure LightMap; procedure Opaque; destructor Destroy;override; property ID:GLuint read GetID; property Width:GLuint read fTextureData.Width; property Height:GLuint read fTextureData.Height; property MIPMapping:boolean read fMIPMapping write fMIPMapping; property Name:string read fName write fName; end; |
After that little(?) header, I will explain why I chose to make it this way. First things first. Why are Enable and Disable virtual? As you may know, switching states in OpenGL, especially texture states, is very expensive. Currently, all these routines do are call glBindTexture. Later, when you have a complicated scene graph, you'll definitely want to optimize texture state changes. Descendants of this TTexture class can use Enable and Disable to notify a manager of a state change so that other polys with that texture dont have to switch states. |
OK. The TGA loader first. |
function TTexture.LoadTGA(aFileName:string):boolean; var hdr:TTGAHeader; useful:array[0..5] of byte; BytesPerPixel, ImageSize, temp, imagetype:GLuint; f:file; i:GLuint; |
Declare the header first, along with an array to hold useful stuff. Add miscellaneous variables as and when required. |
begin result:=false; imagetype:=GL_RGBA; if not FileExists(aFileName) then exit; Assign(f,aFileName); try Reset(f,1); BlockRead(f,hdr,SizeOf(hdr)); if not CompareMem(@hdr[0],@TGAHeader[0],SizeOf(hdr)) then exit; BlockRead(f,useful,SizeOf(useful)); except exit; end; |
Read in the header, and if everything is hunky-dory, then read the 6 useful bytes into the previously declared array. |
fTextureData.Width:=useful[1]*256 + useful[0]; fTextureData.Height:=useful[3]*256 + useful[2]; if (fTextureData.Width<=0)or (fTextureData.Height<=0) or ((useful[4]<>24) and (useful[4]<>32)) then exit; |
Calculate the width and height of the TGA. Also, check if the BPP(Bits Per Pixel) is anything other than 24 or 32. We dont support anything else, so we get out as quickly as we can. |
fTextureData.BPP:=useful[4]; BytesPerPixel:=fTextureData.BPP div 8; ImageSize:=fTextureData.Width*fTextureData.Height*BytesPerPixel; try SetLength(fTextureData.ImageData,ImageSize); except exit; end; try BlockRead(f,fTextureData.ImageData[0],ImageSize) except fTextureData.ImageData:=nil; exit; end; |
Now we have all the required info to read the actual image information. We set the size of the array and read the image data in. |
i:=0; while (i < ImageSize) do begin temp:=fTextureData.Imagedata[i]; fTextureData.Imagedata[i]:=fTextureData.Imagedata[i+2]; fTextureData.Imagedata[i+2]:=temp; Inc(i,BytesPerPixel); end; Close(f); |
What we do here is to swap the R and B components of each quad. This is because the TGA is stored as BGRA quads, and we require RGBA's. After we have done this we close the file. We have no furthur use for it. |
glGenTextures(1,@fTextureData.TexID); glBindTexture(GL_TEXTURE_2D, fTextureData.TexID); if not fMIPMapping then begin if fTextureData.BPP=24 then imagetype:=GL_RGB; glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); glTexImage2D(GL_TEXTURE_2D,0,imagetype,fTextureData.Width,fTextureData.Height,0,imagetype,GL_UNSIGNED_BYTE,@fTextureData.ImageData[0]); end else begin glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); if fTextureData.BPP=24 then imagetype:=GL_RGB; gluBuild2DMipmaps(GL_TEXTURE_2D, 3, fTextureData.Width, fTextureData.Height, imagetype ,GL_UNSIGNED_BYTE, @fTextureData.ImageData[0]); end; |
We
use glGenTextures to retrieve an ID for our texture, and depending upon
the colordepth of the input image, we provide image data either as RGB or RGBA. We also generate mipmaps for the texture if the fMipMapping flag is set |
Here's where we load bitmaps. I'm not going into much detail here, since most of it is a lot like the previous loading. |
function TTexture.LoadBitmap(aFilename:string):boolean; var FileHeader: BITMAPFILEHEADER; InfoHeader: BITMAPINFOHEADER; Palette: array of RGBQUAD; BitmapFile: THandle; BitmapLength: LongWord; PaletteLength: LongWord; ReadBytes: LongWord; Front: ^Byte; Back: ^Byte; Temp: Byte; I : Integer; Width, Height : Integer; pData : Pointer;begin result :=false; BitmapFile := CreateFile(PChar(aFilename), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0); if (BitmapFile = INVALID_HANDLE_VALUE) then exit; // Get header information ReadFile(BitmapFile, FileHeader, SizeOf(FileHeader), ReadBytes, nil); ReadFile(BitmapFile, InfoHeader, SizeOf(InfoHeader), ReadBytes, nil); // Get palette PaletteLength := InfoHeader.biClrUsed; SetLength(Palette, PaletteLength); ReadFile(BitmapFile, Palette, PaletteLength, ReadBytes, nil); if (ReadBytes <> PaletteLength) then exit; Width := InfoHeader.biWidth; Height := InfoHeader.biHeight; BitmapLength := InfoHeader.biSizeImage; if BitmapLength = 0 then BitmapLength := Width * Height * InfoHeader.biBitCount Div 8; // Get the actual pixel data GetMem(pData, BitmapLength); ReadFile(BitmapFile, pData^, BitmapLength, ReadBytes, nil); if (ReadBytes <> BitmapLength) then exit; CloseHandle(BitmapFile); // Bitmaps are stored BGR and not RGB, so swap the R and B bytes. for I :=0 to Width * Height - 1 do begin Front := Pointer(Integer(pData) + I*3); Back := Pointer(Integer(pData) + I*3 + 2); Temp := Front^; Front^ := Back^; Back^ := Temp; end; fTextureData.Width:=Width; fTextureData.Height:=Height; fTextureData.ImageData:=pData; glGenTextures(1,@fTextureData.TexID); glBindTexture(GL_TEXTURE_2D, fTextureData.TexID); if not fMIPMapping then begin glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,fTextureData.Width,fTextureData.Height,0,GL_RGB,GL_UNSIGNED_BYTE,@fTextureData.ImageData[0]) end else begin glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); gluBuild2DMipmaps(GL_TEXTURE_2D, 3, fTextureData.Width, fTextureData.Height, GL_RGB,GL_UNSIGNED_BYTE, @fTextureData.ImageData[0]); end; result :=true; end; |
As you can see, there isnt much difference after we have loaded the image. Now we come to a format worth loading, the Quake2 WAL It's an extermely handy format to have around, since there are lot of great textures in the quake2. PAK. But, of course it has its own peculiarities. The quake2 WAL uses only 256 colors, from a palette common to all textures. So, what we will be doing is to load the palette file, and convert the corresponding color indices into RGB values, before we, as usual hand over the data to OpenGL. |
function TTexture.LoadWAL(aFilename,PaletteFilename:string):boolean; var TexFile,PalFile:file; header:TWALHeader; tempImageData:array of GLubyte; i,CurIndex:integer; Palette:array[0..255,0..2] of byte; |
The function will also need to load the palette, so we are supplying both paths. As before, we declare the required headers. We will store the image data temporarily in tempImageData before transferring it to our main ImageData array. The Palette aray declared merits some attention. In a palette, colors are stored as RGB triads. There are 255 such triads. |
result:=false; if not FileExists(aFilename) then exit; if not FileExists(PaletteFilename) then exit; Assign(TexFile,aFilename); Reset(TexFile,1); Assign(PalFile,PaletteFilename); Reset(PalFile,1); |
Open both the files after making sure they exist. |
BlockRead(TexFile,header,SizeOf(header)); fTextureData.Width:=header.Width; fTextureData.Height:=header.Height; SetLength(tempImagedata,header.Width*header.Height); SetLength(fTextureData.ImageData,header.Width*header.Height*3); BlockRead(TexFile,tempImageData[0],header.Width*header.Height); BlockRead(PalFile,Palette,SizeOf(Palette)); |
Read in the header of the WAL file, and allocate memory appropriately. After allocating memory, we read in the color indices from the texture file. Remember, we still have to convert to RGB values before giving it to OpenGL.We also read in the entire palette in one go. |
if WAL_Brightness<>0 then for CurIndex:=0 to 255 do begin if Palette[CurIndex,0]+WAL_Brightness<255 then Inc(Palette[CurIndex,0],WAL_Brightness); if Palette[CurIndex,1]+WAL_Brightness<255 then Inc(Palette[CurIndex,1],WAL_Brightness); if Palette[CurIndex,2]+WAL_Brightness<255 then Inc(Palette[CurIndex,2],WAL_Brightness); end; |
Most textures loaded directly look really dark (I mean luminance-wise ;) so we brighten up the palette by the requested amount. |
i:=0; for CurIndex:=0 to header.Width*header.Height do begin fTextureData.ImageData[i]:=Palette[tempImageData[CurIndex]][0]; Inc(i); fTextureData.ImageData[i]:=Palette[tempImageData[CurIndex]][1]; Inc(i); fTextureData.ImageData[i]:=Palette[tempImageData[CurIndex]][2]; Inc(i); end; Close(TexFile); Close(PalFile); |
Now, using the indices from tempImageData, we index into the palette and recover the RGB values and put them in the main ImageData array. We now have the (brightened) RGB values, and are ready to hand it over to OpenGL. |
glGenTextures(1,@fTextureData.TexID); glBindTexture(GL_TEXTURE_2D, fTextureData.TexID); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,fTextureData.Width, |
Now we come to the last file format our Texture class is to support, JPG's. You might this is hard, but its surprisingly similar to loading BMPs. Wathc and learn :) |
function TTexture.LoadJPEG(aFilename: String):Boolean; var Data : Array of LongWord; W, Width : Integer; H, Height : Integer; BMP : TBitmap; JPG : TJPEGImage; C : LongWord; Line : ^LongWord; begin result :=false; JPG:=TJPEGImage.Create; try JPG.LoadFromFile(aFilename); except exit; end; // Create Bitmap BMP:=TBitmap.Create; BMP.pixelformat:=pf32bit; BMP.width:=JPG.width; BMP.height:=JPG.height; BMP.canvas.draw(0,0,JPG); // Copy the JPEG onto the Bitmap Width :=BMP.Width; Height :=BMP.Height; SetLength(Data, Width*Height); For H:=0 to Height-1 do begin Line :=BMP.scanline[Height-H-1]; // flip JPEG for W:=0 to Width-1 do begin c:=Line^ and $FFFFFF; // Need to do a color swap Data[W+(H*Width)] :=(((c and $FF) shl 16)+(c shr 16)+(c and $FF00)) or $FF000000; // 4 channel. inc(Line); end; end; fTextureData.Width:=Width; fTextureData.Height:=Height; fTextureData.ImageData:=addr(Data[0]); glGenTextures(1,@fTextureData.TexID); glBindTexture(GL_TEXTURE_2D, fTextureData.TexID); if not fMIPMapping then begin glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,fTextureData.Width,fTextureData.Height,0,GL_RGBA,GL_UNSIGNED_BYTE,fTextureData.ImageData) end else begin glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,MINFILTER); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,MAGFILTER); gluBuild2DMipmaps(GL_TEXTURE_2D, 3, fTextureData.Width, fTextureData.Height, GL_RGBA,GL_UNSIGNED_BYTE, fTextureData.ImageData); end; BMP.free; JPG.free; result :=true; end; |
That was so easy because we used a TJPEGImage to convert the image into a TBitmap, from where it was smooth sailing, since we have already gone throught the rigmarole of writing a BMP loader. |
That's that. We can now relax. The main part of the work is over. The rest of the methods of TTexture are pretty much basic, so I'm not going to go into that here.What we will do, however, is to see how easy this class makes texture loading. |
//create the texture tex:=TTexture.Create; //Load the TGA tex.LoadTexture('fan3.TGA'); //Set up the blend function tex.Transparency; //Enable it!!! tex.Enable; |
Well?!!! What do you think? Is that easy or what? Next lesson will be a tut about a camera class. No more gluLookAt :) |
>>Download the tutorial
source |