Showing posts with label OpenGL Texture DXT Compression. Show all posts
Showing posts with label OpenGL Texture DXT Compression. Show all posts

Sunday, October 16, 2011

Compressor, Part 2/2

What the hell we're we doing again? Oh yes, texture compression. In short, the main advantages are less storage space, less (video)memory space, faster loading/streaming, and if you're a lucky guy, a (little) performance boost cause less bandwidth is required. Reason enough to implement compressed texture formats... but how?

When you target OpenGL, you can encode pixelData into the DXT1, DXT3 or DXT5 format. So first (1.) encode, then (2.) store it in a file (a DDS for example), (3.) load the file later on, and finally (4.) send the (compressed) pixelData to the videocard. Sounds pretty complicated, but we have some nifty tools.

==========================================================
1. The Creation
==========================================================
Load a (raw) image, such as a bitmap or TGA file. Then encode it to a DXT format (or BC in case you target DirectX applications). So... we have to dig in compression algorithms now, right? Of course not. Don't reinvent the wheel, be lazy, and make good use of existing tools.

You can download an existing convertor tool or try to find a plugin for your favourite painting program. There are plugs for Photoshop, Paint Shop Pro, Gimp, Paint.NET... These plugins typically convert a common image format to a DDS file. You can skip step 1 and 2 of this tutorial now, peace of cake. Have a good look though. Not all convertors have equal quality and export options.

When my mobile research-lab was crunching on DDS, I had an old version of Paint Shop Pro though. Just ordered a new version (PSP X4), but no cool plugins at that time. I tried some other exporters, but they usually gave wrong output. Inverted RGB channels, flipped images, Lena changing into a guy, et cetera. Some claimed ATI has a good quality exporter, so I tried this instead: ATI Compress


Too bad, this is not a ready-to-use convertor, not even a command-line tool(I hate those, too lazy to type). It's a DLL you can include in your own programmed tools. Normally I would pass, but with the lack of good exporting tools so far, I gave it a try. And I got to say, it wasn't that hard at all (getting into C++ again took me more time). The ATI library is pretty straight forward. It has a function to compress raw image data, and some basic utilities to read/write DDS files.

1.- Load input image (bitmap, buttmap, tga, png, ...)
2.- Generate Mip-Maps (if you like)
3.- Let the ATI Compress library generate compressed pixelData (for each MipMap level)
4.- Store it as a DDS file (or your own custom format)

