唯杰地图 logo唯杰地图
VJMAP
EN

后期处理

vjmap3d/references/postprocessing/postprocessing.md 34.1 KB806 行
原文

后期处理(Post-processing)是一种高级功能,允许开发者在渲染场景后对图像进行额外的处理,以实现诸如景深效果、光晕、色彩校正、运动模糊等视觉特效,能够显著提升视觉体验。

注意

当在唯杰地图vjmap中使用vjmap3d做为三维图层使用时,因为唯杰地图vjmap不能使用后期处理,所以这种模式下,后期处理将自动禁用。

WebGPU 引擎下的后期处理

render: { engine: "webgpu" } 时,本模块基于 GLSL 的效果合成器(EffectComposer)不可用,后期处理由 WebGPU 后处理模块(three RenderPipeline + TSL 节点)自动接管,对外 API 用法保持一致:

  • 辉光(bloom)、选中/描边(outline/selected)、FXAA/SMAA/AO/景深等常用效果自动切换:entity.selected = true、entity.bloom = true 用法不变。但 app.composer 只是兼容外壳(passes 恒为空数组);app.bloomEffect 只有 selection/intensity/luminanceThreshold,app.outlineEffect/app.selectedEffect 只有 selection/edgeStrength/visibleEdgeColor/hiddenEdgeColor/pulseSpeed;postProcess 参数只认 enable/renderUpdate/useFXAA/useSMAA/autoAdd{Selected,Outline,Bloom}Pass/bloomPass/outlinePass/selectedPass,其余(disableMergeEffect/renderTargetOptions/depthPickingPass 等)被忽略,且 bloomPass.radius 含义不同(mip 权重,默认 0);
  • addEffectRenderPass / removeEffectRenderPass / setEffectRenderPassEnable / setEffectRenderPassOrder 继续可用:内置 pmndrs 效果中只有 KawaseBlurPass、PixelationEffect、ColorDepthEffect、ToneMappingEffect(固定 Reinhard)、DepthOfFieldEffect(固定参数)、SSAOEffect、TextureEffect 映射到 TSL 实现,FXAAEffect/SMAAEffect 在此路径上为空操作;其余含 mainImage 的效果(含自定义 GLSL Effect)会尝试自动转译为 TSL,转译失败时告警并忽略该效果(不会报错中断);只有 mainUv 的效果(如 GlitchEffect)直接告警跳过——故障效果请用便捷方法 addGlitchEffect()(混入在 app 上);也可直接传 TSL 效果函数(见下文 WebGPU 引擎下的自定义效果)。

详见 WebGPU 渲染引擎。

常用方法

方法名 描述
composer 效果合成器
isComposerEnable 效果合成器是否可用
setComposerEnable 设置效果合成器是否可用
addEffectRenderPass 给效果合成器增加渲染通道
removeEffectRenderPass 给效果合成器移除渲染通道
refreshEffectRenderPass 刷新渲染通道
getDepth getDepth
outlineEffect OutlineEffect
bloomEffect bloomEffect
selectedEffect selectedEffect
isPostProcessIdle ⚠️ 仅类型声明(后期处理是否空闲),运行时 undefined,勿用

postProcess 参数(webgl 引擎)

创建 App 时通过 postProcess: {...}(PostProcessModuleOptions)传入,常用键:

