今天和你們分享的是webgl渲染樹形結構的流程。用過threejs,babylonjs的同窗都知道,一個大模型都是由n個子模型拼裝而成的,那麼如何依次渲染子模型,以及渲染每一個子模型在原生webgl中的流程是怎樣的呢,我就以osg框架爲本來,爲同窗們展現出來。html
首先介紹osg框架,該框架是基於openGL的幾何引擎框架,目前個人工做是將其翻譯成爲webgl的幾何引擎,在這個過程當中學習webgl原生架構的原理和工程構造方式,踩了各類坑,每次爬出坑都以爲本身又強大了一點,呵。node
閒話少敘,切入正題,首先咱們要明確一個渲染流程,那就是webgl究竟是怎麼將模型繪製到canvas畫布中去的,這就牽扯到我以前的一片文章《原生WebGL場景中繪製多個圓錐圓柱》,連接地址https://www.cnblogs.com/ccentry/p/9864847.html,這篇文章講述的是用原生webgl向canvas中持續繪製多個模型,但這篇文章的侷限性在於,他重複使用了同一組shader(頂點shader,片斷shader),而且多個模型也不存在父子關係,這就致使了局部座標系和全局座標系的紊亂。今天咱們就來彌補這篇文章的不足之處。web
循序漸進,咱們先討論的是webgl渲染單個模型的過程,首先咱們構造着色器,請看下面着色器代碼編程
attribute vec3 position; attribute vec3 normal; attribute vec4 color; uniform mat4 mvpMatrix; uniform mat4 invMatrix; uniform vec3 lightDirection; uniform vec4 ambientColor; varying vec4 vColor; uniform float lightS; void main(void){ vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0)).xyz; float diffuse = clamp(dot(normal, invLight), 0.0, 1.0) * lightS; vColor = color * vec4(vec3(diffuse), 1.0) + ambientColor; gl_Position = mvpMatrix * vec4(position, 1.0); }
頂點着色器canvas
precision mediump float; varying vec4 vColor; void main(void){ gl_FragColor = vColor; }
片斷着色器
好了,接下來咱們的模型數據怎麼和着色器進行數據連接呢,很簡單,咱們首先建立着色器的gl對象,用以js傳參,請看代碼架構
/** * 生成着色器的函數 */ function create_shader(id){ // 用來保存着色器的變量 var shader; // 根據id從HTML中獲取指定的script標籤 var scriptElement = document.getElementById(id); // 若是指定的script標籤不存在,則返回 if(!scriptElement){return;} // 判斷script標籤的type屬性 switch(scriptElement.type){ // 頂點着色器的時候 case 'x-shader/x-vertex': shader = gl.createShader(gl.VERTEX_SHADER); break; // 片斷着色器的時候 case 'x-shader/x-fragment': shader = gl.createShader(gl.FRAGMENT_SHADER); break; default : return; } // 將標籤中的代碼分配給生成的着色器 gl.shaderSource(shader, scriptElement.text); // 編譯着色器 gl.compileShader(shader); // 判斷一下着色器是否編譯成功 if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){ // 編譯成功,則返回着色器 return shader; }else{ // 編譯失敗,彈出錯誤消息 alert(gl.getShaderInfoLog(shader)); } }
這個函數生成的既能夠是頂點着色器,也能夠是片斷着色器,在此不加贅述,有了着色器的gl對象,咱們就能向着色器裏傳attribute和uniform參數了嗎,顯然不行,那麼接下來咱們就要構造一個能夠向着色器對象傳參數的程序對象gl.program,這也是難點之一,先看代碼框架
/** * 程序對象的生成和着色器鏈接的函數 */ function create_program(vs, fs){ // 程序對象的生成 var program = gl.createProgram(); // 向程序對象裏分配着色器 gl.attachShader(program, vs); gl.attachShader(program, fs); // 將着色器鏈接 gl.linkProgram(program); // 判斷着色器的鏈接是否成功 if(gl.getProgramParameter(program, gl.LINK_STATUS)){ // 成功的話,將程序對象設置爲有效 gl.useProgram(program); // 返回程序對象 return program; }else{ // 若是失敗,彈出錯誤信息 alert(gl.getProgramInfoLog(program)); } }
咱們看到這個函數作了兩件事,第一gl.attachShader將咱們剛剛生成的着色器對象綁定到gl.program編程對象上,第二件事就是gl.useProgram激活綁定好着色器對象的編程對象,固然第二件事有待商榷,那就是若是咱們有多個gl.program是否是每次建立綁定好着色器都要激活,這個要看具體使用場景,再次說明,這裏這種綁定當即激活的方式不建議使用,一半都是綁定完成後等到要使用時才激活。爲了這個還被lasoy老師批評了,哈哈,再次膜拜lasoy老師,阿里大佬。函數
好了,如今咱們有了gl.program編程對象,就可以安心的向shader裏傳attribute和uniform參數了,具體傳參方法不是咱們這篇文章討論的重點,請參考個人上一篇博客《原生WebGL場景中繪製多個圓錐圓柱》,連接地址https://www.cnblogs.com/ccentry/p/9864847.html。post
接下來咱們進入正題,持續繪製多個模型進一個canvas場景。也許同窗們要說,這很簡單啊,每次要繪製一個模型進入場景,就重複上述過程,先構造着色器對象gl.createShader(v-shader1),gl.createShader(f-shader1),而後綁定到程序對象gl.createProgram(program1)上,激活一下gl.useProgram(program1),接下來該穿attribute/uniform就傳參,直接gl.drawElement()不就好了嘛,要繪製多少不一樣的模型就調用這個過程多少次,不就能夠了嘛,哪來那麼多廢話,是否是。對於這種論調,我只能說,邏輯上是徹底正確的,也可以正確無誤地將多個長相各異的模型持續繪製進同一個canvas場景,沒毛病。同窗們就要噴了,那你bb了半天,想說啥呢?好,我就來講一說這麼作的壞處是什麼。請看下面場景學習
場景中的紅色圓錐和紅色圓柱的繪製方式就是相似剛纔那種思想,不斷重複構造着色器對象v-shader1 = gl.createShader(),f-shader1 = gl.createShader(),綁定編程program1 = gl.createProgram(v-shader1,f-shader1),激活編程對象gl.useProgram(program1),而後傳attribute/uniform參數給着色器,空間位置姿態變換,gl.drawElement(),從而繪製出圓錐;再來就是重複這個過程繪製出圓柱,惟一稍有區別的是,我偷懶沒從新構造shader對象,從新綁定program對象,而是重複利用同一套shader和program,只不過每次繪製傳參attribute從新傳了一次,覆蓋前一次的attribute而已,原理其實如出一轍,你們不要學我偷懶。有的同窗看到結果之後更來勁了,你看看,這不是挺好嗎,持續繪製多個不一樣模型成功了呀,有啥問題呀?那麼我就說說問題在哪裏。首先會發生交互的全局座標系紊亂,請看下圖
咱們看到,整個模型錯位了,緣由就是空間變換矩陣並不能和每一個模型相對於世界座標系的相對局部座標系矩陣正確相乘,這就是零散繪製多模型的坑。解決這個問題的方法就是採用樹結構繪製子模型。這也是本文的核心論點,接下來咱們就來看看如何採用樹結構繪製,樹的每一個節點存儲的又是什麼對象。
因爲osg和threejs都有本身的樹結構,因此我也模仿兩者本身構造了個人樹,請看下面代碼
/** * 座標系 * */ let Section = require('./Section'); let Operation = require('./Operation'); let Geometry = require('../core/Geometry'); let MatrixTransform = require('../core/MatrixTransform'); let StateSet = require('../core/StateSet'); let StateAttribute = require('../core/StateAttribute'); let BufferArray = require('../core/BufferArray'); let DrawElements = require('../core/DrawElements'); let Primitives = require('../core/Primitives'); let Depth = require('../core/Depth'); let LineWidth = require('../core/LineWidth'); let Material = require('../core/Material'); let BlendFunc = require('../core/BlendFunc'); let Algorithm = require('../util/Algorithm'); let BoundingBox = require('../util/BoundingBox'); let Vec3 = require('../util/Vec3'); let Vec4 = require('../util/Vec4'); let Plane = require('../util/Plane'); let Quat = require('../util/Quat'); let Mat4 = require('../util/Mat4'); let Utils = require('../util/Utils'); let ShaderFactory = require('../render/ShaderFactory'); let PolyhedronGeometry = require('../model/polyhedron'); let Group = require('../core/Group'); let CoordinateSection = function(viewer){ Section.call(this, viewer); //座標系模型的空間位置和姿態矩陣 this.position = Mat4.new(); this._coordRoot = undefined; this._scale = Vec3.create(1, 1, 1); this._translate = Vec3.new(); this._rotate = Quat.new(); this._scaleMatrix = Mat4.new(); this._translateMatrix = Mat4.new(); this._rotateMatrix = Mat4.new(); }; //繼承Section類 CoordinateSection.prototype = Object.create(Section.prototype); CoordinateSection.prototype.constructor = CoordinateSection; CoordinateSection.prototype = { /** * 建立座標系模型 * root:scene根節點 * */ create : function(root){ //初始化座標系根節點 this._coordRoot = new MatrixTransform(); //幾何 let polyhedronGeo = new PolyhedronGeometry(); //構造單位尺寸的模型 polyhedronGeo.getCone(0.2, 0.5, 16); let geoms = polyhedronGeo.vertices; let array = new Float32Array(geoms); let vertexBuffer = new BufferArray(BufferArray.ARRAY_BUFFER, array, 3); //面索引繪製方式 let indices = []; indices = polyhedronGeo.faces; //幾何體類實例 let geom = new Geometry(); geom.setBufferArray('Vertex', vertexBuffer); let index = new Int8Array(indices); let indexBuffer = new BufferArray(BufferArray.ELEMENT_ARRAY_BUFFER, index, index.length); let prim = new DrawElements(Primitives.TRIANGLES, indexBuffer); geom.setPrimitive(prim); //將幾何對象加入座標系根節點 this._coordRoot.addChild(geom); //渲染組件 let stateSet = new StateSet(); //使用ColorDefaultProgram這組着色器 stateSet.addAttribute(ShaderFactory.createColorDefault.call(this)); stateSet.addAttribute(new Material([1, 0.5, 0, 1])); stateSet.addAttribute(new BlendFunc(BlendFunc.SRC_ALPHA, BlendFunc.ONE_MINUS_SRC_ALPHA)); stateSet.addAttribute(new Depth(Depth.LESS, 0.1, 0.9, false));//深度值在中間 this._coordRoot.setStateSet(stateSet); //將座標系根節點加入場景根節點 root.addChild(this._coordRoot); }, /** * 調整座標軸尺寸姿態 * boundingBox:scene場景的包圍盒 * vec3Translate:場景平移向量 * vec4Rotate:場景旋轉四元數 * vec3Scale:場景縮放向量 * mat4Scale:場景縮放矩陣 * mat4Translate:場景平移矩陣 * mat4Rotate:場景旋轉矩陣 * worldMatrix:當前場景的世界座標 * */ update: function (boundingBox, vec3Scale, vec3Translate, vec4Rotate, mat4Scale, mat4Translate, mat4Rotate, worldMatrix) { if(boundingBox instanceof BoundingBox){//先保證boundingBox是BoundingBox類的實例 let vecSRaw = Vec3.new(); Vec3.copy(vecSRaw, vec3Scale);//克隆縮放向量,防止污染場景縮放向量 let vecS = Vec3.new(); this.computeScale(vecS, vecSRaw);//取場景縮放最長邊的1/4做爲座標系模型縮放比例 let vecT = Vec3.new(); Vec3.copy(vecT, vec3Translate);//克隆平移向量,防止污染場景平移向量 let vecR = Vec4.new(); Vec4.copy(vecR, vec4Rotate);//克隆旋轉向量,防止污染場景旋轉向量 if (boundingBox.valid()) {//場景模型存在的話 let min = boundingBox.getMin(); let max = boundingBox.getMax(); boundingBox.getCenter(vec3Translate); Vec3.sub(this._scale, max, min); } let matW = Mat4.new(); Mat4.copy(matW, worldMatrix); //克隆一個世界座標系矩陣,防止修改場景包圍盒的矩陣 let matS = Mat4.new(); Mat4.copy(matS, mat4Scale); //克隆一個縮放矩陣,防止污染場景包圍盒的縮放矩陣 let matT = Mat4.new(); Mat4.copy(matT, mat4Translate); //克隆一個平移矩陣,防止污染場景包圍盒的平移矩陣 let matR = Mat4.new(); Mat4.copy(matR, mat4Rotate); //克隆一個旋轉矩陣,防止污染場景包圍盒的旋轉矩陣 Mat4.fromScaling(matS, vecS); Mat4.fromTranslation(matT, vecT); Mat4.fromQuat(matR, vecR); Mat4.mul(matW, matT, matR); Mat4.mul(matW, matW, matS); this._coordRoot._matrix = matW; } }, //計算座標系縮放比例 computeScale : function(newScale, boundingBoxScale){ //取場景模型包圍盒最長一邊的1/4 var scale = boundingBoxScale[0] > boundingBoxScale[1] ? boundingBoxScale[0] : boundingBoxScale[1]; scale = scale > boundingBoxScale[2] ? scale : boundingBoxScale[2]; scale *= 1/4; newScale[0] = scale; newScale[1] = scale; newScale[2] = scale; } }; module.exports = CoordinateSection;
在這個構造類中,我將座標系模型作成了一個根節點coordRoot,這個根節點下掛載了一個子模型(圓錐),該子模型下又掛載了三個子節點,1、geometry幾何特徵;2、transformMatrix千萬注意是相對於他的父節點的空間變換矩陣,不是相對於世界座標系的空間變換矩陣,千萬注意;3、stateSet着色器相關對象,就是實現shader,program,傳參attribute,uniform,空間變換,drawElement相關的配置和操做對象。這樣作的好處就顯而易見了,遍歷整棵模型樹,我既能將樹上每個節點都綁定不一樣的shader繪製出來,又能知道子節點相對於父節點的空間變換矩陣,就不會出現剛纔那種錯位的事了。 同窗們看到這裏應該明白樹形結構加載多個子模型的好處了,因爲此次的代碼並不完整,osg也須要nodejs的運行環境,因此事先說明,貼出的代碼只是爲了幫助說明觀點,本文代碼只是局部關鍵部位,並不能運行,若有問題,能夠交流。引用本文請註明出處https://www.cnblogs.com/ccentry/p/9903166.html