镜头效果 - LensPost
模拟真实相机镜头的三种效果,在一次全屏绘制中完成:
- 暗角(Vignette):画面四周逐渐变暗,把视线引向中心;
- 色差(Chromatic Aberration):镜头对不同波长的折射率不同,红、蓝通道沿径向错开,在画面边缘的高对比度轮廓上形成彩边;
- 胶片颗粒(Film Grain):叠加随机噪点,模拟胶片质感。
LensPost 在色调映射之前的线性 HDR 空间中执行,颗粒与暗角会和画面其他部分一起经过 色调映射 曲线。
ts
// 初始化引擎,可在此配置默认参数
let engine = await Engine3D.init({
setting: {
render: {
postProcessing: {
lens: {
vignetteIntensity: 0.55,
chromaticAberration: 0.3,
grainIntensity: 0.15
}
}
}
}
});
// 添加后处理组件
let postProcessing = scene.addComponent(PostProcessingComponent);
// 添加 LensPost
let lens = postProcessing.addPost(LensPost);
// 也可以通过 lens 对象设置,与上面的引擎配置等价
lens.vignetteIntensity = 0.55;
lens.vignetteColor = new Color(0, 0, 0, 1);
lens.chromaticAberration = 0.3;
lens.grainIntensity = 0.15;
// 移除 LensPost 即关闭效果
postProcessing.removePost(LensPost);engine.setting.render.postProcessing.lens 配置参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enable | boolean | false | 是否启用,添加 / 移除 LensPost 时自动设置 |
vignetteIntensity | number | 0.4 | 暗角强度,0 关闭;0.4 为轻微的四角变暗,1 较重 |
vignetteSmoothness | number | 0.4 | 暗角过渡的柔和程度,0(硬边)~ 1(非常平缓) |
vignetteRoundness | number | 1.0 | 1 为圆形暗角(不受宽高比影响),0 跟随屏幕矩形 |
vignetteColor | Color | 黑色 | 四角过渡到的颜色 |
chromaticAberration | number | 0 | 色差强度,0 关闭;0.1 轻微,1 有明显彩边(屏幕左右边缘约错开 5%) |
grainIntensity | number | 0 | 胶片颗粒强度,0 关闭;0.1 轻微,0.5 较重。颗粒与颜色相乘,会随曝光一起缩放 |
grainStatic | boolean | false | 固定颗粒图案,不随帧变化(适合截图、测试) |
下面的示例使用黑白相间的立柱,便于观察画面边缘的色差彩边;天空部分可以看到暗角的过渡与颗粒。
ts
import { AtmosphericComponent, BoxGeometry, CameraUtil, Color, DirectLight, Engine3D, HoverCameraController, KelvinUtil, LensPost, LitMaterial, MeshRenderer, Object3D, PostProcessingComponent, Scene3D, SphereGeometry, View3D } from '@orillusion/core';
import * as dat from 'dat.gui';
/**
* LensPost: vignette, chromatic aberration and film grain.
* High-contrast white / black pillars make the color fringing easy to see toward the screen edges;
* the sky shows the vignette falloff and the grain.
*/
class Sample_Lens {
private scene: Scene3D;
private post: PostProcessingComponent;
private lens: LensPost;
async run() {
const engine = await Engine3D.init({ setting: { shadow: { enable: true, shadowBound: 120 } } });
this.scene = new Scene3D();
this.scene.addComponent(AtmosphericComponent).sunY = 0.65;
const camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(60, engine.aspect, 0.1, 2000);
camera.object3D.addComponent(HoverCameraController).setCamera(30, -18, 70);
const lightObj = new Object3D();
lightObj.rotationX = 50;
lightObj.rotationY = 130;
const light = lightObj.addComponent(DirectLight);
light.lightColor = KelvinUtil.color_temperature_to_rgb(5600);
light.intensity = 3;
light.castShadow = true;
light.enableCSM = true;
this.scene.addChild(lightObj);
this.build();
const view = new View3D();
view.scene = this.scene;
view.camera = camera;
engine.startRenderView(view);
this.post = this.scene.addComponent(PostProcessingComponent);
this.lens = this.post.addPost(LensPost);
this.lens.vignetteIntensity = 0.55;
this.lens.chromaticAberration = 0.3;
this.lens.grainIntensity = 0.15;
this.initGUI();
}
private build() {
const white = new LitMaterial();
white.baseColor = new Color(0.95, 0.95, 0.95);
white.roughness = 0.7;
white.metallic = 0;
const black = new LitMaterial();
black.baseColor = new Color(0.02, 0.02, 0.02);
black.roughness = 0.7;
black.metallic = 0;
const accent = new LitMaterial();
accent.baseColor = new Color(0.9, 0.2, 0.1);
accent.roughness = 0.4;
accent.metallic = 0;
const pillar = new BoxGeometry(3, 1, 3);
for (let x = -7; x <= 7; x++) {
for (let z = -7; z <= 7; z++) {
const h = 4 + ((x * 7 + z * 13) & 7) * 2.2;
const obj = new Object3D();
const mr = obj.addComponent(MeshRenderer);
mr.geometry = pillar;
mr.material = (x + z) & 1 ? white : black;
mr.castShadow = true;
obj.x = x * 8;
obj.z = z * 8;
obj.y = h / 2;
obj.scaleY = h;
this.scene.addChild(obj);
}
}
const ball = new Object3D();
const bm = ball.addComponent(MeshRenderer);
bm.geometry = new SphereGeometry(6, 48, 48);
bm.material = accent;
bm.castShadow = true;
ball.y = 26;
this.scene.addChild(ball);
const floor = new Object3D();
const fm = floor.addComponent(MeshRenderer);
fm.geometry = new BoxGeometry(400, 1, 400);
const mat = new LitMaterial();
mat.baseColor = new Color(0.5, 0.5, 0.5);
mat.roughness = 0.9;
mat.metallic = 0;
fm.material = mat;
fm.castShadow = false;
floor.y = -0.5;
this.scene.addChild(floor);
}
private initGUI() {
const gui = new dat.GUI();
const f = gui.addFolder('LensPost');
// Attaching / detaching the post turns the effect on / off
f.add({ enable: true }, 'enable').onChange((v: boolean) => {
if (v) this.lens = this.post.addPost(LensPost);
else this.post.removePost(LensPost);
});
const l = this.lens;
f.add(l, 'vignetteIntensity', 0, 1.5, 0.01);
f.add(l, 'vignetteSmoothness', 0, 1, 0.01);
f.add(l, 'vignetteRoundness', 0, 1, 0.01);
f.add(l, 'chromaticAberration', 0, 3, 0.01);
f.add(l, 'grainIntensity', 0, 1, 0.01);
f.add(l, 'grainStatic');
f.open();
// Start collapsed so the panel does not cover the scene
gui.close();
}
}
new Sample_Lens().run();
