700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Unity_Shader中级篇_10_Unity Shader入门精要

Unity_Shader中级篇_10_Unity Shader入门精要

时间:2019-07-20 01:30:22

相关推荐

Unity_Shader中级篇_10_Unity Shader入门精要

高级纹理

第七章学习了关于基础纹理的内容,这些纹理包括法线纹理、渐变纹理和遮罩纹理等。这些纹理尽管用处不同,但他们都属于低维(一维或二维)纹理。此时我们将学习一些更复杂的纹理。在10.1中我们会学习如何使用立方体纹理(Cubemap)实现环境映射。然后,我们会在10.2中了解一类特殊的纹理——渲染纹理(Render Texture),渲染纹理非常强大。最后10.3中将学习程序纹理(Procedure Texture)。

10.1立方体纹理

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

和之前见到的纹理不同,立方体纹理一共包含了6张图像,这些图像对应了一个立方体的6个面,立方体纹理的名称也由此而来。立方体的每个面表示沿着世界空间下的轴向(上、下、左、右、前、后)观察所得的图像。那么,我们如何对这样一种纹理进行采样呢?和之前使用二维纹理坐标不同,对立方体纹理采样我们需要提供一个三维的纹理坐标,这个三维纹理坐标表示了我们在世界空间下的一个3D方向。这个方向矢量从立方体的中心出发,当它向外部延伸时就会和立方体的6个纹理之一发生相交,而采样得到的结果就是由该交点计算而来的。图10.1给出了使用方向矢量对立方体纹理采样的过程。

使用立方体纹理的好处在于,它的实现简单快速,而且得到的效果也比较好。但他也有一些缺点,例如当场景中引入了新的物体、光源,或者物体发生移动时,我们就需要重新生成立方体纹理。除此之外,立方体纹理也仅可以反射环境,但不能反射使用了该立方体的物体本身。这是因为,立方体纹理不能模拟多次反射的结果,例如两个金属球互相反射的情况(实时上,Unity5 引入的全局光照系统允许实现这样的自反射效果,19)。由于这样的原因,想要得到令人信服的渲染结果,我们应该尽量对凸面体而不要对凹面体使用立方纹理(因为凹面体会反射自身)。

立方体纹理在实时渲染中有很多应用,最常见的是用于天空盒子(Skybox)以及环境映射。

天空盒子

天空盒子(Skybox)是游戏中用于模拟背景的一种方法。天空盒子这个名字包含了两个信息:它是用来模拟天空的(尽管现在我们仍可以用它模拟室内等背景),它是一个盒子。当我们在场景中使用了天空盒子时,整个场景就被包围在一个立方体内。这个立方体的每个面使用的技术就是立方体纹理映射技术。

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

我们首先来看如何创建一个Skybox材质。

(1)新建一个材质(SkyboxMat)。

(2)在材质的Unity Shader 下拉菜单中选择Unity自带的Shybox/6 Sided,该材质需要六张纹理。

(3)使用本书资源中的Assets/Textures/Chapter10/Cubemaps文件夹下的六张纹理对第二步中的材质赋值,注意这六张纹理的正确位置(如posz纹理对应了Front[+Z]属性)。为了让天空盒子正常渲染,我们需要把这六张纹理的Wrap Mode设置为Clamp,以防止在接缝处出现不匹配的现象。

上面的材质中,除了六张纹理属性外还有三个属性:Tint Color,用于控制该材质的整体颜色;Exposure,用于调整天空盒子的高度;Rotation,用于调整天空盒子沿+y轴方向的旋转角度。

添加Skybox的方法:

(1)在Window——Lighting菜单中,把SkyboxMat赋给Skybox选项。

为了让摄影机正常显示天空盒子,我们还需要保证渲染场景的摄像机的Camera组件中的Clear Flags被设置为Skybox。

需要说明的是,在Window——Lighting——Skybox中设置的天空盒子会应用于该场景中的所有摄像机。如果我们希望某些摄像机可以使用不同的天空盒子,可以通过向该摄像机添加Skybox组件来覆盖掉之前的设置。也就是说,我们可以在摄像机上单机Component——Rendering——Skybox来完成对场景默认天空盒子的覆盖。

在Unity中,天空盒子是在所有不透明物体之后渲染的,而其背后使用的网格是一个立方体或一个细分后的球体。

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

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

在Unity5中,创建用于环境映射的立方体纹理的方法有三种:第一种方法是直接由一些特殊布局的纹理创建;第二种方法是手动创建一个Cubemap资源,再把六张图赋给它;第三种方法是由脚本生成。

