前言📒
鸽了这么久,终于有心情更新一篇文章了🕊自己最近也在断断续续的学着 WebGL 的相关内容,只不过是因为比较懒就没有写文章,我这个人比较佛系,公众号就是学习、分享和交流的一种方式,有小伙伴看了有帮助便是好的,没有规定必须多久更新一篇,遇到好的文章也不会转载(有可能会在写文章时顺手推荐一下),我更不会出面试题的相关文章(因为网上这种文章实在数不胜数)。
又碎碎念、嘚吧嘚了一会,话不多说,让我们一起开启秃头之旅👴
一起变秃👨🎓
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 );
};
在上面的代码中我们不仅仅设置了 canvas
的 width
和 height
,同时还设置了 style
以及 viewport
的 width
和 height
。如果我们将 style
的宽高和 canvas
的宽高设置的不同会出现什么效果呢?以之前 WebGL基础 系列文章中纹理贴图中超级赛人的代码为例,下图是我们正常显示的图片(style
和 canvas
宽高都是 600px):
canvas
尺寸保持不变,我们将 style
的 width
设为 1200px 看一下有什么效果:
可见,图片发生了形变的同时也变得更模糊了,为了避免这种情况的发生,我们一般会将 canvas
、style
和 viewport
的尺寸设为相同的大小。
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
而不是 Scene
(Object3D
是 ThreeJS 中所有物体的基类)。同时,在文档中 render
方法只给出了两个参数,但我们看源码会发现其实它还能再额外接受两个参数 renderTarget
和 forceClear
,这是为了兼容老版本中接受的这两个参数,所以就从文档中移除了,取而代之的是使用 renderer
的 setRenderTarget
和 clear
两个方法。
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...
比如当我们的设备休眠时就会触发该事件。而 丢失上下文 这一事件属于 WebGLContextEvent
,WebGL 的 WebGLContextEvent
会响应 WebGL 上下文状态的重要变换而生成相应事件。事件会通过 DOM 事件系统发送,并被调度到 WebGL 渲染上下文关联的 HTMLCanvasElement
或 OffscreenCanvas
上。可以触发上下文变换的 WebGLContextEvent
包括上下文的丢失、恢复和无法创建上下文。
OffscreenCanvas
顾名思义是 离屏 Canvas,详情点此链接了解:developer.mozilla.org/zh-CN/docs/…
当客户端检测到图形缓冲区关联的 WebGL 渲染上下文丢失时,会执行以下操作:
- 让 canvas 作为上下文的 canvas;
- 如果设置了 WebGL 上下文的 ContextLost 标志,则中止后续步骤;
- 否则设置 ContextLost 标志;
- 给上下文创建的 WebGLObject 实例设置 Invalidated 标志;
- 禁用除 WEBGL_LOST_CONTEXT 外的所有的 Extensions;
- 执行下列任务队列:
- 触发 canvas 的名为
WEBGL_CONTEXT_LOST
的 WebGL 上下文事件,将其statusMessage
设为空值; - 如果未设置事件的 canceled 标志,则中止后续步骤;
- 异步执行下列步骤;
- 等待可恢复的图形缓冲区;
- 任务队列恢复上下文的图形缓冲区;
- 触发 canvas 的名为
第 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 实例时,客户端会执行以下操作:
- 创建新的 WebGLRenderingContext 对象:context;
- 让 context 的 canvas 成为
getContext()
方法所关联的 canvas 或离屏 canvas; - 创建新的 WebGLContextAttributes 对象:contextAttributes;
- 如果调用
getContext()
方法时传递了第二个参数,则使用给定的参数作为 contextAttributes; - 使用 contextAttributes 创建图形缓冲区,并将图形缓冲区与 context 相关联;
- 如果创建失败,则触发相应错误并返回
null
; - 创建新的 WebGLContextAttributes 对象:actualAttributes;
- 基于创建的图形缓冲区属性设置 actualAttributes 的属性;
- 将 context 的创建参数设为 contextAttributes;
- 将 context 的实际参数设为 actualAttributes;
- 返回 context;
解释了创建 context 的流程后,或许有对理解刚刚的疑惑点有所帮助😉(翻了好久 specification)
说了那么久 context,让我们再重新 focus 在 render()
方法上。既然我们会在 requestAnimationFrame
中调用 render()
,那么也就意味着其实 render()
方法内部做的应该就是去绘制每一帧应该展现的场景。在 WebGL 基础文章中绘制时,在每次 requestAnimationFrame
的 loop
中都会都 clear 一下,当然 ThreeJS 中也一样,当判断 context 存在后,就会立即为本次绘制重置上一帧的缓存:
bindingStates.resetDefaultState();
_currentMaterialId = - 1;
_currentCamera = null;
随后便更新 相应变换矩阵,并渲染背景 background.render( currentRenderList, scene, camera, forceClear )
。对于 background 的 render
方法,其内部主要对 background 分为了3种类型:1. 如果 background 为 null,则使用默认的参数 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
这条语句很好理解,因为我们的函数签名中传入的 scene
是 Object3D
类型,所以在 Scene
中有个 readonly
的属性叫做 isScene
(永远为 true
)来判别当前的 Object3D
是否是 Scene
。同理,camera.isArrayCamera
也是一个永远为 true
的 readonly
属性,来用来判别是否是 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.onBeforeRender
是 Object3D
的一个钩子,在渲染之前调用,每个继承 Object3D
的 Object
有自己独立的 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
变量并从缓冲区读取变量嘛?聪明啊老弟~它就是干了这个事儿!
结束语🎬
放弃不难,但坚持一定很酷 ——《解忧杂货店》💕