Normal Map Break Down

Discussion and information relevant to creating special missions, new ships, skins etc.

Moderators: winston, another_commander

Post Reply

Was this helpfull, if no please state a reason

Yes.
5
100%
No.
0
No votes
I'm not sure.
0
No votes
 
Total votes: 5

User avatar
Frame
---- E L I T E ----
---- E L I T E ----
Posts: 1477
Joined: Fri Mar 30, 2007 8:32 am
Location: Witchspace

Normal Map Break Down

Post by Frame »

This is a Normal Map Shader Langauge Tutorial.. I tried to keep it as non technical as I could.

The first thing you need is a Graphic Card that supports shaders.. without shaders.. no normal mapping. and there is nothing you can do about it... You also need to know to make normal maps.. that is beyond the scope of this tutorial.. but basic a grey scale bump map can be converted using plugins in photoshop or other tools, to a normal map.. which is far superior to greyscale bump mapping. There are other more advanced methods.

The second thing you need is the shader files, these are known as

vertex & fragment file types

These are written in a code form format very similar to C

here are the two simple files from my Polaris Class destroyer... These are direct copies of Griffs Coriolis Station.. the undamaged one... They are without any effects other than the normal mapping...

First the Vertex File..

Code: Select all

#ifdef OO_TANGENT_ATTR
attribute vec3      tangent;
#else
const vec3         tangent = vec3(1.0, 0.0, 0.0);
#endif

varying vec2         vTexCoord;
varying vec3         vEyeVector;   // These are all in tangent space
varying vec3         vLight0Vector;
varying vec3         vLight1Vector;


void main()
{
   // Build tangent basis
   vec3 n = normalize(gl_NormalMatrix * gl_Normal);
   vec3 t = normalize(gl_NormalMatrix * tangent);
   vec3 b = cross(n, t);
   
   mat3 TBN = mat3(t, b, n);
   
   vec3 eyeVector = -vec3(gl_ModelViewMatrix * gl_Vertex);
   vEyeVector = eyeVector * TBN;
   
#ifdef OO_LIGHT_0_FIX
   vec3 light0Vector = gl_LightSource[0].position.xyz + eyeVector;
   vLight0Vector = light0Vector * TBN;
#endif
   vec3 light1Vector = gl_LightSource[1].position.xyz + eyeVector;
   vLight1Vector = light1Vector * TBN;
   
   vTexCoord = gl_MultiTexCoord0.st;
   gl_Position = ftransform();
}
This is a simple as it gets... I'm not going to pretend that I understand everything to the spec what is going on here. But generally speaking the vertex file is as its extension indicates used to manipulate the models Vertexes and also relay information in regard to its position and angle to the camera (the screen) to the fragment file.. (texture manipulation file) it does this for all vertexes.. for reasons of simplicity I'm not going to show you examples of how to manipulate vertexes since this tutorial shows you how to make normal maps appear... But you will need at least this code above in a vertex file to make normal mapping work...

Now before we enter into explanation of the fragment file you need to be aware of howto link these in the shipdata.plist file...

here is the releavent shipdata.plist file entries... for the Polaris Class Destroyer

Code: Select all

