Working in TBN space

This shader example demonstrates the possibilities of TBN (Tangent, Binormal, Normal) space:

tbnV.hlsl :

#define IN_HLSL
#include "shdrConsts.h"
#include "hlslStructs.h"

uniform float4x4 modelview;
uniform float4x4 objTrans;  

struct VS_INPUT
float4 pos : POSITION;
float3 T : TANGENT0;
float3 N : NORMAL0;
float2 texc : TEXCOORD0;

struct VS_OUTPUT
float4 pos : POSITION;
float2 texc : TEXCOORD0;
float3 T : TEXCOORD1;
float3 B : TEXCOORD2;
float3 N : TEXCOORD3;
float4 wpos : TEXCOORD4;

float texScale = 1;


OUT.pos = mul(modelview,float4(,1));

Here we need to save our world position.

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

We preserve our texcoord.
texScale is a texture scale factor.

OUT.texc = IN.texc * texScale;

Working in TBN space is far more complicated.
First we need to pull our tangent, normal and binormal (bitangent).
We pull T and N, crossing them will give us B.
Then move TBN to object's world space.

float3 T = normalize(mul(objTrans,;
float3 B = normalize(mul(objTrans,cross(,;
float3 N = normalize(mul(objTrans,;

We pass T, B and N to our pixel shader.

OUT.T = T;
OUT.B = B;
OUT.N = N;

return OUT;

tbnP.hlsl :

#define IN_HLSL
#include "shdrConsts.h"
#include "hlslStructs.h"

For each map we plan to sample, we must define a sampler.
We can use the sampler multiple times.
There are four types of textures (1D, 2D, 3D, and cube map).
We sample them using tex1D(), tex2D(), tex3D() and texCUBE().
Currently T3D does not support volume maps, so tex3D() will act the same as tex2D().
Of course there are more sampling intrinsics, but they are not our target right now.
So our samplers are sampler1D, sampler2D, sampler3D and samplerCUBE.
Samplers' data is saved in gpu's s# registers.
Samplers are supported in shader model 2+ due to the sampler registers support after sm2.0.
The sampler unit corresponds to a texture stage.
A texture can possess several samplers, but they are independant each other, therefore texture copies are unified.

uniform sampler2D diff0 : register(S0);
uniform sampler2D norm0 : register(S1);
uniform float3 eyePos;

Each pixel shader has its input and output semantics.
We pass the texture coordinates, TBN and the world vertex position.
We output a color.

struct PS_INPUT
float2 uv : TEXCOORD0;
float3 T : TEXCOORD1;
float3 B : TEXCOORD2;
float3 N : TEXCOORD3;
float4 wpos : TEXCOORD4;
struct PS_OUTPUT
float4 color : COLOR0;

float Lspower = 64; // light specular exponent
float3 Lpos = {467,409,250}; // light world position
float4 Lcolor = {0,1,0,1}; // light color
float4 ambient = {0.4,0.4,0.4,1}; // object ambient component
float Lrange = 50; // light range
float3 luminanceVector = {0.3,0.59,0.11}; // luminance vector is a well known vector in HLSL

We construct the TBN matrix here.

float3x3 TBN = {normalize(IN.T),normalize(IN.B),normalize(IN.N)};

Sample signed bump here.

float3 tN = normalize(tex2D(norm0, IN.uv)*2 - 1);

We get the eye vector, eyePos is hardcoded in T3D.

float3 eyeVec = normalize(eyePos - IN.wpos);

Move eyeVec to texture space.
Notice that the TBN is the second parameter, because we need to transpose it.
May be the right way is to use transpose(), but this time we'll do it on the fly.

eyeVec = mul(eyeVec,TBN);

We sample the diffuse color.

float4 diffuseColor = tex2D( diff0, IN.uv );

We calculate the light vector and move it to texture space.

float3 L = ( -;
L = mul(L,TBN);

Ok, now we need to calculate the dot products.
For them we need normalized vectors.
Therefore this is a good place to pull the length and calculate the light attenuation.

float distanceToLight = length(L);
float attenuation = saturate((Lrange - distanceToLight) / Lrange);

It is time to calculate the diffuse component of the light.

L = normalize(L);
float diffuse = saturate(dot(tN,L));

We calculate the specular component.

float3 H = normalize(L + eyeVec);
float spec = pow( saturate(dot(tN,H)), Lspower )*(1-pow(1-diffuse,10));

Final color calculation.

float3 color = ambient + (diffuse * attenuation * Lcolor) + (spec * attenuation * Lcolor * smoothstep(0.2,0.8,dot(,luminanceVector)));
Out.color = float4(color,1.0) * diffuseColor;
return Out;

materials.cs :

singleton ShaderData(LightTutorialShader)
DXVertexShaderFile = "shaders/common/tbnV.hlsl";
DXPixelShaderFile = "shaders/common/tbnP.hlsl";

pixVersion = 3.0;

singleton CustomMaterial(LightTutorialMat)
mapTo = "testshapetex";
sampler["diff0"] = "";
sampler["norm0"] = "";
shader = LightTutorialShader;

version = 3.0;