diff --git a/manual/assets/js/src/demos/lens-flare.js b/manual/assets/js/src/demos/lens-flare.js new file mode 100644 index 000000000..f9652c6d6 --- /dev/null +++ b/manual/assets/js/src/demos/lens-flare.js @@ -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); + + }); + +})); diff --git a/manual/content/demos/light-shadow/lens-flare.en.md b/manual/content/demos/light-shadow/lens-flare.en.md new file mode 100644 index 000000000..b452afb88 --- /dev/null +++ b/manual/content/demos/light-shadow/lens-flare.en.md @@ -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 diff --git a/src/effects/LensFlareEffect.js b/src/effects/LensFlareEffect.js new file mode 100644 index 000000000..c49efc50e --- /dev/null +++ b/src/effects/LensFlareEffect.js @@ -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; + + } + + } + + } + +} diff --git a/src/effects/glsl/lens-flare.frag b/src/effects/glsl/lens-flare.frag new file mode 100644 index 000000000..575adf945 --- /dev/null +++ b/src/effects/glsl/lens-flare.frag @@ -0,0 +1,21 @@ +#ifdef FRAMEBUFFER_PRECISION_HIGH + + uniform mediump sampler2D bloomBuffer; + uniform mediump sampler2D featuresBuffer; + +#else + + uniform lowp sampler2D bloomBuffer; + uniform lowp sampler2D featuresBuffer; + +#endif + +uniform float intensity; + +void mainImage(const vec4 inputColor, const vec2 uv, out vec4 outputColor) { + + vec3 bloom = texture(bloomBuffer, uv).rgb; + vec3 features = texture(featuresBuffer, uv).rgb; + outputColor = vec4(inputColor.rgb + (bloom + features) * intensity, inputColor.a); + +} diff --git a/src/effects/index.js b/src/effects/index.js index aceebb1a5..46a7f10ee 100644 --- a/src/effects/index.js +++ b/src/effects/index.js @@ -18,6 +18,7 @@ export * from "./GodRaysEffect.js"; export * from "./GridEffect.js"; export * from "./HueSaturationEffect.js"; export * from "./LensDistortionEffect.js"; +export * from "./LensFlareEffect.js"; export * from "./LUT1DEffect.js"; export * from "./LUT3DEffect.js"; export * from "./NoiseEffect.js"; diff --git a/src/materials/DownsampleThresholdMaterial.js b/src/materials/DownsampleThresholdMaterial.js new file mode 100644 index 000000000..42bf1a679 --- /dev/null +++ b/src/materials/DownsampleThresholdMaterial.js @@ -0,0 +1,105 @@ +import { NoBlending, ShaderMaterial, Uniform, Vector2 } from "three"; + +import fragmentShader from "./glsl/downsample-threshold.frag"; +import vertexShader from "./glsl/downsample-threshold.vert"; + +/** + * A downsample threshold material. + * + * This down-samples the input buffer while applying threshold. + * Based on the article: + * https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom + * which refers to a presentation by Sledgehammer Games: + * https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/ + * + * @implements {Resizable} + */ + +export class DownsampleThresholdMaterial extends ShaderMaterial { + + /** + * Constructs a new downsample threshold material. + */ + + constructor() { + + super({ + name: "DownsampleThresholdMaterial", + uniforms: { + inputBuffer: new Uniform(null), + texelSize: new Uniform(new Vector2()), + luminanceThreshold: new Uniform(0.5), + luminanceSmoothing: new Uniform(0.1) + }, + blending: NoBlending, + toneMapped: false, + depthWrite: false, + depthTest: false, + fragmentShader, + vertexShader + }); + + } + + /** + * The input buffer. + * + * @type {Texture} + */ + + set inputBuffer(value) { + + this.uniforms.inputBuffer.value = value; + + } + + /** + * Sets the size of this object. + * + * @param {Number} width - The width. + * @param {Number} height - The height. + */ + + setSize(width, height) { + + this.uniforms.texelSize.value.set(1.0 / width, 1.0 / height); + + } + + /** + * The luminance threshold. + * + * @type {Number} + */ + + get luminanceThreshold() { + + return this.uniforms.luminanceThreshold.value; + + } + + set luminanceThreshold(value) { + + this.uniforms.luminanceThreshold.value = value; + + } + + /** + * The luminance threshold smoothing. + * + * @type {Number} + */ + + get luminanceSmoothing() { + + return this.uniforms.luminanceSmoothing.value; + + } + + set luminanceSmoothing(value) { + + this.uniforms.luminanceSmoothing.value = value; + + } + +} diff --git a/src/materials/LensFlareFeaturesMaterial.js b/src/materials/LensFlareFeaturesMaterial.js new file mode 100644 index 000000000..0cf790fc2 --- /dev/null +++ b/src/materials/LensFlareFeaturesMaterial.js @@ -0,0 +1,119 @@ +import { NoBlending, ShaderMaterial, Uniform, Vector2 } from "three"; + +import fragmentShader from "./glsl/lens-flare-features.frag"; +import vertexShader from "./glsl/lens-flare-features.vert"; + +/** + * A lens flare features material. + * + * @implements {Resizable} + */ + +export class LensFlareFeaturesMaterial extends ShaderMaterial { + + /** + * Constructs a new lens flare features material. + */ + + constructor() { + + super({ + name: "LensFlareFeaturesMaterial", + uniforms: { + inputBuffer: new Uniform(null), + texelSize: new Uniform(new Vector2()), + ghostAmount: new Uniform(0.1), + haloAmount: new Uniform(0.1), + chromaticAberration: new Uniform(10) + }, + blending: NoBlending, + toneMapped: false, + depthWrite: false, + depthTest: false, + fragmentShader, + vertexShader + }); + + } + + /** + * The input buffer. + * + * @type {Texture} + */ + + set inputBuffer(value) { + + this.uniforms.inputBuffer.value = value; + + } + + /** + * Sets the size of this object. + * + * @param {Number} width - The width. + * @param {Number} height - The height. + */ + + setSize(width, height) { + + this.uniforms.texelSize.value.set(1.0 / width, 1.0 / height); + + } + + /** + * The amount of ghosts. + * + * @type {Number} + */ + + get ghostAmount() { + + return this.uniforms.ghostAmount.value; + + } + + set ghostAmount(value) { + + this.uniforms.ghostAmount.value = value; + + } + + /** + * The amount of halos. + * + * @type {Number} + */ + + get haloAmount() { + + return this.uniforms.haloAmount.value; + + } + + set haloAmount(value) { + + this.uniforms.haloAmount.value = value; + + } + + + /** + * The offset of chromatic aberration. + * + * @type {Number} + */ + + get chromaticAberration() { + + return this.uniforms.chromaticAberration.value; + + } + + set chromaticAberration(value) { + + this.uniforms.chromaticAberration.value = value; + + } + +} diff --git a/src/materials/glsl/downsample-threshold.frag b/src/materials/glsl/downsample-threshold.frag new file mode 100644 index 000000000..1dfdea791 --- /dev/null +++ b/src/materials/glsl/downsample-threshold.frag @@ -0,0 +1,86 @@ +#include + +#ifdef FRAMEBUFFER_PRECISION_HIGH + + uniform mediump sampler2D inputBuffer; + +#else + + uniform lowp sampler2D inputBuffer; + +#endif + +uniform float luminanceThreshold; +uniform float luminanceSmoothing; + +varying vec2 vCenterUv1; +varying vec2 vCenterUv2; +varying vec2 vCenterUv3; +varying vec2 vCenterUv4; +varying vec2 vRowUv1; +varying vec2 vRowUv2; +varying vec2 vRowUv3; +varying vec2 vRowUv4; +varying vec2 vRowUv5; +varying vec2 vRowUv6; +varying vec2 vRowUv7; +varying vec2 vRowUv8; +varying vec2 vRowUv9; + +float clampToBorder(const vec2 uv) { + + return float(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0); + +} + +// Reference: https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom +void main() { + + vec4 color = 0.125 * texture2D(inputBuffer, vec2(vRowUv5)); + + vec4 weight = + 0.03125 * + vec4( + clampToBorder(vRowUv1), + clampToBorder(vRowUv3), + clampToBorder(vRowUv7), + clampToBorder(vRowUv9) + ); + color += weight.x * texture2D(inputBuffer, vec2(vRowUv1)); + color += weight.y * texture2D(inputBuffer, vec2(vRowUv3)); + color += weight.z * texture2D(inputBuffer, vec2(vRowUv7)); + color += weight.w * texture2D(inputBuffer, vec2(vRowUv9)); + + weight = + 0.0625 * + vec4( + clampToBorder(vRowUv2), + clampToBorder(vRowUv4), + clampToBorder(vRowUv6), + clampToBorder(vRowUv8) + ); + color += weight.x * texture2D(inputBuffer, vec2(vRowUv2)); + color += weight.y * texture2D(inputBuffer, vec2(vRowUv4)); + color += weight.z * texture2D(inputBuffer, vec2(vRowUv6)); + color += weight.w * texture2D(inputBuffer, vec2(vRowUv8)); + + weight = + 0.125 * + vec4( + clampToBorder(vRowUv2), + clampToBorder(vRowUv4), + clampToBorder(vRowUv6), + clampToBorder(vRowUv8) + ); + color += weight.x * texture2D(inputBuffer, vec2(vCenterUv1)); + color += weight.y * texture2D(inputBuffer, vec2(vCenterUv2)); + color += weight.z * texture2D(inputBuffer, vec2(vCenterUv3)); + color += weight.w * texture2D(inputBuffer, vec2(vCenterUv4)); + + float l = luminance(color.rgb); + float scale = saturate(smoothstep(luminanceThreshold, luminanceThreshold + luminanceSmoothing, l)); + gl_FragColor = color * scale; + + #include + +} diff --git a/src/materials/glsl/downsample-threshold.vert b/src/materials/glsl/downsample-threshold.vert new file mode 100644 index 000000000..389c23895 --- /dev/null +++ b/src/materials/glsl/downsample-threshold.vert @@ -0,0 +1,38 @@ +uniform vec2 texelSize; + +varying vec2 vCenterUv1; +varying vec2 vCenterUv2; +varying vec2 vCenterUv3; +varying vec2 vCenterUv4; +varying vec2 vRowUv1; +varying vec2 vRowUv2; +varying vec2 vRowUv3; +varying vec2 vRowUv4; +varying vec2 vRowUv5; +varying vec2 vRowUv6; +varying vec2 vRowUv7; +varying vec2 vRowUv8; +varying vec2 vRowUv9; + +void main() { + + vec2 uv = position.xy * 0.5 + 0.5; + + vCenterUv1 = uv + texelSize * vec2(-1.0, 1.0); + vCenterUv2 = uv + texelSize; + vCenterUv3 = uv + texelSize * vec2(-1.0); + vCenterUv4 = uv + texelSize * vec2(1.0, -1.0); + + vRowUv1 = uv + texelSize * vec2(-2.0, 2.0); + vRowUv2 = uv + texelSize * vec2(0.0, 2.0); + vRowUv3 = uv + texelSize * vec2(2.0); + vRowUv4 = uv + texelSize * vec2(-2.0, 0.0); + vRowUv5 = uv + texelSize; + vRowUv6 = uv + texelSize * vec2(2.0, 0.0); + vRowUv7 = uv + texelSize * vec2(-2.0); + vRowUv8 = uv + texelSize * vec2(0.0, -2.0); + vRowUv9 = uv + texelSize * vec2(2.0, -2.0); + + gl_Position = vec4(position.xy, 1.0, 1.0); + +} diff --git a/src/materials/glsl/lens-flare-features.frag b/src/materials/glsl/lens-flare-features.frag new file mode 100644 index 000000000..9c701d1a2 --- /dev/null +++ b/src/materials/glsl/lens-flare-features.frag @@ -0,0 +1,97 @@ +#include + +#ifdef FRAMEBUFFER_PRECISION_HIGH + + uniform mediump sampler2D inputBuffer; + +#else + + uniform lowp sampler2D inputBuffer; + +#endif + +#define SQRT_2 (0.7071067811865476) + +uniform vec2 texelSize; +uniform float ghostAmount; +uniform float haloAmount; +uniform float chromaticAberration; + +varying vec2 vUv; +varying vec2 vAspectRatio; + +vec3 sampleGhost(const vec2 direction, const vec3 color, const float offset) { + + vec2 suv = clamp(1.0 - vUv + direction * offset, 0.0, 1.0); + vec3 result = texture(inputBuffer, suv).rgb * color; + + // Falloff at the perimeter. + float d = clamp(length(0.5 - suv) / (0.5 * SQRT_2), 0.0, 1.0); + result *= pow(1.0 - d, 3.0); + return result; + +} + +vec4 sampleGhosts(float amount) { + + vec3 color = vec3(0.0); + vec2 direction = vUv - 0.5; + + color += sampleGhost(direction, vec3(0.8, 0.8, 1.0), -5.0); + color += sampleGhost(direction, vec3(1.0, 0.8, 0.4), -1.5); + color += sampleGhost(direction, vec3(0.9, 1.0, 0.8), -0.4); + color += sampleGhost(direction, vec3(1.0, 0.8, 0.4), -0.2); + color += sampleGhost(direction, vec3(0.9, 0.7, 0.7), -0.1); + color += sampleGhost(direction, vec3(0.5, 1.0, 0.4), 0.7); + color += sampleGhost(direction, vec3(0.5, 0.5, 0.5), 1.0); + color += sampleGhost(direction, vec3(1.0, 1.0, 0.6), 2.5); + color += sampleGhost(direction, vec3(0.5, 0.8, 1.0), 10.0); + + return vec4(color * amount, 1.0); + +} + +// Reference: https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html +float cubicRingMask(const float x, const float radius, const float thickness) { + + float v = min(abs(x - radius) / thickness, 1.0); + return 1.0 - v * v * (3.0 - 2.0 * v); + +} + +vec3 sampleHalo(const float radius) { + + vec2 direction = normalize((vUv - 0.5) / vAspectRatio) * vAspectRatio; + vec3 offset = vec3(texelSize.x * chromaticAberration) * vec3(-1.0, 0.0, 1.0); + vec2 suv = fract(1.0 - vUv + direction * radius); + vec3 result = vec3( + texture(inputBuffer, suv + direction * offset.r).r, + texture(inputBuffer, suv + direction * offset.g).g, + texture(inputBuffer, suv + direction * offset.b).b + ); + + // Falloff at the center and perimeter. + vec2 wuv = (vUv - vec2(0.5, 0.0)) / vAspectRatio + vec2(0.5, 0.0); + float d = saturate(distance(wuv, vec2(0.5))); + result *= cubicRingMask(d, 0.45, 0.25); + return result; + +} + +vec4 sampleHalos(const float amount) { + + vec3 color = vec3(0.0); + color += sampleHalo(0.3); + return vec4(color, 1.0) * amount; + +} + +void main() { + + vec4 color = vec4(0.0); + color += sampleGhosts(ghostAmount); + color += sampleHalos(haloAmount); + gl_FragColor = color; + +} + diff --git a/src/materials/glsl/lens-flare-features.vert b/src/materials/glsl/lens-flare-features.vert new file mode 100644 index 000000000..6b0fa8ea2 --- /dev/null +++ b/src/materials/glsl/lens-flare-features.vert @@ -0,0 +1,13 @@ +uniform vec2 texelSize; + +varying vec2 vUv; +varying vec2 vAspectRatio; + +void main() { + + vUv = position.xy * 0.5 + 0.5; + vAspectRatio = vec2(texelSize.x / texelSize.y, 1.0); + + gl_Position = vec4(position.xy, 1.0, 1.0); + +}