Back
Featured image of post Shader入门精要-高级纹理

Shader入门精要-高级纹理

模拟反射、折射效果

技术美术——高级纹理

立方体纹理(Cubemap)

立方体纹理(Cubemap) 是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样 反射出周围的环境

优点

  • 立方体纹理的实现简单快捷,而且得到的效果也比较好。

缺点

  • 当场景中引入了新的物体、光源,或者物体发生移动时,我们就需要重新生成立方体纹理。
  • 立方体纹理也仅可以反射环境,但不能反射使用了该立方体纹理的物体本身。这是因为,立方体纹理不能模拟多次反射的结果。

综合考虑下来,立方体纹理适用于凸面体,而不太适用于凹面体 ( 因为凹面体会反射自身 )。

立方体纹理—用于环境映射

立方体纹理最常见的用处是用于环境映射。通过这种方法,我们可以 模拟出金属质感的材质

创建用于环境映射的立方体纹理的三种方法:

  1. 直接由一些特殊布局的纹理创建;
  2. 手动创建一个Cubemap资源,再把6张图赋给它;
  3. 由脚本生成。

第一种方法:提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等(就类似于人物贴图,换成了球形)。然后只需把该纹理的 Texture Type 设置为 Cubemap 即可,Unity 会做好剩下的事情。

第二种方法:先在项目资源中创建一个 Cubemap,然后把它的 6 张纹理拖拽到它的面板中。在 Unity 5 中,官方推荐使用第一种方法创建立方体纹理,这是因为第一种方法可以对纹理数据进行压缩,即可以支持边缘修正、光滑反射(glossy reflection)和 HDR 等功能。

第三种方法:使用 Camera.RenderToCubemap 函数来实现,Camera.RenderToCubemap 函数可以把任意位置观察到的场景图像存储到 6 张图像中,从而创建出该位置上对应的立方体纹理。

  1. 创建一个编辑器脚本,用于将摄像机照射到的图片渲染到 Cubemap 中。由于该代码需要添加菜单条目,因此我们需要把它放在 Editor 文件夹下才能正确执行。原理如下:

    在 renderFromPosition(由用户指定)位置处动态创建一个摄像机,并调用 Camera.RenderToCubemap 函数把从当前位置观察到的图像渲染到用户指定的立方体纹理 cubemap 中,完成后再销毁临时摄像机。

    using UnityEngine;
    using UnityEditor;
    using System.Collections;
    
    public class RenderCubemapWizard : ScriptableWizard {
    
    	public Transform renderFromPosition;
    	public Cubemap cubemap;
    
    	void OnWizardUpdate () {
         helpString = "选择要渲染的坐标位置和要渲染的cubemap";
         isValid = (renderFromPosition != null) && (cubemap != null);
    	}
    
    	void OnWizardCreate () {
         // 创建用于渲染的临时摄像机
         GameObject go = new GameObject( "CubemapCamera");
         go.AddComponent<Camera>();
         // 把它放到物体坐标上
         go.transform.position = renderFromPosition.position;
         // 渲染成cubemap
         go.GetComponent<Camera>().RenderToCubemap(cubemap);
    
         // 销毁临时相机
         DestroyImmediate( go );
    	}
    
    	[MenuItem("GameObject/Render into Cubemap")]
    	static void RenderCubemap () {
         ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
             "Render cubemap", "Render!");
    	}
    }
    
  2. 新建一个用于存储的立方体纹理(在 Project 视图下单击右键,选择 Create -> Legacy -> Cubemap来创建)。为了让脚本可以顺利将图像渲染到该立方体纹理中,我们需要在它的面板中勾选 Readable 选项。

  3. 从 Unity 菜单栏选择 GameObject -> Render into Cubemap,打开我们在脚本中实现的用于渲染立方体纹理的窗口,并把第一步创建的 GameObject 和第二步中的纹理分别拖拽到窗口中的 Render From Position 和 Cubemap 选项。

  4. 单击窗口的 Render!按钮,就可以把从该位置观察到的世界空间下的 6 张图像中渲染到纹理中。

需要注意的是,我们需要为 Cubemap 设置大小,即上图中的 Face size 选项。Face size 值越大,渲染出来的立方体纹理分辨率越大,效果可能更好,单需要占用的内存也越大,这可以由面板最下方显示的内存大小得到。

反射

