He hit me!

Or

Basic lighting


You know what joke they always do when they start teaching lights right? There’s no need for me to say “lights on!” or “let there be light!” ….if I had to choose I’d pick “can has light? Kthxbi”

Before we begin

The three basic lighting channels


Ambient is the base color added to all surfaces. In deep space for instance, this color is complete blackness. An analogy on earth would be all light hitting an object which doesn’t originate directly from a light source, and thus is weakened and close to even on all spots. (light reflected from other objects)

Diffuse is light hitting an object directly from a light source and spreads evenly through the surfaces, depending on the surface angle towards the source.

The most common algorithm for diffuse is Lambertian reflectance, which we will use in our basic lighting shaders. Lambertian uses the dot product between the surface normal and the view direction vector to calculate diffuse.
It produces a smooth and gradual (and somewhat rough, like uncoated wood) lighting texture.

Specular is light reflected in a specular angle, creating the “shiny spot” (or strip) that is seen in reflective surfaces. The size and distribution of the specular depending on the “shininess factor” we provide, and the algorithm we use. This highlight is dynamic, and changes location and spread according to the angle from which the user views the object from.

The two most commonly used methods for specular are Phong and Blinn-Phong, we will learn them both.

What are normals?

A normal is a vector that points perfectly upwards from the surface (orthogonal). It indicates the direction to which the surface (or a specific point across it) is facing.

A normalized vector

Is a vector which all of his components run between 0 and 1 in value keeping their original proportions compared to the vector's magnitude. Essentially, they are clamped so that the length of the vector will be 1 (a unit vector).
The magnitude of a 2d vector will be: √( X2 + Y2) (Pythagoras)
A vector (2, 1) when normalized will become (2/√5, 1/√5)

The dot product

The dot product equals the multiplication of two vector’s magnitudes by each other and the cos(α) between them (|A|*|B|*cos(α)) as if they had originated from the same spot.

This function helps us determine the directional relationship between two vectors.

On unit vectors, the result is always between 1 and -1 (since it uses a cos function):
1. If the two vectors are orthogonal to each other, the result is 0
Image
2. If two vectors are in opposite directions the result is -1 (Note, opposite can be facing each other or from each other)
Image
3. If vectors are facing the same directions the result is 1
Image
4. If the angle between two vectors is sharp (acute), the result is between 0 and 1;
5. If the angle between two vectors is blunt (obtuse), the result is between 0 and -1;

A quick look


Image
Ambient, lambertian diffuse and Phong specular (per vertex) separately

and combined:
Image
(Note, this isn’t a combination of the 3 above, because they combine ugly)

Gouraud shading (per vertex)

Gouraud shading is the simplest, fastest lighting method that combines ambient, diffuse and specular channels to light an object depending on the angle between the direction of the light source and the vertex’s normal.

For this shader, we will use Lambertian model for diffuse, and Phong reflection model for specular.

Adding ambient… or the new RED RING OF DEATH

… It can also be square ring of death… Or a tea pot…

As for ambient, it’s not very difficult. Just declare a COLOR and call it ambientColor. Give it whatever color you wish, but you might not want to make it very intense (0.125 is enough).

Diffuse

Now things will start getting a bit more complicated.
We need to take into consideration a few parameters:

  1. A desired diffuse color
  2. The position of the light
  3. The angle in which the light hits the surface


1 and 2 can be done swiftly:

float4		diffColor 	: COLOR;
float4 		lightPos;		// something like (200 150 0 0) will suffice


Since we will be setting these variables by hand in FXC, remember to either insert values in the declaration or in the material properties.

The plan:

In order to calculate the diffuse color of the vertex, we need to:

  1. Receive the vertex normal
  2. Calculate the direction in which the light hits the vertex
  3. Calculate their dot product
  4. Use the dot product as a factor for the diffuse color
  5. Pass the combined color (diffuse + ambient) to the pixel shader


1: adding normal to vertex input
we need to add a normal to the vertex shader input:

struct vertexIn
{
	float4 position : POSITION;
	float3 normal	: NORMAL;
};


2: calculate the light direction:
In order to do so, we need to subtract vertex’s world position from the light’s world position.
We also normalize the light direction, because its true magnitude doesn’t matter, and that way we can use it as a direction.

//vertex world position
float3 worldpos = mul(input.position, world_m);

//normalized direction vector
float3 lightDir	= normalize( lightPos - worldpos );


