unity 中让一个物体的某个坐标轴跟着一个物体动的方法_游戏引擎中的渲染管线...

前言

渲染管线是做图形的同学最常听到的名词,它描述了游戏中的一帧渲染的流程顺序,有关渲染的所有环节可以说都被囊括其中。这篇文章不打算综述完整的渲染流程,对这个部分比较感兴趣的同学,可以看看这两篇文章,分别介绍了时下流行的Unity和UE的渲染管线,还有这两篇文章讲述GTA V和巫师3的帧分析。总体来说渲染管线的组成大同小异,本文主要关注直接照明部分的管线差异。

一帧由哪些渲染流程组成?

不管是什么样的渲染管线,它们的目的都是尽可能地呈现真实世界中的各类材质,从这个目的出发,一帧渲染要完成的任务可以用这张图比较简单的解释:

7202cf407c65fea7b78cb9b182556cf9.png

实际上,绝大部分渲染引擎要解决的无非就是 直接照明间接照明后处理 。在现代的实时渲染引擎中,直接照明也往往是管线的核心。尽管许多人大概已经听腻了forward/deferred rendering,也大约知道他们的思路和差异,但是你真的了解这些管线实现中的每个细节吗?

从Foward Rendering说起

游戏引擎的设计之初,硬件的计算能力还没有那么强,所以能把一个模型的位置画对、有正确的纹理贴图,就已经谢天谢地了,光照?不存在的。所以那个年代的渲染,也基本不区分直接、间接照明,无非就是一堆trick怼上去,弄几个参数调一调,在一个shader里搞定就好了。后来大家渐渐意识到, 这个世界要有光 ,于是逐渐迭代出了我们如今比较流行的几种理想光源。但是受限于Shader Core的运算性能,一个场景能够渲染的光源仍然有限,每个物体通常只有一盏方向光(全局,用于模拟太阳光)和两盏局部光(点光、聚光灯)的预算,这种情况下,大家能够想到最直观的方案就是扩展原本的shader,把光源数目写死,这种方案就是我们熟悉的 Uber Shader :所有的事情都放在一个shader里做完。

Uber Shader直到今天,在一些性能受限的平台上,仍然是主流方案,比如绝大部分WebGL和手机平台上的引擎,都采用这样的方案。这个方案有一些明显的优点,比 如每个模型只绘制一次,每种材质可以使用不同的光照模型和渲染技术 (比如很容易同时实现卡通渲染、各向异性材质和普通材质),缺点当然也很明显,就是 光源数目受限

在具体实现的时候,Uber Shader通常会根据光源的数目,编译出几个不同的shader来,然后在CPU端提交渲染前,根据场景内灯光的位置半径以及被渲染物体的包围盒,粗略地算出所有影响它的光源,并根据光源的数目选取不同的预编译好的shader(超过最大数目,则使用最近的N个光源)。

Z-Prepass和多遍渲染

随着硬件性能的增强和画质需求的提高,固定数量的光源再也无法满足实际应用的需求;同时由于Depth Test的存在,大量在Uber Shader里被绘制的物体,其实都无法被真正的看到,也就带来了性能浪费。有没有更灵活,同时性能又更好的计算方案呢?大家发现,可以 借助当时大部分硬件都有的early z特性,来避免overdraw的问题 。具体来说,在实际渲染之前,加入了一个称之为 z prepass 的流程,这个流程关闭了color buffer的写入,同时pixel shader极为简单或者索性为空,可以非常快速的执行完毕并且获得场景中的z buffer;紧接着,我们再关闭z buffer的写入,改depth test function为equal。这样整个渲染过程中,那些没有通过depth test的像素,就不必再执行pixel shader的复杂运算和写入,对于overdraw比较大的应用来说,能够大大减少pixel shader的运算和带宽开销,所以已经几乎成为了渲染管线中的标配。

有了z prepass之后,人们就自然而然的想到了支持多光源的方案: 多遍绘制 。几盏光源就绘制几次物体,由于early z的存在,那些不是最终可见的像素并不会实际绘制,所以从性能的角度来讲尚可,灵活度上也大大提高,甚至我们可以结合uber shader和多遍绘制,在光源数目较多的时候,减少draw call的同时又不失灵活度(比如每遍计算四盏光源而不是一盏)。

实际上z prepass并不总是带来性能的提升,因为它本身会使得vertex shader的执行以及draw call次数翻倍。这篇文章就比较详尽地分析了这个问题。