模拟反射效果很简单,我们只需要通过 入射光线的方向表面法线方向来计算反射方向,再利用 反射方向对立方体纹理采样 即可。

反射用到 Shader 的是在漫反射模型上进行修改的:

  1. 增添三个属性,分别用于控制反射颜色、反射程度和捕捉环境映射纹理
  2. 在顶点着色器中哪个使用reflect函数来计算该顶点的反射方向

reflect ( I, N ) 根据入射光线方向I和表面法向量N计算反射向量,仅对三元向量有效

  1. 在片元着色器中使用 texCUBE 函数和顶点着色器中获得的反射方向对立方体纹理进行采样,得到反射颜色
  2. 最后使用 lerp 对原有漫反射与纹理反射颜色进行插值
Shader "Unlit/Reflect"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _ReflectColor("Reflect Color", Color) = (1,1,1,1)
        _ReflectPower("Reflect Power", Range(0.0, 1)) = 0.2
        _CubeMap("CubeMap", Cube) = "_Skybox" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            float4 _Color;
            float4 _ReflectColor;
            float _ReflectPower;
            samplerCUBE _CubeMap;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldRefl : TEXCOOR2;
                SHADOW_COORDS(3)
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.worldRefl = reflect(-UnityWorldSpaceViewDir(o.worldPos), o.worldNormal); // 反射方向

                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half3 reflection = texCUBE(_CubeMap, i.worldRefl).rgb * _ReflectColor.rgb; // 采样 Cubemap 计算反射

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4(ambient + lerp(diffuse, reflection, _ReflectPower) * atten, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

折射

给定入射角时,我们可以使用 斯涅尔定律 (Snell’s law ) 来计算反射角。当光从介质 1 沿着和表面法线夹角为 θ1 的方向斜射入介质 2 时,我们可以使用如下公式计算折射光线与法线的夹角θ2:

η1 sinθ1 = η2 sinθ2

其中 η1 和 η2 分别是两个介质的 折射率 (index of refraction ) 。折射率是一项重要的物理常量,例如真空的折射率是 1,而玻璃的折射率一般是 1.5。

折射用到 Shader 中与反射类似,依旧使用 refract 函数来计算该顶点的折射方向,但不同的是采用了三个参数:

  • 第一个参数即为入射光线方向,它必须是归一化后的矢量;
  • 第二个参数是表面法线,法线方向同样是要归一化后的;
  • 第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值。
Shader "Unlit/Refract"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _RefractColor("Refract Color", Color) = (1,1,1,1)
        _RefractPower("Refract Power", Range(0.0, 1)) = 1
        _RefractRatio("Refract Ratio", Range(0.01, 1)) = 0.2
        _Cubemap("CubeMap", Cube) = "_Skybox" { }
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            float4 _Color;
            float4 _RefractColor;
            float _RefractPower;
            float _RefractRatio;
            samplerCUBE _Cubemap;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldRefra : TEXCOORD2;
                SHADOW_COORDS(3)
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.worldRefra = refract(-normalize(UnityWorldSpaceViewDir(o.worldPos)), normalize(o.worldNormal), _RefractRatio); // 计算折射方向

                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                
                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half3 refraction = texCUBE(_Cubemap, i.worldRefra).rgb * _RefractColor.rgb; // 采样 Cubemap 计算折射

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4(ambient + lerp(diffuse, refraction, _RefractPower) * atten, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Reflective/VertexLit"
}

菲涅尔反射

使用 菲涅尔反射 (Fresnel reflection ) 来根据视角方向控制反射过程,菲涅尔反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅尔等式进行计算。

一个经常使用的例子是,当你站在湖边,直接低头看脚边的水面时,你会发现水几乎是透明的,你可以直接看到水底的小鱼和石子;但是当你抬头看远处的水面时,会发现几乎看不到水下的情景,而只能看到水面反射的环境。这就是所谓的菲涅尔效果。

真实世界的菲涅尔等式是非常复杂的,但在实时渲染中,我们通常会使用一些近似公式来计算。其中一个著名的计算公式就是 Schlick菲涅尔 近似等式:

FSchlick ( v , n ) = F0 + ( 1 - F0 ) ( 1 - v · n)5

其中,F0 是一个反射系数,用于控制菲涅尔反射的强度,v 是视角方向,n 是表面法线。另一个应用比较广泛的等式是 Empricial菲涅尔 近似等式:

FEmpricial ( v , n ) = max ( 0 , min ( 1 , bias + scale * ( 1 - v · n ) power))

其中 bias、scale 和 power 是控制项。

下面将使用 Schlick菲涅尔 近似等式来模拟菲涅尔反射。在片元着色器中,我们套用 Schlick 菲涅尔公式来计算菲尼尔比率,然后用该比率插值混合 漫反射光照反射光照

Shader "Unlit/Fresnel"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _FresnelScale("Fresnel Scale", Range(0.0, 1)) = 0.5
        _CubeMap("Cube Map", Cube) = "_Skybox"
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags {"LightMode"="ForwardBase"}

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            float4 _Color;
            float _FresnelScale;
            samplerCUBE _CubeMap;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldRefl : TEXCOORD2;
                SHADOW_COORDS(3)
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.worldRefl = reflect(-UnityWorldSpaceViewDir(o.worldPos), o.worldNormal); // 计算反射方向

                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half3 reflection = texCUBE(_CubeMap, i.worldRefl).rgb; // 采样 Cubemap
                half fresnel = _FresnelScale + (1.0 - _FresnelScale) * pow((1.0 - dot(worldViewDir, worldNormal)), 5.0); // 计算 Fresnel 效果
                
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4(ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten, 1.0);
            }
            ENDCG
        }
    }
    "Reflective/VertexLit"
}

