(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)

立方体纹理

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

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

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第1张图片

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

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

一、天空盒子

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

使用的话只需要创建一个Skybox材质,这里选择unity自带的Skybox/6 Sided shader,然后选择对应的6张纹理,注意位置要准确对应,为了让天空盒子正常渲染,需要把这6张纹理的Wrap Mode设置为Clamp,以防止接缝处出现不匹配的现象

使用天空盒:

新建场景,在Window->Lighting菜单中,把SkyboxMat赋给Skybox选项,为了让摄像机正常显示天空盒,还需要保证渲染场景的摄像机Camera组件中的Clear Flags 被设置为Skybox

 (十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第2张图片

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

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

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

除了天空盒子,立方体纹理最常见的用处是用于环境映射,通过这种方法可以模拟出金属质感的材质。在unity5中,创建用于环境映射的立方体纹理的方法有三种:第一种方法是直接由一些特殊布局的纹理创建,第二种方法是手动创建一个cubemap资源,再把6张图赋给他,第三种是由脚本生成

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

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

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

使用Camera.RenderToCubemap函数来创建立方体纹理的关键代码如下:

public class RenderCubemapWizard : ScriptableWizard {
	
	public Transform renderFromPosition;
	public Cubemap cubemap;
	
	void OnWizardUpdate () {
		helpString = "Select transform to render from and cubemap to render into";
		isValid = (renderFromPosition != null) && (cubemap != null);
	}
	
	void OnWizardCreate () {
		// create temporary camera for rendering
		GameObject go = new GameObject( "CubemapCamera");
		go.AddComponent();
		// place it on the object
		go.transform.position = renderFromPosition.position;
		// render into cubemap		
		go.GetComponent().RenderToCubemap(cubemap);
		
		// destroy temporary camera
		DestroyImmediate( go );
	}
	
	[MenuItem("GameObject/Render into Cubemap")]
	static void RenderCubemap () {
		ScriptableWizard.DisplayWizard(
			"Render cubemap", "Render!");
	}
}

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

如何使用代码工具:

1.我们使用刚才第一节的场景,创建一个空的GameObject对象,会使用该GameObject的位置信息来渲染立方体纹理。

2.新建一个用于存储的立方体纹理,右键Create->Legacy->cubemap,为了让脚本可以顺利将图像渲染到该立方体纹理中,需要在它的面板勾选Readable选项(注意:在unity2018版本勾选或取消勾选此选项都会让unity崩溃退出,原因不明)

.3.在菜单栏点击GameObject->Render into Cubemap,打开我们在脚本中实现的用于渲染立方体纹理的窗口,并把第一步中创建的gameobject和新建的cubemap分别拖到窗口中的属性里。如下图:

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第3张图片

 单击窗口中的Render!按钮,就可以把从该位置观察到的世界空间下单6张图像渲染到创建的Cubemap中,如下图所示:

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第4张图片

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

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

三、反射

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

准备工作:新建场景,天空盒换成上一节使用的,拖进来一个模型,位置和上一节gameobject位置一致,创建一个材质赋给模型,下面开始写shader:

1.首先声明3个新属性:

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" {}
	}

其中_ReflectColor用于控制反射颜色,_ReflectAmount用于控制这个材质的反射程度,而_Cubemap用于模拟反射的环境映射纹理。

2.在顶点着色器中计算了该顶点处的反射方向,通过CG的reflect函数来实现:

v2f vert(a2v v) {
				v2f o;
				
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
				
				// Compute the reflect dir in world space
				o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
				
				TRANSFER_SHADOW(o);
				
				return o;
			}

物体反射到摄像机中的光线方向,可以由光路可逆原则反向求得。也就是可以计算视角方向关于顶点法线的反射方向对立方体纹理采样。

3.在片元着色器利用反射方向对立方体纹理采样:

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));
				
				// Use the reflect dir in world space to access the cubemap
				fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				// Mix the diffuse color with the reflected color
				fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
				
				return fixed4(color, 1.0);
			}

对立方体纹理采样需要使用CG的texCuBE函数。注意到在上面的计算中,在采样时并没有对i.worldRefl进行归一化操作,因为用于采样的参数仅仅是作为方向变量传递给texCUBE函数的,没有必要归一化操作。然后使用_ReflectAmount来混合漫反射颜色和反射颜色,并和环境光照相加后返回。