Deferred Rendering

到多pass渲染为止,理论上我们已经可以支持无限数量的光源。但即使是结合uber shader,场景内的draw call数量也是随着光源数目的增加,呈O(m*n)的复杂度(m表示场景内物体数量,n表示灯光数量)。而现代游戏画质的提升,其中一个很重要的指标就是局部光源数目的增加,当场景内有数百个光源的时候,多pass也无能为力了。此外,多pass的另外一个缺陷是, 它对光源的剔除是per object的 ,这就意味着即使某个光源只照射到物体很小一部分,也仍然需要一次单独的draw call。有没有办法把物体的绘制和光照计算解耦开呢?在这样的需求下,得益于MRT技术的支持,诞生了延迟渲染。它的核心是G-Buffer,也就是是若干张贴图,存的是计算光照需要的所有参数,通常包含了depth,normal,albedo,roughness,specular和metallic。

传统的延迟渲染在G-Buffer生成之后,会根据光源的形状(light volume),对每个光源执行一次draw call,如果某个像素被light volume覆盖到了,我们就在该像素的位置执行一次当前光源的lighting计算。由于光照计算是线性可叠加的,所以我们只要把color buffer的blend mode设置为ADD,并将source factor和dst factor设置为ONE即可。

80be47188e62a84f987f0e753264adf1.png

Light Volume

需要注意的是,为了防止同一像素被光源正反面计算两次,我们需要在绘制light volume的时候使用 单面渲染,如果摄像机在光源内,则需要开启正面剔除,并且将depth test设置为farOrEqual,如果摄像机在光源之外,则开启背面剔除,并且将depth test设置为nearOrEqual

Deferred rendering的优点是,解耦了mesh draw和light draw,保证物体只被绘制一次,光源也只绘制一次,场景的draw call复杂度变成了O(m+n)。另外,G-Buffer除了用于直接照明外,还能够被用于一些间接照明的效果,比如SSAO,SSR;也正是G-Buffer概念的提出,使得近十年来越来越多的算法从world space向screen space的演进;去年因为硬件加速而流行的ray tracing技术,得益于G-Buffer,也能够大大提高性能。延迟渲染的缺点主要在于带宽:G-Buffer要存的数据越多,带宽开销就越大。

带宽的优化来自于两个方面: 读和写 。写的部分主要是G-Buffer的压缩,在这个方向演化出了许多用于压缩和减小G-Buffer的方案,比如早年Crytek提出的Best Fit Normal和其他一些normal的压缩方案,以及基于YCbCr(或者YCoCg)的色彩空间把三通道的RGB信息压缩到两通道(当然色彩压缩的方案由于最终着色的时候还需要多读一个pixel的值去重建,所以得失如何还不好说)。G-Buffer的压缩方案一个缺陷是, 它可能会导致硬件的blend mode失效(主要影响decal blend)

5be4f097ba67c753752a0c040ac897b0.png

Crysis3用的Thin G-Buffer

7aabed2de67fe2f4a756218ef538092c.png
0cc1c2b6b0f7555b9faa5ecfe9b5dd3d.png

YCbCr的压缩方式

另外一个优化带宽的方法称之为 deferred lighting ,它的思路是,进一步解耦光照和最终着色的流程,我们以一个简单的Blinn-Phong的shading model为例,它的着色计算可以用这个公式描述:

a8272730b8c0e7c5767b0a79f8a0dc95.png

对于k个光源,我们需要读k次albedo和specular的值,但实际上,对同一个像素来说,

b51c1a258a5cf6bdeb35b2e7310f63ba.png

629e2ffaba63182e7b48e5650a0cd1a5.png

作为常数可以提取出来,于是方程改成如下形式:

fbacf944e54bd7c295e1e44a67a5b4f1.png

于是我们在lighting时分别存储

29bff80947b52731b68fd1d9e9036045.png

ec91352f2c9ee8402de5e6923e68ccd3.png

,之后再加一个shading的流程,在shading时读取

d68e5ee4f99543979ff88833b027c98c.png

629e2ffaba63182e7b48e5650a0cd1a5.png

的值,这样,albedo和specular就只需要读取一次,减少了带宽;相应地,我们需要存储两张lighting buffer。当然,我们也可以只存储RGB(Diffuse)和Lum(Specular),然后用

304bb8d520e1e52cb7607fa267dddf61.png