渲染纹理

一个摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现代的 GPU 允许我们把整个三维场景渲染到一个中间缓存中,即 渲染目标纹理 (Render Target Texture,RTT) ,而不是传统的 帧缓冲或后备缓冲 ( back buffer ) 。与之相关的是 多重渲染目标 ( Multiple Render Target ,MRT ) ,这种技术指的是 GPU 允许我们把场景同时渲染到多个目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。

Unity 为渲染目标纹理定义了一种专门的纹理类型—— 渲染纹理 ( Render Texture ) 。在 Unity 中使用渲染纹理通常有两种方式:

  • 一种方式是在 Project 目录下创建一个 渲染纹理 ,然后把某个摄像机的 渲染目标 设置成该 渲染纹理 ,这样一来该摄像机的渲染结果就会实时更新到 渲染纹理 中,而不会显示在屏幕上。使用这种方法,我们还可以选择纹理的分辨率、滤波模式等纹理属性。
  • 另一种方式是在屏幕后处理时使用 GrabPass 命令或 OnRenderImage 函数来获取当前屏幕图像,Unity 会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,下面我们可以在自定义的 Pass 中把它们当成普通纹理来处理,从而实现各种屏幕特效。

镜子效果

关键在于新建一个摄像机,一个 Render Texture,将新建的 Render Texture 当作新建相机的 Target Texture ,新建一个 Shader,shader 里声明一个 2D 纹理属性,将新建的 Render Texture 赋值给它。

  1. 创建一个四边形(Quad),调整它的位置和大小,用于充当镜子。
  2. 为了得到从镜子出发观察到的场景图像,我们需要创建一个摄像机,并调整它的位置、裁剪平面、视角等,使得它显示的图像是我们希望的镜子的图像。
  3. 由于这个摄像机不需要直接显示在屏幕上,而是用于渲染纹理。因此我们可以直接创建一个 Texture 并拖拽到该摄像机的 Target Texture 上。

关于 Quad 的着色器实现也非常简单,只需要声明一个纹理属性,然后将该纹理进行左右翻转后输出。

Shader "Unlit/Mirror"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv = v.texcoord;
                o.uv.x = 1 - o.uv.x; // 反转 x 轴
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
    FallBack Off
}

玻璃效果

Unity 中,还可以在 Unity Shader 中使用一种特殊的 Pass 来完成获取屏幕图像的目的,这就是 GrabPass 。通常会使用 GrabPass 来实现诸如 玻璃等透明材质的模拟 ,与使用简单的透明混合不同,使用 GrabPass 可以让我们对物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不再是简单的和原屏幕颜色混合。

当我们在 Shader 中定义了一个 GrabPass 后,Unity 会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的 Pass 中访问它。需要注意的是,在使用 GrabPass 的时候,我们需要额外小心物体的渲染队列设置。正如之前所说,GrabPass 通常用于渲染透明物体,尽管代码里并不包含混合指令,但我们往往仍然需要把物体的 渲染队列 设置成 透明队列 (即 “Queue”=“Transparent” )。这样才能保证渲染物体时,所有的不透明物体都已经被绘制在屏幕上,从而获得正确的屏幕图像。