在上面的计算中,选择在顶点着色器计算反射方向,当然也可以选择在片元着色器中计算,这样得到的效果更加细腻。效果差别可以忽略不计,因此出于性能方面的考虑选择在顶点着色器计算反射方向。

4.把上一节的cubemap拖拽到Reflection Cubemap属性中,并调整其他参数,效果如下图:

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第5张图片

四、折射

下面讲述如何在unity shader中模拟另外一个环境映射常见应用———折射。折射在物理中的定义:当光线从一种介质(例如空气)斜射入另一种介质(例如玻璃)时,传播方向一般会发生改变。当给定入射角时,可以用斯涅尔定律来计算反射角,当光从介质1沿着和表面法线夹角为θ1的方向斜射入介质2时,可以使用如下公式计算折射光线与法线的夹角θ2:

   n1sinθ1 = n2sinθ2

其中,n1和n2分别是两个介质的折射率。折射率是一项重要的物体常数,例如真空的折射率是1,而玻璃的折射率一般是1.5。下图给出了这些变量之间的关系。

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第6张图片

一般当得到折射方向后就会直接使用它来对立方体纹理进行采样,但这是不符合物体规律的。对一个透明物体来说,一种更准确的模拟方法需要计算两次折射,一次是当光线进入它的内部时,而另一次则是从它内部射出时。但是想要在实时渲染中模拟出第二次折射方向是比较复杂的,而且仅仅模拟一次得到的视觉效果还不错,因此在实时渲染中 通常仅仅模拟第一次折射

实现:

1.首先声明了4个新属性:

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" {}
	}

其中_RefractColor、_RefractAmount、_Cubemap与上节控制反射时使用的属性类似。还用到了一个属性_RefractRatio,我们需要使用该属性得到不同介质的透射比,以此来计算折射方向。

2.在顶点着色器中计算折射方向:

v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
				
				// Compute the refract dir in world space
				o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
				
				TRANSFER_SHADOW(o);
				
				return o;
			}

我们使用了CG的refract函数来计算折射方向,它的第一个参数即为入射光线的方向,它必须是归一化后的矢量;第二个参数是表面法线,法线方向同样是需要归一化后的;第三个参数是入射光线所在的介质的折射率和折射光线所在介质的折射率之间的比值,例如如果光是从空气射到玻璃表面,那么这个参数应该是空气的折射率和玻璃的折射率之间的比值,即1/1.5。它的返回值就是计算而得的折射方向,它的模则等于入射光线的模。

3.然后在片元着色器中使用折射方向对立方体纹理进行采样:

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));
				
				// Use the refract dir in world space to access the cubemap
				fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				// Mix the diffuse color with the refract color
				fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
				
				return fixed4(color, 1.0);
			}

同样,我们也没有对i.worldRefr进行归一化操作,因为对立方体纹理的采样只需要提供方向即可。最后使用_RefractAmount来混合漫反射颜色和折射颜色,并和环境光照相加后返回。

效果图:

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第7张图片

五、菲涅耳反射

在实时渲染中,我们经常会使用菲涅耳反射来根据视角方向控制反射程度。菲涅耳反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,另外一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅耳等式进行计算。一个经典励例子:当在湖边直接低头看脚边的水面时,会发现水几乎是透明的,可以直接看到水底的小鱼和石子,但当抬头看远处的水面时,会发现几乎看不到水下的情景,而只能看到水面反射的环境,这就是所谓的菲涅耳效果。玻璃这种的反光物体更具有菲涅耳效果,几乎任何物体都或多或少包含了菲涅耳效果

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

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

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

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

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

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

 (十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第8张图片

(十四)unity shader之——————高级纹理之立方体纹理cubemap(天空盒skybox、反射、折射、菲涅耳反射)_第9张图片

实现:

1.首先定义相关属性:

Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
		_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
	}

2.在顶点着色器计算世界空间下的法线方向、视角方式和反射方向:

v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, 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;
			}

3.在片元着色器中计算菲涅耳反射,并使用结果值混合漫反射光照和反射光照

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和反射光照相乘后叠加到漫反射光照上,模拟边缘光照的效果

4.然后可以调节_FresnelScale,调节到1时,物体将完全反射Cubemap中的图像,当_FresnelScale为0时,则是一个具有边缘光照效果的漫反射物体。

你可能感兴趣的:(unity,Shader,游戏开发,cube立方体纹理)