# 后期处理

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

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

::: tip 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-引擎下的自定义效果)）。

详见 [WebGPU 渲染引擎](../advance/webgpu.md)。
:::


## 常用方法

|方法名|描述|
|:---|:---|
|[composer](https://vjmap.com/map3d/api/classes/PostProcessModule.html#composer)|效果合成器|
|[isComposerEnable](https://vjmap.com/map3d/api/classes/PostProcessModule.html#isComposerEnable)|效果合成器是否可用|
|[setComposerEnable](https://vjmap.com/map3d/api/classes/PostProcessModule.html#setComposerEnable)|设置效果合成器是否可用|
|[addEffectRenderPass](https://vjmap.com/map3d/api/classes/PostProcessModule.html#addEffectRenderPass)|给效果合成器增加渲染通道|
|[removeEffectRenderPass](https://vjmap.com/map3d/api/classes/PostProcessModule.html#removeEffectRenderPass)|给效果合成器移除渲染通道|
|[refreshEffectRenderPass](https://vjmap.com/map3d/api/classes/PostProcessModule.html#refreshEffectRenderPass)|刷新渲染通道|
|[getDepth](https://vjmap.com/map3d/api/classes/PostProcessModule.html#根据屏幕坐标获取深度值)|getDepth|
|[outlineEffect](https://vjmap.com/map3d/api/classes/PostProcessModule.html#outlineEffect)|OutlineEffect|
|[bloomEffect](https://vjmap.com/map3d/api/classes/PostProcessModule.html#bloomEffect)|bloomEffect|
|[selectedEffect](https://vjmap.com/map3d/api/classes/PostProcessModule.html#selectedEffect)|selectedEffect|
|[isPostProcessIdle](https://vjmap.com/map3d/api/classes/App.html#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`

```js
// 创建像素化效果
const effect = new vjmap3d.PixelationEffect(5); 
// 创建后期效果通道并增加
app.addEffectRenderPass(new EffectPass(app.camera, effect), 2000);
```
<mapFrame>
<pre>
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))
</pre>
</mapFrame>






::: tip 提示
每个 [Pass](https://vjmap.com/map3d/api/classes/Pass.html) 都有一个 [renderToScreen](https://vjmap.com/map3d/api/classes/Pass.html#renderToScreen) 默认为`false`。[EffectComposer](https://vjmap.com/map3d/api/classes/EffectComposer.html) 将自动设置此标志以让所有后期效果的最后一个效果渲染到屏幕上。注意 vjmap3d 的后期处理模块每次 `refreshEffectRenderPass`（添加通道、启停通道时都会触发）都会重算各通道的 `renderToScreen`（最后一个启用的通道上屏），手动改 `renderToScreen` 或 [autoRenderToScreen](https://vjmap.com/map3d/api/classes/EffectComposer.html#autoRenderToScreen) 会被覆盖；某通道不应直接上屏时请设 `pass.disableRenderToScreen = true`（由内部 `CopyPass` 负责上屏）。

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

## 常见后期效果

### [OutlineEffect](https://vjmap.com/map3d/api/classes/OutlineEffect.html)

```js
 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](https://vjmap.com/map3d/api/classes/SelectiveBloomEffect.html)

```js
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](https://vjmap.com/map3d/api/classes/DepthPickingPass.html)

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

### [KawaseBlurPass](https://vjmap.com/map3d/api/classes/KawaseBlurPass.html)

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

### [ToneMappingEffect](https://vjmap.com/map3d/api/classes/ToneMappingEffect.html)

```js
 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](https://vjmap.com/map3d/api/classes/ColorDepthEffect.html)

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

### [DepthOfFieldEffect](https://vjmap.com/map3d/api/classes/DepthOfFieldEffect.html)

```js
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](https://vjmap.com/map3d/api/classes/GlitchEffect.html)

```js
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](https://vjmap.com/map3d/api/classes/PixelationEffect.html)

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

### [SSAOEffect](https://vjmap.com/map3d/api/classes/SSAOEffect.html)

```js
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);
```


## 示例

<mapFrame>
<pre>
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)
</pre>
</mapFrame>


## 自定义后期处理效果

扫光效果

实现的思路 （ 效果参考 [https://www.cnblogs.com/eco-just/p/11667713.html](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值也会变化，最终的效果就是光束左右移动，如动图所示；

```js
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);
```
<mapFrame>
<pre>
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);
</pre>
</mapFrame>


## 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")`）。

```js
// 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` 有效果

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

 示例如下:

<mapFrame>
 <pre>
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();
 </pre>
</mapFrame>
