动态立方体贴图系统
当今,反射是可以让一个 Shader 在感官上具备视觉冲击力的一项关键技术,它是在 Shader 表面上模拟环境反射的一个过程,在这个过程中使用了你周围的世界(环境)信息,并且让 Shader 去反射这些世界(环境)的信息。因为在这部分中我们需要使用一种称为 Cubemap 的新型纹理。这种类型的纹理由 6 张纹理组成,并且用这 6 张纹理以立方体的方式围绕当前表面。所以想象一个立方体,立方体的6个面上被赋予了这6张纹理。这将允许我们去捕获周围环境并将其烘焙到纹理中。
介绍
当物体在环境中移动时,我们的反射并不能真正的反射出真实的世界。例如,如果你有一个由多个房间和走廊组成的环境,我们无法为整个关卡烘焙一个立方体贴图,并将其放在单个立方体贴图中。这并不能反射出房间之间合理的环境。我们会得到一个非常静态、无趣的反射。
有几种方法可以解决这个问题,使一个房间的反射与第二个房间的反射不同。第一种也是最基本的方法是根据 Room 中的位置交换 Cubemap。因此,当您从一个房间移动到另一个房间时,立方体贴图将替换为该房间正确的立方体贴图。第二种方法是角色在环境中移动时,实时更新 Cubemap,最终在游戏进行的每一帧中都会获得一个新的 Cubemap。虽然第二个选项听起来在视觉上更吸引人,因为你会看到立方体贴图之间出现一个短暂切换的一个动作,但它相当昂贵,非常占资源,因此需要与你的游戏所需所有其他资源进行权衡。
在本小节中我们介绍第一个选项,并向你展示如何设置一个非常简单的系统,以便根据环境中基于位置信息来切换两个立方体贴图。本小节的最后一部分提供了有关创建实时反射系统的更多信息,因此,如果你有兴趣并想了解这两种技术之间的区别,那么就可以开始了!
1、准备
- 1. 我们需要创建一个新场景,并在世界中放置一个地平面和一个球体。此外,添加一个定向光源,为我们的着色器获取一些光照。
- 2. 继续向场景添加两个空的 GameObject 构造函数,并分别将它们命名为 pos001 和 pos002。
- 3. 然后,让我们为球体赋予一个新的材质球,并将我们刚刚在上一小节中创建的菲涅尔着色器赋予到我们新材质球上。你的场景现在应类似于如 图6.1 。
- 4. 最后,让我们创建一个脚本并将其命名为 SwapCubemaps.cs。
以下屏幕截图显示了我们准备好的场景的结果,该场景已准备好用于我们的动态反射系统:
图6.1
2、实现
场景准备就绪后,我们可以按照接下来的几个步骤开始编写反射系统代码。
- 步骤1. 让我们在声明类之前添加 [ExecuteInEditMode]。
[ExecuteInEditMode]
public class SwapCubemaps : MonoBehaviour
{
- 步骤2. 然后,我们需要声明一些变量来存储我们系统中的所有数据。我们将在本小节中的下一部分解释这些。
public Cubemap cubeA;
public Cubemap cubeB;
public Transform posA;
public Transform posB;
private Material curMat;
private Cubemap curCube;
- 步骤3. 为了直观地看到立方体贴图位置在空间中的位置,我们需要利用 Unity3D 为我们提供的超棒的 Gizmos 功能。因此,让我们将以下代码添加到脚本的底部:
void OnDrawGizmos()
{
Gizmos.color = Color.green;
if(posA)
{
Gizmos.DrawWireSphere(posA.position, 0.5f);
}
if(posB)
{
Gizmos.DrawWireSphere(posB.position, 0.5f);
}
}
- 步骤4. 现在,我们需要创建一个新函数,该函数将根据我们设置的每个位置之间的距离来确定我们应该使用哪个立方体贴图:
private Cubemap CheckProbeDistance()
{
float distA = Vector3.Distance(transform.position, posA.position);
float distB = Vector3.Distance(transform.position, posB.position);
if(distA < distB)
{
return cubeA;
}
else if(distB < distA)
{
return cubeB;
}
else
{
return cubeA;
}
}
- 步骤5. 最后,我们只需要检查每一帧,看看环境中每个位置之间的距离是多少,并在我们的材质中合适的时候进行交换立方体贴图:
// Update is called once per frame
void Update()
{
//curMat = renderer.sharedMaterial;
curMat = GetComponent<Renderer>().sharedMaterial;
if(curMat)
{
curCube = CheckProbeDistance();
curMat.SetTexture("_Cubemap", curCube);
}
}
- 完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class SwapCubemaps : MonoBehaviour
{
public Cubemap cubeA;
public Cubemap cubeB;
public Transform posA;
public Transform posB;
private Material curMat;
private Cubemap curCube;
void OnDrawGizmos()
{
Gizmos.color = Color.green;
if(posA)
{
Gizmos.DrawWireSphere(posA.position, 0.5f);
}
if(posB)
{
Gizmos.DrawWireSphere(posB.position, 0.5f);
}
}
private Cubemap CheckProbeDistance()
{
float distA = Vector3.Distance(transform.position, posA.position);
float distB = Vector3.Distance(transform.position, posB.position);
if(distA < distB)
{
return cubeA;
}
else if(distB < distA)
{
return cubeB;
}
else
{
return cubeA;
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
//curMat = renderer.sharedMaterial;
curMat = GetComponent<Renderer>().sharedMaterial;
if(curMat)
{
curCube = CheckProbeDistance();
curMat.SetTexture("_Cubemap", curCube);
}
}
}
保存着色器后,返回到 Unity 编辑器中等待着色器编译好后。点击 Play 并来回移动球体。您应该会看到类似于下 图6.2 的结果:
图6.2
3、原理
我们只需通过声明类的 [ExecuteInEditMode] 属性来启动此脚本。这会告诉 Unity,我们希望在编辑器中运行脚本来切换立方体贴图,而不仅仅是在点击 Play 时运行。这将允许我们进行测试立方体贴图的切换,而无需点击 Play — 这样会将工作流程变得更快捷。
然后,该脚本包含一些变量,我们使用这些变量允许使用者输入两个立方体贴图和两个位置信息,我们使用它们来比较距离。最后,我们有两个私有变量,当程序运行时,我们可以使用它们来跟踪当前材质球和立方体贴图。
有了变量,我们就可以使用 OnDrawGizmos() 内置函数来实际显示我们让用户输入的转换位置的信息。这些位置将命令脚本何时切换我们的立方体贴图。
然后我们来了解这个程序的真正内容。我们声明自己的函数/方法,该函数/方法将使用 Vector3.Distance() 计算球体与两个变换中任何一个的距离。然后,它会检查哪个距离更小,并返回该位置的 Cubemap。
最后,在 Update() 函数中,我们从球体获取当前的材质球,或者此脚本附加到的对象,然后简单地分配从自定义函数返回的当前选定的立方体贴图。
这只是一个非常简单的脚本来阐述这个概念,但它可以扩展成一个完整的系统,每个房间都有多个立方体贴图。该系统可以在运行时为我们自动生成所有的立方体贴图,这对于无法负担完全实时反射系统的游戏来说非常有用。
4、另请参阅
您还可以尝试创建实时反射系统,其中 Cubemap 会针对游戏中的每一帧进行更新。这绝对是一个视觉上更吸引人的系统,但确实会以性能为代价:
摘自Unity Shaders and Effets Cookbook - 第四章:Reflecting Your World - 第6节
链接如下: