Unity3D正交-透视混合相机的实现

An implementation of mixed ortho-persp camera in Unity3D

(本文需要一定的Unity3D及其ShaderLab的知识)

1. 动机

在2D游戏开发中,经常会出现需要处理前景遮挡物体。我之前的做法是新建一个相机,然后depth设置大于主相机。把遮挡物体的sprite 都放在这个相机里。一直觉得这样对相机进行分层渲染场景没什么问题,直到有一天,看到了这个插件——SpriteSharp。

Sprite Sharp 最大的好处是在于能够把带有透明通道的sprite,进行透明-不透明像素的分离。原来单独的一个sprite,经过SpriteSharp处理之后,变成了两个关联的sprite。一个是完全不透明的区域,一个是半透明的区域。这样做的好处是,对于不透明的sprite,我们可以开启Zwrite和Ztest,进行遮挡剔除(参见Unity3D Culling and Depth Testing 和 Early Ztest)。这样一来凡是在不透明sprite之后出现的像素都不会去渲染,极大的节省了移动设备上的渲染带宽占用。当前景遮挡物变多的时候,这点尤其明显。因此,在有了SpriteSharp之后,如何让场景里的物体进行充分的ZTest是我接下来要思考的重点。

Unity3D正交-透视混合相机的实现_第1张图片
Sprite Sharp 可以对原先的半透明sprite根据alpha值分离成2个sprite

2. 分层相机带来的问题

一般在Unity3D当中,我们根据项目需要,都会对渲染内容进行分层设置。然后相机分层进行渲染,一般分层的相机设置如下:

Unity3D正交-透视混合相机的实现_第2张图片
层叠的相机

一般需要叠加渲染的相机都会设置ClearFlag = Depth Only,以及对应的Depth值。Depth大的相机会在Depth小的相机之上渲染。这样看似没什么问题,相信很多人也都是这么做的。但是却带来了一个些问题。底层相机渲染的内容,很有可能会被上层相机直接覆盖掉。这样一来,造成了底层相机的渲染的浪费。而如果做Ztest的话,又需要上层相机的不透明内容,先于底层相机渲染,这又与unity3d相机基于depth渲染顺序的方式相矛盾。因此,在FISH中,我去掉了原先分层的相机设置,所有内容,都放在一个正交相机里进行渲染了。这样前景物体能够在RenderQueue = Gemometry里,先于RenderQueue = Transparent 的物体进行渲染,并写入深度缓存,以便之后的透明物体进行ZTest。

3. 单一相机带来的问题

首先场景管理上,原先通过分层设置Layer以及相机LayerMask的方法,这里显然就不行了。虽然我还是保留了UI相机方便管理,但是大部分的场景内容,尤其是有前后遮挡关系的内容,我都尽量的合并在一起了,通过设置物体的Z值结合Sorting Order来控制前后关系。

其次,因为原先前景遮挡的内容,是单独的透视相机渲染的,而现在都放在一个正交相机里的话,前景物体没有了透视相机所形成的视差效果,因此如何对于前景物体在正交相机里实现透视相机的投影变换是接下来要思考的问题。

简单回顾一下,一个物体要正确渲染在屏幕上,需要经历Model - View - Projection 的投影变换。而不论是正交相机还是透视相机,Model - View 这两步变换是一样的。也就是说,对于前景需要视差的物体和一般的物体,他们的顶点Shader里,UNITY_MATRIX_MV 对他们是没差别的。差别只是在于正交相机的Projection Matrix 和 透视相机的Projection Matrix不同。那么如何根据一个正交相机,生成一个透视相机的Projection Matrix呢?

首相,我的正交相机高度是20,nearClipPlane = 0,farClipPlane = 100, position.z = -50f。这样一来,正交相机在xy平面的前后,就各有50的空间。而为了取得最好的透视效果,透视相机的位置需要和正交相机重合,并且透视相机的上下边缘,在通过xy平面的时候,需要和正交相机重合;也就是说,对于处于z=0的sprite,正交相机和透视相机的成像是一致的。此时透视相机的FOV可以通过正交相机计算得到:

fov=2f*Mathf.Rad2Deg*Mathf.Atan2(orthoCam.orthographicSize,Mathf.Abs(orthoCam.transform.position.z));

对于实际透视相机的Projection Matrix的计算,Unity3D里有一个方便的API:Matrix4x4.Perspective,可以通过fov,相机的长宽比,和剪切平面的距离来获得Projection Matrix。有时候,直接计算所得到的Projection Matrix 并不是Unity在Shader里最终会用到的,还需要用GL.GetGPUProjectionMatrix 获得最终使用在Shader里的Projection Matrix。之后我们把获得的变换矩阵设置成一个全局的Shader变量。该部分代码如下:(对于不同设定的正交相机,该代码具有通用性)

Matrix4x4 projmtx = Matrix4x4.Perspective(fov,orthoCam.aspect,orthoCam.nearClipPlane,orthoCam.farClipPlane);

proj=GL.GetGPUProjectionMatrix(projmtx,false);

Shader.SetGlobalMatrix("_PerspCamProj",proj);

Unity3D正交-透视混合相机的实现_第3张图片
正交相机与透视相机的取景框

4. 在正交相机里应用透视投影变换

在前面的部分里,我们已经得到了正交相机对应的透视相机的投影变换矩阵,并保存在了一个_PerspCamProj 的Shader全局变量里。接下来看看,如何对于需要透视变化的sprite应用特定的投影变换。

一般我们在Shader里处理顶点变换的时候,都会这么写:

o.pos = mul (UNITY_MATRIX_MVP, v.vertex);

对于我们的正交相机,这么做会把顶点变换到正交投影空间,而前面提到过,对于不同的相机,UNITY_MATRIX_MV都是相同的,区别只是在P的不同。因此,对于需要透视变换的物体,我们需要应用刚刚计算好的_PerspCamProj变换矩阵。这里先看代码:

#ifdef _PARALLAX_ON

float4 p =mul(UNITY_MATRIX_MVP, IN.vertex);//计算正交投影

OUT.vertex=mul(mul(_PerspCamProj,UNITY_MATRIX_MV), IN.vertex);//计算透视投影

OUT.vertex.z = p.z * OUT.vertex.w;//修正齐次坐标w

#endif

这里特别需要注意的是,为了正确的和正交相机的内容进行深度对比,我们需要让透视相机产生的内容在正交空间里,和正交相机产生的内容深度一致。正交投影变换过后,p.w 也就是齐次坐标的w值是1,而投影变换过后,w值不是1,GPU最后需要除以这个w来确定顶点在透视投影空间的最终的位置。因此我们需要人工的修正一下顶点的z值。

5. 总结

至此,我们了解到了,在正交相机里使用投影透视的一种方法。也大概了解到了用单一相机渲染场景并通过深度测试来节省渲染带宽占用的方法。视差模拟也可以直接在脚本上,通过设置位置来模拟。这里提供一种通过Shader来模拟的思路。希望和大家交流学习。

你可能感兴趣的:(Unity3D正交-透视混合相机的实现)