Page 1 of 2

Comparison of Two Textures: heightmap and normal map data

Posted: Wed Jun 18, 2008 8:17 pm
by dlangdev
Hi,

Just wondering if the following code make sense when setting up textures for parallax view.

The first method used when a colormap and height map texture is available.

colormap: rockwall.bmp
Image

heightmap: rockwall_height.bmp
Image

code:

Code: Select all

video::ITexture* colorMap = driver->getTexture("../../media/rockwall.bmp");
video::ITexture* normalMap = driver->getTexture("../../media/rockwall_height.bmp");
		
driver->makeNormalMapTexture(normalMap, 9.0f);
scene::IMesh* tangentMesh = smgr->getMeshManipulator()->createMeshWithTangents(
			roomMesh->getMesh(0));
			
room = smgr->addMeshSceneNode(tangentMesh);
room->setMaterialTexture(0,	colorMap);
room->setMaterialTexture(1,	normalMap);
room->getMaterial(0).SpecularColor.set(0,0,0,0);
room->setMaterialFlag(video::EMF_FOG_ENABLE, true);
room->setMaterialType(video::EMT_PARALLAX_MAP_SOLID); 
room->getMaterial(0).MaterialTypeParam = 0.02f; // adjust height for parallax effect
// drop mesh because we created it with a create.. call.
tangentMesh->drop();
here's the output...

parallax:
Image

bump:
Image

The second method uses two textures: a colormap and a normal map.

colormap: rockwall.bmp
Image

normalmap: rockwall_nomalMapped.bmp
Image

Code: Select all

video::ITexture* colorMap = driver->getTexture("../../media/rockwall.bmp");
video::ITexture* normalMap = driver->getTexture("../../media/rockwall_nomalMapped.bmp");

scene::IMesh* tangentMesh = smgr->getMeshManipulator()->createMeshWithTangents(
			roomMesh->getMesh(0));
			
room = smgr->addMeshSceneNode(tangentMesh);
room->setMaterialTexture(0,	colorMap);
room->setMaterialTexture(1,	normalMap);
room->getMaterial(0).SpecularColor.set(0,0,0,0);
room->setMaterialFlag(video::EMF_FOG_ENABLE, true);
room->setMaterialType(video::EMT_PARALLAX_MAP_SOLID); 
room->getMaterial(0).MaterialTypeParam = 0.02f; // adjust height for parallax effect
// drop mesh because we created it with a create.. call.
tangentMesh->drop();
The code I skipped was this one...

Code: Select all

driver->makeNormalMapTexture(normalMap, 9.0f);
since the normal map is already loaded, there's no need to generate it.

And here's the output...

Image

What do you think? Does that make sense?

Thanks.

Posted: Wed Jun 18, 2008 10:53 pm
by arras
I would say yes it do. Difference is in quality of your bump map and normal map. From that bump map you probably not gona create normal map of similar quality as the one you have.

Posted: Wed Jun 18, 2008 11:25 pm
by dlangdev
Thanks Arras,

One thing I noticed was the second method lost the parallax effect. It's probably because a height map is required to produce the effect.

That would only mean hitting two birds with one stone, I can now implement two methods to cover/solve interesting problems.

Also, it would be nice for Irrlicht to implement a fin at the edges so that the pixels can stretch to a height defined by the heightmap. Notice the edge is flat and adding a fin in there would solve the flat edge effect.

Posted: Thu Jun 19, 2008 4:08 am
by BlindSide
If you read the method description for "makeNormalMapTexture" closely, it would say that it encodes the heightmap data inside the (invisible :P ) alpha channel. Therefore, there is some unseen data being lost in just using the normal map directly.

Cheers

Posted: Thu Jun 19, 2008 9:39 pm
by dlangdev
Is there a tool for processing a colormap into a heightmap? I can't seem to find one at the moment. What I have at the moment is the Nvidia normalmap tool, though I haven't used all the features in there, it might have a checkbox for setting heightmap data.

Posted: Fri Jun 20, 2008 12:31 am
by dlangdev
It looks like there's a way of generating height data from a colormap by converting color values to grayscale.

The code in c# shown below:

Code: Select all

public Bitmap ConvertToGrayscale(Bitmap source)

{

  Bitmap bm = new Bitmap(source.Width,source.Height);

  for(int y=0;y<bm.Height;y++)

  {

    for(int x=0;x<bm.Width;x++)

    {

      Color c=source.GetPixel(x,y);

      int luma = (int)(c.R*0.3 + c.G*0.59+ c.B*0.11);

      bm.SetPixel(x,y,Color.FromArgb(luma,luma,luma));

    }

  }

  return bm;

}

Image
Image

Reference: http://www.bobpowell.net/grayscale.htm

Posted: Fri Jun 20, 2008 3:41 am
by Halifax
I thought that heightmaps were luminance based though, so even though you may convert it to grayscale, the luminance may not be what you are looking for. And without that it may screw up your heightmap shader.

Posted: Fri Jun 20, 2008 5:54 am
by dlangdev
Halifax wrote:I thought that heightmaps were luminance based though, so even though you may convert it to grayscale, the luminance may not be what you are looking for. And without that it may screw up your heightmap shader.
That is correct, it should take luminance into consideration. It wasn't pasted in the thread, though the matrix formula for luminance was mentioned in the reference.

At the moment, I'm playing around with CNullDriver.h/cpp, I've cloned makeNormalMapTexture() to makeGrayScaleTexture() for setting up the grayscale texture using a colormap as input, the output is a new texture containing grayscale data. Better if it simply passes two textures instead of one, that would make it even simpler.

I'm also looking into adding fractal/perlin textures later, for example makeFractalTexture() or makePerlinTexture(). Then move to code makeColorRampTexture() later.

I have yet to learn A1R5G5B5 and A8R8G8B8, though. I'm definitely a newbie into this. So, don't hold your breath on this one as it might take some time to get this done.

Posted: Fri Jun 20, 2008 10:37 pm
by dlangdev
Hi there,

Please look at the code posted below. It's calculating luminance value using a simple formula.

Not sure exactly if this code will cover it, though.

I'm pasting the partially completed code, please look into it and comment.

The code compiles on my dev machine and I was able to walk through the code in debug mode.

What the code is doing the following:

1) It receives the colormap.
2) Creates the destination texture --> grayscale.
3) Walks the colormap.
4) For each color, calculate height/luminance.
5) Store luminance to alpha.
6) Save color to grayscale map.

Code: Select all