参数 默认 说明
enable 未设 false 时不创建合成器(地图图层模式强制 false)。⚠️ getDepth 及 unprojectAsync(x, y, "depth") 要求显式 true,未设时 getDepth 恒为 undefined
useFXAA / useSMAA false 首帧自动加 FXAA(order 200)/ SMAA(order 100)抗锯齿通道
autoAddRenderPass / autoAddCopyPass true 自动加场景 RenderPass(order 0)/ 末尾 CopyPass(order 100000,仅当最后一个通道不上屏时启用)
autoAddSelectedPass / autoAddOutlinePass / autoAddBloomPass 未设 未设:首次出现选中/描边/辉光实体时按需创建;true:首帧即创建;false:不创建(entity.selected/outline/bloom 不再有效果)
disableSelectedPassWhenIdle / disableOutlinePassWhenIdle / disableBloomPassWhenIdle true 选择集为空时自动停用对应通道
disableMergeEffect false true 时全局不合并相邻 EffectPass;单个通道可在 addEffectRenderPass 前设 pass.disableMergeEffect = true(d.ts 未声明,TS 需 // @ts-ignore),见下文坑位 7
disableCacheEffectPass false 合并通道时不缓存效果组合
bloomPass 见说明 辉光默认 { luminanceThreshold: 0.01, luminanceSmoothing: 0.1, mipmapBlur: true, intensity: 6 },另有 luminancePassEnabled(默认 false)/ignoreBackground(默认 true)/inverted;辉光通道(order 800)默认不合并(bloomPass.disableMergeEffect 默认 true)
selectedPass 见说明 选中描边(order 500)默认 { edgeStrength: 4, pulseSpeed: 0.4, visibleEdgeColor: 0xffff00, hiddenEdgeColor: 0xb0634a, xRay: true, blurriness: 0 }
outlinePass 见说明 描边(order 501)默认 { edgeStrength: 6, pulseSpeed: 0.4, visibleEdgeColor: 0x00ffff, hiddenEdgeColor: 0x634ab0, xRay: true, blurriness: 4 }
depthPickingPass false true 时首帧即加深度拾取通道(否则首次 getDepth 时才加)
disableDepthPickTime 3 深度拾取空闲多少秒后自动停用,0 不停用
depthPickMinZ 0 getDepth 结果(NDC z)小于此值视为无效、返回 undefined
disableDepthPickWhenlogDepthBuffer false 对数深度缓冲时禁用深度拾取
renderTargetOptions 见说明 合成器渲染目标:multisampling 默认 min(4, maxSamples)、frameBufferType 默认 HalfFloatType、stencilBuffer 除非显式 false 否则强制 true
renderUpdate true false 时模块不调用 composer.render(自行渲染)
workaroundEnabled true pmndrs OverrideMaterialManager.workaroundEnabled

disableWhenIdle、autoClear 类型里有但当前未实现。webgpu 引擎只认其中一部分,见上文提示。

按对象排除:给对象(含子孙)打 userData.disableBloom / disableSelected / disableOutline = true,或调 vjmap3d.disableBloom(obj) / vjmap3d.disableSelected(obj) / vjmap3d.disableOutline(obj)(第二参传 true 为取消标记),该对象即使挂在辉光/选中/描边实体下也不参与效果;两种引擎都识别。标记只在选择集变化时读取,请在设置 entity.bloom 等之前打好。

使用

  • 创建后期效果 Effect
  • 创建后期效果通道 Pass
  • 增加后期处理 addEffectRenderPass
// 创建像素化效果
const effect = new vjmap3d.PixelationEffect(5); 
// 创建后期效果通道并增加
app.addEffectRenderPass(new EffectPass(app.camera, effect), 2000);
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
    toEntity: true,
    scale: [10, 10, 10]
});
entity.position.set(-2, 0, 1);
entity.addTo(app);
// 创建像素化效果
const effect = new vjmap3d.PixelationEffect(5); 
// 创建后期效果通道并增加
app.addEffectRenderPass(new vjmap3d.EffectPass(app.camera, effect), 2000);
// ui配置
const ui = await app.getConfigPane({ title: "后期效果设置", style: { width: "250px"}});
let folder = new vjmap3d.UiPanelJsonConfig();
folder.addBinding(effect, "granularity", { min: 0, max: 50, step: 1 });
folder.toJson().forEach(c => ui.appendChild(c))

提示

每个 Pass 都有一个 renderToScreen 默认为false。EffectComposer 将自动设置此标志以让所有后期效果的最后一个效果渲染到屏幕上。注意 vjmap3d 的后期处理模块每次 refreshEffectRenderPass(添加通道、启停通道时都会触发)都会重算各通道的 renderToScreen(最后一个启用的通道上屏),手动改 renderToScreen 或 autoRenderToScreen 会被覆盖;某通道不应直接上屏时请设 pass.disableRenderToScreen = true(由内部 CopyPass 负责上屏)。

