【Unity】相机 Cameras

发布于:2025-06-03 ⋅ 阅读:(140) ⋅ 点赞:(0)

1 前言

        主要介绍官方文档中相机模块的内容。

        关于“9动态分辨率”,这部分很多API文档只是提了一下,具体细节还需要自己深入API才行。

2 摄像机介绍

        Unity 场景在三维空间中表示游戏对象。由于观察者的屏幕是二维屏幕,Unity 需要捕捉视图并将其“平面化”以进行显示。它使用摄像机来实现这一点。在 Unity 中,我们可以通过将一个Camera组件添加到游戏对象来创建摄像机,也可以右键创建相机,如创建Cube一样。

2.1相机的视野

        摄像机的视野是由它的Transform和 Camera 组件来定义。Transform来定义相机的位置、旋转,Z轴是视野方向,Y轴是屏幕顶端,Camera 组件的设置还定义了视图区域的大小和形状。当相机移动和旋转时,显示的视图会随之移动和旋转。

2.2 透视与正交相机

        现实世界中的摄像机(实际相当于人眼)在观察外界事物时,物体距离视点越远,看起来越小。这种众所周知的透视效果在艺术和计算机图形领域广泛应用,对于创建现实场景至关重要。这就是透视摄像机,当然,Unity是支持的。但有时需要专门在没有这种透视效果的条件下渲染视图,例如,需要创建一种与真实世界的对象不完全相同的地图或信息显示效果,显示的对象不随距离变远而缩小,实现这种效果的摄像机称为正交摄像机,Unity 摄像机也有这样的选项。在透视和正交模式下观察场景称为摄像机投影。透视(左)与正交(右)效果:

2.3 视野区域形状

        对于从当前位置能“观察”的最远距离方面,透视和正交摄像机都存在一定的限制。该限制由垂直于摄像机向前 (Z) 方向的平面定义。此平面称为远裁剪面,因为与摄像机距离较远的对象将被“裁剪”(即,不在渲染范围内)。摄像机附近还有一个相应的近裁剪面;可观察的距离范围位于这两个平面之间。

        在非透视模式下,无论距离远近,对象大小不变。这表示,正交摄像机的视体由两个裁剪面之间的长方体定义。如图:

        使用透视摄像机时,对象会随其与摄像机的距离增大而缩小。这表示场景中可视部分的宽度和高度随着距离的增大而增大。因此,透视摄像机的视体不是一个长方体,而是金字塔形状,其顶点位于摄像机位置而底部位于远裁剪面。不过,该形状并不是严格的金字塔形,因为顶部被近裁剪面截断了,这种被截断的金字塔形状称为视锥体。由于视锥体的高度并非常量,视锥体由其宽度与高度之比(称为宽高比)以及顶部与底部之间在顶点处的夹角(称为视野即 FOV)定义。请参阅关于了解视锥体的页面,了解更多详细说明。视椎体如图:

2.4 相机视图的背景

        在渲染场景之前,我们可以设置相机如何处理物体之间的空白区域的背景。例如,我们可以选择在背景上渲染场景之前采用纯色填充背景,或者绘制天空或远处的背景,甚至保留前一帧的内容。有关配置此设置的信息,请参阅 Camera Inspector 参考中的 Background 属性。有关绘制天空的信息,请参见天空

3 使用多个相机

        创建一个场景后,默认是包含一个相机的,这在大多数情况下就已经足够了。但我们也可以在场景中添加任意数量的摄像机,还能以不同的方式组合它们的视图,如下所述。

3.1 切换相机

        默认情况下,摄像机会用它所渲染的画面覆盖整个屏幕,因此同一时刻只能看到一个摄像机的画面(即 depth 属性具有最高值的摄像机)。通过在脚本中禁用一个摄像机并启用另一个,即可从一个摄像机的镜头“切”到另一个,从而得到场景的多个镜头画面。在某些情况下,例如要在俯瞰地图视角和第一人称视角之间切换时,就可以采用这种方法。

using UnityEngine;

public class ExampleScript : MonoBehaviour {
    public Camera firstPersonCamera;
    public Camera overheadCamera;

    // 调用此函数可禁用 FPS 摄像机,并启用overhead相机。
    public void ShowOverheadView() {
        firstPersonCamera.enabled = false;
        overheadCamera.enabled = true;
    }
    
    // 调用此函数可启用 FPS 摄像机,并禁用overhead相机。
    public void ShowFirstPersonView() {
        firstPersonCamera.enabled = true;
        overheadCamera.enabled = false;
    }
}

3.2 渲染画中画

        通常情况下,我们希望至少有一个摄像机视图覆盖整个屏幕(默认设置),但在屏幕的一个小区域内显示另一个视图往往也很有用。例如,我们可能会在驾驶游戏中显示一个后视镜,或者在第一人视角的屏幕角落里显示俯瞰小地图。我们可以使用摄像机的 Viewport Rect 属性设置摄像机在屏幕上的矩形的大小。

        视图矩形的坐标相对于屏幕经过“标准化”,底边和左边是 0.0 坐标,而顶边和右边是 1.0,坐标值为 0.5 表示位于中间位置。除了视图大小之外,还应将具有较小视图的摄像机的 depth 属性设置为一个高于背景摄像机的值。确切的值无关紧要,总之规则是具有较高 depth 值的摄像机的渲染画面会覆盖在较低值摄像机的渲染画面之上。

4 使用物理摄像机 (Physical Camera)

        摄像机组件的 Physical Camera 属性在 Unity 摄像机上模拟真实摄像机格式。这可用于从同样模拟真实摄像机的 3D 建模应用程序导入摄像机信息。

Unity 提供的设置与大多数 3D 建模应用程序的物理摄像机设置相同。控制摄像机视野的两个主要属性是 Focal LengthSensor Size

  • Focal Length:传感器和摄像机镜头之间的距离,即焦距。此属性决定了垂直视野。Unity 摄像机处于 Physical Camera 模式时,改变 Focal Length 也会相应改变视野。焦距越小,视野越大,反之亦然。

  • Sensor Size:捕捉图像的传感器的宽度和高度,表示传感器大小。这些数值决定了物理摄像机的宽高比。可从对应于真实摄像机格式的几个预设传感器大小中进行选择,或设置自定义大小。传感器宽高比与渲染的宽高比(在 Game 视图中设置)不同时,可以控制 Unity 如何将摄像机图像与渲染的图像匹配(请参阅下文中关于 Gate Fit 的信息)。

4.1 Lens Shift

        Lens Shift(镜头位移,组件上的属性) 从传感器上水平和垂直地偏移摄像机的镜头。这样一来便可以改变焦点中心,并在渲染的帧中重新定位拍摄对象,确保很少或完全没有失真。

        这种方法在建筑摄影中很常见。例如,如果要拍摄一座高楼,可以旋转摄像机。但这会使图像失真,导致平行线看起来发生会聚。如图:

如果把镜头上移,而不是旋转摄像机,就可以改变构图以包含楼顶,但平行线保持直线。如图:

同样,可以使用水平镜头位移方法来拍摄宽大的对象,避免由于旋转摄像机而可能产生的失真。如图:

4.1.1 Lens Shift和视锥体倾斜

        镜头移位的一个副作用是会使摄像机的视椎体倾斜。这意味着摄像机的中心线与其视锥体之间的角度在一侧要小于另一侧。如图:

这里再给个动图演示:

即往那边移,视椎体就往哪边倾斜。

        此功能可用于根据视角来创造视觉效果。例如,在赛车游戏中,可能希望将视角保持在接近地面的较低位置。镜头移位是一种不用脚本即可实现视锥体倾斜的方式。

        有关更多信息,请参阅关于使用斜视锥体的文档。

4.2 Gate Fit

        Camera 组件的 Gate Fit 属性决定了 Game 视图和物理摄像机传感器具有不同宽高比时会发生什么情况。

在 Physical Camera 模式中,一个摄像机有两个“门”。

  • 根据 Aspect 下拉菜单中设置的分辨率,在 Game 视图中渲染的区域被称为“分辨率门”。(Game视图可看到的)

  • 摄像机实际看到的区域(由 Sensor Size 属性定义)被称为“胶片门”。(摄像机拍摄到的)

两个门具有不同宽高比时,Unity 让分辨率门“适应”胶片门。有几种适应模式,但是这些模式都会产生以下三种结果之一。

  • 裁剪 (Cropping):适应后,胶片门超过分辨率门时,Game 视图在满足宽高比的情况下渲染尽可能多的摄像机图像面积,并裁掉其余部分。

  • 过扫描 (Overscanning):适应后,胶片门超过分辨率门时,Game 视图仍然对摄像机视野之外的场景部分进行渲染计算。【感觉是不是应该是胶片门小于分辨率门?文档上是超过。】

  • 拉伸 (Stretching):Game 视图渲染完整的摄像机图像,将其水平或垂直拉伸以适应宽高比。

要在 Scene 视图中查看这些门,并查看它们如何相互适应,请选择摄像机并查看其视锥体。分辨率门是摄像机远裁剪面。胶片门是位于视锥体底部的第二个矩形。

4.2.1 Gate Fit 模式

        选择的 Gate Fit 模式决定了 Unity 如何调整分辨率门的大小(因而调整摄像机的视锥体)。胶片门始终保持相同大小。以下部分提供了关于每种 Gate Fit 模式的更多详细信息。

4.2.1.1 Vertical

        Gate Fit 设置为 Vertical 时,Unity 让分辨率门适应胶片门的高度(Y 轴)。对传感器宽度 (Sensor Size > X) 进行的任何更改都不会影响渲染的图像(分辨率门)。

        如果传感器宽高比大于 Game 视图宽高比,Unity 会在两侧裁剪渲染的图像(PS:丢弃了两侧的内容):

如果传感器宽高比小于 Game 视图宽高比,Unity 会在两侧对渲染的图像进行过扫描(PS:两侧绿色是胶片门没有覆盖的区域,进行过扫描):

4.2.1.2 Horizontal

        Gate Fit 设置为 Horizontal 时,Unity 让分辨率门适应胶片门的宽度(X 轴)。对传感器高度 (Sensor Size > Y) 进行的任何更改都不会影响渲染的图像。(基本和Vertical一样,只是一个适应Y轴,一个适应X轴)

        如果传感器宽高比大于 Game 视图宽高比,Unity 会在顶部和底部对渲染的图像进行过扫描:

如果传感器宽高比小于 Game 视图宽高比,则会在顶部和底部裁剪渲染的图像。

4.2.1.3 None

        Gate Fit 设置为 None 时,Unity 让分辨率门适应胶片门的宽度和高度(X 轴和 Y 轴)。Unity 会拉伸渲染的图像以适应 Game 视图宽高比,即会以胶片门的图像为基准,然后拉伸到分辨率门的尺寸。

如图所示,两个都是None模式下的经过适应拉伸的,右下角显示的小窗口就是分辨率门的尺寸,可以看到左侧图像被拉宽了,右侧图像被拉窄了,都是基于胶片门图像来拉伸到分辨率门的尺寸的。另外,此时远裁剪面就是胶片门。

