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


       在介紹Renderer的第一篇,我就提到WebGL1.0對應的是OpenGL ES2.0,也就是可編程渲染管線。之所以單獨強調這一點,算是為本篇埋下一個伏筆。通過前兩篇,我們介紹了VBO和Texture兩個比較核心的WebGL概念。假設生產一輛汽車,VBO就相當於這個車的骨架,紋理相當這個車漆,但有了骨架和車漆還不夠,還需要一台機器人來加工,最終才能成產出這輛汽車。而Shader模塊就是負責這個生產的過程,加工參數(VBO,Texture),執行渲染任務。

       這里假設大家對Shader有一個基本的了解,這一塊內容也很多,不可能簡單兩句輕描淡寫就豁然開朗,而且我也沒有進行過系統的學習,所以就不班門弄斧了。進入主題,來看看Cesium對Shader的封裝。

       opengles_20_pipeline2

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

 

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

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。

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中對這種情況進行了特殊處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM