Cesium深入浅出之阴影贴图【转】
引子
又偷懒了,说好的周更的,又拖了一个月咯。前面两篇写了可视域分析和视频投影,无一例外的都用到了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中原生的,重点看我标注的部分,视频和图片模式时使用混合纹理,可视域模式时混合颜色。