一虏冻、前言
ThreeJs 封裝了 WebGL 進(jìn)行渲染時所涉及到的相關(guān)概念,如光照弹囚,材質(zhì)厨相,紋理以及相機(jī)等。除此之外,其還抽象了場景(Scene)以及用于渲染的渲染器(WebGLRenderer)蛮穿。這些相關(guān)概念都被封裝成了一個對象庶骄,那么它們是如何協(xié)作的呢,關(guān)系又是如何呢践磅?這篇文章主要就是來分析一下 ThreeJs 中核心中的核心单刁,即場景,物體府适,光照羔飞,材質(zhì),紋理以及相機(jī)這些對象是如何渲染的檐春。
下面截取了一個渲染效果圖逻淌,看起來還不錯是不是。這是 ThreeJs 的官方 demo lights / spotlights 的渲染效果圖疟暖。這個 demo 中就基本涉及到了上面所提的核心對象卡儒,下面我將基于此 demo 來分析這些核心對象是如何被組織在一起進(jìn)行渲染的。
二誓篱、demo 解析
Demo 有一點(diǎn)點(diǎn)長朋贬,對于不熟悉 ThreeJs 的人來說會有一點(diǎn)點(diǎn)難度,因此這里主要分析了構(gòu)建窜骄、初始化以及渲染 3 個部分來分別說明锦募。
1.構(gòu)建
// 構(gòu)建渲染器 WebGLRenderer
var renderer = new THREE.WebGLRenderer();
// 設(shè)置顯示比例
renderer.setPixelRatio( window.devicePixelRatio );
// 構(gòu)建一個透視投影的相機(jī)
var camera = new THREE.PerspectiveCamera( 35, window.innerWidth / window.innerHeight, 1, 2000 );
// 構(gòu)建一個軌道控制器,主要就是通過鼠標(biāo)來控制相機(jī)沿目標(biāo)物體旋轉(zhuǎn)邻遏,從而達(dá)到像在旋轉(zhuǎn)場景一樣糠亩,可以從各個不同角度觀察物體
var controls = new THREE.OrbitControls( camera, renderer.domElement );
// 構(gòu)建場景
var scene = new THREE.Scene();
// 構(gòu)建Phong網(wǎng)格材質(zhì)MeshPhongMaterial,該材質(zhì)可以模擬具有鏡面高光的光澤表面准验,一個用于接收陰影的平面赎线,一個用于場景中的物體 Box
var matFloor = new THREE.MeshPhongMaterial();
var matBox = new THREE.MeshPhongMaterial( { color: 0xaaaaaa } );
// 構(gòu)建幾何體,同樣分別用于 平面 和 Box
var geoFloor = new THREE.PlaneBufferGeometry( 2000, 2000 );
var geoBox = new THREE.BoxBufferGeometry( 3, 1, 2 );
// 構(gòu)建平面網(wǎng)格 mesh
var mshFloor = new THREE.Mesh( geoFloor, matFloor );
mshFloor.rotation.x = - Math.PI * 0.5;
// 構(gòu)建 box 網(wǎng)格 mesh
var mshBox = new THREE.Mesh( geoBox, matBox );
// 構(gòu)建環(huán)境光
var ambient = new THREE.AmbientLight( 0x111111 );
// 構(gòu)建 3 個不同顏色的 聚光燈(SpotLight)
var spotLight1 = createSpotlight( 0xFF7F00 );
var spotLight2 = createSpotlight( 0x00FF7F );
var spotLight3 = createSpotlight( 0x7F00FF );
// 聲明用于描述聚光燈的 3 個不同光束幫助器
var lightHelper1, lightHelper2, lightHelper3;
上面代碼中糊饱,基本上每一行都添加了詳細(xì)的注釋垂寥,其中有調(diào)用了一個內(nèi)部的函數(shù) 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;
}
這個方法滞项,主要就是根據(jù)指定的顏色構(gòu)建一個聚光燈并設(shè)置好相應(yīng)的參數(shù)。這里不管是相機(jī)夭坪、光照文判、材質(zhì)還是物體,其詳細(xì)的參數(shù)并不打算在這里一一講述室梅,有需要的話再進(jìn)一步說明戏仓。
2.初始化
function init() {
......
// 將平面疚宇,box,環(huán)境光以及光源輔助器等全部添加到 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 敷待,光照這些都添加進(jìn)場景中,但是要注意嗓奢,相機(jī)并沒有被添加進(jìn)來讼撒。
3.渲染
function render() {
TWEEN.update();
if ( lightHelper1 ) lightHelper1.update();
if ( lightHelper2 ) lightHelper2.update();
if ( lightHelper3 ) lightHelper3.update();
renderer.render( scene, camera );
requestAnimationFrame( render );
}
渲染函數(shù) render() 中最關(guān)鍵的調(diào)用渲染器的 WebGLRenderer#render() 方法同時去渲染場景和相機(jī)浑厚。
根據(jù)上面的分析股耽,以及對 ThreeJs 源碼的分析,梳理出如下 2 個類圖關(guān)系钳幅。
圖中物蝙,渲染器負(fù)責(zé)同時渲染場景以及相機(jī)。而光照和網(wǎng)格都被添加到場景中敢艰。幾何體以及材質(zhì)都是網(wǎng)格的 2 個基本屬性诬乞,也決定一個網(wǎng)格的形狀和表面紋理。
該圖是對上圖的補(bǔ)充钠导,說明光照震嫉,相機(jī)以及網(wǎng)格都屬于 Object3D 對象。在 ThreeJs 中還有許多的類都是繼承自 Object3D 的牡属。
三票堵、渲染分析
1.關(guān)于 WebGL 需要知道的基本知識
1.1 WebGL 的渲染管線
先來看一下 WebGL 的流水線渲染管線圖,如下所示逮栅。這個是必須要了解的悴势,我們可以不必完全理解渲染管線的每個步驟,但我們必須要知道渲染管線的這個流程措伐。
渲染管線指的是WebGL程序的執(zhí)行過程特纤,如上圖所示,主要分為 4 個步驟:
頂點(diǎn)著色器的處理侥加,主要是一組矩陣變換操作捧存,用來把3D模型(頂點(diǎn)和原型)投影到viewport上,輸出是一個個的多邊形担败,比如三角形昔穴。
光柵化,也就是把三角形連接區(qū)域按一定的粒度逐行轉(zhuǎn)化成片元(fragement)氢架,類似于2D空間中傻咖,可以把這些片元看做是3D空間的一個像素點(diǎn)。
片元著色器的處理岖研,為每個片元添加顏色或者紋理卿操。只要給出紋理或者顏色警检,以及紋理坐標(biāo)(uv),管線就會根據(jù)紋理坐標(biāo)進(jìn)行插值運(yùn)算害淤,將紋理或者圖片著色在相應(yīng)的片元上扇雕。
把3D空間的片元合并輸出為2D像素數(shù)組并顯示在屏幕上。
1.2 WebGL 一般的開發(fā)流程
因為作者也沒進(jìn)行過原生的 WebGL 開發(fā)窥摄,而是一上來就擼起了 ThreeJs镶奉。所以 這里僅根據(jù) Open GL ES 的開發(fā)流程,繪制出如下流程圖崭放。
流程圖中關(guān)鍵的第一步在于創(chuàng)建著色器(Shader)程序哨苛,著色器程序主要用 GLSL(GL Shading Language) 語言編寫,其直接由 GPU 來執(zhí)行币砂。第二步是設(shè)置頂點(diǎn)建峭,紋理以及其他屬性,如我們創(chuàng)建的幾何圖元 Box决摧,加載的 obj 文件亿蒸,以及用于矩陣變換的模型矩陣,視圖矩陣以及投影矩陣等掌桩。第三步便是進(jìn)行頂點(diǎn)的繪制边锁,如以點(diǎn)繪制,以直線繪制以及以三角形繪制波岛,對于圖元茅坛,大部分是以三角形繪制。
1.3 坐標(biāo)系以及矩陣變換
關(guān)于坐標(biāo)系與矩陣變換盆色,這里一個幅圖總結(jié)的很不錯灰蛙,畫的很詳細(xì),一眼就能看出其中的意思隔躲。
關(guān)于 WebGL 的基本就介紹這么多摩梧,這里的目的是為了讓后面的分析有個簡單的鋪墊。如果感興趣宣旱,可以參考更多大牛專門介紹 WebGL / Open GL ES 的文章仅父。
2.渲染器 WebGLRenderer 的初始化
WebGLRenderer 的初始化主要在它的構(gòu)造方法 WebGLRenderer() 和 initGLContext() 中。這里先看看構(gòu)造方法 WebGLRenderer() 浑吟。
2.1 構(gòu)造方法 WebGLRenderer()
其初始化的屬性很多笙纤。這里主要關(guān)注其 2 個最核心的屬性 canvas 以及 context。
function WebGLRenderer( parameters ) {
console.log( 'THREE.WebGLRenderer', REVISION );
parameters = parameters || {};
// 如果參數(shù)中有 canvas组力,就有參數(shù)中的省容,如果沒有就通過 document.createElementNS() 來創(chuàng)建一個。和 2D 的概念一樣燎字,這里的 canvas 主要是用來進(jìn)行 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阿宅。參數(shù) webgl 是其中之一,其還可以獲取 2d 的笼蛛。這里獲取到 webgl 的 context洒放,那就意味者可以通過它進(jìn)行 3D 繪制了。
_gl = _context || _canvas.getContext( 'webgl', contextAttributes ) || _canvas.getContext( 'experimental-webgl', contextAttributes );
......
function initGLContext() {
......
_this.context = _gl;
......
}
......
}
如上面的代碼以及注釋滨砍,canvas 就是 html 的標(biāo)準(zhǔn)元素<canvas>往湿,這玩意兒在 Android 那里也叫做 canvas,反正就代表畫布的意思惋戏。而 context 則是從該畫布獲取到的 'webgl' 上下文领追,這個上下文是 WebGLRenderingContext
,WebGLRenderingContext 接口提供基于 OpenGL ES 2.0 的繪圖上下文日川,用于在 HTML <canvas> 元素內(nèi)繪圖蔓腐。后續(xù)的關(guān)于 Open GL ES 的相關(guān)操作都是基于此進(jìn)行的。當(dāng)然龄句,這里還只是創(chuàng)建了用于 Open GL ES 的 WebGLContext,還沒有進(jìn)行初始化散罕。下面再來詳細(xì)看看它在 initGLContext() 方法中是如何進(jìn)行初始化的分歇,初始化中具體又詳細(xì)做了什么具體的事情。
2.2 初始化上下文方法 initGLContext()
function initGLContext() {
/**
* 擴(kuò)展特性
*/
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 );
/**
* 狀態(tài)
*/
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 );
/**
* 屬性存儲輔助類欧漱,主要實現(xiàn) JavaScript 中的變量或者數(shù)組职抡、紋理圖片傳遞到 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() 方法中看起來有很多的組件初始化,但實質(zhì)這些組件也只是進(jìn)行一個最基本的構(gòu)造而已冈钦,沒有進(jìn)一步更深入的過程郊丛。因此,這里也粗略的看一下即可瞧筛。
2.WebGLRenderer#render() 函數(shù)分析
整個 render 的過程是十分復(fù)雜的厉熟,也是漫長的,需要我們耐心去看较幌,去理解揍瑟。先來簡單過一下它的時序圖。
從時序圖可見乍炉,其涉及到的相對象以及步驟是比較多的绢片,共 20 步嘁字。其中涉及到的主要對象有:Scene,Camera,WebGLRenderStates,WebGLRenderLists,WebGLBackground,WebGLProgram,_gl,WebGLBufferRenderer。我們比較熟悉的是 Scene杉畜,因為我們的Object / Mesh 都是被添加到它里面的纪蜒,另外還有 Camera,我們必須要有一個相機(jī)來告訴我們以怎么樣的視角來觀看這個 3D 世界此叠。另外一些不熟悉的對象纯续,WebGLRenderList 管理著我們需要拿去 render 的 Object / Mesh,WebGLBackground 描述了場景的背景灭袁,WebGLProgram 則創(chuàng)建了用于鏈接猬错、執(zhí)行 Shader 的程序,而 WebGLBufferRenderer 則是整個 3D 世界被 render 到的目的地茸歧。
這里不會按照時序圖倦炒,逐步逐步地進(jìn)行分析,而是挑重點(diǎn)软瞎,同時保持與前面所述的 OpenGL ES 的流程一致性上進(jìn)行分析逢唤。
render() 函數(shù)
this.render = function ( scene, camera, renderTarget, forceClear ) {
// 前面是一些參數(shù)的校驗,這里省略
// 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.視景體矩陣計算涤浇,為相機(jī)的投影矩陣與相機(jī)的世界矩陣的逆矩陣的叉乘鳖藕?
_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.強(qiáng)制使用場景的材質(zhì) overrideMaterial 來統(tǒng)一 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 的物體進(jìn)行 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() 是渲染的核心只锭,粗略地看它做了大概以下的事情著恩。
- reset caching for this frame。
- update scene graph蜻展。
- update camera matrices and frustum喉誊。
- init WebGLRenderState。
- 視景體矩陣計算纵顾,為相機(jī)的投影矩陣與相機(jī)的世界矩陣的逆矩陣的叉乘伍茄。
- WebGLRenderList 的初始化。
- shadow 的繪制片挂。
- 背景的繪制幻林。
- render scene。
- 如果overrideMaterial音念,則強(qiáng)制使用場景的材質(zhì) overrideMaterial 來統(tǒng)一 render 物體沪饺。
- 分別對 opaque 和 transparent 的物體進(jìn)行 render。
但這里我們不必關(guān)注每個處理的細(xì)節(jié)闷愤,僅從幾個重要的點(diǎn)著手去理解以及分析整葡。
2.1 update scene graph
即更新整個場景圖,主要就是更新每個物體的 matrix讥脐。如果其含有孩子節(jié)點(diǎn)遭居,則還會逐級更新啼器。在這里,每個物體的 matrix 是通過其 position,quaternion以及scale 計算得來的俱萍,也就是其模型矩陣端壳,而 matrixWorld 又是根據(jù) matrix 計算得來的。如果當(dāng)前節(jié)點(diǎn)沒有父節(jié)點(diǎn)枪蘑,則 matrix 就是 matrixWorld损谦。而如果有的話,那 matrixWorld 則為父節(jié)點(diǎn)的 matrixWorld 與當(dāng)前節(jié)點(diǎn) matrix 的叉乘岳颇。也就是說當(dāng)前節(jié)點(diǎn)的 matrixWorld 是相對于其父親節(jié)點(diǎn)的照捡。
2.2 WebGLRenderList 的初始化
WebGLRenderList 的初始化init()方法本身并沒有什么,其只是在 WebGLRenderLists 中通過將 scene.id 和 camera.id 建立起一定的關(guān)聯(lián)话侧。而這里更重要的目的是確定有哪些對象是要被渲染出來的栗精,這個最主要的實現(xiàn)就在 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 );
}
}
}
}
// 對每個孩子進(jìn)行遞歸遍歷
var children = object.children;
for ( var i = 0, l = children.length; i < l; i ++ ) {
projectObject( children[ i ], camera, sortObjects );
}
}
從方法中瞻鹏,我們大致得到如下結(jié)論:
- 只有可見的光照悲立,精靈,mesh乙漓,line级历,point 會被實際渲染出來。而如果我們只是 new 一個 Object3D 而被指到具體的 3D 對象上叭披,那么理論上它是不會被渲染的。
- 光照與其他 Object3D 不一樣玩讳,它是另外單獨(dú)被放在 currentRenderState 中的涩蜘。
- 對于整個要渲染的場景圖利用遞歸進(jìn)行遍歷,以確保場景圖中的每一個可渲染的 3D object 都可以被渲染出來熏纯。這里簡單回顧一下同诫,Sence 也是繼承自 Object3D 的,而燈光以及可被渲染的 Object 都是作為它的孩子被加入到 Sence 中的樟澜。
通過 WebGLRenderList 的初始化基本就確定了當(dāng)前哪些 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 對象秩贰,然后調(diào)用 renderObject() 方法進(jìn)行進(jìn)一步渲染霹俺。看來臟活都交給了 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 );
}
......
}
關(guān)于計算 mode view matrix 以及 normal matrix丙唧,這里我也不太看明白,所以我選擇先跳過觅玻。先分析后面的步驟想际。這里不管是否 isImmediateRenderObject 其流程上差不太多培漏,所以這里先分析 renderBufferDirect()。
renderBufferDirect()方法
this.renderBufferDirect = function ( camera, fog, geometry, material, object, group ) {
......
// 1.通過WebGLState設(shè)置材質(zhì)的一些屬性
state.setMaterial( material, frontFaceCW );
// 2.設(shè)置 program
var program = setProgram( camera, fog, material, object );
......
if ( updateBuffers ) {
// 3.設(shè)置頂點(diǎn)屬性
setupVertexAttributes( material, program, geometry );
if ( index !== null ) {
// 4.綁定 buffer
_gl.bindBuffer( _gl.ELEMENT_ARRAY_BUFFER, attribute.buffer );
}
}
......
// 5.根據(jù)不同網(wǎng)格類型確定相應(yīng)的繪制模式
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.調(diào)用 WebGLBufferRenderer#render() 方法進(jìn)行渲染
renderer.render( drawStart, drawCount );
}
};
renderBufferDirect()方法是一個比較重要的方法胡本,在這里可以看到一個物體被渲染的“最小完整流程”牌柄。
- 通過WebGLState設(shè)置材質(zhì)的一些屬性。這個比較形象侧甫,因為整個 OpenGL / ES 它就是一個狀態(tài)機(jī)珊佣。這里所設(shè)置的材質(zhì)屬性也是直接調(diào)用底層的 gl_xxx() 之類的方法。而這里實際就是設(shè)置了如 CULL_FACE,depthTest,depthWrite,colorWrite 等等闺骚。
- 設(shè)置 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,也就是我們在流程圖中所說的創(chuàng)建程序
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;
// 獲取頂點(diǎn) shader 以及片元 shader
var vertexShader = shader.vertexShader;
var fragmentShader = shader.fragmentShader;
......
// 創(chuàng)建 program
var program = gl.createProgram();
......
// 構(gòu)造最終用于進(jìn)行渲染的 glsl,并且調(diào)用 WebGLShader 構(gòu)造出 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 關(guān)聯(lián) shader
gl.attachShader( program, glVertexShader );
gl.attachShader( program, glFragmentShader );
......
// 鏈接 program
gl.linkProgram( program );
......
}
program 的初始化方法也是非常多的碰镜,這里簡化出關(guān)鍵部分兢卵。再回憶一下前面的流程圖,就會明白這里主要就是創(chuàng)建 program绪颖、shader 秽荤,關(guān)聯(lián) program 和 shader,以及鏈接程序柠横。鏈接好了程序之后接下來就可以通過 useProgram() 使用 program 了窃款,這一步驟在 setProgram() 中創(chuàng)建好 program 就調(diào)用了。
- 設(shè)置頂點(diǎn)屬性牍氛,就是將我們在外面所構(gòu)造的 geometry 的頂點(diǎn)送到 shader 中去晨继。
- 綁定 buffer。
- 根據(jù)不同網(wǎng)格類型確定相應(yīng)的繪制模式搬俊,如以 LINES 進(jìn)行繪制紊扬,以TRIANGLES 進(jìn)行繪制。
- 調(diào)用 WebGLBufferRenderer#render() 方法進(jìn)行渲染唉擂。如下餐屎,就是進(jìn)行最后的 drawArrays() 調(diào)用,將上層創(chuàng)建的 geometry 以及 material(組合起來就叫做 mesh) 渲染到 3D 場景的 canvas 中楔敌。
function render( start, count ) {
gl.drawArrays( mode, start, count );
info.update( count, mode );
}
四啤挎、總結(jié)
文章同樣以一篇 demo 為入口對渲染過程進(jìn)行了一個簡要的分析,其中還介紹了 OpenGL / WebGL 所需要知道的基礎(chǔ)知識。這其中了解了 OpenGL 的繪制流程以及各坐標(biāo)系之間的關(guān)系以及轉(zhuǎn)換庆聘,而后面的分析都是沿著這個繪制流程進(jìn)行的胜臊。
然而,由于作者的水平有限伙判,而 OpenGL / WebGL 又是如此的強(qiáng)大象对,實在不能面面俱到,甚至對某些知識點(diǎn)也無法透徹分析宴抚。因此勒魔,還請見諒。
最后菇曲,感謝你能讀到并讀完此文章冠绢,如果分析的過程中存在錯誤或者疑問都?xì)g迎留言討論。如果我的分享能夠幫助到你常潮,還請記得幫忙點(diǎn)個贊吧弟胀,謝謝。