多个 EffectPass 会对性能有一定影响,建议的启用或禁用效果的方法是提前准备EffectPass具有所需 Effect 组合的多个实例,并根据需要用 app.setEffectRenderPassEnable(pass, true/false) 启用或禁用这些通道(默认合并模式下相邻 EffectPass 会被合并成一个,只在刷新时重读各通道的 enabled,直接改 pass.enabled 不会生效)。这种方法需要提前进行一些规划,并且会占用更多内存,但却是最有效的解决方案。Effect实例可以在其他EffectPass实例中重复使用以减少内存使用量。

常见后期效果

OutlineEffect

 const effect = new OutlineEffect(app.scene, app.camera, {
    blendFunction: BlendFunction.SCREEN,
    multisampling: Math.min(4, app.renderer.capabilities.maxSamples),
    edgeStrength: 4.0,
    pulseSpeed: 0.4,
    visibleEdgeColor: 0xffff00,
    hiddenEdgeColor: 0xb0634a,
    height: 480,
    blur: false,
    xRay: true
});
let pass = new EffectPass(app.camera, effect);
app.addEffectRenderPass(pass, 500)

SelectiveBloomEffect

const effect = new SelectiveBloomEffect(app.scene, app.camera, {
    luminanceThreshold: 0.01,
    luminanceSmoothing: 0.1,
    mipmapBlur: true,
    intensity: 6.0
});
let pass = new EffectPass(app.camera, effect);
app.addEffectRenderPass(pass, 500)

DepthPickingPass

app.depthPickingPass = new DepthPickingPass()
app.addEffectRenderPass(app.depthPickingPass, 5);

KawaseBlurPass

const blurPass = new KawaseBlurPass({
    height: 480
});
app.addEffectRenderPass(blurPass, 1000);

ToneMappingEffect

 const toneMappingEffect = new ToneMappingEffect({
    mode: ToneMappingMode.REINHARD2_ADAPTIVE,
    resolution: 256,
    whitePoint: 16.0,
    middleGrey: 0.6,
    minLuminance: 0.01,
    averageLuminance: 0.01,
    adaptationRate: 1.0
});
app.addEffectRenderPass(new EffectPass(app.camera, toneMappingEffect), 1200);

ColorDepthEffect

const effect = new ColorDepthEffect({ bits: 21 });
let effectPass = new EffectPass(app.camera, effect)
app.addEffectRenderPass(effectPass, 1200);

DepthOfFieldEffect

const effect = new DepthOfFieldEffect(app.camera, {
    // @ts-ignore
    kernelSize: KernelSize.SMALL,
    worldFocusDistance: 2,
    worldFocusRange: 5,
    bokehScale: 3.0,
    resolutionScale: 0.75
});
let effectPass = new EffectPass(app.camera, effect)
app.addEffectRenderPass(effectPass, 1400);

GlitchEffect

const chromaticAberrationEffect = new ChromaticAberrationEffect();
const effect = new GlitchEffect({
    perturbationMap: ResManager.loadTexture(ResManager.svrUrl("textures/perturb.jpg")),
    chromaticAberrationOffset: chromaticAberrationEffect.offset // optional
});
// GlitchEffect 变换 UV(mainUv),ChromaticAberrationEffect 是卷积效果:二者被默认合并进同一个 EffectPass 会抛
// "Effects that transform UVs are incompatible with convolution effects",须先禁止这两个通道参与合并
const glitchPass = new EffectPass(app.camera, effect);
const caPass = new EffectPass(app.camera, chromaticAberrationEffect);
// @ts-ignore
glitchPass.disableMergeEffect = true;
// @ts-ignore
caPass.disableMergeEffect = true;
app.addEffectRenderPass(glitchPass, 1600);
app.addEffectRenderPass(caPass, 1800);

