UnityShader纹理基础
单张纹理
通常会使用一张纹理来代替物体的漫反射颜色
纹理最初的目的是使用一张图片来控制模型的外观。使用纹理映射技术。逐文素地控制模型地颜色。
纹理映射坐标也被称为UV坐标。
顶点UV坐标的范围通常都被归一化到[0,1]范围内。
纹理采样时使用的纹理坐标不一定是在[0,1]范围内。这种不在[0,1]范围内的纹理坐标有时会非常有用。与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到不在[0,1]范围内的纹理坐标时如何进行纹理采样。
实现
在顶点着色器中
先使用缩放属性进行对纹理顶点进行缩放,然后再使用偏移属性对结果进行偏移
v2f vert(appdata_base v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
//o.uv = TRANSFORM_TEX(o.uv,_MainTex);
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
片元着色器
使用CG的tex2D进行纹理采样。
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal,worldLightDir));
fixed3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
凹凸映射
两种方法:
使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值,称为高度映射
使用一张法线纹理来直接存储表面法线,称为法线映射
高度纹理
纹理中存储的是强度值
缺点:
计算复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算而得。
法线纹理
法线纹理存储的是表面法线方向。
由于法线方向的分量范围在[-1,1],而像素的分量范围为[0,1],因此需要做一个映射
pixel = (normal +1 )/2
这就要求在Shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。
normal = pixel x 2 - 1
对于模型顶点自带的法线,它们定义在模型空间中,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理。
在实际制作中,往往会采用另一种坐标空间,即模型顶点的切线空间来存储法线。
对于模型的每个顶点,它们都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称为是副切线或副法线
这种纹理被称为是切线空间的法线纹理
实践
切线空间
基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照效果。
首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中(这种方法只能处理统一尺度,不包括非统一尺度),即需要知道从模型空间到切线空间的变换矩阵。
这个变换的逆矩阵,即从切线空间到模型空间的变换矩阵是非常容易得到的,只需在顶点着色器中按切线(x轴)、副切线(y)轴、法线(z轴)的顺序按列排列即可得到
那么从模型空间到切线空间的矩阵为 该矩阵的转置矩阵
如果一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就等于它的转置矩阵。
因为Unity按行优先
因此只需在顶点着色器中按切线(x轴)、副切线(y)轴、法线(z轴)的顺序的到变换矩阵
顶点着色器
v2f vert(appdata_tan 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;
// float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)) * v.tangent.w;
// float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
在计算副切线时使用v.tangent.w和叉积结果进行相乘,这是因为和切线与法线方向都垂直的方向有两个,而 w 决定了我们选择其中哪一个方向。
片元着色器
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 ;
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 * saturate(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);
}
首先把packedNormal的xy分量按之前提到的反映射公式映射回法线方向
然后乘以 _BumpScale 来得到tangentNormal的xy分量。
由于法线都是单位矢量,因此tangentNormal.z分量可以由tangentNormal.xy计算而得。
由于使用的是切线空间下的法线纹理,因此可以保证法线方向的z分量为正。
法线空间
基本思想:
在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给偏远着色器。变换矩阵可以由顶点的切线、副切线、和法线在世界空间下的表示来得到。
顶点着色器
v2f vert(appdata_tan 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;
// float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)) * v.tangent.w;
// float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);
fixed3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = mul(unity_ObjectToWorld,v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
片元着色器
fixed4 frag(v2f i):SV_Target{
fixed3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 worldNormal = UnpackNormal(tex2D(_BumpMap,i.uv.zw));
worldNormal.xy *= _BumpScale;
worldNormal.z = sqrt(1.0 - saturate(dot(worldNormal.xy ,worldNormal.xy)));
worldNormal = normalize(half3(dot(i.TtoW0.xyz,worldNormal),dot(i.TtoW1.xyz,worldNormal),dot(i.TtoW2.xyz,worldNormal)));
fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldNormal,worldLightDir));
fixed3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
使用TtoW0、TtoW2、TtoW2存储的变换矩阵把法线变换到世界空间下。这是通过使用点乘操作来实现的每一行和法线相乘来得到的。
渐变纹理
纹理其实可以用于存储任何表面属性。
一种常见的用法就是使用渐变纹理来控制漫反射光照的结果。
冷暖色调的着色技术,很多卡通风格的渲染中都使用了这种技术。
使用这种方式可以自由地控制物体的漫反射光照。不同的渐变纹理有不同的特性。
片元着色器
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed halfLambert = 0.5 * dot(worldNormal,worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
fixed3 halfDir = normalize(worldLightDir+worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
通过对法线和光照方向的点积做一次0.5倍的缩放以及夜歌0.5大小的偏移值来计算半兰伯特部分。这样得到halfLambert的范围被映射到[0,1]之间。
使用halfLambert来构建一个纹理坐标,并用这个纹理坐标对渐变纹理RampTex进行采样。由于RampTex实际上就是一个以为纹理(它在纵轴方向上颜色不变),因此纹理坐标的u和v方向都使用了halfLambert。然后,把渐变纹理采样得到的颜色和材质颜色_Color相乘,得到最终的漫反射颜色。
需要把渐变纹理的Wrap Mode设置为Clamp模式,以防止对纹理进行采样时由于浮点数精度而造成的问题。
遮罩纹理
什么是遮罩呢?简单来说,遮罩允许我们可以保护某些区域,使他们免于某些修改。
我们希望模型表面某些区域的反光强烈一些,而某些区域容易些。为了得到更加细腻的效果,就可以使用一张遮罩纹理来控制光照。
使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(texel.r)来与某种表面属性进行相乘,这样,该通道的值为0时,可以保护表面不受该属性的影响。
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap("Normal Map",2D)="bump"{}
_BumpScale("Bump Scale",Float)=1.0
_SpecularMaks("Specular Mask",2D) = "white"{}
_SpecularScale("Specular Scale",Float) = 1
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8,256))=20
}
SubShader
{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM