Cel-Shading with Outlines




Cel-Shading has been used to render cartoon like or non-realistic images. One example being Jet Grind Radio as seen above.

Concept:
With Cel-Shading, instead of getting smooth lighting, you get hard or banded lighting similar to cartoons. Look at most animes and you will notice that the characters will have very few shades of lighting.
Example:

This guy has got like 3 shades on his skin. The standard NdotL lighting model won't get you this, it will result in a smooth shading along the curves. So how do we get the cel-shaded look? Bias your NdotL.

How it Works:
-Get NdotL result
-Use this value as a texture look up for a 1d gradient texture
-The result of the texture lookup will be your shading value

Example gradient texture:

note: This texture is 2d for the sake of clarity, but it doesn't need a height component to it, just 1 pixel thick is all thats needed.

What this will do:
So now instead of having a smooth change from light to dark as with NdotL, you will get these 3 differnt shading values. This is similar to if you did this:

float dotValue = max(dot(normal, lightVector));
float shade = 1.f;
if(dotValue < 0.2f)
	shade = 0.2f;
else if(dotValue < .6f)
	shade = 0.6f;

return textureColor*shade; 


Ofcourse you could just use if statements in your shader, but those are slower than a texture lookup.

The Pixel Shader Code:
float4 CelShaderPS(float2 tex : TEXCOORD0) : COLOR
{
	float dotValue = max(dot(normal, lightVector));
	float4 shade    = tex2D(gradientSampler, dotValue);
	float4 texColor = tex2D(textureSampler, tex);
	
	return texColor*shade;
}

Thats all there is to it. You can get fancy by changing the color of the shades by having animated gradient textures or using say a yellow-red gradient when there is an explosion, but the code at the pixel shader level is the same.

Outlines:
As you noticed, the dbz anime guy has outlines as does the guy from jet grind radio. Cel shading won't give you these outlines. Thankfully outlining is really easy.

How it works:
-Render normally
-Render in black with inverted culling order and move vertices along the normal

The more you move the verts along the normal, the thicker the outlines.

Vertex Shader Code:

float4x4 matWorldViewProjection; 
float    outlineThickness;

struct VS_OUT
{
	float4 position : POSITION;
	float2 tex	    : TEXCOORD0;
}

VS_OUT NonOutlinedVS(float4 position : POSITION, float2 tex : TEXCOORD0)
{
	VS_OUT outVertex;
	
	outVertex.tex = tex;
	outVertex.position = mul(position, matWorldViewProjection);
	
	return outVertex;
}

VS_OUT OutlineVS(float4 position : POSITION, float3 normal : NORMAL, float2 tex : TEXCOORD0)
{
	VS_OUT outVertex;
	
	position += normal*outlineThickness;
	
	outVertex.tex = tex;
	outVertex.position = mul(position, matWorldViewProjection);	
	
	return outVertex;
}

float4 NonOutlinePS(float2 tex : TEXCOORD0) : COLOR
{
	return tex2D(texSampler, tex);
}

float4 OutlinePS() : COLOR
{
	return float4(0, 0, 0, 1.f);
}

technique Outline
{
	pass p0
	{
		VertexShader = compile vs_1_1 NonOutlineVS();
		PixelShader  = compile ps_1_1 NonOutlineVS();
	}
	pass p1
	{
		VertexShader = compile vs_1_1 OutlineVS();
		PixelShader  = compile ps_1_1 OutlinedPS();
	}
}

So now if you want to have cel-shading with outlines, replace the NonOutlineVS/PS code with the cel-shading code.

Final Result
---->
RiZ '06