Cesium原理篇:Material【轉】


https://www.cnblogs.com/fuckgiser/p/6171245.html

Shader

首先,在本文開始前,我們先普及一下材質的概念,這里推薦材質,普及材質的內容都是截取自該網站,我覺得他寫的已經夠好了。在開始普及概念前,推薦一首我此刻想到的歌《光---陳粒》。

       在真實世界里,每個物體會對光產生不同的反應。鋼看起來比陶瓷花瓶更閃閃發光,一個木頭箱子不會像鋼箱子一樣對光產生很強的反射。每個物體對鏡面高光也有不同的反應。有些物體不會散射(Scatter)很多光卻會反射(Reflect)很多光,結果看起來就有一個較小的高光點(Highlight),有些物體散射了很多,它們就會產生一個半徑更大的高光。如果我們想要在OpenGL中模擬多種類型的物體,我們必須為每個物體分別定義材質(Material)屬性。

       我們指定一個物體和一個光的顏色來定義物體的圖像輸出,並使之結合環境(Ambient)和鏡面強度(Specular Intensity)元素。當描述物體的時候,我們可以使用3種光照元素:環境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)、鏡面光照(Specular Lighting)定義一個材質顏色。通過為每個元素指定一個顏色,我們已經對物體的顏色輸出有了精密的控制。現在把一個鏡面高光元素添加到這三個顏色里,這是我們需要的所有材質屬性:

復制代碼
struct Material
{
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
};
復制代碼

       以上是對材質的一個最簡單概括,我們下面進入Cesium的環節。先來看看Cesium在Shader中對Material的定義:

復制代碼
struct czm_material
{
    vec3 diffuse;
    float specular;
    float shininess;
    vec3 normal;
    vec3 emission;
    float alpha;
};
復制代碼

       和上面給出的結構體大致相同,區別是少了環境光ambient,但多了法向量normal,自發光emission和alpha,我們帶着這個疑問看一下Cesium處理材質的片段着色器:

復制代碼
varying vec3 v_positionEC;
varying vec3 v_normalEC;
void main()
{
    vec3 positionToEyeEC = -v_positionEC;
    vec3 normalEC = normalize(v_normalEC);
#ifdef FACE_FORWARD
    normalEC = faceforward(normalEC, vec3(0.0, 0.0, 1.0), -normalEC);
#endif
    czm_materialInput materialInput;
    materialInput.normalEC = normalEC;
    materialInput.positionToEyeEC = positionToEyeEC;
    czm_material material = czm_getDefaultMaterial(materialInput);
    gl_FragColor = czm_phong(normalize(positionToEyeEC), material);
}
復制代碼

       此時的坐標系是以相機為中心點,首先獲取當前點的位置和法向量,通過czm_getMaterial獲取默認的一個材質對象,gl_FragColor通過czm_phong方法得到對應的顏色。對於phong,在OpenGL SuperBible里面有詳細的說明,大概就是通過material的屬性,根據光的位置和光的顏色,最終計算出在該點當前環境和自身材質的影響下對應的顏色。我們來看看czm_phong的實現:

復制代碼
vec4 czm_phong(vec3 toEye, czm_material material)
{
    
    float diffuse = czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 0.0, 1.0), material);
    if (czm_sceneMode == czm_sceneMode3D) {
        diffuse += czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 1.0, 0.0), material);
    }

    float specular = czm_private_getSpecularOfMaterial(czm_sunDirectionEC, toEye, material) + czm_private_getSpecularOfMaterial(czm_moonDirectionEC, toEye, material);

    vec3 materialDiffuse = material.diffuse * 0.5;
    
    vec3 ambient = materialDiffuse;
    vec3 color = ambient + material.emission;
    color += materialDiffuse * diffuse;
    color += material.specular * specular;

    return vec4(color, material.alpha);
}
復制代碼

       如上是phong顏色計算的算法,我並沒有給出getLambertDiffuse和getSpecular的具體代碼,都是光的基本物理規律。這里要說的是getLambertDiffuse的參數,如果是球面物體時,會調用czm_private_phong,此時參數為czm_sunDirectionEC,也就是太陽的位置,而這里認為光源的位置是靠近相機的某一個點,另外,環境光ambient默認是反射光的一半,這個也說的過去,最后我們看到最終顏色的alpha位是material.alpha。

       上面是Shader中涉及到材質的一個最簡過程:材質最終影響的是片段着色器中的顏色gl_FragColor,而所有czm_開頭的都是Cesium內建的方法和對象,Cesium已經幫我們提供好了光學模型和計算方法,並不需要我們操心,而我們要做的,就是指定對應物體的材質屬性,通過修改material中的屬性值,來影響最終的效果。所以,接下來的問題就是如何指定物體的材質屬性。

       材質的風格有很多種,形狀也不盡相同,線面各異,為此,Cesium提供了Material對象,來方便我們設置材質。