shaders = 
			{
			"back_metal.png" =
				{ 
				vertex_shader = "Frame_station.vertex"; 
				fragment_shader = "Frame_station.fragment"; 
					textures =
					(
						{						
						name = "fx3_Panels_Shiny.png";
						repeat_s = "yes";
						repeat_t = "yes";
						},
						
						{						
						name="fx3_Panels_Bump2.png";
						repeat_s ="yes";
						repeat_t ="yes";
						}
						
					);
				};
What I'm doing here is telling Oolite to:

Code: Select all

vertex_shader = "Frame_station.vertex"; 
fragment_shader = "Frame_station.fragment";
When on this model encountering a texture named back_metal.png then link this texture to:Frame_station.vertex and Frame_station.fragment.

Code: Select all

textures =
					(
						{						
						name = "fx3_Panels_Shiny.png";

Textures named back_metal.png on this particular model is replaced by the Texture named fx3_Panels_Shiny.png

Code: Select all

name="fx3_Panels_Bump2.png";
Add a second texture.. as this is added last, this is not the primary Texture as in it is not shown.. Think of it as an auxiliary texture.. something you want to do something to your primary texture with. In our case we want to use it as a normal map.

Note that there are other ways of doing this but it involves Uniforms.. something I will not address here.

Code: Select all

repeat_s ="yes";
repeat_t ="yes";
This means Tiling is enabled and note that everything still depends on how you projected the UV map in your model editor. but if you set it to tile 3 times on the X axis... this should now show instead of Clamping the texture to the edges... if you ever tiled a texture and tried to look at it you will note it does not look ok... everything outside the UV map box will be stretched...

Now to the juicy part.. the fragment file..

Code: Select all

uniform sampler2D    tex0;
uniform sampler2D    tex1;

varying vec2         vTexCoord;
varying vec3         vEyeVector;   // These are all in tangent space
varying vec3         vLight0Vector;
varying vec3         vLight1Vector;

const float          kSpecExponent = 1.0;
const float          kSpecular = 0.1;

void Light(in vec3 lightVector, in vec3 normal, in vec4 lightColor, in vec3 eyeVector, 
           in float specExponent, inout vec4 totalDiffuse, inout vec4 totalSpecular)
{
   lightVector = normalize(lightVector);
   vec3 reflection = normalize(-reflect(lightVector, normal));
   
   totalDiffuse += gl_FrontMaterial.diffuse * lightColor * max(dot(normal, lightVector), 0.0);
   totalSpecular += lightColor * pow(max(dot(reflection, eyeVector), 0.0), specExponent);
}


#define LIGHT(idx, vector) Light(vector, normal, gl_LightSource[idx].diffuse, eyeVector, kSpecExponent, diffuse, specular)


void main()
{
   vec3 eyeVector = normalize(vEyeVector);
   
   vec2 texCoord = vTexCoord;
   
   vec3 normal = normalize( texture2D(tex1, texCoord).xyz - 0.5);
   normal = normalize(normal);
   vec4 colorMap = texture2D(tex0, texCoord);
   
   vec4 diffuse = vec4(0.0), specular = vec4(0);
   
#ifdef OO_LIGHT_0_FIX
   LIGHT(0, normalize(vLight0Vector));
#endif
   LIGHT(1, normalize(vLight1Vector));
   diffuse += gl_FrontMaterial.ambient * gl_LightModel.ambient;
   
   vec4 color = diffuse * colorMap;
   
// calculate the specular, colour it using the diffuseMap 
   color += colorMap * 5.0 * specular * kSpecular;
  
   color.a = 1.0;
   
   gl_FragColor = color;
}
There is no way around this, some coding experience is required to understand this... but lets take it from the start...

Code: Select all

uniform sampler2D    tex0;
uniform sampler2D    tex1;
Here we declare two uniforms which we will need namely our two textures... you will need to declare them... or else the code cannot see them... Oolite takes care of linking them.. so

tex0 isfx3_Panels_Shiny.png our diffuse map
tex1 isfx3_Panels_Bump2.png our normal map

these next declarations are must be in declarations..

Code: Select all

varying vec2         vTexCoord;
varying vec3         vEyeVector;   // These are all in tangent space
varying vec3         vLight0Vector;
varying vec3         vLight1Vector;
VTexCoord is the uvmap on this particular face fetched from the vextex shader file which got it from the dat file. We get it from the vertex shader file because we could have manipulated its vertexes...

VeyeVector is the vector at which the user is pointing in space, and therefore the way his eyes is pointing..

vLight0Vector
vLight10Vector

not sure about those. but ofcourse something with lighting in space and in the start screen and buy ship screen... They do got their values from the vertex shader since they are of type varying...

Finally these two declarations are used to control how much the normal and color map is lit up... You can alter their settings as you wish however you can never change them on the fly.. meaning you cannot alter them any other place than here becuase they are declared const meaning they are a constant number that never changes after being declared... the declaration is at compile time constant, and in our case the compile time is when we start up Oolite...

Code: Select all

const float          kSpecExponent = 1.0;
const float          kSpecular = 0.1;
Right... now to the scary stuff... the light function... Ease Up, after this you should never concern yourself about it again.. it just has to be there in order for the normal map and color map to light up correctly

Code: Select all

void Light(in vec3 lightVector, in vec3 normal, in vec4 lightColor, in vec3 eyeVector, 
           in float specExponent, inout vec4 totalDiffuse, inout vec4 totalSpecular)
{
   lightVector = normalize(lightVector);
   vec3 reflection = normalize(-reflect(lightVector, normal));
   
   totalDiffuse += gl_FrontMaterial.diffuse * lightColor * max(dot(normal, lightVector), 0.0);
   totalSpecular += lightColor * pow(max(dot(reflection, eyeVector), 0.0), specExponent);
}
what this function does is that it calculates the light reflection and returns the result.. it is used in 3 cases..

Ship Buy Screen
Oolite start screen
Ingame...

unless you want to do some heavy modifying of the light result you should not alter this... however here is a link if you are interested

http://www.lighthouse3d.com/opengl/glsl ... php?lights

Now to the main part, where it all happens namely MAIN

Code: Select all

void main()
{
   vec3 eyeVector = normalize(vEyeVector);
   
   vec2 texCoord = vTexCoord;
   
   vec3 normal = normalize( texture2D(tex1, texCoord).xyz - 0.5);
   normal = normalize(normal);
   vec4 colorMap = texture2D(tex0, texCoord);
   
   vec4 diffuse = vec4(0.0), specular = vec4(0);
   
#ifdef OO_LIGHT_0_FIX
   LIGHT(0, normalize(vLight0Vector));
#endif
   LIGHT(1, normalize(vLight1Vector));
   diffuse += gl_FrontMaterial.ambient * gl_LightModel.ambient;
   
   vec4 color = diffuse * colorMap;
   
// calculate the specular, colour it using the diffuseMap 
   color += colorMap * 5.0 * specular * kSpecular;

// add in the glowing window lights   
   //color += LampColor * colorMap.a; // multiplier here to increase glow effect
   
   color.a = 1.0;
   
   gl_FragColor = color;
}
You always need the function main, Oolite expects it.. and starts to execute it once the game is running and the model is showing in some form...

without it you will probably get an error...

but to try and explain what goes on here...

the first thing you see is all the Normalize functions, now what does that mean.. Without going into to much detail it means bringing our texture face into a correct state so we can manipulate it.

So at this stage you should simply accept that this is so... So copy and paste.. ;-)

Now here is the stuff we are interested in...

Code: Select all

vec3 normal = normalize( texture2D(tex1, texCoord).xyz - 0.5);
We here declare a vec3 meaning a vector with 3 values. XYZ does not mean Model space XYZ but RGB... you always need to use xyz when using a vec3

But our Normal is set to be equal of our normal map RGB channels and from each are subtracted 0.5. I admit i have no idea why the subtract , but I assume it has to-do with the parallax method used to generate the normal maps..

this:

Code: Select all

texture2D(tex1, texCoord)
Is hard for me to explain, but what it is is that the result equals the texture.st where st is the coordinates.. st coordinates are of type float... are basicly XY coordinates but in 3d space. but as we normalize them we can think of them in 2d space... and as XY coordinates...

The texCoord is a vec2 meaning it contains two values
namely s and t and they are floats... and you can manipulate them individualy like this.

Code: Select all

texCoord.s = 0.5
s and t us could be called sub-members of texCoord

But on With the Normal mapping Explanation...

next up we got

Code: Select all

vec4 colorMap = texture2D(tex0, texCoord);
This is our texture map, our base map so to speak which we are going to manipulate in order go get the normal map projected onto there... what we did was declare it of type vec4. we fetch it via its texture coordinates.. found in the dat file... and retrieved via the vertex shader...

Vec4 submembers are known as RGBA (red green blue alpha) channels

so we can manipulate the the different channels individually just like we did with the s and t member of texCoord by simply writing colorMap.a = 1.0 meaning the entire alphamap will be white.. which we are going to-do later :-)

Next Up

we read

Code: Select all

vec4 diffuse = vec4(0.0), specular = vec4(0);
these are two simple declarations of vec4

we are setting all of their values to 0.0 for the diffuse and 0 for the specular..

we are declaring it now because we are going to use it later..

next up we got two things... depending on where the player is...

Code: Select all

#ifdef OO_LIGHT_0_FIX
   LIGHT(0, normalize(vLight0Vector));
#endif
   LIGHT(1, normalize(vLight1Vector));
this calculates the correct lighting depending on where the player is... in the buy ship screen / demo screen or in space screens...

terrible boring so we move on to

Code: Select all

diffuse += gl_FrontMaterial.ambient * gl_LightModel.ambient;
Here we set our diffuse light

Diffuse reflection is the reflection of light from an uneven or granular surface such that an incident ray is seemingly reflected at a number of angles.

also known as our normal map :-)

