干货!如何在高德地图上实现可视化图层合并渲染?

介绍

高德最近对可视化图层组件做了优化,解决了不少问题,涉及到多个场景的合并渲染,后期效果的叠加渲染等等,笔者在这里总结了一些经验跟大家分享。

在可视图层的开发过程中必须解决的一个问题就是多个场景(图层)的渲染,比如在地图上显示交通路线、区域范围、兴趣点3个场景,在视觉上是处于同个空间系统,又能够实现分层控制。最容易想到的办法就是为每个场景创建一个Canvas,并将Canvas标签叠加起来。

b7d56068d1ad649d9cfe6ff2a810ef6b.gif

这样做首先会遇到图层的数量会受限的问题。浏览器对单个页面虽然不限制Canvas标签的数量,但对运行的WebGL上下文数量是有限制的,例如PC上的Chrome支持的上限可能是8个(没有明确说法),手机平板等移动终端会更受限,超越上限时就会出现类似下面的错误,最先创建的上下文会直接被启用不显示。本文的核心问题就是解决WebGL共享的问题。

WARNING: Too many active WebGL contexts

多个场景合并渲染

实现思路

让多个图层的渲染器共享同一个WebGL上下文,就是把所有的图层内容都渲染到同一个Canvas里,这似乎很简单,只要在实例化WebGLRenderer的时候指定同一个context: gl,这样在渲染的时候所有renderer最终输出的目标就是同一个。

6e1bf1c27982c52479b572dc148ed1ab.jpeg

在这里需要注意的有两点:

1. 渲染器的autoClear属性定义了渲染器是否在渲染每一帧之前自动抹除上一次的输出结果,必须设为false,否则你只能看到最后一个渲染器输出的画面。

2. 需要实现多个渲染器能够共享同个空间深度关系(近大远小视觉遮盖),这里也需要保证构造参数depth为true。

最终我们就能够得到理想的输出结果。

3b4c0de5b1714c6531d834b15be64ff3.png

代码实现

由于本文的初衷是为了解决高德地图上多个可视化图层叠加在一个容器显示的问题,在具体实现中使用到CustomLayer,实际上我们需要的是Canvas,可以根据具体项目的需要做调整。

// 1.创建CustomLayer,为最终渲染结果提供容器
customLayer = new AMap.CustomLayer(canvas, {
  zooms: [3, 22],
  zIndex: 0,
  alwaysRender: true
})


const gl = canvas.getContext('webgl')


// 2.逐个创建renderer
const renderer1 = createRenderer()
const renderer2 = createRenderer()


function createRenderer(){
  const {innerWidth, innerHeight} = window
  const renderer = new THREE.WebGLRenderer({
    context:gl,
    alpha: true,
    depth: true //多个场景共享深度关系
  })
  // 保证多个场景叠加不会互相清除
  renderer.autoClear = false
  renderer.setClearAlpha(0)
  renderer.setSize(innerWidth, innerHeight)
  return renderer
}


// 3.逐个渲染renderer
function animate() {
  // 由于是同个坐标系的图层,camera可以是共享的
  renderer.render(scene1, camera)
  renderer2.render(scene2, camera)
  requestAnimationFrame(animate)
}

最终效果如图,这是由两个渲染器叠加渲染的场景,共享同一个空间深度关系。

bb770eecdd20432107ea3ed87e1a89b5.gif

多个图层后期合并渲染

实现思路

如果你接到的需求只到场景叠加合成这个程度,那么问题到这里就解决了。然而如果又有了新需求,支持给每个图层添加后期效果,也支持图层合并后添加后期效果,事情开始没有那么简单。

这里稍微讲一下EffectComposer的工作原理。

1. 创建一个EffectComposer实例,并传入一个渲染器(Renderer)和渲染目标(RenderTarget)。渲染目标可以是屏幕(默认值)或者自定义的帧缓冲对象。

2. 通过addPass方法向EffectComposer实例添加一系列的通道(Pass)。通道可以是渲染通道(RenderPass)、着色器通道(ShaderPass)或其他自定义的通道。

3. 在渲染循环中,调用EffectComposer的render方法来执行后期处理效果的渲染。EffectComposer会按照添加通道的顺序,依次将渲染结果传递给每个通道进行处理。

4. 每个通道在处理渲染结果时,可以应用不同的后期处理效果,如色调映射、模糊、阴影等。通常,通道会使用着色器程序(Shader)来定义定制的渲染效果。

5. 最后,EffectComposer将经过所有通道处理的最终渲染结果输出到渲染目标。如果渲染目标是屏幕,则将结果显示在浏览器中;如果是自定义的帧缓冲对象,则可以进一步处理或传递给其他渲染器。

