Unity源码剖析之Cull篇

在Unity引擎中,引入了可编程渲染管线(SRP)后,剔除(Cull)操作的执行时机被移到了脚本层。这意味着开发者可以更灵活地控制哪些物体需要被渲染,哪些物体可以被剔除,从而提高渲染效率。以下是Unity引擎在SRP下的Cull过程的详细分析,特别是在2019.4版本中。

Cull的基本流程

  1. 相机设置

    • 当你在场景中添加一个相机时,相机会有一系列的设置,包括视锥体(Frustum)和剔除层(Culling Mask)。这些设置决定了相机能看到哪些物体。
  2. 场景物体的添加

    • 当你通过Instantiate方法在场景中添加可渲染物体时,这些物体会被附加到场景树中,并且每个物体上都会有一个Renderer组件。此时,这些物体并不会立即被渲染。
  3. 剔除过程

    • 在SRP中,剔除的过程通常是在渲染循环的开始阶段进行的。Unity会调用用户自定义的渲染管线中的Cull方法。
    • 在这个Cull方法中,Unity会根据相机的视锥体来判断哪些物体是可见的。具体步骤如下:
      • 视锥体剔除:首先,Unity会检查每个物体的包围盒(Bounding Box)是否与相机的视锥体相交。如果不相交,则该物体会被剔除。
      • 层剔除:接着,Unity会根据相机的剔除层设置,进一步筛选出哪些物体是需要渲染的。
  4. 剔除后的渲染

    • 剔除完成后,Unity会将可见的物体传递给渲染管线进行后续的渲染处理。这包括设置材质、绘制网格等操作。

优化剔除的策略

在SRP中,开发者可以通过以下方式优化剔除过程:

  • 自定义剔除逻辑:开发者可以在自定义的SRP中实现更复杂的剔除逻辑,例如基于距离、视角等条件进行剔除。
  • 使用层和标签:合理使用Unity的层和标签功能,可以帮助快速筛选出需要渲染的物体,减少不必要的计算。
  • 剔除体积:可以使用剔除体积(Culling Volume)来限制某些区域内的物体渲染,进一步提高性能。

结论

在Unity 2019.4版本中,SRP的引入使得Cull的执行时机和方式变得更加灵活和高效。通过将剔除逻辑移到脚本层,开发者可以根据具体需求自定义剔除策略,从而优化渲染性能。在实际开发中,合理利用相机设置、层和标签,以及自定义剔除逻辑,可以显著提高游戏的渲染效率。

1、RendererScene

在Unity引擎的渲染系统中,RendererScene的设计是为了高效管理与渲染相关的节点和数据。以下是对RendererScene的详细分析,特别是它如何与场景节点(SceneNode)和渲染节点(RendererNode)进行交互,以及如何处理可见性和边界框(AABB)信息。

RendererScene的结构与功能

  1. Renderer Nodes的构建

    • 在构造场景节点的同时,RendererScene会为每个有效的渲染组件创建对应的渲染节点(RendererNode)。这意味着每当一个场景节点被激活并且其渲染组件处于启用状态时,它就会被添加到RendererScene中。
    • 具体来说,Renderer AwakeFromLoadSetEnabledDeactivate等方法会触发对RendererScene的更新,分别调用AddRendererRemoveRenderer来管理渲染节点的增删。
  2. 多线程安全性

    • 在渲染过程中,可能会出现多线程对渲染节点的访问和修改。为了避免并发问题,RendererScene使用了Pending List来延迟增删操作。这种设计允许在渲染过程中安全地管理节点的状态,确保渲染数据的一致性。
  3. 数据组织与加工

    • 在初步组织了渲染节点后,RendererScene需要进一步整理和加工中间数据,以便为后续的渲染过程做好准备。这包括计算每个渲染节点的边界框(AABB)和可见性信息。

RendererScene中的关键数据

RendererScene主要维护以下三类数据:

  1. SceneNode

    • 这是与渲染节点直接关联的场景节点。每个渲染节点都对应一个场景节点,提供了渲染所需的变换和其他信息。
  2. AABB BoundingBox

    • AABB(Axis-Aligned Bounding Box)是一个用于包围渲染对象的边界框。它在渲染过程中用于快速判断物体是否在相机的视锥体内,从而决定是否需要渲染该物体。虽然在初步组织时并未计算AABB,但在后续处理中会根据场景节点的变换和几何信息来计算。
  3. VisibilityBits

    • 可见性信息(VisibilityBits)用于标记每个渲染节点的可见性状态。这通常是一个位图,表示该节点在当前渲染帧中是否可见。可见性信息的计算通常依赖于相机的视锥体剔除和其他条件。

