上一篇文章里,我们初探 Renderer。这篇文章将介绍常用的 Renderer 中的方法。

clear

this.clear = function ( color, depth, stencil ) {
  let bits = 0;

  if ( color === undefined || color ) bits |= _gl.COLOR_BUFFER_BIT;
  if ( depth === undefined || depth ) bits |= _gl.DEPTH_BUFFER_BIT;
  if ( stencil === undefined || stencil ) bits |= _gl.STENCIL_BUFFER_BIT;

  _gl.clear( bits );
};

clear 方法接收三个可选参数,分别是清空 颜色缓冲区、深度缓冲区和模板缓冲区。而 gl.clear() 方法可以接受一个复合值,所以采用了 bits |= value 形式来清空缓冲区。同时,clearColor/clearDepth/clearStencil 三个方法内部也都是调用的本 clear 方法。

setSize

当我们 new 了一个 Renderer 之后,会经常调用 setSize 来设置 canvas 的大小,那我们就看一下 setSize 的实现:

this.setSize = function ( width, height, updateStyle ) {
  if ( xr.isPresenting ) {
    console.warn( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' );
    return;
  }
  
  _width = width;
  _height = height;
  _canvas.width = Math.floor( width * _pixelRatio );
  _canvas.height = Math.floor( height * _pixelRatio );

  if ( updateStyle !== false ) {
    _canvas.style.width = width + 'px';
    _canvas.style.height = height + 'px';
  }

  this.setViewport( 0, 0, width, height );
};

在上面的代码中我们不仅仅设置了 canvaswidthheight,同时还设置了 style 以及 viewportwidthheight。如果我们将 style 的宽高和 canvas 的宽高设置的不同会出现什么效果呢?以之前 WebGL基础 系列文章中纹理贴图中超级赛人的代码为例,下图是我们正常显示的图片(stylecanvas 宽高都是 600px):

canvas 尺寸保持不变,我们将 stylewidth 设为 1200px 看一下有什么效果:

可见,图片发生了形变的同时也变得更模糊了,为了避免这种情况的发生,我们一般会将 canvasstyleviewport 的尺寸设为相同的大小。

render

render() 是我们使用最多的方法,作为 renderer 的核心,让我们来瞧瞧它的玄机。官方文档中 render() 方法的用法为:

// Methods
.render ( scene : Object3D, camera : Camera ) : null
// Render a scene or another type of object using a camera.

可以看到文档中提到“使用 camera 渲染的是 scene 或其它 object”,故此处 scene 的类型是 Object3D 而不是 SceneObject3DThreeJS 中所有物体的基类)。同时,在文档中 render 方法只给出了两个参数,但我们看源码会发现其实它还能再额外接受两个参数 renderTargetforceClear,这是为了兼容老版本中接受的这两个参数,所以就从文档中移除了,取而代之的是使用 renderersetRenderTargetclear 两个方法。

context

接着看 render 方法,可以看到一个很有趣的语句:

if ( _isContextLost === true ) return;

语义上来说,如果丢失了当前的 context 就直接 return,而什么会触发 WebGL 上下文丢失的事件呢?我们去瞅一眼 specification 便知:

Occurrences such as power events on mobile devices may cause the WebGL rendering context to be lost at any time and require the application to rebuild it...

WebGL Specification​www.khronos.org

比如当我们的设备休眠时就会触发该事件。而 丢失上下文 这一事件属于 WebGLContextEventWebGLWebGLContextEvent 会响应 WebGL 上下文状态的重要变换而生成相应事件。事件会通过 DOM 事件系统发送,并被调度到 WebGL 渲染上下文关联的 HTMLCanvasElementOffscreenCanvas 上。可以触发上下文变换的 WebGLContextEvent 包括上下文的丢失、恢复和无法创建上下文。

OffscreenCanvas 顾名思义是 离屏 Canvas,详情请参考下文

OffscreenCanvas​developer.mozilla.org

图标

