引子
又偷懶了,說好的周更的,又拖了一個月咯。前面兩篇寫了可視域分析和視頻投影,無一例外的都用到了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中原生的,重點看我標注的部分,視頻和圖片模式時使用混合紋理,可視域模式時混合顏色。

