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