当客户端检测到图形缓冲区关联的 WebGL 渲染上下文丢失时,会执行以下操作:

  1. canvas 作为上下文的 canvas
  2. 如果设置了 WebGL 上下文的 ContextLost 标志,则中止后续步骤;
  3. 否则设置 ContextLost 标志;
  4. 给上下文创建的 WebGLObject 实例设置 Invalidated 标志;
  5. 禁用除 WEBGL_LOST_CONTEXT 外的所有的 Extensions
  6. 执行下列任务队列:
  7. 触发 canvas 的名为 WEBGL_CONTEXT_LOSTWebGL 上下文事件,将其 statusMessage 设为空值;
  8. 如果未设置事件的 canceled 标志,则中止后续步骤;
  9. 异步执行下列步骤;
  10. 等待可恢复的图形缓冲区;
  11. 任务队列恢复上下文的图形缓冲区;

1 点会有些难以理解,specification 中的原话是 Let canvas be the context's canvas,下面介绍一下创建上下文的相应过程,可能会有助于了解这句话。

从上面的描述中可以知道,我们必须通过 canvas 或离屏 canvas 获取到上下文才能使用 WebGL API,也就是说每个 WebGLRenderingContext 创建时都需要关联 canvas。每个 canvas 在创建时都会有相应的创建参数:

dictionary WebGLContextAttributes {
    boolean alpha = true;
    boolean depth = true;
    boolean stencil = false;
    boolean antialias = true;
    boolean premultipliedAlpha = true;
    boolean preserveDrawingBuffer = false;
    WebGLPowerPreference powerPreference = "default";
    boolean failIfMajorPerformanceCaveat = false;
    boolean desynchronized = false;
};

平时我们在获取 context 时仅仅是通过 gl.getContext('webgl') 来获取,其实我们可以通过该方法的第二个参数给 context 传递相应属性:

gl.getContext("webgl", { alpha: false, depth: false });

当调用 getContext() 方法返回 context 实例时,客户端会执行以下操作:

  1. 创建新的 WebGLRenderingContext 对象:context
  2. contextcanvas 成为 getContext() 方法所关联的 canvas 或离屏 canvas;
  3. 创建新的 WebGLContextAttributes 对象:contextAttributes
  4. 如果调用 getContext() 方法时传递了第二个参数,则使用给定的参数作为 contextAttributes
  5. 使用 contextAttributes 创建图形缓冲区,并将图形缓冲区与 context 相关联;
  6. 如果创建失败,则触发相应错误并返回 null
  7. 创建新的 WebGLContextAttributes 对象:actualAttributes
  8. 基于创建的图形缓冲区属性设置 actualAttributes 的属性;
  9. context 的创建参数设为 contextAttributes
  10. context 的实际参数设为 actualAttributes
  11. 返回 context

解释了创建 context 的流程后,或许有对理解刚刚的疑惑点有所帮助 (翻了好久 specification)。

说了那么久 context,让我们再重新 focus 在 render() 方法上。既然我们会在 requestAnimationFrame 中调用 render(),那么也就意味着其实 render() 方法内部做的应该就是去绘制每一帧应该展现的场景。在 WebGL 基础文章中绘制时,在每次 requestAnimationFrameloop 中都会都 clear 一下,当然 ThreeJS 中也一样,当判断 context 存在后,就会立即为本次绘制重置上一帧的缓存:

bindingStates.resetDefaultState();
_currentMaterialId = - 1;
_currentCamera = null;

随后便更新 相应变换矩阵,并渲染背景 background.render( currentRenderList, scene, camera, forceClear )。对于 backgroundrender 方法,其内部主要对 background 分为了3种类型:1. 如果 backgroundnull,则使用默认的参数 render;2. 如果是 color, 则使用已有的 color render;3. 如果是 texture,则更新贴图(小声哔哔一句,传入的 camera 并没有用到)。

ThreeJS 中,将要渲染的物体分为了两类:透明物体不透明物体(先不详细介绍,在后续阅读 Object3D 源码时再聊),并通过下面的语句获取并处理这两类 object

