笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。
【Unity Shader】(三) ------ 漫反射和高光反射的实现 【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现 【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理 【Unity Shader】(六) ------ 复杂的光照(上) 【Unity Shader】(七) ------ 复杂的光照(下) 【Unity Shader】(八) ------ 高级纹理之立方体纹理及光线反射、折射的实现 目录
前言
一. 渲染纹理
1.1 什么是渲染纹理
二. Mirror
2.1 准备工作
2.2 实现 shader
三. Glass
3.1 GrabPass
ShaderLab: GrabPass
3.2 准备工作
3.3 实现玻璃 shader
四. 总结
前言
本文承接前文 【Unity Shader】(八) ------ 高级纹理之立方体纹理及光线反射、折射的实现,介绍另外一种高级纹理:渲染纹理及一些相关应用。建议读者先翻看前文再阅读本文会更容易理解。
一. 渲染纹理
渲染纹理是本文的重点介绍对象。如果你使用过 RenderTexture 来实现一些特殊的效果,那么你会更能理解本文的内容。
1.1 什么是渲染纹理
在笔者以前的博文中介绍了许多概念,其中大多提到了 缓冲(buffer)这个名词 ,在之前我们实现的效果中,都是将摄像机的渲染效果输出到颜色缓冲中,然后显示到屏幕上。GPU 允许我们将渲染结果输出到一个中间缓冲,称为渲染目标纹理。
根据官方的定义,我们可知,渲染纹理是一种可以实时更新的特殊纹理,同时我们也可以将它像普通纹理一样应用于一个材质中。那么我们如何创建一个渲染纹理呢?通常我们会使用以下两种方法来创建一个渲染纹理:
- 在 Project 下右键创建
- 利用 GrabPass 或者 OnRenderImage 来获取当前屏幕图像(OnRenderImage 函数是我们实现屏幕特效的核心方法之一,所以我不打算在此处进行介绍)
通过以上的方法我们就可以创建出一个渲染纹理了,那么我们来利用它实现一些效果。
二. Mirror
先来看看我们要实现的效果
可以看到场景中有一面区域可以镜像映射场景中的事物图像,这就是我们要实现的类似镜子的效果。那么现在我们开始实现它。
2.1 准备工作
(1)创建一个场景,其中为了观察效果,我使用了前文实现的立方体纹理来作为天空盒。
(2)创建 2 个 Cube,2 个 Sphere,分别赋予不同的颜色用于区别。当然你可以放上你喜欢的模型。
(3)创建一个 Quad ,将 Quad 的位置放在步骤创建的 Cube 和 Sphere 前面,面向 Cube 和 Sphere 。
(4)创建一个 Material 和 一个 RenderTexture ,命名为 Mirror 。将 RenderTexture 赋予材质,将材质赋予 Quad 。
(5)创建一个摄像机,调整位置,视野,使其相当于 Quad 望向于 Cube 和 Spere,将 RenderTexture 赋予摄像机的 Target Texture。
(6)先观察一下效果。
可以看到 Quad 的确有点像一面镜子一样,但有一点十分诡异。没错,那就是物体位置在 X 轴上相反了。
前面说过,我们调整摄像机,让其相当于望向物体,那么它的视野应该是这样的
如果不做什么修改,直接把 RenderTexture 赋予 Quad,那么 Quad 上的图像就是这样的,很显然不符合我们的思维 习惯
(7)因为镜子是镜像的,所以我们要解决步骤 6 中出现的问题,创建一个 shader 命名为 Mirror,实现以下的效果。
2.2 实现 shader
要解决上述问题其实在思路上是比较简单的,只需要进行 X 轴(水平方向上的翻转)就可以了,只是涉及了 UV 和纹理采样的操作,且不用计算光照等,所以这个 shader 是比较简单的。
I. 定义 Properties 块
我们在 Properties 中只需要一个纹理属性,对应着前面创建的 RenderTexture 。
II. 定义输入输出结构体
III.接下来就是在顶点着色器中翻转 UV 的 x 分量,然后在片元着色器中利用翻转过后的 UV 来对 RenderTexture 采样
完整代码:
Shader "Unity/RenderTexture/Mirror" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; struct a2v { fixed4 vertex : POSITION; fixed4 texcoord : TEXCOORD0; }; struct v2f { fixed4 pos : SV_POSITION; fixed4 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.uv.x = 1 - o.uv.x; return o; } fixed4 frag(v2f i) : SV_Target { return tex2D(_MainTex,i.uv); } ENDCG } } FallBack Off }
IV.关闭 FallBack,保存回到 Unity,查看效果
可以看到镜子确实翻转了。
当我们移动物体的时候
可以看到镜子有实时地映射出图像
三. Glass
介绍完了镜子效果,我们接着来介绍另外一个与镜子相关的物体,玻璃。玻璃绝对是很常见的一种效果,而我们实现这种效果的时候正好可以介绍前文所说的使用 GrabPass 抓取屏幕图像的方法。我们先来看一下官方文档对其的定义。
3.1 GrabPass
ShaderLab: GrabPass
GrabPass is a special pass type - it grabs the contents of the screen where the object is about to be drawn into a texture. This texture can be used in subsequent passes to do advanced image based effects.
可以看到 GrabPass 是一种特殊的 Pass ,它可以抓取屏幕中要将对象绘制到纹理中的内容,而且抓取到的纹理可以在其他 Pass 中使用。而它的使用方法如下:
GrabPass 和我们之前使用的 Pass 一样,写在 SubShader 中,同样可以使用 Name 和 Tag 的命令。它有两种使用方法
- GrabPass {} ,这种方法抓取时,后续的 Pass 可以通过 _GrabTexture 来访问屏幕图像,要注意的是,对于为一个使用它的物体,Unity 都会为其单独进行一次抓取操作。这样每个物体都可以得到不同的屏幕图像,这取决于这个物体的渲染顺序及当前屏幕的缓冲颜色。当然,这样会造成不小的性能消耗。
- GrabPass { “TextureName” } ,指定一张纹理,抓取的屏幕图像会存储到这张纹理中,而后续的 Pass 可以访问这张纹理来访问屏幕图像。这种方法抓取屏幕时,Unity 只会在每一帧为第一个使用这张纹理的物体执行一次抓取屏幕的操作。所以,如果场景中有复数个物体使用了这张纹理,那么它们得到的屏幕图像其实是一样的,且为第一个使用这张纹理的物体得到的屏幕图像。这种方法是比较高效的。
那么这里我们使用第二种方法来实现一个玻璃效果。
3.2 准备工作
(1)创建一个 Cube 和 一个 Sphere,将 Sphere 放置在 Cube 中心。
(2)创建一个 Material 和 一个 shader,命名为 Glass,将 Material 赋予 Cube。
(3)修改 shader。
3.3 实现玻璃 shader
先从我们的需求出发,整理思路。我们要实现的是一个玻璃的效果,那么玻璃必定涉及光线的反射和折射,所以我们要计算光照;同时玻璃是透明的,我们要注意渲染顺序;一般而言,玻璃也有不少是花纹的,所以也涉及纹理采样的操作;而且我们还要抓取屏幕。综合起来,大概如下
- 计算光照
- 纹理采样
- 透明物体的处理
- 屏幕抓取
上面就是我们需要注意的主要的几个板块,那么现在我们开始实现这个 shader。
I. 定义 Properties 块
一般也有许多玻璃是带纹理的,所以这里也定义了普通纹理和法线纹理的属性,同时还有天空盒的属性,至于用不用就看实际情况了。_Distortion 表示光线折射时的扭曲程度,_RefractAmount 为 0 时,只含反射效果,_RefractAmount 为 1 时,只含折射效果。
II. 定义渲染队列,且抓取屏幕
因为玻璃是透明物体,所以渲染队列设置为 Transparent ,而后面的渲染状态的设置读者可能会感到奇怪,这里先不提,在后面的学习中,我们还会看到这个问题的。而在 GrabPass 中,我们指定了一个纹理 _RefractionTex。
III. 定义相匹配的属性
这里需要注意的是,我们定义了 _RefractionTex,为了在其它 Pass 中通过它来访问屏幕图像,同时 _RefractionTex_TexelSize 表示纹理的纹素大小,对屏幕图像采样时使用。
IV.接着定义输入输出结构体
这里需要注意的是,我们要将法线方向从切线空间转换到世界空间中,所以我们要构造一个转换矩阵。而输出结构体中,screen 代表我们要对被抓取的屏幕图像的采样坐标,TtoW0,TtoW1,TtoW2 则用于构建转换矩阵。
V.定义顶点着色器
顶点着色器和片元着色器是最重要的两个部分。这里我们分步骤来解释这里的操作
(1)先对顶点坐标进行空间转换。
(2)利用 Unity 内置函数 ComputeGrabScreenPos 得到对应抓取屏幕图像的采样坐标。我们可以在 UnityCG.cginc 中看到它的定义
(3)然后对纹理采样,如果对纹理的相关操作不熟悉的读者,可以翻看 【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现 这篇博文。
(4)最后构建对应此顶点的转换矩阵,实际上该矩阵是 3 x 3 的矩阵,而定义成 4 维变量则是为了利用 w 分量来存储世界空间的顶点坐标。
VI.定义片元着色器
(1)在顶点着色器中,我们利用 TtoW0,TtoW1,TtoW2 的 w 分量来存储世界空间下的顶点坐标,现在我们直接把它抽出来即可
(2)计算视角方向
(3)利用内置函数 UnpackNormal 得到切线空间下法线方向
(4)计算真正的屏幕坐标,然后采样,得到模拟的折射颜色。对这个算法感到疑惑的读者,可以去查阅一下透视除法
(5)分别利用 TtoW0,TtoW1,TtoW2 和上面得到的切线空间下的法线方向做点乘,就可以得到世界空间下的法线方向
(6)利用得到的新的法线方向来计算反射方向
(7)对主纹理采样
(8)对环境映射进行采样,得到反射颜色
(9)在计算最终颜色的式子中,我们可以看到,如果 _RefractAmount 为 0,那么只有反射颜色,如果 _RefractAmount 为 1,那么只有折射颜色。
VII.完整代码
Shader "Unity/RenderTexture/Glass" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _BumpMap ("Normal Map",2D) = "bump" {} _CubeMap ("Environment CubeMap",Cube) = "_Skybox"{} _Distortion ("Distortion",Range(0,100)) = 10 _RefractAmount ("Refract Amount",Range(0.0,1.0)) = 1.0 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Opaque" } GrabPass { "_RefractionTex" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; samplerCUBE _CubeMap; float _Distortion; float _RefractAmount; sampler2D _RefractionTex; float4 _RefractionTex_TexelSize; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; fixed4 texcoord : TEXCOORD1; }; struct v2f { float4 pos : SV_POSITION; float4 screen : TEXCOORD0; fixed4 uv : TEXCOORD1; float4 TtoW0 : TEXCOORD2; float4 TtoW1 : TEXCOORD3; float4 TtoW2 : TEXCOORD4; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.screen = ComputeGrabScreenPos(o.pos); o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex); o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap); 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.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 { float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; i.screen.xy = offset * i.screen.z + i.screen.xy; fixed3 refrCol = tex2D(_RefractionTex, i.screen.xy / i.screen.w).rgb; bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); fixed3 reflDir = reflect(-worldViewDir, bump); fixed4 texColor = tex2D(_MainTex, i.uv.xy); fixed3 reflCol = texCUBE(_CubeMap, reflDir).rgb * texColor.rgb; fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount; return fixed4(finalColor, 1); } ENDCG } } FallBack "Diffuse" }
VIII.保存,回到 Unity ,查看效果
上图均是 _RefractAmount 为 0.75 的效果。希望读者能够动手实现一下,这样才能比图片更能感受到这个效果。
四. 总结
渲染纹理是十分常用的高级纹理,我们常常用它来实现一些十分精美的效果,除此之外,还有一种程序纹理。程序纹理是指由计算机生成的图像,这些图像可以做到十分的真实及丰富,不过笔者并没有学习过相关知识,所以就不误人子弟了。
在实现玻璃效果的 shader 中,涉及了各方面的操作,整体上还是有点复杂的,如果读者感到吃力或完全看不懂,那我希望读者去翻看一下前面的知识点,包括纹理采样,光线反射,折射这些现象的原理及实现方法。最后,希望本文能对您有所帮助。