笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。
【Unity Shader】(三) ------ 漫反射和高光反射的实现 【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现 【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理 【Unity Shader】(六) ------ 复杂的光照(上) 【Unity Shader】(七) ------ 复杂的光照(下) 目录
前言
关于纹理,之前在【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现 已经解释过相关原理,不过那些是属于低维纹理,在高级纹理中,也有许多纹理是我们常见到的或常用的,同时它们能够实现十分精美的效果。受限于篇幅,本文主要介绍立方体纹理及其相关应用,下一篇中将继续介绍其它高级纹理。
一. CubeMap
1.1 立方体纹理
单单看标题,读者可能会不太明白我要说什么,不过说到天空盒,读者应该就懂了。我们来看一下官方对 CubeMap 的定义:
可以简单理解为:CubeMap 是六个假想面的集合,这六个面对应着一个正方体的 6 个面,每个面表示沿着世界空间下的轴向观察所得的图像。整体代表着环境的反射。 CubeMap 正常用于捕捉环境反射,而读者熟悉的天空盒子和环境映射也是常常使用这种纹理。
1.2 对立方体纹理的采样
我们先说如何采样,在详细说如何制作 CubeMap ,对立方体纹理采样需要提供一个三维的纹理坐标。这个坐标会表示一个方向(世界空间下),这个方向矢量从中心出发,向外延伸,然后和 6 个面相交,然后就可以通过交点来采样得到结果。
1.3 使用立方体纹理的优劣
纹理不止一种,立方体纹理常用也是有其理由的,我们可以看到其优劣
- 实现起来简单快速(稍后会解释),效果好
- 不实时,加入新光源或物体时需重新生成
- 不能模拟多次反射
当然,在现实中,我们在项目中使用普通的立方体纹理作为天空盒子等操作效果已经足够好了。
1.4 立方体纹理的布局
虽然名字中带有立方体,但纹理布局并不全然是一个立方体的展开。事实上,Unity 支持着数种布局的立方体纹理,而且大多数情况下,Unity 会自动检测它们。下面列举几种常见的布局
常见的:
圆柱形布局(全景图常用)
球形布局
默认情况下,Unity 会查看纹理的宽高比以确定最合适的布局
1.5 如何制作立方体纹理
制作立方体纹理有三种方法,下面我们逐一介绍
1.5.1 CubeMap 特殊布局纹理制作
制作立方体纹理,最简单的方法就是在特殊布局的纹理的 Inspector 面板中设置为 Cube,如图
这张纹理就变成了立方体纹理了,然后把这张纹理赋给一个材质便可,此时该材质会自动使用 Skybox / CubeMap 的 shader 。当然,要谨记的是这适用于特殊布局的纹理,常用此方法使用HDR贴图制作天空盒子,效果十分好。
官方也是推荐使用这种方法,因为这种方法可以
- 压缩纹理数据
- 修正边缘,光泽反射卷积(光滑反射)
- 支持HDR
我们来欣赏一下HDR制作的天空盒子
1.5.2 使用 6 张纹理制作
使用 6 张独立不同的纹理手动创建立方体纹理也是常见的一种方法。创建一个材质,shader 设置为 Skybox / 6 Sided 。
要注意的是:
- 每张纹理都是独立的,且要注意其对应的位置
- Wrap Mode 设置为 Clamp ,防止在边界处出现不匹配的现象
- Exposure 代表天空盒子的亮度
就可以实现以下的效果:
谨记:每张纹理都必须正确对应其对应的位置
1.5.3 脚本生成纹理
第三种方法比较特殊,前面两张方法都是使用定义好的贴图,制作出来的立方体纹理也是全局共享的。而第三种方法不使用准备好的图像,而是依赖于脚本,由物体在不同的位置生成。核心方法为 Camera.RenderToCubeMap ,这个方法可以从任意位置观察到的图像存储到 6 张图像中,从而创建当前位置的立方体纹理。我们可以在 Unity 官方脚本手册中找到其解释及用法。
当然需要注意的是:
- Camera.RenderToCubeMap (Cubemap cubemap, int faceMask)是 “静态” 的方法。当场景变化时,立方体纹理不会变化,从效果上看,类似 “烘焙”。
- RenderToCubemap (RenderTexture cubemap, int faceMask) 是 “动态” 的方法,能够实时渲染,但同时也需要注意资源的消耗。
对于这种方法实现的立方体纹理,我不打算在这里赘述了,因为本文重点在后文,感兴趣的读者可以自行实现一下。
.
二. 光线反射
2.1 何为光线反射
反射是光现象中最为常见的一种,且遵循光的反射定律,即光射到一个界面时,其入射光线与反射光线成相同角度。光入射到不同介质的界面上会发生折射,如图
反射时会出现以下情况:
- 反射线跟入射线和法线在同一平面
- 反射线和入射线分居法线两侧,并且与界面法线的夹角(分别叫做入射角和反射角)相等
- 反射角等于入射角
前文介绍了如何制作立方体纹理,现在我们需要用上它来实现一些效果。使用了反射效果的物体看起来就像在表面镀了一层金属膜一样。要模拟反射效果也是比较简单的,理论上只要使用入射光线的方向和表面法线方向计算出反射方向,再用反射方向对立方体纹理采样就行。现在我们来实现一下,此处,我们只注重反射的实现原理,整个场景中只有平行光,没有其它复杂的光源。
2.2 反射的实现
I. 创建一个场景,天空盒使用在 1.5.1 或 1.5.2 中制作的立方体纹理;创建一个 Cube 和一个 Material,一个 shader,命名为 Reflection 。编辑 shader
II. 先定义 Properties 块
其中 _ReflectAmount 控制整体反射程度,_Cubemap 表示要输入的立方体纹理,用来存储反射结果。
III. 包含相关的头文件和声明与 Properties 块 相匹配的属性
其中,要注意的是,立方体纹理的类型为 samplerCUBE
IV. 定义输入输出结构体
在输出结构体中多定义了一个反射方向,所以 SHADOW_COORDS 中的插值寄存器变为 4
V. 定义顶点着色器
顶点着色器里面的操作我们之前已经说过很多次了,这里主要是多了一个计算反射方向的步骤,我们使用 reflect 函数,有关 reflect 函数我在 【Unity Shader】(三) ------ 漫反射和高光反射的实现 中已经介绍过了,读者可以翻看一下
VI. 定义片元着色器
场景中只有平行光,光照计算比较简单,这里不再赘述。而对立方体纹理采样,我们则是用了 texCUBE 函数,我们可以在MSDN上找到它的定义
在使用该函数时,我们也没有对 i.worldReflect 进行归一化,是因为这里的参数是一个纹理坐标
将所有计算结果混合得到最终颜色返回,再加上一个 FallBack "Reflective/VertexLit" 完成。
VII. 保存,回到 Unity 查看效果,以下是不同 _ReflectAmount 的反射效果
很抱歉的是 git 图录制的清晰度不够好,以下两图是 _ReflectAmount 为 1 时的效果
完整代码:
Shader "Unity/01-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 { 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; float _ReflectAmount; samplerCUBE _Cubemap; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldnormal : TEXCOORD0; float3 worldpos : TEXCOORD1; float3 worldViewDir : TEXCOORD2; float3 worldReflect : TEXCOORD3; SHADOW_COORDS(4) }; v2f vert(a2v v) { v2f o; o.pos = UnityWorldToClipPos(v.vertex); o.worldnormal = UnityObjectToWorldNormal(v.normal); o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); o.worldReflect = 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)); fixed3 reflection = texCUBE(_Cubemap,i.worldReflect).rgb * _ReflectColor.rgb; UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); fixed3 color = ambient + lerp(diffuse ,reflection,_ReflectAmount) * atten; return fixed4(color,1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
至此,我们介绍完了反射的效果,接下来,我们来讨论光现象中的另一种常见的折射效果
三. 光线折射
3.1 何为光线折射
光从一种介质进入另一种具有不同折射率的介质,或者在同一种介质中折射率不同的部分运行时,由于波速的差异,使光的运行方向改变的现象就称为光的折射。而光在发生折射时入射角与折射角符合斯涅耳定律:
n1,n2 为折射率,常见的折射率有
- 真空:1
- 水:1.3330
- 玻璃:一般约为 1.5
如果读者遇到其它物质折射的情况,可自行查阅该物质的折射率。
“当得到了折射方向之后,我们就可以使用它来对立方体纹理采样”,相信读者头脑中可能会浮现出这个想法,但这个想法事实上是不够严谨的。对于透明的物体,应该模拟两次折射才会更为准确,光线射入物体内部,光线从内部射出。然而要在实时渲染中模拟出第二种折射是很复杂且耗费资源的,并且模拟第一次折射得到效果在大多数情况下也是良好的,所以,通常来说,我们的确会执行这种不太严谨的想法,即只模拟第一次折射。
3.2 实践
其实折射的 shader 代码和 反射的代码相差不大,所以我们进行和反射一样的操作,然后进行几处修改。下面列出这些值得注意的修改之处。
I. 在 Properties 块中添加一个属性 _RefractRatio,代表不同介质的透射比。比如光从空气射到水体,透射比约为 1 / 1.3;同理光从空气射到玻璃,透射比约为 1 / 1.5;
II. 定义与 Properties 块匹配的属性
III. 在顶点着色器中计算折射方向
这里需要注意的是,我们使用的是 CG 函数 refract,找到它的定义如下
这里需要注意的是,根据定义我们可以得知,参数是 入射光线的方向向量,表面法线的方向向量,透射比,与 reflect 函数不同,这里明确指出需要方向向量,所以我们需要对这两个向量归一化
IV. 其它的代码基本只需要替换相应的变量名字就可以了,这里直接给出完整代码
Shader "Unity/02-Refraction" { Properties { _Color ("Color Tint", Color) = (1,1,1,1) _RefractColor("Refraction Color",Color) = (1,1,1,1) _RefractAmount("Refract Amount",Range(0,1)) = 1 _RefractRatio("Refract Ratio",Range(0,1)) = 1 _Cubemap("Refraction Cubemap",Cube) = "_Skybox"{} } SubShader { 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; float _RefractRatio; samplerCUBE _Cubemap; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldnormal : TEXCOORD0; float3 worldpos : TEXCOORD1; float3 worldViewDir : TEXCOORD2; float3 worldRefract : TEXCOORD3; SHADOW_COORDS(4) }; v2f vert(a2v v) { v2f o; o.pos = UnityWorldToClipPos(v.vertex); o.worldnormal = UnityObjectToWorldNormal(v.normal); o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); o.worldRefract = 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,normalize(i.worldRefract)).rgb * _RefractColor.rgb; UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); fixed3 color = ambient + lerp(diffuse ,Refraction,_RefractAmount) * atten; return fixed4(color,1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
V.保存,回到 Unity 查看效果
不同透射比的折射效果:
由于 gif 图清晰度有限,所以这里做反射和折射的对比,更能看出效果
反射:
折射:
通过对比,我们可以看到图二中的光线似乎是被 “扭曲了” ,这正是光线的角度改变了,也正是我们要实现的折射效果。
四. 菲涅耳反射
4.1 什么是菲涅耳反射
相信读者并不会对这个名字感到陌生,没错,在我们生活中,最最最为常见的菲涅耳反射现象无疑是水边。当你站在湖边或河边时,你能清晰地看到脚边的水边一切景象,而当你抬头看向远处水面时,却只能看到一片白光。
当光射到物体表面时,一部分发生反射,一部分进入物体内部,然后发生折射或反射。反射光和入射光存在一定的比例关系,而这个关系可以由菲涅耳等式计算。
而菲涅耳等式有两条应用广泛的近似等式:
① Schlick 菲涅耳近似等式 :
v 是视角方向,n 是表面法线,F0 是反射系数
② Empricial 菲涅耳近似等式 :
bias,scale,power 是控制项
使用菲涅耳近似等式,我们可以模拟边界处的反射、折射/漫反射光强间的变化。特别是油漆,水面这种材质。本文只是介绍其原理及简单实现,在以后的学习中,我们将会利用它来制作一条有趣的河流或水面。
4.2 简单菲涅耳反射的实现
此处我们使用 Schlick 菲涅耳近似等式 来实现一个菲涅耳反射现象。与实现折射时相同,大部分的计算都是一样的,所以这里同样只提出值得注意的地方。
I. 定义 Properties 块
II. 定义相匹配变量
III. 计算反射方向
IV. 在片元着色器中计算菲涅耳反射,然后混合得到最终颜色
V. 完整代码:
Shader "Unity/03-Fresne" { Properties { _Color ("Color Tint", Color) = (1,1,1,1) _FresnelScale ("Fresnel Scale",Range(0,1)) = 0.5 _Cubemap("Refraction Cubemap",Cube) = "_Skybox"{} } SubShader { Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; float _FresnelScale; samplerCUBE _Cubemap; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldnormal : TEXCOORD0; float3 worldpos : TEXCOORD1; float3 worldViewDir : TEXCOORD2; float3 worldReflect : TEXCOORD3; SHADOW_COORDS(4) }; v2f vert(a2v v) { v2f o; o.pos = UnityWorldToClipPos(v.vertex); o.worldnormal = UnityObjectToWorldNormal(v.normal); o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); o.worldReflect = 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)); fixed3 reflection = texCUBE(_Cubemap,i.worldReflect).rgb; fixed3 fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir,worldnormal),5); UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); fixed3 color = ambient + lerp(diffuse ,reflection,saturate(fresnel)) * atten; return fixed4(color,1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
VI.查看效果
当 _FresnelScale 为 0 时,这时该物体就是一个具有边缘光照效果的漫反射物体
如果觉得图片效果不太明显,读者可以自行实现,感受一下。
五. 总结
对于立方体纹理,读者可能不一定会熟悉,但你一定熟悉天空盒,而立方体纹理正是天空盒和环境映射的实现的常用方法。同时,在场景中存在天空盒的情况下,物体对环境的采光正是本文所探讨的问题。无论是反射或是折射都是再通常不过的现象,所以我们也应该熟悉其实现。希望本文能对您有所帮助。