webgl 引擎默认配置下便捷方法 addGlitchEffect()(混入在 app 上)内部正是上面未设 disableMergeEffect 的写法,同样会抛错,需创建 App 时传 postProcess: { disableMergeEffect: true }。webgpu 引擎下 GlitchEffect 无 TSL 映射(会被跳过),请直接用便捷方法 addGlitchEffect()。

PixelationEffect

const effect = new PixelationEffect(5);
app.addEffectRenderPass(new EffectPass(app.camera, effect), 2000);

SSAOEffect

const normalPass = new NormalPass(app.scene, app.camera);
const effect = new SSAOEffect(app.camera, normalPass.texture, {
    worldDistanceThreshold: 20,
    worldDistanceFalloff: 5,
    worldProximityThreshold: 0.4,
    worldProximityFalloff: 0.1,
    luminanceInfluence: 0.7,
    samples: 16,
    radius: 0.04,
    intensity: 1,
    resolutionScale: 0.5
});
app.addEffectRenderPass(normalPass, 2200);
app.addEffectRenderPass(new EffectPass(app.camera, effect), 2400);

示例

let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let ent = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/soldier.glb", {
    splitSubEntity: true,
    anchor: "front-bottom-right"
});
ent.addTo(app);
ent.selected = true;
ent.bloom = true;
// ui配置
const ui = await app.getConfigPane({ title: "后期效果设置", style: { width: "250px"}});
let mod = app.getModule(vjmap3d.PostProcessModule);
let cfg = mod.uiConfig;
cfg.expanded = true;
ui.appendChild(cfg)

自定义后期处理效果

扫光效果