const opaqueObjects = currentRenderList.opaque;	// 可见物体
const transparentObjects = currentRenderList.transparent;	// 透明物体

if ( opaqueObjects.length > 0 ) renderObjects( opaqueObjects, scene, camera );
if ( transparentObjects.length > 0 ) renderObjects( transparentObjects, scene, camera );

renderObjects

function renderObjects( renderList, scene, camera ) {
  const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

  for ( let i = 0, l = renderList.length; i < l; i ++ ) {
    const renderItem = renderList[ i ];
		// ...

    if ( camera.isArrayCamera ) {
      _currentArrayCamera = camera;
      const cameras = camera.cameras;
      for ( let j = 0, jl = cameras.length; j < jl; j ++ ) {
        const camera2 = cameras[ j ];
        if ( object.layers.test( camera2.layers ) ) {
          // ...
          renderObject( object, scene, camera2, geometry, material, group );
        }
      }
    } else {
			// ...
      renderObject( object, scene, camera, geometry, material, group );
    }
  }
}

对于 secene.isScene 这条语句很好理解,因为我们的函数签名中传入的 sceneObject3D 类型,所以在 Scene 中有个 readonly 的属性叫做 isScene(永远为 true)来判别当前的 Object3D 是否是 Scene。同理,camera.isArrayCamera 也是一个永远为 truereadonly 属性,来用来判别是否是 camera,只不过这并不是 Camera 中的属性,而是 ArrayCamera 类中的属性,该类继承自 PerspectiveCamera

如果传入的 camera 不是 ArrayCamera 类型,则调用 renderObject;否则,循环遍历 ArrayCamera.cameras 并调用 renderObject

renderObject

function renderObject( object, scene, camera, geometry, material, group ) {
  object.onBeforeRender( _this, scene, camera, geometry, material, group );
  // ...

  // 矩阵变换:模型视图矩阵 * 世界逆矩阵 * 世界矩阵
  object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
  object.normalMatrix.getNormalMatrix( object.modelViewMatrix );

  if ( object.isImmediateRenderObject ) {
    // ...
    renderObjectImmediate( object, program );
  } else {
    _this.renderBufferDirect( camera, scene, geometry, material, object, group );
  }

  object.onAfterRender( _this, scene, camera, geometry, material, group );
  // ...
}

object.onBeforeRenderObject3D 的一个钩子,在渲染之前调用,每个继承 Object3DObject 有自己独立的 onBeforeRender 的实现。object.isImmediateRenderObject 又是 ImmediateRenderObject 的一个属性,用来判断是否是需要立即渲染的物体,如果满足条件则调用 renderObjectImmediate 否则调用 renderBufferDirect

renderObjectImmediate

this.renderBufferImmediate = function ( object, program ) {
  bindingStates.initAttributes();
  const buffers = properties.get( object );

  if ( object.hasPositions && ! buffers.position ) buffers.position = _gl.createBuffer();
  // ...
  const programAttributes = program.getAttributes();

  if ( object.hasPositions ) {
    _gl.bindBuffer( _gl.ARRAY_BUFFER, buffers.position );
    _gl.bufferData( _gl.ARRAY_BUFFER, object.positionArray, _gl.DYNAMIC_DRAW );
    bindingStates.enableAttribute( programAttributes.position );
    _gl.vertexAttribPointer( programAttributes.position, 3, _gl.FLOAT, false, 0, 0 );
  }
  // ...
  bindingStates.disableUnusedAttributes();
  _gl.drawArrays( _gl.TRIANGLES, 0, object.count );
  object.count = 0;
};

renderObjectImmediate 中的内容看起来是不是很熟悉呢?createBuffer -> bindBuffer -> bufferData -> enableAttribute -> vertexAttribPointer 这一波操作不就是将缓冲区分配给 attrubute 变量并从缓冲区读取变量嘛?聪明啊老弟~它就是干了这个事儿!