Mip-Maps ?!
In case you never heard of them, Mip-Maps are smaller ("blurred") variants of your texture. Pretty simple, half the size until you reach a 1x1 resolution. So a 256x256 image gets a 128, 64, 32, 16, 8, 4, 2 and 1 variant. But... why would you do that? Well, when looking a textured surface from a distance, one screen-pixel may have to sample from multiple texture-pixels. This can result in pixelated/blocky results, especially for textures with a high-frequency details or pattern such as black-white tiles. When you have mip-maps, the renderer will (automatically) pick a smaller, more blurry, variant to suppress this annoying artifact. A little downside is that mip-maps require extra image space (though the smaller resolution variants really don't take that much).

Sorry if you died from an epileptic attack.

OpenGL / DirectX have functions that generate mip-maps automatically for you. However, generating them takes time! So each time you load a texture that uses mip-mapping, you're wasting even more time with generating them. That's why some image formats (such as DDS) supports the storage of pre-fabricated mip-maps. So instead of rebuilding them each time again, you store smaller variants of the texture ready-to-use in the image file. In other words, it makes the loading times faster.

==========================================================
2. DDS File-Loader
==========================================================
Once your file has been generated somehow, you got to load it again in your app. I chose to use DDS files. Pretty obvious, cause this format supports compressed pixelData, storing prefab mip-maps, and even cubeMaps or layered (3D/Array) textures if you like. On top, it’s a common standard in Gameland nowadays. Monkeys see, monkeys do.

Luckily the DDS format is fairly easy. Mainly cause it doesn’t store its pixels in a wacky way. The array of pixels is stored exactly as OpenGL or DirectX expects it. This makes DDS fast to read, as you don’t have to swap bits or do other tricks before you load it to the GPU. It’s 1 on 1. But wait… D-D-S… stands for “Direct-Draw-Surace”… Isn’t that DirectX slang?? So we can’t use DDS in OpenGL apps?!

It is a DirectX thing indeed, and yep, you need a header file to get things working. But don’t worry. It’s not that your OpenGL program changes into a malformed Siamese twin with DirectX. Download the DirectX SDK, or if you are a Delphi user, you can use this: Clootie DX Pascal headers
Then include “DirectDraw.h / pas” in your program. Done.

// Delphi code.
function TEX_LoadFile_DDS( filename : string ) : TTexData;
var
ddsd :_DDSURFACEDESC2;
fileCode : array[0..3] of char;
factor : integer;
bufferSize : integer;
readBufferSize: integer;
pFile : THandle;
readBytes : Longword;
begin
{ Open file... Calling Powdered toast man }
pFile := CreateFile(PChar(filename), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
if (pFile = INVALID_HANDLE_VALUE) then begin
showMessage( 'DDS Load Error: Cannot open file ' + filename );
Exit;
end;

{ Verify if it is a true DDS file. Not made-in-China fake stuff }
ReadFile( pFile, fileCode, 4, ReadBytes, nil);
if (fileCode[0] + fileCode[1] + fileCode[2] <> 'DDS') then begin
showMessage( 'DDS Load Error: file is not a valid DDS file.'#13+filename );
CloseHandle(pFile);
exit;
end;

{ Read surface descriptor.
A struct that tells what we can expect in this file. }
ReadFile( pFile, ddsd, sizeof(ddsd), ReadBytes, nil );

case ddsd.ddpfPixelFormat.dwFourCC of
FOURCC_DXT1 :
begin
// DXT1's compression ratio is 8:1
result.outputFormat := GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
result.isCompressed := True;
factor := 2;
end;
FOURCC_DXT3 :
begin
// DXT3's compression ratio is 4:1
result.outputFormat := GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
result.isCompressed := True;
factor := 4;
end;
FOURCC_DXT5 :
begin
// DXT5's compression ratio is 4:1
result.outputFormat := GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
result.isCompressed := True;
factor := 4;
end;
else begin
{ Not compressed. Oh shit, didn't implement that! }
result.isCompressed := False;
showMessage( 'DDS Load Error: Uncompressed format not supported!'+#13 + filename );
CloseHandle(pFile);
exit;
end;
end; // case ddsd.ddpfPixelFormat.dwFourCC


{ How big will the buffer need to be to load all of the pixel data
including mip-maps? }
if( ddsd.dwLinearSize = 0 ) then
begin
showMessage( 'DDS Load Error: dwLinearSize is 0!'+#13 + filename );
CloseHandle(pFile);
exit;
end;
if( ddsd.dwMipMapCount > 1 ) then
bufferSize := ddsd.dwLinearSize * factor else
bufferSize := ddsd.dwLinearSize;

{ Allocate pixel buffer, then read the (compressed)
PixelData (containing 1 or more MipMap levels) from the file }
readBufferSize := bufferSize * sizeof(char); // Calc buffer-size
GetMem( result.pixels, readBufferSize ); // Allocate memory
ReadFile( pFile, result.data^ , readBufferSize, ReadBytes, nil);
CloseHandle(pFile); // Close file

{ More output info }
result.width := ddsd.dwWidth;
result.height := ddsd.dwHeight;
result.numMipMaps := ddsd.dwMipMapCount;

{ Do we have a fourth Alpha channel doc? }
if( ddsd.ddpfPixelFormat.dwFourCC = FOURCC_DXT1 ) then
result.components := 3 else
result.components := 4;
end; // TEX_LoadFile_DDS


Just a shot. Playing around with ingame contrast here...

==========================================================
3. Pumping to the video-card
==========================================================
The final, and probably also most easiest step, is loading the (compressed) pixelData to the video-card. No worries, it’s pretty much the same as generating any other texture. Start with the daily stuff:

glEnable( GL_TEXTURE_2D );
glGenTextures( 1, @resultHandle );
glBindTexture( GL_TEXTURE_2D, resultHandle);
// Texture settings. Using mipmapping here...
glTexParameteri(GL_TEXTURE_2D,, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
glTexParameteri(GL_TEXTURE_2D,, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, );

Next we send the pixelData to the Twilight zone. One little thing to keep in mind is that we might have loaded multiple mipmaps, all in the same array of pixelData. So for each mipmap level, calculate the bytesize and offset in the array.

if textureData.usingCompression then begin
if textureData.outputFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT then
nBlockSize := 8 else
nBlockSize := 16;
{ Size of mipmap level 0 (original size) }
nHeight := textureData.height;
nWidth := textureData.width;
nOffset := 0;

{ Send the compressed mipmap(s) data to the texture }
for i:=0 to data.numMipMaps-1 do begin
if nWidth = 0 then nWidth := 1;
if nHeight = 0 then nHeight := 1;

nSize := ((nWidth+3) div 4) * ((nHeight+3)div 4) * nBlockSize;
glCompressedTexImage2DARB( GL_TEXTURE_2D,
i, //mipmap level
data.outputformat, // DXT1,3,5
nWidth,
nHeight,
0,
nSize,
pointer( integer(data.data) + nOffset)
);
nOffset := nOffset + nSize; // Offset in pixel buffer next time
// Half the image size for the next mip-map level...
nWidth := (nWidth div 2);
nHeight := (nHeight div 2);
end; // for i
end else ...

Texture quality
Last, here's a little Kung-fu trick. Probably you noticed the "Texture Quality" setting in most games. Chose between "Godlike, medium or fucked-up". Obviously, computers with lower-end hardware and/or limited video-memory should pick a lower setting. And you want this option too, don't you? Pretty easy. Just skip the first mipmap level(s) in the loop. This way OpenGL will only deal with smaller textures, as the higher level(s) get disposed again. That's all you need to know for now, Daniel San.

Saturday, September 24, 2011

Compressor, Part 1/2

In case you are a programmer or texture-artist, you probably have heard about DDS files or texture compression. But if you are like me, you may have skipped all those articles. Pfff, compression, who needs that? Bitmaps and TGA files work for me, Pixel-data is pixel-data right? Besides, I've been taught that compression makes the quality worse, and reduces the performance because in the end, your pixels still have to be decompressed to a raw format. So what's the point?

Well forgive my ignorance. Guess I'm still traumatized because 12 years ago, I couldn't figure out why my sprites had jaggy pink edges when using JPEG files. Oh yes, the compression did that. But although the quality / performance arguments are valid, texture compression actually gives some nice possibilities. In this post, I'll write about using DXT compressed textures in your OpenGL program, and the DDS file format that can store this format.


1. For beginners: (de)compression
------------------------------------------------------------------------
For starters, as the name sais, compression is a technique to store (large) buffers of data with some tricks to reduce the size. When it comes to images, we usually have a big array of bytes. In a classic RGB (24 bit) image, each pixel takes 3 bytes. 8 bit for red, 8 bit for green, 8 bit for blue. Some images also have an 8-bit alpha channel, which makes the total size of a pixel 32 bit. Calculating the total buffer size is easy:
.....width * height * pixelSize. 512 x 512 x RGBA8(32 bit) = 1 MB

Cool, but 1 megabyte for a pretty small texture is huge (1 floppydisk!). Older photo camera’s would have their SD cards full after just a few clicks. So, that's why JPEG is often used. JPEG uses Huffman encoding, which is based on the probability a certain piece of data (pixelcolor) occurs. If we only have 20 different colors in an image, we can index the pixelcolors with a 5-bit (=32 possibilities) code. Further compression can be done by giving frequent colors a short bitcode, while rare colors get a longer code. Well, for a way better description, check this.

Smart huh? However, compressing the image has three main disadvantages. First there is usually quality loss. You probably have seen blurry blocks in JPEG images, especially when the color contrast is low. To put it simple, most compressed images NEVER reach the quality of a raw format (such as BMP or uncompressed TGA / PNG). Then again, if the resolution is big enough, compression settings are ok, and/or the small details don't really matter anyway, the loss is acceptable.

Second problem is the decompression-time. It simply takes some more time to convert the cryptic pixelmess to a raw format. Your video-card needs to produce raw (RGB 8-bit) pixeldata first, before the monitor can show it. Compare it to translating a Chinese movie to English. First a translator has to write down the English subtitles or do voice acting before it can be shown on TV. This process takes some time.

A third little problem is the compression itself. Just like decoding, encoding takes some extra effort as well. Luckily computers are fast these days, so you won't really notice. However... it's not fast enough to do many textures on the fly while rendering a game. This is why it usually happens "offline":

1.- Create your image (photograph / paint / capture raw data)
2.- Compress it in whatever file format
3.- Load in your application
4.- Decompress it back to raw-data
5.- Use (render) it.


Usually some quality loss is acceptable.


2. Compression & realtime Graphics
------------------------------------------------------------------------
So far, the only main advantage of using compressed images, is the reduced space on disk. You can't just load a JPEG and send it to the video-card texture memory right away. Nope, first you'll have to decode the data, then send a raw buffer of colors to the video-mem. So in the end you are still using the same amount of memory, it took extra time to decode, AND you lost quality. In other words, screw JPEG or any other compressed image format for graphics.

But wait. Things have changed a bit. Video-card memory has grown a lot last 10 years. From 32 MB to a gig or more. Unfortunately, games grow even faster. We want ultra-high res textures, and LOT's of them. Using all those textures at the same time requires quite a lot memory space, and bandwidth. Bandwidth? Imagine 10.000 hungry fat guys. You need 100 mcDonalds trucks to transport hamburgers. If all those trucks need to travel through a tunnel at the same time you get traffic-jams. Wouldn't it be nice if a single truck can carry more hamburgers, ending up with less trucks?


So, the graphic masters came up with compression algorithms specially designed for video-cards. I'm a newbie here as well, so I can't tell you the fine details, but you may have heard about DXT1, DXT3, DXT5, S3TC, BC1..7. Each of them is a compression method. Picking one is a trade-of between quality and space, and it also depends on what kind of image you want to compress (a normalMap, a simple blurry texture, a grayscale image, transparency yes/no). The compression ratio can be up to 1:6, which means a 1MB texture becomes ~170 kb.

The way the pixels are trashed together, is compatible with OpenGL / DirectX. That means you don't have to decompress the pixeldata first, before sending it to the videocard. No, you can send the packed buffer right away. That means the video-memory will be spared as well. Decompression happens on the fly by the hardware. I'm not sure if there is no performance penalty at all, but it seems the hardware can read compressed data just as fast as raw data. In fact...
you can even boost the performance! Remember the mcDonalds trucks? Since you need to transport less data on the buses, there will be less bandwidth issues as well. Now you probably won't notice this is you were only using a few textures anyway, but complex scenery such as a FPS game can benefit... Up to 20% according to some!

So far only 10 different textures are used in this snapshot. Or well, actually 20 as most objects also use a second or third image (normal/emissive/specular/height Map). Most textures have a resolution of 512x512 or 1024x1024, RGBA. So in total, there would be around (1024*1024 * 4(rgba) * 20(textures) = 80 MB of texture data pumped around every cycle (mip-maps, shadowMaps and deferred buffers not included). With compression, we can reduce this to ~20 MB.

Another cool detail is the loading time. Since you don’t need to convert stuff, and your harddisk or Blu-ray has to read less bytes, streaming goes faster. Who doesn’t like fast loading times? Tower22 streams maps and textures in the background while playing, so faster streaming is certainly nice.

Advantages
+ Less space required on disk / CD-Rom / Floppy disk / Blu-ray / Tape
+ Smaller files are loaded / streamed faster as well
+ Pre-build mipMaps in DDS files make the loading even faster.
+ Less space required in the video memory
+ Less bandwidth required. *Could* be a performance win.
+ Less quality maybe... but you can use bigger resolutions as well!
Disadvantages
- Quality loss. In some cases, compression is not worth it.
- Extra offline steps for the artists to produce compressed (DDS) files.

About the quality loss, this depends a bit. Matt(MJP) gave me some tips:

* Chose the proper compression methods
......- DXT1: Lower quality, higher compression, 1-bit alpha only(on/off)
......- DXT3: Better quality RGB, 4-bits for the alpha. Suitable for transparent ...... textures like foliage or a metal fence/grate.
......- DXT5: Suitable for images with smooth alpha channels or for normalMaps. Compress R and G, reconstruct B in shader.

* Use the right compression tool (nVidia, ATI, Microsoft, ...). Some do a better job than others, so carefully pick your weapons.

* You could pick a bigger resolution to compensate the quality loss, and still use less space in the end in some cases.

* Keep the option to use uncompressed textures, in case the quality sucks too much hairy balls.


Okidoki, but how in the name of SaintCrap can we make use of it? Consider three steps:
1.- Create a file with compressed image data... DDS files!
2.- Load the file in your program
3.- Create a (OpenGL/DirectX) texture using the compressed data

Next time, I'll tell you how to create DDS files, how to load them, and how to create an OpenGL texture using an (DXT) compressed image. Now it's time for beer.