【GAD翻译馆】为你的Unity2D游戏增加一些透视效果

发表于2017-11-13
评论3 6.1k浏览

译者: 刘超(君临天下) 审校:梁君(君儿)

除非另有说明,以下博客文章均由Gamasutra论坛的成员撰写。

其所表达的观点和想法均是这些作者的,而并非是Gamasutra公司或其母公司的。

对于我们的游戏,Verdant Skies,我们希望它能有一个传统的手绘效果的外观,但是我们也希望让它有一个完整的三维透视效果,并且增加一些诸如阴影和流水反射的效果,使得它看起来更加有趣。这些正好非常切合我们小团队的技术特点。

在旧的二维硬件上,通过控制图形绘制顺序,计算每一个单元的位置和缩放比例,在某些程度上也是可能构建类似的效果的。自从我们使用了三维引擎(Unity)之后,我们可以节省一些麻烦!使用默认的设置,Unity并不能帮助你构建一个具有类似效果的投影,并且还会有一些问题。首先,Unity并不真正的知道这些元素在类似的场景中如何进行深度方向的排序。其次,所需要的投影并非是Unity本身提供的基本投影,如果你使用这个默认的投影,效果看起来非常糟糕。然而,通过正确的设置和一个简单的矩阵计算,不需要大量的工作便可以得到一个经典的投影效果。例如,尝试使用我们的交互WebGL示例。在文章末尾的资源章节包含了一个连接到我们使用的相机的脚本文件。

绘制顺序

第一个问题是如何按照正确的顺序绘制场景中的元素。由于大多数的二维游戏使用了Alpha混合,所以Unity必须在不借助z缓冲区的前提之下按照正确的顺序绘制场景中的元素。Unity中的排序支持对于二维游戏是有一些限制的,因此人们需要进行大量的创造性的尝试,尽管这些尝试非常的乏味。幸运的是,尽管需要一些特征的组合,但是仍然存在一个简单的解决方案。

由于Verdant Skies使用了3D透视视图,因此不可能使用z值来准确的进行深度方向的排序。沿着z轴移动目标物体将会改变它们的大小并且破坏它们的反射和阴影。此外,由于Unity是通过它们的中心点来进行深度排序的,因此我们无法依靠相机来自动化的对Verdant Skies中的元素进行排序,也无法使得一些元素放置在地面上而其它元素垂直站立。如果玩家走过地面中间的区域,它们的元素将会消失在地面!使用排序层进行排序得到的顺序也不是很理想,因为它需要每次当目标对象移动时更新排序顺序。除此之外,排序顺序的属性提供了非常小的精度(仅支持16位),这对场景的大小有一定的限制。

取而代之的是,我们从头到尾使用排序层来绘制元素,并且使用正交相机对同一层的元素进行深度方向的排序。首先绘制基础部分,然后在上方绘制细节部分(石头、田地等等),最后绘制站立的元素。由于地面总是平坦的,植物和人类总是站立的等等,所以这些分类层永远不会改变,从而成为了一个简单的设置。

剩下的任务是在同一层中对元素进行排序。这部分真的只关注站立的对象,并且我们想要它们通过场景中的深度进行自动的排序。正交相机已经默认被设置为正确的排序模式。然而,对于透视相机默认的排序模式会根据它们与相机中心的距离进行排序。这对于这些元素是不合适的,因为向左或者向右移动相机将会引起前后关系的变化。特别注意的是对于像是建筑物或者是树木这种大型的元素会分散注意力。幸运的是,你可以像正交相机一样,通过对深度方向进行排序来配置你的透视相机。你可以很容易的在Start()方法中配置你的相机。

1
camera.transparencySortMode = TransparencySortMode.Orthographic;

将它们放在一起,排序层首先从地面绘制元素,然后从前往后根据相机的深度排序绘制别的元素。所有的排序在无需脚本干预的前提之下将会自动进行。

2D投影

接下来的任务是正确的渲染投影。以上面的截图为例。所有的元素没有任何变形的情况下平行的绘制在屏幕上。地面通过30°角度并使用60°视角进行观察,得到了一些微妙的透视效果。(这在静态图像中是很难看到的,但是尽量尝试上面的例子)。理想情况下,它应该能够在不写大量代码的前提下实现这个效果,或者通过改变场景中的所有对象来使得相机工作。

最有效的解决方案是尝试使用透视相机,但是上述截图显示了该问题。尽管使用了最合适的60°视角,但是相机仍然像是从屏幕的底部来直接看着元素的,这看起来像是一片很薄的纸。另一方面,顶部的树木看起来是没问题的。

