法线贴图
法线贴图(或是法线纹理)其实就是一张图片中的RGB通道分别存储着法线方向的纹理(有些为了数据压缩将X,Y存储在RG通道,Z是通过1-dot(xy,xy)来近似计算)。
它的由来是因为高模运行低效,所以使用了一种贴图将模型的表面凹凸方向的细节都存在一张图。然后再低模中的片段着色阶段对贴图采样出来的数据当做法线来模拟计算凹凸导致的光影强弱现象。
因为大多数凹凸都可以用光影强弱来伪装实现,就好像我们在一张纸上画出很有凹凸,或是很有立体感的东西,因为我们将光影细节也画进去了,所以看起来很逼真。
所以在计算机3D图形渲染上,我们也可以使用相同的方式来处理,那么光影与法线有啥关系啊?因为我们的shader计算光照模型,基本离不开法线数据来依赖计算。
法线用在物体表面的光反射计算
看图理解:
物体反射光影强弱:而眼睛(我们的相机)接收到的光影强度与这个夹角大小成反比,所以光影强弱,就是表示物体表面法线的方向相对我们接收方向有所不一导致的。
在法线贴图RGB通道存储着某个模型表面的法线向量数值,这些向量可以是相对一个的该模型的:世界空间、对象空间、或是切线空间。
法线贴图种类
-
世界空间的法线,使用局限性比较大:那就是这个模型只能是静止的(不能有transform的变化,即:缩放、旋转、位移都不能用),性能上比较高吧,因为shader中也没有必要为法线贴图的数值进行空间的转换。
-
对象空间的法线,使用局限性没有世界空间的那么大,它可以用于同一个模型下的所有对象中使用,这些对象,可以有不一样的位移,旋转,缩放的情况下使用。
但缩放的话需要x,y,z缩放都是统一的值(x,y,z都相同标量),否则使用这个非统一缩放的变换矩阵应用到法线变换的话,会有方向不对的问题。
如下图:
- 切线空间的法线
为此,又多了一种叫,切线空间纹理。
为何叫切线,在世界空间下看起来就像是带有曲线似的外表的模型中,我们的切线坐标x,y是与模型表面平行的,就像是曲面的切线似的,因为叫切线。
在切线空间,我们的法线向量相对每个多边形表面空间坐标来存储的。
以纹理u,v对应表面纵横向:切线x,副切线y,同时垂直于两切线的是法线z,以uv采样对应出来的数据就是,这三个(x,y,z)数据组成的方向。
因为这存在于切线空间,所以不论我们的模型再怎么变换,顶点映射的uv坐标都不会变,对应的切线空间法线方向就可以采样出来计算该片段的表面信息
另外,我们的切线空间的法线贴图,为何偏蓝色,看下面的图来理解:
- 法线向量对应颜色(0~1)范围的:normal:(-1,0,1)对应颜色color:(0,0.5,1)
- 对应(0~255)颜色范围:normal:(-1,0,1)对应颜色color:(0,127,255)
根据上图,normal.z分量不可能小于0,对应255范围颜色,就是不小于127的分量值,所以切线空间的法线整体会偏蓝色
切线空间的法线数据烘焙到纹理中
再来看看法线贴图数据对应的烘焙的高模表面方向数据
如这么一张切线空间的偏蓝色的法线贴图
关于切线空间介绍也可以看这里。
Examples
前面说了一些关于法线贴图的东西,更多可以查看:References提供的连接,这里推荐一篇法线的博文
但关于切线空间的使用,我们下面分两种方式来实现:
在开始看下面例子前,先说明一下,我们的T2W意思是:Tangent space To World space的简写
W2T是:World space To Tangent space的。
切线空间的法线贴图T2W方式应用
下面例子对应Project的Unity工程中的T2W.unity场景
下面是主要的shader
// jave.lin 2019.06.27
Shader "Test/TestTangent2WorldNormalMap"{
Properties{
_Color("Main Color", Color) = (1,1,1,1) // 主色调
_NormalMap("Normal Map", 2D) = "bump"{} // 法线贴图
_NormalScale("NormalScale", Range(0, 10)) = 1 // 法线的凹凸程度
_HalfLambertIntensity("Half-Lambert Itensity", Range(0, 1)) = 0 // Diffuse的半Lambert强度
_RimIntensity("Rim Intensity", Range(0, 2)) = 0.1 // 使用法线检测边缘光的强度
_AmbientIntensity("Ambient Intensity", Range(0, 1)) = 0.5 // 使用法线检测边缘光的强度
_SpecularIntensity("Specular Intensity", Range(0, 100)) = 1 // 高光模型强度系数1
_SpecularStrengthen("Specular Strengthen", Range(0, 1)) = 1 // 高光模型强度系数2
_SelfShadowIntensity("Self Shadow Intensity", Range(0, 1)) = 0.35 // 自阴影强度
}
SubShader{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
//#define H_PRECISION
#ifdef H_PRECISION
#define vec3 float3
#define vec4 float4
#define mat3x3 float3x3
#define uv2 float2
#define uv4 float4
#else
#define vec3 fixed3
#define vec4 fixed4
#define mat3x3 fixed3x3
#define uv2 half2
#define uv4 half4
#endif // end H_PRECISION
// 下面的数据精度不能丢
#define color3 fixed3
#define color4 fixed4
#define pos3 float3
#define pos4 float4
#define scalar32 float
#define scalar16 half
#define scalar11 fixed
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f {
pos4 pos : SV_POSITION;
mat3x3 tbn : TEXCOORD0;/* 0,1,2 */
uv2 uv : TEXCOORD3;
pos3 worldPos : TEXCOORD4;
LIGHTING_COORDS(5, 6)
};
color4 _Color;
scalar32 _RimIntensity;
scalar32 _AmbientIntensity;
scalar32 _SpecularIntensity;
scalar32 _SpecularStrengthen;
scalar32 _HalfLambertIntensity;
scalar32 _SelfShadowIntensity;
sampler2D _NormalMap;
half _NormalScale;
uv4 _NormalMap_ST;
inline mat3x3 getTBN (vec3 normal, vec4 tangent) {
vec3 wNormal = UnityObjectToWorldNormal(normal); // 将法线从对象空间转换到世界空间
vec3 wTangent = UnityObjectToWorldDir(tangent.xyz); // 将切线从对象空间转换到世界空间
vec3 wBitangent = cross(wNormal, wTangent) * tangent.w; // 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线,tangent.w 决定朝向
return mat3x3(wTangent, wBitangent, wNormal); // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
}
v2f vert (pos4 vertex : POSITION, uv2 uv : TEXCOORD0, vec3 normal : NORMAL, vec4 tangent : TANGENT){
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
o.tbn = getTBN(normal, tangent);
o.uv = TRANSFORM_TEX(uv, _NormalMap);
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// sample tangent space normal & decode data
// normal
color4 normalData = tex2D(_NormalMap, i.uv);
vec3 normal = UnpackNormal(normalData);
normal.xy *= _NormalScale;
normal = normalize(mul(normal, i.tbn));
// lighting variables
vec3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
vec3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// coefficient
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
atten *= saturate(dot(normal, lightDir));
_HalfLambertIntensity = 1 - _HalfLambertIntensity;
_SelfShadowIntensity = 1 - _SelfShadowIntensity;
// diffuse
scalar32 lightDotNormal = dot(lightDir, normal) * _HalfLambertIntensity * 0.5 + (1 - _HalfLambertIntensity) * 0.5; //halfLambert
color3 diffuse = _Color.rgb * _LightColor0.rgb * lightDotNormal;
// specular
// half angle
//vec3 halfAngleDir = normalize(lightDir + viewDir);
//scalar32 halfAngleDotNormal = max(0, dot(halfAngleDir, normal));
//color3 specular = fixed3(_LightColor0.rgb * pow(halfAngleDotNormal, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// directly reflect
vec3 reflectDir = reflect(-lightDir, normal);
scalar32 refelctDotView = max(0, dot(reflectDir, viewDir));
color3 specular = fixed3(_LightColor0.rgb * pow(refelctDotView, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// rim
scalar32 viewDotNormal = dot(viewDir, normal);
color3 rim = _LightColor0.rgb * (1 - max(0, viewDotNormal)) * _RimIntensity;
// ambient
color3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientIntensity;
// shadow
color3 combined = diffuse + specular + rim + ambient;
color3 uppercolor = combined;
color3 lowwercolor = combined * atten;
color3 shadowcolor = lowwercolor + combined * _SelfShadowIntensity;
combined = clamp(shadowcolor, lowwercolor, uppercolor);
// output
return color4(combined, 1);
}
ENDCG
}
}
Fallback "Diffuse"
}
在上面代码上,我们先看:getTBN函数
inline mat3x3 getTBN (vec3 normal, vec4 tangent) {
vec3 wNormal = UnityObjectToWorldNormal(normal); // 将法线从对象空间转换到世界空间
vec3 wTangent = UnityObjectToWorldDir(tangent.xyz); // 将切线从对象空间转换到世界空间
vec3 wBitangent = cross(wNormal, wTangent) * tangent.w; // 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线,tangent.w 决定朝向
return mat3x3(wTangent, wBitangent, wNormal); // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
}
该矩阵用于对向量的T2W与W2T之间的转换用的,为何它可以使用与两空间的互相转换,因为这矩阵的三轴基向量都是相互垂直的,这里的代码注释也是这么写着,说是正交矩阵的逆矩阵就是转置矩阵的意思。
上面的tangent, normal,都是Unity MeshRenderer或是SkinnedMeshRenderer都帮我们封装好传入到shader中的语义数据,所以我们可以很方便就可以拿来用,关键语义对了就行,Unity shader语义看我之前翻译Unity的一遍关于Unity语义的文章(大神跳过)。
有tangent,normal数据后,我们通过叉乘cross cg函数,就可以求出bitangent副切线了(某些文章看到也有叫副法线,估计都是错了吧,因为表面的切线是可以有N条的,但表面的法线肯定只有一条,所以什么副法线的叫法都是错的吧?)
tangent, normal, bitangent,都齐了,我们就可以通过这三基向量构建T2W,W2T的TBN矩阵了,就是上面的 getTBN 函数了
主要看我们的vert,frag函数,我们在vert中构建tbn矩阵后,传到frag中使用,在frag对normalMap采样并解码出来的切线空间的法线,我们再用tbn与向量左乘,mul(vec, tbn),即可将向量转为world space的。
运行效果:
切线空间的法线贴图W2T方式应用
下面例子对应Project的Unity工程中的W2T.unity场景
下面是主要的shader
// jave.lin 2019.06.27
Shader "Test/TestWorld2TangentNormalMap"{
Properties{
_Color("Main Color", Color) = (1,1,1,1) // 主色调
_NormalMap("Normal Map", 2D) = "bump"{} // 法线贴图
_HalfLambertIntensity("Half-Lambert Itensity", Range(0, 1)) = 0 // Diffuse的半Lambert强度
_RimIntensity("Rim Intensity", Range(0, 2)) = 0.1 // 使用法线检测边缘光的强度
_AmbientIntensity("Ambient Intensity", Range(0, 1)) = 0.5 // 使用法线检测边缘光的强度
_SpecularIntensity("Specular Intensity", Range(0, 100)) = 1 // 高光模型强度系数1
_SpecularStrengthen("Specular Strengthen", Range(0, 1)) = 1 // 高光模型强度系数2
_SelfShadowIntensity("Self Shadow Intensity", Range(0, 1)) = 0.35 // 自阴影强度
}
SubShader{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
//#define H_PRECISION
#ifdef H_PRECISION
#define vec3 float3
#define vec4 float4
#define mat3x3 float3x3
#define uv2 float2
#define uv4 float4
#else
#define vec3 fixed3
#define vec4 fixed4
#define mat3x3 fixed3x3
#define uv2 half2
#define uv4 half4
#endif // end H_PRECISION
// 下面的数据精度不能丢
#define color3 fixed3
#define color4 fixed4
#define pos3 float3
#define pos4 float4
#define scalar32 float
#define scalar16 half
#define scalar11 fixed
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f {
pos4 pos : SV_POSITION;
uv2 uv : TEXCOORD0;
pos3 worldPos : TEXCOORD1;
LIGHTING_COORDS(2, 3)
vec3 lightDir : TEXCOORD4;
vec3 viewDir : TEXCOORD5;
};
color4 _Color;
scalar32 _RimIntensity;
scalar32 _AmbientIntensity;
scalar32 _SpecularIntensity;
scalar32 _SpecularStrengthen;
scalar32 _HalfLambertIntensity;
scalar32 _SelfShadowIntensity;
sampler2D _NormalMap;
uv4 _NormalMap_ST;
// 注意这个tbn因为三个基向量都垂直,所以tbn逆矩阵就是transpose(tbn)
// mul(tbn, vec) ==> tangent space 2 world space
// mul(vec, tbn) or mul(traspose(tbn), vec) ==> word space 2 tangent space
inline mat3x3 getTBN (vec3 normal, vec4 tangent) {
vec3 wNormal = UnityObjectToWorldNormal(normal); // 将法线从对象空间转换到世界空间
vec3 wTangent = UnityObjectToWorldDir(tangent.xyz); // 将切线从对象空间转换到世界空间
vec3 wBitangent = cross(wNormal, wTangent) * tangent.w; // 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线,tangent.w 决定朝向
return mat3x3(wTangent, wBitangent, wNormal); // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
}
v2f vert (pos4 vertex : POSITION, uv2 uv : TEXCOORD0, vec3 normal : NORMAL, vec4 tangent : TANGENT){
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
o.uv = TRANSFORM_TEX(uv, _NormalMap);
mat3x3 tbn = getTBN(normal, tangent);
o.lightDir = mul(tbn, _WorldSpaceLightPos0.xyz); // world 2 tangent
o.viewDir = mul(tbn, _WorldSpaceCameraPos.xyz - o.worldPos); // world 2 tangent
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// sample tangent space normal & decode data
// normal
color4 normalData = tex2D(_NormalMap, i.uv);
vec3 normal = UnpackNormal(normalData);
normal.xy *= _NormalScale;
normal = normalize(normal);
// lighting variables
vec3 lightDir = normalize(i.lightDir);
vec3 viewDir = normalize(i.viewDir);
// coefficient
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
atten *= saturate(dot(normal, lightDir));
_HalfLambertIntensity = 1 - _HalfLambertIntensity;
_SelfShadowIntensity = 1 - _SelfShadowIntensity;
// diffuse
scalar32 lightDotNormal = dot(lightDir, normal) * _HalfLambertIntensity * 0.5 + (1 - _HalfLambertIntensity) * 0.5; //halfLambert
color3 diffuse = _LightColor0.rgb * lightDotNormal;
// specular
// half angle
//vec3 halfAngleDir = normalize(lightDir + viewDir);
//scalar32 halfAngleDotNormal = max(0, dot(halfAngleDir, normal));
//color3 specular = fixed3(_LightColor0.rgb * pow(halfAngleDotNormal, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// directly reflect
vec3 lightReflectDir = reflect(-lightDir, normal);
scalar32 refelctDotView = max(0, dot(lightReflectDir, viewDir));
color3 specular = fixed3(_LightColor0.rgb * pow(refelctDotView, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// rim
scalar32 viewDotNormal = dot(viewDir, normal);
color3 rim = _LightColor0.rgb * (1 - max(0, viewDotNormal)) * _RimIntensity;
// ambient
color3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientIntensity;
// combine the diffuse, specular, rim, ambient
color3 combined = _Color.rgb * diffuse + specular + rim + ambient;
// shadow
color3 uppercolor = combined;
color3 lowwercolor = combined * atten;
color3 shadowcolor = lowwercolor + combined * _SelfShadowIntensity;
combined = clamp(shadowcolor, lowwercolor, uppercolor);
// output
return color4(combined, 1);
}
ENDCG
}
}
Fallback "Diffuse"
}
运行例子,我们将颜色换了,以区别
我们可以看到与T2W的区别是,我们在vert阶段就像所有的光照需要的向量都转到切线空间下的了,所以我们的frag阶段的计算相比T2W来说就少了一个矩阵左乘的运算量,frag的运算量尽量减少还是对的。这样我们在frag所有光照都只要在切线空间下计算就OIK了。
但是W2T的方式还是有个问题:
如果你的切线空间的法线贴图需要在frag中用在环境cube采样,那么我建议你用T2W,来看看我的例子就知道了。虽然两种都可以:
将切线空间的法线用于frag的环境cube采样(texCUBE)
unity的切线空间的法线贴图的shader例子在这:Unity Vertex/Fragment Shader Normal Mapping Example,也可以去看看,不过他的例子中只有T2W的。
先来看看我们的T2W + texCUBE的shader
// jave.lin 2019.06.27
Shader "Test/balloncatT2W"{
Properties{
_Color("Main Color", Color) = (1,1,1,1) // 主色调
_MainTex("Main Tex", 2D) = "white"{} // 主纹理
_OcclusionMap("Occlusion", 2D) = "white" {} // 遮蔽贴图
_NormalMap("Normal Map", 2D) = "bump"{} // 法线贴图
_EnvironmentMap("EnvironmentMap", Cube) = "blackcube" {} // 环境贴图
_HalfLambertIntensity("Half-Lambert Itensity", Range(0, 1)) = 0 // Diffuse的半Lambert强度
_RimIntensity("Rim Intensity", Range(0, 2)) = 0.1 // 使用法线检测边缘光的强度
_AmbientIntensity("Ambient Intensity", Range(0, 1)) = 0.5 // 使用法线检测边缘光的强度
_SpecularIntensity("Specular Intensity", Range(0, 100)) = 80 // 高光模型强度系数1
_SpecularStrengthen("Specular Strengthen", Range(0, 1)) = 1 // 高光模型强度系数2
_SelfShadowIntensity("Self Shadow Intensity", Range(0, 1)) = 0.5 // 自阴影强度
_ReflectIntensity("Reflect Intensity", Range(0, 10)) = 1 // 环境反射强度
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 环境反射率
}
SubShader{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
//#define H_PRECISION
#ifdef H_PRECISION
#define vec3 float3
#define vec4 float4
#define mat3x3 float3x3
#define uv2 float2
#define uv4 float4
#else
#define vec3 fixed3
#define vec4 fixed4
#define mat3x3 fixed3x3
#define uv2 half2
#define uv4 half4
#endif // end H_PRECISION
// 下面的数据精度不能丢
#define color3 fixed3
#define color4 fixed4
#define pos3 float3
#define pos4 float4
#define scalar32 float
#define scalar16 half
#define scalar11 fixed
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f {
pos4 pos : SV_POSITION;
mat3x3 tbn : TEXCOORD0;/* 0,1,2 */
uv2 uv : TEXCOORD3;
pos3 worldPos : TEXCOORD4;
LIGHTING_COORDS(5, 6)
};
color4 _Color;
scalar32 _RimIntensity;
scalar32 _AmbientIntensity;
scalar32 _SpecularIntensity;
scalar32 _SpecularStrengthen;
scalar32 _HalfLambertIntensity;
scalar32 _SelfShadowIntensity;
scalar32 _ReflectIntensity;
scalar32 _Reflectivity;
sampler2D _MainTex;
sampler2D _OcclusionMap;
sampler2D _NormalMap;
samplerCUBE _EnvironmentMap;
uv4 _MainTex_ST;
inline mat3x3 getTBN (vec3 normal, vec4 tangent) {
vec3 wNormal = UnityObjectToWorldNormal(normal); // 将法线从对象空间转换到世界空间
vec3 wTangent = UnityObjectToWorldDir(tangent.xyz); // 将切线从对象空间转换到世界空间
vec3 wBitangent = cross(wNormal, wTangent) * tangent.w; // 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线,tangent.w 决定朝向
return mat3x3(wTangent, wBitangent, wNormal); // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
}
v2f vert (pos4 vertex : POSITION, uv2 uv : TEXCOORD0, vec3 normal : NORMAL, vec4 tangent : TANGENT){
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
o.tbn = getTBN(normal, tangent);
o.uv = TRANSFORM_TEX(uv, _MainTex);
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// sample tangent space normal & decode data
// normal
color4 normalData = tex2D(_NormalMap, i.uv);
vec3 normal = UnpackNormal(normalData);
normal.xy *= _NormalScale;
normal = normalize(mul(normal, i.tbn));
// lighting variables
vec3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
vec3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// coefficient
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
atten *= saturate(dot(normal, lightDir));
_HalfLambertIntensity = 1 - _HalfLambertIntensity;
_SelfShadowIntensity = 1 - _SelfShadowIntensity;
// diffuse
scalar11 lightDotNormal = dot(lightDir, normal) * _HalfLambertIntensity * 0.5 + (1 - _HalfLambertIntensity) * 0.5; //halfLambert
color3 texcolor = tex2D(_MainTex, i.uv).rgb;
color3 diffuse = _LightColor0.rgb * lightDotNormal;
// specular
// half angle
//vec3 halfAngleDir = normalize(lightDir + viewDir);
//scalar11 halfAngleDotNormal = dot(halfAngleDir, normal) * 0.5 + 0.5;
//color3 specular = fixed3(_LightColor0.rgb * pow(halfAngleDotNormal, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// directly reflect
vec3 lightReflectDir = reflect(-lightDir, normal);
scalar32 refelctDotView = max(0, dot(lightReflectDir, viewDir));
color3 specular = fixed3(_LightColor0.rgb * texcolor * pow(refelctDotView, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// rim
scalar11 viewDotNormal = dot(viewDir, normal);
color3 rim = _LightColor0.rgb * (1 - max(0, viewDotNormal)) * _RimIntensity;
// ambient
color3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientIntensity;
scalar11 ambientGrey = dot(ambient, 1) / 3;
// combine the diffuse, specular, rim, ambient
color3 combined = _Color.rgb * texcolor * diffuse + specular + rim + ambient * lightDotNormal;
// occlusion
scalar11 occlusion = tex2D(_OcclusionMap, i.uv).r;
combined *= occlusion;
// shadow
color3 uppercolor = combined;
color3 lowwercolor = combined * atten;
color3 shadowcolor = lowwercolor + combined * _SelfShadowIntensity;
combined = clamp(shadowcolor, lowwercolor, uppercolor);
// environment
vec3 viewReflectDir = reflect(-viewDir, normal);
color3 environment = texCUBE(_EnvironmentMap, viewReflectDir).rgb * diffuse + specular + rim + ambient * lightDotNormal;
environment *= _ReflectIntensity;
environment += (combined * ambientGrey * (1 - _Reflectivity));
environment += texcolor * (1 - _Reflectivity);
combined = lerp(combined, environment, _Reflectivity);
// output
return color4(combined, 1);
}
ENDCG
}
}
Fallback "Diffuse"
}
此T2W在frag有一次mul(tbn, normal),在texCUBE(cube, normal)中刚好也是需要world space的normal,所以这里就是我说的为何如果切线的法线贴图 + texCUBE在T2W下相对可读性高一些,性能无差别。
在下面来看看W2T的,唯一不太一样的是在frag的texCUBE时,我们需要将mul(normal,tbn)转回到世界空间的normal来计算world space 下的reflect方向,再使用reflect采样cube。
运行效果:
再来看看我们的W2T + texCUBE的shader
// jave.lin 2019.06.27
Shader "Test/balloncatW2T"{
Properties{
_Color("Main Color", Color) = (1,1,1,1) // 主色调
_MainTex("Main Tex", 2D) = "white"{} // 主纹理
_OcclusionMap("Occlusion", 2D) = "white" {} // 遮蔽贴图
_NormalMap("Normal Map", 2D) = "bump"{} // 法线贴图
_EnvironmentMap("EnvironmentMap", Cube) = "blackcube" {} // 环境贴图
_HalfLambertIntensity("Half-Lambert Itensity", Range(0, 1)) = 0 // Diffuse的半Lambert强度
_RimIntensity("Rim Intensity", Range(0, 2)) = 0.1 // 使用法线检测边缘光的强度
_AmbientIntensity("Ambient Intensity", Range(0, 1)) = 0.5 // 使用法线检测边缘光的强度
_SpecularIntensity("Specular Intensity", Range(0, 100)) = 80 // 高光模型强度系数1
_SpecularStrengthen("Specular Strengthen", Range(0, 1)) = 1 // 高光模型强度系数2
_SelfShadowIntensity("Self Shadow Intensity", Range(0, 1)) = 0.5 // 自阴影强度
_ReflectIntensity("Reflect Intensity", Range(0, 10)) = 1 // 环境反射强度
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 环境反射率
}
SubShader{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
//#define H_PRECISION
#ifdef H_PRECISION
#define vec3 float3
#define vec4 float4
#define mat3x3 float3x3
#define uv2 float2
#define uv4 float4
#else
#define vec3 fixed3
#define vec4 fixed4
#define mat3x3 fixed3x3
#define uv2 half2
#define uv4 half4
#endif // end H_PRECISION
// 下面的数据精度不能丢
#define color3 fixed3
#define color4 fixed4
#define pos3 float3
#define pos4 float4
#define scalar32 float
#define scalar16 half
#define scalar11 fixed
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct v2f {
pos4 pos : SV_POSITION;
uv2 uv : TEXCOORD0;
pos3 worldPos : TEXCOORD1;
LIGHTING_COORDS(2, 3)
mat3x3 tbn : TEXCOORD4;
vec3 lightDir : TEXCOORD7;
vec3 viewDir : COLOR;
};
color4 _Color;
scalar32 _RimIntensity;
scalar32 _AmbientIntensity;
scalar32 _SpecularIntensity;
scalar32 _SpecularStrengthen;
scalar32 _HalfLambertIntensity;
scalar32 _SelfShadowIntensity;
scalar32 _ReflectIntensity;
scalar32 _Reflectivity;
sampler2D _MainTex;
sampler2D _OcclusionMap;
sampler2D _NormalMap;
samplerCUBE _EnvironmentMap;
uv4 _MainTex_ST;
inline mat3x3 getTBN (vec3 normal, vec4 tangent) {
vec3 wNormal = UnityObjectToWorldNormal(normal); // 将法线从对象空间转换到世界空间
vec3 wTangent = UnityObjectToWorldDir(tangent.xyz); // 将切线从对象空间转换到世界空间
vec3 wBitangent = cross(wNormal, wTangent) * tangent.w; // 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线,tangent.w 决定朝向
return mat3x3(wTangent, wBitangent, wNormal); // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
}
v2f vert (pos4 vertex : POSITION, uv2 uv : TEXCOORD0, vec3 normal : NORMAL, vec4 tangent : TANGENT){
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
o.uv = TRANSFORM_TEX(uv, _MainTex);
o.tbn = getTBN(normal, tangent);
o.lightDir = mul(o.tbn, normalize(_WorldSpaceLightPos0.xyz)); // w2t
o.viewDir = mul(o.tbn, normalize(_WorldSpaceCameraPos.xyz - o.worldPos)); // w2t
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag (v2f i) : SV_Target {
// sample tangent space normal & decode data
// normal
color4 normalData = tex2D(_NormalMap, i.uv);
vec3 normal = UnpackNormal(normalData);
normal.xy *= _NormalScale;
normal = normalize(normal);
// lighting variables
vec3 lightDir = normalize(i.lightDir);
vec3 viewDir = normalize(i.viewDir);
// coefficient
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
atten *= saturate(dot(normal, lightDir));
_HalfLambertIntensity = 1 - _HalfLambertIntensity;
_SelfShadowIntensity = 1 - _SelfShadowIntensity;
// diffuse
scalar11 lightDotNormal = dot(lightDir, normal) * _HalfLambertIntensity * 0.5 + (1 - _HalfLambertIntensity) * 0.5; //halfLambert
color3 texcolor = tex2D(_MainTex, i.uv).rgb;
color3 diffuse = _LightColor0.rgb * lightDotNormal;
// specular
// half angle
//vec3 halfAngleDir = normalize(lightDir + viewDir);
//scalar11 halfAngleDotNormal = dot(halfAngleDir, normal) * 0.5 + 0.5;
//color3 specular = fixed3(_LightColor0.rgb * pow(halfAngleDotNormal, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// directly reflect
vec3 lightReflectDir = reflect(-lightDir, normal);
scalar32 refelctDotView = max(0, dot(lightReflectDir, viewDir));
color3 specular = fixed3(_LightColor0.rgb * texcolor * pow(refelctDotView, 100 - _SpecularIntensity)) * _SpecularStrengthen * atten; // specular
// rim
scalar11 viewDotNormal = dot(viewDir, normal);
color3 rim = _LightColor0.rgb * (1 - max(0, viewDotNormal)) * _RimIntensity;
// ambient
color3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientIntensity;
scalar11 ambientGrey = dot(ambient, 1) / 3;
// combine the diffuse, specular, rim, ambient
color3 combined = _Color.rgb * texcolor * diffuse + specular + rim + ambient * lightDotNormal;
// occlusion
scalar11 occlusion = tex2D(_OcclusionMap, i.uv).r;
combined *= occlusion;
// shadow
color3 uppercolor = combined;
color3 lowwercolor = combined * atten;
color3 shadowcolor = lowwercolor + combined * _SelfShadowIntensity;
combined = clamp(shadowcolor, lowwercolor, uppercolor);
// environment
vec3 viewReflectDir = reflect(-viewDir, normal);
//vec3 viewReflectDir = reflect(mul(-viewDir, i.tbn), mul(normal, i.tbn));
// 因为我们的normal只能在frag shader中的normal map读取,然后来算出viewDir入射方向, normal法线的反射方向
// 且texCUBE只能用世界坐标,所以这里又将切线空间下的viewReflectDir转回世界坐标
// 总体来说还是使用T2W好些用,除非说不需要texCUBE功能的可以用W2T性能会更高,因为矩阵变换基本在顶点shader处理
viewReflectDir = mul(viewReflectDir, i.tbn); // t2w
color3 environment = texCUBE(_EnvironmentMap, viewReflectDir).rgb * diffuse + specular + rim + ambient * lightDotNormal;
environment *= _ReflectIntensity;
environment += (combined * ambientGrey * (1 - _Reflectivity));
environment += texcolor * (1 - _Reflectivity);
combined = lerp(combined, environment, _Reflectivity);
// output
return color4(combined, 1);
}
ENDCG
}
}
Fallback "Diffuse"
}
所以,我们看到,在frag,我们在处理texCUBE时,还得将reflect出来的dir T2W处理一次
运行效果与T2W的一样,这里就不贴图出来了
总结
T2W
tbn = {t,b,n} 与向量左乘,即:mul(vec, tbn)就是T2W变换应用,或是变成右乘方式,使用tbn转置,其实还是用tbn的列乘以了vec的行而已,写法不一样:mul(transpose(tbn), vec)翻过来写法
tbn =
{
t.x, t.y, t.z,
b.x, b.y, b.z,
n.x, n.y, n.z
}
总之可以理解为newVec = {dot(t, vec), dot(b, vec), dot(n, vec))后,就是T2W
W2T
那么W2T呢,还记得我上面说的,T2W正交的tbn的转置就是逆矩阵,就是W2T的tbn矩阵
所以,还是有几种写法:
tbn = {t,b,n} 与向量右乘,即:mul(tbn, vec)
tbn = {t,b,n} 与向量左乘,即:mul(vec, transpose(tbn))
- tbn右乘vec,即:tbn行乘以vec列,这里的vec4 作为vec1x4(1行4列),就是tangent space to world space
- tbn左乘vec,即:vec行乘以tbn列,这里的vec4 作为vec4x1(4行1列),就是world space to tangent space
tbn={t,b,n},T2W:vec行乘tbn列,W2T:tbn行乘vec列
顺便说一下左乘与右乘区别:
- mul(vec, mat) 左乘,速记:vec在左,所以左乘,以向量的行与矩阵列相乘
- mul(mat, vec) 右乘,速记:vec在右,所以右乘,以矩阵的行与向量列相乘
Project
链接: https://pan.baidu.com/s/1p4KryJ_tN46tMZVh3QrDMQ 提取码: 9gej 复制这段内容后打开百度网盘手机App,操作更方便哦