Use cesium’s post-processing to achieve a spotLight effect similar to three.js

1. Background

When we use cesium to do some lighting effects, such as sweeping effects, we will find that cesium does not have customized lights like three.js or babylon.js. cesium only has direct light, which is sunlight. If we want to implement something like three What to do with the lighting effects of .js?

Generally there are three effects:

1. Direct shading by the fragment shader. This is the same no matter which engine it is in. There are two types of subdivisions. One is to calculate lighting through UV coordinates; the other is to add three-dimensional coordinates. For example, to do For a sweeping effect, the center of the sweeping needs to be defined by yourself, that is, the Cartesian coordinates are transferred from the CPU to the GPU. I won’t go into details here, but dig a hole first; but this has a disadvantage, because this is the surface coloring of the current object. , there is no way to affect other objects; just imagine, when the light is turned down, why are you the only one illuminated? What to do next to you? This light is too eccentric; there is nothing you can do, you can only use the same light on the objects next to you. Colored

2. Through post-processing, this is actually a bit similar to the UV coloring mentioned above, but there are also differences. It adds the color of the light to the surface color of the original object. Of course, you can also directly replace the object surface with pure color. The color, but this is not a light. In cesium, because you can see objects without lights, it will make the object found by the light brighter. How to count it as being found? This is what we have to calculate. It will be explained in the code

Three. Through templates, I don’t know much about this. I don’t understand it very well and have never used it, but it is said that it can be used in three.js

This time it is implemented through post-processing, but the coordinates are also passed in from the outside, which is similar to the first point. As for the coordinate conversion part inside, that is, the conversion done before passing the coordinates into the GPU, I use it as a reference. Forgot about the big guy. Although I want to respect the originality, I no longer know which one is original. Hahaha. It feels like many of them are the same on the Internet, so I won’t change the coordinate conversion part. Respect the originality and color the important things. The device part

