ThreeJs 封装了 WebGL 进行渲染时所涉及到的相关概念,如光照,材质,纹理以及相机等。除此以外,其还抽象了场景(Scene)以及用于渲染的渲染器(WebGLRenderer)。这些相关概念都被封装成了一个对象,那么它们是如何协做的呢,关系又是如何呢?这篇文章主要就是来分析一下 ThreeJs 中核心中的核心,即场景,物体,光照,材质,纹理以及相机这些对象是如何渲染的。html
下面截取了一个渲染效果图,看起来还不错是否是。这是 ThreeJs 的官方 demo lights / spotlights 的渲染效果图。这个 demo 中就基本涉及到了上面所提的核心对象,下面我将基于此 demo 来分析这些核心对象是如何被组织在一块儿进行渲染的。web
Demo 有一点点长,对于不熟悉 ThreeJs 的人来讲会有一点点难度,所以这里主要分析了构建、初始化以及渲染 3 个部分来分别说明。canvas
// 构建渲染器 WebGLRenderer
var renderer = new THREE.WebGLRenderer();
// 设置显示比例
renderer.setPixelRatio( window.devicePixelRatio );
// 构建一个透视投影的相机
var camera = new THREE.PerspectiveCamera( 35, window.innerWidth / window.innerHeight, 1, 2000 );
// 构建一个轨道控制器,主要就是经过鼠标来控制相机沿目标物体旋转,从而达到像在旋转场景同样,能够从各个不一样角度观察物体
var controls = new THREE.OrbitControls( camera, renderer.domElement );
// 构建场景
var scene = new THREE.Scene();
// 构建Phong网格材质MeshPhongMaterial,该材质能够模拟具备镜面高光的光泽表面,一个用于接收阴影的平面,一个用于场景中的物体 Box
var matFloor = new THREE.MeshPhongMaterial();
var matBox = new THREE.MeshPhongMaterial( { color: 0xaaaaaa } );
// 构建几何体,一样分别用于 平面 和 Box
var geoFloor = new THREE.PlaneBufferGeometry( 2000, 2000 );
var geoBox = new THREE.BoxBufferGeometry( 3, 1, 2 );
// 构建平面网格 mesh
var mshFloor = new THREE.Mesh( geoFloor, matFloor );
mshFloor.rotation.x = - Math.PI * 0.5;
// 构建 box 网格 mesh
var mshBox = new THREE.Mesh( geoBox, matBox );
// 构建环境光
var ambient = new THREE.AmbientLight( 0x111111 );
// 构建 3 个不一样颜色的 聚光灯(SpotLight)
var spotLight1 = createSpotlight( 0xFF7F00 );
var spotLight2 = createSpotlight( 0x00FF7F );
var spotLight3 = createSpotlight( 0x7F00FF );
// 声明用于描述聚光灯的 3 个不一样光束帮助器
var lightHelper1, lightHelper2, lightHelper3;
复制代码
上面代码中,基本上每一行都添加了详细的注释,其中有调用了一个内部的函数 createSpotlight() ,以下。数组
function createSpotlight( color ) {
var newObj = new THREE.SpotLight( color, 2 );
newObj.castShadow = true;
newObj.angle = 0.3;
newObj.penumbra = 0.2;
newObj.decay = 2;
newObj.distance = 50;
newObj.shadow.mapSize.width = 1024;
newObj.shadow.mapSize.height = 1024;
return newObj;
}
复制代码
这个方法,主要就是根据指定的颜色构建一个聚光灯并设置好相应的参数。这里不论是相机、光照、材质仍是物体,其详细的参数并不打算在这里一一讲述,有须要的话再进一步说明。bash
function init() {
......
// 将平面,box,环境光以及光源辅助器等所有添加到 scene 中
scene.add( mshFloor );
scene.add( mshBox );
scene.add( ambient );
scene.add( spotLight1, spotLight2, spotLight3 );
scene.add( lightHelper1, lightHelper2, lightHelper3 );
document.body.appendChild( renderer.domElement );
onResize();
window.addEventListener( 'resize', onResize, false );
controls.target.set( 0, 7, 0 );
controls.maxPolarAngle = Math.PI / 2;
controls.update();
}
复制代码
初始化主要就是将平面,box ,光照这些都添加进场景中,可是要注意,相机并无被添加进来。app
function render() {
TWEEN.update();
if ( lightHelper1 ) lightHelper1.update();
if ( lightHelper2 ) lightHelper2.update();
if ( lightHelper3 ) lightHelper3.update();
renderer.render( scene, camera );
requestAnimationFrame( render );
}
复制代码
渲染函数 render() 中最关键的调用渲染器的 WebGLRenderer#render() 方法同时去渲染场景和相机。dom
根据上面的分析,以及对 ThreeJs 源码的分析,梳理出以下 2 个类图关系。 ide
图中,渲染器负责同时渲染场景以及相机。而光照和网格都被添加到场景中。几何体以及材质都是网格的 2 个基本属性,也决定一个网格的形状和表面纹理。函数
该图是对上图的补充,说明光照,相机以及网格都属于 Object3D 对象。在 ThreeJs 中还有许多的类都是继承自 Object3D 的。工具
先来看一下 WebGL 的流水线渲染管线图,以下所示。这个是必需要了解的,咱们能够没必要彻底理解渲染管线的每一个步骤,但咱们必需要知道渲染管线的这个流程。
渲染管线指的是WebGL程序的执行过程,如上图所示,主要分为 4 个步骤:
顶点着色器的处理,主要是一组矩阵变换操做,用来把3D模型(顶点和原型)投影到viewport上,输出是一个个的多边形,好比三角形。
光栅化,也就是把三角形链接区域按必定的粒度逐行转化成片元(fragement),相似于2D空间中,能够把这些片元看作是3D空间的一个像素点。
片元着色器的处理,为每一个片元添加颜色或者纹理。只要给出纹理或者颜色,以及纹理坐标(uv),管线就会根据纹理坐标进行插值运算,将纹理或者图片着色在相应的片元上。
把3D空间的片元合并输出为2D像素数组并显示在屏幕上。
由于做者也没进行过原生的 WebGL 开发,而是一上来就撸起了 ThreeJs。因此 这里仅根据 Open GL ES 的开发流程,绘制出以下流程图。
流程图中关键的第一步在于建立着色器(Shader)程序,着色器程序主要用 GLSL(GL Shading Language) 语言编写,其直接由 GPU 来执行。第二步是设置顶点,纹理以及其余属性,如咱们建立的几何图元 Box,加载的 obj 文件,以及用于矩阵变换的模型矩阵,视图矩阵以及投影矩阵等。第三步即是进行顶点的绘制,如以点绘制,以直线绘制以及以三角形绘制,对于图元,大部分是以三角形绘制。
关于坐标系与矩阵变换,这里一个幅图总结的很不错,画的很详细,一眼就能看出其中的意思。
关于 WebGL 的基本就介绍这么多,这里的目的是为了让后面的分析有个简单的铺垫。若是感兴趣,能够参考更多大牛专门介绍 WebGL / Open GL ES 的文章。
WebGLRenderer 的初始化主要在它的构造方法 WebGLRenderer() 和 initGLContext() 中。这里先看看构造方法 WebGLRenderer() 。
####2.1 构造方法 WebGLRenderer() 其初始化的属性不少。这里主要关注其 2 个最核心的属性 canvas 以及 context。
function WebGLRenderer( parameters ) {
console.log( 'THREE.WebGLRenderer', REVISION );
parameters = parameters || {};
// 若是参数中有 canvas,就有参数中的,若是没有就经过 document.createElementNS() 来建立一个。和 2D 的概念同样,这里的 canvas 主要是用来进行 3D 渲染的画布。
var _canvas = parameters.canvas !== undefined ? parameters.canvas : document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' ),
_context = parameters.context !== undefined ? parameters.context : null,
......
// initialize
var _gl;
......
// 从 canvas 中获取 context。参数 webgl 是其中之一,其还能够获取 2d 的。这里获取到 webgl 的 context,那就意味者能够经过它进行 3D 绘制了。
_gl = _context || _canvas.getContext( 'webgl', contextAttributes ) || _canvas.getContext( 'experimental-webgl', contextAttributes );
......
function initGLContext() {
......
_this.context = _gl;
......
}
......
}
复制代码
如上面的代码以及注释,canvas 就是 html 的标准元素
####2.2 初始化上下文方法 initGLContext()
function initGLContext() {
/**
* 扩展特性
*/
extensions = new WebGLExtensions( _gl );
capabilities = new WebGLCapabilities( _gl, extensions, parameters );
if ( ! capabilities.isWebGL2 ) {
extensions.get( 'WEBGL_depth_texture' );
extensions.get( 'OES_texture_float' );
extensions.get( 'OES_texture_half_float' );
extensions.get( 'OES_texture_half_float_linear' );
extensions.get( 'OES_standard_derivatives' );
extensions.get( 'OES_element_index_uint' );
extensions.get( 'ANGLE_instanced_arrays' );
}
extensions.get( 'OES_texture_float_linear' );
/**
* 工具类
*/
utils = new WebGLUtils( _gl, extensions, capabilities );
/**
* 状态
*/
state = new WebGLState( _gl, extensions, utils, capabilities );
state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ) );
state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ) );
info = new WebGLInfo( _gl );
properties = new WebGLProperties();
/**
* 纹理辅助类
*/
textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
/**
* 属性存储辅助类,主要实现 JavaScript 中的变量或者数组、纹理图片传递到 WebGL 中
*/
attributes = new WebGLAttributes( _gl );
/**
* 几何图元
*/
geometries = new WebGLGeometries( _gl, attributes, info );
/**
* Object 类存储类
*/
objects = new WebGLObjects( geometries, info );
morphtargets = new WebGLMorphtargets( _gl );
/**
* WebGL program
*/
programCache = new WebGLPrograms( _this, extensions, capabilities );
renderLists = new WebGLRenderLists();
renderStates = new WebGLRenderStates();
/**
* 背景
*/
background = new WebGLBackground( _this, state, objects, _premultipliedAlpha );
/**
* Buffer
*/
bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info, capabilities );
indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info, capabilities );
info.programs = programCache.programs;
_this.context = _gl;
_this.capabilities = capabilities;
_this.extensions = extensions;
_this.properties = properties;
_this.renderLists = renderLists;
_this.state = state;
_this.info = info;
}
复制代码
initGLContext() 方法中初始化了不少的组件,有的组件很容易就能看出来是做什么用的,而有的组件可能就没那么好知道意思,须要等到具体分析 render() 方法时,用到时再来理解。不过,虽然 initGLContext() 方法中看起来有不少的组件初始化,但实质这些组件也只是进行一个最基本的构造而已,没有进一步更深刻的过程。所以,这里也粗略的看一下便可。
整个 render 的过程是十分复杂的,也是漫长的,须要咱们耐心去看,去理解。先来简单过一下它的时序图。
从时序图可见,其涉及到的相对象以及步骤是比较多的,共 20 步。其中涉及到的主要对象有:Scene,Camera,WebGLRenderStates,WebGLRenderLists,WebGLBackground,WebGLProgram,_gl,WebGLBufferRenderer。咱们比较熟悉的是 Scene,由于咱们的Object / Mesh 都是被添加到它里面的,另外还有 Camera,咱们必需要有一个相机来告诉咱们以怎么样的视角来观看这个 3D 世界。另一些不熟悉的对象,WebGLRenderList 管理着咱们须要拿去 render 的 Object / Mesh,WebGLBackground 描述了场景的背景,WebGLProgram 则建立了用于连接、执行 Shader 的程序,而 WebGLBufferRenderer 则是整个 3D 世界被 render 到的目的地。 这里不会按照时序图,逐步逐步地进行分析,而是挑重点,同时保持与前面所述的 OpenGL ES 的流程一致性上进行分析。
render() 函数
this.render = function ( scene, camera, renderTarget, forceClear ) {
// 前面是一些参数的校验,这里省略
// 1.reset caching for this frame
......
// 2.update scene graph
if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
// 3.update camera matrices and frustum
if ( camera.parent === null ) camera.updateMatrixWorld();
.....
// 4. init WebGLRenderState
currentRenderState = renderStates.get( scene, camera );
currentRenderState.init();
scene.onBeforeRender( _this, scene, camera, renderTarget );
// 5.视景体矩阵计算,为相机的投影矩阵与相机的世界矩阵的逆矩阵的叉乘?
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
_frustum.setFromMatrix( _projScreenMatrix );
_localClippingEnabled = this.localClippingEnabled;
_clippingEnabled = _clipping.init( this.clippingPlanes, _localClippingEnabled, camera );
// 6.WebGLRenderList 的初始化
currentRenderList = renderLists.get( scene, camera );
currentRenderList.init();
projectObject( scene, camera, _this.sortObjects );
......
// 7. shadow 的绘制
if ( _clippingEnabled ) _clipping.beginShadows();
var shadowsArray = currentRenderState.state.shadowsArray;
shadowMap.render( shadowsArray, scene, camera );
currentRenderState.setupLights( camera );
if ( _clippingEnabled ) _clipping.endShadows();
//
if ( this.info.autoReset ) this.info.reset();
if ( renderTarget === undefined ) {
renderTarget = null;
}
this.setRenderTarget( renderTarget );
// 8.背景的绘制
background.render( currentRenderList, scene, camera, forceClear );
// 9.render scene
var opaqueObjects = currentRenderList.opaque;
var transparentObjects = currentRenderList.transparent;
if ( scene.overrideMaterial ) {
// 10.强制使用场景的材质 overrideMaterial 来统一 render 物体。
var overrideMaterial = scene.overrideMaterial;
if ( opaqueObjects.length ) renderObjects( opaqueObjects, scene, camera, overrideMaterial );
if ( transparentObjects.length ) renderObjects( transparentObjects, scene, camera, overrideMaterial );
} else {
// 11.分别对 opaque 和 transparent 的物体进行 render
// opaque pass (front-to-back order)
if ( opaqueObjects.length ) renderObjects( opaqueObjects, scene, camera );
// transparent pass (back-to-front order)
if ( transparentObjects.length ) renderObjects( transparentObjects, scene, camera );
}
// Generate mipmap if we're using any kind of mipmap filtering ..... // Ensure depth buffer writing is enabled so it can be cleared on next render state.buffers.depth.setTest( true ); state.buffers.depth.setMask( true ); state.buffers.color.setMask( true ); state.setPolygonOffset( false ); scene.onAfterRender( _this, scene, camera ); ...... currentRenderList = null; currentRenderState = null; }; 复制代码
render() 是渲染的核心,粗略地看它作了大概如下的事情。
但这里咱们没必要关注每一个处理的细节,仅从几个重要的点着手去理解以及分析。
即更新整个场景图,主要就是更新每一个物体的 matrix。若是其含有孩子节点,则还会逐级更新。在这里,每一个物体的 matrix 是经过其 position,quaternion以及scale 计算得来的,也就是其模型矩阵,而 matrixWorld 又是根据 matrix 计算得来的。若是当前节点没有父节点,则 matrix 就是 matrixWorld。而若是有的话,那 matrixWorld 则为父节点的 matrixWorld 与当前节点 matrix 的叉乘。也就是说当前节点的 matrixWorld 是相对于其父亲节点的。
WebGLRenderList 的初始化init()方法自己并无什么,其只是在 WebGLRenderLists 中经过将 scene.id 和 camera.id 创建起必定的关联。而这里更重要的目的是肯定有哪些对象是要被渲染出来的,这个最主要的实现就在 projectObject() 方法中。
function projectObject( object, camera, sortObjects ) {
if ( object.visible === false ) return;
var visible = object.layers.test( camera.layers );
if ( visible ) {
// 是否为光照
if ( object.isLight ) {
currentRenderState.pushLight( object );
......
} else if ( object.isSprite ) {
// 是否为精灵
if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {
......
currentRenderList.push( object, geometry, material, _vector3.z, null );
}
} else if ( object.isImmediateRenderObject ) {
// 是否为当即要渲染的 Object
......
currentRenderList.push( object, null, object.material, _vector3.z, null );
} else if ( object.isMesh || object.isLine || object.isPoints ) {
// 是否为 mesh,line,points
......
if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {
......
if ( Array.isArray( material ) ) {
var groups = geometry.groups;
for ( var i = 0, l = groups.length; i < l; i ++ ) {
......
if ( groupMaterial && groupMaterial.visible ) {
currentRenderList.push( object, geometry, groupMaterial, _vector3.z, group );
}
}
} else if ( material.visible ) {
// 可见便可渲染
currentRenderList.push( object, geometry, material, _vector3.z, null );
}
}
}
}
// 对每一个孩子进行递归遍历
var children = object.children;
for ( var i = 0, l = children.length; i < l; i ++ ) {
projectObject( children[ i ], camera, sortObjects );
}
}
复制代码
从方法中,咱们大体获得以下结论:
经过 WebGLRenderList 的初始化基本就肯定了当前哪些 Object3D 对象是须要渲染的,接下来就是逐个 Object3D 的渲染了。
function renderObjects( renderList, scene, camera, overrideMaterial ) {
for ( var i = 0, l = renderList.length; i < l; i ++ ) {
var renderItem = renderList[ i ];
......
if ( camera.isArrayCamera ) {
......
} else {
_currentArrayCamera = null;
renderObject( object, scene, camera, geometry, material, group );
}
}
}
复制代码
renderObjects 就是遍历全部的 Object3D 对象,而后调用 renderObject() 方法进行进一步渲染。看来脏活都交给了 renderObject()。
function renderObject( object, scene, camera, geometry, material, group ) {
......
// 计算 mode view matrix 以及 normal matrix
object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
object.normalMatrix.getNormalMatrix( object.modelViewMatrix );
if ( object.isImmediateRenderObject ) {
......
} else {
_this.renderBufferDirect( camera, scene.fog, geometry, material, object, group );
}
......
}
复制代码
关于计算 mode view matrix 以及 normal matrix,这里我也不太看明白,因此我选择先跳过。先分析后面的步骤。这里不论是否 isImmediateRenderObject 其流程上差不太多,因此这里先分析 renderBufferDirect()。
renderBufferDirect()方法
this.renderBufferDirect = function ( camera, fog, geometry, material, object, group ) {
......
// 1.经过WebGLState设置材质的一些属性
state.setMaterial( material, frontFaceCW );
// 2.设置 program
var program = setProgram( camera, fog, material, object );
......
if ( updateBuffers ) {
// 3.设置顶点属性
setupVertexAttributes( material, program, geometry );
if ( index !== null ) {
// 4.绑定 buffer
_gl.bindBuffer( _gl.ELEMENT_ARRAY_BUFFER, attribute.buffer );
}
}
......
// 5.根据不一样网格类型肯定相应的绘制模式
if ( object.isMesh ) {
if ( material.wireframe === true ) {
......
renderer.setMode( _gl.LINES );
} else {
switch ( object.drawMode ) {
case TrianglesDrawMode:
renderer.setMode( _gl.TRIANGLES );
break;
case TriangleStripDrawMode:
renderer.setMode( _gl.TRIANGLE_STRIP );
break;
case TriangleFanDrawMode:
renderer.setMode( _gl.TRIANGLE_FAN );
break;
}
}
} else if ( object.isLine ) {
......
if ( object.isLineSegments ) {
renderer.setMode( _gl.LINES );
} else if ( object.isLineLoop ) {
renderer.setMode( _gl.LINE_LOOP );
} else {
renderer.setMode( _gl.LINE_STRIP );
}
} else if ( object.isPoints ) {
renderer.setMode( _gl.POINTS );
} else if ( object.isSprite ) {
renderer.setMode( _gl.TRIANGLES );
}
if ( geometry && geometry.isInstancedBufferGeometry ) {
if ( geometry.maxInstancedCount > 0 ) {
renderer.renderInstances( geometry, drawStart, drawCount );
}
} else {
// 6.调用 WebGLBufferRenderer#render() 方法进行渲染
renderer.render( drawStart, drawCount );
}
};
复制代码
renderBufferDirect()方法是一个比较重要的方法,在这里能够看到一个物体被渲染的“最小完整流程”。
function setProgram( camera, fog, material, object ) {
.....
.....
var materialProperties = properties.get( material );
var lights = currentRenderState.state.lights;
if ( material.needsUpdate ) {
initMaterial( material, fog, object );
material.needsUpdate = false;
}
......
// 这里的 program 即 WebGLProgram,也就是咱们在流程图中所说的建立程序
var program = materialProperties.program,
p_uniforms = program.getUniforms(),
m_uniforms = materialProperties.shader.uniforms;
if ( state.useProgram( program.program ) ) {
refreshProgram = true;
refreshMaterial = true;
refreshLights = true;
}
......
p_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );
p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
p_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );
return program;
}
复制代码
这个方法自己是很长的,这里省略了一万字.... 咱们再来看看其主要所作的事情,这里的 program 就是 WebGLProgram。而想知道 program 具体是什么,这里就涉及到了 WebGLProgram 的初始化。
function WebGLProgram( renderer, extensions, code, material, shader, parameters, capabilities ) {
var gl = renderer.context;
var defines = material.defines;
// 获取顶点 shader 以及片元 shader
var vertexShader = shader.vertexShader;
var fragmentShader = shader.fragmentShader;
......
// 建立 program
var program = gl.createProgram();
......
// 构造最终用于进行渲染的 glsl,而且调用 WebGLShader 构造出 shader
var vertexGlsl = prefixVertex + vertexShader;
var fragmentGlsl = prefixFragment + fragmentShader;
// console.log( '*VERTEX*', vertexGlsl );
// console.log( '*FRAGMENT*', fragmentGlsl );
var glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
var glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl );
// 将 program 关联 shader
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
......
// 连接 program
gl.linkProgram( program );
......
}
复制代码
program 的初始化方法也是很是多的,这里简化出关键部分。再回忆一下前面的流程图,就会明白这里主要就是建立 program、shader ,关联 program 和 shader,以及连接程序。连接好了程序以后接下来就能够经过 useProgram() 使用 program 了,这一步骤在 setProgram() 中建立好 program 就调用了。 3. 设置顶点属性,就是将咱们在外面所构造的 geometry 的顶点送到 shader 中去。 4. 绑定 buffer。 5. 根据不一样网格类型肯定相应的绘制模式,如以 LINES 进行绘制,以TRIANGLES 进行绘制。 6. 调用 WebGLBufferRenderer#render() 方法进行渲染。以下,就是进行最后的 drawArrays() 调用,将上层建立的 geometry 以及 material(组合起来就叫作 mesh) 渲染到 3D 场景的 canvas 中。
function render( start, count ) {
gl.drawArrays( mode, start, count );
info.update( count, mode );
}
复制代码
文章一样以一篇 demo 为入口对渲染过程进行了一个简要的分析,其中还介绍了 OpenGL / WebGL 所须要知道的基础知识。这其中了解了 OpenGL 的绘制流程以及各坐标系之间的关系以及转换,然后面的分析都是沿着这个绘制流程进行的。
然而,因为做者的水平有限,而 OpenGL / WebGL 又是如此的强大,实在不能面面俱到,甚至对某些知识点也没法透彻分析。所以,还请见谅。
最后,感谢你能读到并读完此文章,若是分析的过程当中存在错误或者疑问都欢迎留言讨论。若是个人分享可以帮助到你,还请记得帮忙点个赞吧,谢谢。