【Unity Shader学习笔记】Unity高级纹理 - 立方体纹理
立方体纹理(Cubemap)是环境映射(Environment Mapping)的一种实现方法。
环境映射可以让物体看起来像镀了层金属一样反射周围的环境。
立方体纹理一共包含六张图像,对应了一个立方体的六个面。
采样的时候,给出一个三维坐标,由这个三维坐标确定一个方向向量。这个向量延伸后与立方体的交点就是采样结果。
缺点:场景发生改变的时候需要重新生成立方体纹理。
不能模拟多次反射,如两个金属球。请使用 PBR (基于物理的渲染)技术。
尽量使用凸多面体。(凹多面体会反射自身)
最常见的用途是天空盒子(Skybox)和环境映射。
1、天空盒 Skybox
使用天空盒的时候,整个场景被包含在一个立方体内,这个立方体每个面都使用立方体纹理映射技术。
创建一个 Skybox 材质,在 Window - Rendering - Lighting 中赋给相关设置即可。
还需要保证渲染场景的摄像机的 Camera 组件中的 Clear Hags 被设置为 Skybox。
Unity 中,天空盒是在所有不透明物体后渲染的,其背后的网格是一个立方体或者一个细分的球体。
2、创建用于环境映射的立方体纹理
创建用于环境映射的立方体纹理的方法有三种:
- 直接由一些特殊布局的纹理创建。
提供一张具有特殊布局的纹理,将纹理的 Texture Type 设置为 Cubemap 即可。 - 手动创建一个 Cubemap 资源, 再把 6 张图陚给它。
仅用于 Unity 5 以前的版本,但不支持边缘修正、压缩、光滑反射(glossy reflection)、HDR。 - 由脚本生成。
利用 Unity 提供的 Camera.RenderToCubemap 函数实现。该函数能够从任意位置处观察到的场景存储在 6 张图像中,从而创建出该位置上对应的立方体纹理。
下面使用脚本实现:
- 创建一个 Legacy - Cubemap ,勾选 Readable 选项。
- 编写 C# 脚本。这个脚本创建了一个新窗口。
using UnityEngine; using UnityEditor; using System.Collections; 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<Camera>(); // place it on the object go.transform.position = renderFromPosition.position; // render into cubemap go.GetComponent<Camera>().RenderToCubemap(cubemap); // destroy temporary camera DestroyImmediate( go ); } [MenuItem("GameObject/Render into Cubemap")] static void RenderCubemap () { ScriptableWizard.DisplayWizard<RenderCubemapWizard>( "Render cubemap", "Render!"); } }
- 这个脚本在 GameObject 中添加了 Render into Cubemap 窗口。在这个窗口将 GameObject、Cubemap 拖入,点击 Render 即可生成 Cubemap。
3、反射
- 通过入射光线、表面法线来计算反射方向。
使用 CG 的 reflect 函数计算反射方向:
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
第一个参数为视线方向的反方向,第二个参数为法线方向 - 利用反射方向对立方体纹理采样。
使用 texCUBE 函数对立方体采样,不需要进行归一化,因为只代表方向。
使用 lerp 函数利用 _ReflectAmount 混合漫反射颜色与反射颜色,并于环境光相加后返回。
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) }; 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); // Compute the reflect dir in world space 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)); // 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); } ENDCG } } FallBack "Reflective/VertexLit" }
本案例中,我们在顶点着色器中计算反射方向。
在片元着色器中计算能够得到更加细腻的结果。(性能消耗也更大)

4、折射
4.1、原理
可以使用斯涅尔定律(Snell’s Law)来计算反射角。
其中,η1 和 η2 分别是两个介质的折射率 (index of refraction)。
折射率是一项重要的物理常数,例如真空的折射率是 1,而玻璃的折射率一般是 1.5。

对一个透明物体来说, 更准确的模拟方法需要计算两次折射:当光线进入它的内部时、从它内部射出时。
由于效率,我们一般只模拟入射
4.2、实现
在顶点着色器中,使用 refract()
函数计算折射方向。
- 第一个参数即为入射光线的方向, 它必须是归一化后的矢量。
- 第二个参数是表面法线, 法线方向同样需要是归一化后的;
- 第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值。
例如如果光是从空气射到玻璃表面, 那么这个参数应该是空气的折射率和玻璃的折射率之间的比值, 即 1/1.5。
它的返回值就是计算而得的折射方向, 它的模则等于入射光线的模。
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
最后使用 lerp 混合漫反射颜色与折射颜色。
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 = UnityObjectToClipPos(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; } 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); } ENDCG } } FallBack "Reflective/VertexLit" }

5、菲涅尔反射
菲涅耳反射描述了一种光学现象, 即当光线照射到物体表面上时, 一部分发生反射, 一部分进入物体内部, 发生折射或散射。
被反射的光和入射光之间存在一定的比率关系, 这个比率关系可以通过菲涅耳等式进行计算。

实时渲染中,我们使用 Schlick 菲涅耳近似等式:
Fo 是一个反射系数, 用于控制菲涅耳反射的强度, v 是视角方向, n 是表面法线。
另一个应用比较广泛的等式是 Empricial 菲涅耳近似等式:
其中,bias 、scale 和 power 是控制项 。
常用于车漆、水面效果
下面是一个使用 Schlick 菲涅耳近似等式的 Shader:
使用 lerp 函数根据截取到(0,1)的 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 ("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.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; 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); } ENDCG } } FallBack "Reflective/VertexLit" }
效果如下:

一些实现也会直接把 fresnel 和反射光照相乘后叠加到漫反射光照上, 模拟边缘光照的效果。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步