Rain effects, better lighting model, optimized code

Post tutorial Report RSS Fake Specular Maps from Diffuse Texture

Using the dot product of the diffuse texture, we can create fake, grayscale spec maps for textures / surfaces that don't have a real spec map texture baked-in.

Posted by on - Basic Textures

NOTE: Came up with this method while messing with The Dark Mod's lighting shaders, hence screenshots of The Dark Mod demonstrating it. However, I'm posting this tutorial under FUEL: RESHADED, since it's a generalized tutorial on faking specular maps.

The Specular Dilemma

Specular is the shine you see on surfaces from light reflection.

While Physically-Based Rendering (PBR) is the industry standard in lighting models today, a lot of games & game engines still use more basic models that just have light, shadow, diffuse/color texture and maybe a specular map texture to make the light shine/dull on the surface in various ways. FUEL & The Dark Mod are examples.

In a perfect world, you'd go through all your textures, and generate a specular map texture for everything.

But, this can be very time-consuming. Even if you use a tool to automate this with batch-processing, you can still waste a lot of time hand-tweaking the specular textures to get them "just right".

Also, since you're adding more textures (IE: adding a specular texture to everything in addition to the diffuse/color texture), you increase the memory load on the graphics card. Texture pulls are expensive in the graphics pipeline, and textures get stored in memory. So, you've increased processing and memory.

Another consideration is that maybe you're going to update your game engine to do Physically-Based Rendering (PBR) later. So, it's a bit annoying wasting time generating specular maps now if you're just going to replace it all with albedo, reflectivity, roughness, etc maps later.

A half-measure is to usually just add specular maps to the very prominent things in the game world, and then fall back with a very minor, base specular value to apply to anything else.

In FUEL, they did this with many things. The vehicles had specular map textures. Some surfaces (eg: metal grating) had a specular map to make ridges shine more. In The Dark Mod, metal rings on barrels would shine. Metallic surfaces would shine.

But, now we have the issue of inconsequential things, and/or things that shouldn't have much shine anyways.

If we fallback on using a minor, generic shine amount, we can end up with a uniform shine on things. The normal / bump map helps break up the monotony a bit, but it's still a generic shine value applied to all surfaces that don't have a specular map texture.

Fake It Till You Make It

While a specular map texture can be an RGB to take color into account, many times in old-school games or more basic lighting models, the specular texture map is just a single-channel, grayscale texture. The grayscale handles black-to-white that covers 0% shine (black) to 100% shine (white), with shades of gray being %'s in-between.

Since we already pulled a diffuse/color texture for a surface, we can simply fake a spec map for it by grayscaling the diffuse/color texture.

0EHz1P1

Diffuse Textures in The Dark Mod grayscaled to see what they'd look like as fake spec maps


Grayscaling the diffuse texture is pretty easy: all we're doing is averaging together the Red, Green & Blue channels of the diffuse/color texture.

  • Grayscale Luminance = ( Red + Green + Blue ) / 3

You can rewrite this to be...

  • Grayscale Luminance = ( Red + Green + Blue ) * 0.33

And, you can rewrite that as...

  • Grayscale Luminance = ( Red * 0.33 ) + ( Green * 0.33 ) + ( Blue * 0.33 )

Why are we re-writing it? Because once we look at it this way, it's a perfect candidate for the dot() product function in HLSL/GLSL instead of coding it out by hand.

  • Grayscale Luminance = dot( diffuse.rgb, 0.33 )

To ensure it stays within 0 to 1, we can saturate/clamp the result, too.

For HLSL we get something like this...

float3 diffuseTexture = tex2D( diffuseSampler, diffuseUV );
float fakeSpecMap = saturate( dot( diffuseTexture.rgb, 0.33 ) );

For GLSL we get something like this...

vec3 diffuseTexture = texture( diffuseSampler, diffuseUV );
float fakeSpecMap = clamp( dot( diffuseTexture.rgb, vec3(0.33) ), 0.0, 1.0 );

If you really want to get fancy, you can replace 0.33 with a luminosity value, since we're pulling a luminance of the diffuse texture...

HLSL

float3 LUMINOSITY = float3( 0.21, 0.72, 0.07 );
float3 diffuseTexture = tex2D( diffuseSampler, diffuseUV );
float fakeSpecMap = saturate( dot( diffuseTexture.rgb, LUMINOSITY ) );

GLSL

vec3 LUMINOSITY = vec3( 0.21, 0.72, 0.07 );
vec3 diffuseTexture = texture( diffuseSampler, diffuseUV );
float fakeSpecMap = clamp( dot( diffuseTexture.rgb, LUMINOSITY ), 0.0, 1.0 );

From there, you just multiply your spec dot shine coefficient (result of your Blinn-Phong or Phong or what-not calculation) by the fake spec map to add variation to the shine based on how light or dark the textures diffuse/color texture is.