来恢复RGB(Specular),以节约lighting buffer的写入带宽。此外,当我们使用菲涅尔项去更好的模拟specular的时候,deferred lighting又变得不可行了,这时候我们只能把

c07b6d5fa66323cf7b6fa3301224e658.png

近似为

3cb6fa35ef19079c18ce78d83323f8cc.png

。有关deferred lighting的更多细节,可以参考这篇文章和这篇slide。

Visibility Buffer/Deferred Texturing 是在deferred lighting基础上更为激进的方案,它的思路很简单:既然G-Buffer这么费,我们索性不渲染G-Buffer,改成渲染Visibility Buffer,这个buffer上 只存primitive ID,uv和贴图ID ,我们根据这些属性,分别从UAV和bindless texture里面读取我们shading真正需要的vertex attributes和贴图的属性,根据uv的差分自行计算mip-map:

82ddded51b7b7fd09b05dde8020a9b2e.png

传统G-Buffer和Visibility Buffer在渲染流程上的差异

1bcd213d42677e8f02085e09f7f16c0d.png

Visibility Buffer的layout

这个方法确实能够一定程度上减少G-Buffer的容量(所有材质相关的属性都不存了),但是它的缺点是需要bindless texture的支持,而且由于贴图是在screen space根据Material ID动态索引的,所以从cache friendly的角度来说,我觉得不见得好(毕竟像素之间可能出现贴图跳变)。

除了上述的方案去优化读写带宽,针对传统的deferred rendering,还有一些方法是旨在减少light volume中无效的像素。我们之前已经描述了light volume的方法,但是,试想这种情况:

e5a2746c28058a2af5e4dfeb39dc0266.png

当光源整个位于z buffer前面时,整个light volume的区域都不需要实际渲染

这种情况下,虽然整个光源的正面都通过了NearOrEqual的测试,但其实它的所有区域都不对场景产生光照贡献。针对这种情况,有两种优化方案:

(1) Depth Bounds Test :这个方案利用了硬件中的一个称之为depth bounds test的特性,在普通depth test之外,额外增加一次测试,这个测试从depth buffer里面读取深度,然后用这个深度和depth bound test设置的zmin和zmax值作比较,如果depth值没有落在[zmin, zmax]这个区间内,则直接reject这个像素不执行pixel shader。利用这个优化,我们可以在绘制每个光源的时候,计算它的depth bound,并设置给管线,这样,上图的情况就不会实际着色。这个方法在killzone2中曾被使用,cryengine也用过类似的优化。

(2) 2-Pass Stencil Culling :这个方案剔除更精确,缺点是每个光源要绘制两遍:我们需要真正着色的像素,其实是深度值小于light volume背面,大于light volume正面的那些位置。所以我们先绘制light volume的正面,pixel shader置为空,将NearOrEqual的像素用stencil buffer标记出来;然后绘制light volume的背面,pixel shader设置为光照的shader,depth test设置为FarOrEqual,并且只有被上一个pass标记的像素才通过stencil test,这样一来,就能够精确标记并减少light shader的浪费,在killzone2中也有使用。另外,这篇silde提到了一种 clustered stencil culling 的方法,能够一次绘制多个光源的stencil,结合instance rendering,能够进一步减少2-pass stencil culling的状态切换开销。

Tiled Based Deferred Rendering & Forward+

在上述方法之后,工业界开始继续寻找解耦光照和几何计算的方法。在上述方法的基础之上,发展出的就是 tiled based 方法。实际上TBDR和Forward+是tiled based方法在forward和deferred上各自的体现,相较于过去的管线,tiled based的方法增加了一个 light culling 的流程,这个流程把整个屏幕分割成若干个tile(通常每个tile是16*16个pixel),每个tile各自计算出一个单独的light list,找出场景中那些对当前tile有贡献的光源。然后对每个tile中的pixel,只需要计算其对应的tile中light list内的光源对该像素的贡献。

21ddc2e7ba5218e25e15de66f24ba4d8.png

light culling需要一个额外的compute shader来计算,依据是当前tile的screen positon和depth bound(基于这两个信息可以计算出view frustum)。 对于foward方法来说,是在z-prepass之后,而对deferred方法来说,是在G-Buffer pass之后 。每个group内单独地对场景所有光源进行相较测试,每次可以并行测试256盏光源(16*16)。对于foward来说,计算出来的light list需要存储在一个UAV Buffer中,然后 再执行一次geometry pass ,根据每个pixel所在的tile去索引UAV Buffer中的light list,然后用动态的循环去计算每个光源对当前pixel的贡献;而对于deferred来说,light list可以存储在compute group对应的shared memory中(因为light culling就是以compute group为单位去运行的),light culling之后,在 同一个compute shader内 执行光照计算。

