渲染引擎开发之框架搭建

本来只是想学习一下最新的图形API,并在基础上做一些图形相关的技术尝试。但是后来发现,要想深入学习这两块知识,必须自己动手搭建一个渲染引擎,渲染引擎大部分的工作其实和图形技术无关,更多的是合理的规划资源以及对图形API的封装,用以支撑一个复杂的渲染场景。而图形相关的技术是在渲染引擎的基础上实现各种期望的效果。渲染引擎是基础,尝试开发一个渲染引擎并在自己的引擎上尝试各种渲染效果,才是学习的正确打开方式,戒骄戒躁,不要被各种高级渲染效果迷乱了双眼,基础才是最重要的。这篇文章是设计文稿,并不是对代码的总结,而且本人能力和精力有限,文章中写的内容不一定正确,还是请大家带着审视的态度阅读文章,如果发现错误,真诚的希望您能指正出来。

设计需求

1.不需要参考任何成熟的商业引擎,按照自己的设计需求写一个渲染引擎,实现后再去对比成熟的商业引擎看看人家是怎么设计的。

2.引擎支持多种图形API,比如DX12及Vulcan。

3.引擎采用ECS架构。

4.引擎更多的是为了学习,因此在设计上会采用更容易理解的方式去设计。因此不追求极致的性能和存储空间。

5.能够支撑诸如遮挡剔除,高级光影效果,大气,海洋,植被等复杂场景。

为什么要使用ECS架构

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

  • 数据和逻辑分离
  • 更友好的缓存命中
  • 更适合多线程编程
  • 结构更清晰

面向对象设计三大基本原则封装,继承,多态。其中的继承思想很不合理,比如最开始接触游戏的时候,对一路继承过来的PlayerObject对象真的很头疼,后来大家都不用继承改用组合了。

封装,组合才是面向对象合理的一种表现方式。ECS架构很接近组合的思想,只是把数据和操作拆分出来,看上去像是面向对象和面向过程的结合。Entity有一定的封装性,它告诉我们它拥有哪些组件(数据),Componet是对数据的抽象,而System就是面向过程的处理这些数据。

如何支持多种图形API

  • 合理的拆分接口比如每个API都有Init,Draw函数,这些接口使用预编译区分。
  • 对于CPU层次的设计是通用的。
  • 针对不同的API接口使用不同的System处理,比如我们可以有DXRenderSystem,VulcanRenderSystem。

现在开始来设计一个渲染引擎,第一个问题,一个复杂的场景有n个不同的物体,它们使用了不同的shader,不同的贴图,我们该如何组织这些物体将其正确的渲染。

我们把问题简单化,先不管细节,假定场景中有n个Entity,该如何正确的渲染?

嗯很简单嘛,一个for循环搞定。

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

我们从图形API的角度来看看这个Draw需要做哪些工作

  • 设置顶点,所以缓冲区,我们把这些数据抽象叫做MeshData。
  • 设置常量缓存区如顶点坐标变换矩阵,相机位置等,我们把这部分内容抽象为Constant。
  • 设置渲染状态,比如深度测试,BlendState,我们把这部分抽象为RenderState。
  • 设置VS,PS
  • 设置贴图

我们改写一下伪代码

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();
}

很好我们已经支持了类似勾边效果的多Pass渲染,现在又来了一个问题,我们希望在一些低端设备上关闭勾边效果来换取游戏的流畅运行。我们引入了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();
}

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

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

图中绿色的部分都为资源,这些资源都需要从磁盘中加载到内存比如模型,贴图,声音,shader。读取磁盘是很浪费cpu的操作,因此我们要对这些资源进行管理,我们采用引用技术的方式管理这些资源,使用引用计数会不会出现循环引用,造成内存泄漏呢?在这里不会,这些资源只会被别人引用,不会引用别的对象,可以放心使用引用计数。我们会在合适的时候调用GC来释放那些没有被引用的资源,比如场景切换的时候。

针对上面的需求我们按照ECS架构进行类的抽象:

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

EntityManager:管理Enity的单例

IResource:声明引用计数的接口

ResourceManager:管理资源的单例

CPTexture:CP前缀代表Component,贴图数据在CPU端存储的类,继承与IResource。

CPMeshData:顶点数据在CPU端存储的类,继承与IResource。

CPShader:对应ID3DBlob类,此处需要与其他图形API进行区分,游戏启动后常驻内存。

CPPipelineState:对应ID3D12PipelineState类,此处需要与其他图形API进行区分,游戏启动后常驻内存,有多少个Mat就应该由多少个CPPipelineState。

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

CPPass:包含PassConstant数据以及贴图,PipelineState的引用。

CPSubShader:包括多个CPPass。

CPMaterial:包含多个CPSubShader及RenderPriority。

CPRenderQueue:渲染队列,里面存储EnityID,用来对Entity做排序。

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

STTextureLoader:加载贴图文件到CPTexture,这里会有针对不同图形API的预编译代码。

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

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

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

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

STPipeline:组织渲染流程的类,比如先进行相机剔除,然后对RenderQueue进行排序,最后按照RenderQueue渲染每一个物体。可以在这里配置前向渲染,也可以配置延迟渲染。里面会调用各种System完成相关工作,其他的System就不列举了。随着功能的增加会陆陆续续添加各种System。

再配上一个界面,一个垃圾渲染引擎就此诞生。

 

 

 

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值