using DDS compressed textures wth ofTexture

I’m working on a project where I am using lots of textures, so I was looking into how to save room on the graphics card, and I came across DDS compression. Not only does the format stay compressed on the card, but it is also much quicker to load.

If you are on a Windows machine, you can get a-Photoshop-plugin-from-NVidia to make them.

On a Mac, you’ll have to get SquishDDS.

As for loading them, I tried adding a function to ofTexture:

ddsFormat should be either GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, or GL_COMPRESSED_RGBA_S3TC_DXT5_EXT

  
  
void ofTexture::loadCompressedData(unsigned char * data, int w, int h, int ddsFormat)  
{  
	// ------------------------  Start OF code  
	GLint prevAlignment;  
	glGetIntegerv(GL_UNPACK_ALIGNMENT, &prevAlignment);  
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);  
	glEnable(textureTarget);  
	glBindTexture(textureTarget, (GLuint)textureName[0]);  
	if ( w > tex_w || h > tex_h) {  
		printf("image data too big for allocated texture. not uploading... \n");  
		return;  
	}  
	  
	width 	= w;  
	height 	= h;  
	  
	//compute new tex co-ords based on the ratio of data's w, h to texture w,h;  
	if (GLEE_ARB_texture_rectangle){  
		tex_t = w;  
		tex_u = h;  
	} else {  
		tex_t = (float)(w) / (float)tex_w;  
		tex_u = (float)(h) / (float)tex_h;  
	}  
	// ------------------------  end OF code  
	  
	  
	  
	// ------------------------ start Jeff code  
	int numMipMaps = 0;  
	  
	int tempH = h;  
	int tempW = w;  
	while(tempH > 1 || tempW > 1)  
	{  
		numMipMaps++;  
		if((tempH /= 2) == 0) tempH = 1;  
		if((tempW /= 2) == 0) tempW = 1;  
	}  
       glTexParameteri(textureTarget, GL_TEXTURE_MAX_LEVEL, numMipMaps-1);  
	int nBlockSize;  
	  
	if( ddsFormat == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT )   
		nBlockSize = 8;  
	else  
		nBlockSize = 16;  
	  
	int nSize;  
	int nOffset = 0;  
	  
	// Load the mip-map levels  
	tempH = h;  
	tempW = w;  
	for( int i = 0; i < numMipMaps; ++i )  
	{  
		if( tempW == 0 ) tempW  = 1;  
		if( tempH == 0 ) tempH = 1;  
		  
		nSize = ((tempW+3)/4) * ((tempH+3)/4) * nBlockSize;  
		  
		glCompressedTexImage2DARB(textureTarget,  
								  i,  
								  GL_RGBA,  // always RGBA, right?  
								  tempW,  
								  tempH,  
								  0,  
								  nSize,  
								  data + nOffset );  
		  
		nOffset += nSize;  
		  
		// Half the image size for the next mip-map level...  
		tempW/=2;  
		tempH/=2;  
	}  
	// ------------------------ end Jeff code  
	  
	  
	  
	// ------------------------ Start OF code   
	glDisable(textureTarget);  
	//------------------------ back to normal.  
	glPixelStorei(GL_UNPACK_ALIGNMENT, prevAlignment);  
	  
	bFlipTexture = false;  
	// ------------------------ end OF code  
}  
  

I cobbled this code together from some examples I found online, but it doesn’t work properly. At first, the textures I was getting were complete trash. Then, after fooling around for a while, I noticed that some some of the textures were *almost* right. Still a bit garbled, but it made me think that I am on the right track. Or, it might be something else completely – for instance, maybe FreeImage isn’t reading in the image properly.

Anyone have any idea what I am doing wrong?

As I (kind of) suspected, the problem has to do with loading the image in via ofImage/Free Image.

When I load it with fopen() instead, it works. I still have a few issues, but I will paste in the code here when I am done.

