最简单的回答,从效果上看,就是把数据绘制到画布上了。
稍微深入一点,笼统的说,就是threeJS帮我们调用webgl的api 。链接上下文程序, 编译着色器, 向webgl输入顶点,法线以及材质的属性(包含各种贴图)等数据, 最终按照一定的顺序去调用绘画api, gl.drawElement() 或者gl.drawArray(),最终绘制结果就呈现在了显示器上。下面可以认为是draw方法调用之后发生了什么,也就是流水线,或者叫渲染管线。
flowchart LR
A([基本处理])-->B[顶点着色器]
B-->C(图元装配)
C-->D[光栅化]
D--片元-->E[片元着色器]
E-->F(裁剪测试)
F-->H(深度测试\n深度测试)
H-->I(颜色缓冲混合)
I-->J(抖动就当没有他)
J-->K(缓冲帧)
当然,可能并不是这样的,上述过程是原生api在第一次绘制的时候所发生的事,但是像链接上下文程序,编译着色器可能只需执行一次。
在不看源码的情况下,这个问题可以这样糊弄过去,在需要改动一些东西的时候,我们可以看一看源码。
下面一起来看threeJS代码。 当前版本 147dev。就直接从 WebglRender.js开始。
值得注意的是,threejs的编程风格,会用到不少的临时变量作为缓存,或者说全局变量作为中转。
render
render方法只有两个参数 scene camera。
进入之后,先检查相机和webgl上下文, 更新 scene 和相机的世界矩阵(即模型矩阵)。
updateMatrixWorld 这个方法会递归更新子节点,所以在根节点scene上调用一次就会更新场景里的全部物体。如果物体的matrixWorldAutoUpdate属性或者传参为true,就会更新子节点。默认这个值是为true的。
看看要不要启用XR接口,如果启用,把相机替换为XR的相机。XR (extension reality) 扩展现实 。
调用scene的前置渲染钩子。
获取渲染状态 ,初始化后,压入渲染状态栈(就一个普通数组),renderState中存储着光影相关的数据。
计算投影视图矩阵。缓存到_projScreenMatrix,并据此计算世界坐标系中的可视边界。 fustum 根据投影矩阵,计算世界坐标系中的可视区域, 内含六个平面。
处理裁剪相关。
获取渲染队列,初始化后放到渲染队列栈(一个数组)中。每个渲染队列 区分了opaque transmissive transparent,第一次的时候,这里是三个空数组。
projectObject( scene, camera, 0, _this.sortObjects )递归处理object3d的可见性、 渲染顺序、 光照阴影相关, 并且填充了渲染队列。renderList内部会自行处理,区分opaque transmissive transparent ,数组中的元素就暂且认为是一个Mesh。
给渲染队列排序
渲染投影贴图
渲染背景
渲染场景,怎么渲染的,且看下文。
调用场景渲染的后置钩子。
下面就是一些重置 、弹栈的收尾工作了。
renderScene
此函数就是按照 不透明物体 光学透明物体 半透明物体 这样的顺序,依次进行渲染的,更细致的mesh排序已经在上一步完成了。
从currentRenderList中列出 不透明物体 光学透明物体 半透明物体。
currentRenderState.setupLightsView( camera );
设置绘图尺寸。
如果有transmissive ,开始渲染其对应的缓冲帧,只包含不透明物体,这个缓冲帧会生成一个纹理用于后续合成透明效果。看到这里就可以知道,透过使用了transmissive的物体,是只能看见不透明物体的。
接着,依次开始渲染 ,不透明物体 、光学透明物体、半透明物体。
渲染完成之后, 恢复渲染器的默认设置, 开启深度测试, 开启深度写入, 开启颜色全通道写入, 关闭多边形偏移。
因此,要改变默认设置就需要在每一次Render之前去修改。
renderObjects
处理覆盖材质,所谓覆盖材质就是,当场景使用了覆盖材质时,场景内所有物体都将使用这种材质,舍弃原本的材质,所以这里需要单独传入一个material参数给后面。
遍历renderlist 渲染物体,如果物体可见则渲染。
layers 定义了0 - 31,共32个层级,只有物体和相机同层时可见。默认值是0 ,2的0次 ,test方法是用,按位与 不等于0 , 因为 mask 的值是2的n次方。
renderObject
调用物体渲染的前置钩子。
设置物体的视图矩阵和法线矩阵。
调用材质的前置钩子。
如果是透明且双层 ,先渲染后面,再渲染前面, 否则正常渲染。 这里是透明效果的一点优化。
调用物体渲染的后置钩子。
renderBufferDirect( camera, scene, geometry, material, object, group )
到了这一步,可以说渲染流程已经到底了,后面的可以不看了。
这个函数命名已经说明了它的功能,渲染到缓冲区,至于是具体是哪个缓冲区它不管,如果是默认缓冲区,那么结果就会呈现在画布上,如果是其他的帧缓冲区,可能会形成纹理。
先确定了三角形的绕匝方向,然后设置,不明白的是为何这个顺逆时针会和模型矩阵的值有关。
确定当前所需的程序后,传入数据。 因为多材质就意味着多着色器,一次绘制中实际上是只能使用一组着色器的,所以,就有了多绘制程序,提前编译好,切换材质时,仅仅需要切换程序即可,无需再次编译。
计算顶点绘制的起始索引 结束索引 ,绘制数量, 后面调用draw方法时会用到。计算过程,过于繁琐,计算完成后还有验算。
它分了 BufferRenderer 和 indexedBufferRenderer 分别对应有索引和无索引的情况。
let renderer = bufferRenderer;
if ( index !== null ) {
attribute = attributes.get( index );
renderer = indexedBufferRenderer; // 有索引用另一个
renderer.setIndex( attribute );
}
然后根据Object3d的类型 mesh line points sprite points 等还有更细的划分,来确定最终绘图模式, 点线面。
最后调用渲染方法, 分为instanceMesh, instanceBufferGeometry和其他。 instance 是用于优化,当要绘制大量相同的几何体,不同材质(同一类材质的属性不同)时的一种优化。 传入的参数group就是
Renderer.render()和renderer.renderInstances 可以说就是调用原生的draw方法了。 此Renderer是前面的 BufferRenderer或indexedBufferRenderer
结束
调用render方法之前,就是准备好数据。
调用render方法之后,主要是给物体分了三个队列,按一定顺序去渲染。
而在渲染物体(mesh line point)这里,才会去激活对应的程序, 更新uniform数据,因为, 渲染多个不同材质的物体,需要切换程序,每次调用render的时候,uniform数据是最有可能发生变化的,一般情况下存在顶点缓冲对象里的数据是不会轻易修改的。
最终将图形绘制到缓冲里。
下图可以说,就是全文的内容了。