一个正交的投影效果不会好很多。尽管它很好的完成了隐藏别的元素的工作,但是效果仅仅是一个平面而已。现在它把所有的元素进行了扭曲,看起来十分的拥挤!对于这些基本投影最简单的解决方案是如果我们把所有的元素进行倾斜使得它们都指向相机,那么这看起来是十分不错的。事实上,这正是该解决方案要做的工作。由于对场景中的每一个元素进行倾斜是十分乏味的,而数学拯救了我们。

在所有的这些情况中,我们正在寻找从一个空间坐标系(三维坐标系)到另一个空间坐标系(屏幕坐标系)的映射。这是投影的本质,并且Unity除了提供这两个之间的变换之外还提供了很多有用的东西。你可能会听过过二视投影或者三视投影,正交投影以及等距投影。这些都是线性投影的例子,但是也有大量的非线性投影,例如鱼眼镜头和你在地球上看到的那些令人疯狂的投影。

由于实时3D图形学是非常依赖矩阵运算的,因此由Unity提供的两个基本投影通过矩阵表达是一点也不奇怪的。事实上,任何线性投影都可以通过矩阵来表达,所以这个有趣的问题找出哪些投影是线性的。最开始考虑的方法是:如果在现实空间中存在一条直线,那么在投影空间它也是一条直线。这个规则在这里同样有效,任何屏幕中的直线是由现实空间的直线投影得到的。给定一个正确的矩阵,我们应该能够通过Unity来渲染我们想要的任何线性投影,甚至是Ultima的独特的倾斜投影。

尽管这篇文章是关于如何对2D场景进行Unity投影修正的,但是它对于3D场景同样是适用的,并且可以被用于相同的情形下。事实上,世界的连接这款游戏采用这种同样的技术使得它的效果更加接近旧的2D赛达尔游戏的效果。该游戏甚至可以以3D立体的方式进行渲染而不会对显示造成任何明显的失真。

(来自 ZeldaDungeon)

在Unity中使用自定义投影

在所有基础的3D引擎中,矩阵运算被用来将目标对象从真实世界中的位置转换至屏幕上进行显示。该矩阵称之为模型-观察-投影矩阵,被分为了三部分。首先,模型矩阵负责模型坐标系到世界坐标系的转换(Transform.localToWorldMatrix)。每个物体都有它自己的模型矩阵,基于该矩阵变换能够得到它与其他物体之间的相对位置。接下来,观察矩阵负责世界坐标系和相机坐标系之间的转换(Camera.worldToCameraMatrix)。最后,投影矩阵负责相机坐标系和屏幕坐标系之间的转换(Camera.projctionMatrix)。在一个场景中,观察矩阵和投影矩阵是被所有的物体共享的,并且决定了对屏幕的整体投影。通常情况下,投影矩阵只是负责正交投影和透视投影之间的选择以及通过相机进行渲染的屏幕的整体尺度。把它想象成一个相机的镜头。