4.2.1.4 Fill 和 Overscan

Gate Fit 设置为 FillOverscan 时,Unity 根据分辨率门和胶片门的宽高比,自动进行垂直或水平适应。

  • Fill 让分辨率门适应胶片门的较小轴,并裁剪摄像机图像的其余部分。(这种模式下,胶片门>=分辨率门)

  • Overscan 让分辨率门适应胶片门的较大轴,并对摄像机图像边界以外的区域进行过扫描。(这种模式下,胶片门<=分辨率门)

PS:具体可以自己动手试试看看,这里说小、大轴说得太模糊了,可以理解为分辨率门匹配两轴,门小时匹配的轴是小轴,门大时匹配的轴是大轴。

5 摄像机和深度纹理

        相机可以生成深度(depth)、深度+法线(normals)或运动矢量(vector)纹理。这是一个简化的G-buffer纹理,可以用于后期处理效果或实现自定义光照模型。这些主要用于效果,例如,后期处理效果经常使用深度信息。

        深度纹理中的像素值在0 ~ 1之间,呈非线性分布。精度通常为32位或16位,具体取决于所使用的配置和平台。当从深度纹理读取时,返回0到1范围内的高精度值。如果我们需要获得与Camera的距离,或者其他0-1的线性值,请使用helper macros手动计算。

        大多数现代硬件和图形 API 都支持深度纹理。下面列出了特殊要求:Direct3D 11+ (Windows), opengl3 + (Mac/Linux), Metal (iOS)和流行的控制台支持深度纹理。

        可使用脚本中的Camera.depthTextureMode变量来启用摄像机的深度纹理模式。还可以使用Shader Replacement功能来自行构建类似纹理。

有三种可能的深度纹理模式:

  • DepthTextureMode.Depth:深度纹理。

  • DepthTextureMode.DepthNormals: 深度和视图空间法线打包成一个纹理。

  • DepthTextureMode.MotionVectors:当前帧的每个屏幕纹理像素(texel)的每像素屏幕空间运动。打包到RG16 纹理中。

这些是标志(flag),因此可以指定上述纹理的任何组合。

5.1 DepthTextureMode.Depth 纹理

        此模式将构建一个屏幕大小的深度纹理。

        渲染深度纹理时使用的着色器通道与用于阴影投射物渲染的着色器通道相同(ShadowCaster 通道类型)。因此,通过扩展,如果着色器不支持阴影投射(即在着色器或任何后备着色器中没有阴影投射物通道),则使用该着色器的对象将不会显示在深度纹理中。

  • 让着色器 fallback 到另一个具有阴影投射通道的着色器,或者

  • 如果我们在使用 surface shaders,那么添加 addshadow 指令也能让它们生成阴影通道。

请注意,仅“不透明”对象(这些对象的材质和着色器设置为使用小于等于 2500 的 render queue )会渲染到深度纹理中。

5.2 DepthTextureMode.DepthNormals 纹理

        这将构建屏幕大小的 32 位(8 位/通道)纹理,其中视图空间法线编码到 R&G 通道中,而深度编码到 B&A 通道中。法线使用立体投影(Stereographic projection)进行编码,深度是打包到两个 8 位通道中的 16 位值。

        UnityCG.cginc include文件具有一个 helper 函数 DecodeDepthNormal,可从编码的像素值中解码深度和法线。返回的深度在 0 到 1 范围内。

        关于如何使用深度和法线纹理的例子,请参考 Replacing shaders at runtime 或在 Post-processing and full-screen effects中替换环境遮挡(Ambient Occlusion)。

5.3 DepthTextureMode.MotionVectors 纹理

        这将构建屏幕大小的 RG16(16 位浮点/通道)纹理,其中屏幕空间像素运动编码到 R&G 通道中。像素运动在屏幕 UV 空间中进行编码。

        当从这个纹理运动中采样时,从编码像素返回的范围是[-1,1]。这将是上一帧到当前帧的UV偏移量。

5.4 提示和技巧

        摄像机的Inspector面板会指示摄像机何时渲染深度或深度+法线纹理。

        从摄像机请求深度纹理的方式(Camera.depthTextureMode)可能意味着在禁用需要深度纹理的效果后,摄像机可能仍会继续渲染深度纹理。若摄像机上存在多个效果,其中每个效果都需要深度纹理,则无法在禁用单个效果的情况下自动禁用深度纹理渲染。

        在实现复杂的着色器或图像效果时,切记平台之间的渲染差异。尤其是,在图像效果中使用深度纹理通常需要对 Direct3D和抗锯齿进行特殊处理。

        在某些情况下,深度纹理可能直接来自本机 Z 缓冲区。如果在深度纹理中看到了瑕疵,请确保使用该纹理的着色器未写入 Z 缓冲区(使用 ZWrite Off)。

5.5 着色器变量

        深度纹理可用于在着色器中作为全局着色器属性进行采样。通过声明名为 _CameraDepthTexture 的采样器,我们将能够为摄像机采样主深度纹理。

        _CameraDepthTexture 始终引用摄像机的主深度纹理。相比之下,我们可以使用 _LastCameraDepthTexture 来引用任何摄像机渲染的最后一个深度纹理。例如,如果我们使用辅助摄像机渲染脚本中的半分辨率深度纹理并希望将其提供给后期处理着色器,这种做法可能很有用。

        运动矢量纹理(启用时)在着色器中可用作全局着色器属性。通过声明名为“_CameraMotionVectorsTexture”的采样器,我们可以为当前执行渲染的摄像机采样纹理。

5.6 部分内部原理

        深度纹理可以直接来自实际的深度缓冲(depth buffer),或者在单独的通道中渲染,这取决于所使用的渲染路径和硬件。通常当使用延迟着色渲染路径(Deferred Shading rendering path)时,深度纹理是“免费的”,因为它们是G-buffer渲染的产物。

        当 DepthNormals 纹理在单独通道中渲染时,此过程通过 Shader Replacement 完成。因此,务必在我们的着色器中设置正确的“RenderType”标签。

        启用时,MotionVectors 纹理始终来自额外的渲染通道。Unity 会将移动的游戏对象渲染到此缓冲区中,并构建从最后一帧到当前帧的运动。【?官方要不要你自己看看在说什么,“额外”、“此缓冲区”?】

6 相机技巧(Camera Tricks)

        当设计某些视觉效果或与场景中的物体交互时,理解相机是如何工作的是有用的。本节将解释相机视角的本质以及它如何能够增强游戏玩法。

6.1 了解视椎体(View Frustum)

        视锥体一词表示看起来像顶部切割后平行于底部的金字塔的实体形状。这是透视摄像机可以看到和渲染的区域的形状。以下思维实验应该有助于解释为什么会这样。

        想象一下,将一根直杆(例如扫帚柄或一支铅笔)正对着摄像机,然后拍照。如果杆垂直于摄像机镜头保持在图片中心位置,那么只有其一端可在图片上显示为圆圈;所有其他部分都会被遮挡。如果将杆向上移动,下侧将开始变得可见,但可通过向上倾斜杆再次将其隐藏。如果继续向上移动杆并进一步将其向上倾斜,则圆形末端最终将到达图片的顶部边缘。此时,在世界空间中由此杆跟踪的界线上方(就是此杆往上)的任何对象在图片上都不可见。

        很容易将杆向左、向右或向下(或者水平和垂直方向的任何组合方式)移动或旋转。“隐藏”杆的角度仅取决于它在两个轴上距离屏幕中心的距离。

        这一思维实验的意义在于,摄像机图像中的任何一点实际上都对应于世界空间中的一条线,在图像中只能看到这条线上的一个点。这条线上该位置背后的一切都会被遮挡。

        图像的外边缘由对应于图像四个角的发散线界定。如果这些线向后追踪到摄像机,它们最终会聚合到同一个点。在 Unity 中,此点恰好位于摄像机的Transform位置,也称为透视中心。从屏幕顶部和底部中心聚合到透视中心处的线所形成的角度称为视野(通常缩写为 FOV)。

        如上所述,任何落在图像边缘的发散线之外的物体对摄像机而言均不可见,但是针对摄像机渲染的内容还有另外两个限制。近剪裁面(Near clipping plane)和远剪裁面(Far clipping plane)平行于摄像机的 XY 平面,两者沿中心线相隔一定的距离。比近剪裁面更靠近摄像机的任何对象以及比远裁剪面更远离摄像机的任何对象都不会被渲染。

图像四角的发散线以及两个裁剪面定义了截头金字塔:视锥体。

6.1.1 远裁剪平面和浮点数学(远物体闪烁问题)

        物体、光线和阴影如果距离较远,可能会闪烁。出现闪烁是因为距离太大,无法用浮点数学精确计算位置。在每一帧中,物体、光线或阴影的位置都略有不同,因此它们会移进或移出视锥

使用以下方法之一尽量减少闪烁:

  • 减少Camera组件中的远裁剪平面距离,以避免物体的距离变得太大而无法精确计算。

  • 让场景中的所有东西都变小,以减少整个场景的距离。

Unity以世界空间位置作为参考点来计算光线和阴影,例如3D场景中的0,0,0。闪烁发生在光和影远离世界空间位置的时候。为了最小化闪烁,我们可以启用相机相对剔除(camera-relative culling),以至于Unity使用相机位置作为阴影计算的相对位置。参见Culling settings in Graphics settings

6.2 距摄像机一定距离的视锥体的大小

        距摄像机一定距离的视锥体的横截面将在世界空间中定义一个构成可见区域的矩形。此形状有时可用于计算此矩形在给定距离处的大小,或者查找矩形为给定大小时所处的距离。例如,如果移动的摄像机需要始终将对象(例如玩家)完全保持在镜头中,则不得过于接近摄像机以免该对象的一部分被截断。

        可使用以下公式来计算视锥体在给定距离处的高度(均以世界单位表示):

var frustumHeight = 2.0f * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);

…还可反转该过程以计算获得指定视锥体高度所需的距离:

var distance = frustumHeight * 0.5f / Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);

当高度和距离已知时,也可以计算 FOV(视野)角度:

var cameraFieldOfView = 2.0f * Mathf.Atan(frustumHeight * 0.5f / distance) * Mathf.Rad2Deg;

视椎体高度、宽度的相互计算:

var frustumWidth = frustumHeight * camera.aspect;
var frustumHeight = frustumWidth / camera.aspect;

6.3 摄像机射线

        了解视椎体部分说明了摄像机视图中的任何一点都对应于世界空间中的一条线。有时使用这条线的数学表示形式是有用的,Unity 能够以 Ray 对象的形式提供该表示形式。Ray 始终对应于视图中的一个点,因此 Camera 类提供 ScreenPointToRayViewportPointToRay 两个函数。两者之间的区别在于 ScreenPointToRay 期望以像素坐标的形式提供该点,而 ViewportPointToRay 则接受 0..1 范围内的标准化坐标(其中 0 表示视图的左下角,1 表示右上角)。这些函数中的每一个函数都返回由一个原点和一个矢量(该矢量显示从该原点出发的线条方向)组成的 Ray。射线 (Ray) 源自近裁剪面而不是摄像机 (Camera) 的 transform.position 点。

