【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考

1.切线空间

uv空间,u,v定义可以类比顶点坐标系x,y,z.
u表示纹理水平方向,v表示纹理竖直方向

Tangent Space,其实一个坐标系,也就是原点+三个坐标轴决定的一个相对空间,
我们只要搞清楚原点和三个坐标轴是什么就可以了。

在Tangent Space中,
坐标原点就是顶点的位置,
z轴是该顶点本身的法线方向(N)。
(tangent)T 与该点相切的切线,这样的切线本来有无数条,但模型一般会给定该顶点的一个tangent,这个tangent方向一般是使用和纹理坐标方向相同的那条tangent(T)。
(bitangent或者说binormal)B,副切线或者说副法线,是通过normal和tangent的叉乘得到相互垂直的线。

(下面有几张图,来自知乎,侵删)
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第1张图片
v2-5f2b99607cf1790cdd955149f0436ae2_hd.jpg
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第2张图片
v2-cc97e8e2d0eff2084045cf19d61bf58b_hd.jpg
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第3张图片
v2-af0f3390357b898decbb9e7a50046822_hd.jpg
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第4张图片
v2-fc03b4d2c6628f502b3a3daf59d290d2_hd.jpg

2.切线空间矩阵推导

【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第5张图片
[图片上传中...(20170228214738798.png-5153d1-1524496131680-0)]
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第6张图片
20170228214738798.png

该图显示了一个三角形及其所在的切线空间。
注意在局部坐标系里,因为这个局部的切平面式垂直于Z轴即法线的,所以虽然uv坐标式二维的,但是只有加上第三维z等于0即可以当成一个三维坐标使用

接上图我们有如下等式:


写成矩阵形式



即:


基本到这里就可以求出切线空间的T和B,即tangent 和binormal了,虽然理论上来说是这么算,但在实际的unity shader中,tangent是直接由unity从内部传入到shader数据结构中,比如如下结构,然后binormal则有normal和tangent进行叉乘得到

struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;//切线方向
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

网上有说 tangent是由外部模型软件在导出的时候就已经同时计算好了tangent信息,但是否真是这样,还是说untiy内部进行了tangent的计算,不得而知,希望有看到的童鞋在评论中给出答案。不胜感激!
以我的观点来说,应该是在三维软件中会自带tangent,当然,在程序角度来说,如果你想单纯使用代码的方式来生成一个三维物体,那么tangent则需要你自己用上述方法在程序中实现。

3.切线空间法线扰动实践

在网上随便找了两张图,效果不是特别好,但意思到了也可以了
我也贴出来吧


【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第7张图片
p3.jpg
【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第8张图片
part1.jpg

这里要特别注意,需要编辑器的图片材质中把法线贴图类型转换成Normal贴图类型


【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第9张图片
QQ截图20180425233630.jpg

凹凸为0的效果图


【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第10张图片
QQ截图20180425233551.jpg

调整凹凸参数得到的效果图


【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考_第11张图片
QQ截图20180425233612.jpg

还是很明显的可以看出区别来

shader代码如下

Shader "Custom/NormalMapTangentSpaceMat"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _BumpMap("Normal Map",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        _Specular("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss",Range(8.0,256)) = 20
    }
        SubShader
        {
            Tags { "RenderType" = "Opaque"
                "LightMode" = "ForwardBase"
            }

            LOD 100

            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                // make fog work
                #pragma multi_compile_fog

                #include "UnityCG.cginc"
                #include "Lighting.cginc"

                fixed4 _Color;
                sampler2D _MainTex;
                float4 _MainTex_ST;
                sampler2D _BumpMap;
                float4 _BumpMap_ST;
                float _BumpScale;
                fixed4 _Specular;
                float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv : TEXCOORD0;
                float3 lightDir:TEXCOORD1;
                float3 viewDir:TEXCOORD2;
            };
             
            
            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
                 
                TANGENT_SPACE_ROTATION;
                o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
 

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentViewDir = normalize(i.viewDir);

                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);

                fixed3 tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss);

                return fixed4(ambient + diffuse+ specular, 1.0);

            }
            ENDCG
        }
    }
}

这里用了unity 内建宏定义

TANGENT_SPACE_ROTATION

#define TANGENT_SPACE_ROTATION \
    float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
    float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
1.为啥这里要乘以v.tangent.w,

经过一段时间的搜索找到如下解释,有勘误还请指正
首先我们切换回到纹理的定义
对于纹理UV来说,
OpenGL 从左到右(0,1),从下到上(0,1)
DirectX 从左到右(0,1),从下到上(1,0)
可见对于切线 ,即UV的U,对于OpenGL和DirectX都是左到右(左侧为0.0,右侧为1.0)。
而对于binormal,即UV的V,这在OpenGL和DirectX中是不同的。
OpenGL是自下而上的,DirectX是自上而下的。
这也是许多引擎和3D工具对“+ Y / -Y”法线贴图的偏好来自不同的地方。
Unity是+ Y,这是OpenGL标准,
Unreal是-Y,这是DirectX标准。
显然,Unity在Windows上使用DirectX,所以大部分时间W组件都是负值的。
然而,如果纹理UV被反转,不是因为网格反转,而是因为网格的纹理被镜像。
所以,如果纹理被镜像,则需要w来存储这个值

2.rotation 为什么表达了从object空间到tangent空间的转换
    float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

首先我们知道float3x3( v.tangent.xyz, binormal, v.normal )是按行优先往矩阵里放东西的,也就是说
这个东西
其实等同于如下矩阵


等同于矩阵

我们竖过来写是因为,一般考虑矩阵左乘的时候,我们把列空间作为基向量,如果放成行向量,那就是右乘了,这样不太符合大众习惯。
接上面,由于是单位正交矩阵,上述矩阵等同于如下表达式。(关于这一点请查阅相关线性代数知识,即正交矩阵的逆矩阵等同于其转置)

我们知道

中,注意,这里面的这三个列向量空间是属于Object空间的,我们前面其实提到了v.normal是属于Object空间的,千万别认为是v.normal是局部的[0,0,1],所以它表达了从Tangent空间到Object空间的转换关系,关于这一点请参照相关线性代数,尤其要理解列空间基向量的概念。
所以,自然的
表达的是从Object空间到Tangent空间的转换关系。
也即
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
表达了从Object空间到Tangent空间的转换关系。
得证

4.附:世界空间下的法线扰动测试

效果同上,代码如下

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/NormalMapWorldSpaceMat"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex ("Main Texture", 2D) = "white" {}
        _BumpMap("Normal Map",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        _Specular("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss",Range(8.0,256)) = 20
    }
        SubShader
        {
            Tags { "RenderType" = "Opaque"
                "LightMode" = "ForwardBase"
            }

            LOD 100

            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                // make fog work
                #pragma multi_compile_fog

                #include "UnityCG.cginc"
                #include "Lighting.cginc"

                fixed4 _Color;
                sampler2D _MainTex;
                float4 _MainTex_ST;
                sampler2D _BumpMap;
                float4 _BumpMap_ST;
                float _BumpScale;
                fixed4 _Specular;
                float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 TtoW0:TEXCOORD1;//插值寄存器只能存储最多float4大小的分量,所以这里将矩阵拆分三行向量,
                float4 TtoW1:TEXCOORD2;
                float4 TtoW2:TEXCOORD3;
                float4 TtoW3:TEXCOORD4;
                float3 worldPos:TEXCOORD5;
            };
             
            
            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
                 
                o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
                 

                //注意这里的worldNormal,worldTangent,worldBinormal,均表达了模型空间到世界空间的转换,因此三个列空间基向量排列构成的矩阵表达了从模型空间到世界空间的变换,对此存在疑问的童鞋可以查看相关线性代数课程
                //本人也写过线性代数方面的总结性文章,可以查看本人主页查找线性代数。
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, 0);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, 0);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, 0);
                o.TtoW3 = float4(0, 0, 0, 1);//存粹为了好看,其实这些完全可以省略

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldPos = i.worldPos;
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));


                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                //将切线空间的法线转化到世界空间。
                fixed3 worldNormal = normalize(fixed3(dot(i.TtoW0.xyz,tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));


                ///same
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);

                return fixed4(ambient + diffuse+ specular, 1.0);

            }
            ENDCG
        }
    }
}

你可能感兴趣的:(【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考)