Yeah, for whatever reason Unity simply did not include a function to do that with their Surface shaders, which are roughly analogous to Unreal’s node based material, in terms of hiding most of the complicated parts of the shader from the user and just letting them fill out a list of needed values.
So you understand, there’s no problem doing any of this in a vertex fragment shader. It’s fairly straight forward there, you compute the data you need and use it, done. All this pain is from Unity Surface Shader generator code making assumptions and doing unnecessary optimizations.
The node based shader tools for Unity, Shader Forge and Amplify Shader Editor, both have ways to handling world to tangent space transforms. Shader Forge is all custom and directly generates vertex fragment shaders, so it has no problems. Amplify Shader Editor builds on top of Surface Shaders, so they do have this problem, but my understanding is they solve it with the brute force method … basically the option #2 I linked to above.
Because surface shaders suck if you want to do “simple” things. Here’s a shader that converts a world space normal map to tangent space and outputs the color in UV space.
Shader Code
Shader "Custom/World Normal to Tangent Normal" {
Properties {
[NoScaleOffset] _Normal ("World Normal", 2D) = "bump" {}
}
SubShader
{
LOD 100
Pass
{
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
// float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 texcoord : TEXCOORD0;
half3x3 worldToTangent : TEXCOORD1;
};
sampler2D _Normal;
v2f vert (appdata v)
{
v2f o;
o.pos = float4(v.texcoord * 2.0 - 1.0, 0.5, 1.0);
o.texcoord = v.texcoord;
half3 wNormal = UnityObjectToWorldNormal(v.normal);
half3 wTangent = UnityObjectToWorldDir(v.tangent.xyz);
// compute bitangent from cross product of normal and tangent
half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
// output the world to tangent space matrix
o.worldToTangent = half3x3(wTangent, wBitangent, wNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half3 worldNormal = tex2Dlod(_Normal, float4(i.texcoord.xy, 0, 0)).rgb * 2.0 - 1.0;
return fixed4(normalize(mul(i.worldToTangent, worldNormal)), 1);
}
ENDCG
}
}
}
The issue is Unity’s Surface shader generator is pre-maturely optimizing it away, since the data is being used, just not in a way the generator was written to check for. Hence the error. They could pass the correct data along 100% of the time and the shader compiler would properly optimize it away for them, but they don’t. The vast majority of the time this isn’t a problem and it reduces shader compile times, so there is a reason they’re doing it, but since they did not anticipate people wanting to output world space normals in Surface shaders it becomes an issue for the few of us who do. The funny thing is after the surface function it immediately transforms the tangent space normal output of the surf function into world space. Much could be solved by having an option to simply skip that line. However I have a suspicion that Unity is looking to sunset Surface Shaders entirely as they’re overhauling their rendering systems from the ground up, so I don’t expect to see this feature ever added.
Just that my hack to get access to the tangents seems to fail when using the custom lighting model, but I didn’t spend time to figure out why. I converted the first shader you posted to use Standard and it all “just worked” (with the addition of WorldNormalVector() * 0.0001), but I could not reproduce it with the custom lighting function. With the “option #2” you don’t have to worry about that.