https://www.cnblogs.com/fuckgiser/p/5975274.htmlhtml
在介紹Renderer的第一篇,我就提到WebGL1.0對應的是OpenGL ES2.0,也就是可編程渲染管線。之因此單獨強調這一點,算是爲本篇埋下一個伏筆。經過前兩篇,咱們介紹了VBO和Texture兩個比較核心的WebGL概念。假設生產一輛汽車,VBO就至關於這個車的骨架,紋理至關這個車漆,但有了骨架和車漆還不夠,還須要一臺機器人來加工,最終才能成產出這輛汽車。而Shader模塊就是負責這個生產的過程,加工參數(VBO,Texture),執行渲染任務。node
這裏假設你們對Shader有一個基本的瞭解,這一塊內容也不少,不可能簡單兩句輕描淡寫就豁然開朗,並且我也沒有進行過系統的學習,因此就不班門弄斧了。進入主題,來看看Cesium對Shader的封裝。正則表達式
圖1:ES2.0可編程渲染管線編程
上圖是可編程渲染管線的一個大概流程,咱們關注的兩個橙色的圓角矩形部分,分別是頂點着色器和片源着色器。既然是可編程渲染管線,面向Shader的開發者提供了一種稱爲GLSL的語言,若是你懂C的話,二者語法是至關的,因此從語法層面學習成本不大。gulp
首先,Cesium提供了ShaderSource類來加載GLSL代碼,咱們來看一下它對應的拷貝構造函數:數組
ShaderSource.prototype.clone = function() { return new ShaderSource({ sources : this.sources, defines : this.defines, pickColorQuantifier : this.pickColorQualifier, includeBuiltIns : this.includeBuiltIns }); };
在使用上,一般只須要指定前兩個參數,就能夠建立一個頂點或片元着色器,好比在Globe中建立渲染地球的着色器代碼就是這麼的簡單:緩存
// 頂點着色器 this._surfaceShaderSet.baseVertexShaderSource = new ShaderSource({ sources : [GroundAtmosphere, GlobeVS] }); // 片元着色器 this._surfaceShaderSet.baseFragmentShaderSource = new ShaderSource({ sources : [GlobeFS] });
固然用起來簡單,但其內部實現仍是有些複雜的,在介紹ShaderSource前須要先了解兩個知識點:CzmBuiltins&AutomaticUniforms。app
Cesium中提供了一些經常使用的GLSL文件,文件夾結構以下圖:框架
圖2:BuiltIn文件夾清單函數
如圖所示主要分爲三類(常量,方法,結構體),這些都是Cesium框架內部比較經常使用的基本結構和方法,屬於內建類型,它們的特色是前綴均爲czm_而且經過CzmBuiltins.js(打包時gulp會自動生成該文件)引用全部內建的GLSL代碼:
// 1 常量,例如:1 / Pi onst float czm_oneOverPi = 0.3183098861837907; // 方法,例如:八進制解碼,地形數據中用於數據壓縮 vec3 czm_octDecode(vec2 encoded) { encoded = encoded / 255.0 * 2.0 - 1.0; vec3 v = vec3(encoded.x, encoded.y, 1.0 - abs(encoded.x) - abs(encoded.y)); if (v.z < 0.0) { v.xy = (1.0 - abs(v.yx)) * czm_signNotZero(v.xy); } return normalize(v); } // 結構體,例如:材質 struct czm_material { vec3 diffuse; float specular; float shininess; vec3 normal; vec3 emission; float alpha; };
然而做爲參數而言,僅僅有這些Const常量仍是不夠的,好比在一個三維場景中,隨着位置的變化,相機的狀態也是須要更新的,好比ModelViewMatrix,ProjectMatrix以及ViewPort等變量一般也須要參與到GLSL的計算中,Cesium提供了AutomaticUniform類,用來封裝這些內建的變量,構造函數以下:
function AutomaticUniform(options) { this._size = options.size; this._datatype = options.datatype; this.getValue = options.getValue; }
全部的內部變量均可以基於該構造函數建立,並添加到AutomaticUniforms數組中,而且在命名上也遵照czm_*的格式,經過命名就能夠知道該變量是否是內建的,若是是,則從CzmBuiltins和AutomaticUniforms對應的列表(建立列表並維護的過程則是在ShaderSource中完成的,下面會講)中找其對應的值就能夠,這樣,Cesium內部自動調用這些變量而不須要用戶來處理,若是不是,則須要用戶本身定義一個uniformMap的數組來本身維護。以下是AutomaticUniforms的代碼片斷,能夠看到AutomaticUniforms中建立了czm_viewport變量,類型是vec4,並提供了getValue的方式,負責傳值。
var AutomaticUniforms = { czm_viewport : new AutomaticUniform({ size : 1, datatype : WebGLConstants.FLOAT_VEC4, getValue : function(uniformState) { return uniformState.viewportCartesian4; } }) } return AutomaticUniforms;
但這還有一個問題,只提供了getValue的方式,能夠把值傳到GLSL中,但這個值是怎麼獲取的,也就是setValue是如何實現,並且不須要用戶來關心。若是你看的足夠自信,會發現getValue中有一個uniformState參數,正是UniformState這個類的功勞了,Scene在初始化時會建立該屬性,而UniformState提供了update方法,在每一幀Render中都會更新這些變量值,不須要用戶本身來維護。
綜上所述,也就是Cesium內部有一套內建的變量,常量,方法和結構體,這些內容之間有一套完整的機制保證他們的正常運做,而ShaderSource的第一個做用就是在初始化的時候聲明_czmBuiltinsAndUniforms屬性,並加載CzmBuiltins和AutomaticUniforms中的內建屬性,創建一個全局的黃頁,爲整個程序服務。另外要強調的是,這個過程是在加載ShaderSource.js腳本時執行的,只會運行一次,不須要每次new ShaderSource的時候執行。
ShaderSource._czmBuiltinsAndUniforms = {}; // 合併automatic uniforms和Cesium內建實例 // CzmBuiltins是打包時自動建立的,裏面包括全部內建實例的類型和命名 for ( var builtinName in CzmBuiltins) { if (CzmBuiltins.hasOwnProperty(builtinName)) { ShaderSource._czmBuiltinsAndUniforms[builtinName] = CzmBuiltins[builtinName]; } } // AutomaticUniforms數組是在AutomaticUniforms.js中建立並返回 for ( var uniformName in AutomaticUniforms) { if (AutomaticUniforms.hasOwnProperty(uniformName)) { var uniform = AutomaticUniforms[uniformName]; if (typeof uniform.getDeclaration === 'function') { ShaderSource._czmBuiltinsAndUniforms[uniformName] = uniform.getDeclaration(uniformName); } } }
上面介紹了ShaderSource的建立,當用戶建立完VertexShaderSource和FragmentShaderSource後,下面就要建立ShaderProgram,將這兩個ShaderSource關聯起來。以下是SkyBox中建立ShaderProgram的示例代碼:
command.shaderProgram = ShaderProgram.fromCache({ context : context, vertexShaderSource : SkyBoxVS, fragmentShaderSource : SkyBoxFS, attributeLocations : attributeLocations });
vertexShaderSource和fragmentShaderSource都屬於以前咱們提到的ShaderSource概念,attributeLocations則對應以前的VBO中VertexBuffer。GLSL中變量分爲兩種,一類是attribute,好比位置,法線,紋理座標這些,每個頂點對應的值都不一樣,一類是uniform,跟頂點無關,值都相同的。這裏須要傳入attribute變量,而uniform在渲染時纔會指定。咱們來看一下fromCache的內部實現,詳細的介紹一下:
ShaderProgram.fromCache = function(options) { // Cesium提供了ShaderCache緩存機制,能夠重用ShaderProgram return options.context.shaderCache.getShaderProgram(options); }; ShaderCache.prototype.getShaderProgram = function(options) { // 合併該ShaderProgram所用到的頂點和片元着色器的代碼 var vertexShaderText = vertexShaderSource.createCombinedVertexShader(); var fragmentShaderText = fragmentShaderSource.createCombinedFragmentShader(); // 建立Cache緩存中Key-Value中的Key值 var keyword = vertexShaderText + fragmentShaderText + JSON.stringify(attributeLocations); var cachedShader; // 若是已存在,則直接用 if (this._shaders[keyword]) { cachedShader = this._shaders[keyword]; // No longer want to release this if it was previously released. delete this._shadersToRelease[keyword]; } else { // 若是不存在,則須要建立新的ShaderProgram var context = this._context; var shaderProgram = new ShaderProgram({ gl : context._gl, logShaderCompilation : context.logShaderCompilation, debugShaders : context.debugShaders, vertexShaderSource : vertexShaderSource, vertexShaderText : vertexShaderText, fragmentShaderSource : fragmentShaderSource, fragmentShaderText : fragmentShaderText, attributeLocations : attributeLocations }); // Key-Value中的Value值 cachedShader = { cache : this, shaderProgram : shaderProgram, keyword : keyword, count : 0 }; 添加到Cache中,並更新該Cache容器內總的shader數目 shaderProgram._cachedShader = cachedShader; this._shaders[keyword] = cachedShader; ++this._numberOfShaders; } // 該ShaderProgram的引用計數值 ++cachedShader.count; return cachedShader.shaderProgram; };
不難發現,fromCache最終是經過shaderCache.getShaderProgram方法實現ShaderProgram的建立,從這能夠看出Cesium提供了ShaderCache緩存機制,能夠重用ShaderProgram,經過雙面的代碼註釋能夠很好的理解這個過程。另外,1經過createCombinedVertexShader/createCombinedFragmentShader方法,生成最終的GLSL代碼(下面會詳細介紹),並2建立ShaderProgram。下面討論一下1和2的具體實現。
前面咱們提到Cesium提供了豐富的內建函數和變量,這樣提升了代碼的重用性,正由於如此,很能夠出現一個GLSL代碼是由多個代碼片斷組合而成的,所以ShaderSource.sources是一個數組類型,能夠加載多個GLSL文件。這樣,天然要提供一個多文件合併成一個GLSL代碼的方法。
但合併代碼並不僅是單純文本的疊加,算是一個簡易的語法解析器,特別是一些內建變量的聲明,咱們來看一下combine代碼的大體邏輯:
function combineShader(shaderSource, isFragmentShader) { var i; var length; // sources中的文本合併 var combinedSources = ''; var sources = shaderSource.sources; if (defined(sources)) { for (i = 0, length = sources.length; i < length; ++i) { // #line needs to be on its own line. combinedSources += '\n#line 0\n' + sources[i]; } } // 去掉代碼中的註釋部分 combinedSources = removeComments(combinedSources); // 最終的GLSL代碼 var result = ''; // 支持的版本號 if (defined(version)) { result = '#version ' + version; } // 添加預編譯宏 var defines = shaderSource.defines; if (defined(defines)) { for (i = 0, length = defines.length; i < length; ++i) { var define = defines[i]; if (define.length !== 0) { result += '#define ' + define + '\n'; } } } // 追加內建變量 if (shaderSource.includeBuiltIns) { result += getBuiltinsAndAutomaticUniforms(combinedSources); } result += '\n#line 0\n'; // 追加combinedSources中的代碼 result += combinedSources; return result; }
註釋部分是基本的邏輯,1合併sources中的文件,2刪除註釋,3提取版本信息,4拼出最終的代碼。4.1版本聲明,4.2預編譯宏,4.3內建變量的聲明,4.4加載步驟1中的代碼。這裏的邏輯都還比較容易理解,但4.3,內建變量的聲明仍是比較複雜的,咱們專門介紹一下。
function getBuiltinsAndAutomaticUniforms(shaderSource) { var dependencyNodes = []; // 獲取Main根節點 var root = getDependencyNode('main', shaderSource, dependencyNodes); // 生成該root依賴的全部節點,保存在dependencyNodes generateDependencies(root, dependencyNodes); // 根據依賴關係排序 sortDependencies(dependencyNodes); // 建立須要的內建變量聲明 var builtinsSource = ''; for (var i = dependencyNodes.length - 1; i >= 0; --i) { builtinsSource = builtinsSource + dependencyNodes[i].glslSource + '\n'; } return builtinsSource.replace(root.glslSource, ''); }
該部分的重點在於對dependencyNode的維護,咱們先看看該節點的結構:
dependencyNode = { name : name, glslSource : glslSource, dependsOn : [], requiredBy : [], evaluated : false };
以下圖,是根節點對應的值:
其中,name就是名稱,根節點就是main函數入口;glslSource則是其內部的代碼;dependsOn是他依賴的節點;requiredBy是依賴他的節點;evaluated用來表示該節點是否已經解析過。有了根節點root,下面就是順藤摸瓜,最終構建出全部節點的隊列,這就是generateDependencies函數作的事情,僞代碼以下:
function generateDependencies(currentNode, dependencyNodes) { // 更新標識,當前節點已經結果過 currentNode.evaluated = true; // 正則表達式,搜索當前代碼中符合czm_*的全部內建變量或函數 var czmMatches = currentNode.glslSource.match(/\bczm_[a-zA-Z0-9_]*/g); if (defined(czmMatches) && czmMatches !== null) { // remove duplicates czmMatches = czmMatches.filter(function(elem, pos) { return czmMatches.indexOf(elem) === pos; }); // 遍歷czmMatches找到的全部符合規範的變量,創建依賴關係,是一個雙向鏈表 czmMatches.forEach(function(element) { if (element !== currentNode.name && ShaderSource._czmBuiltinsAndUniforms.hasOwnProperty(element)) { var referencedNode = getDependencyNode(element, ShaderSource._czmBuiltinsAndUniforms[element], dependencyNodes); // currentNodetNode依賴referencedNode currentNode.dependsOn.push(referencedNode); // referencedNode被currentNodetNode依賴 referencedNode.requiredBy.push(currentNode); // recursive call to find any dependencies of the new node generateDependencies(referencedNode, dependencyNodes); } }); } }
有了這個節點隊列還並不能知足要求,由於隊列中的元素是按照在glsl代碼中出現的前後順序來解析的,而元素之間也存在一個依賴關係,因此咱們須要一個過程,把這個無序隊列轉化爲一個有依賴關係的雙向鏈表,這就是sortDependencies函數的工做。這實際上是一個樹的廣度優先的遍歷,左右上的順序,遍歷的過程當中會解除requiredBy的關聯,有興趣的能夠看一下源碼。最後會判斷是否有循環依賴的錯誤狀況。也算是一個依賴關係的語法解析器。
至此,ShaderSource基本完成了本身的核心使命,固然,若是是拾取狀態,屬於特殊狀況,則會更新片源着色器的代碼,對於選中的地物賦予選中風格(顏色),對應的函數爲:ShaderSource.createPickFragmentShaderSource。
有了最終版本的着色器代碼後,終於能夠建立ShaderProgram了,構造函數以下,自己也是一個空殼,只有在渲染中第一次使用該ShaderProgram時進行WebGL層面的調用,避免沒必要要的資源消耗:
function ShaderProgram(options) { this._vertexShaderSource = options.vertexShaderSource; this._vertexShaderText = options.vertexShaderText; this._fragmentShaderSource = options.fragmentShaderSource; this._fragmentShaderText = modifiedFS.fragmentShaderText; this.id = nextShaderProgramId++; }
主要介紹渲染狀態中ShaderProgram的相關操做。
代碼如上,在渲染時會先綁定該ShaderProgram,若是是第一次則會初始化。註釋是裏面的關鍵邏輯,應該比較容易理解,這裏值得強調的是對uniform的區分,方便後面渲染中參數的傳值。
ShaderProgram.prototype._bind = function() { // 初始化 initialize(this); // 綁定 this._gl.useProgram(this._program); }; function initialize(shader) { // 若是已經建立,則不須要初始化 if (defined(shader._program)) { return; } var gl = shader._gl; // 建立該Program,若是編譯有錯,則拋出異常 var program = createAndLinkProgram(gl, shader, shader._debugShaders); // 獲取attribute變量的數目 var numberOfVertexAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); // 獲取uniform變量的列表 var uniforms = findUniforms(gl, program); // 根據czm_*規則區分uniform,分爲自定義uniform和內建uniform var partitionedUniforms = partitionUniforms(shader, uniforms.uniformsByName); // 保存屬性 shader._program = program; shader._numberOfVertexAttributes = numberOfVertexAttributes; shader._vertexAttributes = findVertexAttributes(gl, program, numberOfVertexAttributes); shader._uniformsByName = uniforms.uniformsByName; shader._uniforms = uniforms.uniforms; shader._automaticUniforms = partitionedUniforms.automaticUniforms; shader._manualUniforms = partitionedUniforms.manualUniforms; shader.maximumTextureUnitIndex = setSamplerUniforms(gl, program, uniforms.samplerUniforms); }
createUniform封裝了全部Uniform類型的建立方法,並提供set函數,實現變量值和WebGL之間的傳遞。構造函數以下:
function createUniform(gl, activeUniform, uniformName, location) { switch (activeUniform.type) { case gl.FLOAT: return new UniformFloat(gl, activeUniform, uniformName, location); case gl.FLOAT_VEC2: return new UniformFloatVec2(gl, activeUniform, uniformName, location); case gl.FLOAT_VEC3: return new UniformFloatVec3(gl, activeUniform, uniformName, location); case gl.FLOAT_VEC4: return new UniformFloatVec4(gl, activeUniform, uniformName, location); case gl.SAMPLER_2D: case gl.SAMPLER_CUBE: return new UniformSampler(gl, activeUniform, uniformName, location); case gl.INT: case gl.BOOL: return new UniformInt(gl, activeUniform, uniformName, location); case gl.INT_VEC2: case gl.BOOL_VEC2: return new UniformIntVec2(gl, activeUniform, uniformName, location); case gl.INT_VEC3: case gl.BOOL_VEC3: return new UniformIntVec3(gl, activeUniform, uniformName, location); case gl.INT_VEC4: case gl.BOOL_VEC4: return new UniformIntVec4(gl, activeUniform, uniformName, location); case gl.FLOAT_MAT2: return new UniformMat2(gl, activeUniform, uniformName, location); case gl.FLOAT_MAT3: return new UniformMat3(gl, activeUniform, uniformName, location); case gl.FLOAT_MAT4: return new UniformMat4(gl, activeUniform, uniformName, location); default: throw new RuntimeError('Unrecognized uniform type: ' + activeUniform.type + ' for uniform "' + uniformName + '".'); } }
這樣,咱們找到全部的uniforms,並根據其對應的type來封裝,set方法至關於虛函數,不一樣的類型有不一樣的實現方法,這樣的好處是在傳值時直接調用set方法,而不須要由於類型的不一樣而分散注意力。
咱們在ShaderProgram初始化的時候,已經完成了對attribute變量的賦值過程,如今則是對uniform變量的賦值。這裏分爲兩種狀況,自定義和內建uniform兩種狀況,嚴格說還包括紋理的samplerUniform變量。
對應自定義的變量,會構造一個uniformMap賦給DrawCommand(後續會介紹,負責整個渲染的調度,將VBO,Texture,Framebuffer和Shader串聯起來),以下是一個最簡單的UniformMap示例:
var uniformMap = { u_initialColor : function() { return this.properties.initialColor; } }
其中u_initialColor就是該uniform變量的name,return則是其返回值。接下來咱們來看看setUniforms代碼:
ShaderProgram.prototype._setUniforms = function(uniformMap, uniformState, validate) { var len; var i; if (defined(uniformMap)) { var manualUniforms = this._manualUniforms; len = manualUniforms.length; for (i = 0; i < len; ++i) { var mu = manualUniforms[i]; mu.value = uniformMap[mu.name](); } } var automaticUniforms = this._automaticUniforms; len = automaticUniforms.length; for (i = 0; i < len; ++i) { var au = automaticUniforms[i]; au.uniform.value = au.automaticUniform.getValue(uniformState); } // It appears that assigning the uniform values above and then setting them here // (which makes the GL calls) is faster than removing this loop and making // the GL calls above. I suspect this is because each GL call pollutes the // L2 cache making our JavaScript and the browser/driver ping-pong cache lines. var uniforms = this._uniforms; len = uniforms.length; for (i = 0; i < len; ++i) { uniforms[i].set(); } };
首先,不管是manualUniforms仍是automaticUniforms,都是通過createUniform封裝後的uniform,這裏更新它們的value,經過uniformMap或getValue方法,這兩個在上面的內容中已經介紹過,而後uniforms[i].set(),實現最終向WebGL的傳值。這裏我保留了Cesium的註釋,裏面是一個頗有意思的性能調優,不妨本身看看。
終於寫完了,有一種如釋重負的感受。ShaderProgram自己並不複雜,但自己是一個面向過程的方式,Cesium爲了達到面向狀態的目的作了大量的封裝,在使用中更容易理解和維護。本文主要介紹這種封裝的思路和技巧,對我而言,這個過程當中仍是頗有收穫,也加深了我對Shader的理解。我一直擔憂不少人可能看完後似懂非懂,確實知識點不少,並且之間的聯繫也很緊密,關鍵是須要對WebGL在這一塊的內容須要有一個紮實的認識,才能較好的解讀這層封裝的意義。我儘可能說的詳細一些,但精力和能力有限,我自認爲對這一塊瞭解已經很清晰了,但也不敢打包票。因此,若是真的想要了解,仍是須要親自調試代碼,親自查探一下本文中提到的相關代碼部分。
另外,我的認爲shadersource在combine函數中仍是很消耗計算的,若是執行ShaderProgram.fromCache都會執行此函數兩遍(頂點和片元),因此這也是一個性能隱患處,好比GlobeSurfaceTile,若是每個Tile都從ShaderCache中獲取對應的ShaderProgram,儘管ShaderProgram只會建立一次,但每一次在Cache中經過Key查找Value的過程當中,構建Key的代價也是很大的,這也是爲何Cesium有提供了GlobeSurfaceShaderSet來的緣由所在(之一)。
最後要提醒一下,本文主要提供的是一個大概的流程,對於一些特殊狀況並未涉及,好比GlobeSurfaceTile中,有可能出現多影像圖層疊加的狀況,也就是多重紋理,但N不固定的狀況,GlobeSurfaceShaderSet.prototype.getShaderProgram中對這種狀況進行了特殊處理。