如果使用第一种方法,我们需要提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等。然后,我们只需要把该纹理的Texture Type设置为Cubemap即可,Unity会为我们做好剩下的事情。在基于物理的渲染中,我们通常会使用一张HDR图像来生成高质量的Cubemap(18)。(/Manual/class-Cubemap.html)

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

前面两种方法都需要我们提前准备好立方体纹理的图像,他们得到的立方体纹理往往是被场景中的物体所共同的。但在理想情况下,我们希望根据物体在场景中位置的不同,生成它们各自不同的立方体纹理。这时,我们就可以在Unity中使用脚本来创建。这时通过利用Unity提供的Camera.RenderToCubemap函数来实现的。Camera.RenderToCubemap函数可以把从任意位置观察到的场景图像存储到六张图像中,从而创建出该位置上对应的立方体纹理。

在Unity的脚本手册(/ScriptReference/Camera.RenderToCubemap.html)中给出了如何使用Camera.RenderToCubemap函数来创建立方体纹理的代码。

using UnityEngine;using UnityEditor;using System.Collections;public class RenderCubemapWizard : ScriptableWizard {public Transform renderFromPosition;public Cubemap cubemap;void OnWizardUpdate () {//选择transform,以呈现from和cubemap以呈现helpString = "Select transform to render from and cubemap to render into";isValid = (renderFromPosition != null) && (cubemap != null);}void OnWizardCreate () {// 为渲染创建临时相机GameObject go = new GameObject( "CubemapCamera");go.AddComponent<Camera>();// 把它放在对象上go.transform.position = renderFromPosition.position;// 渲染成cubemapgo.GetComponent<Camera>().RenderToCubemap(cubemap);// 破坏临时相机DestroyImmediate( go );}[MenuItem("GameObject/Render into Cubemap")]static void RenderCubemap () {ScriptableWizard.DisplayWizard<RenderCubemapWizard>("Render cubemap", "Render!");}}

在上面的代码中,我们在renderFromPosition(由用户指定)位置处动态创建一个摄像机,并调用Camera.RenderToCubemap函数把从当前位置观察到的图像渲染到用户指定的立方体纹理cubemap中,完成后在销毁临时摄像机。由于该代码需要添加菜单栏条目,因此我们需要把它放在Editor文件夹下才能正确执行。

当准备好上述代码后,要创建一个Cubemap非常简单。

(1)我们使用10.1.1中的场景,并创建一个空的GameObject对象。我们会使用该GameObject的位置信息来渲染立方体纹理。

(2)新建一个用于存储的立方体纹理(在Project视图下单机右键,选择Create-Legacy-Cubemap来创建)。(Cubemap_0)。为了让脚本可以顺利将图像渲染到该立方体纹理中,我们需要在它的面板中勾选Readable选项。

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

(4)单机窗口中的Render!按钮,就可以把从该位置观察到的世界空间下的六张图形渲染到Cubemap_0中。

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

准备好了需要的立方体纹理后,我们就可以对物体使用环境映射技术。而环境映射最常见的应用就是反射和折射。

反射

使用了反射效果的物体通常看起来就像镀了层金属。想要模拟反射效果很简单,我们只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。

(1)创建场景(Scene_10_1_3)。替换掉Unity5中场景默认的天空盒子,而把10.1.1中创建的天空盒子材质拖拽到Window-Lighting-Skybox组件来覆盖默认的天空盒子。

(2)拖拽一个Teaot模型。

(3)新建一个材质,(ReflectionMat),把材质赋给第二步中的模型。

(4)新建一个Unity Shader,在本书资源直中,该Shader名为Chapter10-Reflection。把Chapter10-Reflection赋给第三步中创建的材质。

Shader "Unity Shaders Book/Chapter 10/Reflection" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)//用于控制反射颜色_ReflectColor ("Reflection Color", 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)};//在顶点着色器中计算了该顶点处的反射方向,这是通过使用CG的reflect函数来实现的“v2f vert(a2v v) {v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(_Object2World, 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));// 在世界空间中使用反射目录来访问cubemapfixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);// 将漫反射的颜色与反射的颜色混合fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;return fixed4(color, 1.0);}//对立方体纹理的采样需要使用CG的texCUBE函数。注意到,在上面的计算中,我们在采样时//并没有对i.worldRefl进行归一化操作。这是因为,用于采样的参数仅仅是作为方向变量传递给//texCUBE函数的,因此我们没有必要进行一次归一化的操作。然后,我们是使用_ReflectAmount来//混合漫反射颜色和反射颜色,并和环境光照相加后返回。//在上面的计算中,我们选择在顶点着色器中计算反射方向。当然,我们也可以选择子啊片元着色器中计算,//这样得到的效果更加细腻。但是,对于绝大多数人来说这种差别往往是可以忽略不计的,因此//出于性能的考虑,我们选择在顶点着色器中计算反射方向。ENDCG}}FallBack "Reflective/VertexLit"}

折射

在这一节中,我们将学习如何在Unity Shader中模拟另一个环境映射的常见应用——折射。

折射的物理原理比反射复杂一些。我们使用斯涅尔定律(Snell“s Law)来计算反射角。当光从介质1沿着和表面法线夹角为01的方向斜射入介质2时,我们可以使用如下公式计算折射光线与法线的夹角02

其中,η1和η2分别是两个介质的的折射率(index of refraction)。折射率是一项重要的物理常数,例如真空的折射率是1,而玻璃的折射率一般是1.5。图10.8给出了这些变量之间的关系。

通常来说。当得到折射方向后我们就会直接使用它来对立方体纹理进行采样,但这是不符合物理规律的。对一个透明物体来说,一种更准确的模拟方法需要计算两次折射——一次是当光线进入它的内部是,而另一次则是从它内部折射出时。但是,想要在实时渲染中模拟出第二次折射方向是比较复杂的,而且仅仅模拟一次得到的效果从视觉上看起来“也挺像那么回事的”。正如我们之前提到的——图形学第一准则“如果它看起来是对的,那么它就是对的”。因此,在实时渲染中我们通常仅模拟第一次折射。

为此,我们需要做如下准备工作。

(1)新建一个场景,在资源中,该场景名为Scene_10_1_4,我们替换掉Unity5中场景默认的天空盒子,而把10.1.1节中创建的天空盒子材质拖拽到Window-Lighting-Skybox选项中(当然,我们也可以为摄像机添加Skybox组件来覆盖默认的天空盒子)。

(2)向场景中拖拽一个Teapot模型,并调整它的位置。

(3)新建一个材质(RefractionMat),把材质赋给第二步中的模型。

(4)新建一个Unity Shader(Chapter10-Refraction),并赋给第三步中创建的材质。

折射效果的实现略微复杂一些。

Shader "Unity Shaders Book/Chapter 10/Refraction" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)_RefractColor ("Refraction Color", Color) = (1, 1, 1, 1)_RefractAmount ("Refraction Amount", Range(0, 1)) = 1//我们需要使用该属性得到不同介质的透射比,以此来计算折射方向。_RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5_Cubemap ("Refraction 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;float _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 = mul(UNITY_MATRIX_MVP, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(_Object2World, v.vertex).xyz;o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);// 计算世界空间中的折射目录o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);TRANSFER_SHADOW(o);return o;}//我们使用了CG的refract函数来计算折射方向。它的第一个参数即为入射光线的方向,它必须是//归一化后的矢量;第二个参数是表面法线,法线方向同样需要是归一化后的;第三个参数是入射光线//所在介质的折射率和折射光线所在介质的折射率之间的比值,例如如果光是从空气射到//玻璃表面,那么这个参数应该是空气的折射率和玻璃的折射率之间的比值,即1/1.5,它的返回值就是//计算而得的折射方向,它的模则等于入射光线的模。//然后,我们在片元着色器中使用折射方向对立方体纹理进行采样: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));// 使用世界空间中的折射目录来访问立方体贴图fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);// 将漫射色与折射色混合fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;return fixed4(color, 1.0);}//同样,我们也没有对i.worldRefr进行归一化操作,因为对立方体纹理的彩样只提供方向即可。//最后,我们使用_RefractAmount来混合漫反射颜色和折射颜色,并和环境光照相加后返回。ENDCG}} FallBack "Reflective/VertexLit"}

菲涅尔反射

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

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

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

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

使用上面的菲涅耳近似等式,我们可以在边界处模拟反射光强和折射光强/漫反射光强之间的变化。在许多车漆、水面等材质的渲染中,我们会经常使用菲涅耳反射来模拟更加真实的反射效果。

接下来我们使用Schlick菲涅耳近似等式来模拟菲涅耳反射。效果图如下,

(1)创建场景(Scene_10_1_5),修改天空盒。

(2)拖拽一个Teapot模型。

(3)新建一个材质(FresnelMat),把材质赋给第二步中的模型。

(4)新建一个Unity Shader(Chapter10-Fresnel),并赋给第三步中创建的材质。

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_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 = mul(UNITY_MATRIX_MVP, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(_Object2World, 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;UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);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);}//我们使用Schlick菲涅耳近似等式来计算Fresnel变量,并使用它来混合漫反射光照和反射光照//。一些实现也会直接把fresnel和反射光照相乘后叠加到漫反射光照上,模拟边缘关照的效果。ENDCG}} FallBack "Reflective/VertexLit"}

当我们把_FresnelScale调节到1时,物体将完全反射Cubemap中的图像;当_FresnelScale为0时,则是一个具有边缘光照效果的漫反射物体。在15.2中会用菲涅耳反射和混合反射和折射光线,以此来模拟一个简单的水面效果。