实现的思路 ( 效果参考 https://www.cnblogs.com/eco-just/p/11667713.html )

1.在自定义通道上,先获取场景原本的uv,然后原样输出;

2.在fs里,降低gl_FragColor的亮度(乘以小于1.0的数,这里取0.3),这样,画面立马暗了下来;

3.画一条直线 y = -x;在这条直线上的部分颜色亮度为1.0,实际上光束是有宽度的,所以我们取一下abs(dy) < 0.1的时候亮度为1.0,那么光束的宽度为0.2 * 根号2;

4.光束内的abs(dy),范围是0~0.1,那么可以用smoothstep函数来做一个平滑插值处理,使得光束的亮度由中间向两边平滑过渡;

5.添加uniform参数time实现动画,此时直线函数变为y = -x + time,随着time值的变化,自然y值也会变化,最终的效果就是光束左右移动,如动图所示;

let frag = /* glsl */`
    varying vec3 vPosition;
    uniform float iTime;
    uniform float iSpeed;
    uniform float iBrightness;
    uniform float iLineWidth;
    vec3 rgb2hsv(vec3 c)
    {
        vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
        vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
        vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
        float d = q.x - min(q.w, q.y);
        float e = 1.0e-10;
        return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
    }
    vec3 hsv2rgb(vec3 c)
    {
        vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
        vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
        return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
    }
    void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
        float x = vPosition.x;
        float lighty = -x + iSpeed * sin(iTime);
        float alpha = abs(vPosition.y - lighty);
        if(alpha < iLineWidth / 10.){
            float a = 1.0 -  alpha / 0.1;
            float enda = smoothstep(0.0,1.0,a);
            vec3 hsv = rgb2hsv(inputColor.xyz);
            hsv.z += enda * iBrightness / 5.;
            hsv.z = clamp(hsv.z, 0., 1.);
            outputColor.xyz = hsv2rgb(hsv);
            outputColor.a = inputColor.z;
        }else{
            outputColor = inputColor;
        }
    }
`
// 效果参考 https://www.cnblogs.com/eco-just/p/11667713.html
let effect = new vjmap3d.Effect("ScenLine", frag,  {
    // @ts-ignore
    uniforms: new Map([
        ["iTime", app.commonUniforms.iTime],
        ["iSpeed", new THREE.Uniform(1.5)],
        ["iBrightness", new THREE.Uniform(1.5)],
        ["iLineWidth", new THREE.Uniform(1.0)]
    ])
});
const pass = new vjmap3d.EffectPass(app.camera, effect);
app.addEffectRenderPass(pass, 700);
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true }, // 是否显示坐标网格
        defaultLights: false,
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
app.scene.add(new THREE.AmbientLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.5));
let data = [];
let color = new THREE.Color();
let startColor = new THREE.Color("#ff0000")
let endColor = new THREE.Color("#ffff00")
let heightArr = []
for(let i = 0; i < 20; i++) {
    for(let j = 0; j < 20; j++) {
        let shape = new THREE.Shape();
        let radius = Math.random() * 2 + 0.5
        shape.ellipse(i * 5 - 50, j * 5 - 50, radius, radius, 0, Math.PI * 2, false, 0);
        let points = shape.getPoints()
        let height = 20 * Math.random()  + 5;
        color = color.lerpColors(startColor, endColor, height / 20);
        heightArr.push(height)
        data.push({
            color: "#ff0000",
            color2: color.clone(),
            borderColor:  "#ffff00",
            coordinates: points.map(p => [p.x, p.y]),
            extrude: {
                depth: height
            }
        })
    }
}
let fillExtrusion = new vjmap3d.FillExtrusionsEntity({
    data: data,
    borderStyle: {
        linewidth: 2
    },
})
fillExtrusion.addTo(app);
let val = { v: 0}
let tm = new Date();
new vjmap3d.Tween(val).to(3000, { v: 1 }, { easing: 'easeInSine',
    onProgress: (targe, key, start, end, alpha, reversed) => {
        let curTm = new Date();
        tm = curTm;
        let depthData = []
        for(let n = 0; n < data.length; n++) {
            let depth = THREE.MathUtils.lerp(0, 1, alpha) * heightArr[n]  + 0.1
            depthData.push({
                index: n,
                depth: depth,
                color2: color.lerpColors(startColor, endColor, depth / 20  + 5)
            })
        }
        fillExtrusion.setDepths(depthData);
    }
}).yoyoForever().start();
app.cameraControl.loadState({
    cameraTarget: new THREE.Vector3(0.390014119563027,1.1852643743923352e-18, 9.854902063178976),
    cameraPosition: new THREE.Vector3(0.4061914276850882,57.326314956526076, 82.76816394954821)
})
// 增加后期处理
let frag = /* glsl */`
    varying vec3 vPosition;
    uniform float iTime;
    uniform float iSpeed;
    uniform float iBrightness;
    uniform float iLineWidth;
    vec3 rgb2hsv(vec3 c)
    {
        vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
        vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
        vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
        float d = q.x - min(q.w, q.y);
        float e = 1.0e-10;
        return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
    }
    vec3 hsv2rgb(vec3 c)
    {
        vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
        vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
        return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
    }
    void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
        float x = vPosition.x;
        float lighty = -x + iSpeed * sin(iTime);
        float alpha = abs(vPosition.y - lighty);
        if(alpha < iLineWidth / 10.){
            float a = 1.0 -  alpha / 0.1;
            float enda = smoothstep(0.0,1.0,a);
            vec3 hsv = rgb2hsv(inputColor.xyz);
            hsv.z += enda * iBrightness / 5.;
            hsv.z = clamp(hsv.z, 0., 1.);
            outputColor.xyz = hsv2rgb(hsv);
            outputColor.a = inputColor.z;
        }else{
            outputColor = inputColor;
        }
    }
`
// 效果参考 https://www.cnblogs.com/eco-just/p/11667713.html
let effect = new vjmap3d.Effect("ScenLine", frag,  {
    // @ts-ignore
    uniforms: new Map([
        ["iTime", app.commonUniforms.iTime],
        ["iSpeed", new THREE.Uniform(1.5)],
        ["iBrightness", new THREE.Uniform(1.5)],
        ["iLineWidth", new THREE.Uniform(1.0)]
    ])
});
const pass = new vjmap3d.EffectPass(app.camera, effect);
app.addEffectRenderPass(pass, 700);

WebGPU 引擎下的自定义效果