Fabric

       我們先來看看Cesium都提供了哪些內建材質類型,以及如何創建對應的Material,我也是參考的Cesium在github wike上對Fabric的介紹,更詳細的內容可以自己去看。在Cesium中,Fabric是描述材質的一種json格式。材質可以很簡單,就是對象表面的一個貼圖,也可以是一個圖案,比如條形或棋盤形。

1

       比如ImageType類型,Cesium提供了如下兩種方式來設置:

復制代碼
// 方法一
primitive.appearance.material = new Cesium.Material({
    fabric : {
        type : 'Image',
        uniforms : {
            image : '../images/Cesium_Logo_Color.jpg'
        }
    }
});
// 方法二
primitive.appearance..material = Material.fromType('Image');
primitive.appearance..uniforms.image = 'image.png';
復制代碼

       Cesium默認提供了十八個類型:

  • ColorType
  • ImageType
  • DiffuseMapType
  • AlphaMapType
  • SpecularMapType
  • EmissionMapType
  • BumpMapType
  • NormalMapType
  • GridType
  • StripeType
  • CheckerboardType
  • DotType
  • WaterType
  • RimLightingType
  • FadeType
  • PolylineArrowType
  • PolylineGlowType
  • PolylineOutlineType

       當然,Cesium支持多個Type的疊加效果,如下是DiffuseMap和NormalMap的一個疊加,components中指定material中diffuse、specular、normal的映射關系和值:

復制代碼
primitive.appearance.material = new Cesium.Material({
    fabric : {
        materials : {
            applyDiffuseMaterial : {
                type : 'DiffuseMap',
                uniforms : {
                    image : '../images/bumpmap.png'
                }
            },
            normalMap : {
                type : 'NormalMap',
                uniforms : {
                    image : '../images/normalmap.png',
                    strength : 0.6
                }
            }
        },
        components : {
            diffuse : 'diffuseMaterial.diffuse',
            specular : 0.01,
            normal : 'normalMap.normal'
        }
    }
});
復制代碼

       當然,這些都滿足不了你的欲望?你也可以自定義一個自己的MaterialType,我們先了解Cesium.Material的內部實現后,再來看看自定義Material。

Material

       用戶通常只需要指定type,uniforms,components三個屬性,構建一個Fabric的JSON。這是因為Material在初始化時,會加載上述默認的十八個類型,比如對應的ColorType代碼:

復制代碼
Material.ColorType = 'Color';
Material._materialCache.addMaterial(Material.ColorType, {
    fabric : {
        type : Material.ColorType,
        uniforms : {
            color : new Color(1.0, 0.0, 0.0, 0.5)
        },
        components : {
            diffuse : 'color.rgb',
            alpha : 'color.a'
        }
    },
    translucent : function(material) {
        return material.uniforms.color.alpha < 1.0;
    }
});
// 創建material
polygon.material = Cesium.Material.fromType('Color');
polygon.material.uniforms.color = new Cesium.Color(1.0, 1.0, 0.0, 1.0);
復制代碼

       其他的類型也大概相同,在初始化的時候已經全部構建。因此,用戶在執行創建時,已經有了一個ColorMaterial,只是對里面的一些屬性修改為自己的期望值的過程。我們具體Material.fromType的具體內容:

復制代碼
Material.fromType = function(type, uniforms) {
    var material = new Material({
        fabric : {
            type : type
        }
    });

    return material;
};

function Material(options) {
    initializeMaterial(options, this);
    
    if (!defined(Material._uniformList[this.type])) {
        Material._uniformList[this.type] = Object.keys(this._uniforms);
    }
}

function initializeMaterial(options, result) {
    var cachedMaterial = Material._materialCache.getMaterial(result.type);
    
    createMethodDefinition(result);
    createUniforms(result);
    
    // translucent
}
復制代碼

        initializeMaterial則是其中的重點,里面有三個關鍵點:1createMethodDefinition,2createUniforms,3translucent,我們來看看都做了什么

