Skip to content

镜头效果 - 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 配置参数:

参数类型默认值描述
enablebooleanfalse是否启用,添加 / 移除 LensPost 时自动设置
vignetteIntensitynumber0.4暗角强度,0 关闭;0.4 为轻微的四角变暗,1 较重
vignetteSmoothnessnumber0.4暗角过渡的柔和程度,0(硬边)~ 1(非常平缓)
vignetteRoundnessnumber1.01 为圆形暗角(不受宽高比影响),0 跟随屏幕矩形
vignetteColorColor黑色四角过渡到的颜色
chromaticAberrationnumber0色差强度,0 关闭;0.1 轻微,1 有明显彩边(屏幕左右边缘约错开 5%)
grainIntensitynumber0胶片颗粒强度,0 关闭;0.1 轻微,0.5 较重。颗粒与颜色相乘,会随曝光一起缩放
grainStaticbooleanfalse固定颗粒图案,不随帧变化(适合截图、测试)

下面的示例使用黑白相间的立柱,便于观察画面边缘的色差彩边;天空部分可以看到暗角的过渡与颗粒。

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
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();