后期处理
后期处理(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的效果(含自定义 GLSLEffect)会尝试自动转译为 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 支持两种参数形式:
- 兼容 webgl 写法:传入 pmndrs 的
EffectPass/Effect。内置效果(PixelationEffect、ColorDepthEffect、GlitchEffect等)自动映射到 TSL 实现;自定义 GLSLEffect(mainImage(inputColor, uv, outputColor)写法,如上文的扫光效果)会尝试自动转译为 TSL——大部分常规 GLSL 可以直接工作,转译失败时在控制台告警并忽略该效果; - 原生 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();