GrabPass 的两种形式

  • 直接使用 GrabPass{} ,然后在后续的 Pass 中直接使用 _GrabTexture 来访问屏幕图像。但是当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity 都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色。
  • 使用 GrabPass{“TextureName”} ,正如本节所实现,我们可以在后续的 Pass 中使用 TextureName 来访问屏幕图像。使用这种方法同样可以抓取屏幕,但 Unity 只会在每一帧为第一个使用名为 TextureName 的纹理的物体执行一次屏幕抓取操作,而这个纹理同时也可以在其它 Pass 中被访问。这种方法更加高效,因为不管场景中有多少物体使用了该命令,每一帧中 Unity 都只会执行一次抓取操作,这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。

在本节中,我们将会使用 GrabPass 来模拟一个玻璃效果。

思路

这种效果实现非常简单,我们首先使用一张法线纹理来修改模型的法线信息,然后使用反射的方法,通过一个 Cubemap 来模拟玻璃反射,而在模拟折射时,则使用了 GrabPass 获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果。

具体实现

  1. 搭建环境:建立一个房间,然后放置了一个立方体和球体,其中球体位于立方体内部,这是为了模拟玻璃对内部物体的折射效果。创建一个着色器付给立方体。
  2. 在着色器中,我们把 Queue 设置成 Transparent 可以保证该物体渲染时,其它所有不透明物体都已经被渲染到屏幕上了,否则就可能无法正确得到“透过玻璃看到的图像”。
  3. 然后设置 RenderTypeOpaque 则是为了在使用 着色器替换 ( Shader Replacement ) 时,该物体可以在需要时被正确渲染。这通常发生在我们需要得到摄像机的深度和法线纹理时。
  4. 通过关键词 CrabPass 定义了一个抓取屏幕图像的 Pass。在这个 Pass 中我们定义了一个字符串,该字符串内部的名称决定了抓取到的屏幕图像将会被存入哪个纹理中。实际上,我们可以省略声明该字符串,但直接声明纹理名称的方法往往可以得到更高的性能。
  5. 定义了 _RefractionTex_RefractionTex_TexelSize 变量,这对应了在使用 GrabPass 时指定的纹理名称。 _RefractionTex_TexelSize 可以让我们得到该像素的纹理大小,例如一个大小为 256×512 的纹理,它的像素大小为(1/256, 1/512)。我们需要在对屏幕图像的采样坐标进行偏移时使用该量。
  6. 在顶点着色器中通过调用内置的 ComputeGrabScreenPos 函数来得到对应被抓取的屏幕图像的采样坐标。
  7. 在片元着色器中我们对法线纹理进行采样,得到切线空间下的法线方向。我们使用该值和 _Distortion 属性以及 _RefractionTex_TexelSize 来对屏幕图像的采样所需的坐标进行偏移,模拟折射效果。 _Distortion 值越大,偏移量越大,玻璃背后的物体看起来变形程度越大。
  8. 在对屏幕纹理进行采样时使用了 齐次除法 获取视口坐标下的坐标:( i.scrPos.xy / i.scrPos.w )
  9. 最后,我们使用 _RefractAmount 属性对反射和折射颜色进行混合,作为最终的输出颜色。