Tiled based方法可以说进一步减少了带宽,因为对于一个像素来说,相较于传统forward/deferred方法,涉及光照计算的pixel shader只执行一次,也就意味着 material data(比如albedo,normal,specular,roughness,metallic等)都只需要读取一次,并且最终结果也是一次写入 。但是它也有比较明显的缺陷:相较于传统的deferred shading,TBDR的方法对于光源的剔除更粗粒度了( 逐tile而不是逐pixel的 ),所以 当光源数量不够多的时候,这个方法并没有明显的性能优势

Tiled based基本的剔除方法是基于frustum的,它的缺陷在于, 基于frustum culling的方案对场景光源的剔除不够精确 ,这也是它在较少光源时性能不如传统deferred的原因之一。针对这个问题,很多方案提出了更精确的相交测试的方案,比如结合depth bound算出当前tile的AABB,然后基于AABB和bounding sphere做相交测试。

e8dd6b63c18a640688e1194a3bda3e85.png
3241e2c8e5e115f3aa27662d3d151505.png

基于frustum plane的剔除和AABB剔除的结果差异,可以看出基于AABB的方法大大减少了无效的tile

另一个相交测试不准的原因是,场景里 每个tile的depth的分布并不是连续的 ,在[zmin, zmax]区间内的很多深度范围可能是没有像素的,而光源可能正好分布在这个区间内。对于这个问题,分别有两个解决方案,其一是 depth split ,简单说就是把一个tile根据[zmin, zmax]再一分为二,分别计算每个区间的light list,另一个方案叫作 2.5D culling ,就是把当前tile的depth区间分成32段(刚好是一个uint32),再把tile里每个像素的depth映射到其中一段,然后写入一个depthMask,然后根据light bounding box,也把它的深度映射成一个lightMask,根据(depthMask & lightMask == 0)来判断一个tile是否真的和一个light相交。

d57540070bdd77e1e61011f95bab2099.png

2.5D culling的算法示意图

除了一般性的相交测试的准确性问题和depth discontinuous的问题之外,这篇文章还提到了结合light volume(也就是光栅器)的结果进一步提高light culling准确率的方法,感兴趣的同学可以读一读。

针对Foward+和TBDR,这篇slide做了一个比较详尽的比较,总体来说结论还比较直观:当光源 并不是真的非常多(超过2048个) 时, 只有使用MSAA时,Foward+好于TBDR;否则均是TBDR好于Foward+ ,鉴于目前各种screen space的AA方案的成熟(SMAA,FXAA,TAA等),MSAA实际上已经变得越来越鸡肋,从性能的角度已是一个拖累。加上我们之前说到的G-Buffer带来的额外红利,目前市面上的引擎仍然是主流使用deferred框架。而针对无法表征多中shading model的问题, 目前的deferred引擎多数采用shading model ID + custom data的形式,再加上在lighting时做动态分支的方法来处理

Clustered Shading

尽管我们觉得tiled based的方法似乎已经能够比较好的解决多光源的问题(至少场景内几百上千个光源没什么问题),但是对现代的3A大作来说,这仍显得不够。现代3A大作的要求是,场景内要数千乃至上万个光源,鉴于我们已经提出了light culling的方案,那我们就需要进一步地思考 如何用更细粒度的数据结构来存储light list ,基于这个思考,比较直观的结果就是clustered shading,这个思路给light list的划分增加了一个维度,即depth(当然也可以再增加normal的维度),它根据view frustum的zmin,zmax把场景进一步根据depth划分成若干个slice(基于指数的划分,通常16个),然后在每个slice上对场景中的所有灯光进行light culling,具体的计算方案和tiled based提到的一些方案类似,只是这里 不再需要处理深度不连续的问题 。所以slice的light list被计算出来后,会经过一个sort和compact的流程(基于compute shader),最终形成的存储结构有三个:

(1)一个3D Texture,用于存储每个depth slice上的光源的起始位置和光源数量;

(2)一个UAV Buffer,存储每个光源的的具体索引,UAV中的offset和num lights由(1)决定;

(3)一个Constant Buffer,存储具体的光源数据。