为了进行自定义投影的设置,首先选择采用正交投影还是透视投影。投影矩阵会处理这部分,你可以使用Unity中的常规的相机检视器来进行设置。然后每一个轴指向屏幕的方向会通过观察矩阵进行设置。通常情况下,Unity会在每一帧根据相机的变换来更新这个矩阵,但是可以通过自定义的值的写入来覆盖Camera.worldToCaemraMatrix中的属性。由于我们知道地面在什么情况下看起来是比较好的,所以我们不需要改变x轴或者y轴的输出。具体来说,我们想要世界的方向总是与相机的方向一致。由于矩阵的每一列都与每一个轴一一对应,因此我们仅需要改变z轴对应的那列即可。那么代码看起来如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void OnPreCull(){
    // First calculate the regular worldToCameraMatrix.
    // Start with transform.worldToLocalMatrix.
    var m = camera.transform.worldToLocalMatrix;
    // Then, since Unity uses OpenGL's view matrix conventions
    // we have to flip the z-value.
    m.SetRow(2, -m.GetRow(2));
 
    // Now for the custom projection.
    // Set the world's up vector to always align with the camera's up vector.
    // Add a small amount of the original up vector to
    // ensure the matrix will be invertible.
    // Try changing the vector to see what other projections you can get.
    m.SetColumn(2, 1e-3f*m.GetColumn(2) - new Vector4(0, 1, 0, 0));
 
    camera.worldToCameraMatrix = m;

设置观察矩阵的最佳的位置是在camera的OnPreCull()事件方法中。该方法在所有的更新方法执行之后调用,但是会在所有的渲染工作之前被调用。

用户输入坐标系

使用鼠标或者是触摸输入的游戏需要将屏幕坐标系转换为世界坐标系。在一个基本的2D游戏中,Camera.ScreenToWorldPoint()是足够使用的,但是当使用自定义投影时,它变得有一些复杂。尽管可以使用现有的UnityAPI来构建光线并检查它与场景的相交情况,但是如果你的场景都是平面的话,那么有一个更加简便的方法。编写你自己的ScreenToWorldPoint()函数,仅需要几行代码。基本的想法是Unity使用了观察-投影矩阵来将世界坐标系转换至屏幕上,因此使用该矩阵的逆矩阵,我们能够将屏幕上的点转换至世界坐标系中。通过改变矩阵来忽略Z轴,是能够忽略场景的深度并且仅得到平面上的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static Matrix4x4 ScreenToWorldMatrix(Camera cam){
    // Make a matrix that converts from
    // screen coordinates to clip coordinates.
    var rect = cam.pixelRect;
    var viewportMatrix = Matrix4x4.Ortho(rect.xMin, rect.xMax, rect.yMin, rect.yMax, -1, 1);
 
    // The camera's view-projection matrix converts from world coordinates to clip coordinates.
    var vpMatrix = cam.projectionMatrix*cam.worldToCameraMatrix;
 
    // Setting column 2 (z-axis) to identity makes the matrix ignore the z-axis.
    // Instead you get the value on the xy plane!
    vpMatrix.SetColumn(2, new Vector4(0, 0, 1, 0));
 
    // Going from right to left:
    // convert screen coords to clip coords, then clip coords to world coords.
    return vpMatrix.inverse*viewportMatrix;
 
public Vector2 ScreenToWorldPoint(Vector2 point){
    return ScreenToWorldMatrix(camera).MultiplyPoint(point);

将该转换分为两个部分可以提供更好的灵活性来通过矩阵缓存的方法在同一帧中转换更多的点。

为场景编辑器自定义投影

使用最少的代码,可以使得Unity渲染类似经典的2D视频游戏中的自定义的投影,并且支持透视效果、输出以及自动的绘制顺序排序。最后需要解决的问题时如何在Unity场景视图中将自定义投影通过WYSIWYG进行编辑。

幸运的是,最近的Unity版本提供了hooks来完成该工作。首先,相机脚本需要具有[ExecuteInEditMode]属性,否则只能在Unity处于播放模式时才能奏效。接下来的代码,在上述列出的OnPreCull()代码上进行构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void OnEnable(){
    // Optional, only enable the callbacks when in the editor.
    if(Application.isEditor){
        // These callbacks are invoked for all cameras including
    // the scene view and camera previews.
        Camera.onPreCull += ScenePreCull;
        Camera.onPostRender += ScenePostRender;
    
 
private void OnDisable(){
    if(Application.isEditor){
        Camera.onPreCull -= ScenePreCull;
        Camera.onPostRender -= ScenePostRender;
    
 
private void ScenePreCull(Camera cam){
    // If the camera is the scene view camera, call our pre cull method.
    if(cam.cameraType == CameraType.SceneView) OnPreCull();
 
private void ScenePostRender(Camera cam){
    // Unity's gizmos don't like it when you change the worldToCameraMatrix.
    // The workaround is to reset it after rendering.
    if(cam.cameraType == CameraType.SceneView) cam.ResetWorldToCameraMatrix();

这在场景视图中提供了一个有用的,但是并非很完美的编辑体验。虽然你可以在检视器中较好的编辑一个对象,但是场景视图中的有一些控件可能是显示不正确的。特别的,z轴的偏移变换不会显示在正确的位置上,并且矩形变换将会遇到错误的轴。其中一些问题可以通过禁用onPostRender事件来解决,但是这样会导致别的问题。总之不会有一个完美的编辑体验,但是你能够对它进行更好的改进。

总结

所以使用一些额外的矩阵数学,你能够为自己避免大量的麻烦。通过引擎来为你工作,而不是通过3D引擎来获得伪2D引擎效果。作为奖励,大量的效果,例如反射、阴影能够很简单的获得。在Verdant Skies中,我们有额外的两个相机来采用不同的投影渲染场景。反射效果可以通过将向上的矢量朝向相机的下方即可,而阴影效果将阴影朝向地面的方向即可。


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引