在图形学中,立方体纹理 (Cubemap)
是环境映射 (Environment Mapping) 的一种实现方法。
和之前见到的纹理不同,立方体纹理一共包含了6张图像,这些图像对应了一个立方体的6个面,立方体纹理的名称也由此而来。立方体的每个面表示沿着世界空间下的轴向(上、下、左、右、前、后)观察所得的图像。
对立方体纹理采样我们需要提供一个三维的纹理坐标,这个三维纹理坐标表示了我们在世界空间下的一个3D方向。这个方向矢量从立方体的中心出发,当它向外部延伸时就会和立方体的6个纹理之一发生相交,而采样得到的结果就是由该交点计算而来的,如下图所示:
使用立方体纹理的好处在于,它的实现简单快速,而且得到的效果也比较好。
但它也有一些缺点,例如当场景中引入了新的物体、光源,或者物体发生移动时,我们就需要重新生成立方体纹理。除此之外,立方体纹理也仅可以反射环境,但不能反射使用了该立方体纹理的物体本身。这是因为,立方体纹理不能模拟多次反射的结果。所以我们应该尽量对凸面体而不要对凹面体使用立方体纹理,因为凹面体会反射自身。
立方体纹理在实时渲染中有很多应用,最常见的是用于天空盒子(Skybox)以及环境映射。
天空盒子 (Skybox)
是游戏中用于模拟背景的一种方法。天空盒子这个名字包含了两个信息:它是用来模拟天空的(尽管现在我们仍可以用它模拟室内等背景),它是一个盒子。当我们在场景中使用了天空盒子时,整个场景就被包围在一个立方体内。这个立方体的每个面使用的技术就是立方体纹理映射技术。
下面是在 Unity 中自定义天空盒的方法:
Window -> Rendering -> Lighting
窗口中将新材质赋给 skybox material 选项Clear Flags
设置为 Skybox,可以得到如下的天空盒效果需要说明的是,在 Window → Rendering → Lighting 中设置的天空盒子会应用于该场景中的所有摄像机。如果我们希望某些摄像机可以使用不同的天空盒子,可以通过向该摄像机添加Skybox 组件来覆盖掉之前的设置。
在 Unity 中,天空盒子是在所有不透明物体之后渲染的,而其背后使用的网格是一个立方体或一个细分后的球体。
在 Unity 中,创建用于环境映射的立方体纹理的方法有三种:第一种方法是直接由一些特殊布局的纹理创建;第二种方法是手动创建一个Cubemap资源,再把6张图赋给它;第三种方法是由脚本生成。
如果使用第一种方法,我们需要提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等。然后,我们只需要把该纹理的Texture Shape 设置为 Cube 即可,然后在 Mapping 选项中设置纹理的布局属性,Unity会为我们做好剩下的事情。
第二种方法是旧的创建方法,我们可以通过Assets > Create > Legacy > Cubemap
创建一个Cubemap,然后把6张纹理拖曳到它的面板中。现在官方推荐使用第一种方法创建立方体纹理,这是因为第一种方法可以对纹理数据进行压缩,而且可以支持边缘修正、光滑反射(glossy reflection)和 HDR 等功能。
前面两种方法都需要我们提前准备好立方体纹理的图像,它们得到的立方体纹理往往是被场景中的物体所共用的。但在理想情况下,我们希望根据物体在场景中位置的不同,生成它们各自不同的立方体纹理。这时,我们就可以在Unity中使用脚本来创建。这是通过利用Unity提供的Camera.RenderToCubemap
函数来实现的。Camera.RenderToCubemap
函数可以把从任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理,代码如下:
using UnityEngine;
using UnityEditor;
public class RenderCubeMap: ScriptableWizard {
public Transform renderFromPosition;
public Cubemap cubemap;
[MenuItem("Cubemap/RenderCubeMap")]
private static void MenuEntryCall() {
DisplayWizard<RenderCubeMap>("Render Cubemap", "Render");
}
private void OnWizardCreate() {
var go = new GameObject("CubemapCamera");
go.AddComponent<Camera>();
go.transform.position = renderFromPosition.position;
go.GetComponent<Camera>().RenderToCubemap(cubemap);
DestroyImmediate(go);
}
}
将上述代码放入项目后,我们依据下面的步骤来创建 Cubemap:
Assets > Create > Legacy > Cubemap
创建一个 Cubemap, 并勾选其Readable
选项Cubemap > RenderCubeMap
打开窗口需要注意的是,我们需要为 Cubemap 设置 Face size,Face size值越大,渲染出来的立方体纹理分辨率越大,效果可能更好,但需要占用的内存也越大,这可以由面板最下方显示的内存大小得到。
准备好了需要的立方体纹理后,我们就可以对物体使用环境映射技术。而环境映射最常见的应用就是反射和折射。
想要模拟反射效果很简单,我们只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。代码如下:
Shader "Chapter 10/ReflectionShader"
{
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" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _ReflectColor;
float _ReflectAmount;
// 声明 Cubemap 使用 samplerCUBE
samplerCUBE _Cubemap;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldReflection : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 使用内置函数 reflect 计算反射方向
o.worldReflection = 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));
// 对立方体纹理的采样需要使用CG的texCUBE 函数
// 我们在采样时并没有对i.worldRefl进行归一化操作。这是因为,用于采样的参数仅仅是作为方向变量传递给texCUBE函数的,因此我们没有必要进行归一化
fixed3 reflection = texCUBE(_Cubemap, i.worldReflection).rgb * _ReflectColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 使用_ReflectAmount来混合漫反射颜色和反射颜色,并和环境光照相加后返回, lerp 为线性插值函数
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
在上面的计算中,我们选择在顶点着色器中计算反射方向。当然,我们也可以选择在片元着色器中计算,这样得到的效果更加细腻。但是,对于绝大多数人来说这种差别往往是可以忽略不计的,因此出于性能方面的考虑,我们选择在顶点着色器中计算反射方向。最后效果如下:
折射的物理原理比反射复杂一些。我们在初中物理就已经接触过折射的定义:当光线从一种介质(例如空气)斜射入另一种介质(例如玻璃)时,传播方向一般会发生改变。当给定入射角时,我们可以使用斯涅尔定律 (Snell's Law)
来计算反射角。当光从介质1沿着和表面法线夹角为 θ1 的方向斜射入介质2时,我们可以使用如下公式计算折射光线与法线的夹角 θ2 :
其中,η1 和 η2 分别是两个介质的折射率 (index of refraction) 。
通常来说,当得到折射方向后我们就会直接使用它来对立方体纹理进行采样,但这是不符合物理规律的。对一个透明物体来说,一种更准确的模拟方法需要计算两次折射—— 一次是当光线进入它的内部时,而另一次则是从它内部射出时。但是,想要在实时渲染中模拟出第二次折射方向是比较复杂的,而且仅仅模拟一次得到的效果从视觉上看起来“也挺像那么回事的”。正如我们之前提到的——图形学第一准则“如果它看起来是对的,那么它就是对的”。因此,在实时渲染中我们通常仅模拟第一次折射。代码如下:
Shader "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" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _RefractColor;
float _RefractAmount;
float _RefractRatio;
samplerCUBE _Cubemap;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRafraction : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 使用内置函数 refract 计算折射角度
o.worldRafraction = 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));
fixed3 refraction = texCUBE(_Cubemap, i.worldRafraction).rgb * _RefractColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 使用_ReflectAmount来混合漫反射颜色和反射颜色,并和环境光照相加后返回, lerp 为线性插值函数
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
我们使用了CG的 refract
函数来计算折射方向。它的第一个参数即为入射光线的方向,它必须是归一化后的矢量;第二个参数是表面法线,法线方向同样需要是归一化后的;第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值,例如如果光是从空气射到玻璃表面,那么这个参数应该是空气的折射率和玻璃的折射率之间的比值,即1/1.5。它的返回值就是计算而得的折射方向,它的模则等于入射光线的模
效果如下:
在实时渲染中,我们经常会使用菲涅耳反射 (Fresnel reflection) 来根据视角方向控制反射程度。
通俗地讲,菲涅耳反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅耳等式进行计算。一个经常使用的例子是,当你站在湖边,直接低头看脚边的水面时,你会发现水几乎是透明的,;但是,当你抬头看远处的水面时,会发现几乎看不到水下的情景。这就是所谓的菲涅耳效果。
真实世界的菲涅耳等式是非常复杂的,但在实时渲染中,我们通常会使用一些近似公式来计算。其中一个著名的近似公式就是Schlick 菲涅耳近似等式
:
其中,F0 是一个反射系数,用于控制菲涅耳反射的强度,v 是视角方向,n 是表面法线。
另一个应用比较广泛的等式是 Empricial 菲涅耳近似等式
:
![在这里插入图片描述](https://img-blog.csdnimg.cn/6aa7258e3e5b454985d3403dfebcfeb1.png
其中,bias 、scale 和 power 是控制项。
下面是 Schlick 菲涅耳近似等式
的 Shader 实现:
Shader "Chapter 10/Fresnel"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
float _FresnelScale;
samplerCUBE _Cubemap;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldReflection : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldReflection = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldViewDir = normalize(i.worldViewDir);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed3 reflection = texCUBE(_Cubemap, i.worldReflection).rgb;
// 使用Schlick菲涅耳近似等式来计算fresnel变量,并使用它来混合漫反射光照和反射光照
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);
}
ENDCG
}
}
}
可以调整Fresnel Scale
参数来查看不同的效果, _FresnelScale调节到1时,物体将完全反射Cubemap中的图像;当_FresnelScale为0时,则是一个具有边缘光照效果的漫反射物体:
现代的GPU允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理 (Render Target Texture,RTT)
,而不是传统的帧缓冲或后备缓冲(back buffer)
。
与之相关的是多重渲染目标 (Multiple Render Target,MRT)
,这种技术指的是 GPU 允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。
Unity为渲染目标纹理定义了一种专门的纹理类型——渲染纹理 (Render Texture)
。在Unity中使用渲染纹理通常有两种方式:
GrabPass
命令或 OnRenderImage
函数来获取当前屏幕图像,Unity 会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,下面我们可以在自定义的 Pass 中把它们当成普通的纹理来处理,从而实现各种屏幕特效本节我们将学习如何使用渲染纹理来模拟镜子效果,镜子实现的原理很简单,它使用一个渲染纹理作为输入属性,并把该渲染纹理在水平方向上翻转后直接显示到物体上即可:
Shader "Chapter 10/Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 镜子里显示的图像都是左右相反的,所以需要对x轴进行翻转
o.uv.x = 1-o.uv.x;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
在 Unity 中,我们还可以在 Unity Shader 中使用一种特殊的 Pass 来完成获取屏幕图像的目的,这就是GrabPass
。当我们在Shader中定义了一个GrabPass
后,Unity 会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的 Pass 中访问它。接下来我们将使用GrabPass
来实现简单的玻璃效果。
Shader 代码如下:
Shader "Chapter 10/GlassRefraction"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
// 方块纹理,用于玻璃反射效果
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
// 法线纹理,用于玻璃折射效果
_BumpMap ("Normal Map", 2D) = "bump" {}
// 控制模拟折射时图像的扭曲程度
_Distortion ("Distortion", Range(0, 100)) = 10
// 控制折射比率
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
}
SubShader
{
// 需要把物体的渲染队列设置成透明队列
// 这样才可以保证当渲染该物体时,所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
LOD 100
// 获取当前屏幕渲染纹理
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;
float4 _RefractionTex_TexelSize;
struct appdata
{
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 (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 通过调用内置的 ComputeGrabScreenPos 函数来得到对应被抓取的屏幕图像的采样坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
// 计算 _MainTex 和_ BumpMap 的采样坐标,并把它们分别存储在一个float4类型变量的xy和zw分量中
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
// 由于我们需要在片元着色器中把法线方向从切线空间变换到世界空间下,以便对 Cubemap 进行采样,
// 因此,我们需要在这里计算该顶点对应的从切线空间到世界空间的变换矩阵,
// 并把该矩阵的每一行分别存储在TtoW0、TtoW1和TtoW2的xyz分量中
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;
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;
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
// 对屏幕纹理采样获得折射颜色
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);
// 对 Cubemap 采样获得反射颜色
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
在前面的实现中,我们在 GrabPass 中使用一个字符串指明了被抓取的屏幕图像将会存储在哪个名称的纹理中。实际上,GrabPass 支持两种形式:
GrabPass { }
,然后在后续的Pass中直接使用 _GrabTexture
来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色GrabPass { "TextureName" }
,。使用这种方法同样可以抓取屏幕,但 Unity 只会在每一帧时为第一个使用名为 TextureName 的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其他Pass中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都只会执行一次抓取工作,但这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。GrabPass的好处在于实现简单,我们只需要在Shader中写几行代码就可以实现抓取屏幕的目的。
但从效率上来讲,使用渲染纹理的效率往往要好于GrabPass,尤其在移动设备上。使用渲染纹理我们可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再次渲染一遍,但我们可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其他方法来控制摄像机是否需要开启。而使用GrabPass获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的带宽影响。而且在移动设备上,GrabPass虽然不会重新渲染场景,但它往往需要CPU直接读取后备缓冲(back buffer)中的数据,破坏了CPU和GPU之间的并行性,这是比较耗时的,甚至在一些移动设备上这是不支持的。
Unity引入了命令缓冲 (Command Buffers)
来允许我们扩展Unity的渲染流水线。使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理中,然后在那里进行一些额外的操作,例如模糊等,最后把图像传递给需要使用它的物体进行处理和显示。除此之外,命令缓冲还允许我们实现很多特殊的效果,读者可以在Unity官方手册找到更多内容。
程序纹理 (Procedural Texture)
指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素,例如木头、石子等。使用程序纹理的好处在于我们可以使用各种参数来控制纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这使得我们可以得到更加丰富的动画和视觉效果。
下面我们将使用一个算法来生成一个波点纹理。
首先导入开源插件https://github.com/LMNRY/SetProperty
,用于在编辑模式更方便的改变属性。
然后我们新建一个脚本 ProceduralTextureGeneration 用于生成程序纹理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 在编辑器模式下运行
[ExecuteInEditMode]
public class ProceduralTextureGeneration : MonoBehaviour
{
public Material material = null;
private Texture2D m_generatedTexture = null;
// 纹理的大小
[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();
}
}
// 混合颜色
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;
}
void _UpdateMaterial()
{
Debug.Log("Update Material");
if (material != null) {
m_generatedTexture = _GenerateProceduralTexture();
material.SetTexture("_MainTex", m_generatedTexture);
}
}
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;
}
void Start()
{
if (material == null) {
Renderer renderer = gameObject.GetComponent<Renderer>();
if (renderer == null) {
Debug.LogWarning("Cannot find a renderer.");
return;
}
material = renderer.material;
}
_UpdateMaterial();
}
}
新建一个 cube, 挂载此脚本,便可在编辑模式中调整 cube 的纹理了,效果如下:
在Unity中,有一类专门使用程序纹理的材质,叫做程序材质 (Procedural Materials)
。程序材质和它使用的程序纹理并不是在 Unity 中创建的,而是使用了一个名为Substance Designer
的软件在Unity外部生成的。这些材质都是以.sbsar
为后缀的,我们可以直接把这些材质像其他资源一样拖入Unity项目中。当把这些文件导入Unity后,Unity就会生成一个程序纹理资源 (Procedural Material Asset)
。
程序纹理的强大之处很大原因在于它的多变性,我们可以通过调整程序纹理的属性来控制纹理的外观,甚至可以生成看似完全不同的纹理。