If some surfaces have a specular map texture already, then you can use an "if" statement to branch on some variable to determine what needs a fake spec map generated.

In FUEL, they use a compile-time flag (#ifdef bSpecular) to denote what objects have a specular map and which don't. So, I can just create a branch on that. FUEL uses grayscale specular texture maps, so I just have to pull one channel (I pull the .r channel in the code below). They also have the specular power stored in the .alpha channel. So, I pull both red & alpha channel for spec map & power, respectively. If the compile-time flag isn't set, it falls back on generating a fake spec map from the diffuse.rgb texture, and just uses whatever default value was already give in the spcpow variable before hitting this part of the code.

HLSL

#ifdef	bSpecular
float2	spc			= tex2DB(sSpecular, uv.xy, bias).ra;
		spccol		*= spc.r;	// specular grayscale map
		spcpow		*= spc.g;	// specular power
#else
		// if no specular texture map,
		// make a fake grayscale one out of the diffuse texture
		spccol		*= saturate(dot( tDiffuse.rgb, 0.33 ));
#endif

For The Dark Mod, they used a base specular materials value for surfaces (params[var_DrawId].specularColor.rgb). For surfaces that don't have specular maps, they default it to 0 (pitch-black). So, we can just test for pitch-black on that value by dot'ing it by 0.33 to grayscale it and see if it's black. If not, then we assume a specular map texture. If it is black, then we branch fall back by generating a fake spec map from diffuse texture.

GLSL

if ( dot(params[var_DrawId].specularColor.rgb, vec3(0.33)) > 0.0 )
{
	specular.rgb	= textureSpecular( var_TexSpecular ).rgb	// spec map
				* params[var_DrawId].specularColor.rgb;	// spec color
	specularPower	= specular.z;	// use spec map for spec pow mix
	specularMulti	= 5.0; // boost spec map to help stand-out more
} else {
	// otherwise fake one by grayscaliing diffuse texture
	specular.rgb	= vec3( fakeSpecMap( matDiffuse.rgb ) );
	specularMulti	= 2.0;// mild spec shine on things that don't have it
	specularPower	= 0.0;// lowest spec pow for broader, more diffused shine
}


A step further...

When experimenting with this on The Dark Mod lighting shaders, I stumbled across this article SplashDamage.com talking about inverting the diffuse texture color to create a more uniform / dull shine...

Wiki.splashdamage.com

The Dark Mod has direct light & ambient light runs, and this method worked best with ambient light, helping to blend specular shines into the ambient light in shadows. It didn't look good in direct light, because the uniformity of the shine stuck out like a sore thumb.

But, the way you do this is simple... just 1-diffuse.rgb.

Here's the GLSL snippet from The Dark Mod shaders I tweaked for ambient/shadow lighting...

} else {
	// else fake one by grayscaling diffuse texture
	specular.rgb	= vec3( fakeSpecMap( matDiffuse.rgb ) );	// grayscale diffuse color for spec map
	specular.rgb	*= ( vec3(1.0) - matDiffuse.rgb );// inverse diffuse color as spec color
	specMul		= 1.5;// low amount of shine
	specPow		= 0.0;// broader, more diffused specular
}

The part to note is...

  • specular.rgb *= ( vec3(1.0) - matDiffuse.rgb );

... which inverts the diffuse texture colors before grayscaling them as a fake spec map.

You'll need to experiment with this to see what looks good for your situation. On The Dark Mod, I use it for ambient/shadow lighting, but not direct lighting. In FUEL, since everything is very brightly lit with sun, I don't use it at all. Inverting the diffuse seems to work well in dark shadows.

And even further...

If you're doing fresnel lighting, you can apply the specular map & fake specular map to it to add variation to that as well. You'll have to experiment with this, because in some cases, like with very dark textures, a fake spec map generated from it might negate the fresnel rimlighting almost completely. But, in many cases, it helps create variation in the fresnel rimlighting.

In The Dark Mod, once again, I applied the fake spec map to the ambient / shadow lighting fresnel rimlight. It looked good in dark ambient/ shadow lighting, but not so much in direct lighting, where torches needed to create very bright rimlights on edges.

Also, for fresnel lighting, multiply in the diffuse texture color to colorize your fresnel rimlighting. Otherwise you will get a very uniform fresnel rimlighting that looks weird in shadows. (EG: remember Elder Scrolls Oblivion, where characters in the dark had a weird blue haze on their edges? That's because the fresnel rimlight wasn't taking the objects surface diffuse texture color into account).

Wrap Up

Faking spec maps is easy, and adds very little processing or memory bandwidth to your shaders. It helps enhance textures that don't have specular without having to bend over backwards creating a lot of specular textures by hand. By tweaking them to use a minor amount of specular, you can have an overall specular in your scene to enhance it, which will keep it from looking flat.

eESVElQ

The Dark Mod tweaked with fake spec maps & fresnels

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: