ThreeJs学习笔记——渲染(render)分析

6,079 阅读15分钟

一、前言

ThreeJs 封装了 WebGL 进行渲染时所涉及到的相关概念,如光照,材质,纹理以及相机等。除此之外,其还抽象了场景(Scene)以及用于渲染的渲染器(WebGLRenderer)。这些相关概念都被封装成了一个对象,那么它们是如何协作的呢,关系又是如何呢?这篇文章主要就是来分析一下 ThreeJs 中核心中的核心,即场景,物体,光照,材质,纹理以及相机这些对象是如何渲染的。

下面截取了一个渲染效果图,看起来还不错是不是。这是 ThreeJs 的官方 demo lights / spotlights 的渲染效果图。这个 demo 中就基本涉及到了上面所提的核心对象,下面我将基于此 demo 来分析这些核心对象是如何被组织在一起进行渲染的。

SpotLight.gif

二、demo 解析

Demo 有一点点长,对于不熟悉 ThreeJs 的人来说会有一点点难度,因此这里主要分析了构建、初始化以及渲染 3 个部分来分别说明。

1.构建

// 构建渲染器 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;

}

这个方法,主要就是根据指定的颜色构建一个聚光灯并设置好相应的参数。这里不管是相机、光照、材质还是物体,其详细的参数并不打算在这里一一讲述,有需要的话再进一步说明。

2.初始化

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 ,光照这些都添加进场景中,但是要注意,相机并没有被添加进来。

3.渲染

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() 方法同时去渲染场景和相机。

根据上面的分析,以及对 ThreeJs 源码的分析,梳理出如下 2 个类图关系。

WebGLRenderer.jpg

图中,渲染器负责同时渲染场景以及相机。而光照和网格都被添加到场景中。几何体以及材质都是网格的 2 个基本属性,也决定一个网格的形状和表面纹理。

RenderObject.jpg

该图是对上图的补充,说明光照,相机以及网格都属于 Object3D 对象。在 ThreeJs 中还有许多的类都是继承自 Object3D 的。

三、渲染分析

1.关于 WebGL 需要知道的基本知识

1.1 WebGL 的渲染管线

先来看一下 WebGL 的流水线渲染管线图,如下所示。这个是必须要了解的,我们可以不必完全理解渲染管线的每个步骤,但我们必须要知道渲染管线的这个流程。

WebGL 流水线

渲染管线指的是WebGL程序的执行过程,如上图所示,主要分为 4 个步骤:

  1. 顶点着色器的处理,主要是一组矩阵变换操作,用来把3D模型(顶点和原型)投影到viewport上,输出是一个个的多边形,比如三角形。

  2. 光栅化,也就是把三角形连接区域按一定的粒度逐行转化成片元(fragement),类似于2D空间中,可以把这些片元看做是3D空间的一个像素点。

  3. 片元着色器的处理,为每个片元添加颜色或者纹理。只要给出纹理或者颜色,以及纹理坐标(uv),管线就会根据纹理坐标进行插值运算,将纹理或者图片着色在相应的片元上。

  4. 把3D空间的片元合并输出为2D像素数组并显示在屏幕上。

1.2 WebGL 一般的开发流程

因为作者也没进行过原生的 WebGL 开发,而是一上来就撸起了 ThreeJs。所以 这里仅根据 Open GL ES 的开发流程,绘制出如下流程图。

OpenGL ES  开发流程图.jpg

流程图中关键的第一步在于创建着色器(Shader)程序,着色器程序主要用 GLSL(GL Shading Language) 语言编写,其直接由 GPU 来执行。第二步是设置顶点,纹理以及其他属性,如我们创建的几何图元 Box,加载的 obj 文件,以及用于矩阵变换的模型矩阵,视图矩阵以及投影矩阵等。第三步便是进行顶点的绘制,如以点绘制,以直线绘制以及以三角形绘制,对于图元,大部分是以三角形绘制。

1.3 坐标系以及矩阵变换

关于坐标系与矩阵变换,这里一个幅图总结的很不错,画的很详细,一眼就能看出其中的意思。

坐标系与矩阵变换

关于 WebGL 的基本就介绍这么多,这里的目的是为了让后面的分析有个简单的铺垫。如果感兴趣,可以参考更多大牛专门介绍 WebGL / Open GL ES 的文章。

2.渲染器 WebGLRenderer 的初始化

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 的标准元素,这玩意儿在 Android 那里也叫做 canvas,反正就代表画布的意思。而 context 则是从该画布获取到的 'webgl' 上下文,这个上下文是 WebGLRenderingContext,WebGLRenderingContext 接口提供基于 OpenGL ES 2.0 的绘图上下文,用于在 HTML 元素内绘图。后续的关于 Open GL ES 的相关操作都是基于此进行的。当然,这里还只是创建了用于 Open GL ES 的 WebGLContext,还没有进行初始化。下面再来详细看看它在 initGLContext() 方法中是如何进行初始化的,初始化中具体又详细做了什么具体的事情。

####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() 方法中看起来有很多的组件初始化,但实质这些组件也只是进行一个最基本的构造而已,没有进一步更深入的过程。因此,这里也粗略的看一下即可。

2.WebGLRenderer#render() 函数分析

整个 render 的过程是十分复杂的,也是漫长的,需要我们耐心去看,去理解。先来简单过一下它的时序图。

Render时序图.jpg

从时序图可见,其涉及到的相对象以及步骤是比较多的,共 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() 是渲染的核心,粗略地看它做了大概以下的事情。

  1. reset caching for this frame。
  2. update scene graph。
  3. update camera matrices and frustum。
  4. init WebGLRenderState。
  5. 视景体矩阵计算,为相机的投影矩阵与相机的世界矩阵的逆矩阵的叉乘。
  6. WebGLRenderList 的初始化。
  7. shadow 的绘制。
  8. 背景的绘制。
  9. render scene。
  10. 如果overrideMaterial,则强制使用场景的材质 overrideMaterial 来统一 render 物体。
  11. 分别对 opaque 和 transparent 的物体进行 render。

但这里我们不必关注每个处理的细节,仅从几个重要的点着手去理解以及分析。

2.1 update scene graph

即更新整个场景图,主要就是更新每个物体的 matrix。如果其含有孩子节点,则还会逐级更新。在这里,每个物体的 matrix 是通过其 position,quaternion以及scale 计算得来的,也就是其模型矩阵,而 matrixWorld 又是根据 matrix 计算得来的。如果当前节点没有父节点,则 matrix 就是 matrixWorld。而如果有的话,那 matrixWorld 则为父节点的 matrixWorld 与当前节点 matrix 的叉乘。也就是说当前节点的 matrixWorld 是相对于其父亲节点的。

2.2 WebGLRenderList 的初始化

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 );

		}

	}

从方法中,我们大致得到如下结论:

  1. 只有可见的光照,精灵,mesh,line,point 会被实际渲染出来。而如果我们只是 new 一个 Object3D 而被指到具体的 3D 对象上,那么理论上它是不会被渲染的。
  2. 光照与其他 Object3D 不一样,它是另外单独被放在 currentRenderState 中的。
  3. 对于整个要渲染的场景图利用递归进行遍历,以确保场景图中的每一个可渲染的 3D object 都可以被渲染出来。这里简单回顾一下,Sence 也是继承自 Object3D 的,而灯光以及可被渲染的 Object 都是作为它的孩子被加入到 Sence 中的。

通过 WebGLRenderList 的初始化基本就确定了当前哪些 Object3D 对象是需要渲染的,接下来就是逐个 Object3D 的渲染了。

2.3 renderObjects

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()。

2.4 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()方法是一个比较重要的方法,在这里可以看到一个物体被渲染的“最小完整流程”。

  1. 通过WebGLState设置材质的一些属性。这个比较形象,因为整个 OpenGL / ES 它就是一个状态机。这里所设置的材质属性也是直接调用底层的 gl_xxx() 之类的方法。而这里实际就是设置了如 CULL_FACE,depthTest,depthWrite,colorWrite 等等。
  2. 设置 program。
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 又是如此的强大,实在不能面面俱到,甚至对某些知识点也无法透彻分析。因此,还请见谅。

最后,感谢你能读到并读完此文章,如果分析的过程中存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,还请记得帮忙点个赞吧,谢谢。