Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add LensFlareEffect #695

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions manual/assets/js/src/demos/lens-flare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
CubeTextureLoader,
FogExp2,
IcosahedronGeometry,
LoadingManager,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
SRGBColorSpace,
WebGLRenderer
} from "three";

import {
EffectComposer,
EffectPass,
RenderPass,
LensFlareEffect
} from "postprocessing";

import { Pane } from "tweakpane";
import { SpatialControls } from "spatial-controls";
import { calculateVerticalFoV, FPSMeter } from "../utils";
import * as Domain from "../objects/Domain";

function load() {

const assets = new Map();
const loadingManager = new LoadingManager();
const cubeTextureLoader = new CubeTextureLoader(loadingManager);

const path = document.baseURI + "img/textures/skies/sunset/";
const format = ".png";
const urls = [
path + "px" + format, path + "nx" + format,
path + "py" + format, path + "ny" + format,
path + "pz" + format, path + "nz" + format
];

return new Promise((resolve, reject) => {

loadingManager.onLoad = () => resolve(assets);
loadingManager.onError = (url) => reject(new Error(`Failed to load ${url}`));

cubeTextureLoader.load(urls, (t) => {

t.colorSpace = SRGBColorSpace;
assets.set("sky", t);

});

});

}

window.addEventListener("load", () => load().then((assets) => {

// Renderer

const renderer = new WebGLRenderer({
powerPreference: "high-performance",
antialias: false,
stencil: false,
depth: false
});

renderer.debug.checkShaderErrors = (window.location.hostname === "localhost");
const container = document.querySelector(".viewport");
container.prepend(renderer.domElement);

// Camera & Controls

const camera = new PerspectiveCamera();
const controls = new SpatialControls(camera.position, camera.quaternion, renderer.domElement);
const settings = controls.settings;
settings.rotation.sensitivity = 2.2;
settings.rotation.damping = 0.05;
settings.translation.damping = 0.1;
controls.position.set(-1, -0.3, -30);
controls.lookAt(0, 0, -35);

// Scene, Lights, Objects

const scene = new Scene();
scene.fog = new FogExp2(0x373134, 0.06);
scene.background = assets.get("sky");
scene.add(Domain.createLights());
scene.add(Domain.createEnvironment(scene.background));
scene.add(Domain.createActors(scene.background));

const sun = new Mesh(
new IcosahedronGeometry(1, 3),
new MeshBasicMaterial({
color: 0xffddaa,
transparent: true,
fog: false
})
);

sun.position.set(0, 0.06, -1).multiplyScalar(1000);
sun.scale.setScalar(40);
sun.updateMatrix();
sun.frustumCulled = false;

// Post Processing

const composer = new EffectComposer(renderer, {
multisampling: Math.min(4, renderer.capabilities.maxSamples)
});

const effect = new LensFlareEffect(scene, camera, {
intensity: 1.0
});

const effectPass = new EffectPass(camera, effect);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(effectPass);

// Settings

const fpsMeter = new FPSMeter();
const featuresMaterial = effect.featuresPass.fullscreenMaterial;
const downsampleMaterial = effect.downsamplePass.fullscreenMaterial;
const pane = new Pane({ container: container.querySelector(".tp") });
pane.addBinding(fpsMeter, "fps", { readonly: true, label: "FPS" });

const folder = pane.addFolder({ title: "Settings" });
folder.addBinding(effect, "intensity", { min: 0, max: 10, step: 0.01 });
folder.addBinding(featuresMaterial, "ghostAmount", { min: 0, max: 1, step: 1e-3 });
folder.addBinding(featuresMaterial, "haloAmount", { min: 0, max: 1, step: 1e-3 });
folder.addBinding(featuresMaterial, "chromaticAberration", { min: 0, max: 20, step: 0.1 });

let subfolder = folder.addFolder({ title: "Luminance Filter" });
subfolder.addBinding(downsampleMaterial, "luminanceThreshold", { min: 0, max: 1, step: 0.01 });
subfolder.addBinding(downsampleMaterial, "luminanceSmoothing", { min: 0, max: 1, step: 0.01 });

// Resize Handler

function onResize() {

const width = container.clientWidth, height = container.clientHeight;
camera.aspect = width / height;
camera.fov = calculateVerticalFoV(90, Math.max(camera.aspect, 16 / 9));
camera.updateProjectionMatrix();
composer.setSize(width, height);

}

window.addEventListener("resize", onResize);
onResize();

// Render Loop

requestAnimationFrame(function render(timestamp) {

fpsMeter.update(timestamp);
controls.update(timestamp);
composer.render();
requestAnimationFrame(render);

});

}));
15 changes: 15 additions & 0 deletions manual/content/demos/light-shadow/lens-flare.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
layout: single
collection: sections
title: Lens Flare
draft: false
menu:
demos:
parent: light-shadow
weight: 100
script: lens-flare
---

# Lens Flare

### External Resources
214 changes: 214 additions & 0 deletions src/effects/LensFlareEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { SRGBColorSpace, Uniform, WebGLRenderTarget } from "three";
import { Resolution } from "../core/Resolution.js";
import { BlendFunction } from "../enums/BlendFunction.js";
import { EffectAttribute } from "../enums/EffectAttribute.js";
import { KernelSize } from "../enums/KernelSize.js";
import { DownsampleThresholdMaterial } from "../materials/DownsampleThresholdMaterial.js";
import { LensFlareFeaturesMaterial } from "../materials/LensFlareFeaturesMaterial.js";
import { KawaseBlurPass } from "../passes/KawaseBlurPass.js";
import { MipmapBlurPass } from "../passes/MipmapBlurPass.js";
import { ShaderPass } from "../passes/ShaderPass.js";
import { Effect } from "./Effect.js";

