天天看點

cesium比較差別 opengl_Cesium原理篇:Material

Shader 首先,在本文開始前,我們先普及一下材質的概念,這裡推薦材質,普及材質的内容都是截取自該網站,我覺得他寫的已經夠好了。在開始普及概念前,推薦一首我此刻想到的歌《光---陳粒》。 在真實世界裡,每個物體會對光産生不同的反應。鋼看起來比陶瓷花瓶更閃閃發光,一個木頭箱子不會像鋼箱子一樣對光産生很強的反射。每個物體對鏡面高光也有不同的反應。有些物體不會散射(Scatter)很多光卻會反射(Reflect)很多光,結果看起來就有一個較小的高光點(Highlight),有些物體散射了很多,它們就會産生一個半徑更大的高光。如果我們想要在OpenGL中模拟多種類型的物體,我們必須為每個物體分别定義材質(Material)屬性。

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

structMaterial

{

vec3 ambient;

vec3 diffuse;

vec3 specular;floatshininess;

};

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

struct czm_material

{

vec3 diffuse;floatspecular;floatshininess;

vec3 normal;

vec3 emission;floatalpha;

};

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

varying vec3 v_positionEC;

varying vec3 v_normalEC;voidmain()

{

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;returnvec4(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格式。材質可以很簡單,就是對象表面的一個貼圖,也可以是一個圖案,比如條形或棋盤形。

cesium比較差別 opengl_Cesium原理篇:Material

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

//方法一

primitive.appearance.material = newCesium.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 = newCesium.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 = newMaterial({

fabric : {

type : type

}

});returnmaterial;

};functionMaterial(options) {

initializeMaterial(options,this);if (!defined(Material._uniformList[this.type])) {

Material._uniformList[this.type] = Object.keys(this._uniforms);

}

}functioninitializeMaterial(options, result) {var cachedMaterial =Material._materialCache.getMaterial(result.type);

createMethodDefinition(result);

createUniforms(result);//translucent

}

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

functioncreateMethodDefinition(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 incomponents) {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;returnmaterial;

}

可以對照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;returnmaterial;

}

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

functioncreateUniform(material, uniformId) {//根據變量的類型,建立對應的return value方法

if (uniformType === 'sampler2D') {

material._uniforms[newUniformId]= function() {returnmaterial._textures[uniformId];

};

material._updateFunctions.push(createTexture2DUpdateFunction(uniformId));

}else if (uniformType === 'samplerCube') {

material._uniforms[newUniformId]= function() {returnmaterial._textures[uniformId];

};

material._updateFunctions.push(createCubeMapUpdateFunction(uniformId));

}else if (uniformType.indexOf('mat') !== -1) {var scratchMatrix = newmatrixMap[uniformType]();

material._uniforms[newUniformId]= function() {returnmatrixMap[uniformType].fromColumnMajorArray(material.uniforms[uniformId], scratchMatrix);

};

}else{

material._uniforms[newUniformId]= function() {returnmaterial.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(newCesium.Primitive({

geometryInstances :newCesium.GeometryInstance({

geometry :newCesium.RectangleGeometry({

rectangle : Cesium.Rectangle.fromDegrees(-120.0, 20.0, -60.0, 40.0),

vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT

})

}),

appearance :newCesium.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);

}returnrs;

};

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;

}returnrs;

};

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

functionEllipsoidSurfaceAppearance(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;functioncreateRenderStates(primitive, context, appearance, twoPasses) {var renderState =appearance.getRenderState();

}

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

接着,就是建立ShaderProgram:

functioncreateShaderProgram(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:

functioncreateCommands(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之間的關系我們後續再介紹這種建立方式下的不同之處。