render: { engine: "webgpu" } 时,addEffectRenderPass 支持两种参数形式:

  1. 兼容 webgl 写法:传入 pmndrs 的 EffectPass / Effect。内置效果(PixelationEffect、ColorDepthEffect、GlitchEffect 等)自动映射到 TSL 实现;自定义 GLSL Effect(mainImage(inputColor, uv, outputColor) 写法,如上文的扫光效果)会尝试自动转译为 TSL——大部分常规 GLSL 可以直接工作,转译失败时在控制台告警并忽略该效果;
  2. 原生 TSL 写法(推荐):直接传入 TSL 效果函数 (colorNode, scenePass, app) => 新颜色节点,其中 colorNode 是上一效果的输出颜色节点,scenePass 可用于获取深度等信息(如 scenePass.getTextureNode("depth"))。
// webgpu 引擎: 原生 TSL 自定义效果(灰度示例)
await app.whenReady();
const { mix, vec3, vec4, dot, uniform } = await import("three/tsl");
const uStrength = uniform(1);
const grayEffect = (colorNode) => {
    const gray = dot(colorNode.rgb, vec3(0.299, 0.587, 0.114));
    return mix(colorNode, vec4(vec3(gray), colorNode.a), uStrength);
};
app.addEffectRenderPass(grayEffect, 2000);
// 运行期调整 uniform
uStrength.value = 0.5;
// 启用/禁用与移除(与 webgl 引擎同一套 API)
app.setEffectRenderPassEnable(grayEffect, false);
app.removeEffectRenderPass(grayEffect);

此外,addSSAOEffect / addDepthOfFieldEffect / addKawaseBlurPass / addToneMappingEffect / addColorDepthEffect / addPixelationEffect / addGlitchEffect 等便捷方法在两种引擎下均可用(webgpu 下为等价的 TSL 实现,可传参数,如 addPixelationEffect(granularity)、addKawaseBlurPass({ sigma })、addDepthOfFieldEffect({ focusDistance, focalLength, bokehScale });webgl 版无参。两种引擎都不返回句柄,需要后续启停/调参的效果请自己构造后 addEffectRenderPass)。注意:

  • webgpu 下 WebGPU 后处理模块在渲染器就绪后才挂载,须在 await app.whenReady() 之后调用——之前调用会落到 webgl 版 mixin(此时没有 composer)被静默丢弃;
  • webgl 默认配置下便捷方法 addGlitchEffect() 会抛错(见上文 GlitchEffect 与下文坑位 7),需 postProcess: { disableMergeEffect: true }。

使用threejs后期处理效果

如果不使用vjmap3d提供的后期处理,需要使用threejs示例中提供的后期处理效果,需要进行如下操作:

  • 禁止vjmap3d的后期处理
  • 使用threejs后期处理效果
  • 响应onBloomedChange、onOutlineChange或onSelectedChange的事件,使实体设置为bloom、outline 或 selected 有效果

注意

本节示例基于 three/addons/postprocessing 的 EffectComposer(GLSL),仅适用于默认的 webgl 引擎。webgpu 引擎下如需完全接管后期处理,请使用 three 的 RenderPipeline(import { RenderPipeline } from "three/webgpu";旧名 PostProcessing 自 r183 起已弃用,仅为其别名)/ TSL 节点体系(three/tsl)自行组装。