So this is what I am working with now. It’s loading most of my textures just fine, but there is one set that is a little corrupted - there is a weird stripe of the image that is wrapping around to the left side, even though wrapping is turned off. and GL_COMPRESSED_RGBA_S3TC_DXT3_EXT is the only format that is working. It won’
t load GL_COMPRESSED_RGBA_S3TC_DXT1_EXT textures.

Hope you guys are having fun at Ars!

  
  
void ofTexture::loadCompressedData(string filename, int w, int h, int internalFormat)  
{  
       filename = ofToDataPath(filename);  
  
	allocate(w, h, internalFormat);  
	  
	// ------------------------  Start OF code  
	GLint prevAlignment;  
	glGetIntegerv(GL_UNPACK_ALIGNMENT, &prevAlignment);  
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);  
	glEnable(textureTarget);  
	glBindTexture(textureTarget, (GLuint)textureName[0]);  
	if ( w > tex_w || h > tex_h) {  
		printf("image data too big for allocated texture. not uploading... \n");  
		return;  
	}  
	  
	width 	= w;  
	height 	= h;  
	  
	//compute new tex co-ords based on the ratio of data's w, h to texture w,h;  
	if (GLEE_ARB_texture_rectangle){  
		tex_t = w;  
		tex_u = h;  
	} else {  
		tex_t = (float)(w) / (float)tex_w;  
		tex_u = (float)(h) / (float)tex_h;  
	}  
	// ------------------------  end OF code  
	  
	  
	  
	// ------------------------ start Jeff code  
	  
	ifstream::pos_type size;  
	char * data;  
	ifstream file (filename.c_str(), ios::in|ios::binary|ios::ate);  
	if (file.is_open())  
	{  
		size = file.tellg();  
		data = new char[size];  
		file.seekg (0, ios::beg);  
		file.read(data, size);  
		file.close();  
	}  
	else cout << "Unable to open file";  
	  
	int numMipMaps = 0;  
	  
	int tempH = h;  
	int tempW = w;  
  
	while(tempH > 1 || tempW > 1)  
	{  
		numMipMaps++;  
		if((tempH /= 2) == 0) tempH = 1;  
		if((tempW /= 2) == 0) tempW = 1;  
	}  
  
	glTexParameteri(textureTarget, GL_TEXTURE_MAX_LEVEL, numMipMaps-1);  
  
	GLint blockSize = (internalFormat == GL_COMPRESSED_RGB_S3TC_DXT1_EXT ? 8 : 16 );  
	int nSize;  
	int nOffset = 0;  
	  
	// Load the mip-map levels  
	tempH = h;  
	tempW = w;  
	for( int i = 0; i < numMipMaps; ++i )  
	{  
		if( tempW == 0 ) tempW  = 1;  
		if( tempH == 0 ) tempH = 1;  
		  
		nSize = ((tempW+3)/4) * ((tempH+3)/4) * blockSize;  
	  
		glCompressedTexSubImage2D(textureTarget,  
								  i,  
								  0,  
								  0,  
								  tempW,  
								  tempH,  
								  internalFormat,   
								  nSize,  
								  data + nOffset );  
		/*  
		glCompressedTexImage2D(textureTarget,   
							   i,   
							   internalFormat,   
							   width,   
							   height,   
							   0,   
							   nSize,   
							   data + nOffset);  
		*/  
		switch(glGetError())  
		{  
			case GL_NO_ERROR:  
				break;  
			case GL_INVALID_OPERATION:  
				cout << "invalid operation" << endl;  
				break;  
			case GL_INVALID_ENUM:  
				cout << "invalid enum" << endl;  
				break;  
			case GL_INVALID_VALUE:  
				cout << "invalid value" << endl;  
				break;  
			case GL_STACK_OVERFLOW:  
				cout << "stack overflow" << endl;  
				break;  
			case GL_STACK_UNDERFLOW:  
				cout << "stack underflow" << endl;  
				break;  
			case GL_OUT_OF_MEMORY:  
				cout << "out of memory" << endl;  
				break;  
			default:  
				cout << "Unknown openGL error" << endl;  
				break;  
		}  
  
		nOffset += nSize;  
		  
		// Half the image size for the next mip-map level...  
		tempW/=2;  
		tempH/=2;  
	}  
	  
	delete[] data;  
	// ------------------------ end Jeff code  
  
	  
		  
	// ------------------------ Start OF code   
	glDisable(textureTarget);  
	//------------------------ back to normal.  
	glPixelStorei(GL_UNPACK_ALIGNMENT, prevAlignment);  
	  
	bFlipTexture = false;  
	// ------------------------ end OF code  
}  
  

