Renderer.render(scene ,camera)大概发生了什么

1,669 阅读6分钟

最简单的回答,从效果上看,就是把数据绘制到画布上了。

稍微深入一点,笼统的说,就是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。

给渲染队列排序

渲染投影贴图

渲染背景

渲染场景,怎么渲染的,且看下文

调用场景渲染的后置钩子。

下面就是一些重置 、弹栈的收尾工作了。

image.png

renderScene

此函数就是按照 不透明物体 光学透明物体 半透明物体 这样的顺序,依次进行渲染的,更细致的mesh排序已经在上一步完成了。

currentRenderList中列出 不透明物体 光学透明物体 半透明物体。

currentRenderState.setupLightsView( camera );

设置绘图尺寸。

如果有transmissive ,开始渲染其对应的缓冲帧,只包含不透明物体,这个缓冲帧会生成一个纹理用于后续合成透明效果。看到这里就可以知道,透过使用了transmissive的物体,是只能看见不透明物体的。

接着,依次开始渲染 ,不透明物体 、光学透明物体、半透明物体。

渲染完成之后, 恢复渲染器的默认设置, 开启深度测试, 开启深度写入, 开启颜色全通道写入, 关闭多边形偏移。

因此,要改变默认设置就需要在每一次Render之前去修改。

image.png

renderObjects

处理覆盖材质,所谓覆盖材质就是,当场景使用了覆盖材质时,场景内所有物体都将使用这种材质,舍弃原本的材质,所以这里需要单独传入一个material参数给后面。

遍历renderlist 渲染物体,如果物体可见则渲染。

layers 定义了0 - 31,共32个层级,只有物体和相机同层时可见。默认值是0 ,2的0次 ,test方法是用,按位与 不等于0 , 因为 mask 的值是2的n次方。

image.png

renderObject

调用物体渲染的前置钩子。

设置物体的视图矩阵和法线矩阵。

调用材质的前置钩子。

如果是透明且双层 ,先渲染后面,再渲染前面, 否则正常渲染。 这里是透明效果的一点优化。

调用物体渲染的后置钩子。

image.png

renderBufferDirect( camera, scene, geometry, material, object, group )

到了这一步,可以说渲染流程已经到底了,后面的可以不看了。

这个函数命名已经说明了它的功能,渲染到缓冲区,至于是具体是哪个缓冲区它不管,如果是默认缓冲区,那么结果就会呈现在画布上,如果是其他的帧缓冲区,可能会形成纹理。

先确定了三角形的绕匝方向,然后设置,不明白的是为何这个顺逆时针会和模型矩阵的值有关。

确定当前所需的程序后,传入数据。 因为多材质就意味着多着色器,一次绘制中实际上是只能使用一组着色器的,所以,就有了多绘制程序,提前编译好,切换材质时,仅仅需要切换程序即可,无需再次编译。

计算顶点绘制的起始索引 结束索引 ,绘制数量, 后面调用draw方法时会用到。计算过程,过于繁琐,计算完成后还有验算。

它分了 BufferRendererindexedBufferRenderer 分别对应有索引和无索引的情况。

    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是前面的 BufferRendererindexedBufferRenderer

结束

调用render方法之前,就是准备好数据。

调用render方法之后,主要是给物体分了三个队列,按一定顺序去渲染。

而在渲染物体(mesh line point)这里,才会去激活对应的程序, 更新uniform数据,因为, 渲染多个不同材质的物体,需要切换程序,每次调用render的时候,uniform数据是最有可能发生变化的,一般情况下存在顶点缓冲对象里的数据是不会轻易修改的。

最终将图形绘制到缓冲里。

下图可以说,就是全文的内容了。