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格式。材質可以很簡單,就是對象表面的一個貼圖,也可以是一個圖案,比如條形或棋盤形。
比如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之間的關系我們後續再介紹這種建立方式下的不同之處。