• 如何用WebGPU流畅渲染千万级2D物体:基于光追管线


    大家好~我们已经实现了百万级2D物体的流畅渲染,不过是基于计算管线实现的。本文在它的基础上,改为基于光追管线实现,主要进行了CPU和GPU端内存的优化,成功地将渲染的2D物体数量由4百万提高到了2千万

    相关文章如下:
    如何用WebGPU流畅渲染百万级2D物体?

    本文不需要实现构建和遍历BVH,而是直接使用光追管线提供的加速结构
    本文的重点工作在于对CPU内存和GPU内存的优化,突破内存限制(如突破加速结构最大大小限制),使其支持千万级物体的数据

    本文使用WebGPU Node项目,作者的介绍在这里。它在底层封装了Vulkan SDK,在上层提供了WebGPU API,实现了在Nodejs环境中使用WebGPU API和光追管线来实现硬件加速的光线追踪(需要使用nvdia的RTX显卡)!
    我在2020年就已经基于该项目实现了3D场景渲染,相关介绍如下:
    WebGPU+光线追踪Ray Tracing 开发三个月总结

    需求

    跟百万级的Demo的需求是一样的,除了提高渲染的2D物体数量到千万级,目的是为了探索基于硬件的光追管线的实现能带来的优化极限

    成果

    我们最终能够流畅渲染2千万个圆环

    性能指标:

    • 跟百万级的Demo的FPS一样,为45左右,也就是每帧花费21毫秒

    硬件:

    • Win10操作系统
    • Nodejs环境+Vulkan驱动
    • RTX2060s显卡

    跟百万级Demo的性能比较

    提高的地方:

    • 渲染的物体数量多了4倍

    降低的地方:

    • 构造加速结构的时间多了1.5倍
      不过相信随着RTX显卡的升级,会越来越快

    下面让我们从0开始,介绍实现和优化的步骤:

    1、选择渲染的算法

    跟百万级的Demo一样,选择光线追踪算法

    不过这里需要发送Primary Ray去计算射线与物体的相交,这样才能触发光追管线中关于相交的着色器

    2、实现内存需求

    跟百万级的Demo一样,场景依然使用ECS架构

    3、渲染1个圆环

    光追管线支持两种geometry的类型:
    三角形和AABB包围盒

    因为圆环是参数化的(参数化为圆点坐标、半径、圆环宽度),所以geometry类型使用AABB包围盒

    这种类型geometry被称为“Procedural Geometry”

    光追管线的加速结构跟百万级Demo的TopLevel、BottomLevel类似,分为TLAS(top level acceleration structure)和BLAS(bottom level acceleration structure),如下图所示:
    image

    因为场景只有1个Geometry和1个Instance,所以在BLAS中加入1个aabb(根据Geometry的参数计算aabb,其中aabb的min.z和max.z设为0),在TLAS中加入1个Instance的数据(主要包括该圆环的transform matrix、instanceId)

    现在介绍下光追管线通常包含哪些着色器:

    • .rgen
      每个像素会执行一次着色器,分别产生一条Primary Ray
    • .rint
      该着色器用于AABB包围盒的geometry类型,当Primary Ray与AABB相交时被触发
    • .rchit
      该着色器用来处理Primary Ray与第一个圆环相交的情况
    • .rmiss
      该着色器用来处理Primary Ray与所有圆环都没有相交的情况
    • rahit
      Primary Ray与第一个圆环相交后继续传播,当与第二个及以后的圆环相交时触发该着色器(本Demo没用到该着色器)

    渲染的步骤为:

    1、在.rgen着色器中逐个像素产生Primary Ray
    2、如果Primary Ray与AABB相交,则执行.rint着色器;否则执行.rmiss着色器,将像素设置为黑色
    在执行.rint着色器时:

    • 根据gl_LaunchIDEXT、gl_LaunchSizeEXT得到像素坐标,即为Primary Ray与AABB的相交点
    • 如果该像素坐标在圆环内,则通过reportIntersectionEXT来触发.rchit着色器

    在执行.rchit着色器时:

    • 根据material设置像素颜色(为红色)

    关于Procedural Geometry渲染的参考资料如下:
    Vulkan->Intersection Shader - Tutorial
    D3D12 Raytracing Procedural Geometry sample

    4、渲染10个圆环

    场景中创建10个圆环,它们的ECS数据为:
    10个gameObject
    10个transform
    1个geometry
    1个material

    geometry仍然只有1个,但instance变为10个,所以在TLAS中改为加入10个Instance的数据,BLAS不变

    渲染结果如下图所示:
    image

    5、显示FPS

    我们使用“平均采样法”来计算FPS。这个方法跟我之前实现的深度学习->优化算法->动量法->指数加权移动平均类似,并且那里还做了数学证明。

    这个方法的基本思想就是计算多个相邻帧的平均值,但又不需要额外的空间来保存多帧

    参考资料如下:
    帧率(FPS)计算的六种方法总结

    6、实现剔除

    实现思路如下:
    将TLAS中每个instance的层设为transform matrix中的position.z;
    将Primary Ray的flag设为gl_RayFlagsOpaqueEXT,这样的话Primary Ray与最大层的圆环相交时会触发.rchit着色器,然后就停止传播,从而实现只渲染最大层的圆环

    注:这里的层不再是从1开始的正整数,而是为0.00001, 0.00002这样的小数。这是因为为了性能考虑,将Primary Ray的最大距离设为一个很小的值(如1.0),所以层的值不能超过1.0

    7、测试渲染极限

    当尝试跟百万级Demo一样渲染4百万个圆环时,报了下面的错误:

    Error: Out of memory Error: vkAllocateMemory failed with VK_ERROR_OUT_OF_DEVICE_MEMORY
    

    这是因为WebGPU Node项目底层使用Vulkan来渲染,它申请的单个TLAS的内存容纳不了4百万个instance数据

    所以我们减少圆环数量,发现当其为3百50万个时,能够成功渲染。其中构造加速结构的时间为25秒,FPS为1000(因为这里使用setInterval而不是requestAnimationFrame启动主循环,所以FPS可以大于60)

    8、拆分加速结构

    如果要渲染4百万个甚至更多的圆环,就需要将1个TLAS拆成多个TLAS(BLAS还是不变,只有一个)

    这样的话,如果每个TLAS都有3百50万个Instance数据,那么3个TLAS就可以有超过1千万个Instance数据了

    如何拆分TLAS呢?

    首先,我们可以根据Instance的层,按照下面的方法将所有Instance分为3组:

    • 每组有最小层和最大层这两个数据,将层在其中的Instance放在该组中
    • 每组按照层的大小从大到小排列,使得下一组的最大层小于等于上一组的最小层

    然后,将每组的Instance数据分别传入1个TLAS中;
    然后,创建3个bindGroup,每个bindGroup分别使用1个TLAS;
    最后,在begin ray tracing pass时,绑定对应的bindGroup,traceRays3次,代码如下:

    ...
        let commandEncoder = device.createCommandEncoder({});
        let passEncoder = commandEncoder.beginRayTracingPass({});
        passEncoder.setPipeline(pipeline);
    
        bindGroups.forEach(bindGroup => {
            passEncoder.setBindGroup(0, bindGroup);
            passEncoder.traceRays(
                0, // sbt ray-generation offset
                1, // sbt ray-hit offset
                2, // sbt ray-miss offset
                window.width,  // query width dimension
                window.height, // query height dimension
                1              // query depth dimension
            );
        })
    
        passEncoder.endPass();
    
        queue.submit([commandEncoder.finish()]);
    

    这里我们先对层最大的一组Instance做遍历,然后再对层小一点的一组Instance做遍历,然后再对层更小一点的一组Instance做遍历。。。。。。

    这是一个优化的方法,因为每个像素只渲染最大层的圆环的颜色,所以如果在遍历1个TLAS时Primary Ray与圆环相交而得到了像素的颜色,那么就不再对后面的TLAS进行遍历了!

    9、拆分Instance Buffer

    Instance Buffer存储了所有Instance的组件数据,是一个storage buffer。当Instance的数量大于1千万后,它的大小会超过buffer的最大大小:128MB,会报错

    所以,跟拆分加速结构TLAS一样,我们把Instance Buffer也对应拆分,使得3个bindGroup分别使用1个TLAS和1个Instance Buffer

    10、优化CPU端的内存占用

    之前是在优化GPU端的内存占用,现在要优化CPU端的内存占用

    当Instance数量超过1千6百万时,在CPU端设置TLAS的instances数据时会超出内存大小,报下面的错误:

    FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
    

    现在的实现思路为:

    我们有5组Instance,对应5个TLAS,每个TLAS最多有3百50万个Instance

    首先现在创建了5个数组,每个数组最多保存3百50万个Instance数据;
    然后将对应的数组设置为对应的TLAS的instances数据

    因为这5个数组共包含了1千6百万个Instance数据,超出了CPU端v8的内存限制,所以会报错

    可以做下面两个优化来解决问题:

    • 因为每个数组的最大容量都是一样的(3百50万),所以只创建1个容量为3百50万的数组,通过"arr[index]=instance数据"来保存一个组的Instance数据,然后将其设置给对应的TLAS;然后重复使用该数组,保存下一组的Instance数据,将其设置给对应的TLAS,依次类推。。。。。。
      经过这样的优化, CPU端就只需要分配3百50万个Instance数据的内存了,大大减少了内存占用
    • 使用ArrayBuffer而不是数组来存储每组的Instance数据,这样可以提高速度并且减少内存占用

    11、测试渲染极限

    现在我们来渲染2千万个圆环,测试下FPS

    渲染结果如下图所示:

    image

    性能指标如下:

    • FPS为1000以上
    • 构造加速结构的时间为150秒

    为什么FPS跟渲染3百50万个圆环时一样?
    因为第一个TLAS包含的3百50万个圆环为最高层,它们已经填满了屏幕,所以遍历第一个TLAS后就停止遍历了!

    我们希望测试下最坏的情况,所以强制遍历所有的TLAS,此时FPS为45左右

    当我们尝试渲染大于2千万个圆环时,报了下面的错误:

    Error: Out of memory Error: vkAllocateMemory failed with VK_ERROR_OUT_OF_DEVICE_MEMORY
    

    这说明虽然每个TLAS没有超过最大限制(3百50万个Instance数据),但是应该是超过了总的大小限制,也就是所有TLAS包含的总的Instance数据(大于2千万个)超过了限制!
    除非能够修改WebGPU Node项目中的Vulkan关于TLAS内存分配的代码,否则我们通过WebGPU API无法修改该限制(因为WebGPU Node没有提供相关的WebGPU API)

    12、尝试优化构造加速结构

    在创建BLAS和TLAS时,可以指定为下面几个flag:

    NONE	
    ALLOW_UPDATE
    PREFER_FAST_TRACE
    PREFER_FAST_BUILD
    LOW_MEMORY
    

    当修改为PREFER_FAST_BUILD而不是PREFER_FAST_TRACE,并没有效果!不知道是因为WebGPU Node在Vulkan这层没有处理这个flag还是显卡RTX2060s不支持这个flag?

    其它的优化思路包括:

    • 不需要一次性构造所有TLAS,而是按需构造对应的TLAS
    • 在worker中构造TLAS和BLAS,使其不阻塞主线程

    目前虽然构造加速结构比较慢,但随着RTX显卡的升级,相信会有改善!毕竟这个是由显卡完成的,我们这边能优化的余地较小

    更多分析

    我们来分析下面几个情况:

    为什么“增大圆环geometry的半径,FPS会明显下降”?

    这是因为:
    这会增大圆环的AABB->增加重叠的AABB数量->增大遍历的开销->最终降低FPS

    如果BLAS加入两个圆环的AABB,是否就能渲染4千万个圆环了?

    这样是可以的。之前实现的渲染是渲染2千万个Instance,而每个Instance对应的BLAS只有一个圆环,所以就只渲染了2千万个圆环。

    实际上可以像这样增加渲染的圆环数量,但是因为总的AABB的数量增加了一倍,导致遍历开销增大一倍,所以FPS也会下降一倍

    能不能在相邻的两帧分别绘制不同的2千万个Instance,这样就可以渲染4千万个Instance了?

    这样子的话不仅FPS会下降一倍,而且也是不可行的,因为不管分成几次draw call,都需要把所有Instance数据载入到TLAS中(因为所有的Instance数据只创建一次,除非每帧都创建对应的Instance数据并且把之前的Instance数据销毁!但这样的话CG开销应该会比较大),这样所有的TLAS包含的Instance数据大小一样不能超过2千万个

    可以在相邻的两帧分别绘制不同的1千万个Instance,但这样还不如在一帧中绘制2千万个Instance省事

    总结

    感谢大家的支持和学习~

    本文主要使用光追管线,优化了内存占用,将2D物体数量提升到了千万级

    使用光追管线相比使用计算管线的优点是:

    • 不需要实现BVH,实现和维护更加简单
    • 因为是硬件加速射线相交计算,遍历的性能更好

    缺点是:

    • 兼容性更差
      需要Nodejs环境和RTX显卡
    • 不能使用最新的WebGPU标准
      WebGPU Node项目使用的是2020年版本的WebGPU标准,不是最新的标准

    所以,我们可以考虑在Web版产品中使用浏览器的WebGPU标准(基于计算管线)来实现,而在桌面版产品中基于光追管线来实现

    后续的改进方向

    目前已达到加速结构的内存限制(2千万个Instance),不容易继续增加Instance数量

    后面可以考虑优化基于计算管线的Demo,从百万级提升到千万级;
    另外可以考虑GPU LOD,在计算着色器中替换geometry,从而减少BLAS的占用

    参考资料

    如何用WebGPU流畅渲染百万级2D物体?
    WebGPU+光线追踪Ray Tracing 开发三个月总结
    Vulkan->Intersection Shader - Tutorial
    帧率(FPS)计算的六种方法总结
    WebGPU Ray-Tracing Spec
    GLSL_NV_ray_tracing
    Node.js heap out of memory
    Node.js Default Memory Settings

    欢迎浏览上一篇博文:如何用WebGPU流畅渲染百万级2D物体?

  • 相关阅读:
    MySQL表碎片整理
    MySQL表碎片整理
    MySQL配置索引页的合并阈值
    MySQL InnoDB配置统计信息
    MySQL InnoDB配置统计信息
    代码检查工具sonarqube介绍及使用
    gradle多模块构建集成swagger
    springboot不加载bootstrap.properties文件
    nexus3.x启动不起来
    linux防火墙查看状态firewall、iptable
  • 原文地址:https://www.cnblogs.com/chaogex/p/16557207.html
Copyright © 2020-2023  润新知