We then grab our normal map with our color map in the next line..
here by mutiplying theire values, obeying mathematical rules..

Code: Select all

vec4 color = diffuse * colorMap;
We finally add the extra lightning and the colorMap

Code: Select all

color += colorMap * 5.0 * specular * kSpecular;
for some reason Griff set the alpha map to be 1.0 = all white. I tried setting it to 0.(no apparent effect). I presume Oolite will or had crashed in the past had the alpha map been non existent... So It was manually added here as a safe guard.

If your texture has an alphamap, and you are using it for something, you should remove this line. (I think :? )

Code: Select all

color.a = 1.0
finally return the result to Oolite

Code: Select all

gl_FragColor = color;
If you are keen on learning more shader langauge, read up griffs examples and/or read here

http://www.lighthouse3d.com/opengl/glsl/index.php?intro

Hope this was of help to you

PS. do not forget to vote... :-)
man this was supposed to be 15 min write up.. it took 2 hours at least...
to bed to bed and let loose the pigeons of peace... :twisted:
Bounty Scanner
Number 935
User avatar
Griff
Oolite 2 Art Director
Oolite 2 Art Director
Posts: 2483
Joined: Fri Jul 14, 2006 12:29 pm
Location: Probably hugging his Air Fryer

Post by Griff »

really great stuff Frame!
I noticed one thing, in the fragment shader you can delete these two lines from the main part as they might be confusing, they're left over from when the alpha channel in the diffuse map worked as a glow map:

