如何用3D透视效果更好地绘制三维透视游戏?

如何用3D透视效果更好地绘制三维透视游戏?

在较旧的 2D 硬件上,通过控制图形绘制的顺序以及计算每个单元的位置和缩放比例,可以在某种程度上创建类似的效果。 由于我们使用3D引擎(Unity),我们可以省去一些麻烦! 使用默认设置,Unity不会帮助您构建具有类似效果的投影,并且存在一些问题。 首先,Unity 并不真正知道这些元素在类似场景中如何按深度顺序排列。 其次,所需的投影并不是Unity本身提供的基础投影。 如果使用这个默认投影,效果会看起来很糟糕。 然而,通过正确的设置和简单的矩阵计算,无需大量工作即可实现经典的阴影效果。 例如,尝试我们的交互式 WebGL 示例。 本文末尾的资源部分包含一个连接到我们使用的相机的脚本文件。

抽签顺序

第一个问题是如何以正确的顺序绘制场景的元素。 由于大多数 2D 游戏都使用 Alpha 混合,因此 Unity 必须以正确的顺序绘制场景元素,而无需求助于 z 缓冲区。 Unity 中的排序支持对于 2D 游戏来说有些限制,因此需要大量的创造性实验,尽管非常乏味。 幸运的是,存在一个简单的解决方案,尽管它需要组合功能。

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

相反,我们使用排序图层从头到尾绘制元素,并使用正交相机对同一图层中的元素进行深度排序。 首先绘制底座,然后绘制顶部的细节(石头、田地等),最后绘制站立的元素。 由于地面总是平坦的透视相机制作2d游戏,植物和人类总是站立的等等,这些分类层永远不会改变,使其成为一个简单的设置。

剩下的任务是对同一层内的元素进行排序。 这部分实际上只是关于站立的物体,我们希望它们能够根据场景的深度自动排序。 默认情况下,正交相机已设置为正确的排序模式。 但是,透视相机的默认排序模式根据对象与相机中心的距离对对象进行排序。 这对于这些元素来说是不合适的,因为向左或向右移动相机会导致上下文发生变化。 要特别小心建筑物或树木等可能分散注意力的大型元素。 幸运的是,您可以像正交相机一样通过对深度方向进行排序来配置透视相机。 您可以在 Start() 方法中轻松配置相机。

相机.transparencySortMode =

TransparencySortMode.正交;

综上所述游戏开发素材,排序层首先从地面绘制元素,然后根据相机的深度从前到后按顺序绘制其他元素。 所有排序都会自动完成,无需脚本干预。

二维投影

下一个任务是正确渲染投影。 以上面的截图为例。 所有元素均平行于屏幕绘制,没有任何扭曲。 通过 30° 角和使用 60° 视角观察地面,会产生一些微妙的透视效果。 (这在静态图像中很难看到,但请尝试上面的示例)。 理想情况下,它应该能够实现这种效果,而无需编写大量代码,或者通过更改场景中的所有对象来使相机工作。

最有效的解决方案是尝试使用透视相机,但上面的屏幕截图显示了问题。 尽管使用了最佳的 60° 视角,但摄像头看起来仍然像是从屏幕底部直接观察元素,这使得它看起来像一张薄纸。 另一方面,顶部的树木看起来很好。

正交投影也不会做得更好。 虽然它很好地隐藏了其他元素,但效果只是一个平面。 现在所有的元素都扭曲了,看起来非常拥挤! 这些基本投影的最简单解决方案是,如果我们倾斜所有元素,使它们指向相机,那么这看起来相当不错。 事实上,这正是该解决方案的作用。 由于倾斜场景中的每个元素非常乏味,因此数学可以帮助我们。

在所有这些情况下,我们都在寻找从一个空间坐标系(3D 坐标系)到另一个空间坐标系(屏幕坐标系)的映射。 这就是投影的本质游戏角色,除了在这两者之间进行转换之外,Unity 还提供了很多有用的东西。 您可能听说过双眼或三重投影、正投影和等距投影。 这些都是线性投影的例子透视相机制作2d游戏,但还有大量的非线性投影,比如鱼眼镜头和你在地球上看到的那些疯狂的投影。

由于实时3D图形严重依赖矩阵运算,因此Unity提供的两种基本投影通过矩阵来表达也就不足为奇了。 事实上,任何线性投影都可以用矩阵来表示,因此找出哪些投影是线性的是一个有趣的问题。 第一个考虑的方法是:如果真实空间中存在直线,那么它在投影空间中也是直线。 同样的规则也适用于此,屏幕上的任何直线都是直线在真实空间中的投影。 给定一个正确的矩阵,我们应该能够通过Unity渲染我们想要的任何线性投影,甚至是Ultima独特的倾斜投影。

图片

虽然本文是关于如何在 2D 场景上进行 Unity 投影校正,但它也适用于 3D 场景,并且可以在相同的情况下使用。 事实上,《World Connect》使用了相同的技术,使其更接近旧版 2D Zelda 游戏的效果。 游戏甚至可以以立体 3D 方式渲染,显示屏上不会出现任何明显的失真。

(来自塞尔达地下城)

在 Unity 中使用自定义投影

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

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

私人无效OnPreCull(){

// 首先计算常规的 worldToCameraMatrix。

// 从transform.worldToLocalMatrix开始。

var m =camera.transform.worldToLocalMatrix;

// 然后,由于Unity使用OpenGL的视图矩阵约定

// 我们必须翻转 z 值。

m.SetRow(2, -m.GetRow(2));

// 现在进行自定义投影。

// 将世界的向上向量设置为始终与相机的向上向量对齐。

// 添加少量原始向上向量

// 确保矩阵是可逆的。

// 尝试更改向量以查看可以获得哪些其他投影。

m.SetColumn(2, 1e-3f*m.GetColumn(2) - new Vector4(0, 1, 0, 0));

相机.worldToCameraMatrix = m;

设置观察矩阵的最佳位置是在相机的OnPreCull()事件方法中。 在执行所有更新方法之后、任何渲染工作之前调用此方法。

用户输入坐标系

使用鼠标或触摸输入的游戏需要将屏幕坐标系转换为世界坐标系。 在基本的 2D 游戏中,Camera.ScreenToWorldPoint() 就足够了,但是当使用自定义投影时,它会变得有点复杂。 虽然可以使用现有的 Unity API 来构造光线并检查它如何与场景相交,但如果您的场景都是平坦的,则有一种更简单的方法。 编写您自己的 ScreenToWorldPoint() 函数只需要几行代码。 基本思想是Unity使用观察投影矩阵将世界坐标系转换到屏幕上,因此利用这个矩阵的逆,我们可以将屏幕上的点转换到世界坐标系上。 通过改变矩阵忽略Z轴,我们可以忽略场景的深度,只获取平面上的点。

公共静态Matrix4x4 ScreenToWorldMatrix(相机凸轮){

// 创建一个矩阵,将其转换为

// 屏幕坐标到剪辑坐标。

var 矩形 = cam.pixelRect;

var viewportMatrix = Matrix4x4.Ortho(矩形.xMin, 矩形.xMax, 矩形.yMin, 矩形.yMax, -1, 1);

// 相机的视图投影矩阵从世界坐标转换为剪辑坐标。

var vpMatrix = cam.projectionMatrix*cam.worldToCameraMatrix;

// 将第 2 列(z 轴)设置为恒等会使矩阵忽略 z 轴。

// 相反,你会得到 xy 平面上的值!

vpMatrix.SetColumn(2, new Vector4(0, 0, 1, 0));

// 从右到左:

// 将屏幕坐标转换为剪辑坐标,然后将剪辑坐标转换为世界坐标。

返回 vpMatrix.inverse*viewportMatrix;

公共 Vector2 ScreenToWorldPoint(Vector2 点){

return ScreenToWorldMatrix(camera).MultiplyPoint(point);

将变换分成两部分提供了更好的灵活性,可以通过矩阵缓存变换同一帧中的更多点。

自定义场景编辑器的投影

使用最少的代码,Unity 可以渲染类似于经典 2D 视频游戏中的自定义投影,并支持透视效果、输出和自动绘制顺序。 最后需要解决的问题是如何在Unity场景视图中通过所见即所得的方式编辑自定义投影。

幸运的是,最近的 Unity 版本提供了完成这项工作的钩子。 首先,相机脚本需要具有[ExecuteInEditMode]属性,否则只有在Unity处于播放模式时才起作用。 下一个代码基于上面列出的 OnPreCull() 代码构建。

私有无效OnEnable(){

// 可选,仅在编辑器中启用回调。

if(Application.isEditor){

// 这些回调被所有相机调用,包括

// 场景视图和相机预览。

Camera.onPreCull += ScenePreCull;

Camera.onPostRender += ScenePostRender;

私有无效OnDisable(){

if(Application.isEditor){

Camera.onPreCull -= ScenePreCull;

Camera.onPostRender -= ScenePostRender;

私人无效ScenePreCull(相机凸轮){

// 如果相机是场景视图相机,则调用我们的预剔除方法。

if(cam.cameraType == CameraType.SceneView) OnPreCull();

私人无效ScenePostRender(相机摄像头){

// 当你改变 worldToCameraMatrix 时,Unity 的 gizmos 不喜欢它。

// 解决方法是渲染后重置它。

if(cam.cameraType == CameraType.SceneView) cam.ResetWorldToCameraMatrix();

这在场景视图中提供了有用但并不完美的编辑体验。 尽管您可以在检查器中很好地编辑对象,但场景视图中的某些控件可能会显示不正确。 特别是,z轴偏移变换不会出现在正确的位置,矩形变换会遇到错误的轴。 其中一些问题可以通过禁用 onPostRender 事件来解决,但这会导致其他问题。 这不会是完美的编辑体验,但您可以改进它以获得更好的效果。

总结

因此,使用一些额外的矩阵数学,你可以省去很多麻烦。 让引擎为你工作,而不是通过 3D 引擎来获得伪 2D 引擎效果。 额外的好处是,可以轻松获得大量效果,例如反射和阴影。 在 Verdant Skies 中,我们有两个额外的摄像机来用不同的投影渲染场景。 反射效果可以通过将向上矢量从相机指向下方来实现,而阴影效果可以通过将阴影指向地面来实现。

文章来源:https://blog.csdn.net/woliuqiangdong/article/details/120259341