阅读 437

从零开始构建自己的WebGL3D引擎---思考与设计

引言 : 学习webgl已经接近2年时间了,对常见的开源3D引擎也比较熟悉了,但是目前为止three.js、babyLon、clayGL、cesium对webgl2新特性的支持也不是特别多。从今天开始,准备着手构建自己的3D引擎,一是希望能有自我提升,二是希望能做一个更快速、更炫酷、更容易扩展的引擎,并且希望能对webgl2的新特性有更多的支持。(目前引擎设计主要参考three.js和clayGL、少部分可能会参考babylon、如果后续做地球部分应该会参考cesium,由于对playCanvas使用不多,后续在引擎设计的时候也会着重去看playCanvas的源码并将好的思想融入进来。总而言之,在设计引擎的过程中自己多思考,然后多看这些开源引擎的思路,取其精华)

  1. 首先介绍下几个开源引擎:

Three.js github.com/mrdoob/thre… 最火的web3D引擎,功能最丰富而且最便于使用,笔者对其的研究比较深入,上层的一些设计可能会对其参考较多

ClayGL github.com/pissang/cla… ClayGL是一个用于构建可扩展Web3D应用程序的WebGL图形库,目前主要应用到echarts中,笔者对其比较熟悉,后续对它的一些分析可能会比较多

Babylon.js github.com/BabylonJS/B… 一个完整的JavaScript框架,用于构建HTML5,WebGL,WebVR和Web Audio的3D游戏和体验,该引擎主要使用typescript,性能强大,对webgl2支持较多

Cesium.js github.com/CesiumGS/ce… 一个3D地球引擎,主要是解决地球上的一些计算和大尺度问题,后续做地球可以参考

PlayCanvas github.com/playcanvas/… PlayCanvas是一个开源游戏引擎。目前对其使用不多。它是ECS架构,架构比较清晰,渲染列表的标脏处理的不错。

2.搭建工具和语言:

要着手做自己的引擎,首先要构建一套打包工具,我这里使用webpack+gulp+eslint进行构建。由于对typescript不太熟悉,目前还是主要使用es6的语法(后续会逐步适应typescript)。用构建工具构建项目的过程比较简单,这里就不一一记录了。

3.架构和渲染设计:

该如何设计一个架构:我们先看一下three做了什么? 其实只干了两件事 Graph Tree 场景树管理 Render 渲染

如何构建场景树:我们看看three的设计思路:

Render渲染 首先反思一下Three.js,three在渲染上有什么改进空间? 答:对Three比较熟悉的人应该知道,Three的最大痛点就是Renderer的设计包含了太多东西: 更新场景中所有物体的矩阵 生成渲染列表(分类,收集, 视锥裁剪,排序...) 更新灯光信息 执行渲染

Three在renderer中做了太多的事情,这确实不是太合适,主要是为了让用户更加容易使用吧,但是这个设计确实也带来了很多扩展和性能上的问题。 这样设计使其在实现VAO、UniformBlock、MRT这些关键技术的时候带来了极大的困难,现在也没有实现这几个非常关键的技术,这其实才是最大的痛点。 而一个引擎是否强大的核心是有没有强大的延迟渲染系统,显然Three不具备,我们看看为什么?

后期和延迟渲染经常会遇到的场景: 一般在后处理的时候我们希望替换某些物体的材质为材质1,替换另一些物体的材质为材质2,也就是常用的scheme策略 graph LR 场景-->物体 物体-->材质1 物体-->材质2

如果要用Three.js要如何实现那? scene.traverse(object => { // change material1 });

renderer.render(scene, camera, target);

scene.traverse(object => { // change material2 });

renderer.render(scene, camera, target);

// ...

实际上: 1.遍历场景替换材质1(遍历浪费) 2.更新场景中所有物体的矩阵 3.生成渲染列表(分类,收集, 视锥裁剪,排序...) 4.执行渲染 5.遍历场景替换材质2(遍历浪费) 6.更新场景中所有物体的矩阵(浪费) 7.生成渲染列表(分类,收集, 视锥裁剪,排序...)(浪费) 8.执行渲染 9.... (参考shawn0326的文章juejin.cn/post/684490…)

所以我们在实现Render的时候,希望render能只专注于渲染,而且为了后处理,我们希望render在渲染之前用户可以自定义自己的材质,默认使用MRT scene.updateMatrix();//将更新矩阵移出renderer,可以手动更新

const renderList = renderer.getRenderList(scene, camera);//获取渲染列表,将渲染列表的获取分离出来

const light = renderer.getLight();//灯光信息单独处理,生成uniformBlock renderer.renderBackground(scene);//单独渲染背景 // 渲染渲染列表 renderer.renderRenderList(renderList, camera, light, { getMaterial: item => { // 这里可以通过item.object动态判断使用哪种材质 return material1; } }); //当然我们也提供一个直接的render函数,便于用户使用,该render直接调用上述几个接口组装在一起 renderer.render(scene, camera);

实际上: 1.更新场景树 2.生成渲染列表(分类,收集, 视锥裁剪,排序...) 3.更新并计算灯光和阴影 4.渲染背景 5.使用材质1渲染 目前这样设计思路非常明确,用户可以更加灵活的使用renderer,并以更加优化的方式实现自己的延迟渲染策略。后续还应该思考如何内置一个更加方便的延迟渲染策略以及MRT。 当然渲染这块在实现的时候我会使用VAO和uniformBlock,后续在实现渲染之后再详细说明思路和好的设计方式。

4.矩阵运算设计

矩阵运算的设计:这部分是我们要关注的重点,仔细看了下three的设计,矩阵运算这部分的设计是直接使用数组进行计算的,这种设计不是太理想。为什么那?因为后续很难将矩阵运算移植到webAssembly,而且还有很多的js对象和数组的创建,内存开销也不小。 因此我设计的引擎需要摒弃这种思路,希望后续能更好更快的切换到webAssembly。目前矩阵运算主要借助gl-matrix库(github.com/toji/gl-mat… 想要让引擎更加快速简单,我还是希望使用dirty机制来触发引擎的更新,特别是矩阵运算的时候。如果你做了position、rotation包括灯光的修改,我希望能有个好的机制直接触发引擎的更新,这样就不用每帧去更新和计算这些参数,能给引擎带来更好的性能。

5.上游工具

第一步先支持gltf以及一些gltf的扩展吧。目前大部分工具都可以导出到gltf。后续希望能实现更加强大的粒子系统和编辑器。

文章分类
前端
文章标签