Normal Mapping Without Precomputed Tangents

Posted By: HeelX

Normal Mapping Without Precomputed Tangents - 04/05/13 13:48

Hi,

when you are doing per-pixel shading and you use precomputed tangents, you are bending a vertex normal probably like this:

Code:
float3x3 matTangent;

struct VS_IN
{
	//...
	float3 Normal : NORMAL;
	float4 Tangent : TEXCOORD2;
	//...
};

struct VS_OUT
{
	//...
	float3 Normal : TEXCOORD2;
	float3 Tangent : TEXCOORD3;
	float3 Binormal : TEXCOORD4;
	//...
};

VS_OUT VS (VS_IN In)
{
	//...
	Out.Normal = mul(In.Normal.xyz, (float3x3)matWorld);
	Out.Tangent = mul(In.Tangent.xyz, (float3x3)matWorld);
	Out.Binormal = mul(cross(In.Tangent.xyz, In.Normal.xyz) * In.Tangent.w, (float3x3)matWorld);
	//...
}

float4 PS (VS_OUT In): COLOR
{
	//...
	float3 tsBump = (tex2D(smpNormal, In.Texcoord.xy).rgb * 2 - 1);	
	float3 wsBumpNormal = normalize(In.Tangent * tsBump.r + In.Binormal * tsBump.g + In.Normal * tsBump.b);
	//...
}



This is fast, easy and the classic approach, but requires tangent precomputation. If this is not feasible (too many texcoords used or for whatever reason it is not desired), you can compute the tangent frame on the fly in the pixel shader just with the vertex normal, the view direction to the pixel's surface position and the texcoord, like this:

Code:
struct VS_IN
{
	//...
	float3 Normal : NORMAL;
	//...
};

struct VS_OUT
{
	//...
	float3 Normal : TEXCOORD2;
	float3 ViewDir : TEXCOORD3;
	//...
};

VS_OUT VS (VS_IN In)
{
	//...
	Out.Normal = mul(In.Normal.xyz, (float3x3)matWorld);
	Out.ViewDir = vecViewPos - mul(In.Pos.xyz, (float3x3)matWorld);
	//...
}

// Calculates a cotangent frame without precomputed tangents by Christian Schüler
// ported from GLSL to HLSL; see: http://www.thetenthplanet.de/archives/1180
float3x3 calcWsCotangentFrame (float3 wsNormal, float3 wsInvViewDir, float2 tsCoord)
{
    // get edge vectors of the pixel triangle
    float3 dp1 = ddx(wsInvViewDir);
    float3 dp2 = ddy(wsInvViewDir);
    float2 duv1 = ddx(tsCoord);
    float2 duv2 = ddy(tsCoord);
 
    // solve the linear system
    float3 dp2perp = cross(dp2, wsNormal);
    float3 dp1perp = cross(wsNormal, dp1);
    float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
 
    // construct and return a scale-invariant cotangent frame
    float invmax = rsqrt(max(dot(T,T), dot(B,B)));
    return float3x3(T * invmax, B * invmax, wsNormal);
}

float4 PS (VS_OUT In): COLOR
{
	//...
	float3 tsBump = (tex2D(smpNormal, In.Texcoord.xy).rgb * 2 - 1);	
	float3 wsBumpNormal = normalize(mul(tsBump, calcWsCotangentFrame(In.Normal, -In.ViewDir, In.Texcoord)));
	//...
}


Of course this is slower than with precomputed tangents, but only about ~14 instructions and requires shader model 3.0. It is in particular useful for procedural geometry. The code was ported from GLSL and is from Christian Schüler (see his blog).

I hope this is useful for some people out there.
Posted By: Kartoffel

Re: Normal Mapping Without Precomputed Tangents - 04/05/13 14:02

Thanks for sharing this.
But aren't the tangents usually computed on the cpu?
So this should be faster when using a lot of geometry (especially for animated models).
Posted By: HeelX

Re: Normal Mapping Without Precomputed Tangents - 04/05/13 15:38

I am not 100% sure, but I guess so. When you clip pixels with an early z-pass, have GPU animated models and lots of geometry, I guess you can benefit from this technique. By the way, the author claims that the visual results are also more pleasing than the standard approach; I guess this comes in handy on large deformed body parts.
Posted By: Hummel

Re: Normal Mapping Without Precomputed Tangents - 04/06/13 18:14

Thanks for sharing. Have you done some stability testing? As far as I remember, the old ShaderX4 version wasn't that stable.

EDIT:
Code:
Out.ViewDir = vecViewPos - mul(In.Pos.xyz, (float3x3)matWorld);

-> shouldn't this be:
Code:
Out.ViewDir = vecViewPos - mul(float4(In.Pos.xyz, 1), (float4x4)matWorld);

instead?
Posted By: HeelX

Re: Normal Mapping Without Precomputed Tangents - 04/06/13 18:31

Originally Posted By: Hummel
Have you done some stability testing? As far as I remember, the old ShaderX4 version wasn't that stable.
No. To be honest, I forgot the handedness of the precomputed tangent frame which is stored in the tangent .w component and tried this approach instead - and it worked in an instant! After I noticed the missed handedness factor (to be multiplied with the crossed' binormal) I switched back to precomupted tangents and skipped the pixelshader approach because of improved performance.

Originally Posted By: Hummel
shouldn't this be ... instead?
Hm. I did it that way because position- and direction vectors are for me always XYZ... why should I use the full homogenous transform? However, I see no noticeable difference if I use the full 4x4 homogenous transform and cast back to the XYZ vector. And last but not least, in the default.fx the world matrix is being casted to a 3x3, too, if a XYZ vector is passed. So.. I guess this shouldn't be too wrong, I guess smile
Posted By: Hummel

Re: Normal Mapping Without Precomputed Tangents - 04/06/13 18:58

But you are missing the translation this way. Might very well be that for the cotangent frame calculation it is not relevant since you are using only derivatives.

To be precise it should be:
Code:
Out.ViewDir.xyz = vecViewPos.xyz - mul(float4(In.Pos.xyz, 1), (float4x4)matWorld).xyz;

© 2024 lite-C Forums