Shader "Unlit/Glass"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _BumpTex("BumpTex", 2D) = "bump" {}
        _Cubemap("Cube Map", Cube) = "_Skybox" {}
        _Distortion("Distortion", Range(0.0, 100)) = 10
        _RefractAmount("RefractAmount", Range(0.0, 1.0)) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }

        GrabPass 
        {
            "_RefractionTex"  // GrabPass:用于抓取屏幕图像的 pass,内部字符串决定输出纹理名称
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            samplerCUBE _Cubemap;
            float _Distortion;
            float _RefractAmount;

            sampler2D _RefractionTex;        // 获取屏幕图像输出的纹理
            float4 _RefractionTex_TexelSize; // 获取纹理的纹素大小

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 screenPos : TEXCOORD1; // 屏幕采样坐标
                float4 TtoW0 : TEXCOORD2;
                float4 TtoW1 : TEXCOORD3;
                float4 TtoW2 : TEXCOORD4;
            };

            v2f vert (appdata 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 * _BumpTex_ST.xy + _BumpTex_ST.zw; // 缩放和平移

                o.screenPos = ComputeGrabScreenPos(o.pos); // ComputeGrabScreenPos 获取抓取屏幕的采样坐标

                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 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
            {
                half3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                half3 bump = UnpackNormal(tex2D(_BumpTex, i.uv.zw));
                half2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                i.screenPos.xy = offset * i.screenPos.z + i.screenPos.xy;

                half3 refrCol = tex2D(_RefractionTex, i.screenPos.xy / i.screenPos.w).rgb;

                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

                half3 reflDir = reflect(-worldViewDir, bump);
                half4 texColor = tex2D(_MainTex, i.uv.xy);
                half3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;

                half3 col = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;

                return half4(col, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

渲染纹理 VS GrabPass

  • GrabPass 的好处在于实现简单,我们只需在 Shader 中写几行代码就可以实现抓取屏幕的问题。
  • 从效率上来讲,使用渲染纹理的效率往往要好于 GrabPass ,尤其在移动设备上。使用渲染纹理我们可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再次渲染一遍,但我们可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其它方法来控制摄像机是否需要开启。

GrabPass 获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的带宽影响。

在移动设备上,GrabPass 虽然不会重新渲染场景,但它往往需要 CPU 直接读取后备缓冲(back buffer)中的数据,破坏了 CPU 和 GPU 之间的并行性,这是比较耗时的,甚至在一些移动设备上这是不支持的。

命令缓存 (Command Buffers)

在 Unity5 中,Unity 引入了 命令缓冲(Command Buffers) 来允许我们扩展 Unity 的渲染流水线。使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理中,然后在那里进行一些额外的操作,例如模糊等,最后把图像传递给需要使用它的物体进行处理和显示。

有关命令缓冲区更多知识,移步:图形命令缓冲区 - Unity 手册

程序纹理

程序纹理(Procedural Texture) 指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素,例如木头、石子等。

使用 程序纹理 的好处在于我们可以使用各种参数来控制纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这是我们可以得到更加丰富的动画和视觉效果。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[ExecuteInEditMode]//用于编辑器下运行
public class ProceduralTextureGeneration : MonoBehaviour
{
    // 声明材质
    public Material material = null;

    #region Material properties//材质属性
    // 纹理大小
    [SerializeField, SetProperty("textureWidth")]
    // 值通常是2的整数幂
    private int m_textureWidth = 512;
    public int textureWidth
    {
        get
        {
            return m_textureWidth;
        }
        set
        {
            m_textureWidth = value;
            _UpdateMaterial();
        }
    }
    // 纹理背景颜色
    [SerializeField, SetProperty("backgroundColor")]
    private Color m_backgroundColor = Color.white;
    public Color backgroundColor
    {
        get
        {
            return m_backgroundColor;
        }
        set
        {
            m_backgroundColor = value;
            _UpdateMaterial();
        }
    }
    // 圆点颜色
    [SerializeField, SetProperty("circleColor")]
    private Color m_circleColor = Color.yellow;
    public Color circleColor
    {
        get
        {
            return m_circleColor;
        }
        set
        {
            m_circleColor = value;
            _UpdateMaterial();
        }
    }
    // 模糊因子
    [SerializeField, SetProperty("blurFactor")]
    // 用来磨合圆形边界
    private float m_blurFactor = 2.0f;
    public float blurFactor
    {
        get
        {
            return m_blurFactor;
        }
        set
        {
            m_blurFactor = value;
            _UpdateMaterial();
        }
    }
    #endregion

    /// <summary>生成的纹理</summary>
    private Texture2D m_generatedTexture = null;

    // 初始化
    void Start()
    {
        // 检测材质是否为空
        if (material == null)
        {
            // 获取渲染器
            Renderer renderer = gameObject.GetComponent<Renderer>();
            if (renderer == null)
            {
                Debug.LogWarning("找不到渲染器。");
                return;
            }
            // 获取渲染器的材质
            material = renderer.sharedMaterial;
        }

        _UpdateMaterial();
    }

    /// <summary>
    /// 生成纹理
    /// </summary>
    private void _UpdateMaterial()
    {
        // 材质不为空
        if (material != null)
        {
            // 调用方法生成程序纹理
            m_generatedTexture = _GenerateProceduralTexture();
            // 将纹理赋值给材质
            material.SetTexture("_MainTex", m_generatedTexture);
        }
    }

    private Color _MixColor(Color color0, Color color1, float mixFactor)
    {
        Color mixColor = Color.white;
        mixColor.r = Mathf.Lerp(color0.r, color1.r, mixFactor);
        mixColor.g = Mathf.Lerp(color0.g, color1.g, mixFactor);
        mixColor.b = Mathf.Lerp(color0.b, color1.b, mixFactor);
        mixColor.a = Mathf.Lerp(color0.a, color1.a, mixFactor);
        return mixColor;
    }
    /// <summary>
    /// 生成程序纹理
    /// </summary>
    /// <returns></returns>
    private Texture2D _GenerateProceduralTexture()
    {
        Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);

        // 定义圆与圆之间的间距
        float circleInterval = textureWidth / 4.0f;
        // 定义圆的半径
        float radius = textureWidth / 10.0f;
        // 定义模糊系数
        float edgeBlur = 1.0f / blurFactor;

        for (int w = 0; w < textureWidth; w++)
        {
            for (int h = 0; h < textureWidth; h++)
            {
                // 使用背景颜色进行初始化
                Color pixel = backgroundColor;

                // 依次画九个圆
                for (int i = 0; i < 3; i++)
                {
                    for (int j = 0; j < 3; j++)
                    {
                        // 定义圆的圆心
                        Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));

                        // 计算当前像素与圆心的距离
                        float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;

                        // 模糊圆的边界
                        Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));

                        // 与之前得到的颜色进行混合
                        pixel = _MixColor(pixel, color, color.a);
                    }
                }
                // 写入像素
                proceduralTexture.SetPixel(w, h, pixel);
            }
        }
        // 将像素值写入纹理
        proceduralTexture.Apply();

        return proceduralTexture;
    }
}