6.3.1 射线投射

        来自摄像机的射线最常见的用途是将射线投射( raycast )到场景中。射线投射从原点沿着射线方向发送假想的“激光束”,直至命中场景中的碰撞体。随后会返回有关该对象和 RaycastHit 对象内的投射命中点的信息。这是一种基于对象在屏幕上的图像来定位对象的非常有用的方法。例如,可使用以下代码确定鼠标位置处的对象:

using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public Camera camera;

    void Start(){
        RaycastHit hit;
        Ray ray = camera.ScreenPointToRay(Input.mousePosition);
        
        if (Physics.Raycast(ray, out hit)) {
            Transform objectHit = hit.transform;
            
            // 对射线投射命中的对象执行一些操作。
        }
    }
}

6.3.2 沿着射线移动摄像机

        获取对应于屏幕位置的射线再沿着该射线移动摄像机有时很有用。例如,需要允许用户使用鼠标选择对象,然后放大该对象,同时将其“固定”到鼠标下的相同屏幕位置(这种操作可能很有用,例如,当摄像机正在查看战术地图时)。执行此操作的代码非常简单:

using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public bool zooming;
    public float zoomSpeed;
    public Camera camera;

    void Update() {
        if (zooming) {
            Ray ray = camera.ScreenPointToRay(Input.mousePosition);
            float zoomDistance = zoomSpeed * Input.GetAxis("Vertical") * Time.deltaTime;
            //使用射线方向来计算
            camera.transform.Translate(ray.direction * zoomDistance, Space.World);
        }
    }
}

6.4 使用斜视锥体

        默认情况下,视锥体围绕摄像机的中心线对称安放,但这并不是必须的。视锥体可设置为“倾斜的”,即一侧与中心线的角度小于对侧与中心线的角度。

        这种做法使得图像一侧的透视看起来更加紧凑,给人的印象是观察者非常靠近在该边缘处可见的对象。此功能的一个用法示例是赛车游戏;如果视锥体在其底部边缘变平,则从观察者的角度看起来会更贴近道路,凸显了速度感。如图:

        在内置渲染管线中,使用斜视锥体的摄像机只能使用前向渲染路径。如果摄像机设置为使用延迟着色 (Deferred Shading) 渲染路径并使视锥体倾斜,Unity 会强制该摄像机使用前向渲染路径。

6.4.1 设置视锥体倾斜度

        虽然 Camera 组件没有专门用于设置视锥体倾斜度的功能,但可以通过启用摄像机的 Physical Camera 属性并应用 Lens Shift 设置,或通过添加脚本来更改摄像机的投影矩阵,从而实现这样的功能。

使用 Lens Shift 设置视锥体倾斜度

        这个在上面的“4 使用物理相机”部分已经有过介绍了,可以去看。

使用脚本设置视锥体倾斜度

        以下脚本示例显示了如何通过更改摄像机的投影矩阵来快速实现斜视锥体。请注意,仅当游戏运行播放模式时才能看到脚本的效果。

using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    void SetObliqueness(float horizObl, float vertObl) {
        Matrix4x4 mat  = Camera.main.projectionMatrix;
        mat[0, 2] = horizObl;
        mat[1, 2] = vertObl;
        Camera.main.projectionMatrix = mat;
    }
} 

我们不需要了解投影矩阵是如何工作的。horizObl 和 vertObl 值分别设置水平和垂直倾斜量。值为零表示无倾斜。正值使视锥体向右或向上移动(倾斜),从而使左边或底边变平。负值使视锥体向左或向下移动(倾斜),从而使视锥体的右边或顶边变平。如果将此脚本添加到摄像机并在游戏运行时将游戏切换到 Scene 视图,则可以直接看到效果。在检视面板中改变 horizObl 和 vertObl 的值时,摄像机视锥体的线框会发生变化。任一变量中的值为 1 或 –1 表示视锥体的一侧与中心线完全齐平。此范围之外的值也是允许使用的,但通常没有必要。

7 遮挡剔除

        “遮挡剔除”过程可防止 Unity 为那些被其他游戏对象完全挡住(遮挡)的游戏对象执行渲染计算。

        摄像机在每一帧中执行剔除操作,这些操作会检查场景中的渲染器,并排除(剔除)那些不需要绘制的渲染器。默认情况下,摄像机执行视锥体剔除,这一过程将排除所有不在摄像机视锥体范围内的渲染器。但是,视锥体剔除不会检查渲染器是否被其他游戏对象遮挡,因此 Unity 仍会浪费 CPU 和 GPU 时间进行在最终帧中不可见的渲染器的渲染操作。遮挡剔除将阻止 Unity 执行这些徒劳的操作。

何时使用遮挡剔除

要确定遮挡剔除是否有可能改善项目的运行时性能,请考虑以下事项:

  • 防止无意义的渲染操作可以节省 CPU 和 GPU 时间。Unity 的内置遮挡剔除在 CPU 上执行运行时计算,这可能会抵消其节省的 CPU 时间。因此,当项目因过度绘制而具有 GPU 密集型特征时,遮挡剔除最有可能提高性能。

  • Unity 在运行时将遮挡剔除数据加载到内存中。必须确保有足够的内存来加载此数据。

  • 当场景中一些界限明确的小区域被实体游戏对象彼此隔开时,遮挡剔除的效果最好。一个常见的例子是通过走廊连接的房间。

  • 可以使用遮挡剔除来遮挡动态游戏对象,但动态游戏对象不能遮挡其他游戏对象。如果项目会在运行时生成场景几何体,则 Unity 的内置遮挡剔除不适用于该项目。

遮挡剔除的工作原理

        遮挡剔除会在 Unity Editor 中生成有关场景的数据,然后在运行时使用该数据来确定摄像机可以看到的内容。这种生成数据的过程称为烘焙。

        在对遮挡剔除数据进行烘焙时,Unity 将场景划分为多个单元,并生成描述单元内几何体以及相邻单元之间可见性的数据。然后,Unity 尽可能合并单元,以减小生成的数据的大小。要配置烘焙过程,可以在 Occlusion Culling 窗口中更改参数,并在场景中使用遮挡区域

        在运行时,Unity 会将这些烘焙的数据加载到内存中,并且对于每个启用了 Occlusion Culling 属性的摄像机,将会对数据执行查询以确定该摄像机可以看到的内容。请注意,启用遮挡剔除后,摄像机将执行视锥体剔除和遮挡剔除。

其他

        Unity 使用 Umbra 库来执行遮挡剔除。有关详细介绍 Umbra 的文章的链接,请参阅“遮挡剔除其他资源小节”

7.1 遮挡剔除使用

7.1.1 设置场景

        在开始之前,请确定场景中所有要设定为静态遮挡物 (Static Occluder)(这些游戏对象不会移动,但会阻挡后面的游戏对象)和静态被遮挡物 (Static Occludee)(这些游戏对象不会移动,但会被静态遮挡物遮挡)。一个游戏对象可以同时是静态遮挡物和静态被遮挡物。

静态遮挡物

        适合作为静态遮挡物的游戏对象包括中型到大型的实体游戏对象(例如墙壁或建筑物)。要被设定为静态遮挡物,游戏对象必须满足以下条件:

  • 对象身上有 Terrain 或 Mesh Renderer 组。

  • 不透明。

  • 在程序运行时,不移动。

请注意,如果使用了 LOD 组,则 Unity 会使用静态遮挡物的基础细节级别游戏对象 (LOD0) 来确定要遮挡的对象。如果游戏对象的轮廓在 LOD0 和其他 LOD 级别之间变化很大,这个游戏对象可能不适合设定为静态遮挡物。

静态被遮挡物

        任何可能在运行时被遮挡的游戏对象都适合设定为静态被遮挡物,包括小的或透明的游戏对象。要被设定为静态被遮挡物,游戏对象必须满足以下条件:

  • 具有任何类型的 Renderer 组件。

  • 在程序运行时,不移动。

设置场景

        确定了要设定为静态遮挡物和静态被遮挡物的游戏对象之后,便可以设置场景。主要是设置物体为静态遮挡物、静态被遮挡物,以及相机开启遮挡剔除属性:

设置遮挡物或被遮挡物:

设置相机:

7.1.2 烘焙数据

操作如下:

  1. 在顶部菜单中,选择 Window > Rendering > Occlusion Culling 以打开Occlusion Culling窗口。

  2. 选择Bake选项卡。

  3. 在 Inspector 窗口的右下角,按 Bake 按钮。Unity 会生成遮挡剔除数据,将数据另存为项目中的资源,并将该资源与当前场景关联。

7.1.3 结果可视化

        在设置好以上内容后,接下来就是看遮挡剔除的效果。

        首先确保 Occlusion Culling 窗口和 Scene 视图均为可见状态。当 Occlusion Culling 窗口可见时,Unity 在 Scene 视图中显示遮挡剔除数据和 Occlusion Culling 弹出窗口。

然后,在场景中选择一个摄像机,选中Occlusion Culling窗口中的Visualization选项卡。此时就可以看到小球已经消失了:

一个是被遮挡而消失,一个是在相机视椎体外而消失。注意,若遮挡物较小,即使遮挡住视野也可能无法隐藏背后的被遮挡物。后续我们可以在 Occlusion Culling 弹出窗口配置可视化设置。

        如有需要调整烘焙,在 Occlusion Culling 窗口Bake选项卡中调整烘焙设置,然后重新烘焙即可。

        我们移动相机,调整到可以看到小球的位置,就可以将消失的小球重新显示,这里就不演示了。

        如果使用的是内置渲染管线,则可以使用 Scene 视图模式 Overdraw 来查看正在发生的过度绘制数量,并可以使用 Game 视图中的 Stats 面板来查看 Unity 正在渲染的三角形、顶点和批处理数量。

7.2 对动态游戏对象使用遮挡剔除

        静态游戏对象和动态游戏对象在 Unity 的遮挡剔除系统中的行为不同:

  • Unity 可以将静态游戏对象作为静态遮挡物和/或静态被遮挡物烘焙到遮挡剔除数据中。

  • Unity 无法将动态游戏对象烘焙到遮挡剔除数据中。动态游戏对象可以在运行时充当被遮挡物,而不能充当遮挡物。

        要确定动态游戏对象是否充当被遮挡物,可以在任何类型的渲染器组件上设置 Dynamic Occlusion 属性。启用 Dynamic Occlusion 后,渲染器在摄像机的视图中被静态遮挡物 (Static Occluder) 阻挡时,Unity 会剔除渲染器。禁用 Dynamic Occlusion 后,渲染器在摄像机的视图中被静态遮挡物 (Static Occluder) 阻挡时,Unity 不会剔除渲染器。如Mesh Renderer组件:

        默认情况下会启用 Dynamic Occlusion。为了获得特定的效果,例如在墙后的角色周围绘制轮廓,可能需要禁用 Dynamic Occlusion。

        如果确定 Unity 绝对不应该将遮挡剔除应用于特定的游戏对象,则可以禁用 Dynamic Occlusion 以减少运行时计算并降低 CPU 使用率。虽然这些计算对每个游戏对象的影响很小,但如果规模足够大,减少这些计算可能有利于提高性能。(PS:牺牲GPU,利于CPU)

