OpenShader月(nian)报 #2

咕咕,咕咕咕
(ge le, qi keng le)

时隔了很久之后...终于有了第三篇月报,在上一篇中我说到5月要肝毕设,那么,之后的时间? 之后的时间确实没有认真肝OpenShader,有很长一段时间在摸鱼,所以这段时间的进度怎么看都不像半年时间该做出来的...

  • Shadow Caster的裁剪

    在之前,阴影渲染也占据了不少性能开销,就像以前我提到在早期版本测试时使用3个阴影图的CSM时性能就急剧下降了,虽然池化VBO有效降低了渲染时的Drawcall开销,但在MC准备渲染数据时的开销依然不少,因此最好的办法就是在Shadow Caster中也进行一次裁剪,去除掉不能对玩家的视角形成阴影的物体和地形.

    关于裁剪体的计算,先将玩家镜头的视锥体朝向光源镜头的近裁面做一次投影,在投影得到的二维图形中找出最外层的边,每一个边沿着光源镜头的视线方向延伸得到面后,这些面就成了构成裁剪体的裁面.在这个裁剪体范围之内的物体大部分都能对玩家视角内行成阴影.如果说还有什么不完善的话,主要是刚才得到的裁剪体不包括近裁面和远裁面,对太阳光来说近裁面是没有必要的,而不计算远裁面就导致了一些本来不会贡献阴影的物体也被渲染了,会增加一些不必要的开销,一个未来的改进是选择玩家镜头的视锥体中,正面朝向光源的那些裁剪面,用这些裁面再做一次裁剪,就能去除掉那些false positive了.

    举例,比如如果这张图是一个渲染场景,中间高亮的橙色椎体是玩家镜头的视锥体,左上方为环境光源及其方向,圆锥、圆柱、棱角球和方块分别是4个可渲染的物体,那么在无Shadow Caster的裁剪时,这4个物体全部需要渲染.

    使用现在的视锥构建方法时,圆柱被剔除掉了,因为它即不在玩家视线内,也不会给玩家视线内的物体贡献阴影,然而方块仍然会被渲染,尽管它不会贡献阴影.

    最终目标是制作一个这样的裁剪体,保留下的物体只有圆锥 - 它会给玩家视线内贡献阴影,以及棱角球 - 它在玩家视线内.

  • 重新设计的多线程渲染流程
    显然,在OpenGL中不会有真正意义上的高性能多线程渲染(疯狂切context确实可以多线程,但它不够高性能),因此我们设计的多线程渲染流程也和其他的同类产品一样,努力压榨出更多的可在CPU端并行处理的东西,比如裁剪、收集渲染信息等,然后将它们分派到子线程上执行,主线程只保留最必要的工作:执行渲染指令.
    那么,我们先来看看上次月报(17年5月,应该叫半年报了😭)时的渲染流程:

    在旧的渲染流程中,整个渲染被分为准备阶段和渲染阶段,在准备阶段程序会收集并配置渲染信息,主要工作分为镜头(计算每一个摄像机的位置和MVP矩阵)、地形(统计哪些RenderChunk需要被渲染)和实体(统计哪些实体和TileEntity需要被渲染)这三部分,此三部分有顺序依赖,不可调换顺序(地形需要镜头的信息来进行视锥裁剪,实体需要知道哪些区块要被渲染). 在旧设计中这三部分都会在准备阶段进行,尽管主线程在处理到每个部分时都会将工作分发给工作线程来进行,从而实现了并行化,但这里还有一个问题 - 主线程被完全浪费了,事实上渲染阶段其实无需等待准备工作全部完成时才进行,有些渲染操作并不涉及到地形和实体,比如天空背景和云朵;此外渲染阶段多线程的利用率非常低 - 只用到了两个线程. 因此我们重构的目标时尽可能缩短准备阶段,并尽量提高渲染阶段的多线程化程度,最终重新设计的渲染流程是这样的:

    在新的渲染流程中,只有镜头配置必须在准备阶段进行,其余的工作都是在渲染阶段进行,在启动渲染阶段时会有三个线程开始工作,主线程等待并处理渲染指令,一个工作线程生成渲染指令,另一个工作线程会串行处理地形的可见性判断,为什么这一步要单线程处理? 这涉及到它的工作方式,地形的可见性判断是用来找出哪些RenderChunk需要被渲染,主要的手段一是视锥裁剪,二是以玩家所在的RenderChunk为起点做一次广度优先搜索,为了防止搜索时重复搜索已处理过的节点,MC会在RenderChunk中标记一个时间戳,每次搜索到节点时都会尝试更新时间戳,如果更新成功(节点时戳比系统时戳旧)那么这个RenderChunk就是未被处理过的,如果更新失败(节点时戳与系统时戳一致)那么这个节点就是已经被处理过的节点,在多遍渲染时程序需要多次进行可见性判断,比如在渲染阴影时,需要有一个pass处理ShadowMap绘制的地形,一个pass处理玩家视角绘制的地形,如果想在此步骤并行处理的话,就需要把时戳换成一个原子整数,然后为每个pass分配一个bitflag,当此pass处理完一个节点时,就给那个原子整数标记上bitflag,显然,当所有的pass处理完毕之后还得再将那个原子整数清零,在旧的渲染流程中我就是这样做的,其结果呢? 嗯...确实是并行化了,但是在性能测试中发现更新原子变量的操作非常的慢,特别是最后重新清零的那个步骤,相比之下,之前串行处理时就不需要这个步骤,因为时戳始终是递增的,在处理完一个pass后,只要将系统时戳再喜+1就能继续处理下一个pass. 因此再三斟酌后,我将这一步换回了由一个线程串行处理所有pass的可见性判断. 但是之后的工作依然是并行处理,每当一个pass完成可见性判断后,程序就会再给线程池分配两个新任务 - 生成这个pass的地形渲染指令,以及准备实体的渲染信息.

  • 帧缓冲的Pingpong

    以前我在Shadersmod教程中提到过不要在一个着色器中对一个RenderTarget即读即写,因为在Texture Barrier出现之前对一个纹理在读出的同时进行写入属于未定义的行为,其结果是不可预料的,因此一般采用的策略是为一个需要即读即写的RenderTarget准备两个纹理,一个纹理作为读纹理,一个作为写纹理,每次运行完渲染后交换两个纹理的职责,原本读的下次变成写的,原本写的下次变成读的,这个方法被称为Pingpong. Shadersmod没有提供Pingpong功能,因此我们只能自行回避即读即写操作(其实回避的是异处读写,在片元着色器中原地读写的话在大部分显卡上实际上是允许的). 而OpenShader提供了Pingpong功能,在默认配置下程序会自动检测每个stage的帧缓冲和绑定的纹理,如果检测到一个纹理即被绑定到输入又被绑定到输出的话,就会自动启用Pingpong.

  • 图像统计 (WIP)

    图像统计之前我们已经介绍过了,主要是给像HDR之类的后处理特效用,不过...现在的OpenShader的图像统计功能非常菜鸡,仅仅只是达到了Shadersmod的水平,换句话说,就是只提供了获取屏幕中央深度的功能... 这个功能其实很鸡肋,想知道深度缓冲中央的值,直接给我texture(gdepth, vec(0.5, 0.5)).r不就得了嘛! 将这个值做成一个uniform除了卡流水线以外没任何用处,尽管中央深度可以通过将其替换为纹理采样来规避(怎么替换将是下一期月报的内容(豹笑)),但像统计全屏幕平均亮度这样的操作就只能靠实打实的图像统计来实现了,因此图像统计这方面还有不少工作要做...

  • RenderChunk渲染参数缓存 (WIP)

    这个...应该是最让我崩溃的部分,大部分功能在去年8月末就完成了,但由于两个折磨人的Bug导致它一直到现在都没彻底完工.

    简单地说,在之前准备渲染参数时需要程序逐一获取每一个RenderChunk的参数,这涉及到了大量的内存访问,以及随之而来的cache miss,即使是原本很简单的操作也会花费大量的时间,渲染参数缓存就是提前将这些参数缓存在一个整数数组中,将所有操作都简化成数组的复制和整数操作 -- 利用了OpenGL所有对象都以整数句柄的形式存在的特点. 比如渲染一个池化的RenderChunk时需要知道它在哪个VBO中、它的首地址偏移量和顶点数量,这些参数都可以缓存在整数数组中.

    这个优化被证明为是成功的,确实能提高不少性能,然而却带来了两个新bug.

    第一个bug是当玩家向任意一个方向移动一定距离时,就会发现反方向消失的区块会在面前出现...事先说明,这个bug已经被解决了,某天在我肝了几宿HOI4后决定再看看能不能解决这个让我崩溃了两个多月的bug,结果在灵光一现后(我一直笃信debug的时候灵感是最重要的 233)几个骚断点找出原因了,原因是我对MC自带的RenderChunk表(我对它的称呼,MCP把那个类称为ViewFrustum...咳)的理解不充分,我原本以为它会完整地利用所有空间,也就是表中每一项都会是一个可见的RenderChunk,然而实际上MC并没有充分利用每一个空间,在某些情况下,比如游戏载入之后玩家向某一个方向持续移动时,会出现某些项中的RenderChunk存在但却不该被渲染的情况. 原版MC中通过一个RenderChunk中的标记来判断是否该被渲染,而我在编写渲染参数缓存时没能正确缓存这个属性,结果导致了这个问题.


    远处那一排非常突兀的地形就是bug所导致的情况,实际上它们应该是在我背后过来的地方的某排区块.

    而第二个bug...则把我困惑到怀疑人生. 简单地说,在设计上,当内存池发生变化时,变化的内容(VBO的id、指针偏移量、数据长度)会被立刻刷新入缓存,然而现在存在的问题是在罕见的情况下缓存和实际内容会有一帧的不同步,结果导致因使用了错误的参数而渲染出错.


    出错的一帧的截屏

    这个bug一直到现在我都没能解决...最近倒是想出了一个workaround,暂时还没有实现,看看下一次更新前能不能做完吧 (鸽?)

  • 一些...杂七杂八的东西

    因为没有changelog,所以更新了什么东西都是靠翻git的变更记录来一点点查的(笑),主要是修bug和一些优化,其实现在OpenShader的状态是三十六拜后就差一哆嗦,然而这一哆嗦就是几个月都没哆嗦出来 😂 这段时间一直都在忙别的,看看这俩月能不能把最后一点肝出来吧,下一篇日志我们会介绍一些native方面的工作--