ThreeJs學習筆記——渲染(render)分析

仰簡發表於2019-04-10

一、前言

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 又是如此的強大,實在不能面面俱到,甚至對某些知識點也無法透徹分析。因此,還請見諒。

最後,感謝你能讀到並讀完此文章,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,還請記得幫忙點個贊吧,謝謝。

相關文章