c638433b61146a692bea80699102ca08.png

整个light list的存储结构

436f1dbb5f756162087e4b5c9e6c895a.png

depth slice的一些划分形式

在实际shading的时候,每个像素根据自己的depth和screen position,找到对应的depth slice,从3D Texture里拿到offset和num lights,再执行一个num lights次循环,从offset处取到UAV Buffer的索引,然后从constant buffer里拿到具体的光源数据执行光照计算。

Tiled Based和Clustered Based的方法除了能够提高bandwidth的利用率,另外一个重要的优势是, 它和forward/deferred方案是正交的 ,这意味着不管你是什么基本管线,都可以使用这个方法处理光源列表,而对于现代引擎来说, 它的渲染管线往往是以deferred为主,结合forward处理一些半透明和特殊材质的混合管线 ,因此tiled/clusterred的方案就更显示出了它们的优势。

结语

有关本文涉及的部分管线在移动端性能对比,可以参见这篇slide。在Siggraph 2017上,这篇slide历数了各种管线和光照的优化方案,并且在指令级别和数据结构的角度提出了许多光照剔除优化的方案,也非常建议大家详细阅读。

其实我常常觉得,哪怕是一个简单的技术,哪怕你每天在和它打交道,或许仍有些许内容你是不了解的。就像本文中的前向渲染和延迟渲染,很难说有那个图形工程师对此没有了解。然而有时候,当你从头梳理这项基本技术的时候,还是会发现许多前人为此付出的努力,并且许多看似过时的技术,当你清楚地理解了它产生的原因和解决的问题后,某些思路仍然能够迁移到其他问题上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 您好,可以使用以下代码实现让一个物体的-X轴指向另一个物体: Vector3 direction = target.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(-direction, Vector3.up); ### 回答2: 在Unity,我们可以通过编写脚本来使一个物体的-X轴指向另一个物体。首先,我们需要获取到两个物体的引用。 我们可以使用Transform组件来获取和操作物体的位置和旋转信息。为了使物体B的-X轴指向物体A,我们可以使用以下代码: ```csharp using UnityEngine; public class PointXAxisToObject : MonoBehaviour { public Transform targetObject; // 目标物体的引用 void Update() { if (targetObject != null) { Vector3 direction = targetObject.position - transform.position; direction.y = 0; // 保持在水平平面上 Quaternion rotation = Quaternion.LookRotation(-direction); transform.rotation = rotation; } } } ``` 在这个代码,我们在Update方法实时计算物体A指向物体B的旋转角度。首先,我们通过计算两个物体之间的方向向量,得出指向物体B的方向。然后,我们将该方向向量的Y轴分量设为0,即保持在水平平面上。 接下来,我们使用LookRotation方法将方向向量转换为旋转角度。由于我们希望物体A的-X轴指向物体B,所以需要将方向向量取反,即变为向量的反方向。最后,我们将计算出的旋转角度赋值给物体A的旋转。 在Unity编辑器,你可以将上述脚本添加到物体A上,并将物体B指定为目标物体。这样,每当物体B的位置发生变化时,物体A就会自调整自身的旋转,使-X轴始终指向物体B。 ### 回答3: 在Unity,可以通过以下代码将一个物体的-X轴指向另一个物体: 1. 首先,你需要获取两个物体的引用。假设一个物体是"obj1",另一个物体是"obj2",你可以使用以下代码获取它们的引用: ```csharp GameObject obj1 = GameObject.Find("obj1"); GameObject obj2 = GameObject.Find("obj2"); ``` 确保在场景有名为"obj1"和"obj2"的游戏物体。 2. 接下来,你需要计算出一个指向目标物体的向量。可以使用以下代码: ```csharp Vector3 direction = obj2.transform.position - obj1.transform.position; ``` 这将给出一个从obj1指向obj2的向量。 3. 然后,你可以使用以下代码旋转obj1,使其-X轴指向obj2: ```csharp Quaternion targetRotation = Quaternion.LookRotation(-direction, Vector3.up); obj1.transform.rotation = targetRotation; ``` 上述代码将使用LookRotation函数计算出一个旋转的目标四元数,然后将该四元数分配给obj1的rotation属性,从而将obj1的-X轴指向obj2。 请注意,上述代码假设obj1和obj2已经添加了相应的组件(例如Transform组件)并在场景正确设置了位置。如果物体不在场景物体名字不正确,代码将无法正常工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值