数据结构示例

RendererScene中,这些数据通常以动态数组的形式存储,以便于在运行时进行增删改查操作。以下是一个简化的示例:

// These arrays are always kept in sync
dynamic_array<SceneNode>    m_RendererNodes;      // 存储渲染节点对应的场景节点
dynamic_array<AABB>         m_BoundingBoxes;      // 存储每个渲染节点的AABB
dynamic_array<UInt8>        m_VisibilityBits;     // 存储每个渲染节点的可见性信息

总结

RendererScene在Unity的渲染系统中扮演着重要的角色,它负责管理与渲染相关的节点和数据。通过使用Pending List来处理多线程增删操作,RendererScene能够在高效和安全的环境中组织渲染数据。随着渲染过程的推进,RendererScene会继续计算和更新AABB和可见性信息,为最终的渲染做好准备。这种设计不仅提高了渲染效率,也为开发者提供了更大的灵活性。

2、可见性更新

在Unity引擎中,RendererScene负责管理渲染节点的可见性信息。可见性更新是渲染循环中的一个重要环节,确保只有在当前帧需要渲染的物体被通知并处理,从而提高性能。以下是对可见性更新过程的详细分析。

可见性状态

每个渲染节点的可见性信息(VisibilityBits)有四个状态(mask):

  1. 不可见(0)
  2. 当帧应该可见kVisibleCurrentFrame
  3. 上一帧可见kVisiblePreviousFrame
  4. 已经可见kBecameVisibleCalled

这些状态帮助引擎跟踪每个渲染节点在不同帧中的可见性变化。

可见性更新流程

  1. Early Update阶段

    • 在Unity的更新循环中,RendererNotifyInvisible是一个属于EarlyUpdate类别的步骤,发生在渲染的早期阶段。此时,RendererScene会遍历每个渲染节点,检查其可见性状态。
  2. 遍历渲染节点

    • 对于每个渲染节点,检查其VisibilityBits
      • 如果状态为“上一帧可见”(kVisiblePreviousFrame),则认为该节点在当前帧不可见,并调用RendererBecameInvisible通知相关组件。
      • 更新可见性状态:
        • 如果当前状态为“当帧应该可见”(kVisibleCurrentFrame),则更新为“上一帧可见”。
        • 否则,更新为“不可见”。
    int nodeCount = m_RendererNodes.size();
    for (int i = 0; i < nodeCount; ++i) {
        SceneNode& node = m_RendererNodes[i];
        UInt8& vbits = m_VisibilityBits[i];
        if (vbits == kVisiblePreviousFrame) {
            static_cast<Renderer*>(node.renderer)->RendererBecameInvisible();
        }
        // Roll visibility over to next frame
        vbits = (vbits & kVisibleCurrentFrame) ? kVisiblePreviousFrame : 0;
    }
    
  3. Cull阶段

    • 在可见性更新后,进行剔除(Cull)操作。Cull的结果会影响哪些物体在当前帧是可见的。Cull完成后,RendererScene会通过SceneAfterCullingOutputReady回调进行可见性更新。
  4. 更新可见性位

    • 对于静态和动态物体,更新其可见性位:
      • 对于静态物体,遍历可见列表,将其状态更新为“当帧应该可见”。
      • 对于动态物体,类似地更新其状态。
    // Update visibility bits for static objects
    for (int i = 0; i < visible[kStaticRenderers].size; ++i) {
        int index = visible[kStaticRenderers].indices[i];
        m_VisibilityBits[index] |= kVisibleCurrentFrame;
    }
    // Update visibility bits for dynamic objects
    size_t offset = GetStaticObjectCount();
    for (int i = 0; i < visible[kDynamicRenderer].size; ++i) {
        int index = visible[kDynamicRenderer].indices[i] + offset;
        m_VisibilityBits[index] |= kVisibleCurrentFrame;
    }
    
  5. 通知可见性变化

    • 对于当前帧可见的渲染节点,调用RendererBecameVisible通知相关组件,并设置kBecameVisibleCalled标志。
    for (unsigned i = 0, n = m_RendererNodes.size(); i < n; ++i) {
        SceneNode& node = m_RendererNodes[i];
        UInt8& vbits = m_VisibilityBits[i];
        if (vbits == kVisibleCurrentFrame) {
            static_cast<Renderer*>(node.renderer)->RendererBecameVisible();
            vbits |= kBecameVisibleCalled;
        }
    }
    

总结

可见性更新是Unity渲染循环中的一个关键步骤,确保渲染系统能够高效地管理哪些物体在当前帧需要被渲染。通过维护VisibilityBits,引擎能够在每一帧中跟踪物体的可见性状态。

3、AABB Bounds更新

在Unity引擎中,AABB(Axis-Aligned Bounding Box)更新是渲染和剔除(Cull)过程中的一个重要环节。AABB用于快速判断物体是否在视锥体内,从而决定是否需要进行详细的渲染计算。以下是关于AABB更新的详细分析。

AABB更新的时机

AABB的更新必须在剔除(Cull)之前进行,但不能过早。原因如下:

  1. Transform更新:在一帧中,物体的Transform(位置、旋转、缩放)可能会发生变化,因此AABB的更新需要在Transform更新之后进行。
  2. Renderer特定更新:某些Renderer(如ParticleSystemRenderer和SkinnedMeshRenderer)需要根据每个粒子或当前动作来更新AABB,因此需要在这些Renderer完成更新后再进行AABB的更新。

因此,AABB的更新通常发生在PostLateUpdate阶段的UpdateAllRenderers中。

AABB和WorldMatrix的更新

RendererUpdateManager::UpdateAll中,AABB和WorldMatrix会被更新。更新WorldMatrix的原因包括:

  • MVP矩阵构建:Renderer在渲染时需要构造模型-视图-投影(MVP)矩阵,而WorldMatrix是构建MVP矩阵的基础。因此,Renderer需要在每一帧中更新WorldMatrix。
  • Transform Chain:虽然其他Scene Node的Transform可以按需更新,但Renderer需要在每帧中确保其WorldMatrix是最新的,以便进行正确的渲染。

AABB的更新策略

不同类型的Renderer可能有不同的AABB更新策略。引擎设计上将更新的实现路由给每种Renderer,以便于管理和优化。

以MeshRenderer为例

InitializeClass时,MeshRenderer会注册其更新方法:

GetRendererUpdateManager().RegisterDispatchUpdate(kRendererMesh, TransformChangeSystemMask(0), DispatchUpdate, PrepareDispatchUpdate, PrepareSingleRendererUpdate, FinalizeUpdate);

这里的TransformChangeDispatch是一个监控Transform变化的框架,能够处理平移、旋转、缩放和Local Bounds变化。所有Renderer监控的Mask都包含:

  • TransformChangeDispatch::kInterestedInGlobalTRS
  • TransformChangeDispatch::kInterestedInRendererUpdate

这使得在一帧内发生的Transform变化能够被有效地传递给注册的方法。

AABB更新的具体过程

RendererUpdateManager::UpdateAll中,系统会遍历每种Renderer并依次进行Prepare、Update和Finalize操作。以MeshRenderer为例,更新过程如下:

  1. 读取Transform

    TransformAccessReadOnly transformAccess = changedTransforms[i];
    
  2. 计算WorldMatrix

    affineX worldMatrix = CalculateGlobalMatrix(transformAccess);
    
  3. 加载Local AABB

    aabb localAABB = aabbLoad(&renderer.GetWritableTransformInfo().localAABB);
    
  4. 计算World AABB

    aabb worldAABB = aabbTransform(worldMatrix, localAABB);
    
  5. 存储WorldMatrix和AABB

    StoreWorldMatrix(renderer, worldMatrix, CalculateHierarchyTransformType(transformAccess), jobData->motionVectorFrameIndex);
    StoreAABB(renderer, jobData, worldAABB);
    

总结

AABB的更新是Unity渲染流程中的一个关键环节,确保在剔除之前,所有Renderer的包围盒信息都是最新的。通过在PostLateUpdate阶段更新AABB和WorldMatrix,Unity能够高效地管理渲染对象的可见性,从而提高渲染性能。不同类型的Renderer通过注册各自的更新方法,实现了灵活的更新策略,确保了系统的高效性和可扩展性。

4、CullScriptable

在Unity的渲染管线中,剔除(Cull)是一个至关重要的步骤,它决定了哪些对象需要被渲染,哪些可以被忽略,从而提高渲染效率。Cull的过程与相机密切相关,因为每个相机都有自己的视锥体和剔除参数。以下是关于CullScriptable的详细分析。

Cull与相机的关系

在Unity中,场景可以包含多个相机,而每个相机的渲染过程都是独立的。RendererScene的更新是每帧一次,而Cull和渲染则是针对每个相机进行的。在通用渲染管线(URP)中,RenderSingleCamera是每个相机的入口点。

  1. CullingParameters:每个相机都有一组CullingParameters,这些参数包括LayerMask、裁剪平面等信息。这些参数在Native层处理,但可以通过URP封装的相机参数进行覆盖,以适应特定的渲染需求。

  2. Cull的开始:一旦CullingParameters准备好,就可以调用Cull方法进行剔除:

    var cullResults = context.Cull(ref cullingParameters);
    