3: dot product
HLSL provides a function to calculate the dot product of two vectors:

//vertex world position
float dotNL 	= dot ( lightDir, input.normal );


4: make a diffuse color factor

//this value will later be multiplied by diffCollor to create a color vector
Float 	diff 	= saturate(dotNL);


Note that I used a function called saturate. This function clamps the value to between 0 and 1 (this is not equivalent to abs()! Values below 0 will become 0 and above 1 will become 1)

I did that because I don’t want my values to turn negative or exceed 1, because I’m accumulating color values. A negative dot product will sabotage ambient color by negating it where it doesn’t have any effect (such the opposite side of the light)

5: The state of things
At the moment, our program should look something like this:

float4x4 	worldViewProj_m : WorldViewProjection;
float4x4 	world_m		: World;

// make sure you fill these three values
float4 	lightPos;
float4	ambientColor	: COLOR;
float4	diffColor	: COLOR;

struct vertexIn
{
	float4 position : POSITION;
	float3 normal	: NORMAL;
};

struct vertexOut
{
	float4 position : POSITION;
	float4 finColor	: COLOR;
};

struct pixelIn
{
	float4 finColor	: COLOR;
};

vertexOut mainVS(vertexIn input)
{
	vertexOut output = (vertexOut)0;
	
	output.position = mul(input.position, worldViewProj_m);
	float3 worldpos = mul(input.position, world_m);
	
	float3 lightDir	= normalize( lightPos - worldpos );
	
	float 	dotNL 	= dot ( lightDir, input.normal );
	float 	diff 	= saturate(dotNL);
	
// combine the two lights
	output. finColor = diff * diffColor + ambientColor;
	return output;
}

float4 mainPS( pixelIn input ) : COLOR
{
	return input.lightColor;
}


Although you should be seeing sharp edged vertices, FXC seem to soften these. But you can see a somewhat murky line of vertices along the transition line to the dark side.

Specular

How do we calculate specular according to phong? We need to take into consideration these few things:

  1. The specular reflection angle of the light along the vertex.
  2. The angle of the camera

Specular reflection

Phong model provides the simplest, most strait forward algorithm for specular reflection.
It goes like this:
(Theory only, actual code will come later):

R = (2 * dotNL * N) – L

R : specular reflection vector;
dotNL : dot product of vertex normal and light direction vectors;
N : vertex normal vector;
L : light direction vector;

Line by line

Let’s see why and how, and don’t worry – it’s all pictures! Even a hamster can catch up!

Step 1: dot
Let's use two simple 2d vectors, separated by 45 degrees
Image
As mentioned, the dot product is cos(45)

Step 2: midway vector
Image
Our midway vector value is now: (0, 2*cos(45) )
(Do not confuse this with half-way vector used in Blinn)

Step 3: the reflection vector
To achieve a reflection vector, we will have to add a negative L vector (or subtract L, however you see it)
Image
The R vector is our reflection vector, and it equals ( -cos(45), cos(45) )
Its values are opposite to L along X.

Calculating the view

The second part of creating specular is to calculate the factor of the view angle (after we calculated the directional vector from the vertex to the camera) on the specular distribution depending on the angle between the reflection ray and the direction to the view, and according to a predefined factor we choose in advance.

In order to do that, we take the dot product of the direction of the view and the reflection vector, and pow() it by desired “shininess”.
Image

V – The view’s direction
dotVR – Dot product of V and R

The actual code

So, now we know what to do, let’s do it!

First thing we have to do, is to add a global variable that will automatically track the camera position (when we export to ogre we will have one provided from ogre, don’t worry)

float4  cameraPos  :  CameraPosition;


Secondly, we need to calculate the direction from the vertex to the camera:
(again, since it’s a directional vector, we normalize it)

float3 viewDir	= normalize( cameraPos - worldpos );


Now we calculate the reflection vector:

float3 	ref 	=  (input.normal * 2 * dotNL) - lightDir;


