前面我们学习了基础纹理和法线纹理,现在我们认识到纹理就是顶点的某种数据,我们下面将学习渐变纹理和遮罩纹理。之后我们再补充一些透明的基本做法,那么入门精要的基础篇就完成了。
渐变纹理
渐变纹理是用来控制物体漫反射的,我们本来计算漫反射是采用兰伯特公式计算:
不过曾经有人提出一种基于冷到暖色调变化进行着色的技术,使物体的轮廓线比直接使用漫反射更加明显,而且具有多种色调的变化,卡通渲染中大多使用这种技术。原理就是修改上面公式中的最后一项$n \cdot l$,我们将用这个值扩展得到的二维向量$(n \cdot l, n \cdot l)$来作为uv坐标,使用特定的渐变纹理进行采样,得到需要的颜色并返回到上面公式中作为最后一项。
因此实现渐变纹理只需要轻微改变漫反射即可。我们使用半兰伯特光照模型的漫反射,看看它的效果如何。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| Shader "Learn/Ramp Texture" { Properties{ _Color("Color Tint", Color) = (1,1,1,1) _RampTex("Ramp Tex", 2D) = "white" {} _Specular("Specular", Color) = (1,1,1,1) _Gloss("Gloss", Range(8.0,256)) = 20 }
SubShader{ Pass{ Tags { "LightMode" = "ForwardBase"}
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc"
fixed4 _Color; sampler2D _RampTex; float4 _RampTex_ST; fixed4 _Specular; float _Gloss;
struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; };
v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o; }
fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldPos = normalize(i.worldPos); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(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 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 halfDir = normalize(worldNormal + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0); }
ENDCG } } FallBack "Specular"
}
|
除了漫反射外,其它地方完全不用修改,得到下面的结果:
我们看到了明显的颜色变化,而非之前平滑的颜色过渡了,这就是渐变纹理,书中还提供了其它纹理,可以查看它们的不同效果。
遮罩纹理
在前面的学习中,我们可能会苦恼于镜面光的位置无法控制,因为物体表面不同部分对光的反射各不相同,使用统一的计算显然无法得到不同的结果。但是使用遮罩纹理就可以实现这一点,遮罩纹理可以保护某些区域,在采样的时候将得到一个矢量,它的RGBA通道可能存储了不同的数据,那么在计算中我们让光照乘以其中某个值,让这个通道原本的光照值被改变,就能完成对模型不同表面的遮罩。
具体实现上,让顶点根据默认的uv坐标,对遮罩纹理进行采样,然后直接和镜面光相乘,就可以了,因为我们这里使用的遮罩纹理很简单,它的每个通道都是相同的值,全部表示镜面光强度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| Shader "Learn/MaskTexture" { Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white" {} _BumpMap("Normal Map", 2D) = "bump" {} _BumpScale("Bump Scale", Float) = 1.0 _SpecularMask("Specular Mask", 2D) = "white" {} _SpecularScale("Specular Scale", Float) = 1.0 _Specular("Specular", Color) = (1,1,1,1) _Gloss("Gloss", Range(8.0,256)) = 20 }
SubShader{ Pass{ Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM #pragma vertex vert #pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float _BumpScale; sampler2D _SpecularMask; float _SpecularScale; float4 _Specular; float _Gloss;
struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; };
struct v2f{ float4 pos : SV_POSITION; float2 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;
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);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv)); 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 specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale; //修改镜面光计算 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0); }
ENDCG } } FallBack "Diffuse" }
|
上面的代码中我们使用主纹理的缩放、平移对所有纹理进行了操作,这意味着调整主纹理的属性能影响到其它纹理。下面是这个着色器的结果。
上面提供的讲解都很简单,以后我们可能会进一步使用它们。
透明效果
纹理基础结束后,我们进入透明效果的学习,之前的学习中我们只使用了颜色通道的RGB值,Alpha并没有使用,他就是表示透明度的,当这个值为1,像素完全显示;这个值为0,像素不会显示。
我们将学习两种实现透明的方式,透明度测试(无法实现真正的半透明)和透明度混合。
透明度测试
只要一个片元的透明度不满足条件,那么将直接舍弃这个片元,它不会对颜色缓冲有任何影响。在Unity Shader中我们可以使用clip函数完成这个测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Learn/Alpha Test" { Properties{ _Color("Main Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white" {} _Cutoff("Alpha Cutoff", Range(0,1)) = 0.5 }
SubShader{ Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
Pass{ Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff;
struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; }
fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv);
clip(texColor.a - _Cutoff);
fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, 1.0); }
ENDCG } } FallBack "Transparent/Cutout/VertexLit" }
|
我们在SubShader开头使用了三个标签,第一个表示将该模型的渲染归于AlphaTest渲染队列。在渲染过程中,由于透明物体的存在,我们必须先渲染不透明物体,然后再渲染半透明物体,最终得到正确的渲染效果。不过即使这样做也会有问题,因为不同物体之间可能会有遮挡,这时我们不能直接让两个半透明物体渲染,因为它们的深度关系随着形状而改变,必须进一步分割模型网络才能得到正确结果。
Unity中定义了五个渲染队列,它们的含义如下:
- Bakcground:在其它队列之前绘制,适合背景上的物体;
- Geometry:默认的队列,不透明物体使用该队列;
- AlphaTest:使用透明度测试的物体;
- Transparent:在Geometry和AlphaTest之后,并且从后往前绘制使用了透明度混合的物体;
- Overlay:在最后渲染。
所以这里我们使用第一个队列。第二个标签是IgnoreProjector,开启后Shader不会受到投影器干扰;最后一个是RenderType,设置为TransparentCutout表示让Unity将这个Shader归入提前定义的组中,指明这个Shader使用了透明度测试。只需要知道,使用透明度测试的Shader一般都要使用这三个标签。
后面的代码就是之前很熟悉的了,不再讲解。这里注意Clip函数的使用即可。
完成后将这一章对应的纹理贴上去(素材都在github上),这张纹理的alpha通道存储了不同的透明度。调整透明度的阈值,看到立方体的某些面消失了。
这个效果当然不是真正的透明,我们下面来看看透明度混合的做法。
透明度混合
开启透明度混合后,透明度将作为混合因子来对颜色进行处理。要注意的是,使用透明度混合必须关闭深度写入,但是却需要使用深度测试,这是因为深度写入将直接剔除那些在透明物体之后的点,同时我们又必须通过深度测试判断物体的前后顺序,所以这里深度测试对于我们来说是只读的,它不会影响深度缓存的内容。
使用Unity的混合命令就可以开启混合,它指定了混合时使用的混合函数。
这里我们使用第二种函数,它处理RGBA的每一个通道,数学公式如下:
除了相乘后加法以外,我们还可以使用其它不同的公式,使用BlendOp BlendOperation命令可以指定混合公式:
其中还可以进一步设置SrcFactor为源的Alpha值,DstFactor为1减去源的Alpha值。源指的是当前处理的颜色,也就是没有写入颜色缓存的值;目标指的是已经存在颜色缓存中的值,混合就是将当前处理的颜色和存在颜色缓冲的颜色进行混合,得到的新颜色再次覆盖原来颜色缓存中的值。将它转化成数学公式:
当然也有其它的指定方式,语法为[Blend SrcFactor DstFactor, SrcFactorA DstFactorA]。其中Factor可以替换为以下的值:
下面我们来实现这个Shader,使用和上面一样的Shader,然后修改几个地方。
首先将Properties修改如下,记得一并修改Pass中声明的变量:
1 2 3 4 5 6 7 8 9 10
| Properties{ _Color("Main Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white" {} _AlphaScale("Alpha Scale", Range(0,1)) = 1 } fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale;
|
新增的变量AlphaScale用于控制整体的透明度。然后修改标签如下:
1
| Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
|
RenderType指明了Shader是一个透明度混合的Shader。我们还需要在Pass中关闭深度写入,设置混合因子:
1 2 3 4 5
| Pass{ Tags {"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha
|
接下来修改片元着色器:
1 2 3 4 5 6 7 8 9 10 11
| fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale); }
|
其实仅修改了返回值的Alpha,就得到下面的效果了:
到这里我们不得不面对前面提到的一个问题:模型本身具有复杂遮挡关系时,如何保证每个部分的排序正确?由于我们关闭了深度写入,所以无法对模型进行像素级别的深度排序,不可避免地会出现一些排序错误的情况。这里我们可以使用开启深度写入的半透明效果解决这个问题。
开启深度写入的半透明效果
我们使用两个Pass来渲染一个模型,第一个Pass开启深度写入但是不写入颜色,这时我们的深度缓冲中已经存在了正确的深度信息;第二个Pass进行正常的透明混合,此时就可以使用和前面相同的操作了。只不过这种方法增加了性能开销。
我们只需要在前一个小节的Shader中加入以下的Pass:
1 2 3 4
| Pass{ ZWrite On ColorMask 0 }
|
这个Pass开启了深度写入,然后使用ColorMask语义设置颜色通道的写掩码,可以是RGB/A/0/RGBA的任意组合,0表示Pass不写入任何颜色通道,不会修改颜色缓冲。
我们得到了这样的效果:
如果使用前一节的Shader,得到的就是错误的渲染:
双面渲染的透明效果
之前的渲染中,我们没有开启双面渲染,这导致半透明物体的内部结构完全是看不到的,看起来就像只有半个一样。因此我们可以使用Cull指令来控制需要剔除哪个面,它的语法是:
Cull Back | Front | Off
设置为Back时背对摄像机的图元不会被渲染,也是默认情况;Front时朝向摄像机的图元不会被渲染;Off时关闭剔除功能。
透明度测试的双面渲染效果
只需要在前面透明度测试的代码中添加下面的Cull命令就能看到效果:
1 2 3 4
| Pass{ Tags {"LightMode" = "ForwardBase"}
Cull Off
|
透明度混合的双面渲染效果
我们使用两个Pass,第一个Pass负责渲染背面,第二个Pass负责渲染正面,Unity将按顺序执行Pass,所以背面将先被渲染,这保证了渲染顺序的正确。所以将之前Shader中的Pass复制一份,第一个Pass开启剔除正面,第二个Pass开启剔除背面。
1 2 3 4 5 6 7 8 9 10
| Pass{ Tags {"LightMode" = "ForwardBase"} Cull Front
Pass{ Tags {"LightMode" = "ForwardBase"} Cull Back
|
得到下面的结果:
透明总结
到这里入门系列就结束了,透明部分主要是使用了各项ShaderLab的设置,没有太多的代码,重点是实现透明效果的两种方法,以及混合的原理,通过开启面的剔除可以得到双面渲染的结果。这里还尝试了使用多个Pass处理问题,是比较消耗性能的做法。
后面部分是中级篇,讲了进阶光照,高级纹理和动态Shder的实现。