1 registered members (SBGuy),
652
guests, and 3
spiders. |
Key:
Admin,
Global Mod,
Mod
|
|
|
Normal Mapping Without Precomputed Tangents
#420882
04/05/13 13:48
04/05/13 13:48
|
Joined: Jul 2001
Posts: 6,904
HeelX
OP
Senior Expert
|
OP
Senior Expert
Joined: Jul 2001
Posts: 6,904
|
Hi, when you are doing per-pixel shading and you use precomputed tangents, you are bending a vertex normal probably like this:
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:
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.
Last edited by HeelX; 04/05/13 13:48.
|
|
|
Re: Normal Mapping Without Precomputed Tangents
[Re: HeelX]
#420883
04/05/13 14:02
04/05/13 14:02
|
Joined: Jun 2009
Posts: 2,210 Bavaria, Germany
Kartoffel
Expert
|
Expert
Joined: Jun 2009
Posts: 2,210
Bavaria, Germany
|
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).
POTATO-MAN saves the day! - Random
|
|
|
Re: Normal Mapping Without Precomputed Tangents
[Re: HeelX]
#420925
04/06/13 18:14
04/06/13 18:14
|
Joined: Mar 2006
Posts: 2,252
Hummel
Expert
|
Expert
Joined: Mar 2006
Posts: 2,252
|
Thanks for sharing. Have you done some stability testing? As far as I remember, the old ShaderX4 version wasn't that stable. EDIT:
Out.ViewDir = vecViewPos - mul(In.Pos.xyz, (float3x3)matWorld);
-> shouldn't this be:
Out.ViewDir = vecViewPos - mul(float4(In.Pos.xyz, 1), (float4x4)matWorld);
instead?
Last edited by Hummel; 04/06/13 18:18.
|
|
|
Re: Normal Mapping Without Precomputed Tangents
[Re: Hummel]
#420926
04/06/13 18:31
04/06/13 18:31
|
Joined: Jul 2001
Posts: 6,904
HeelX
OP
Senior Expert
|
OP
Senior Expert
Joined: Jul 2001
Posts: 6,904
|
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. 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
Last edited by HeelX; 04/06/13 18:32.
|
|
|
Re: Normal Mapping Without Precomputed Tangents
[Re: HeelX]
#420927
04/06/13 18:58
04/06/13 18:58
|
Joined: Mar 2006
Posts: 2,252
Hummel
Expert
|
Expert
Joined: Mar 2006
Posts: 2,252
|
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:
Out.ViewDir.xyz = vecViewPos.xyz - mul(float4(In.Pos.xyz, 1), (float4x4)matWorld).xyz;
Last edited by Hummel; 04/06/13 19:01.
|
|
|
|