第七章依次学习了单张纹理的映射、凹凸映射(法线纹理)、渐变纹理映射、遮罩纹理映射,一整章跟着敲下来,感觉自己对于纹理的理解也比之前更深入了
下面按小节进行总结,只会记录觉得重要的点,且不会对书上详细讲过的内容进行过多记录
7.1单张纹理
这一节主要讲了一个直接铺在物体表面的贴图会对颜色产生的影响,引入了纹理采样函数tex2D()、材质反射率albedo、纹理属性WrapMode以及tilling和offset
纹理采样函数:tex2D(sampler2D, float2)
作用:根据uv坐标(float2)获取纹理(sampler2D)上对应位置的颜色值
纹理属性WrapMode以及tilling和offset
WrapMode中选择repeat后纹理就有了tilling(缩放)和offset(平移)两个float2属性,它们被一起打包进了“纹理名_ST”的float4变量中
想让tilling和offset有效果,需要:
- 在pass块中声明该变量
sampler2D _MainTex;
float4 _MainTex_ST;
- 在顶点着色器中对uv进行变换(下面的代码二选一)
o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw // 内部实现
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 封装函数
之后在片元着色器中调用tex2D()函数来获取颜色值就可以了
材质反射率albedo
其实就是把纹理上的颜色和自定义_Diffuse颜色相乘得到的颜色值
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
fixed3 ambient = albedo * UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = albedo * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
它会影响到ambient和diffuse的效果,但不会影响specular的效果
就像用白光打在黑色地面上和打在红色地面上看到的效果不同一样
7.2 凹凸映射
这里感觉是这一章最难的地方了,主要是第一次接触到切线空间,以及需要自己构建变换矩阵的情况
因为不熟练,在写和法线纹理有关shader的时候有一个会偶尔突然卡住脑子,然后又突然惊醒的地方,就是:
嗯?我在干什么?我不是可以直接从a2v里获得NORMAL吗?我从法线纹理里采样的又是啥?喵喵喵?哦对…当然是…要对法线做修改才要用法线纹理啊傻孩子…
所以下面就啰嗦一点用“法线”和“新的法线”来区分,老年人记性不大好…
高度纹理和法线纹理的区别
高度纹理存储的是强度值(intensity),是相对高度,表示模型表面局部的海拔高度。它是一张灰度图,白高黑低
法线纹理存储的是法线方向,由于法线的分量范围是[-1, 1],像素分量范围是[0, 1],需要将法线映射到[0,1]
p i x e l pixel pixel = n o r m a l + 1 2 \frac{normal + 1} {2} 2normal+1
法线纹理的映射以及法线纹理的Inspector里的一些属性
根据上面所说,在获取法线纹理的像素值后,还需要将它还原为原来的法线:
n o r m a l normal normal = 2 ∗ p i x e l − 1 2 * pixel - 1 2∗pixel−1
如果提前将纹理的Texture Type设定为Normal Map的话,就可以直接在shader中使用UnpackNormal()函数了;否则只能用上面的式子自己来还原法线
fixed4 packedNormal = tex2D(_BumpMap, i.uv);
normal = UnpackNormal(packedNormal);
当使用高度纹理而不是法线纹理时,还需要勾选Create From Grayscalse
这样Unity会根据高度纹理中的灰度生成法线纹理,在后面的处理中就可以借用这个生成的法线纹理把这张高度纹理当成切线空间下的法线纹理来对待了
切线空间
上图就是最直观的解释:原点是该顶点本身,z轴是该顶点的法线方向
,x轴是该顶点的切线方向,y轴是该顶点的副切线(bitangent)方向
其中副切线可由法线和切线叉积得到,不会存在顶点数据中
模型空间的法线纹理 vs 切线空间的法线纹理
左图为模型空间中的法线纹理,右图为切线空间中的法线纹理
它们之间的关系就像世界空间中的顶点坐标和模型空间中的顶点坐标一样,互为父子坐标系,一个相对,一个绝对:
模型空间中的法线纹理是所有顶点都以模型的空间为参照系
切线空间中的法线纹理是所有顶点都以自己的空间为参照系
且因为y轴就是该顶点的法线方向,在这一点上的新的法线方向其实存储的是对原法线的扰动:如果没有扰动,就是(0,0,1)根正苗蓝无需修改;如果有了扰动,比如(0.2,0.3,0.8),不再是纯正的蓝色,就意味着这一点的法线会被做一些修改(扰动),进而影响渲染的效果
切线空间法线纹理的优点
- 高自由度:不与模型相绑定,也可以适用于其他网格
- 可进行uv动画:通过平移uv达到平移物体表面凹凸的效果
- 可重用法线纹理:比如可以用1张法线纹理覆盖砖块的6个面
- 可压缩:切线空间中法线的z方向总为正(虽然不一定重合),所以可以只存储xy方向,推导得到z方向
计算副切线以及从切线空间到世界空间的变换矩阵
切线空间的x轴(切线)和z轴(法线)可以直接从模型中获得,y轴(副切线)可以用切线和法线进行叉积得到。于是就得到了变换矩阵rotation:
float3 bitangent = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, bitangent, v.normal);
求副切线的时候乘了v.tangent.w是因为,叉乘的结果有两个方向,乘上v.tangent.w可以决定最后使用哪一个方向(一开始不懂,觉得叉乘明明可以通过右手定则判断方向,怎么就需要另外想办法判断方向了呢?然后发现自己傻了电脑没有右手…)
或者直接使用Unity的宏指令得到rotation:
TANGENT_SPACE_ROTATION;
插值寄存器的空间利用
由于新的法线存储在切线空间,且对法线纹理的采样发生在片元着色器,所以必须要把变换矩阵传入片元着色器
但是一个插值寄存器最多只能存储float4大小的值,变换矩阵则是float3x3
所以就要把这个矩阵拆分成3个float3变量存到3个TEXCOOD里
这时为了能最大化利用寄存器的空间,还可以给这3个float3多加一个worldPos分量,拼成3个float4;同理,也可以把_MainTex和_BumpMap的uv合并为float4放入同一个TEXCOORD中
struct v2f{
float4 pos : SV_POSITION;
float4 texcoord : TEXCOORD0;
float4 Tangent2World1 : TEXCOORD1;
float4 Tangent2World2 : TEXCOORD2;
float4 Tangent2World3 : TEXCOORD3;
};
v2f vert{
// ...
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap.zw;
// ...
o.Tangent2World1 = float4(worldTangent.x, worldBitangent.x, worldNormal.x, worldPos.x);
o.Tangent2World2 = float4(worldTangent.y, worldBitangent.y, worldNormal.y, worldPos.y);
o.Tangent2World3 = float4(worldTangent.z, worldBitangent.z, worldNormal.z, worldPos.z);
return o;
}
在切线空间和世界空间下计算光照的好处
在切线空间下计算会比较快,因为不需要再传递一个变换矩阵到片元着色器;
在世界空间下计算会比较常用,因为会有一些特殊的纹理也需要在世界空间进行计算,比如CubeMap(期待期待)
7.3 渐变纹理
令人激动!我似乎就要看到了三渲二在向我招手!(不是)
原理上讲,其实和7.1中的单张纹理很像,都是通过纹理的颜色影响最终呈现的颜色,只不过这个纹理的v方向上颜色没有变化,且不会影响到环境光。对比一下:
7.1中的单张纹理
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
fixed3 ambient = albedo * UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = albedo * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 specular = _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
7.3 中的渐变纹理
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Diffuse.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = diffuseColor * _LightColor0.rgb;
fixed3 specular = _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
可以看出,二者都有一个通过纹理颜色和diffuse颜色相乘获得的颜色参数,只不过
- 前者中这个颜色参数影响到了diffuse和ambient,后者只影响到了diffuse
- 前者纹理采样时直接使用了uv坐标,后者因为v方向颜色无变化,在uv方向上都使用halfLambert构造了相同的坐标(因为halfLambert范围为[0,1])
注意事项
要把Wrap Mode设定为Clamp,否则会因采样时的浮点精度出现下面的问题:
因为虽然halfLambert范围在[0,1],但还是可能出现1.00001的值,这是一旦选择的是Repeat,halfLambert就会得到0.00001的值,从渐变图右侧的颜色突变到了左侧的颜色。改成Clamp不让它重复就好了
7.4 遮罩纹理
个人感觉这是最有意思的一种纹理,有点像开关、混合权重
书中给出的是用遮罩纹理的r通道存储specular的掩码值,来达到逐像素控制specular强度的效果,方法如下:
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
fixed3 specular = specularMask * _Specular.rgb * _LightColor0.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss);
书中还提到了其他玩法:
比如r通道存放高光反射的强度,g通道存放边缘光照的强度,b通道存放高光反射的指数,a通道存放自发光强度
这就让我想起一个关于向量的笑话:
物理系学生:害 向量就是用3个分量表示方向和距离的变量
计算机系学生:啥?向量不就是一个存储3、4个变量的结构体吗?
结果现在也可以用到纹理上了:
美术:害 纹理就是贴在材质上的贴图
敲shader的:害 纹理就是一个存储4个变量的结构体
学习总结
这一章主要是在熟悉shader中的纹理采样,以及慢慢适应使用切线空间,解锁纹理的更多玩法,还学习了尽可能压榨插值寄存器的思想
只要把第六章的基本光照shader玩好,这一章可以过得很轻松,理解了原理,只要在基本光照shader上改一些参数、增加一些变量就可以了