10.2 渲染纹理

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

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

镜子效果

(1)新建场景(Scene_10_2_1)。去掉天空盒子。

(2)新建材质(MirrorMat)。

(3)新建一个Unity Shader(Chapter10-Mirror)。把新的Shader赋给第二步中创建的材质。

(4)在场景中创建六个立方体,构成围绕着摄像机的房间的六面墙。给他们赋予在9.5节2中创建的标准材质,并让他们的颜色互不相同。然后添加点光源。

(5)创建3个球体和两个正方体,调整他们的位置和大小,并给他们赋予在9.5中创建的标准材质。

(6)创建一个四边形(Quad),把它作为镜子。把第二步中创建的材质赋给它。

(7)在Project视图下创建一个渲染纹理(Create -Render Texture),(MirrorTexture)

(8)最后,为了得到从镜子出发观察到的场景图像,我们还需要创建一个摄像机,并调整它的位置、裁剪平面、视角等,使得它的显示图像是我们希望的镜子图像。由于这个摄像机不需要直接显示在屏幕上,而是用于渲染到纹理。因此,我们把第七步中创建的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 fragsampler2D _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 = mul(UNITY_MATRIX_MVP, v.vertex);o.uv = v.texcoord;// Mirror needs to filp xo.uv.x = 1 - o.uv.x;return o;}//我们翻转了x分量的纹理坐标,这是因为,镜子里显示的图像都是左右相反的。//在片元着色器中对渲染纹理进行采样和输出fixed4 frag(v2f i) : SV_Target {return tex2D(_MainTex, i.uv);}ENDCG}} FallBack Off}

将MirrorTexture渲染纹理拖拽到材质的Main Tex属性中,就可以得到下图的效果。

我们把渲染纹理的分辨率大小设置为256×256。有时,这样的分辨率会使图像模糊不清,此时我们可以使用更高的分辨率或更多的抗锯齿采样等。但需要注意的是,更高的分辨率会影响宽和性能,我们应当尽量使用较小的分辨率。

玻璃效果

我们通常会使用GrabPass来实现诸如玻璃等透明材质的模拟,以使用简单的透明混合不同,使用GrabPass可以让我们对该物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不再是简单的和原屏幕颜色进行混合。

需要注意的是,在使用GrabPass时,我们需要额外小心物体的渲染队列设置。正如之前所说,GrabPass通常同于渲染透明物体,尽管代码里并不包含混合指令,但我们往往当渲染该物体时,所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。

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

(1)新建场景(Scene_10_2_2)。

(2)新建材质(GlassRefractionMat)。

(3)新建Unity Shader(Chapter10-GlassRefraction),并赋给第二步中的材质。

(4)构建一个测试玻璃效果的场景。将第二步中的材质赋给需要呈现玻璃效果的物体。