7.3 遮挡剔除和场景加载

        在运行时,无论打开了多少个场景,Unity 一次仅加载一个遮挡剔除数据资源。因此,必须根据计划是一次加载一个场景还是一次加载多个场景,从而以不同方式准备遮挡剔除数据。

7.3.1 一次加载一个场景

        如果使用 LoadSceneMode.Single 一次加载一个场景,则必须分别按以下方式烘焙每个场景的遮挡剔除数据:

  1. 在 Unity Editor 中,打开需要烘焙遮挡剔除数据的单个场景。

  2. 烘焙场景的数据。Unity 会生成数据并将保存为 Assets/[场景名称]/OcclusionCullingData.asset。Unity 会将对此资源的引用添加到打开的场景中。

  3. 保存场景。

  4. 对每个场景重复步骤 1、2 和 3。

        在运行时,按以下方式加载场景:

  1. 将场景加载为项目的默认场景,或者使用 LoadSceneMode.Single 来加载场景。如果之前有活动场景,则 Unity 会卸载活动场景以及活动场景的遮挡数据资源(如果有)。Unity 随后加载我们的场景及其遮挡数据资源。

7.3.2 一次加载多个场景

        如果使用 LoadSceneMode.Additive 一次加载多个场景,则必须按以下方式一起烘焙这些场景的数据:

  1. 在 Unity Editor 中,打开 Unity 将在运行时加载的场景组的第一个场景。该场景将变为活动场景。

  2. 以累加方式打开组中的其他场景,使所有这些场景同时在 Unity Editor 中打开。

  3. 烘焙所有场景的数据。Unity 会生成所有打开的场景的数据,并将其保存为 Assets/[活动场景名称]/OcclusionCullingData.asset。Unity 会将对此资源的引用添加到所有打开的场景中。

  4. 保存场景。

        在运行时,按以下方式加载场景:

  1. 加载场景组的第一个场景作为项目的默认场景,或使用 LoadSceneMode.Single 来加载场景。如果之前有活动场景,则 Unity 会卸载活动场景以及活动场景的遮挡数据资源(如果有)。Unity 随后加载我们的场景以及共享的遮挡数据资源。

  2. 根据需要,使用 LoadSceneMode.Additive 加载其他场景。如果 Unity 发现以累加方式加载的场景的遮挡数据与活动场景的遮挡数据相同,则遮挡剔除将按预期工作。

        请注意,共享的遮挡数据资源的文件较大。使用较大的遮挡数据资源不会在运行时对 CPU 产生任何其他影响。

7.4 遮挡区域

        使用 Occlusion Area 组件可以定义遮挡剔除系统中的视图体积。视图体积是在运行时摄像机可能处于的场景区域。在烘焙时,Unity 在视图体积内生成更高精度的数据。在运行时,当摄像机位于视图体积内的时候,Unity 进行更高精度的计算。

        如果尚未在场景中定义任何视图体积,Unity 将在烘焙时创建一个视图体积,其中包含标记为 Occluder Static 或 Occludee Static 的所有场景几何体。在大型或复杂场景中,这可能导致不必要的大量数据、漫长的烘焙时间以及资源密集的运行时计算。为了避免发生这种情况,请将遮挡区域 (Occlusion Areas) 放置在场景中,从而定义摄像机可能处于的区域的视图体积。

使用遮挡区域 (Occlusion Area) 组件来定义视图体积

  1. Occlusion Area 组件添加到场景中的空游戏对象。

  2. 在 Inspector 窗口中,配置 Size 属性,使包围体积涵盖所需区域。

  3. 在 Inspector 窗口中,启用 Is View Volume

  • Size:设置遮挡区域 (Occlusion Area) 的大小。

  • Center:设置遮挡区域 (Occlusion Area) 的中心。默认情况下,此设置为 0,0,0,位于盒体的中心。

  • Is View Volume:如果启用此属性,遮挡区域 (Occlusion Area) 将定义视图体积。如果禁用此属性,遮挡区域 (Occlusion Area) 不会定义视图体积。必须启用此属性才能使遮挡区域 (Occlusion Area) 生效。

7.4 遮挡入口

        遮挡入口 (Occlusion Portal) 可以是打开或关闭状态。遮挡入口关闭时,它将遮挡其他游戏对象。遮挡入口打开时,它不会遮挡其他游戏对象。

        如果场景中有一个处于打开和关闭状态的游戏对象(例如门),可以创建一个在遮挡剔除系统中表示该游戏对象的遮挡入口。然后,可以根据该游戏对象的状态来设置遮挡入口的打开状态。无需将 Occlusion Portal 组件置于其表示的游戏对象上,即放在空物体上,其就能起到打开与关闭的作用(类似一个无形的墙壁)。

7.4.1 在场景中设置遮挡入口

  1. 选择场景中的合适游戏对象来充当遮挡入口。适合作为遮挡入口的游戏对象包括中型到大型的实体游戏对象(例如门)。

  2. 确保未将此游戏对象标记为 Occluder Static 或 Occludee Static。

  3. Occlusion Portal 组件添加到游戏对象。

  4. 烘焙场景的遮挡数据。

  5. 确保 Occlusion Culling 窗口(Visualization选项卡)、Inspector 面板和 Scene 视图均为可见状态。

  6. 在 Scene 视图中,将摄像机移至遮挡入口正前方的位置。

  7. 选择具有 Occlusion Portal 组件的游戏对象。

  8. 在 Inspector 窗口中,开启和关闭 Occlusion Portal 组件的 Open 属性。在 Scene 视图中,观察遮挡剔除的差异。

        如图,这里我把中间的墙壁Cube设置为非静态,并添加了Occlusion Portal组件,然后烘焙,之后通过控制Occlusion Portal组件的Open属性来看效果:

可以看到在打开时,对象是被显示了,在关闭时,对象是被隐藏了。需要注意,这里我是挂载的Cube上,此时Cube是非静态的,已经不具备遮挡效果了,全程都是Occlusion Portal在发挥作用,其遮挡,其不遮挡,换言之,这里的Cube换成一个空物体也可以实现此效果,但同样的,就跟前面我们调整Cube大小一样,需要调整Occlusion Portal的尺寸足够大才能有遮挡效果。

        Occlusion Portal组件在图也能看到,就三个属性:

  • Open: 如果启用此属性,遮挡入口将打开并且不会遮挡渲染器。如果禁用此属性,遮挡入口将关闭并且会遮挡渲染器。

  • Center:设置遮挡入口的中心。默认值为 0,0,0。

  • Size:定义遮挡入口的大小。

7.4.2 在运行时打开和关闭遮挡入口

        使用脚本将 Occlusion Portal 组件的Open属性设置为所需状态。代码如:

void OpenDoor() {
     // 通过组件切换为打开状态,这样Unity就会渲染它后面的游戏对象。
    myOcclusionPortal.open = true;
    
    // 后续操作。比如:由于上面已经打开了遮挡入口,后面的对象被渲染了,这时候我们就可以在这里执行“开门动画”。
    …
}

7.5 Occlusion Culling窗口

        打开 Occlusion Culling 窗口的方法是导航到顶部菜单,然后选择 Window > Rendering > Occlusion Culling。Occlusion Culling 窗口有 3 个选项卡:ObjectBakeVisualization。除此之外,当 Occlusion Culling 窗口和 Scene 视图均可见时,Scene 视图中将显示 Occlusion Culling 弹出窗口(在右下角)。Occlusion Culling 窗口底部是 BakeClear 按钮,单击 Bake 按钮可烘焙遮挡剔除数据,单击 Clear 按钮可删除之前烘焙的数据。

7.5.1 Object选项卡

        在 Object 选项卡中,可以单击 All、Renderers 和 Occlusion Areas 按钮以筛选 Hierarchy 窗口的内容。

        在 Hierarchy 窗口或 Scene 视图中选择一个渲染器,即可在 Occlusion Culling 窗口中查看和更改渲染器的遮挡剔除设置。

        在 Hierarchy 窗口或 Scene 视图中选择一个遮挡区域,然后在 Occlusion Culling 窗口中查看和更改遮挡区域的 Is View Volume 设置。

        在 Hierarchy 窗口或 Scene 视图中什么都不选择时,激活Occlusion Areas按钮,可以单击 Create New Occlusion Area 以便在场景中创建新的遮挡区域。

7.5.2 Bake选项卡

        在 Bake 选项卡中,可以微调遮挡剔除烘焙过程的参数。请通过配置这些设置在烘焙时间、运行时数据大小和可视化结果之间找到平衡。Set Default Parameters 按钮可将参数重置为默认值。各属性:

  • Smallest Occluder:可以遮挡其他游戏对象的最小游戏对象的大小(以米为单位)。通常,要使文件最小且烘焙时间最短,应选择在场景中产生良好结果的最大值。

  • Smallest Hole: 摄像机可以看到的最小间隙的直径(以米为单位)。通常,要使文件最小且烘焙时间最短,应选择在场景中产生良好结果的最大值。

  • Backface Threshold:如果需要减小烘焙数据的大小,Unity 可以在烘焙时对场景进行采样,并排除场景中可见遮挡物几何体所含背面超过给定百分比的部分。背面百分比很高的区域可能在几何体的下方或内部,因此不太可能是在运行时摄像机所在的某个位置,大概率相机永远看不到。默认值 100 表示绝不会从数据中删除区域。值越小,产生的文件就越小,但可能会导致视觉失真。【PS:背面百分比应该是指相对整体表面来说的。而且这里的背面应该是指相机看不到的面。】

        更多配置技巧,见“遮挡剔除其他资源小节”中链接的文章。

7.5.3 Visualization 选项卡

        当 Visualization 选项卡可见时,如果在 Scene 视图或 Hierarchy 窗口选择一个摄像机,则 Unity 将更新 Scene 视图,从所选摄像机的视角显示遮挡剔除的效果。可以使用 Scene 视图中的 Occlusion Culling 弹出窗口来配置可视化设置。

7.5.4 Occlusion Culling弹出窗口

        同时让Scene窗口、Occlusion Culling窗口可见,即可在Scene中出现弹出窗口。

        在烘焙数据之前,弹出窗口如下:

        在烘焙数据之后,选中Object、Bake选项卡,依旧如上图所示。若选中Visualization选项卡,弹出窗口如下:

实际,下拉框代表两种模式:Edit、Visualize。其与我们上面说的选项卡选择是同步变化的,可以直接在弹出窗口中修改,比如这里可以再修改为Edit,这时Occlusion Culling窗口就会自动切到Object选项卡,再次修改为Visualize,自动切到Visualization选项卡。

Edit 模式

  • View Volumes:启用此选项后,Scene 视图将包含蓝线,这些蓝线显示遮挡剔除数据中的单元格。单元格大小受 Smallest Occluder 设置的影响:值越小,产生的单元格越多且越小,从而使精度提高并且文件增大。

