传统上,游戏使用一种称为_反射贴图_的技术来模拟来自对象的反射,同时将处理开销保持在可接受的水平。此技术假定场景中的所有反射对象都可以“看到”(因此会反射)完全相同的周围环境。如果游戏的主角(比如闪亮的汽车)处于开放空间中,此技术将非常有效,但是当角色进入不同的周围环境时,便看起来不真实;如果一辆汽车驶入隧道但天空仍然在窗户上产生明显反射,看起来就很奇怪。
Unity 通过使用__反射探针__改进了基本反射贴图,这种探针可在场景中的关键点对视觉环境进行采样。通常情况下,应将这些探针放置在反射对象外观发生明显变化的每个点上(例如,隧道、建筑物附近区域和地面颜色变化的地方)。当反射对象靠近探针时,探针采样的反射可用于对象的反射贴图。此外,当几个探针位于彼此附近时,Unity 可在它们之间进行插值,从而实现反射的逐渐变化。因此,使用反射探针可以产生非常逼真的反射,同时将处理开销控制在可接受的水平。
场景中某个点的视觉环境可由立方体贴图表示。立方体贴图在概念上很像一个在内部表面绘有六个方向(上、下、左、右、前、后)平面图像的盒子。
天空盒立方体贴图的内部表面(去除了前面)
为了让对象显示反射,其着色器必须能够访问表示立方体贴图的图像。对象表面的每个点都可在表面朝向的方向(即表面法向矢量的方向)上“看到”立方体贴图的一小块区域。着色器在此处使用立方体贴图的颜色来计算对象表面应该是什么颜色;镜面材质可能会准确反射颜色,而闪亮的汽车可能会略微褪色和着色。
如上所述,传统的反射贴图仅使用单个立方体贴图来表示整个场景的周围环境。立方体贴图可由美术师绘制,也可通过从场景中的一个点进行六次“快照”(每个立方体面对应一次快照)获得。反射探针在这方面进行了改进,允许在场景中设置许多预定义的点,在这些位置点可创建立方体贴图快照。因此,可以在场景中存在明显不同反射的任何位置点记录周围视图。
除了视点之外,探针还有一个由场景中的不可见盒体形状定义的效果区域。在探针区域内通过的反射对象的反射立方体贴图由该探针临时提供。当对象从一个区域移动到另一个区域时,立方体贴图会相应改变。
反射探针有三种基本类型,通过检视面板中的 Type 属性可进行相应选择:
**可以通过单击 Reflection Probe Inspector 底部的 Bake 按钮或 Lighting 窗口中的 Build 按钮来触发烘焙。**如果在 Lighting 窗口中启用了 Auto_,则在 Scene 视图中放置对象时,烘焙探针将自动更新。**烘焙探针的反射只能显示 Inspector 中标记为 Reflection Probe Static_ 的对象。(烘焙的时候注意如果想把某物体烘焙进去,该物体应该勾选ReflectionProbe static)**此设置向 Unity 指示这些对象不会在运行时移动。
这意味着反射不仅限于静态对象,而且**可以实时更新以显示场景中的变化。**但是,刷新探针的视图需要相当长的处理时间,因此谨慎管理更新就显得十分重要。**Unity 允许从脚本中触发更新,以便能够准确控制更新的发生时间。**此外,还有一个选项是应用_时间切片_来探测更新,使更新可在几帧内逐渐发生。
这些探针允许在 Editor 中烘焙视图,就像烘焙探针一样,还能为反射提供自定义的立方体贴图。自定义探针无法在运行时更新。
下面将详细说明这三种类型。
烘焙反射探针是在 Unity Editor 中捕获反射立方体贴图并存储以供后续在播放器中使用的探针。捕获过程完成后,反射将被“冻结”,因此**烘焙探针无法响应移动对象在场景中引起的运行时变化。**但是,与实时探针(能响应变化)相比,烘焙探针的处理开销要低得多,并且在许多用途中是可接受的。例如,如果只有一个移动的反射对象,那么它只需要反射其静态环境。
应将探针的 Type 属性设置为 Baked 或 Custom 以将其用作烘焙探针
烘焙探针捕获的反射只能包含标记为 Reflection Probe Static 的场景对象(使用检视面板左上角的 Static 菜单标记所有对象)。可以使用 Culling Mask 和 Clipping Planes 属性进一步优化包含在反射立方体贴图中的对象;具体的工作方式与摄像机的工作方式相同**(探针本质上类似于通过旋转观察六个立方体贴图面每一面的摄像机)**。
当 Auto 选项启用时(从 Lighting 窗口中设置),烘焙反射将在对象放置到场景中时自动更新。如果不使用自动烘焙,则需要单击 Reflection Probe Inspector 中的 Bake 按钮以更新探针。(Lighting 窗口中的 Build 按钮也将触发探针更新。)
无论使用自动烘焙还是手动烘焙,烘焙过程都将继续在 Editor 中工作时异步进行。但是,如果移动任何静态对象、更改其材质或以其他方式改变其视觉外观,则会重新启动烘焙过程。
默认情况下,自定义探针的工作方式与烘焙探针的工作方式相同,但自定义探针还有其他选项可以更改此行为。
自定义探针检视面板上的 Dynamic Objects 属性允许未标记为 Reflection Probe Static 的对象包含在反射立方体贴图中。但是请注意,这些对象的位置在烘焙时仍然会在反射中“冻结”。
Cubemap 属性允许将自定义的立方体贴图分配给探针,从而使探针完全独立于可从其视点“看到”的对象。可以使用此属性来设置从 3D 建模应用程序生成的天空盒或立方体贴图作为反射源。
烘焙探针可用于许多用途,并具有良好的运行时性能,但它们的缺点是不能在播放器中实时更新。这意味着对象可在场景中移动但不能让它们的反射随之移动。如果这种问题带来了太多限制,可以使用实时探针,它们会在运行时更新反射立方体贴图。这种效果会带来更高的处理开销,但可提供更强的真实感。
使用实时探针
要使探针在运行时更新,应在反射探针检视面板中将其 Type 属性设置为 Realtime。不需要将对象标记为 Reflection Probe Static 来捕获它们的反射(就像使用烘焙探针一样)。但是,可以使用 Culling Mask 和 Clipping Planes 属性在反射立方体贴图中选择性地排除对象;具体的工作方式与摄像机的工作方式相同(探针本质上类似于通过旋转观察六个立方体贴图面每一面的摄像机)。
在 Editor 中,实时探针与烘焙探针具有大致相同的工作流程,但它们的渲染速度往往更快。当 Auto 选项启用时(从 Lighting 窗口中设置),反射将在对象放置到场景中时自动更新。如果不使用自动烘焙,则需要单击 Reflection Probe Inspector 中的 Bake 按钮以更新探针。(Lighting 窗口中的 Build 按钮也将触发探针更新。)
无论使用自动烘焙还是手动烘焙,烘焙过程都将继续在 Editor 中工作时异步进行。但是,如果移动任何静态对象、更改其材质或以其他方式改变其视觉外观,则会重新启动烘焙过程。
注意:目前,实时探针仅在 Reflection Probe Static 对象移动或改变其外观时才会更新在 Scene 视图中的反射。这意味着,即使移动的动态对象出现在反射中,这些对象也不会触发更新。应该从 Lighting 窗口的 Build 按钮弹出窗口中选择 Bake Reflection Probes 选项,从而在动态对象改变时更新反射。
这里主要讲解一下烘焙反射探针的实现
从GameObject->light->Reflection Probe创建,就可以得到一个反射探针:
立方体空间是反射空间,只有在这个空间里的物体才会被反射出来,最好把探针放在一个物体列表下,可以将探针中心与物体重合,之后将探针采集的立方体贴图应用于此物体(如一个球),那么就可以直接对此立方体贴图进行采样,这样比较精确
注意被烘焙的物体为静态物体,反射探针只反射静态物体,如果不勾选静态物体,不会反射。
创建shader并读取我们获得的烘焙贴图:
shader代码:
Shader "Unlit/RP_Cubemap_reflection_shader"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_ReflectColor("_ReflectColor", Color) = (1, 1, 1, 1)
_ReflectAmount("_ReflectAmount", Range(0,1)) = 1
_Reflection_CubeMap("_Reflection_CubeMap", Cube) = "_Skybox"{}
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8,255)) = 8.0
}
SubShader
{
Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline"}
Pass
{
Tags {"LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS //接收阴影
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE //得到正确的阴影坐标
#pragma multi_compile _ _SHADOWS_SOFT //软阴影
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _Diffuse;
float4 _ReflectColor;//用于控制反射的天空盒子的颜色量
float _ReflectAmount;//用于控制反射的天空盒子的颜色和漫反射diffuse在总体反射中的占比
samplerCUBE _Reflection_CubeMap;
half4 _Specular;
half _Gloss;
CBUFFER_END
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 vertex_normal : NORMAL;
};
struct v2f
{
float4 position_CS : SV_POSITION;
float4 position_WS : TEXCOORD0;
float3 normal_WS : TEXCOORD1;
float3 view_dir_WS : TEXCOORD2;
float3 reflect_dir_WS : TEXCOORD3;
};
v2f vert (appdata v)
{
v2f o;
o.position_CS = TransformObjectToHClip(v.vertex);//获取裁剪空间的顶点坐标
o.position_WS = mul(unity_ObjectToWorld, v.vertex);//获取世界空间的顶点坐标
o.normal_WS = TransformObjectToWorldNormal(v.vertex_normal);//获取世界空间的法线
o.view_dir_WS = GetWorldSpaceViewDir(o.position_WS);
o.reflect_dir_WS = reflect(-normalize(o.view_dir_WS), normalize(o.normal_WS));//获取反射光线
return o;
}
half4 frag (v2f i) : SV_Target
{
float4 SHADOW_COORDS = TransformWorldToShadowCoord(i.position_WS);//获取将世界空间的顶点坐标转换到光源空间获取的阴影坐标,这里在片元着色器里面进行,利用了插值之后的结果
Light main_light = GetMainLight(SHADOW_COORDS);
float3 light_dir_WS = normalize(TransformObjectToWorld(main_light.direction));//获取世界空间的光照单位矢量
float3 view_dir_WS = normalize(i.view_dir_WS);
float3 reflect_dir_WS = normalize(i.reflect_dir_WS);
float3 normal_WS = normalize(i.normal_WS);
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//获取环境光强度
half3 diffuse = main_light.color.rgb * _Diffuse.rgb * saturate(dot(normal_WS, light_dir_WS));//获取漫反射强度,light.color.rgb是光照强度
half3 reflection = texCUBE(_Reflection_CubeMap,reflect_dir_WS).rgb * _ReflectColor.rgb;//获取反射的天空盒子的颜色
half3 half_dir = normalize(light_dir_WS + view_dir_WS);//获取半程向量
half3 specular = main_light.color.rgb * _Specular.rgb * pow(saturate(dot(half_dir,normal_WS)), _Gloss);//获取高光
reflection = lerp(diffuse, reflection, _ReflectAmount);//通过平滑函数获得最佳的反射
half3 return_color = ambient + reflection + specular;
return half4(return_color, 1.0);
}
ENDHLSL
}
UsePass "Universal Render Pipeline/Lit/ShadowCaster"
}
Fallback "Diffuse"
}
创建材质,在cubemap中选择我们得到的烘焙贴图,将材质拖到物体上,就可以获得反射效果:
如果加入了其他物体并设为静态物体,想要更新反射需要重新点击Bake实现烘焙贴图的更新。