Skip to content

Lens Effects - LensPost ​

Simulates three effects of a real camera lens in a single fullscreen pass:

  • Vignette: the image darkens toward the corners, drawing the eye to the center;
  • Chromatic aberration: the lens refracts different wavelengths differently, so the red and blue channels shift apart along the radius, leaving colored fringes on high-contrast edges near the border of the image;
  • Film grain: random noise for a film look.

LensPost runs in linear HDR space before tone mapping, so the grain and the vignette go through the same tone mapping curve as the rest of the image.

ts
// Initialize the engine; default parameters can be configured here
let engine = await Engine3D.init({
    setting: {
        render: {
            postProcessing: {
                lens: {
                    vignetteIntensity: 0.55,
                    chromaticAberration: 0.3,
                    grainIntensity: 0.15
                }
            }
        }
    }
});

// Add the post-processing component
let postProcessing = scene.addComponent(PostProcessingComponent);

// Add LensPost
let lens = postProcessing.addPost(LensPost);

// The lens object can be configured too, equivalent to the engine settings above
lens.vignetteIntensity = 0.55;
lens.vignetteColor = new Color(0, 0, 0, 1);
lens.chromaticAberration = 0.3;
lens.grainIntensity = 0.15;

// Removing LensPost turns the effect off
postProcessing.removePost(LensPost);

engine.setting.render.postProcessing.lens parameters:

ParameterTypeDefaultDescription
enablebooleanfalseWhether the effect is on; set automatically when LensPost is added / removed
vignetteIntensitynumber0.4Vignette strength; 0 off, 0.4 a gentle darkening of the corners, 1 heavy
vignetteSmoothnessnumber0.4Falloff softness, 0 (hard edge) ~ 1 (very gradual)
vignetteRoundnessnumber1.01 is a circular vignette regardless of aspect ratio, 0 follows the screen rectangle
vignetteColorColorblackColor the corners fade to
chromaticAberrationnumber0Chromatic aberration strength; 0 off, 0.1 subtle, 1 obvious fringing (about a 5% shift at the left and right edges)
grainIntensitynumber0Film grain strength; 0 off, 0.1 subtle, 0.5 heavy. The grain is multiplicative, so it scales with exposure
grainStaticbooleanfalseFreeze the grain pattern (for screenshots and tests)

The example below uses alternating black and white pillars, which make the color fringing near the edges easy to see; the sky shows the vignette falloff and the grain.

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

Released under the MIT License