Cesium原理篇:6 Render模塊(3: Shader)【轉】

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的封裝。正則表達式

      opengles_20_pipeline2

圖1:ES2.0可編程渲染管線編程

 

       上圖是可編程渲染管線的一個大概流程,咱們關注的兩個橙色的圓角矩形部分,分別是頂點着色器和片源着色器。既然是可編程渲染管線,面向Shader的開發者提供了一種稱爲GLSL的語言,若是你懂C的話,二者語法是至關的,因此從語法層面學習成本不大。gulp

ShaderSource建立

       首先,Cesium提供了ShaderSource類來加載GLSL代碼,咱們來看一下它對應的拷貝構造函數:數組

複製代碼
ShaderSource.prototype.clone = function() {
    return new ShaderSource({
        sources : this.sources,
        defines : this.defines,
        pickColorQuantifier : this.pickColorQualifier,
        includeBuiltIns : this.includeBuiltIns
    });
};
複製代碼
  • sources 
    必須,代碼自己,這裏是一個數組,能夠是多個代碼片斷的疊加
  • defines 
    非必須,執行該代碼時聲明的預編譯宏
  • pickColorQualifier 
    非必須,當須要點擊選中地物時設置此參數,值爲'uniform',下面會介紹其大概
  • includeBuiltIns 
    非必須,默認爲true,認爲須要加載Cesium自帶的GLSL變量或function,下面會詳細介紹

       在使用上,一般只須要指定前兩個參數,就能夠建立一個頂點或片元着色器,好比在Globe中建立渲染地球的着色器代碼就是這麼的簡單:緩存

複製代碼
// 頂點着色器
this._surfaceShaderSet.baseVertexShaderSource = new ShaderSource({
    sources : [GroundAtmosphere, GlobeVS]
});

// 片元着色器
this._surfaceShaderSet.baseFragmentShaderSource = new ShaderSource({
    sources : [GlobeFS]
});
複製代碼

ShaderSource腳本加載

       固然用起來簡單,但其內部實現仍是有些複雜的,在介紹ShaderSource前須要先了解兩個知識點:CzmBuiltins&AutomaticUniforms。app

CzmBuiltins

       Cesium中提供了一些經常使用的GLSL文件,文件夾結構以下圖:框架

 

1

圖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;
};
複製代碼

AutomaticUniforms

       然而做爲參數而言,僅僅有這些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);
        }
    }
}
複製代碼

Shader建立

       上面介紹了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
};
複製代碼

以下圖,是根節點對應的值:

 

QQ截圖20161023174801

 

       其中,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了,構造函數以下,自己也是一個空殼,只有在渲染中第一次使用該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

       代碼如上,在渲染時會先綁定該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);
}
複製代碼
findUniforms

       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方法,而不須要由於類型的不一樣而分散注意力。

 

_setUniforms

       咱們在ShaderProgram初始化的時候,已經完成了對attribute變量的賦值過程,如今則是對uniform變量的賦值。這裏分爲兩種狀況,自定義和內建uniform兩種狀況,嚴格說還包括紋理的samplerUniform變量。

uniformMap

       對應自定義的變量,會構造一個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中對這種狀況進行了特殊處理。

相關文章
相關標籤/搜索