import * as Cesium from 'cesium'
// circle spread
class CircleDiffusion {
    viewer: any
    lastStageList: Array<any>
    constructor(viewer: any) {
      this.viewer = viewer
      this.lastStageList = []
    }
    add(
      position: Array<number>,
      radio=300
    ) {
      this.lastStageList.push(
        this.showCircleScan(position, radio)
      )
    }
    clear() {
      this.lastStageList.forEach((ele: any) => {
        this.clearScanEffects(ele)
      })
      this.lastStageList = []
    }
    /**
     * Circle diffusion
     * @param {*} position scan center such as [117.270739,31.84309,32]
     * @param {*} scanColor scan color such as "rgba(0,255,0,1)"
     * @param {*} maxRadius scanning radius, unit meter such as 1000 meters
     * @param {*} duration duration, in milliseconds such as 4000
     */
    showCircleScan(
      position: Array<number>,
      maxRadius: number,
    ) {
      const cartographicCenter = new Cesium.Cartographic(
        Cesium.Math.toRadians(position[0]),
        Cesium.Math.toRadians(position[1]),
        position[2]
      )
      const lastStage = this._addCircleScanPostStage(
        cartographicCenter,
        maxRadius,
      )
      return lastStage
    }
    /**
     * Add diffusion effect scan lines
     * @param {*} cartographicCenter scanning center
     * @param {*} maxRadius scanning radius
     */
    _addCircleScanPostStage(
      cartographicCenter: any,
      maxRadius = 300,
    ) {
      const _Cartesian3Center =
        Cesium.Cartographic.toCartesian(cartographicCenter)
      const _Cartesian4Center = new Cesium.Cartesian4(
        _Cartesian3Center.x,
        _Cartesian3Center.y,
        _Cartesian3Center.z,
        1
      )
      const _CartograhpicCenter1 = new Cesium.Cartographic(
        cartographicCenter.longitude,
        cartographicCenter.latitude,
        cartographicCenter.height + 500
      )
      const _Cartesian3Center1 =
        Cesium.Cartographic.toCartesian(_CartograhpicCenter1)
      const _Cartesian4Center1 = new Cesium.Cartesian4(
        _Cartesian3Center1.x,
        _Cartesian3Center1.y,
        _Cartesian3Center1.z,
        1
      )
      const _scratchCartesian4Center = new Cesium.Cartesian4()
      const _scratchCartesian4Center1 = new Cesium.Cartesian4()
      const _scratchCartesian3Normal = new Cesium.Cartesian3()
      const _this = this
      const ScanPostStage = new Cesium.PostProcessStage({
        fragmentShader: _this._getScanSegmentShader(),
        uniforms: {
          u_scanCenterEC: function() {
            const temp = Cesium.Matrix4.multiplyByVector(
              _this.viewer.camera._viewMatrix,
              _Cartesian4Center,
              _scratchCartesian4Center
            )
            return temp
          },
          u_scanPlaneNormalEC: function() {
            const temp = Cesium.Matrix4.multiplyByVector(
              _this.viewer.camera._viewMatrix,
              _Cartesian4Center,
              _scratchCartesian4Center
            )
            const temp1 = Cesium.Matrix4.multiplyByVector(
              _this.viewer.camera._viewMatrix,
              _Cartesian4Center1,
              _scratchCartesian4Center1
            )
            _scratchCartesian3Normal.x = temp1.x - temp.x
            _scratchCartesian3Normal.y = temp1.y - temp.y
            _scratchCartesian3Normal.z = temp1.z - temp.z
            Cesium.Cartesian3.normalize(
              _scratchCartesian3Normal,
              _scratchCartesian3Normal
            )
            return _scratchCartesian3Normal
          },
          u_radius: function() {
            return maxRadius
          },
          u_scanColor: new Cesium.Color(0.2,0.25,0.3,0.1),
        },
      })
      this.viewer.scene.postProcessStages.add(ScanPostStage)
      return ScanPostStage
    }
    /**
     * Diffusion effect Shader
     */
    _getScanSegmentShader() {
      const scanSegmentShader =
        `uniform sampler2D colorTexture;
          uniform sampler2D depthTexture;
          in vec2 v_textureCoordinates;
          uniform vec4 u_scanCenterEC;
          uniform vec3 u_scanPlaneNormalEC;
          uniform float u_radius;
          uniform vec4 u_scanColor;
          vec4 toEye(in vec2 uv, in float depth){
            vec2 xy = vec2((uv.x * 2.0 - 1.0),(uv.y * 2.0 - 1.0));
            vec4 posInCamera = czm_inverseProjection * vec4(xy, depth, 1.0);
            posInCamera =posInCamera / posInCamera.w;
            return posInCamera;
          }
          vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point){
              vec3 v01 = point - planeOrigin;
              float d = dot(planeNormal, v01);
              return (point - planeNormal * d);
          }
          float getDepth(in vec4 depth){
              float z_window = czm_unpackDepth(depth);
              z_window = czm_reverseLogDepth(z_window);
              float n_range = czm_depthRange.near;
              float f_range = czm_depthRange.far;
              return (2.0 * z_window - n_range - f_range) / (f_range - n_range);
          }
          
          void main(){
              out_FragColor = texture(colorTexture, v_textureCoordinates);
              float depth = getDepth(texture(depthTexture, v_textureCoordinates));
              vec4 viewPos = toEye(v_textureCoordinates, depth);
              vec3 scanCenterEC = u_scanCenterEC.xyz + vec3(cos(czm_frameNumber * 0.05) * 300.0, sin(czm_frameNumber * 0.05) * 300.0, sin(czm_frameNumber * 0.01) * 300.0);
              vec3 prjOnPlane = pointProjectOnPlane(u_scanPlaneNormalEC.xyz, scanCenterEC.xyz, viewPos.xyz);
              float dis = length(prjOnPlane.xyz - scanCenterEC.xyz);
              if(dis < u_radius){
                out_FragColor + = pow(150.0/max(dis,150.0),2.0) * u_scanColor;
              }
          }
        `
      return scanSegmentShader
    }
    /**
     * Clear special effects objects
     * @param {*} lastStage special effects object
     */
    clearScanEffects(lastStage: any) {
      this.viewer.scene.postProcessStages.remove(lastStage)
    }
  }
  
  export default CircleDiffusion

In the shader, the radius and angle are given, and the attenuation from the light to the center point is implemented through an index. You can customize it for optimization. The index I chose is 2, which can be changed according to actual project needs.