Sub-Surface Scattering

This shader example demonstrates the Sub-Surface Scattering effect:

sssV.hlsl :

#define IN_HLSL
#include "shaders/common/shdrConsts.h"
#include "shaders/common/hlslStructs.h"

uniform float4x4 modelview;
uniform float4x4 objTrans;
uniform float3 vEye;

struct VS_INPUT
{
float4 pos : POSITION;
float3 T : TANGENT;
float3 N : NORMAL;
float2 texc : TEXCOORD0;
};

struct VS_OUTPUT
{
float4 pos : POSITION;
float2 texc : TEXCOORD0;
float3 wN : TEXCOORD1;
float3 eyeDir : TEXCOORD2;
float3 wT : TEXCOORD3;
float3 wB : TEXCOORD4;
float3 wPos : TEXCOORD5;
};

VS_OUTPUT main(VS_INPUT IN)
{
VS_OUTPUT OUT;

Move TBN to world space.

OUT.wT = normalize(mul(objTrans,IN.T).xyz);
OUT.wB = normalize(mul(objTrans,cross(IN.T,IN.N)).xyz);
OUT.wN = normalize(mul(objTrans,IN.N).xyz);

Save our world position.

OUT.wPos = mul(objTrans,IN.pos);

Preserve our texcoords.

OUT.texc = IN.texc;

Get the eye direction.

OUT.eyeDir = -normalize(vEye);

Transformation.

OUT.pos = mul(modelview,IN.pos);
return OUT;
}



sssP.hlsl :

#define IN_HLSL
#include "shaders/common/shdrConsts.h"
#include "shaders/common/hlslStructs.h"

We need three samplers.

uniform sampler2D diff0 : register(S0);
uniform sampler2D thick0 : register(S1);
uniform sampler2D norm0 : register(S2);

struct PS_INPUT
{
float2 texc : TEXCOORD0;
float3 wN : TEXCOORD1;
float3 eyeDir : TEXCOORD2;
float3 wT : TEXCOORD3;
float3 wB : TEXCOORD4;
float3 wPos : TEXCOORD5;
};

struct PS_OUTPUT
{
float4 color : COLOR0;
};

float getProduct(float3 V1, float3 V2)
{
return dot(V1, V2) * 0.5 + 0.5;
}

In this example we'll calculate the specular component using the 'half angle".
Ok, what exactly is a half angle ?
H = L+E
It is a vector pointing half-way between the Light and Eye vectors.
Using a half vector is more expensive, but will lead to better specular results.
NdotH is the dot product between the normal and the half angle.
NdotH = 1 for the vertex directly in front of the camera/light.
Now, let us move our camera 90 degrees around the object.
The half angle is at 45 degrees from our start point, so if we are concerned to the NdotH of the vertex directly in front of us, it should return 0.5.
NdotH = 0 when you are looking at the light towards a surface pointing directly away from it (the light on one side of the object and the eye at the complete opposite).

float addSpecular(float3 wN, float3 Ldir, float Spower)
{
float3 halfAngle = normalize(wN + Ldir);
return pow(saturate(dot(wN, halfAngle)), Spower);
}

PS_OUTPUT main(PS_INPUT IN)
{
PS_OUTPUT OUT;

float3 Lpos = {467,408,250}; // light position
float3 Lcolor = {1.0, 1.0, 1.0}; // light color
float3 Scolor = {1.0, 1.0, 1.0}; // specular color
float Spower = 64; // specular exponent

float skinDepth = 0.3;
float3 heat = {0.8,0.1,0.1}; // heat color
float rimMult = 1.5;
float influenceMult = 12; // light source influence

We calculate the light influence and light direction.

float influence = (1.0f/distance(Lpos, IN.wPos)) * influenceMult;
float3 Ldir = normalize(Lpos - IN.wPos);

We store the world normal.
The reason we are normalizing again is because the vertex shader sometimes can cause problems.
And we can pull unnormalized normal, therefore we are doing it again.

float3 wN = normalize(IN.wN);

We sample our normal map and recalculate wN.

float3 bumpColor = tex2D(norm0, IN.texc);
wN.x = dot(bumpColor.x, IN.wT);
wN.y = dot(bumpColor.y, IN.wT);
wN.z = dot(bumpColor.z, IN.wN);

We calculate the light additive color.

float4 LdotN = getProduct(Ldir, wN) * influence;
float3 Lcomponent = (float3)(skinDepth * max(0, dot(-wN, Ldir)));
Lcomponent += skinDepth * getProduct(-IN.eyeDir, Ldir);
Lcomponent *= influence;

Apply some heat - this is our predominant light color.

Lcomponent.r *= heat.r;
Lcomponent.g *= heat.g;
Lcomponent.b *= heat.b;
Lcomponent.rgb *= tex2D(thick0, IN.texc).r;
float3 edgeHeat = (float3)(1.0f - max(0.0f, dot(wN, IN.eyeDir)));
edgeHeat *= edgeHeat;
edgeHeat *= max(0.0f, dot(wN, Ldir)) * Scolor;

Here we are ready to calculate the final color.

LdotN *= tex2D(diff0, IN.texc);
float4 finalCol = LdotN + float4(Lcomponent, 1.0f);
finalCol.rgb += (edgeHeat * rimMult * influence * finalCol.a);
finalCol.rgb += (addSpecular(wN, Ldir, Spower) * influence * Scolor * finalCol.a * 0.05f);
finalCol.rgb *= Lcolor;
OUT.color = finalCol;
return OUT;
}

Now we are ready to test the shader.
Be sure your light position is very close to the object, because we defined a very little influence.



materials.cs :

singleton ShaderData(SSSShaderData)
{
DXVertexShaderFile = "shaders/common/sssV.hlsl";
DXPixelShaderFile = "shaders/common/sssP.hlsl";

pixVersion = 3.0;
};

singleton CustomMaterial(SSS)
{
mapTo = "testshapetex";

sampler["diff0"] = "diffusemap.dds";
sampler["thick0"] = "normalmap.dds";
sampler["norm0"] = "normalmap.dds";

shader = SSSShaderData;
version = 3.0;
};

back...