Cull的实现

在Native层,Cull的实现可以分为几种情况:

1. BatchRendererGroup

BatchRendererGroup允许开发者在脚本层进行批量绘制。它的优势在于:

  • GPU实例化:通过批量绘制,可以利用GPU实例化技术,减少绘制调用的数量,从而提高性能。
  • 自定义Cull:开发者可以通过OnPerformCulling方法自定义Cull逻辑,而不是逐个物体进行Cull。这种方式可以将多个Renderer合并为一个Batch进行处理,减少更新和Cull的开销。
2. CullingGroup

CullingGroup是Unity提供的一个脚本层API,允许开发者维护一组包围球(Bounding Sphere)并管理它们与物体(或Renderer)的对应关系。其工作流程如下:

  • 维护包围球:开发者可以创建和管理多个包围球,并将它们与场景中的物体关联。
  • 可见性检查:在进行Renderer的Cull之前,Unity会先对CullingGroup进行Cull,检查哪些包围球是可见的。
  • 增量更新:在Cull完成后,RendererScene会进行UpdateAll更新,但只更新变化的部分,而不是全量更新,这样可以减少Cull的负载。
3. 特殊系统(如Terrain)

对于一些特殊类型的系统(如Terrain),Unity使用QuadTree等数据结构进行高效的剔除。这种方法能够快速判断哪些区域是可见的,从而优化渲染性能。

4. RendererScene的Cull

最后,RendererScene会进行实际的Cull操作。此时,所有的剔除逻辑和可见性判断都已经完成,RendererScene会根据Cull的结果更新需要渲染的对象。

总结

CullScriptable在Unity的渲染管线中扮演着重要角色,它通过与相机的紧密结合,确保了渲染过程的高效性。通过BatchRendererGroup和CullingGroup等机制,Unity能够在CPU和GPU之间实现高效的批量处理和剔除,减少不必要的计算和绘制调用,从而提升整体性能。对于特殊类型的对象,Unity还提供了更为高效的剔除策略,如QuadTree等,进一步优化了渲染流程。

5、CullScene

在Unity的渲染管线中,CullScene的过程是一个复杂而高效的步骤,旨在确保只有可见的渲染器被传递给渲染模块。以下是CullScene的详细分析,涵盖了从Renderer的分类到最终可见Renderer Nodes列表的生成过程。

Renderer的分类

在Cull开始之前,RendererScene会通过UpdateAll方法更新所有的Renderer,确保获取到最新的Renderer状态。接下来,调用PrepareCullingParametersRendererArrays将Renderer进行分类,分类的枚举如下:

enum
{  
  kStaticRenderers = 0,  
  kDynamicRenderer,  
  kSceneIntermediate,  
  kCameraIntermediate,  
  kTreeRenderer,  
  kCustomCullRenderers,  
  kStandardVisibleListCount,  
  kSpriteGroup = kStandardVisibleListCount,  
  kBatchRendererGroupsStart  
};  

这些分类有助于在Cull过程中快速筛选出不需要渲染的对象。

Cull的过程

Cull的过程使用了Job System来提高性能,具体步骤如下:

  1. LOD Mask和Layer Mask筛选

    • 首先,Cull会检查每个Renderer的LOD Mask和Layer Mask。这一步骤可以快速剔除掉与当前相机视图无关的Renderer,减少后续计算的负担。
  2. 视锥体裁剪

    • 接下来,Cull会使用优化过的投影体(如平头椎体或长方体)与Renderer的AABB(轴对齐包围盒)进行碰撞检测。通过计算,判断AABB是否在视锥体的内部,从而进一步筛选出可见的Renderer。
  3. 基于相机Layer的Distance Cull

    • 最后,Cull会根据相机的Layer设置进行距离剔除。不同的Layer可以设置不同的裁剪距离,默认情况下,Renderer会被剔除掉超过远裁剪面的对象。

数据处理与合并

由于Renderer的数量可能非常庞大,Cull过程中的每个Job会将输入分成多个Blocks,每个Block包含一部分Renderer Nodes。经过前面的筛选后,可能会出现不紧密排列的情况,因此需要一个额外的Job来重新组合这些数据。

  • 可见Node的Index:在这个过程中,蓝色表示可见的Node的Index,而灰色表示不可见的Node的Index(如图3所示)。