(5)为了得到本场景使用的环境映射纹理,我们使用了10.1.2节中实现的创建立方体纹理的脚本(通过Gameobject——Render into Cubemap打开编辑窗口)来创建它,该Cubemap名为Glass_Cubemap。

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//值为1时,该玻璃只包括折射效果,影响玻璃的透明度。_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0}SubShader {// 我们必须是透明的,所以在这之前画出其他的物体Tags { "Queue"="Transparent" "RenderType"="Opaque" }//我们首先在SubShader的标签中将渲染队列设置成Transparent,尽管在后面的RederType被设置为了Opaque。//这两者看似矛盾,但实际上服务于不同的需求。我们之前说过,把Queue设置成Transparent可以确保该物体渲染时其他所有//不透明物体都已经被渲染到屏幕上,否则就可能无法正确得到"透过玻璃看到的图像"。而设置RenderType则是为了在使用着色器//替换(Shader Replacement)时,该物体可以在需要时被正确渲染。这通常发生在我们需要得到摄像机的深度和法线纹理时,(13)// 这个通道将物体后面的屏幕抓取到一个纹理中。// 我们可以在下一个通道中看到结果GrabPass { "_RefractionTex" }//在这个Pass中我们定义了一个字符串,该字符串内部的名称决定了抓取得到的屏幕图像将会被存入哪个纹理中。//该字符串也可以省略,但直接声明纹理名称的方法往往可以得到更高的性能。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;//可以得到该纹理的妏素大小,例如一个大小为256×512的纹理,它的妏素大小为(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 = mul(UNITY_MATRIX_MVP, v.vertex);//在进行了必要的顶点坐标变换后o.scrPos = ComputeGrabScreenPos(o.pos);//通过内置的ComputeGrabScreenPos函数来得到对应被抓取的屏幕图像的采样坐标。//我们可以在UnityCG.cginc文件中找到它的声明,它的主要代码和ComputeGrabScreenPos基本相似,最大的不同是针对//平台差异造成的采样坐标问题(5.6.1)进行了处理。o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);//接着我们计算了_MainTex和_BumpMap的采样坐标,并把他们分别存储在一个float4//类型变量的xy和zw分量中。float3 worldPos = mul(_Object2World, v.vertex).xyz; 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); //由于我们需要在片元着色器中把法线方向从切线空间(由法线纹理采样得到)//变换到世界坐标下,以便对Cubemap进行采样,因此,我们需要在这里计算该顶点对应的从切线空间到世界空间的变换//矩阵,并把该矩阵的每一行分别存储在TtoW0、TtoW1和TtoW2的xyz分量中。这里面使用的数学方法就是,得到切线//空间下的3个坐标轴(xyz轴分别对应了副切线、切线和法线的方向)在世界空间喜的表示,再把他们依次按列组成//一个变换矩阵即可。TtoW0等值的w轴同样被利用起来,用于存储世界空间下的顶点坐标。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));//我们首先通过TtoW0等变量的w分量得到世界坐标,并用该值得到片元对应的视角方向。// 在切空间中得到法线fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); //随后,我们对法线纹理进行采样,得到切线空间下的法线方向。// 计算切线的偏移量float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;//我们使用该值的_Distortion属性以及_RefractionTex_TexelSize来对屏幕图像的采样坐标进行偏移,模拟折射效果。//_Distortion值越大,偏移量越大,玻璃背后的物体看起来变形程度越大。在这里,我们选择使用切线空间下的//法线方向来进行偏移,是因为该空间下的法线可以反映顶点局部空间下的法线方向。随后,我们对//scrPos透视除法得到真正的屏幕坐标(4.9.3),再使用该坐标对抓取的屏幕图像//_RefractionTex进行采样,得到模拟的折射颜色。// 将正常值转换为世界空间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;//之后,我们把法线方向从切线空间变换到了世界空间下(使用变换矩阵的每一行,即TtoW0、TtoW1和TtoW2,分别和//法线方向点乘,构成新的法线采样),并据此得到视角方向相对于法线方向的反射方向。随后使用反射方向对Cubemap//进行采样,并把结果和主纹理颜色相乘后得到反射颜色。fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;return fixed4(finalColor, 1);//最后,我们使用_RefractAmount属性对反射和折射颜色进行混合,作为最终的输出颜色} ENDCG}}FallBack "Diffuse"}

完成后,我们把本书资源中的Glass_Diffuse.jpg和Glass_Normal.jpg文件赋给材质的Main Tex 和Normal Map属性,把之前创建的Class_Cubemap赋给Environment Cubemap 属性,再调整_RefractAmount属性即可得到类似下图中的玻璃效果。

在前面的现实中,我们在GrabPass中使用一个字符串指明了被抓取的屏幕图像将会存储在哪个名称的纹理中。实际上,GrabPass支持两种形式。

·直接使用GrabPass{},然后在后续的Pass中直接使用_GrabTexture来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色。

·使用GrabPass{“TextureName”},我们可以在后续的Pass中使用TextureName来访问屏幕图像。使用这种方法同样可以抓取屏幕,但Unity只会在每一帧时为第一个使用名为TextureName的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其他Pass中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都只会执行一次抓取工作,但这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。

渲染纹理 vs.GrabPass

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

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

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

10.3 程序纹理

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

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