Code: Select all

// add in the glowing window lights    
   //color += LampColor * colorMap.a; // multiplier here to increase glow effect 
you're quite correct, if you are using the alpha channel of your diffuse map to drive an effect you don't want to include the color.a = 1.0 line, as it will overwrite whatever value is in your alpha channel with 1.0, i don't quite know why we do this, i assume 1.0 is full opacity for an alpha channel, so if transparency is added later to the Oolite graphics engine the shader will still run as we expect and not suddenly start drawing ghostly, faded out, transparent ships!

in case it's not obvious, // is used to make a 'comment' in the code, anything written after it on that line is ignored
use /* and */ if you want to comment out multiple lines of code, eg

/*
Griff is a fraud! All his shaders are
stolen from examples on the interwebz
*/
pmw57
---- E L I T E ----
---- E L I T E ----
Posts: 389
Joined: Sat Sep 26, 2009 2:14 pm
Location: Christchurch, New Zealand

Re: Normal Map Break Down

Post by pmw57 »

Frame wrote:
Now here is the stuff we are interested in...

Code: Select all

vec3 normal = normalize( texture2D(tex1, texCoord).xyz - 0.5);
We here declare a vec3 meaning a vector with 3 values. XYZ does not mean Model space XYZ but RGB... you always need to use xyz when using a vec3

But our Normal is set to be equal of our normal map RGB channels and from each are subtracted 0.5. I admit i have no idea why the subtract , but I assume it has to-do with the parallax method used to generate the normal maps..
I suspect that the values fall into the range 0...1 and removing 0.5 turns them into -0.5...0.5 which is more appropriate for bumps and dips in the surface of the texture.

I won't dwell on this for much further though because you are doing an incredible job at this already as it is.

Keep up the good work :D

Edit: Confirmed - http://www.quakeworld.nu/forum/viewtopi ... 328#p46328
normalmaps being textures (logically 0 to 1) means you need to scale and bias them in order to make them actual normalmaps
A trumble a day keeps the doctor away, and the tax man;
even the Grim Reaper keeps his distance.
-- Paul Wilkins
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Re: Normal Map Break Down

Post by JensAyton »

Frame wrote:

Code: Select all

vec3 normal = normalize( texture2D(tex1, texCoord).xyz - 0.5);
We here declare a vec3 meaning a vector with 3 values. XYZ does not mean Model space XYZ but RGB... you always need to use xyz when using a vec3
This is incorrect, you can use .rgb (or .stp, if you really want). If it doesn’t work, your drivers are severely broken. (GLSL specification, section 5.5.)
Post Reply