介绍
最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图层中。
后期特效其实有点类似照片的后期滤镜处理,是对渲染结果的二次处理,可以实现发光、模糊、色调调整、镜头暗角、模拟环境光遮蔽等各种效果,为了方便理解,下面的讲解我将以辉光效果为例,学会了一种其他效果思路类似。
方案调研
Three官方提供了非常简单的方法实现后期特效,貌似仅需要完成以下两个步骤就可以完成我们想要的需求,代码也非常清晰简单:
jsx
import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing//UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
...
// 1.在图层初始化完成后,创建效果合成器
onLayerInit(){
const { scene, camera, renderer } = this
const renderScene = new RenderPass(scene, camera)
// 后期泛光特效
bloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.clientWidth, this.container.clientHeight), 1, 0, 0)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius
composer = new EffectComposer(renderer)
// 以下代码会遮盖地图
composer.addPass(renderScene)
composer.addPass(bloomPass)
}
// 2.更新合成器
onRender () {
if (composer) {
composer.render()
}
}
本以为这样做就可以开心收工了,燃鹅事情并没有那么简单,把这套方案移入高德的GLCustomLayer中,出现了这样的情况,后期效果直接把地图底图盖住了。
出现这种情况的原因是实现辉光效果而编写的着色器,它会直接修改整个画面的alpha通道而导致透明效果丢失,因此需要单独修改UnrealBloomPass.js。
然而光是这样还不够,经过各种尝试,仍无法直接在GLCustomLayer上解决地图被遮盖的问题,后来咨询了高德地图开发团队的技术大佬,他给我的建议是后期效果层独立展示,于是就沿着这个思路进行了第二轮尝试。
这里面有几个关键步骤是必须的:
- 修改UnrealBloomPass着色器代码
- 使用输出通道new OutputPass()置于特效通道的后面
- 在customLayer图层中,每次渲染就更新特效合成器EffectComposer
由于我这边是不希望之前开发的可视化图层做太多的修改去迁就这个后期效果的,也有对性能较差的终端机器优雅降级的考虑,索性把后期效果独立为EffectLayer层,以方便灵活地装载或剥离,最终实现了这个效果。
实现步骤
-
修改 UnrealBloomPass.js,由于这个文件在npm包中不能随意修改,我另外写了一个UnrealBloomPass1 继承并覆盖了UnrealBloomPass的方法
jsximport * as THREE from 'three' import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js' class UnrealBloomPass1 extends UnrealBloomPass { constructor (resolution, strength, radius, threshold) { super(resolution, strength, radius, threshold) } getSeperableBlurMaterial (kernelRadius) { ... fragmentShader: `#include <common> varying vec2 vUv; uniform sampler2D colorTexture; uniform vec2 invSize; uniform vec2 direction; uniform float gaussianCoefficients[KERNEL_RADIUS]; void main() { float weightSum = gaussianCoefficients[0]; vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; float alphaSum; for( int i = 1; i < KERNEL_RADIUS; i ++ ) { float x = float(i); float w = gaussianCoefficients[i]; vec2 uvOffset = direction * invSize * x; vec4 sample1 = texture2D( colorTexture, vUv + uvOffset ); vec4 sample2 = texture2D( colorTexture, vUv - uvOffset ); diffuseSum += (sample1.rgb + sample2.rgb) * w; alphaSum += (sample1.a + sample2.a) * w; // weightSum += 2.0 * w; } // gyrate: overwrite this line for alpha pass // gl_FragColor = vec4(diffuseSum/weightSum, 1.0); gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum); }` }) } } export { UnrealBloomPass1 }
-
编写EffectLayer
jsximport * as THREE from 'three' import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' import { UnrealBloomPass1 } from '../plugins/three/examples/jsm/postprocessing/UnrealBloomPass.js' import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js' import _ from 'lodash' class EffectLayer { // 此处省去一些内部变量 _style = { // 光照强度阈值 threshold: 0.0, // 泛光强度 strength: 1.0, // 泛光半径 radius: 1.5 } /** * 创建一个实例 * @param {Object} config * @param {Layer} config.layer 目标图层,要求是Layer的相关子类 * @param {Number} [config.zIndex=120] 图层的层级 * @param {EffectStyle} [config.style] 后期特效的配置项 */ constructor (config) { const conf = _.merge(this._conf, config) this._style = _.merge(this._style, conf.style) if (!conf.layer.scene || !conf.layer.camera) { console.error('缺少场景和相机') return } this.init() } init () { this.createLayer() this.addEffect() } }
-
创建自定义图层customLayer
jsxcreateLayer () { const canvas = document.createElement('canvas') this._customLayer = new AMap.CustomLayer(canvas, { zooms: [3, 22], zIndex: this._conf.zIndex, alwaysRender: true }) this._canvas = canvas }
-
创建特效合成器
jsxaddEffect () { const { scene, camera, container, renderer, map } = this._conf.layer const { clientWidth, clientHeight } = container // 创建渲染器 const effectRender = new THREE.WebGLRenderer({ canvas: this._canvas, alpha: true, antialias: false, stencil: false, depth: false }) // renderer.setClearColor(0xff0000); effectRender.autoClear = false effectRender.setSize(clientWidth, clientHeight) // 后期效果 const renderScene = new RenderPass(scene, camera) // 后期辉光特效 const bloomPass = new UnrealBloomPass1(new THREE.Vector2(clientWidth, clientHeight), 1, 0, 0) bloomPass.clear = false // 输出通道 const outputPass = new OutputPass() outputPass.clear = false this.updatePass() const composer = new EffectComposer(effectRender) composer.addPass(renderScene) composer.addPass(bloomPass) composer.addPass(outputPass) this._composer = composer this._bloomPass = bloomPass this._customLayer.render = function () { if (composer) { // 每次渲染就更新特效合成器 composer.render() } } map.add(this._customLayer) } updatePass() { const {_bloomPass} = this if (_bloomPass) { _bloomPass.threshold = this._style.threshold _bloomPass.strength = this._style.strength _bloomPass.radius = this._style.radius } // 添加其他特效通道... }
-
使用EffectLayer
jsx//之前编写的可视化图层 const layer = new GLlayers.POI3dLayer({ map: getMap(), zooms: [10, 22] }) layer.on('complete', (layer) => { let effectLayer = new GLlayers.EffectLayer({ layer: layer, //把图层传入effectLayer style:{ threshold: 0.0, strength: 1.0, radius: 0.5, } }) })
注意:以上方案three.js版本为0.157, 该版本对three/example/jsm/postprocessing目录中的后期效果通道相关文件做了较多调整,如果是用之前的three.js版本,修改内容可能有所不同。
至此我们就可以在之前的可视化图层基础上,加入几行代码实现辉光效果,以下是挑选一部分图层加上EffectLayer之后的效果,肉眼可见还是有很明显区别的。当然在使用过程中也发现了个别图层原有的问题需要做进一步优化。
待解决问题
使用独立图层展示后期特效层有个明显缺点,无法关联默认基本图层的场景要素深度信息,最主要的影响是高德的建筑白模图层和自定义可视化图层的远近遮挡关系会丢失,导致可视化图层永远在最前面。比如下面这个城市主要道路的辉光效果,这个是需要后面花时间去解决的,写这篇文章的时候又找到几个方案,有时间再试一把,毕竟上面留给我的时间不多了。