引子
又偷懶了,說好的周更的,又拖了一個月咯。前面兩篇寫了可視域分析和視頻投影,無一例外的都用到了ShadowMap也就是陰影貼圖,因此覺得又必要單獨寫一篇陰影貼圖的文章。當然了,還有另外一個原因,文章中視頻投影是利用Cesium自帶的Entity方式實現的,毫無技術性可言,在文章結尾我說了可以使用ShadowMap方式來做,原理類似於可視域分析,那么今天我就把實現方式給大家說一下。
預期效果
照例先看一下預期的效果,既然說了陰影貼圖,當然不能滿足於只貼視頻紋理了,這里我放了三張圖,代表着我用了三種紋理:圖片、視頻、顏色。小伙伴驚奇的發現,顏色貼圖不就是可視域分析么?嘿嘿,是的,因為原理都是一樣的嘛。
實現原理
上面說了實現原和可視域分析是一樣的,涉及到的知識點ShadowMap、Frustum、Camera之類的請參考Cesium深入淺出之可視域分析,這里不在贅述。只簡單講一點,陰影貼圖支持不同的紋理,那么我們要做的就是創建一個ShadowMap,然后把不同類型的Texture傳給他就可以了。
具體實現
實現流程與可視域分析也大致相似,類→創建Camera→創建ShadowMap→創建PostProcessStage→創建Frustum,只多了一步設置Texture,當然最核心的內容是在shader里。
因為代碼高度重合,這里就不貼全部代碼了,只貼核心代碼,如果有疑問的可以留言、私信、群里詢問,我看到了都會回答的。
構造函數
1 // 定義變量 2 /** 紋理類型:VIDEO、IMAGE、COLOR */ 3 #textureType; 4 /** 紋理 */ 5 #texture; 6 /** 觀測點位置 */ 7 #viewPosition; 8 /** 最遠觀測點位置(如果設置了觀測距離,這個屬性可以不設置) */ 9 #viewPositionEnd; 10 // ... 11 12 // 構造函數 13 constructor(viewer, options) { 14 super(viewer); 15 16 // 紋理類型 17 this.#textureType = options.textureType; 18 // 紋理地址(紋理為視頻或圖片的時候需要設置此項) 19 this.#textureUrl = options.textureUrl; 20 // 觀測點位置 21 this.#viewPosition = options.viewPosition; 22 // 最遠觀測點位置(如果設置了觀測距離,這個屬性可以不設置) 23 this.#viewPositionEnd = options.viewPositionEnd; 24 25 // ... 26 27 switch (this.#textureType) { 28 default: 29 case VideoShed.TEXTURE_TYPE.VIDEO: 30 this.activeVideo(); 31 break; 32 case VideoShed.TEXTURE_TYPE.IMAGE: 33 this.activePicture(); 34 break; 35 } 36 this.#refresh() 37 this.viewer.scene.primitives.add(this); 38 }
定義紋理類型,視頻和圖片投影需要引入紋理文件,初始化的時候設置文件路徑,顏色投影不需要任何操作。
視頻紋理
1 /** 2 * 投放視頻。 3 * 4 * @author Helsing 5 * @date 2020/09/19 6 * @param {String} textureUrl 視頻地址。 7 */ 8 activeVideo(textureUrl = undefined) { 9 if (!textureUrl) { 10 textureUrl = this.#textureUrl; 11 } else { 12 this.#textureUrl = textureUrl; 13 } 14 const video = this.#createVideoElement(textureUrl); 15 const that = this; 16 if (video /*&& !video.paused*/) { 17 this.#activeVideoListener || (this.#activeVideoListener = function () { 18 that.#texture && that.#texture.destroy(); 19 that.#texture = new Texture({ 20 context: that.viewer.scene.context, 21 source: video, 22 width: 1, 23 height: 1, 24 pixelFormat: PixelFormat.RGBA, 25 pixelDatatype: PixelDatatype.UNSIGNED_BYTE 26 }); 27 }); 28 that.viewer.clock.onTick.addEventListener(this.#activeVideoListener); 29 } 30 }
視頻紋理是通過html5的video標簽引入,需要動態創建標簽,不過需要注意的是視頻標簽的釋放是個問題,常規方式並不能徹底釋放,最好不要每次都創建新的標簽。
圖片紋理
1 /** 2 * 投放圖片。 3 * 4 * @author Helsing 5 * @date 2020/09/19 6 * @param {String} textureUrl 圖片地址。 7 */ 8 activePicture(textureUrl = undefined) { 9 this.deActiveVideo(); 10 if (!textureUrl) { 11 textureUrl = this.#textureUrl; 12 } else { 13 this.#textureUrl = textureUrl; 14 } 15 const that = this; 16 const img = new Image; 17 img.onload = function () { 18 that.#textureType = VideoShed.TEXTURE_TYPE.IMAGE; 19 that.#texture = new Texture({ 20 context: that.viewer.scene.context, 21 source: img 22 }); 23 }; 24 img.onerror = function () { 25 console.log('圖片加載失敗:' + textureUrl) 26 }; 27 img.src = textureUrl; 28 }
圖片紋理使用的是Image對象加載的,要注意的是在異步回調中設置紋理。
PostProcessStage
1 /** 2 * 創建后期程序。 3 * 4 * @author Helsing 5 * @date 2020/09/19 6 * @ignore 7 */ 8 #addPostProcessStage() { 9 const that = this; 10 const bias = that.#shadowMap._isPointLight ? that.#shadowMap._pointBias : that.#shadowMap._primitiveBias; 11 const postStage = new PostProcessStage({ 12 fragmentShader: VideoShedFS, 13 uniforms: { 14 helsing_textureType: function () { 15 return that.#textureType; 16 }, 17 helsing_texture: function () { 18 return that.#texture; 19 }, 20 helsing_alpha: function () { 21 return that.#alpha; 22 }, 23 helsing_visibleAreaColor: function () { 24 return that.#visibleAreaColor; 25 }, 26 helsing_invisibleAreaColor: function () { 27 return that.#invisibleAreaColor; 28 }, 29 shadowMap_texture: function () { 30 return that.#shadowMap._shadowMapTexture; 31 }, 32 shadowMap_matrix: function () { 33 return that.#shadowMap._shadowMapMatrix; 34 }, 35 shadowMap_lightPositionEC: function () { 36 return that.#shadowMap._lightPositionEC; 37 }, 38 shadowMap_texelSizeDepthBiasAndNormalShadingSmooth: function () { 39 const t = new Cartesian2; 40 t.x = 1 / that.#shadowMap._textureSize.x; 41 t.y = 1 / that.#shadowMap._textureSize.y; 42 return Cartesian4.fromElements(t.x, t.y, bias.depthBias, bias.normalShadingSmooth, that.#combinedUniforms1); 43 }, 44 shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness: function () { 45 return Cartesian4.fromElements(bias.normalOffsetScale, that.#shadowMap._distance, that.#shadowMap.maximumDistance, that.#shadowMap._darkness, that.#combinedUniforms2); 46 }, 47 } 48 }); 49 this.#postProcessStage = this.viewer.scene.postProcessStages.add(postStage); 50 }
后處理程序中的重點是向shader中傳入uniforms參數,如紋理類型,可視域顏色、非可視域顏色等。最后就是重頭戲着色器代碼。
1 export default ` 2 varying vec2 v_textureCoordinates; 3 uniform sampler2D colorTexture; 4 uniform sampler2D depthTexture; 5 uniform sampler2D shadowMap_texture; 6 uniform mat4 shadowMap_matrix; 7 uniform vec4 shadowMap_lightPositionEC; 8 uniform vec4 shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness; 9 uniform vec4 shadowMap_texelSizeDepthBiasAndNormalShadingSmooth; 10 uniform int helsing_textureType; 11 uniform sampler2D helsing_texture; 12 uniform float helsing_alpha; 13 uniform vec4 helsing_visibleAreaColor; 14 uniform vec4 helsing_invisibleAreaColor; 15 16 vec4 toEye(in vec2 uv, in float depth){ 17 vec2 xy = vec2((uv.x * 2.0 - 1.0),(uv.y * 2.0 - 1.0)); 18 vec4 posInCamera =czm_inverseProjection * vec4(xy, depth, 1.0); 19 posInCamera =posInCamera / posInCamera.w; 20 return posInCamera; 21 } 22 float getDepth(in vec4 depth){ 23 float z_window = czm_unpackDepth(depth); 24 z_window = czm_reverseLogDepth(z_window); 25 float n_range = czm_depthRange.near; 26 float f_range = czm_depthRange.far; 27 return (2.0 * z_window - n_range - f_range) / (f_range - n_range); 28 } 29 float _czm_sampleShadowMap(sampler2D shadowMap, vec2 uv){ 30 return texture2D(shadowMap, uv).r; 31 } 32 float _czm_shadowDepthCompare(sampler2D shadowMap, vec2 uv, float depth){ 33 return step(depth, _czm_sampleShadowMap(shadowMap, uv)); 34 } 35 float _czm_shadowVisibility(sampler2D shadowMap, czm_shadowParameters shadowParameters){ 36 float depthBias = shadowParameters.depthBias; 37 float depth = shadowParameters.depth; 38 float nDotL = shadowParameters.nDotL; 39 float normalShadingSmooth = shadowParameters.normalShadingSmooth; 40 float darkness = shadowParameters.darkness; 41 vec2 uv = shadowParameters.texCoords; 42 depth -= depthBias; 43 vec2 texelStepSize = shadowParameters.texelStepSize; 44 float radius = 1.0; 45 float dx0 = -texelStepSize.x * radius; 46 float dy0 = -texelStepSize.y * radius; 47 float dx1 = texelStepSize.x * radius; 48 float dy1 = texelStepSize.y * radius; 49 float visibility = (_czm_shadowDepthCompare(shadowMap, uv, depth) 50 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy0), depth) 51 + _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy0), depth) 52 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy0), depth) 53 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, 0.0), depth) 54 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, 0.0), depth) 55 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy1), depth) 56 + _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy1), depth) 57 + _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy1), depth) 58 ) * (1.0 / 9.0); 59 return visibility; 60 } 61 vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point){ 62 vec3 v01 = point -planeOrigin; 63 float d = dot(planeNormal, v01) ; 64 return (point - planeNormal * d); 65 } 66 67 void main(){ 68 const float PI = 3.141592653589793; 69 vec4 color = texture2D(colorTexture, v_textureCoordinates); 70 vec4 currentDepth = texture2D(depthTexture, v_textureCoordinates); 71 if(currentDepth.r >= 1.0){ 72 gl_FragColor = color; 73 return; 74 } 75 float depth = getDepth(currentDepth); 76 vec4 positionEC = toEye(v_textureCoordinates, depth); 77 vec3 normalEC = vec3(1.0); 78 czm_shadowParameters shadowParameters; 79 shadowParameters.texelStepSize = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.xy; 80 shadowParameters.depthBias = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.z; 81 shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.w; 82 shadowParameters.darkness = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.w; 83 shadowParameters.depthBias *= max(depth * 0.01, 1.0); 84 vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz); 85 float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); 86 vec4 shadowPosition = shadowMap_matrix * positionEC; 87 shadowPosition /= shadowPosition.w; 88 if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))){ 89 gl_FragColor = color; 90 return; 91 } 92 shadowParameters.texCoords = shadowPosition.xy; 93 shadowParameters.depth = shadowPosition.z; 94 shadowParameters.nDotL = nDotL; 95 float visibility = _czm_shadowVisibility(shadowMap_texture, shadowParameters); 96 97 if (helsing_textureType < 2){ // 視頻或圖片模式 98 vec4 videoColor = texture2D(helsing_texture, shadowPosition.xy); 99 if (visibility == 1.0){ 100 gl_FragColor = mix(color, vec4(videoColor.xyz, 1.0), helsing_alpha * videoColor.a); 101 } 102 else{ 103 if(abs(shadowPosition.z - 0.0) < 0.01){ 104 return; 105 } 106 gl_FragColor = color; 107 } 108 } 109 else{ // 可視域模式 110 if (visibility == 1.0){ 111 gl_FragColor = mix(color, helsing_visibleAreaColor, helsing_alpha); 112 } 113 else{ 114 if(abs(shadowPosition.z - 0.0) < 0.01){ 115 return; 116 } 117 gl_FragColor = mix(color, helsing_invisibleAreaColor, helsing_alpha); 118 } 119 } 120 }`;
可以看出着色器代碼並不復雜,而且其中大部分是Cesium中原生的,重點看我標注的部分,視頻和圖片模式時使用混合紋理,可視域模式時混合顏色。