//! Creates a grayscale map from a colormap texture.
video::ITexture* CNullDriver::makeGrayscaleTexture(video::ITexture* texture)
{
	if (!texture)
		return 0;

	if (texture->getColorFormat() != ECF_A1R5G5B5 &&
		texture->getColorFormat() != ECF_A8R8G8B8 )
	{
		os::Printer::log("Error: Unsupported texture color format for making grayscale map.", ELL_ERROR);
		return 0;
	}

	struct colorBitFields {
		unsigned a :8;
		unsigned r :8;
		unsigned b :8;
		unsigned g :8;
	};
	struct colorBitFields aBitFld;
	int luma;
	core::dimension2d<s32> dim = texture->getSize();
	video::ITexture* grayScaleTexture;
	core::stringc tn = texture->getName();
	tn.append("_grayscale");
	grayScaleTexture = addTexture(dim, tn.c_str() ,texture->getColorFormat());
	if (texture->getColorFormat() == ECF_A8R8G8B8)
	{
		// ECF_A8R8G8B8 version
		s32 *p = (s32*)texture->lock();
		s32 *q = (s32*)grayScaleTexture->lock();
		if (!p)
		{
			os::Printer::log("Could not lock source texture for making grayscale map.", ELL_ERROR);
			return 0;
		}
		if (!q)
		{
			os::Printer::log("Could not lock destination texture for making grayscale map.", ELL_ERROR);
			return 0;
		}
		s32 pitch = texture->getPitch() / 4;
		s32* in = new s32[dim.Height * pitch];
		memcpy(in, p, dim.Height * pitch * 4);
		for (s32 x=0; x<pitch; ++x)
		{
			for (s32 y=0; y<dim.Height; ++y)
			{
				int nsiz = sizeof(p[y*pitch + x]);
				//if ( nsiz == 4 ) {
					aBitFld.r = p[y*pitch + x] & 0xFF000000;
					aBitFld.g = p[y*pitch + x] & 0x00FF0000;
					aBitFld.b = p[y*pitch + x] & 0x0000FF00;
					aBitFld.a = p[y*pitch + x] & 0x000000FF;
				//}
				luma = (int)(aBitFld.r*0.3 + aBitFld.g*0.59+ aBitFld.b*0.11); 
				aBitFld.a = luma;

				//method 1: return unmodded rgb color
				q[y*pitch + x] = video::SColor(
					(s32)aBitFld.a, // store height in alpha
					(s32)aBitFld.r, (s32)aBitFld.g, (s32)aBitFld.b).color;

				//method 2: return modded rgb color
				q[y*pitch + x] = video::SColor(
					(s32)aBitFld.a, // store height in alpha
					(s32)aBitFld.a, (s32)aBitFld.a, (s32)aBitFld.a).color;

			}
		}
		delete [] in;
		texture->unlock();
		grayScaleTexture->unlock();

	}

// more code here taken out for brevity's sake.

	grayScaleTexture->regenerateMipMapLevels();

	return grayScaleTexture;


Posted: Sat Jun 21, 2008 1:06 am
by dlangdev
Here are the results...

Using the code posted above, it was able to generate grayscale data using only the color data by translating colorscale to grayscale. The image shown below.

With the help of the texture-to-image code, I was able to dump texture data to file. See http://irrlicht.sourceforge.net/phpBB2/ ... magetofile for more details.

The original image...
Image

The grayscale image....

Code: Select all

grayScaleMap = driver->makeGrayscaleTexture(colorMap);
Image

Then the grayscale data passed through makeNormalMapTexture() and here is the result...

Image

What's so cool about this code is it provides simpler texture workflow. With this feature, you can easily make bump maps by simply using your original images, the code will convert them on-the-fly.

That's probably it for this one, though. I hope someone picks this up and runs with it.

I'll post the cleaned-up code later. But you know well you can easily figure the code out from here.

Posted: Sat Jun 21, 2008 1:21 am
by dlangdev
Hmm, I just noticed there were differences in how the two normal maps were generated.

Here's the nvidia version.

Image

And the version generated by the code posted above...

Image

Anyway, that's not really a big deal, I'm just pointing out the difference between two tools.

Posted: Sat Jun 21, 2008 6:41 am
by Halifax
Your values seems a little scattered and jittered unlike the nVidia version. It would be cool to see the two normals maps applied to a model in real-time for comparision.

Posted: Sat Jun 21, 2008 8:58 am
by dlangdev
Halifax wrote:Your values seems a little scattered and jittered unlike the nVidia version.
Yeah, I played around with the values by passing it through one to several filters. The image above came from a one-pass filter, I noticed it only after finishing the testing.
Halifax wrote: It would be cool to see the two normals maps applied to a model in real-time for comparision.
I'll post another set of images later as I'm still cleaning up the code. So far, the code pretty much fills my need for simpler bumpmapping workflow.

To answer that question, the on-the-fly bumpmap looks like the one shown below. Depending on what filters are applied.

Image

So far, the render looks pretty good, though not as good as the heightmap texture found in the demo. I think the heightmap texture from the demo was manually meticulously prepared. I don't have time to work on those kinds of stuff. I'd rather push them to a function and let the code handle it.

By the way, I got the idea from Maya version 2008. It has tons of cool features that covers most of the tedious boring hard work, though.

In due time, they'll eventually get coded/ported over to Irrlicht.

Posted: Sat Jun 21, 2008 8:00 pm
by Halifax
Yeah, your normal map looks pretty good. I'm not sure if this is possible, but if it is, then I would say you might benefit from "scaling" the normal map by 2 or something when it is applied.

Posted: Sun Jun 22, 2008 9:36 am
by dlangdev
Yup, I did play around with the filters, and was able to get different results.

grayscale:
Image

normal:
Image

output:
Image

Overall, I'm very happy with the results I got, it took a lot of testing time, but the results seeing how they rendered was really worth all the effort.

All that was rendered using the sample demo per pixel lighting code. I took out the heightmap and replaced that with a make gray scale function.

Looks like I won't be needing the Nvidia normal mapping tool any longer.

Huzzzah!