CullingOutput与CullResults

经过视锥体裁剪后,Cull的结果会存储在CullingOutputvisible中。这个输出按照Renderer的分类存储,每个List中包含对应的Node Index。

  • Job Fence同步:所有的Jobs会通过sceneCullingOutputIsReady JobFence进行同步,确保所有的Cull操作完成后再进行下一步处理。

  • 数据加工到CullResults:在所有的Cull操作完成后,存储的Index需要进一步加工到CullResultsrendererCullCallbacks中,最终形成Renderer Nodes的列表。

最终结果

经过以上的处理,最终得到的可见Renderer Nodes列表是可以交付给渲染模块进行渲染的。然而,在实际渲染之前,可能还需要进行一些额外的处理,例如:

  • 排序:根据渲染顺序对可见的Renderer进行排序,以优化渲染性能。
  • 状态更新:确保所有Renderer的状态(如材质、变换等)都是最新的,以便在渲染时能够正确显示。

总结

CullScene的过程通过高效的分类、筛选和数据处理,确保了只有可见的Renderer被传递给渲染模块。这一过程不仅提高了渲染效率,还减少了不必要的计算和资源消耗,为Unity的实时渲染提供了强有力的支持。

6、交付渲染前的操作

在Unity的渲染管线中,交付渲染前的操作是一个至关重要的步骤,确保所有可见的渲染对象(包括Renderer Nodes、Lights和Reflection Probes)都被正确处理和准备。以下是这一过程的详细分析:

1. 处理可见的对象

在Cull完成后,首先需要处理的不仅是可见的Renderer Nodes,还包括与渲染相关的Lights和Reflection Probes。这些元素同样会影响最终的渲染结果,因此需要在Cull后进行相应的更新和准备。

2. 通知可见的Renderers

调用afterCullingOutputReady方法,通知所有可见的Renderers。这一时刻,RendererScene会进行UpdateVisibility,更新每个Renderer的可见性状态。同时,所有与Renderer相关的MonoBehaviour(如脚本层的Renderer Component)会接收到OnWillRenderObject消息,允许它们在渲染前进行必要的准备。

3. DispatchGeometryJobs

在Cull过程中,DispatchGeometryJobs是一个关键环节。尽管它看似与Cull没有直接关系,但它实际上负责调用rendererCullingOutputReady回调,传递Cull筛选出来的Renderer Nodes。

  • 复杂Renderer的处理:大多数Renderer并不需要对Cull结果进行额外操作,但某些复杂的Renderer(如ParticleSystemRenderer)需要进行额外的处理。这些Renderer的渲染过程并不是简单地绘制一个Mesh,而是需要根据Cull结果生成实际的绘制对象。例如,ParticleSystemRenderer需要计算每个粒子并生成对应的面片,同时将数据动态批处理到VBO(Vertex Buffer Object)、IBO(Index Buffer Object)等渲染缓冲区中。

4. 组织渲染数据

在Cull完成后,得到的是Renderer实例的列表,但直接将这些实例用于渲染显然不够精细。因此,引入了SharedRendererScene的概念,它与RendererScene不同,专门用于存储实际的Render Node。

  • 提取Render Node:通过ExtractSceneRenderNodeQueue方法,从Renderer实例中提取Render Node。每个Renderer会通过RegisterPrepareRenderNodesCallback注册Prepare Render Node的方法。

  • MeshRenderer的准备:以MeshRenderer为例,如果Mesh没有经过Prepare,会调用PrepareMesh,主要构造渲染所需的VBO、IBO等渲染缓冲区。同时,最重要的rendererSpecificData会被赋值,对于MeshRenderer来说,这就是MeshRenderingData

5. 注册渲染回调

最后,Renderer会注册一系列的回调函数,包括executeBatchedCallbackexecuteCallbackcleanupCallback等。这些回调函数负责对每个Render Node(或这一批Render Node)进行渲染。

  • 回调的作用:这些回调函数的注册和执行是渲染过程中的重要环节,确保每个Renderer在渲染时能够正确处理其对应的Render Node。虽然这些细节在分析渲染时会更为清晰,但在此阶段,了解它们的目的和大致功能是足够的。

总结

在交付渲染前的操作中,Unity通过一系列的步骤确保所有可见的Renderer、Lights和Reflection Probes都被正确处理。通过Cull、通知、几何调度、渲染数据组织和回调注册等环节,最终形成一个高效且精细的渲染准备过程。这些步骤不仅提高了渲染效率,还确保了渲染结果的准确性和质量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值