总结

常规反射

v2f vert (appdata v)
{
    ...
    o.worldRefl = reflect(-UnityWorldSpaceViewDir(o.worldPos), o.worldNormal); // 反射方向

    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    ...
    half3 reflection = texCUBE(_CubeMap, i.worldRefl).rgb * _ReflectColor.rgb;

    half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

    return half4(ambient + lerp(diffuse, reflection, _ReflectPower), 1.0);
}

常规折射

v2f vert (appdata v)
{
    ...
    o.worldRefra = refract(-normalize(UnityWorldSpaceViewDir(o.worldPos)), normalize(o.worldNormal), _RefractRatio);

    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    ...
    half3 refraction = texCUBE(_Cubemap, i.worldRefra).rgb * _RefractColor.rgb;

    half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

    return half4(ambient + lerp(diffuse, refraction, _RefractPower), 1.0);
}

菲涅尔反射

v2f vert (appdata v)
{
    ...
    o.worldRefl = reflect(-UnityWorldSpaceViewDir(o.worldPos), o.worldNormal);

    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    ...
    half3 reflection = texCUBE(_CubeMap, i.worldRefl).rgb;
    half fresnel = _FresnelScale + (1.0 - _FresnelScale) * pow((1.0 - dot(worldViewDir, worldNormal)), 5.0);
    
    half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;

    return half4(ambient + lerp(diffuse, reflection, saturate(fresnel)), 1.0);
}

镜子

v2f vert(a2v v)
{
    ...    
    o.uv = v.texcoord;
    // 需要镜像图像
    o.uv.x = 1 - o.uv.x;
    
    return o;
}

fixed4 frag(v2f i): SV_Target
{
    // 对纹理采样输出
    return tex2D(_MainTex, i.uv);
}

玻璃

v2f vert (appdata v)
{
    ...
    o.screenPos = ComputeGrabScreenPos(o.pos); // ComputeGrabScreenPos 获取抓取屏幕的采样坐标
    ...
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    ...
    half2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
    i.screenPos.xy = offset * i.screenPos.z + i.screenPos.xy;

    half3 refrCol = tex2D(_RefractionTex, i.screenPos.xy / i.screenPos.w).rgb;

    bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

    half3 reflDir = reflect(-worldViewDir, bump);
    half4 texColor = tex2D(_MainTex, i.uv.xy);
    half3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;

    half3 col = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;

    return half4(col, 1.0);
}