《Unity Shader入门精要》学习笔记第十章 高级纹理

本文章用于帮助自己学习,因此只记录一些个人认为比较重要或者还不够熟悉的内容。
原作者:http://blog.csdn.net/candycat1992/article/

第十章 高级纹理

10.1 立方体纹理

在图形学中,立方体纹理(Cubemap)是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样 反射岀周围的环境。
立方体纹理一共包含了6张图像,这些图像对应了一个立方体的6个面。
与之前使用二维纹理坐标不同,对立方体纹理采样我们需要提供一个三维的纹理坐标,这个三维纹理坐标表示了我们在世界空间下的一个3D方向。这个方向矢量从立方体的中心岀发,当它向外部延伸时就会和立方体的6个纹理之一发生相交,而釆样得到的结果就是由该交点计算而来的,如图:
在这里插入图片描述
使用立方体纹理的好处在于,它的实现简单快速,而且得到的效果也比较好。但它也有一些缺点,例如当场景中引入了新的物体、光源,或者物体发生移动时,我们就需要重新生成立方体纹理。

10.1.1天空盒子

天空盒子(Skybox)是游戏中用于模拟背景的一种方法。当我们在场景中使用了天空盒子时,整个场景就被包围在一个立方体内。这个立方体的每个面使用的技术就是立方体纹理映射技术

在Unity中,想要使用天空盒子非常简单。我们只需要创建一个Skybox材质,再把它赋给该场景的相关设置即可。
我们首先来看如何创建一个Skybox材质。

  1. 新建一个材质。
  2. 在SkyboxMat的Shader下拉菜单中选择Unity自带的 Skybox,该材质需要6张纹理
  3. 使用6张纹理对第2步中的材质赋值,注意这6张纹理的正确位置(如posz纹理对应了Front [+Z]属性)。为了让天空盒子正常渲染,我们需要把这6张纹理的Wrap Mode设置为Clamp,以防止在接缝处出现不匹配的现象。

上述步骤得到的材质如下图所示。
在这里插入图片描述
然后在Lighting菜单中,把SkyboxMat 赋给Skybox 选项即可,效果如图所示:
在这里插入图片描述
在Unity中,天空盒子是在所有不透明物体之后渲染的,而其背后使用的网格是一个立方体 或一个细分后的球体。

10.1.2创建用于环境映射的立方体纹理

除了天空盒子,立方体纹理最常见的用处是用于环境映射。通过这种方法,我们可以模拟出金属质感的材质。
在Unity中,创建用于环境映射的立方体纹理的常用方法有两种:第一种方法是直接由一些特殊布局的纹理创建;第二种方法是由脚本生成。
如果使用第一种方法,我们需要提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等。然后,我们只需要把该纹理的Texture Type设置为Cubemap即可,Unity 会为我们做好剩下的事情。在基于物理的渲染中,我们通常会使用一张HDR图像来生成高质量的Cubemap。可在官方文档(http://docs.unity3d.com/Manual/class-Cubemap.html) 中找到更多的资料。
第二种方法,可以根据物体在场景中位置的不同,生成它们各自不同的立方体纹理。这时,我们就可以在Unity中使用脚本来创建。这是通过利用Unity提供的 Camera.RenderToCubemap函数来实现的。Camera.RenderToCubemap函数可以把从任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理。
在 Unity 的脚本手册(http://docs.unity3d.com/ScriptRefbrence/Camera.RenderToCubemap.html) 中给出了如何使用Camera.RenderToCubemap函数来创建立方体纹理的代码。其中关键代码如下:

void OnWizardCreate()
{
    // 创建临时摄像机用于渲染
    GameObject go = new GameObject("CubemapCamera");
    go.AddComponent<Camera>();

    //renderFromPosition (由用户指定)位置处动态创建一个摄像机
    go.transform.position = renderFromPosition.position;
    go.transform.rotation = Quaternion.identity;

    //调用Camera.RenderToCubemap函数把从当前位置观察到的图像渲染到用户指定的立方体纹理cubemap中
    go.GetComponent<Camera>().RenderToCubemap(cubemap);

    // 销毁临时摄像机
    DestroyImmediate(go);
}

之后创建一个Cubemap

  1. 创建一个空的GameObject对象
  2. 新建一个用于存储的立方体纹理(在Project视图下单击右键,选择Create — Legacy — Cubemap来创建)。为了让脚本可以顺利将图像渲染到该立方体纹理中,我们需要在它的面板中勾选Readable选项
  3. 从Unity菜单栏选择GameObject -> Render into Cubemap,打开我们在脚本中实现的用于渲染立方体纹理的窗口,并把第1步中创建的GameObject和第2步中创建的Cubemap_0分别拖曳到窗口中的Render From Position和Cubemap选项,然后单击窗口中的Render!按钮,就可以把从该位置观察到的世界空间下的6张图像渲染到 Cubemap_0中。如下图所示。

在这里插入图片描述
在这里插入图片描述

10.1.3反射

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



Shader "Unity Shaders Book/Chapter 10/Reflection"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        //控制反射颜色
        _ReflectColor("ReflectionColor",Color) = (1,1,1,1)
        //控制材质反射程度
        _ReflectAmount("Reflect Amount",Range(0,1)) = 1
        //模拟反射的环境映射纹理
        _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}
    }
    SubShader 
	{
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#pragma multi_compile_fwdbase
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			fixed4 _ReflectColor;
			fixed _ReflectAmount;
			samplerCUBE _Cubemap;
			
			struct a2v 
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f 
			{
				float4 pos : SV_POSITION;
				float3 worldPos : TEXCOORD0;
				fixed3 worldNormal : TEXCOORD1;
				fixed3 worldViewDir : TEXCOORD2;
				//世界空间下的反射方向
				fixed3 worldRefl : TEXCOORD3;
				SHADOW_COORDS(4)
			};
			
			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.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
				
				//计算世界空间下的反射方向
				o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
				
				TRANSFER_SHADOW(o);
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target 
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));		
				fixed3 worldViewDir = normalize(i.worldViewDir);		
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
				
				//使用反射方向对Cubemap进行采样
				fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				//使用_ReflectAmount混合漫反射颜色和反射颜色
				fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
				
				return fixed4(color, 1.0);
			}
			
			ENDCG
		}
	}
	FallBack "Reflective/VertexLit"
}

效果如下:
在这里插入图片描述
在上面的计算中,我们选择在顶点着色器中计算反射方向。当然,我们也可以选择在片元着色器中计算,这样得到的效果更加细腻。但是,对于绝大多数人来说这种差别往往是可以忽略不计的,因此出于性能方面的考虑,我们选择在顶点着色器中计算反射方向。

10.1.4折射

折射:当光线从一种介质斜射入另一种介质时,传播方向一般会发生改变。当给定入射角时,我们可以使用斯涅耳定律来计算反射角。当光从介质1沿着表面法线夹角为θ(1)的方向斜射入介质2时,我们可以使用如下公式计算折射光线与法线的夹角θ(2):
在这里插入图片描述
其中η(1)和η(2) 分别是两个介质的折射率。例如真空的折射率是1,而玻璃的折射率一般是1.5。下图给出了这些变量之间的关系。
在这里插入图片描述
对一个透明物体来说,一种更准确的模拟方法需要计算两次折射——一次是当光线进它的内部时,而另一次则是从它内部射出时。但是,想要在实时渲染中模拟出第二次折射方向是比较复杂的,因此,在实时渲染中通常仅模拟第一次折射
代码:

Shader "Unity Shaders Book/Chapter 10/Refraction"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _RefractColor("ReflectionColor",Color) = (1,1,1,1)
        _RefractAmount("Reflect Amount",Range(0,1)) = 1
        //得到不同介质的透射比
        _RefractRatio("Refraction Ratio",Range(0.1,1)) = 0.5
        _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#pragma multi_compile_fwdbase
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			fixed4 _RefractColor;
			fixed _RefractAmount;
            fixed _RefractRatio;
			samplerCUBE _Cubemap;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
				fixed3 worldNormal : TEXCOORD1;
				fixed3 worldViewDir : TEXCOORD2;
				fixed3 worldRefr : TEXCOORD3;
                SHADOW_COORDS(4)
            };



            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.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
                //计算折射方向,
                //第一个参数即为入射光线的方向,这里是视线方向因为光路可逆
                //第二个参数是表面法线,
                //第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值.
                o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);

                TRANSFER_SHADOW(o);

                return o;
            }

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

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));

                //使用折射方向对Cubemap进行采样
				fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				//使用_ReflectAmount混合漫反射颜色和折射颜色
				fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;

                return fixed4(color, 1.0);
            }
            ENDCG
        }
    }
}

效果如下:
在这里插入图片描述

10.1.5 菲涅耳反射

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

计算菲涅耳反射需要使用菲涅耳等式。真实世界的菲涅耳等式是非常复杂的,但在实时渲染中,我们通常会使用一些近似公式来计算。其中一个著名的近似公式就是Schlick菲涅耳近似等式
在这里插入图片描述
其中,F(0)是一个反射系数,用于控制菲涅耳反射的强度,v是视角方向,n是表面法线。另一个应用比较广泛的等式是Empricial菲涅耳近似等式
在这里插入图片描述
其中,bias、scale和power是控制项。
代码:

Shader "Unity Shaders Book/Chapter 10/Fresnel"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _FresnelScale("Fresnel Scale",Range(0,1)) = 0.5
        _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}

    }
    SubShader
	{
		Tags { "RenderType" = "Opaque" "Queue" = "Geometry"}

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

			CGPROGRAM

			#pragma multi_compile_fwdbase

			#pragma vertex vert
			#pragma fragment frag

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

			fixed4 _Color;
			fixed _FresnelScale;
			samplerCUBE _Cubemap;

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

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldPos : TEXCOORD0;
				fixed3 worldNormal : TEXCOORD1;
				fixed3 worldViewDir : TEXCOORD2;
				fixed3 worldRefl : TEXCOORD3;
				SHADOW_COORDS(4)
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);

				o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);

				TRANSFER_SHADOW(o);

				return o;
			}

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

				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

				//计算阴影和光照衰减
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)

				fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;

				//使用Schlick菲涅耳近似等式来计算fresnel变量
				fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);

				//使用fresnel混合漫反射光照和反射光照
				fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));

				fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;

				return fixed4(color, 1.0);

			}
			ENDCG
		}
	
	}	
    FallBack "Reflective/VertexLit"
}

效果如下:
在这里插入图片描述
如图是Fresnel Scale为0时的效果,只具有边缘光照,就像现实中的全反射现象,只有在视线方向与表面法线达到一定角度时才会发生。

10.2 渲染纹理

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

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

10.2.1 镜子效果

  1. 在场景中创建6个立方体,并调整它们的位置和大小,使得它们构成围绕着摄像机的房 间的6面墙。给它们赋予在9.5节中创建的标准材质。向场景中添加3 个点光源,并调整它们的位置,使它们可以照亮整个房间。
  2. 创建3个球体和两个正方体,调整它们的位置和大小,并给它们赋予在9.5节中创建的标准材质。这些物体将作为房间内的饰品。
  3. 创建一个四边形(Quad),调整它的位置和大小,它将作为镜子。把创建的镜子材质赋给它。
  4. 在Project视图下创建一个渲染纹理(右键单击Create — Render Texture),命名为MirrorTexture。它使用的纹理设置如下图所示。
  5. 最后,为了得到从镜子出发观察到的场景图像,我们还需要创建一个摄像机。由于这个摄像机不需要直接显示在屏幕上,而是用于渲染到纹理。因此,我们把第4步中创建的MirrorTexture拖曳到该摄像机的Target Texture上。下图显示了摄像机面板和渲染纹理的相关设置。

在这里插入图片描述
在这里插入图片描述

镜子材质代码:

Shader "Unity Shaders Book/Chapter 10/Mirror"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            sampler2D _MainTex;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 texcoord : TEXCOORD0;
            };

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



            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                //翻转x分量的纹理坐标,
                //因为镜子里图像左右相反
                o.uv.x = 1 - o.uv.x;

                return o;
            }

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

保存后返回场景,并把我们创建的MirrorTexture渲染纹理拖曳到材质的Main Tex属性中,效果如下:
在这里插入图片描述

10.2.2玻璃效果

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

需要注意的是,在使用GrabPass的时候,我们需要额外小心物体的渲染队列设置。正如之前所说,GrabPass通常用于渲染透明物体,尽管代码里并不包含混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(即"Queue"=“Transparent”)。这样才可以保证当渲染该物体时, 所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。
代码:


Shader "Unity Shaders Book/Chapter 10/Glass Refraction" 
{
	Properties 
	{
		_MainTex ("Main Tex", 2D) = "white" {}
		_BumpMap ("Normal Map", 2D) = "bump" {}
		_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {}
		//控制折射时图像扭曲程度
		_Distortion ("Distortion", Range(0, 100)) = 10
		//控制折射程度(值为0时该玻璃只包含反射效果,值为1时只包括折射效果)
		_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
	}
	SubShader 
	{
		//Queue设置成Transparent可以确保该物体渲染时,
		//其他所有不透明物体都已经被渲染到屏幕上了
		Tags { "Queue"="Transparent" "RenderType"="Opaque" }
		
		//定义一个抓取屏幕图形的Pass
		//该字符串内部的名称决定了抓取得到的屏幕图像将会被存入哪个纹理中
		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;
			fixed _RefractAmount;
			//对应使用GrabPass时指定的纹理名称
			sampler2D _RefractionTex;
			//_TexelSize可以得到该纹理的纹素大小,
			//例如一个大小为256X512的纹理,它的纹素大小为(1/256, 1/512)。
			//我们需要在对屏幕图像的釆样坐标进行偏移时使用该变量
			float4 _RefractionTex_TexelSize;
			
			struct a2v 
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT; 
				float2 texcoord: TEXCOORD0;
			};
			
			struct v2f 
			{
				float4 pos : SV_POSITION;
				float4 scrPos : TEXCOORD0;
				float4 uv : TEXCOORD1;
				float4 TtoW0 : TEXCOORD2;  
			    float4 TtoW1 : TEXCOORD3;  
			    float4 TtoW2 : TEXCOORD4; 

			};
			
			v2f vert (a2v v) 
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				//调用函数得到顶点坐标对应被抓取的屏幕图像的采样坐标
				o.scrPos = 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).xyz;  
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 
				
				//切线空间到世界空间变换矩阵,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 = normalize(UnityWorldSpaceViewDir(worldPos));
				
				//得到切线空间法线方向
				fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));	
				
				//对屏幕图像的采样坐标进行便宜,模拟折射效果
				float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
				//offset乘以scrPos的z分量,z分量越大折射效果越明显
				i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
				//对scrPos进行齐次除法得到真正的屏幕坐标,
				//然后用该坐标对抓取的屏幕图像进行采样,得到折射颜色
				fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.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"
}

效果如下:
在这里插入图片描述
实际上,GrabPass支持两种形式:

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

10.2.3渲染纹理 vs. GrabPass

尽管GrabPass和之前使用的渲染纹理+额外的摄像机的方式都可以抓取屏幕图像,但它们还是有一些不同的。GrabPass的好处在于实现简单,我们只需要再Shader中写几行代码就可以实现抓取屏幕的目的。而要使用渲染纹理的话,我们首先需要创建一个渲染纹理和一个额外的摄像机,再把该摄像机的Render Target 设置为新建的渲染纹理对象,最后把该渲染纹理传递给响应的Shader。

但从效率上来说,使用渲染纹理的效率往往要好于GrabPass,尤其在移动设备上。使用渲染纹理我们可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再次渲染一遍,但我么可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其他方法来控制摄像机是否需要开启。而使用GrabPass获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的带宽影响。而且在移动设备上,GrabPass虽然不会重新渲染场景,但它往往需要CPU直接读取后备缓冲中的数据,破坏了CPU和GPU之间的并行性,这是比较耗时的,甚至在一些移动设备上这是不支持的。

10.3程序纹理

程序纹理(Procedural Texture)指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素,例如木头、石子等。
使用程序纹理的好处在于我们可以使用各种参数来控制纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这使得我们可以得到更加丰富的动画和视觉效果。

10.3.1在Unity中实现简单的程序纹理

代码:

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

[ExecuteInEditMode]
public class ProceduralTextureGeneration : MonoBehaviour {

	//声明一个材质,
	//这个材质将使用脚本中生成的程序纹理
	public Material material = null;

	//声明纹理参数
	#region Material properties
	[SerializeField, SetProperty("textureWidth")]
	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.blue;
	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

	//声明纹理变量保存生成的纹理
	private Texture2D m_generatedTexture = null;

	//在Start函数中进行检查,得到需要使用该程序纹理的材质
	void Start () {
		if (material == null) {
			//如果材质为空,
			//就从使用该脚本的物体上得到相应的材质
			Renderer renderer = gameObject.GetComponent<Renderer>();
			if (renderer == null) {
				Debug.LogWarning("Cannot find a renderer.");
				return;
			}

			material = renderer.sharedMaterial;
		}

		_UpdateMaterial();
	}

	private void _UpdateMaterial() {
		if (material != null) {
			//调用函数生成程序纹理,赋给m_generatedTexture变量
			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;
	}

	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;

				//依次画9个圆
				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;
	}
}

效果如下:
在这里插入图片描述

10.3.2Unity的程序材质

在Unity中,有一类专门使用程序纹理的材质,叫做程序材质(Procedural Materials)。这类材质和我们之前使用的那些材质在本质上是一样的,不同的是,它们使用的纹理不是普通的纹理, 而是程序纹理。需要注意的是,程序材质和它使用的程序纹理并不是在Unity中创建的,而是使用了一个名为Substance Designer的软件在Unity外部生成的。

Substance Designer是一个非常出色的纹理生成工具,很多3A的游戏项目都使用了由它生成 的材质。我们可以从Unity的资源商店或网络中获取到很多免费或付费的Substance材质。这些材质都是以.sbsar为后缀的,我们可以直接把这些材质像其他资源一样拖入Unity项目中。
当把这些文件导入Unity后,Unity就会生成一个程序纹理资源(Procedural Material Asset)。

猜你喜欢

转载自blog.csdn.net/weixin_46124783/article/details/115312629