import fragmentShader from "./glsl/lens-flare.frag";

/**
* A lens flare effect.
*
* Based on https://www.froyok.fr/blog/2021-09-ue4-custom-lens-flare/
*/

export class LensFlareEffect extends Effect {

/**
* Constructs a new lens flare effect.
*
* @param {Object} [options] - The options.
* @param {Number} [options.intensity] - The intensity of the lens flare.
*/

constructor({
blendFunction = BlendFunction.SRC,
intensity = 1.0,
resolutionScale = 0.5,
width = Resolution.AUTO_SIZE,
height = Resolution.AUTO_SIZE,
resolutionX = width,
resolutionY = height
} = {}) {

super("LensFlareEffect", fragmentShader, {
blendFunction,
attributes: EffectAttribute.CONVOLUTION,
uniforms: new Map([
["bloomBuffer", new Uniform(null)],
["featuresBuffer", new Uniform(null)],
["intensity", new Uniform(intensity)]
])
});

/**
* A render target for intermediate results.
*
* @type {WebGLRenderTarget}
* @private
*/

this.renderTarget1 = new WebGLRenderTarget(1, 1, { depthBuffer: false });
this.renderTarget1.texture.name = "LensFlare.Target1";

/**
* A render target for intermediate results.
*
* @type {WebGLRenderTarget}
* @private
*/

this.renderTarget2 = new WebGLRenderTarget(1, 1, { depthBuffer: false });
this.renderTarget2.texture.name = "LensFlare.Target2";

/**
* A downsample threshold pass.
*
* @type {ShaderPass}
* @readonly
*/

const downsampleMaterial = new DownsampleThresholdMaterial();
this.downsamplePass = new ShaderPass(downsampleMaterial);

/**
* This pass blurs the input buffer to create non-starburst glare (bloom).
*
* @type {MipmapBlurPass}
* @readonly
*/

this.blurPass = new MipmapBlurPass();
this.blurPass.levels = 8;
this.uniforms.get("bloomBuffer").value = this.blurPass.texture;

/**
* This pass blurs the input buffer of the lens flare features.
*
* @type {KawaseBlurPass}
* @readonly
*/

this.featuresBlurPass = new KawaseBlurPass({ kernelSize: KernelSize.SMALL });
this.uniforms.get("featuresBuffer").value = this.renderTarget1.texture;

/**
* A lens flare features pass.
*
* @type {ShaderPass}
* @readonly
*/

const featuresMaterial = new LensFlareFeaturesMaterial();
this.featuresPass = new ShaderPass(featuresMaterial);

/**
* The render resolution.
*
* @type {Resolution}
* @readonly
*/

const resolution = this.resolution = new Resolution(this, resolutionX, resolutionY, resolutionScale);
resolution.addEventListener("change", (e) => this.setSize(resolution.baseWidth, resolution.baseHeight));

}

/**
* The intensity of the lens flare.
*
* @type {Number}
*/

get intensity() {

return this.uniforms.get("intensity").value;

}

set intensity(value) {

this.uniforms.get("intensity").value = value;

}

/**
* Updates this effect.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} inputBuffer - A frame buffer that contains the result of the previous pass.
* @param {Number} [deltaTime] - The time between the last frame and the current one in seconds.
*/

update(renderer, inputBuffer, deltaTime) {

const renderTarget1 = this.renderTarget1;
const renderTarget2 = this.renderTarget2;

this.downsamplePass.render(renderer, inputBuffer, renderTarget1);
this.blurPass.render(renderer, renderTarget1, null);
this.featuresBlurPass.render(renderer, renderTarget1, renderTarget2);
this.featuresPass.render(renderer, renderTarget2, renderTarget1);

}

/**
* Updates the size of internal render targets.
*
* @param {Number} width - The width.
* @param {Number} height - The height.
*/

setSize(width, height) {

const resolution = this.resolution;
resolution.setBaseSize(width, height);
const w = resolution.width, h = resolution.height;

this.renderTarget1.setSize(w, h);
this.renderTarget2.setSize(w, h);
this.downsamplePass.fullscreenMaterial.setSize(w, h);
this.blurPass.setSize(w, h);
this.featuresBlurPass.setSize(w, h);
this.featuresPass.fullscreenMaterial.setSize(w, h);

}

/**
* Performs initialization tasks.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {Boolean} alpha - Whether the renderer uses the alpha channel or not.
* @param {Number} frameBufferType - The type of the main frame buffers.
*/

initialize(renderer, alpha, frameBufferType) {

this.downsamplePass.initialize(renderer, alpha, frameBufferType);
this.blurPass.initialize(renderer, alpha, frameBufferType);
this.featuresBlurPass.initialize(renderer, alpha, frameBufferType);
this.featuresPass.initialize(renderer, alpha, frameBufferType);

if(frameBufferType !== undefined) {

this.renderTarget1.texture.type = frameBufferType;
this.renderTarget2.texture.type = frameBufferType;

if(renderer !== null && renderer.outputColorSpace === SRGBColorSpace) {

this.renderTarget1.texture.colorSpace = SRGBColorSpace;
this.renderTarget2.texture.colorSpace = SRGBColorSpace;

}

}

}

}
Loading