这一节里,我们会使用一个算法来生成一个波点纹理,如下图所示,我们可以在脚本中调整一些参数,如背景颜色、波点颜色等,以控制最终生成的纹理外观。

(1)新建场景(Scene_10_3_1)。

(2)新建材质(ProceduralTextureMat)。

(3)我们使用第七章的一个Unity Shader ——Chapter7-SingleTexture,把它赋给第二步中创建的材质。

(4)新建一个立方体,把第二步中的材质赋给它。

(5)我们并没有为ProceduralTextureMat材质赋予任何纹理,这是因为,我们要用脚本来创建程序纹理。为此,我们再创建一个脚本ProceduralTextureGeneration.cs,并把它拖拽到第四步创建的立方体。

using UnityEngine;using System.Collections;using System.Collections.Generic;//为了让该脚本能够在编辑器模式下运行。[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.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// #region和#endregion仅仅是为了组织代码,并没有其他作用。由于我们生成的纹理是由若干圆点构成的,//因此在上面的代码中,我们声明了四个纹理属性:纹理的大小,数值通常是二的整数幂:纹理的背景//颜色;圆点的颜色;模糊因子,这个参数是用来模糊圆形边界的。注意到,对于每个属性我们使用了//get/set的方法,为了在面板上修改属性时仍可以执行set函数,我们使用了一个开源插件SetProperty(/LMNRY/SetProperty/blob/master/Scripts/SetPropertyExample.cs).//这使得当我们修改了材质属性时,可以执行_UpdateMaterial函数来使用新的属性重新生成程序纹理。//为了保护生成的程序纹理,我们声明了一个Texture2D类型的纹理变量。private Texture2D m_generatedTexture = null;//下面开始编写各个函数。首先,我们需要在Start函数中进行相应的检查,以得到需要使用该程序纹理的材质// Use this for initializationvoid Start () {if (material == null) {Renderer renderer = gameObject.GetComponent<Renderer>();if (renderer == null) {Debug.LogWarning("Cannot find a renderer.");return;}material = renderer.sharedMaterial;}_UpdateMaterial();}//在上面的代码里,我们首先检查了material变量是否为空,如果为空,就尝试从使用该脚本所在的物体上得到相应的材质。完成后,调用//_UpdateMaterial函数来为其生成程序纹理。private void _UpdateMaterial() {if (material != null) {m_generatedTexture = _GenerateProceduralTexture();material.SetTexture("_MainTex", m_generatedTexture);}}//它确保material不为空,然后调用_GenerateProceduralTexture函数来生成一张程序纹理,并赋给//m_generatedTexture变量。完成后,利用material.SetTexture函数把生成的纹理赋给材质。材质//material中需要有一个名为_MainTex的纹理属性。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;// 一个一个地画九个圆圈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;}//代码首先初始化一张二维纹理,并且提前计算了一些生成纹理时需要的变量。然后,使用了一个两层//的嵌套循环遍历纹理中的每个像素,并在纹理上一次绘制9个圆形。最后,调用Texture2D.Apply函数来强制把像素值写入//纹理中,并返回该程序纹理。}

Unity 的程序材质

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

Snbstance Designer是一个非常出色的纹理生成工具,很对3A的游戏项目都使用了由它生产的材质。我们可以从Unity的资源商店或网络中获取到很多免费后付费的Substance材质。这些材质都是以.sbsar为后缀的,(资源来源于 http://www./en/#!/content/1352)。

当把这些文件导入Unity后,Unity就会生成一个程序纹理资源(Procedural Material Asset)。程序纹理资源可以包含一个或多个程序材质,例如下图就包含了两个程序纹理——Cereals和Cereals_1,每个程序纹理使用了不同的纹理参数,因此Unity它们生成了不同的程序纹理。例如Cereals_Diffuse 和Cereals_1_Diffuse等。

通过单击程序材质,我们可以在程序纹理的面板上看到该材质使用的Unity Shader以及属性、生成程序纹理使用的纹理属性、材质预览等薪资。

程序材质的使用和普通材质是一样的。我们把他们拖拽到相应的模型上即可。读者 可以在本书资源的Scene_10_3_2中找到这样的示例场景。程序纹理的强大之处很大原因在于它的多变性,我们可以通过调整程序纹理的属性来控制纹理的外观,甚至可以生成看似完全不同的纹理。

可以看出,程序材质的自由度很高,而且可以和Shader配合得到非常出色的视觉效果,它是一种非常强大的材质类型。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。