復制代碼
function createMethodDefinition(material) {
    // 獲取components屬性
    // ColorType:{ diffuse : 'color.rgb', alpha : 'color.a'}
    var components = material._template.components;
    var source = material._template.source;
    if (defined(source)) {
        material.shaderSource += source + '\n';
    } else {
        material.shaderSource += 'czm_material czm_getMaterial(czm_materialInput materialInput)\n{\n';
        material.shaderSource += 'czm_material material = czm_getDefaultMaterial(materialInput);\n';
        if (defined(components)) {
            for ( var component in components) {
                if (components.hasOwnProperty(component)) {
                    // 根據components中的屬性,修改Material中對應屬性的獲取方式
                    material.shaderSource += 'material.' + component + ' = ' + components[component] + ';\n';
                }
            }
        }
        // 封裝得到片段着色器中獲取material的函數
        material.shaderSource += 'return material;\n}\n';
    }
}
復制代碼

       如上是Key1的作用,拼裝出片段着色器中獲取material的函數,如果Type是Color下,獲取的函數代碼如下:

復制代碼
czm_material czm_getMaterial(czm_materialInput materialInput)
{
    czm_material material = czm_getDefaultMaterial(materialInput);
    material.diffuse = color.rgb;
    material.alpha = color.a;
    return material;
}
復制代碼

       可以對照ColorType的FabricComponents屬性,對號入座。下面就是對Fabric的uniforms屬性的解析過程了:createUniforms。這里主要有兩個作用,第一,根據uniforms,在片源着色器中聲明對應的uniform變量,比如ColorType中uniform對應的color變量,則需要聲明該變量,當然cesium做了一個特殊的處理,給他們一個標號,保證唯一:更新后的代碼如下:

復制代碼
uniform vec4 color_0;
czm_material czm_getMaterial(czm_materialInput materialInput)
{
    czm_material material = czm_getDefaultMaterial(materialInput);
    material.diffuse = color_0.rgb;
    material.alpha = color_0.a;
    return material;
}
復制代碼

      第二個作用是為后面的uniformMap做准備,聲明了變量了,當然需要准備好該變量的賦值,建立好這個key-value的過程,保存到material._uniforms數組中:

復制代碼
function createUniform(material, uniformId) {
    // 根據變量的類型,建立對應的return value方法
    if (uniformType === 'sampler2D') {
        material._uniforms[newUniformId] = function() {
            return material._textures[uniformId];
        };
        material._updateFunctions.push(createTexture2DUpdateFunction(uniformId));
    } else if (uniformType === 'samplerCube') {
        material._uniforms[newUniformId] = function() {
            return material._textures[uniformId];
        };
        material._updateFunctions.push(createCubeMapUpdateFunction(uniformId));
    } else if (uniformType.indexOf('mat') !== -1) {
        var scratchMatrix = new matrixMap[uniformType]();
        material._uniforms[newUniformId] = function() {
            return matrixMap[uniformType].fromColumnMajorArray(material.uniforms[uniformId], scratchMatrix);
        };
    } else {
        material._uniforms[newUniformId] = function() {
            return material.uniforms[uniformId];
        };
    }
}
復制代碼

       createUniforms方法后則是對translucent的處理,這個會影響到Pimitive創建RenderState,以及渲染隊列的設置。將Fabric中的translucent方法保存在material._translucentFunctions中。

Primitive

       此時,我們已經創建好一個color類型的Material,將其賦給對應的Primitive,代碼如下:

primitive.appearance.material = Cesium.Material.fromType('Color');

       這里出現了一個新的的對象:Appearance。這里,Material只是負責片段着色器中,材質部分的代碼,而Appearance則負責該Primitvie整個Shader的代碼,包括頂點着色器和片段着色器兩個部分,同時,需要根據Appearance的狀態來設置對應的RenderState,可以說Appearance是在Material之上的又一層封裝。一共有MaterialAppearance、EllipsoidSurfaceAppearance等六類,大同小異,每個對象的屬性值不同,但邏輯上統一有Appearance來負責。我們看如下一個Primitive的創建:

復制代碼
var rectangle = scene.primitives.add(new Cesium.Primitive({
    geometryInstances : new Cesium.GeometryInstance({
        geometry : new Cesium.RectangleGeometry({
            rectangle : Cesium.Rectangle.fromDegrees(-120.0, 20.0, -60.0, 40.0),
            vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
        })
    }),
    appearance : new Cesium.EllipsoidSurfaceAppearance({
        aboveGround : false
    })
}));
復制代碼

       如上創建的是一個EllipsoidSurfaceAppearance,創建時如果沒有指定Material,則內部默認采用ColorTyoe的材質。當執行Primitive.update時,Appearance的就發揮了自己的價值:

Primitive.prototype.update = function(frameState) {
    createRenderStates(this, context, appearance, twoPasses);
    createShaderProgram(this, frameState, appearance);
    createCommands(this, appearance, material, translucent, twoPasses, this._colorCommands, this._pickCommands, frameState);
}

       首先Appearance基類提供了默認的defaultRenderState,也提供了getRenderState的方法,如下:

復制代碼
Appearance.getDefaultRenderState = function(translucent, closed, existing) {
    var rs = {
        depthTest : {
            enabled : true
        }
    };

    if (translucent) {
        rs.depthMask = false;
        rs.blending = BlendingState.ALPHA_BLEND;
    }

    if (closed) {
        rs.cull = {
            enabled : true,
            face : CullFace.BACK
        };
    }

    if (defined(existing)) {
        rs = combine(existing, rs, true);
    }

    return rs;
};

Appearance.prototype.getRenderState = function() {
    var translucent = this.isTranslucent();
    var rs = clone(this.renderState, false);
    if (translucent) {
        rs.depthMask = false;
        rs.blending = BlendingState.ALPHA_BLEND;
    } else {
        rs.depthMask = true;
    }
    return rs;
};
復制代碼

       然后,各個子類按照自己的需要,看是否使用基類的方法,還是自己有特殊用處,比如EllipsoidSurfaceAppearance類:

復制代碼
function EllipsoidSurfaceAppearance(options) {
    this._vertexShaderSource = defaultValue(options.vertexShaderSource, EllipsoidSurfaceAppearanceVS);
    this._fragmentShaderSource = defaultValue(options.fragmentShaderSource, EllipsoidSurfaceAppearanceFS);
    this._renderState = Appearance.getDefaultRenderState(translucent, !aboveGround, options.renderState);
}
EllipsoidSurfaceAppearance.prototype.getRenderState = Appearance.prototype.getRenderState;

function createRenderStates(primitive, context, appearance, twoPasses) {
    var renderState = appearance.getRenderState();
}
復制代碼

       這樣,EllipsoidSurfaceAppearance采用自己的頂點着色器和片段着色器的代碼,但RenderState和getRenderState方法都直接用的基類的,因此,當primitive調用createRenderStates方法時,盡管當前的appearance可能類型不一,但確保都有統一一套調用接口,最終創建滿足當前需要的RS,當然,這里主要是translucent的區別。

       接着,就是創建ShaderProgram:

復制代碼
function createShaderProgram(primitive, frameState, appearance) {
    var vs = primitive._batchTable.getVertexShaderCallback()(appearance.vertexShaderSource);
    var fs = appearance.getFragmentShaderSource();
}

Appearance.prototype.getFragmentShaderSource = function() {
    var parts = [];
    if (this.flat) {
        parts.push('#define FLAT');
    }
    if (this.faceForward) {
        parts.push('#define FACE_FORWARD');
    }
    if (defined(this.material)) {
        parts.push(this.material.shaderSource);
    }
    parts.push(this.fragmentShaderSource);

    return parts.join('\n');
};
復制代碼

       這里代碼比較清楚,就是通過Appearance獲取vs和fs,這里多了一個batchTable,這是因為該Primitive可能是批次的封裝,因此需要把batch部分的vs和appearance的vs合並,batchTable后面有時間的話,在單獨介紹。這里可以看到getFragmentShaderSource,增加了一下宏,同時,在Appearance中,不僅有自己的fragmentShaderSource,同時也把我們之前在Material中封裝的material.shaderSource也追加進去。真的是海納百川的歷程。

       這樣,就來到最后一步,構建Command:

復制代碼
function createCommands(primitive, appearance, material, translucent, twoPasses, colorCommands, pickCommands, frameState) {
    var uniforms = combine(appearanceUniformMap, materialUniformMap);
    uniforms = primitive._batchTable.getUniformMapCallback()(uniforms);
    var pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;
    // ……
    colorCommand.uniformMap = uniforms;
    colorCommand.pass = pass;
    //……    
}
復制代碼

      可見,Material的uniforms合並后綁定到了command的uniformMap中,另外translucent也用來判斷渲染隊列。至此,Material->Appearance->Renderer的整個過程就結束了。可見,Material主要涉及到初始化和Primitive.update部分。

       當然,之前我們介紹過,通過創建Entity的方式,也可以通過DataSourceDisplay這個過程最終創建Primitive並添加到PrimitiveCollection這種方式。這和直接構建Primitive基本相似,只是多繞了一圈。當然,這一圈也不是白繞的,因為會做批次的處理,合並多個風格相似的Geometry。當然,這就牽扯到Batch,Appearance以及MaterialProperty之間的關系我們后續再介紹這種創建方式下的不同之處。


免責聲明!

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



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