Ha! I’m talking to myself here. Oh well.

I finally got this working perfectly. I ended up stealing a lot of the code-from-this-post, where the author describes how to do to the conversion from your original format to DDS on the GPU, and then save it out to disk. The great thing is that it takes only one minor change to ofTexture. Perhaps zach or theo would consider adding it into the official release.

So, I’ll start out with the ofTexture modification. The change is in the loadData function. You need to add this check where you are loading in the texture data.

this:

  
	// update the texture image:  
	glEnable(textureTarget);  
		glBindTexture(textureTarget, (GLuint)textureName[0]);  
glTexSubImage2D(textureTarget,0,0,0,w,h,glDataType,GL_UNSIGNED_BYTE,data);	  
	glDisable(textureTarget);  
  

becomes this:

  
  
	// update the texture image:  
	glEnable(textureTarget);  
		glBindTexture(textureTarget, (GLuint)textureName[0]);  
	  
		if(glDataType == GL_COMPRESSED_RGB_S3TC_DXT1_EXT ||   
		   glDataType ==  GL_COMPRESSED_RGBA_S3TC_DXT3_EXT ||   
		   glDataType==  GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)  
			glCompressedTexSubImage2D(textureTarget,0,0,0,w,h,glDataType,size,data);  
		else  
			glTexSubImage2D(textureTarget,0,0,0,w,h,glDataType,GL_UNSIGNED_BYTE,data);	  
	glDisable(textureTarget);  
  

All this does is accept the compressed data type. You also have to add a ‘size’ parameter to the loadData function. I couldn’t figure out a way around this, so I just overloaded loadData().

Next there are some utility functions, which I can’t seem to paste in here for some reason, so they are here:

http://pastecode.net/?action=viewpost&tag=3082

