如何搭建一个适合自己的高级渲染引擎?

如何搭建一个适合自己的高级渲染引擎?

设计要求

1、无需参考任何成熟的商用引擎。 根据自己的设计需求编写渲染引擎。 实现后游戏引擎中的api 虚拟现实系统,与成熟的商业引擎进行比较,看看它们是如何设计的。

2、引擎支持多种图形API,如DX12、Vulcan等。

3、引擎采用ECS架构。

4.引擎更多的是为了学习,所以会设计得更容易理解。 因此,我们并不追求极致的性能和存储空间。

5.能够支持遮挡消除、高级光影效果、大气、海洋、植被等复杂场景。

为什么要使用ECS架构

我只是觉得ECS是一个很好的架构思想,所以我想通过这个引擎来验证这个架构。

面向对象设计的三个基本原则是封装、继承和多态。 继承的想法是非常不合理的。 例如,当我第一次接触游戏时,我确实对一路继承的PlayerObject对象感到困扰。 后来大家就不再用继承,而用组合了。

封装和组合是面向对象的合理表达方法。 ECS架构非常接近组合的思想,但是将数据和操作分离。 它看起来像是面向对象和面向过程的结合。 实体具有一定程度的封装性。 它告诉我们它有哪些组件(数据)。 Component是数据的抽象,System以面向过程的方式处理这些数据。

如何支持多种图形API

现在让我们设计一个渲染引擎。 第一个问题是一个复杂的场景有n个不同的对象,它们使用不同的着色器和不同的贴图。 我们应该如何组织这些对象才能正确渲染它们。

让我们把问题简单化,忽略细节。 假设场景中有n个实体。 如何正确渲染它们?

游戏引擎中的api 虚拟现实系统,这很简单。 可以通过 for 循环来完成。

void DrawFrame()
{
    for( i = 0; i < entity.count; i++)
    {
        Draw(entity);
    }
    
    Present();
}

我们从图形API的角度来看一下这个Draw需要做什么工作。

我们来重写一下伪代码

void DrawFrame()
{
    for( i = 0; i < entity.count; i++)
    {
        SetVB();
        SetIB();
        SetConstant();
        SetRenderState();
        SetVSAndPS();
        SetTexture();
        ExecuteCommandLists();//执行渲染
    }
    
    Present();
}

现在添加一个要求,每个Entity可以渲染多次,比如轮廓效果。 为了表示一次渲染,我们抽象了一个概念,叫做Pass。 一个Pass代表了一个完整的渲染管线的执行。 每个Pass使用相同的VB和IB,因此不需要重置它们。 对于常量缓冲区,有些属于Entity,有些属于Pass,所以我们将其拆分为EntityConstant和PassConstant,并且VS、PS和纹理可能会被重置。

void DarwFrame()
{
    for(int i = 0; i < Entity.cout; i++)
    {
        SetVB()
        SetIB()
        SetObjectConstant();
        for(int j = 0; j < Pass.Cout; j++)
        {
            SetPassConstant();
            SetVSAndPS();
            SetTexture();
            ExecuteCommandLists();//执行渲染
        }
    }
    
    Present();
}

很高兴我们已经支持具有类似连接效果的多通道渲染。 现在还有另一个问题。 我们希望关闭一些低端设备上的挂机效果,以换取游戏的流畅运行。 我们引入了SubShader的概念来解决这个问题。 因为这是一个学习引擎,我们不会为不同的硬件编写不同的Shader。 但为了后续的可扩展性,我们还是在框架中加入了SubShader。

void DarwFrame()
{
    for(int i = 0; i < Entity.cout; i++)
    {
        SetVB();
        SetIB();
        SetObjectConstant();
        SubShader = SelectSubShader();
        for(int j = 0; j < SubShader.Pass.Cout; j++)
        {
            SetPassConstant();
            SetVSAndPS();
            SetTexture();
            ExecuteCommandLists();//执行渲染
        }
    }
    
    Present();
}

渲染引擎先渲染不透明物体,然后渲染透明物体,所以我们需要对Entities进行排序。 我们需要为每个实体分配渲染优先级。 同时,我们为每个Entity分配一个Material来管理渲染相关的所有数据,我用一个图来表示每个抽象对象的归属关系:

假设场景中有多个物体,分别使用两种材质A和B,那么渲染顺序AAABBB和ABABAB有什么区别? 很明显,使用AAABBB可以降低渲染状态切换的成本。 但如果场景中开启了遮挡剔除,我们需要根据相机的位置进行排序渲染,这样就无法保证AABBBB渲染。 因此,我们的渲染引擎需要根据不同的技术对渲染对象进行分类和排序。

图中绿色部分是资源。 这些资源需要从磁盘加载到内存中,例如模型、纹理、声音和着色器。 读磁盘是一个浪费CPU的操作,所以我们需要管理这些资源。 我们使用参考技术来管理这些资源。 使用引用计数会导致循环引用并导致内存泄漏吗? 这里不行,这些资源只会被别人引用,不会引用其他对象。 您可以放心地使用引用计数。 我们会在适当的时候调用GC来释放没有被引用的资源3D场景,比如切换场景的时候。

针对上述需求,我们根据ECS架构抽象出类:

Entity:代表游戏中的一个实体,包括各种组件如:Transform、meshdata引用、mat引用等。

EntityManager:管理Entity的单个实例

IResource:声明一个用于引用计数的接口

ResourceManager:管理资源的单例

CPTexture:CP前缀代表Component,是CPU端存储纹理数据的类,继承自IResource。

CPMeshData:CPU端存储顶点数据的类开发学习,继承自IResource。

CPShader:对应ID3DBlob类。 需要与其他图形API区分开来。 游戏开始后它会驻留在内存中。

CPPipelineState:对应于 ID3D12PipelineState 类。 需要与其他图形API区分开来。 游戏开始后它会驻留在内存中。 CPPipelineState 的数量应该与 Mat 的数量一样多。

CPObjectConstant:主要是坐标变换矩阵。

CPPass:包含 PassConstant 数据、纹理和对 PipelineState 的引用。

CPSubShader:包括多个CPPass。

CPMaterial:包含多个CPSubShader和RenderPriority。

CPRenderQueue:渲染队列,存储EntityID,用于对Entities进行排序。

STFBXLoader:S是System的缩写,它将FBX文件加载到CPMeshData中。

STTextureLoader:将纹理文件加载到CPTexture。 不同的图形API都会有预编译的代码。

STMaterialLoader:解析Mat文件,类似于解析Unity Shader文件。 编译原理大师登场,又是一个大工程。

STSceneLoader:使用场景图来管理场景资源。

STOctreeSceneManager:八叉树管理场景中的实体并执行视锥体剔除。 如果继续使用KD树剔除,标记是否显示Entity。 如果不进行KD消除,则将通过测试的Entity直接放入CPRenderQueue中。

STKDtreeScaneManager:KD树管理场景中的Entity,进行视锥体剔除和遮挡剔除,并将通过测试的Entity放入CPRenderQueue中。

STPipeline:组织渲染过程的类,比如先进行相机剔除,然后对RenderQueue进行排序,最后根据RenderQueue来渲染各个对象。 您可以在此处配置前向渲染或延迟渲染。 会调用各种系统来完成相关工作,其他系统就不一一列举了。 随着功能的增加,各种系统也会陆续增加。

再加上一个接口,一个垃圾渲染引擎就诞生了。

文章来源:https://blog.csdn.net/liran2019/article/details/107286338