Visualize 模式

        此预览遮挡剔除的结果。如果选择了某个摄像机,则预览与该摄像机相关。否则,预览与我们在 Visualize 模式下选择的最后一个摄像机相关

  • Camera Volumes:启用此选项后,我们会看到黄线,这些黄线指示 Unity 为其生成遮挡剔除数据的场景区域。这取决于场景几何体以及在场景中使用 Occlusion Areas 选项定义的任何视图体积 (View Volumes)。当摄像机在黄线之外时,Unity 不会执行遮挡剔除。我们还可以看到灰线,这些灰线指示摄像机当前位置所对应的遮挡剔除数据中的单元格以及当前单元格中的细分。Smallest Hole 设置定义了单元格内细分的最小大小:值越小,每个单元格产生的细分越多且越小,从而使精度提高并且文件增大。

  • Visibility Lines:启用此选项后,我们会看到绿线,这些绿线指示当前选择的摄像机可以看到的内容。

  • Portals: 启用此选项后,我们可以看到一些线代表遮挡数据中单元格之间的连接。当前可见的入口是当前所选摄像机可以看到的入口。

7.6 遮挡剔除其他资源

        Unity 使用 Umbra 库来执行遮挡剔除。在以下文章中可以找到有关 Umbra 的更多信息,包括有关烘焙过程、遮挡剔除数据内部的数据结构以及 Umbra 执行的运行时操作的信息:

        Umbra 在 Gamasutra 上发表的这篇文章详细介绍了 Umbra 的工作方式:Next Generation Occlusion Culling by Umbra

        此 Unity 博客系列包含有关优化 Umbra 的指南。这个系列与 Unity 的旧版本相关,此后用户界面发生了变化,但核心原理仍然相同:

8 CollingGroup API

        CullingGroup 提供一种将我们自己的系统融合到 Unity 剔除和 LOD 管线中的方法。这可用于许多目的;例如:

  • 模拟一群人,同时只为现在实际可见的角色提供完整的游戏对象

  • 构建由 Graphics.DrawProcedural 驱动的 GPU 粒子系统,但是跳过对墙背后的粒子系统的渲染

  • 跟踪不在摄像机视野范围内的生成点,以便在生成敌人时不让玩家看到他们“弹入”视图

  • 将角色从近处的全质量动画和 AI 计算切换到远处更低质量、更低成本的行为

  • 在场景中设置 10,000 个标记点,并在玩家进入其中任何标记点的 1m 范围内时有效发现这一状态

API 的工作原理是让我们提供一系列包围球体。然后计算这些球体相对于特定摄像机的可见性,以及可视为 LOD 级别号的“距离带”值。

PS:注意,这里的距离等级使我们给定阈值,然后计算距离在阈值之间哪个范围,给出距离等级。不是LOD组件。需要使用SetBoundingDistances、SetDistanceReferencePoint函数实现。

8.1 如何使用

8.1.1 创建与释放 Group

        没有用于处理 CullingGroup 的组件或可视化工具,只能通过脚本访问它们。可使用“new”运算符来构造 CullingGroup:

CullingGroup group = new CullingGroup();

要让CullingGroup执行能见度 和/或 距离计算,请指定它应该使用的相机:

group.targetCamera = Camera.main;

使用球体的位置和半径来创建并填充 BoundingSphere 结构数组,并将其与实际位于数组中的球体数一起传递给 SetBoundingSpheres。球体的数量不需要与数组的长度相同。Unity 建议创建一个足够大的数组来保存我们一次拥有的最多球体(即使我们最初在数组中实际拥有的球体数量非常少)。如果使用较大的数组,我们可以根据需要添加或删除球体,而无需在运行时进行调整数组大小这样计算成本高昂的过程。

BoundingSphere[] spheres = new BoundingSphere[1000];//数组
spheres[0] = new BoundingSphere(Vector3.zero, 1f);//添加球体
group.SetBoundingSpheres(spheres);//设置给Group
group.SetBoundingSphereCount(1);//设置数量

此时,CullingGroup 将开始计算每帧单个球体的可见性。

要清理 CullingGroup 并释放它使用的所有内存,请通过标准的 .NET IDisposable 机制来处置 CullingGroup:

group.Dispose();
group = null;

PS:务必执行释放操作。测试中发现,若不执行释放操作,即使停止运行了,回调函数仍然可以在控制台输出内容。

8.1.2 通过 onStateChanged 回调来接收结果

        为响应球体而更改其可见性或距离状态的最有效方法是使用 onStateChanged 回调字段。将其设置为一个函数,该函数以 CullingGroupEvent 结构作为参数。对于已改变状态的每个球体,将在剔除完成后调用此函数。CullingGroupEvent 结构的成员会告诉我们球体的先前状态和新状态。(PS:变化后触发,根据参数判断是如何变化的。)

group.onStateChanged = StateChangedMethod;

private void StateChangedMethod(CullingGroupEvent evt)
{
    if(evt.hasBecomeVisible)//判断是否变为可见
        Debug.LogFormat("Sphere {0} has become visible!", evt.index);
    if(evt.hasBecomeInvisible)//判断是否变为不可见
        Debug.LogFormat("Sphere {0} has become invisible!", evt.index);
}

PS:距离等级的变化也会触发。

8.1.3 通过 CullingGroup 查询 API 来接收结果

        除了 onStateChanged 委托之外,CullingGroup 还提供一个 API,用于检索包围球体数组中任何球体的最新可见性和距离结果。要检查单个球体的状态,请使用 IsVisible 和 GetDistance 方法:

bool sphereIsVisible = group.IsVisible(0);
int sphereDistanceBand = group.GetDistance(0);

        要检查多个球体的状态,可使用 QueryIndices 方法。此方法将扫描连续范围的球体以查找与指定可见性或距离状态相匹配的球体。

// 分配一个数组来保存生成的球体索引 - 数组的大小决定每次调用检查的最大球体数
int[] resultIndices = new int[1000];
// 还要设置一个 int 来存储已放入数组的实际结果数
int numResults = 0;

// 查找所有可见的球体
numResults = group.QueryIndices(true, resultIndices, 0);
// 查找位于距离带 1 中的所有球体
numResults = group.QueryIndices(1, resultIndices, 0);
// 查找隐藏在距离带 2 中的所有球体,跳过前 100 个球体
numResults = group.QueryIndices(false, 2, resultIndices, 100);

请记住,仅在 CullingGroup 使用的摄像机实际执行剔除时,才更新查询 API 检索的信息。(PS:距离等级变化,API查询内容也会更新)

8.2 实践考虑

        在考虑如何将 CullingGroup 应用于项目时,请考虑 CullingGroup 设计的以下方面。

8.2.1 利用可见性

        CullingGroup 为其计算可见性的所有体积都由包围球体定义;实际上,由位置(球体中心)和半径值定义。出于性能原因,不支持其他包围形状。在实践中,这意味着我们将定义一个球体来完全包围希望剔除的对象。如果需要更紧密拟合,请考虑使用多个球体来覆盖对象的不同部分,并根据所有球体的可见性状态做出决定。

        为了评估可见性,CullingGroup 需要知道应该从哪个摄像机可见性开始计算。目前,单个 CullingGroup 仅支持单个摄像机。如果需要评估多个摄像机的可见性,应为每个摄像机使用一个 CullingGroup 并合并结果(PS:我的理解是将多个结果进行综合考虑)。

        CullingGroup 将仅基于视锥体剔除和静态遮挡剔除来计算可见性。它不会将动态对象视为潜在遮挡物。

8.2.2 利用距离

        CullingGroup 能够计算某个参考点(例如,摄像机或玩家的位置)与每个球体上最近点之间的距离。此距离值不会直接提供给我们,而是使用我们提供的一组阈值来量化,以便计算离散的“距离带”整数结果。目的是将这些距离带解读为“近距离”、“中距离”、“远距离”等。

        一个对象从一个区域移到另一个区域时,CullingGroup 将提供回调,让我们有机会进行某些操作,例如我们该对象的行为更改为 CPU 使用强度较低的操作。(还是onStateChanged回调)

        超出最后一个距离的任何球体都将被视为不可见,这使我们可以轻松构建一个剔除实现来完全停用非常远的对象。如果不想要此行为,只需将最终阈值设置为无限远的距离。【没找到API有提供这种隐藏,可能是指让用户自己实现这种远距离剔除效果】

        每个 CullingGroup 仅支持一个参考点。

        PS:关于距离部分需要额外的函数设置,如SetBoundingDistances、SetDistanceReferencePoint,不过官方并未提供相关示例代码。

8.2.3 可视化、距离等级测试

        由于官方没有距离等级的示例代码,这里就做了下整体实验:

        首先是创建了两个球,与相机的分布:

代码中在两个球的位置分别创建了球体,半径为1,0下标球体在原点处,1下标球体在左侧那个。

代码如下:

using UnityEngine;

public class test1 : MonoBehaviour
{
    //组
    CullingGroup group;

    void Start()
    {
        //创建组
        group = new CullingGroup();
        //设置相机
        group.targetCamera = Camera.main;

        //创建球体数组,放入两个球体,设置给组
        BoundingSphere[] spheres = new BoundingSphere[1000];//数组
        spheres[0] = new BoundingSphere(Vector3.zero, 1f);//添加球体
        spheres[1] = new BoundingSphere(new Vector3(-10f, 0f, -5f), 1f);//添加球体
        group.SetBoundingSpheres(spheres);//设置给Group
        group.SetBoundingSphereCount(2);//设置数量

        //设置相机为距离参考点
        group.SetDistanceReferencePoint(Camera.main.transform);
        //设置距离阈值,比如这里设置了4个阈值,产生了5个距离等级:0-3,3-6,6-10,10-15,15-
        group.SetBoundingDistances(new float[] { 3, 6, 10, 15 });

        //绑定回调函数
        group.onStateChanged = StateChangedMethod;
    }

    private void Update()
    {
        //0号球体距离参考点(相机)的距离等级
        Debug.Log("距离:" + group.GetDistance(0));
    }

    private void OnDestroy()
    {
        //释放组
        group.Dispose();
        group = null;
    }

    /// <summary>
    /// 回调函数。
    /// </summary>
    /// <param name="evt"></param>
    private void StateChangedMethod(CullingGroupEvent evt)
    {
        Debug.Log("回调执行!");

        if (evt.hasBecomeVisible)//判断是否变为可见
            Debug.LogFormat("Sphere {0} has become visible!", evt.index);
        if (evt.hasBecomeInvisible)//判断是否变为不可见
            Debug.LogFormat("Sphere {0} has become invisible!", evt.index);
    }
}

先看“可视化”触发回调函数的情况:

可以看到,运行时先触发了一次“距离回调”和“可视化回调”,可视化回调触发是因为看到了,若相机默认视野内无球体,则运行时是不会触发的。后续旋转视野,分别触发了两次“可视化回调”,球体0不可视,以及球体0可视。

然后是距离测试:

距离等级有5个,则数值分别是:0,1,2,3,4。直接看相机的Z轴数值变化,根据值所处阈值的间隔,打印不同的距离等级,且变化距离等级时,会触发回调函数,起始1一次,后续4次变化,总共执行回调5次,不过图中是6次,因为中途触发了一次旁边小球可视化的回调。需要注意,距离是球体到参考点的最近距离,即,这里用与判断在那个阈值间隔内的距离不是参考点到球体中心的距离,而是到球体表明的距离,要考虑半径(代码里设置是1)。比如图中,初始相机z为-3,即距离为2,所以在0级别;按照阈值,当距离超过3后就会变为1级别,那么考虑到半径,这里变为1级别时,相机z轴应大于-4(考虑符号是小于-4),图中也的确是这样,可观察图中z值变化以及等级变化。

        至此实验结束,通过这些我们也就能应用可见性与距离了。关于距离的API我就找到这两个,应该是没有其他的了。

8.2.4 性能和设计

        CullingGroup API 不允许我们对场景进行更改后立即请求包围球体的新可见性状态。出于性能原因,CullingGroup 仅在执行整个摄像机剔除期间计算新的可见性信息;此时,我们可以通过回调或 CullingGroup 查询 API 来获取信息。实际上,这意味着我们应该以异步方式处理 CullingGroup。

        提供给 CullingGroup 的包围球体数组将由 CullingGroup 引用,而不是复制。这意味着我们应该保留对传递给 SetBoundingSpheres 的数组的引用,并可修改此数组的内容(因为CullingGroup 没有提供访问数组的变量,所以我们需要保留原来的引用,方便修改),而无需再次调用 SetBoundingSpheres。如果需要多个 CullingGroup 来计算同一组球体的可见性和距离(例如,对于多个摄像机),那么让所有 CullingGroup 共享相同的包围球体数组实例会很高效。

9 动态分辨率

        动态分辨率是一种摄像机设置,允许动态缩放单个渲染目标,以便减少 GPU 上的工作负载。在应用程序的帧率降低的情况下,可以逐渐缩小分辨率来保持帧率稳定。如果性能数据表明由于应用程序受 GPU 限制而导致帧率即将降低,则 Unity 会触发此缩放。还可以手动触发缩放,方法是抢占应用程序中 GPU 资源消耗特别高的部分的优先级,并通过脚本控制缩放。如果缩放逐渐进行,动态分辨率几乎不可察觉。

9.1 渲染管线兼容性

        动态分辨率支持取决于项目使用的渲染管线( render pipeline)。

功能 内置渲染管线 通用渲染管线 (URP) 高清渲染管线 (HDRP)
动态分辨率 是 (1) 是 (1) 是 (2)

注意:

  1. 内置渲染管道和通用渲染管道(URP)都支持本文档中描述的动态分辨率(即本节所描述的)。

  2. 高清渲染管线(High Definition Render Pipeline, HDRP)支持动态分辨率,但我们可以以不同的方式启用和使用它。有关HDRP中的动态分辨率的信息,请参见Dynamic resoluton in HDRP

9.2 支持的平台

        Unity在iOS, macOS和tvOS(仅限Metal), Android(仅限Vulkan), Windows Standalone(仅限DirectX 12)和UWP(仅限DirectX 12)上支持动态分辨率。

9.3 对渲染目标的影响

        使用动态分辨率,Unity 不会重新分配渲染目标。从概念上讲,Unity 会缩放渲染目标;但实际上,Unity 使用锯齿,而且缩小的渲染目标仅使用原始渲染目标的一小部分。Unity以完整的分辨率分配渲染目标,然后动态分辨率系统将它们缩小并再次放大(【官方翻译是“备份”?】),使用原始目标的一部分而不是重新分配新目标。

9.4 缩放渲染目标

        使用动态分辨率时,渲染目标具有 DynamicallyScalable 标志。可通过设置此标志来指明 Unity 是否应该将此渲染纹理作为动态分辨率过程的一部分进行缩放。摄像机还具有 allowDynamicResolution 标志(组件上也可以设置),可以用它来设置动态分辨率,这样就不需要覆盖渲染目标,如果我们只是想应用动态分辨率到一个不太复杂的场景。(PS:说的应该是设置相机标识,就不用设置目标的标识了,但考虑性能问题,这种使用方法一般只在不太复杂的场景使用。)

9.5 MRT 缓冲区

        在 Camera 组件上启用 Allow Dynamic Resolution 时,Unity 会缩放该摄像机的所有目标。

9.6 控制缩放

        可以通过 ScalableBufferManager 控制缩放。借助 ScalableBufferManager 可以控制已标记为由动态分辨率系统进行缩放的所有渲染目标的动态宽度和高度缩放。

        例如,假设应用程序以理想的帧率运行,但在某些情况下,由于粒子增多、后期效果和屏幕复杂性等多重因素的影响,GPU 性能会下降。Unity FrameTimingManager 可以检测 CPU 或 GPU 性能何时开始下降。因此,可使用 FrameTimingManager 计算新的所需宽度和高度缩放以将帧率保持在所需范围内,并将缩放降低到该值以保持性能稳定(立即进行或在设定的帧数内逐渐进行)。当屏幕复杂度降低并且 GPU 性能稳定时,可将宽度和高度缩放提高到我们计算过的 GPU 可以处理的值。

9.7 示例

        这个示例脚本演示了API的基本用法。将其添加到场景中的相机身上,并在相机设置中勾选 Allow Dynamic Resolution。我们还需要打开 Player 设置(菜单:Edit > Project settings,然后选择Player类别),并勾选 Enable Frame Timing Stats 复选框。有关Enable FrameTiming Stats属性背后的功能的更多信息,请参阅 FrameTimingManager

        单击鼠标或用一根手指点击屏幕,分别将高度和宽度分辨率降低 scaleWidthIncrementscaleHeightIncrement 变量中的大小。用两根手指点击会以相同的值提高分辨率。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DynamicResolutionTest : MonoBehaviour
{
    public Text screenText;

    FrameTiming[] frameTimings = new FrameTiming[3];

    public float maxResolutionWidthScale = 1.0f;
    public float maxResolutionHeightScale = 1.0f;
    public float minResolutionWidthScale = 0.5f;
    public float minResolutionHeightScale = 0.5f;
    public float scaleWidthIncrement = 0.1f;
    public float scaleHeightIncrement = 0.1f;

    float m_widthScale = 1.0f;
    float m_heightScale = 1.0f;

    // 跨帧保持的动态分辨率算法变量
    uint m_frameCount = 0;

    const uint kNumFrameTimings = 2;

    double m_gpuFrameTime;
    double m_cpuFrameTime;

    // 使用此函数进行初始化
    void Start()
    {
        int rezWidth = (int)Mathf.Ceil(ScalableBufferManager.widthScaleFactor * Screen.currentResolution.width);
        int rezHeight = (int)Mathf.Ceil(ScalableBufferManager.heightScaleFactor * Screen.currentResolution.height);
        screenText.text = string.Format("Scale: {0:F3}x{1:F3}\nResolution: {2}x{3}\n",
            m_widthScale,
            m_heightScale,
            rezWidth,
            rezHeight);
    }

    // 每帧调用一次 Update
    void Update()
    {
        float oldWidthScale = m_widthScale;
        float oldHeightScale = m_heightScale;

        // 一根手指降低分辨率
        if (Input.GetButtonDown("Fire1"))
        {
            m_heightScale = Mathf.Max(minResolutionHeightScale, m_heightScale - scaleHeightIncrement);
            m_widthScale = Mathf.Max(minResolutionWidthScale, m_widthScale - scaleWidthIncrement);
        }

        // 两根手指提高分辨率
        if (Input.GetButtonDown("Fire2"))
        {
            m_heightScale = Mathf.Min(maxResolutionHeightScale, m_heightScale + scaleHeightIncrement);
            m_widthScale = Mathf.Min(maxResolutionWidthScale, m_widthScale + scaleWidthIncrement);
        }

        if (m_widthScale != oldWidthScale || m_heightScale != oldHeightScale)
        {
            ScalableBufferManager.ResizeBuffers(m_widthScale, m_heightScale);
        }
        DetermineResolution();
        int rezWidth = (int)Mathf.Ceil(ScalableBufferManager.widthScaleFactor * Screen.currentResolution.width);
        int rezHeight = (int)Mathf.Ceil(ScalableBufferManager.heightScaleFactor * Screen.currentResolution.height);
        screenText.text = string.Format("Scale: {0:F3}x{1:F3}\nResolution: {2}x{3}\nScaleFactor: {4:F3}x{5:F3}\nGPU: {6:F3} CPU: {7:F3}",
            m_widthScale,
            m_heightScale,
            rezWidth,
            rezHeight,
            ScalableBufferManager.widthScaleFactor,
            ScalableBufferManager.heightScaleFactor,
            m_gpuFrameTime,
            m_cpuFrameTime);
    }

    // 估算下一帧时间并在必要时更新分辨率缩放。
    private void DetermineResolution()
    {
        ++m_frameCount;
        if (m_frameCount <= kNumFrameTimings)
        {
            return;
        }
        FrameTimingManager.CaptureFrameTimings();
        FrameTimingManager.GetLatestTimings(kNumFrameTimings, frameTimings);
        if (frameTimings.Length < kNumFrameTimings)
        {
            Debug.LogFormat("Skipping frame {0}, didn't get enough frame timings.",
                m_frameCount);

            return;
        }

        m_gpuFrameTime = (double)frameTimings[0].gpuFrameTime;
        m_cpuFrameTime = (double)frameTimings[0].cpuFrameTime;
    }
}

PS:测试没效果?【先放着

9.8 缩放发生时的控制

        ScalableBufferManager.ResizeBuffers函数在调用时立即缩放渲染纹理。但是,我们可以通过使用DynamicallyScalableExplicit标志来修改此行为。当我们调用RenderTexture.ApplyDynamicScale时,标记为DynamicallyScalableExplicit的渲染纹理会缩放,取代调用ScalableBufferManager.ResizeBuffers时的自动缩放。缩放会导致渲染纹理内容无效,所以我们必须使用DynamicallyScalableExplicitRenderTexture.ApplyDynamicScale来确保渲染纹理数据在缩放因子改变时仍然存在。

        例如,时间抗锯齿通过重用前一帧的数据来提高当前帧的视觉质量。如果动态分辨率比例因子在帧之间发生变化,则需要保留前一帧的数据。我们可以通过使用DynamicallyScalableExplicit标记包含这些数据的渲染纹理来实现这一点,允许它们在调用ScalableBufferManager.ResizeBuffers之后仍然有效。我们只需要使用RenderTexture.ApplyDynamicScale调整当前帧的渲染纹理大小,确保前一帧的渲染纹理在采样时仍然有效。(PS:最后我感觉是提供了两种思路,一种是把前一帧标记flag,然后调用ResizeBuffers进行缩放,由于flag的存在则进而保留前一帧数据。第二种是标记当前帧flag,然后调用ApplyDynamicScale来缩放当前帧,进而保留了前一帧数据。)

9.9 其他