示例如下:

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    render: {
        renderUpdate: false
    },
    scene: {
        defaultLights: false
    },
    stat: { show: true, left: "0" },
    camera: {
        fov: 40,
        near: 1, 
        far: 200,
        position: [  0, 0, 20],
        lookAt: [0, 0, 0]
    },
    entityOutline: {
        enable: false // 这里需要设置成false
    },
    postProcess: {
        enable: false
    }
})
let scene = app.scene, camera = app.camera, renderer = app.renderer;
//
const BLOOM_SCENE = 1;
//
const bloomLayer = new THREE.Layers();
bloomLayer.set( BLOOM_SCENE );
//
const params = {
    threshold: 0,
    strength: 1,
    radius: 0.5,
    exposure: 1
};
//
const outlineParams = {
    edgeStrength: 3.0,
    edgeGlow: 0.1,
    edgeThickness: 1.0,
    pulsePeriod: 2,
    rotate: false,
    usePatternTexture: false
};
//
const darkMaterial = new THREE.MeshBasicMaterial( { color: 'black' } );
const materials = {};
//
const renderScene = new RenderPass( scene, camera );
//
const bloomPass = new UnrealBloomPass( new THREE.Vector2( app.containerSize.width,app.containerSize.height ), 1.5, 0.4, 0.85 );
bloomPass.threshold = params.threshold;
bloomPass.strength = params.strength;
bloomPass.radius = params.radius;
//
const bloomComposer = new EffectComposer( renderer );
bloomComposer.renderToScreen = false;
bloomComposer.addPass( renderScene );
bloomComposer.addPass( bloomPass );
//
const mixPass = new ShaderPass(
    new THREE.ShaderMaterial( {
        uniforms: {
            baseTexture: { value: null },
            bloomTexture: { value: bloomComposer.renderTarget2.texture }
        },
        vertexShader: /* glsl */`
            varying vec2 vUv;
        void main() {

            vUv = uv;

            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

        }
    `,
    fragmentShader: /* glsl */`
        uniform sampler2D baseTexture;
		uniform sampler2D bloomTexture;

		varying vec2 vUv;

		void main() {

			gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );

		}

    `,
    defines: {}
} ), 'baseTexture'

); mixPass.needsSwap = true; // const outputPass = new OutputPass(); // const finalComposer = new EffectComposer( renderer ); finalComposer.addPass( renderScene ); // let outlinePass = new OutlinePass( new THREE.Vector2( app.containerSize.width, app.containerSize.height ), scene, camera ); outlinePass.pulsePeriod = 3; outlinePass.visibleEdgeColor.set("#ffff00") finalComposer.addPass( outlinePass ); // const textureLoader = new THREE.TextureLoader(); let assetsPath = "http://vjmap.com/map3d/resources/three/" textureLoader.load(assetsPath + 'textures/tri_pattern.jpg', function ( texture ) { outlinePass.patternTexture = texture; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; } ); finalComposer.addPass( mixPass ); finalComposer.addPass( outputPass ); // let effectFXAA = new ShaderPass( FXAAShader ); effectFXAA.uniforms[ 'resolution' ].value.set( 1 / app.containerSize.width, 1 / app.containerSize.height ); finalComposer.addPass( effectFXAA ); // const gui = new GUI(); // const bloomFolder = gui.addFolder( 'bloom' ); bloomFolder.add( params, 'threshold', 0.0, 1.0 ).onChange( function ( value ) { bloomPass.threshold = Number( value ); } ); bloomFolder.add( params, 'strength', 0.0, 3 ).onChange( function ( value ) { bloomPass.strength = Number( value ); } ); bloomFolder.add( params, 'radius', 0.0, 1.0 ).step( 0.01 ).onChange( function ( value ) { bloomPass.radius = Number( value ); } ); // const toneMappingFolder = gui.addFolder( 'tone mapping' ); toneMappingFolder.add( params, 'exposure', 0.1, 2 ).onChange( function ( value ) { renderer.toneMappingExposure = Math.pow( value, 4.0 ); } ); const outlineFolder = gui.addFolder( 'outline' ); outlineFolder.add( outlineParams, 'edgeStrength', 0.01, 10 ).onChange( function ( value ) { outlinePass.edgeStrength = Number( value ); } ); outlineFolder.add( outlineParams, 'edgeGlow', 0.0, 1 ).onChange( function ( value ) { outlinePass.edgeGlow = Number( value ); } ); outlineFolder.add( outlineParams, 'edgeThickness', 1, 4 ).onChange( function ( value ) { outlinePass.edgeThickness = Number( value ); } ); outlineFolder.add( outlineParams, 'pulsePeriod', 0.0, 5 ).onChange( function ( value ) { outlinePass.pulsePeriod = Number( value ); } ); outlineFolder.add( outlineParams, 'rotate' ); outlineFolder.add( outlineParams, 'usePatternTexture' ).onChange( function ( value ) { outlinePass.usePatternTexture = value; } ); // const conf = { visibleEdgeColor: '#ffff00', hiddenEdgeColor:'#190a05' } outlineFolder.addColor( conf, 'visibleEdgeColor' ).onChange( function ( value ) { outlinePass.visibleEdgeColor.set( value ); } ); outlineFolder.addColor( conf, 'hiddenEdgeColor' ).onChange( function ( value ) { outlinePass.hiddenEdgeColor.set( value ); } ); // app.signal.onContainerSizeChange.add(() => { let width = app.containerSize.width, height = app.containerSize.height; bloomComposer.setSize( width, height ); finalComposer.setSize( width, height ); effectFXAA.uniforms[ 'resolution' ].value.set( 1 / width, 1 / height ); }) function disposeMaterial( obj ) { if ( obj.material ) { obj.material.dispose(); } } // function render() { scene.traverse( darkenNonBloomed ); bloomComposer.render(); scene.traverse( restoreMaterial );

