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

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

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

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

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

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

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

PlayCanvas
https://github.com/playcanvas/engine
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的文章https://juejin.im/post/5e5dbe23e51d4526f363b123)

所以我们在实现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库(https://github.com/toji/gl-matrix),这个库比较简单轻量而且是直接使用并得到内存数组。接口调用完成可以直接上传uniform,非常方便快捷。
想要让引擎更加快速简单,我还是希望使用dirty机制来触发引擎的更新,特别是矩阵运算的时候。如果你做了position、rotation包括灯光的修改,我希望能有个好的机制直接触发引擎的更新,这样就不用每帧去更新和计算这些参数,能给引擎带来更好的性能。

5.上游工具

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

你可能感兴趣的:(webgl,three.js)