使用EffectComposer效果处理器实现后期效果,多个后期效果处理器直接叠加渲染的话,后者输出到内容会整个覆盖前者,如下图所示,我们只能看到最后一个处理器输出的结果。

b79bc2f0e4bde2f1b165710daa6e74e3.png

对于这个问题我尝试了各种解决思路均不理想,后来终于在一篇关于如何给场景内容做局部效果的文章里发现了答案——把每个图层的效果处理器转为着色通道,实现效果叠加,如下图所示。

ea615c9b8e021e4dbcf6f26959af9cb2.png

实现原理是这样:

1. 每个图层对应一个EffectComposer,可以独立添加各种通道Pass最后期效果处理,将处理结果转换为一个着色通道ShaderPass待用。

2. 创建一个EffectComposer实例作为最终效果合成器(需要一个空的渲染通道RenderPass垫底),把步骤1产生的ShaderPass逐个添加进来。

3. 最终效果合成器还可以增加Pass对所有图层整体加效果(如有必要),在最后增加一个OutputPass通道,调用render将结果渲染到屏幕。

为加深理解我画了一张原理图,执行顺序从左到右,从上到下。

9dae75b9aca602524905bc17c2f1398e.png

代码实现

1. 创建容器CustomLayer,每个图层创建renderer和对应composer。

// 1.创建CustomLayer,为最终渲染结果提供容器
customLayer = new AMap.CustomLayer(canvas, {
  zooms: [3, 22],
  zIndex: 0,
  alwaysRender: true
})


const gl = canvas.getContext('webgl')


// 2.逐个创建renderer
const renderer1 = createRenderer()
const renderer2 = createRenderer()


// 3.创建效果处理器
const composer1 = createComposer(renderer1, scene1)
const composer2 = createComposer(renderer2, scene2)


function createComposer(renderer, scene){


    const renderScene = new RenderPass(scene, camera)
    renderScene.clearDepth = false


    const composer = new EffectComposer(renderer)
    composer.renderToScreen = false
    composer.addPass(renderScene)


    //... 根据需要,添加一些后期效果通道


    return composer
  }

2. 创建最终处理器,把其他处理器结果转为ShaderPass加入,叠加的模式其实可以根据实际需要做定制,比如将叠加的内容进行互相遮盖或者混合。

// 最终合成处理器
const renderer = this.createRenderer()
const composer = new EffectComposer(renderer)


const renderPass = new RenderPass(new THREE.Scene(), new THREE.Camera())
renderPass.clear = true
composer.addPass(renderPass) // 渲染通道


// 添加其他处理器结果
composer.addPass(createPassShader(composer1))
composer.addPass(createPassShader(composer2))


// 添加最终效果和输出通道
const bloomPass = this.createBloomPass()
composer.addPass(bloomPass) 
composer.addPass(this.createOutputPass()) 


/**
 * 创建1个着色通道用于叠加渲染处理器
 * @param { EffectComposer} composer
 * @param {Object} option
 */
createPassShader (composer, option = { mode: 0 }) {
  const { mode } = option


  const res = new ShaderPass(new THREE.ShaderMaterial({
    uniforms: {
      baseTexture: { value: null },
      coverTexture: { value: null },
      mode: { value: mode } // 0 颜色叠加,1 baseTexture遮盖coverTexture, 2 coverTexture遮盖baseTexture
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
    `,
    fragmentShader: `
        uniform sampler2D baseTexture;
        uniform sampler2D coverTexture;
        uniform int mode;
        varying vec2 vUv;
        void main() {


          vec4 baseColor = texture2D(baseTexture, vUv);
          vec4 bloomColor = texture2D(coverTexture, vUv);


          if(mode == 0){
            gl_FragColor = ( baseColor + vec4( 1.0 ) * bloomColor );
          }else if(mode == 1){
            gl_FragColor = bloomColor * (1.0 - baseColor.a) + baseColor;
          }else if(mode ==2){
            gl_FragColor = baseColor * (1.0 - bloomColor.a) + bloomColor;
          }
        }
    `,
    defines: {}
  }), 'baseTexture')

3. 逐帧渲染过程。

animate (time) {


  // 各图层仅渲染到缓冲对象 
  composer1.render()
  composer2.render()


  // 最终合并渲染
  composer.render()
  requestAnimationFrame(this.animate)
}

至此可以看到如图所示最终效果,三个图层的内容都加上了后期的辉光效果,但是所有物体在一个画布渲染,所有后期效果在另一个画布渲染。

29c9208b4b641bee6545aee23eb6aa6d.gif

本文由高德开发者大本营成员—张林海提供

并非官方开发文档,仅代表作者个人观点

关注「高德技术」,了解更多

推荐阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值