作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要笔记9:基础纹理C
原文发布时间:2020年3月11日
文章类型: 授权转载
无法原谅一个什么都不做,却对未来抱有期待的自己。——《樱花庄的宠物女孩》
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新的版本:“2020.1.0.Alpha 25”
上节说到了凹凸映射和法线压缩的问题,当然,其实讲了一些原书没有的东西,其实很多东西也是我在写笔记的时候的发现的。(可能这就是费曼学习法吧),在本篇中,会补充一下上节中没有讲到的东西,另外就是把在世界空间下计算凹凸映射补充完整,然后补一下纹理遮罩。
之前就提到了,凹凸映射可以在切线空间下计算,也可以在世界空间下计算,那么我们下面讲一下怎么在世界空间下计算。本质上,不论是在切线空间计算,还是在世界空间下计算,都是围绕着怎么获取切线空间变换矩阵来的。如果忘记了该矩阵是怎么获得的,可以回到上一节回顾一下。
在世界空间下计算其实相对来说比在切线空间下计算要麻烦更多,但是往往很多东西都需要在世界空间下计算。比如环境映射等,所以为了统一,我们一般还是会选择在世界空间下计算,作为标准方式。
由于我们要在世界空间下计算,因此我们要在顶点着色器中把切线空间变换矩阵计算出来,然后传递给片元着色器,不过由于结构体当中不能储存矩阵类型的变量,所以为了传递一个矩阵,我们只能把一个float3x3类型矩阵拆成3个float3类型。比如下面这样:
struct vertexOutput{
//对于为什么要用vertexOutput来作为结构体的名字
//我只能说我个人非常讨厌C命名风格,v2f这样的结构体名过于反人类了。
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 TtoW1 : TEXCOORD1;
float3 TtoW2 : TEXCOORD2;
float3 TtoW3 : TEXCOORD3;
float3 worldPos : TEXCOORD4;
}
这样的话,算上worldPos,我们一共有4个float3类型的变量,为了充分利用插值寄存器的空间,我们其实可以把4个float3类型的变量,换成3个float4类型的变量。这样,三个float4的第四个分量w,用于储存worldPos的分量xyz。如下所示
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 TtoW1 : TEXCOORD1;
float4 TtoW2 : TEXCOORD2;
float4 TtoW3 : TEXCOORD3;
}
另外就是要记住,我们计算出来的矩阵是“切线转世界”矩阵,而不是“世界转切线”矩阵。在上节中我们在切线空间下得到的矩阵是“世界转切线”矩阵,因而这次我们的排列方式要用那个矩阵的逆矩阵。
Shader "UShaderMagicBook/bumpMapWorldSpace"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_BumpTex("Bump Texture",2D) = "bump"{}
_BumpScale("Bump Scale",Float) = 1.0
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
struct vertexInput{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 TtoW1 : TEXCOORD1;
float4 TtoW2 : TEXCOORD2;
float4 TtoW3 : TEXCOORD3;
};
sampler2D _MainTex;
sampler2D _BumpTex;
float _BumpScale;
fixed4 _BaseColor;
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
//得到切线空间的逆矩阵,由于该矩阵只有线性变换,所以矩阵的逆矩阵就是它的转置矩阵
o.TtoW1 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW2 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW3 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
float3 worldPos = float3(i.TtoW1.w,i.TtoW2.w,i.TtoW3.w); //先取出模型的世界坐标
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); //计算光源方向
fixed3 bump = UnpackNormal(tex2D(_BumpTex,i.uv));
bump.xy *= _BumpScale;
//这个Bump目前是切线空间的哦(/ ~ \)
bump = normalize(
float3(dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump),dot(i.TtoW3.xyz,bump))
);
//和一个矩阵相乘就相当于和一个列向量组的每个向量相乘后组合在一起。
//以下代码用于计算光照
fixed3 albedo = _BaseColor.xyz * tex2D(_MainTex,i.uv).xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.xyz * albedo * saturate(dot(bump,lightDir));
return fixed4(diffuse + ambient,1.0);
}
ENDCG
}
}
}
下面是最终的效果,不过和在切线空间下计算没有啥区别。
另外,我们可以给它加上高光计算。
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0.xyz * pow(saturate(dot(halfDir,bump)),_Gloss);
最终效果看起来有点油腻,墙面本来就不应该有高光才对。计算高光的完整代码和计算漫反射没有太大区别,完整代码可以在文章结尾的github链接找到。
当你逐渐的深入着色器,你会发现,只要是和“表面(Surface)”有关的属性,都可以采用一张纹理来存储,包括法线纹理,渐变纹理,噪声纹理等。
这节要讲一下渐变纹理,用它来控制模型的表面从冷色调到暖色调的,或者从暗到明的一种着色技术。不过难度不是很大,只是用于控制表面的颜色而已。使用渐变纹理也可以做出“卡通渲染(Toon Rendering)”效果,不过真正的NPR(非真实感渲染,Non-Photorealistic Rendering)是一个非常大的概念,可以单独的出一个系列来说了。
那么在开始实践之间,我们可以尝试使用ps制作简单的渐变纹理。
我做了两个特色比较鲜明的渐变纹理。然后导入到Unity里面作为渐变纹理来使用。另外,我在网上下载了一个比较好看的纹理,也放到里面。
说白了,我们用渐变纹理和主纹理混合在一起,就完事了。(虽然这和书上说的渐变纹理不是一回事,诶,别打我~),完整代码可以在文章结尾的github连接中找到。
fixed3 texColor = tex2D(_MainTex,i.uv).xyz * _BaseColor.xyz;
fixed3 albedo = tex2D(_RampTex,i.uv).xyz * texColor;
//把主纹理和渐变纹理按照正片叠底混合在一起
好了,上面那个渐变纹理我也不知道能不能称为渐变纹理,但是它是对主纹理进行色彩渐变。和书上所说的,对光照进行渐变不太一样。
我们需要准备这样一张渐变纹理,它的色彩在高度上没有变化。只在横轴上有变化。
当这个渐变纹理全部为白色的时候,就相当于没有渐变。根据光照的强度来对这张纹理上的颜色进行采样,然后和原来的光照计算的纹理进行混合。
说白了,就是把原来的光照亮度越亮的部分和这个纹理上越右的部分混合,越暗的部分,和纹理越偏左的部分混合。这样说是为了方便大家理解,为了方便实践,我们要再换种说法。
靠左靠右,采样值靠近0或者靠近1(只考虑横轴就可以了)。
越暗越亮,光照强度靠近0或者靠近1(这里控制的是漫反射亮度)。
我们再来观察一下兰伯特漫反射计算的公式
可以发现,光源强度I是固定的,albedo是主纹理,也是固定的,唯一有变化的就是右边的部分。所以我们先计算该部分,然后用它来对渐变纹理进行采样,再和主纹理,表面颜色混合。下面为完整的代码。我们其实只要看片元着色器就可以了。
Shader "UShaderMagicBook/RampTextureLight.shader"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
_RampTex("Ramp Texture",2D) = "ramp"{}
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_Gloss("Gloss",Range(8,100)) = 20.0
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
struct vertexInput{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
sampler2D _MainTex;
sampler2D _RampTex;
fixed4 _BaseColor;
float _Gloss;
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); //计算光源方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); //计算视角方向
fixed3 worldNormal = normalize(i.worldNormal);
fixed halfLambert = 0.5 * dot(worldNormal,lightDir) + 0.5;
fixed3 albedo = tex2D(_RampTex,fixed2(halfLambert,halfLambert)).xyz * _BaseColor.xyz * tex2D(_MainTex,i.uv).xyz;
//正片叠底
//以下代码用于计算光照
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.xyz * saturate(dot(worldNormal,lightDir)) * albedo;
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0.xyz * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
return fixed4(diffuse + ambient + specular,1.0);
}
ENDCG
}
}
}
当然,细心的读者可能会发现我们这里采用了半兰伯特漫反射公式,而没有使用兰伯特公式。这是由于半兰伯特可以从根源上防止亮度小于0带来的纯黑问题(之前提到过)。
那个时候我们采用了环境光的方式来解决,但是在这里,我们事先使用这个东西计算了主纹理,而主纹理又要和环境光混合,因而在这样一个条件下,我们的环境光也产生了和渐变纹理一样的渐变效果,最终结果就是又出现了纯黑问题。(完全丢失模型细节),为了更直观一点,我们可以观察最终结果做一个对比。
最左侧是没有使用任何渐变纹理(也可以当成是渐变纹理为纯白色),中间是使用了半兰伯特光照模型对渐变纹理进行采样的效果,可以看到它的对比更高了(更实际的意义是我们可以自己来控制漫反射光照了),最右侧是使用兰伯特光照模型来对渐变纹理进行采样,它的效果不够完美,不过如果你能保证你的渐变纹理中没有纯黑的部分,也可以使用兰伯特光照模型来计算。
ok,这是笔记的最后一个部分,也是第7章的最后一个部分了。
什么是遮罩纹理呢,说起来和渐变纹理有点联系,也是希望光照的部分不要对模型表面的所有部分都一视同仁,遮罩纹理就是对高光进行控制,不要让所有的部分都使用同样的高光参数。
说白了,遮罩纹理和渐变纹理其实有点像,只不过它是和高光部分进行混合。
在我们的石转块纹理中,如果石转块表面有高光,那么这个高光应该是石头上的,而不是石头缝里的,但是对于没有遮罩的石头缝来说,它也会产生高光,所以我们石头缝这个位置的高光应该是没有的,或者说这里的系数用0来表示。
由于该系数是一个float类型的变量,所以我们的遮罩纹理应该是一个灰度图,正好,其实我们只要把原图给灰度化,就可以满足要求了。
ok~,如果要实现其他的效果,还需要进一步的调整纹理。
Shader "UShaderMagicBook/SpecularMask"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_BumpTex("Bump Texture",2D) = "bump"{}
_BumpScale("Bump Scale",Float) = 1.0
_MaskTex("Mask Texture",2D) = "mask"{}
_Gloss("Gloss",Range(8.0,200.0)) = 20.0
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
struct vertexInput{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 TtoW1 : TEXCOORD1;
float4 TtoW2 : TEXCOORD2;
float4 TtoW3 : TEXCOORD3;
};
sampler2D _MainTex;
sampler2D _BumpTex;
float _BumpScale;
fixed4 _BaseColor;
sampler2D _MaskTex;
float _Gloss;
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
//得到切线空间的逆矩阵,由于该矩阵只有线性变换,所以矩阵的逆矩阵就是它的转置矩阵
o.TtoW1 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW2 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW3 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
float3 worldPos = float3(i.TtoW1.w,i.TtoW2.w,i.TtoW3.w); //先取出模型的世界坐标
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); //计算光源方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpTex,i.uv));
bump.xy *= _BumpScale;
//这个Bump目前是切线空间的哦(/ ~ \)
bump = normalize(
float3(dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump),dot(i.TtoW3.xyz,bump))
);
//和一个矩阵相乘就相当于和一个列向量组的每个向量相乘后组合在一起。
//以下代码用于计算光照
fixed3 albedo = _BaseColor.xyz * tex2D(_MainTex,i.uv).xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0.xyz * tex2D(_MaskTex,i.uv).r * pow(saturate(dot(halfDir,bump)),_Gloss);
fixed3 diffuse = _LightColor0.xyz * albedo * saturate(dot(bump,lightDir));
return fixed4(diffuse + ambient + specular,1.0);
}
ENDCG
}
}
}
代码和之前的代码没有太大区别,增加了遮罩纹理,并且在下面对遮罩纹理进行采样,用采样到的数据来控制高光的缩放。
不要由于我们使用的遮罩纹理只是用来控制夹缝处的高光的,总得来看,其实没啥区别,我们可以使用各种不同的遮罩纹理来控制表面的高光区域。
另外本次使用的所有代码都已经上传到了我的github中,有需要的可以前往下载,如果这个系列的文章对你有帮助,不要忘记点一手关注,或者给我的github贡献一颗⭐,谢谢啦。