9.10 FrameTimingManager

        FrameTimingManager是一个API,它捕获应用程序中单个帧期间有关性能的详细定时数据。我们可以使用这些数据来评估框架,以了解应用程序无法满足性能目标的原因。

在以下情况,使用FrameTimingManager:

  • 我们需要逐帧调试。

  • 我们希望使用动态分辨率特性。

  • 我们需要使用自适应性能(Adaptive Performance)包。

        Frame timings不会取代来自分析器(Profiler)的数据,在高级剖析应用程序之后(借助分析器),使用FrameTimingManager来调查特定的细节。FrameTimingManager在记录数据时会降低性能,因此它无法准确测量应用程序的执行情况。

9.10.1 怎样启用FrameTimingManager

        注意:FrameTimingManager对于Development Player 构建总是处于激活状态。

        在发布版本和Unity编辑器中启用FrameTimingManage:

  1. 前往 Edit > Project Settings > Player.

  2. Other Settings 中,导航到 Rendering 标题.

  3. 启用 Frame Timing Stats 属性。

如果我们使用OpenGL平台,我们还需要启用OpenGL: Profiler GPU Recorders属性来测量GPU使用情况。属性位置在前面动态分辨率那里有截图,两个属性都是在一起的。

        注意:在Unity版本2021.2及更早的版本中,OpenGL Profiler GPU Recorder禁用Frame Timing Stats属性,因此我们不能同时使用它们。

9.10.2 如何使用FrameTimingManager

        要访问FrameTimingManager记录的数据,使用以下方法之一。

9.10.2.1 View frame time data with a Custom Profiler module

        查看自定义分析器模块中的帧时间数据:

  1. 根据Creating a custom profiler module的说明创建自定义分析器模块。

  2. 在“分析器模块编辑器”窗口中,选择自定义模块。

  3. 在Available Counters面板中,选择Unity。

  4. 选择Render以打开包含与内存使用相关的分析器计数器的子菜单,其中包括Frame Timing Stats属性所启用的那些计数器。然后可以单击子菜单中的相关计数器,将它们添加到自定义模块中。

下表描述了在启用Frame Timing Stats时,变为可用的每个计数器的目的:

  • CPU Total Frame Time (ms):CPU总帧时间,以毫秒为单位。Unity将其计算为两帧结束之间的时间,包括任何开销或帧之间等待的时间。

  • CPU Main Thread Frame Time (ms):帧开始到主线程完成帧内执行的工作之间的时间,以毫秒为单位。

  • CPU Main Thread Present Wait Time (ms):在帧期间等待Present()所花费的CPU时间。

  • CPU Render Thread Frame Time (ms):渲染线程开始工作到Unity调用Present()函数之间的时间,以毫秒为单位。

  • GPU Frame Time (ms):GPU渲染单帧时开始和结束的时间差,以毫秒为单位。

9.10.2.2 Retrieve timestamp data from the FrameTimingManager C# API

        使用FrameTimingManager API访问时间戳信息。在每个变量中,FrameTimingManager记录帧期间特定事件发生的时间。下面显示了API中可用的值,按照Unity在一个帧中执行它们的顺序:

  • frameStartTimestamp:帧开始时的CPU时钟时间。

  • firstSubmitTimestamp:Unity在此帧期间向GPU提交第一个作业时的CPU时钟时间。

  • cpuTimePresentCalled:Unity为当前帧调用Present()函数时的CPU时钟时间。

  • cpuTimeFrameComplete:GPU完成渲染帧并中断CPU的CPU时钟时间。

9.10.2.3 Record data with specific profiler counters

        我们可以使用ProfilerRecorder API而不是FrameTimingManager c# API来读取FrameTimingManager的值。这样做的好处是,当我们使用ProfilerRecorder API时,FrameTimingManager仅在将记录器附加到特定计数器时记录值。这种行为使我们能够控制哪些计数器收集数据,从而减少FrameTimingManager对性能的影响。

        下面的例子展示了如何使用ProfilerRecord API只跟踪CPU主线程帧时间变量:

using Unity.Profiling;

using UnityEngine;

public class ExampleScript : MonoBehaviour

{

    string statsText;

    ProfilerRecorder mainThreadTimeRecorder;

    void OnEnable()

    {
        mainThreadTimeRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Internal, "CPU Main Thread Frame Time");
    }

    void OnDisable()

    {
        mainThreadTimeRecorder.Dispose();
    }

    void Update()

    {

        var frameTime = mainThreadTimeRecorder.LastValue;

        // Your code logic here

    }
}

9.10.3 FrameTimingManager如何工作

        FrameTimingManager提供的结果带有四帧延迟。这是因为计时结果不能在每帧结束时立即可用,所以FrameTimingManager等待获取该帧的CPU和GPU数据。

        延迟不能保证精确的计时结果,因为GPU可能没有任何可用的资源来返回结果,或者可能无法正确返回结果。

        在某些情况下,FrameTimingManger改变了它产生FrameTimeComplete时间戳的方式:

  • 如果GPU支持GPU时间戳,GPU会提供一个FrameTimeComplete时间戳。

  • 如果GPU不支持GPU时间戳并返回GPU时间,则FrameTimingManager计算gpuFrameTime的值。该值为上报的GPU Time与FirstSubmitTimestamp值之和。

  • 如果GPU不支持GPU时间戳并且不返回GPU时间,FrameTimingManager将PresentTimestamp的值设置为FrameTimeComplete的值。

基于tile的延迟渲染GPU,其可能不准确

        对于使用基于tile延迟渲染架构的GPU,例如Apple设备中的Metal GPU,报告的GPU Time可能大于报告的帧时间。

        这可能发生在GPU处于高负载下,或者GPU管道已满时。在这些情况下,GPU可能会延迟某些渲染阶段的执行。因为FrameTimingManager测量帧渲染开始和结束之间的时间,阶段之间的任何间隙都会增加报告的GPU时间。

        在下面的示例中,没有可用的GPU资源,因为GPU将作业从顶点队列传递到片段队列。GPU的图形API因此延迟了下一阶段的执行。当这种情况发生时,GPU时间测量包括各阶段工作时间和阶段间的任何间隙。结果是FrameTimingManager报告的GPU时间测量值比预期的要高。

        下面图表显示了在Metal API中,报告的GPU时间差异是如何发生的:

9.10.4 平台支持

属性 描述 Supported 注释
Windows DirectX 11
DirectX 12
OpenGL
Vulkan
macOS Metal 由于基于tile的延迟渲染GPU的行为,可能会报告比总帧时间更大的GPU时间测量。
Linux OpenGL 部分支持 不支持GPU时间测量。
Vulkan
Android OpenGL ES
Vulkan
iOS Metal 由于基于tile的延迟渲染GPU的行为,可能会报告比总帧时间更大的GPU时间测量。
tvOS Metal 由于基于tile的延迟渲染GPU的行为,可能会报告比总帧时间更大的GPU时间测量。
WebGL WebGL 部分支持 不支持GPU时间测量。

9.10.5 其他资源

10 深度学习超级采样

        NVIDIA深度学习超级采样(DLSS)是一种使用人工智能来提高图形性能和质量的渲染技术。我们可以用它来:

  • 以高帧率和分辨率运行实时光线追踪世界。

  • 为栅格化图形提供性能和质量提升。这对虚拟现实应用程序特别有用,可以帮助它们以更高的帧速率运行。这有助于消除在低帧率下出现的迷失方向、恶心和其他负面影响。

有关DLSS的更多信息,请参阅 NVIDIA documentation

10.1 需求和兼容性

        本节包括有关DLSS的硬件要求和渲染管线兼容性的信息。

        有关硬件和驱动程序要求的信息,请参阅 NVIDIA documentation

10.1.1 渲染管线兼容性

功能 内置渲染管线 通用渲染管线 (URP) 高清渲染管线 (HDRP) Scriptable Render Pipeline (SRP)
DLSS No (1) No (1) No (1)

注意:

  1. 底层DLSS框架代码位于核心Unity中,但HDRP是唯一使用它来实现DLSS的渲染管道。有关更多信息,请参阅 API documentation

10.1.2 支持的平台

        Unity在以下平台上支持DLSS:

  • DirectX 11在Windows 64位。

  • DirectX 12在Windows 64位。

  • Vulkan在Windows 64位。

        Unity不支持Metal、Linux、使用x86架构的Windows (Win32)或任何其他平台的DLSS。要为Windows构建项目,请使用x86_64体系结构(Win64)。

10.2 使用DLSS

        关于如何在HDRP中使用DLSS的信息,请参阅 Deep learning super sampling in HDRP

        如图,在没有DLSS的情况下,飞船演示项目以50%的屏幕百分比渲染,其图像模糊,尤其是周围有火花的视觉效果。

如图,飞船演示项目以50%的屏幕百分比渲染,但使用DLSS。这张图像比之前的图像不那么模糊,尤其是周围的火花视觉效果。

质量模式

        可用的质量模式如下:

  • Auto:选择当前输出分辨率的最佳DLSS模式。

  • Balance:平衡性能与图像质量。

  • Quality:强调图像质量胜过性能。

  • Performance:强调性能和图像质量。【为什么是“和”?】

  • Ultra Performance:产生最高的性能和最低的图像质量。此模式仅适用于8k分辨率(7680 x 4320像素)。

11 多显示

        使用多显示功能可以同时在最多八台不同的监视器上显示应用程序的最多八个不同摄像机视图。此功能可用于 PC 游戏、街机游戏机或公共显示装置等设施。

Unity 在以下平台上支持多显示功能:

  • 桌面平台 (Windows, macOS X, and Linux)

  • Android(OpenGL ES 和 Vulkan)

  • iOS

有些功能只适用于某些平台。有关兼容性的更多信息,请参阅 DisplayScreenFullScreenMode API。

11.1 激活多显示功能

        Unity 的默认显示模式仅为一台显示器。在运行应用程序时,需要使用 Display.Activate() 显式激活其他显示器的显示,激活的显示不能停用。

        激活额外显示的最佳时间是在应用程序创建新场景时。一个好方法是将脚本组件附加到默认摄像机,确保仅在启动过程中调用一次 Display.Activate()

示例脚本

using UnityEngine;
using System.Collections;

public class ActivateAllDisplays : MonoBehaviour
{
    void Start ()
    {
    	Debug.Log ("displays connected: " + Display.displays.Length);
        
    	// Display.displays[0] 是主要的默认显示,并始终为 ON,因此从索引 1 开始。
    	// 检查是否有其他可用的显示,并激活每个显示。
        for (int i = 1; i < Display.displays.Length; i++)
    	{
        	Display.displays[i].Activate();
    	}
    }
    
    void Update()
    {

    }
}

11.2 在项目中进行多显示预览

        如果场景中有两个相机。要在项目中预览不同的摄像机视图,请按照以下步骤操作:

        为每个相机设置所属Display,如图:

然后再Game视图中,选择对应的Display,如图:

11.3 支持的API

        Unity 支持以下 UnityEngine.Display API 方法:

  • public void Activate(): 根据当前监视器的宽度和高度激活具体显示。必须在启动新场景时进行一次此调用。可从新场景中的摄像机或虚拟游戏对象附加的用户脚本进行此调用。

  • public void Activate(int width, int height, int refreshRate):仅限 Windows。激活自定义宽度和高度的特定显示。在 Linux 和 macOS X 上,辅助显示上始终使用当前显示分辨率(如果可用)。

12 相机组件

        摄像机是为玩家捕捉和展示世界的设备。通过自定义和操纵摄像机,我们可以让自己的游戏呈现出真正的独特性。在场景中可拥有无限数量的摄像机。这些摄像机可设置为以任何顺序在屏幕上任何位置或仅在屏幕的某些部分进行渲染。

12.1 相机Inspector面板参考

根据项目使用的渲染管线,Unity 在 Camera Inspector 中显示不同的属性。

  • 如果我们的项目使用通用渲染管线 (URP),请参阅 URP 包文档微型网站

  • 如果我们的项目使用高清渲染管线 (HDRP),请参阅 HDRP 包文档微型网站

  • 如果我们的项目使用内置渲染管线,Unity 会显示以下属性:

  • Clear Flags:确定将清除屏幕的哪些部分。使用多个摄像机来绘制不同游戏元素时,这会很方便。

  • Background:在绘制视图中的所有元素之后但没有天空盒的情况下,应用于剩余屏幕部分的颜色。

  • Culling Mask:由摄像机渲染的对象层。在检视面板中将层分配到对象。

  • Projection:切换摄像机模拟透视的功能。

    • Perspective:以完整透视角度渲染对象。

    • Orthographic:均匀渲染对象,没有透视感。注意:在正交模式下不支持延迟渲染。始终使用前向渲染。

  • Size:摄像机的视口大小。选为Orthographic模式时,将出现此属性。正交下视椎体变为矩形体,改变此值会改变矩形体视野大小面的尺寸,不影响视野距离。可以选中相机改变此值,在Scene中观察视野区域变化。

  • FOV Axis:选为Perspective模式时,将出现此属性。视野轴。

    • Horizontal:摄像机使用水平视野轴。

    • Vertical: 摄像机使用垂直视野轴。

  • Field of view:选为Perspective模式时,将出现此属性。摄像机的视角,以沿着FOV Axis下拉框中指定的轴的度数为单位来测量。

  • Physical Camera:启用此属性后,Unity 将使用模拟真实摄像机属性的属性(Focal Length、Sensor Size 和 Lens Shift)计算 Field of View。

    • ISO:传感器灵敏度(ISO)。

    • Shutter Speed:曝光时间,以秒为单位。

    • Aperture:光圈数,以f-stop为单位。

    • FocusDistance:镜头的焦距。如果我们将focusDistanceMode设置为FocusDistanceMode.Camera,则Fieldvolume的深度将覆盖使用此值。

    • Blade Count:光圈叶片(Diaphragm blades)的数量。

    • Curvature:将孔径范围映射到叶片曲率。

    • Barrel Clipping:“猫眼”效应对散焦(光学渐晕)的影响强度。

    • Anamorphism:拉伸传感器以模拟变形外观。正值将垂直扭曲相机,负值将水平扭曲相机。

    • Focal Length:设置摄像机传感器和摄像机镜头之间的距离(以毫米为单位)。较小的值产生更宽的 Field of View,反之亦然。更改此值时,Unity 会相应自动更新 Field of View 属性。

    • Sensor Type:指定希望摄像机模拟的真实摄像机格式。从列表中选择所需的格式。选择摄像机格式时,Unity 会自动将 Sensor Size > X 和 Y 属性设置为正确的值。如果手动更改 Sensor Size 值,Unity 会自动将此属性设置为 Custom。

    • Sensor Size:设置摄像机传感器的大小(以毫米为单位)。选择 Sensor Type 时,Unity 会自动设置 X 和 Y 值。如果需要,可以输入自定义值。X:传感器的宽度。Y:传感器的高度。

    • Lens Shift:从中心水平或垂直移动镜头。值是传感器大小的倍数;例如,在 X 轴上平移 0.5 将使传感器偏移其水平大小的一半。可使用镜头移位来校正摄像机与拍摄对象成一定角度时发生的失真(例如,平行线会聚)。沿任一轴移动镜头均可使摄像机视锥体倾斜。X:传感器水平偏移。Y:传感器垂直偏移。

    • Gate Fit:具体见“4 使用物理相机”小节。

  • Clipping Planes:开始和停止渲染位置到摄像机的距离。(近远裁剪面)

    • Near:相对于摄像机的最近绘制点。(近裁剪面)

    • Far: 相对于摄像机的最远绘制点。(远裁剪面)

  • Viewport Rect:通过四个值的指示,将在屏幕上绘制此摄像机视图的位置。值范围为 0–1。X: 绘制摄像机视图的起始水平位置。Y:绘制摄像机视图的起始垂直位置。W:屏幕上摄像机输出的宽度。H:屏幕上摄像机输出的高度。

  • Depth:深度。摄像机在绘制顺序中的位置。具有更大值的摄像机将绘制在具有更小值的摄像机之上。

  • Rendering Path:定义摄像机将使用的渲染方法。

    • Use Graphics Settings:使用Graphics Settings中指定的渲染路径 (Rendering Path)。

    • Forward:每种材质采用一个通道渲染所有对象。

    • Deferred:绘制所有对象一次,不带照明,然后在渲染队列结束时绘制所有对象的照明。

    • Legacy Vertex Lit:此摄像机渲染的所有对象都将渲染为顶点光照对象。

  • Target Texture:引用将包含摄像机视图输出的渲染纹理。设置此引用将禁用此摄像机的渲染到屏幕功能。

  • Occlusion Culling:为此摄像机启用遮挡剔除 (Occlusion Culling)。遮挡剔除意味着隐藏在其他对象后面的对象不会被渲染,例如,如果对象在墙后面。具体使用更复杂些,具体见“7 遮挡剔除”小节。

  • HDR:为此摄像机启用高动态范围渲染。请参阅高动态范围渲染以了解详细信息。

  • MSAA:为此摄像机启用多重采样抗锯齿。

  • Allow Dynamic Resolution:为此摄像机启用动态分辨率渲染。请参阅“9 动态分辨率”小节以了解详细信息。

  • Target Display:定义要渲染到的外部设备。值为 1 到 8 之间。

12.2 详细信息

        为了向玩家显示游戏,摄像机至关重要。可对摄像机进行自定义、为其编写脚本或对其进行管控以实现任何可想象的效果。对于拼图游戏,摄像机可保持静态以获得拼图的完整视图。对于第一人称射击游戏,可让摄像机跟随玩家角色,并将其置于角色的视线水平。对于赛车游戏,可能希望让摄像机跟随玩家的车辆。

        可创建多个摄像机,并将每个摄像机分配给不同的深度 (Depth)。按照从低深度到高深度的顺序绘制摄像机。换言之,深度为 2 的摄像机将绘制在深度为 1 的摄像机之上。我们可以调整Viewport Rect属性的值,从而调整屏幕上摄像机视图的大小和位置。这样就能创建多种迷你视图,如导弹摄像机、地图视图、后视镜等。

12.2.1 Render path

        Unity 支持不同的渲染路径。我们应该根据自己的游戏内容和目标平台/硬件而选择使用哪一个渲染路径。不同的渲染路径具有不同的功能和性能特征,主要影响光照和阴影。 在 Player 设置中选择项目使用的渲染路径。此外,可针对每个摄像机重写渲染路径。

        有关渲染路径的更多信息,请查看渲染路径页面。

12.2.2 Clear Flags

        每个摄像机在渲染其视图时都会存储颜色和深度信息。使用多个摄像机时,每个摄像机都会在缓冲区中存储自己的颜色和深度信息,随着每个摄像机渲染而累积越来越多数据。场景中的任何特定摄像机渲染其视图时,我们可以设置 Clear Flags 来清除不同的缓冲区信息集合。为此,请选择以下四个选项之一:

Skybox

        这是默认设置。屏幕的任何空白部分将显示当前相机的天空框。如果当前相机没有设置天空盒,它将默认为在Lighting窗口中选择的天空盒(Window→Rendering→Lightings),然后它将回落到背景色。另外,可以将 Skybox component 添加到相机上,具体可看链接中的内容。

Solid color

        屏幕的任何空白部分都将显示当前摄像机的背景颜色(相机组件上的一个属性)。

Depth only

        只清除深度信息。(PS:“渲染过的像素如果没有需要渲染的物体,则会保持原来的颜色值,否则会渲染新的物体颜色值,不论这个点之前是什么(因为深度值被 Clear)”,这是我找到的解释,可以帮助理解下。)

        如果要绘制玩家的枪支而不使其陷入环境中,请设置一个深度 (Depth) 为 0 的摄像机来绘制环境,并设置另一个深度为 1 的摄像机来单独绘制武器。将武器摄像机的 Clear Flags 设置为 Depth Only。这样枪支就永远在环境最前方了,枪支之外的空白部分则是0深度相机所绘制的环境。

        可以简单理解为适用于多相机的情况,可让设置一个低深度的相机,以及一个高深度相机,并把高深度相机设置为此模式来渲染个别对象,就可以实现让低深度相机作为背景,高深度相机渲染对象在最上层。若只有一个相机,那么会出现于下面Don't clear类似的涂抹效果,因为会保持原来的颜色值嘛,多个相机的话也保持,只不过保持的是低深度相机所渲染的背景。

Don’t clear

        此模式不会清除颜色或深度缓冲区,结果是将每帧绘制在下一帧之上,从而产生涂抹效果。此模式通常不用于游戏,更可能与自定义着色器一起使用。

        请注意,在某些 GPU(主要是移动端 GPU)上,不清除屏幕可能会导致其内容在下一帧中未定义。在某些系统上,屏幕可能包含前一帧图像、纯黑色屏幕或随机有色像素。

【PS:所以下一帧这个说法是对的吗?????】

12.2.3 裁剪面 (Clip Planes)

        没啥说的,就一点:裁剪面会定义视椎体的形状,视椎体之外的内容不会被渲染,即视椎体剔除,无论我们是否在游戏中使用遮挡剔除 (Occlusion Culling),都会发生视椎体剔除。

12.2.4 正交模式 (Orthographic)

        将摄像机标记为Orthographic,就会从摄像机视图移除全部透视。此模式最常用于创建等距视图游戏或 2D 游戏。

        请注意,雾效在正交摄像机模式下均匀渲染,因此可能无法按预期显示。这是因为post-perspective空间的 Z 坐标用于雾的“深度”。这对于正交摄像机而言并不是严格准确的,但之所以使用该模式,是因为其在渲染过程中有一定的性能优势。

12.2.5 渲染纹理 (Render Texture)

        把相机的视图置于渲染纹理上,然后把纹理应用于另一对象,就可以创建类似监视器的效果,在对象身上展示相机视角的内容。

13 后记

        多数是直接复制官方文档说辞,不懂的地方可则可自行查询资料,必然比文档讲得更好。


网站公告

今日签到

点亮在社区的每一天
去签到