( dotNL and lightDir had already been calculated, you didn’t forget right? It's not like you fell asleep by now or something)

Lastly, we calculate the dotVR (view and reflection dot), calculate the specular light, and add the product to the output color:

float	dotRV	= dot(ref, viewDir);
float 	spec 	= pow(saturate(dotRV), [your specular factor here] );
	
output.finColor = diff * diffColor + spec * specColor  + ambientColor;


Note that I used saturate on dotRV as well. If I wouldn’t have, we would have a dark spot that mirrors the specular in position and behavior swallowing ambient light in the back of the object.

It’s kind of cool though (the ambient devouring blackness), try it.

The state of things

By now, two program looks something like this:

float4	specColor	: COLOR;
…
vertexOut mainVS(vertexIn input)
{
	vertexOut  output = (vertexOut )0;
	
	output.position = mul(input.position, worldViewProj_m);
	float3 worldpos = mul(input.position, world_m);
	
	float3 lightDir	= normalize( lightPos - worldpos );
	float3 viewDir	= normalize( cameraPos - worldpos );
	
	float 	dotNL 	= dot ( lightDir, input.normal );
	float 	diff 	= saturate(dotNL);
	
	float3 	ref 	=  (input.normal * 2 * dotNL) - lightDir;
	float	dotRV	= dot(ref, viewDir);
	float 	spec 	= pow(saturate(dotRV),15);
	
	output.finColor = diff  * diffColor + spec*specColor  + ambientColor;
	return output;
}

float4 mainPS( pixelIn input ) : COLOR
{
	return input.finColor;
}


Our first per vertex shader is now complete!
You can try and add some other factors, such as light intensity or whatever…
Image
The result you should be getting. I divided the diffuse by 3 to make the specular show how ugly it is when using per vertex.

Image
With higher shiny number you can see the effect of per vertex on specular better.

I know, it’s ugly, its supposed to be.

‘Exporting’ to OGRE

Unlike our earlier shaders, this shader actually has substance. This means you have to be more acute with your exporting skillz.
Make sure that:

  1. You provide all needed externals to each program that need them (globals in FXC)
  2. Remember to swap mul() components.

About lights

When retrieving lights from OGRE via the scripts, you need to add an index – this tells ogre which light to choose. The index run from 0 to n, when 0 is the closest to the object and n is the farthest.
Such as:

param_named_auto lightPos light_position 0


When you finish, your files should look something like these:

The HLSL file:

struct vertexIn
{
	float4 position : POSITION;
	float3 normal	: NORMAL;
};

struct vertexOut
{
	float4 position : POSITION;
	float4 finColor	: COLOR;
};

struct pixelIn
{
	float4 finColor	: COLOR;
};

vertexOut mainVS( 		vertexIn input,
			uniform float4x4  worldViewProj_m,
			uniform float4x4  world_m,
			uniform float4 	 cameraPos,
			uniform float4 	 lightPos,
			uniform float4	 ambientColor,
			uniform float4 	 diffColor,
			uniform float4 	 specColor,
			uniform float	 shiny	
		)
{
	vertexOut output = (vertexOut)0;
	
	output.position = mul(worldViewProj_m, input.position);
	float3 worldpos = mul(world_m, input.position);
	
	float3 	lightDir= normalize( lightPos - worldpos );
	float3 	viewDir	= normalize( cameraPos - worldpos );
	
	float 	dotNL 	= dot ( lightDir, input.normal );
	float 	diff 	= saturate(dotNL);
	
	float3 	ref 	=  (input.normal * 2 * dotNL) - lightDir;
	float	dotRV	= dot(ref, viewDir);
	float 	spec 	= pow(saturate(dotRV), shiny);
	
	output.finColor = diff* diffColor + spec*specColor + ambientColor;
	return output;
}

float4 mainPS( pixelIn input ) : COLOR
{
	return input.finColor;
}


The material script

vertex_program vv hlsl
{
	source balrg.hlsl
	entry_point mainVS
	target vs_1_1
	default_params
	{
		param_named_auto 	worldViewProj_m 	worldViewProj_matrix
		param_named_auto 	world_m 		world_matrix
		param_named_auto 	cameraPos		camera_position
		param_named_auto 	lightPos		light_position 0
		param_named_auto 	ambientColor		ambient_light_colour 0 
		param_named_auto 	diffColor		light_diffuse_colour 0
		param_named_auto 	specColor		light_specular_colour 0
		param_named	shiny float 25.0
	}
}

fragment_program pp hlsl
{
	source balrg.hlsl
	entry_point mainPS
	target ps_2_0
}

material textry
{
	technique
	{
		pass
		{
			vertex_program_ref vv
			{
			}
			fragment_program_ref pp
			{
			}
		}
	}
}