Skip to main content

Let’s have a little break

And

Play with normals


What?

Because we learned quite a lot already, and standing just before another episode of brain damaging, let’s talk about some neat tricks you didn’t think are so simple.

These tricks revolve around the use of normals and a better understanding of them. We will use them to affect our output.

Glowing edges


Ever played a game that had models shine with a halo around their edges? This trick is very common in toon like rendering, and we will now create a primitive version of it.

This version is best suited for primitive objects or highlighting objects softly without applying specific light.

How does it work?


How do we detect an edge?

We will define an edge as a part of the object that faces the camera in high angles (hence why best for primitives) and use the dot product of the normal and the view direction as a factor for the pixel color.
Image

In the edges the dot will be 0, and if we (1 – edge), we get the desired factor for color.

Try and do it yourself before you read on.

The code (FXC)

So, we’ll use the simplest shader possible, without anything that is not directly connected to this effect.

In order to achieve this, we will first need to supply the pixel shader with the needed data: the normal, and the direction to the camera.

Copy to clipboard
float4x4 worldViewProj_m : WorldViewProjection; float4x4 world_m : World; float4 cameraPos : CameraPosition; float4 glowColor : COLOR; float glowExponent; float glowRejection; struct vertexIn { float4 position : POSITION; float3 normal : NORMAL; }; struct vertexOut { float4 position : POSITION; float3 normal : TEXCOORD0; float3 viewDir : TEXCOORD1; }; struct pixelIn { float3 normal : TEXCOORD0; float3 viewDir : TEXCOORD1; }; vertexOut mainVS( vertexIn input) { vertexOut output = (vertexOut)0; output.position = mul(input.position, worldViewProj_m); output.normal = input.normal; output.viewDir = cameraPos - mul(input.position, world_m) ; return output; } float4 mainPS(pixelIn input) : COLOR { float4 finColor = glowColor; return finColor; }


I added these three optional control variables:
glowColor : desired color for the glowing edges
glowExponent: higher = finer glow
glowRejection: minimal value for glow to appear (sudden drop)

State of things:

Right now, our vertex program outputs all we need it to:

  1. Vertex position in world-view-projection
  2. Vertex normal.
  3. Direction to camera.


But our pixel program has yet to do anything but return the glow color uniformly to all pixels.

Calculating the edges

So, as we said, we need to calculate the dot product between the normal and the viewDir (dotNV). But in order to achieve an accurate glow, we need to normalize them each pixel before we calculate.

Than we calculate the edge as one minus dotNV. I raise it to the power of my glowExponent variable, to be able to control how fine the glow is. After that, we can calculate the output color by multiplying edge by glowColor we defined earlier.

I also added a rejection value; we can use it to sharply drop from glow to black if we want to control this part as well.

So, here it goes:

Copy to clipboard
float4 mainPS(pixelIn input) : COLOR { input.normal = normalize(input.normal); input.viewDir= normalize(input.viewDir); float dotNV = dot ( input.normal, input.viewDir ); float edge = pow (1-dotNV, glowExponent); if( edge < glowRejection ) edge = 0; float4 finColor = glowColor*edge; return finColor; }

And that’s it!

Results


Image
Exponent set to 1, rejection 0, and color white.

Image
Same, with exponent of 2

Image
Red color, exponent of 0.5 and rejection at 0.5