So, you can do something like this:
(I haven’t actually tested this exact code – it is simplified from the code I am using.

  
  
/**  
This function takes the path to an image like a jpg or png  
It looks for another file in the same directory with the same base name, except with a .dss extension  
If it exists, it loads that instead.    
If it doesn't, it creates the compressed texture and saves it to the directory and uses the compressed data.  
*/  
ofTexture loadTexture(string filename)  
	{  
	    ofTexture tex;     
	  
		// Dissect the path and get the extension and base name of the file  
		string base = filename.substr(0, filename.length() - 3);  
		string ext = filename.substr(filename.length() - 3);  
		string dds_path = base + "dds";  
  
		// These will be passed by reference to the utility functions.  They will be populated.  
		GLint size, w, h;  
		GLenum compressedFormat;  
		GLubyte *pData = NULL;  // the compressed data holder.  
		  
		// Try to load the DDS image.  
		if(pData = LoadCompressedImage(dds_path.c_str(), &w, &h, &compressedFormat, &size))  
		{	  
			tex.allocate(w, h, compressedFormat);  
			tex.loadData(pData, w, h, compressedFormat, size);  
		}  
		else // If it doesn't exist,   
		{  
			ofImage img;  // we create an ofImage  
			img.loadImage(filename);  // Load in the original  
  
			// Compress the pixels with the utility function  
			pData = CompressPixels(img.bpp, img.getPixels(), img.width, img.height, &compressedFormat, &size);  
			  
			// Now load the data into the modified ofTexture   
			tex.allocate(img.width, img.height, compressedFormat);  
			tex.loadData(pData, img.width, img.height, compressedFormat, size); // Load that data into the texture  
  
			// Save out the image so that next time we can skip the creation process  
			// and just load the .dds file right in.  It's super quick.  
			SaveCompressedImage(dds_path.c_str(), img.width, img.height,  compressedFormat, size, pData);  
  
		}  
		delete pData; // get rid of the pData   
  
		return tex;  
	}  
  

One other thing while I’m here. I wonder if something like this would be useful to integrate into OF also:

  
  
function ofCheckGLError() {  
       switch(glGetError())  
	{  
		case GL_NO_ERROR:  
			break;  
		case GL_INVALID_OPERATION:  
			cout << "invalid operation" << endl;  
			break;  
		case GL_INVALID_ENUM:  
			cout << "invalid enum" << endl;  
			break;  
		case GL_INVALID_VALUE:  
			cout << "invalid value" << endl;  
			break;  
		case GL_STACK_OVERFLOW:  
			cout << "stack overflow" << endl;  
			break;  
		case GL_STACK_UNDERFLOW:  
			cout << "stack underflow" << endl;  
			break;  
		case GL_OUT_OF_MEMORY:  
			cout << "out of memory" << endl;  
			break;  
		default:  
			cout << "Unknown openGL error" << endl;  
			break;  
	}  
}  
  

nice work !! I 'm sure alot of folks will find this useful.

thanks!!
zach

sounds like the perfect thing to add to the advTexture memo had put together as an example here:
http://www.openframeworks.cc/forum/view-…-8&start=30
maybe someone can put it together and make something out of it. I think these advanced texture things should definitely be a separate powerful texture class for more advanced users. Great work!

P.S. If you get the time to hook it up it would be great;)

BTW I don’t think you where talking to yourself, I think that people didn’t know much about it and where just reading and following along with you, so you showed us something cool.

Sorry - I didn’t mean the “talking to myself” bit to sound like a complaint. I just thought it was funny that I kept replying to myself.

But yeah - this code is definitely useful for anyone using any significant amount of textures wanting to squeeze out a little bit more performance.

Wow – I totally missed that thread about ofxSprite, because that is exactly what I am working on here, and I wrote mine from scratch. I really need to improve my forum searching skills.

Anyway, right now I am looking into frustum-culling----to-remove-my-sprites-from-the-screen-when-they-are-no-longer-visible. Maybe you could work that into ofxSprite somehow too.

I’ll catch up on the other thread and see if I can contribute.

Hmmm… I brought this sketch from my MacBook Pro running 10.5 with a NVIDIA GeForce 8600M GT to my Mac Pro tower running 10.4 with a RadeonX1900 and now my textures aren’t compressing properly. I just get trash values. I also tested it on a Vaio with an ATI 9700 mobile and it worked fine.

Super disappointing. Has anyone else had issues working between 10.4 and 10.5 or between a MacBook Pro and a Mac Pro?

According to this: http://developer.apple.com/graphicsimaging/opengl/capabilities/, EXT_texture_compression_dxt1 and EXT_texture_compression_s3tc are supported on both the NVIDIA GeForce 8600M GT card in my laptop running 10.5, and the RadeonX1900 in my Mac pro. So I can’t figure out why it would be different.

Okay. Unless I have misinterpreted something, despite what it says on the-apple-site, it seems that my Mac Pro just doesn’t support the compression that I am using.

Using OpenGL Profiler (which is extremely useful, BTW – /Developer/Applications/Graphics Tools/OpenGL Profiler), I tracked the error down to the call to glCompressedTexSubImage2D (see above), which was throwing GL_INVALID_OPERATION, which, according to the-documentation is thrown when:

GL_INVALID_OPERATION is generated if parameter combinations are not
supported by the specific compressed internal format as specified in the
specific texture compression extension.

So I guess the RadeonX1900/Mac Pro just sucks.

Does the openGl framework in 10.4 even have that feature? I have had problems with that framework in 10.4 because some of it is just outdated.