// render the entire scene, then render bloom scene on top
finalComposer.render();

} app.signal.onAppRender.add(render); // function darkenNonBloomed( obj ) { if ( obj.isMesh && bloomLayer.test( obj.layers ) === false ) { materials[ obj.uuid ] = obj.material; obj.material = darkMaterial; } } function restoreMaterial( obj ) { if ( materials[ obj.uuid ] ) { obj.material = materials[ obj.uuid ]; delete materials[ obj.uuid ]; } } // 下面的代码是支持实体中设置bloom辉光效果 const getBloomChildMeshes = ( objects, results) => { objects.forEach(object => { const children = object.children; if (object.isMesh || object.isSprite || object.isLine) { if (!results.has(object)) { if (object.userData.disableBloom !== true) { results.add(object) } } } for ( let i = 0, l = children.length; i < l; i ++ ) { getBloomChildMeshes([ children[ i ]], results); } }) } // function onBloomChange(bloomObjects) { let results = new Set(); // 过滤出当前场景的 let curBloomObjects = bloomObjects.filter(obj => vjmap3d.getOwerScene(obj) == app.scene) getBloomChildMeshes(curBloomObjects, results); app.scene.traverse(obj => { if (results.has(obj)) { obj.layers.enable( BLOOM_SCENE ) } else { obj.layers.disable( BLOOM_SCENE ) } }) } app.signal.onBloomedChange.add(onBloomChange); // function onOutlineChange(outlineObjects) { let results = new Set(); // 过滤出当前场景的 let curObjects = outlineObjects.filter(obj => vjmap3d.getOwerScene(obj) == app.scene) getBloomChildMeshes(curObjects, results); outlinePass.selectedObjects = Array.from(results); } app.signal.onOutlineChange.add(onOutlineChange); // 初始化场景 function setupScene() { scene.traverse( disposeMaterial ); scene.children.length = 0; const geometry = new THREE.IcosahedronGeometry( 1, 15 ); for ( let i = 0; i < 10; i ++ ) { const color = new THREE.Color(); color.setHSL( Math.random(), 0.7, Math.random() * 0.2 + 0.05 ); const material = new THREE.MeshBasicMaterial( { color: color } ); const sphere = new THREE.Mesh( geometry, material ); sphere.position.x = Math.random() * 10 - 5; sphere.position.y = Math.random() * 10 - 5; sphere.position.z = Math.random() * 10 - 5; sphere.position.normalize().multiplyScalar( Math.random() * 4.0 + 2.0 ); sphere.scale.setScalar( Math.random() * Math.random() + 0.2 ); //scene.add( sphere ); let meshEntity = vjmap3d.Entity.fromObject3d(sphere); meshEntity.addTo(app) if ( Math.random() < 0.25 ) meshEntity.bloom = true; if ( Math.random() < 0.25 ) meshEntity.outline = true; meshEntity.pointerEvents = true meshEntity.signal.onPointerDownUp.add(e => { if (vjmap3d.isLeftButton(e)) { // 如果是左键 meshEntity.bloom = !meshEntity.bloom } else { meshEntity.outline = !meshEntity.outline } }) } } setupScene();