You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lightdm-webkit-theme-osmos/src/utils/postprocess.ts

355 lines
15 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import GL from './gl'
interface RenderTarget {
fbo: WebGLFramebuffer
texture: WebGLTexture
}
// ─── Shared pass vertex shader ───────────────────────────────────────────────
const PASS_VERT = `#version 300 es
in vec2 aPosition;
out vec2 vUv;
void main() {
vUv = aPosition * 0.5 + 0.5;
gl_Position = vec4(aPosition, 0.0, 1.0);
}`
// ─── Separable Gaussian blur ─────────────────────────────────────────────────
// Call twice (horizontal then vertical) for a full 2D Gaussian blur.
// uDirection = (1,0) for horizontal, (0,1) for vertical.
const BLUR_FRAG = `#version 300 es
precision highp float;
uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform vec2 uDirection;
uniform float uRadius;
// Step multiplier: sample every N pixels instead of every 1.
// Higher values cover a wider area with the same 17 taps — key for wide diffuse bloom.
uniform float uStepMult;
in vec2 vUv;
out vec4 fragColor;
void main() {
vec2 step = uDirection / uResolution * uStepMult;
vec4 color = vec4(0.0);
float total = 0.0;
for (float i = -8.0; i <= 8.0; i++) {
float w = exp(-i * i / (2.0 * uRadius * uRadius));
color += texture(uTexture, vUv + step * i) * w;
total += w;
}
fragColor = color / total;
}`
// ─── Brightness extraction ────────────────────────────────────────────────────
// Keeps only pixels whose luminance exceeds uThreshold; everything else goes black.
// Used before the wide bloom pass so scattering only originates from bright light sources.
const EXTRACT_FRAG = `#version 300 es
precision highp float;
uniform sampler2D uTexture;
uniform float uThreshold;
in vec2 vUv;
out vec4 fragColor;
void main() {
vec4 c = texture(uTexture, vUv);
float lum = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));
float bright = max(0.0, lum - uThreshold) / max(1.0 - uThreshold, 0.001);
fragColor = vec4(c.rgb * bright, 1.0);
}`
// ─── Pixelation ──────────────────────────────────────────────────────────────
// Quantises screen coordinates to uPixelSize×uPixelSize blocks, sampling from
// the block centre — gives a retro pixel-art look applied before bloom.
const PIXELATE_FRAG = `#version 300 es
precision highp float;
uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform float uPixelSize; // block size in screen pixels (1 = no effect)
in vec2 vUv;
out vec4 fragColor;
void main() {
vec2 block = floor(gl_FragCoord.xy / uPixelSize) * uPixelSize + uPixelSize * 0.5;
fragColor = texture(uTexture, block / uResolution);
}`
// ─── Dual-bloom composite ─────────────────────────────────────────────────────
// Combines the original render with two independent bloom layers:
// bloom1 — bright tight bloom (enhances perceived saturation / intensity)
// bloom2 — wide dim bloom (simulates lens scattering / atmospheric glow)
const COMPOSITE_FRAG = `#version 300 es
precision highp float;
uniform sampler2D uBase;
uniform sampler2D uBloom1; // bright tight bloom
uniform sampler2D uBloom2; // wide dim bloom
uniform float uIntensity1;
uniform float uIntensity2;
in vec2 vUv;
out vec4 fragColor;
void main() {
vec4 base = texture(uBase, vUv);
vec4 bloom1 = texture(uBloom1, vUv);
vec4 bloom2 = texture(uBloom2, vUv);
fragColor = clamp(base + bloom1 * uIntensity1 + bloom2 * uIntensity2, 0.0, 1.0);
}`
const QUAD = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1])
export interface PostProcessOptions {
// Tight bloom — bright, few passes, small radius
// Bloom layer A
passesA?: number
radiusA?: number
intensityA?: number
stepMultA?: number // sample stride (1 = every pixel, 3 = every 3rd pixel → 3× wider spread)
// Bloom layer B — optionally extract bright pixels first for true scattering effect
passesB?: number
radiusB?: number
intensityB?: number
stepMultB?: number
thresholdB?: number // luminance threshold before blur: 0 = blur everything, 0.5 = only bright pixels
// Pixelation applied before bloom — 1 = off, higher = larger pixel blocks
pixelSize?: number
}
export default class PostProcessGL extends GL {
private _mainTarget!: RenderTarget
private _pingTarget!: RenderTarget
private _pongTarget!: RenderTarget
private _extraTarget!: RenderTarget // holds one bloom result while the other is computed
private _blurProgram!: WebGLProgram
private _extractProgram!: WebGLProgram
private _pixelateProgram!: WebGLProgram
private _compositeProgram!: WebGLProgram
private _quadBuffer!: WebGLBuffer
private _extractTarget!: RenderTarget
private _pixelateTarget!: RenderTarget
ppOptions: Required<PostProcessOptions>
ppEnabled: boolean
constructor(
el: HTMLCanvasElement,
vertexShader: string,
fragmentShader: string,
width: number,
height: number,
glOptions?: ConstructorParameters<typeof GL>[5],
ppOptions: PostProcessOptions = {}
) {
super(el, vertexShader, fragmentShader, width, height, glOptions)
this.ppEnabled = Object.keys(ppOptions).length > 0
this.ppOptions = {
passesA: 1, radiusA: 5, intensityA: 1.2, stepMultA: 1,
passesB: 1, radiusB: 0.5, intensityB: 1.2, stepMultB: 1, thresholdB: 0,
pixelSize: 1,
...ppOptions
}
this._initPostProcess()
}
// ─── Setup ──────────────────────────────────────────────────────────────────
private _createTarget(w: number, h: number): RenderTarget {
const gl = this.ctx
const texture = gl.createTexture()!
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
const fbo = gl.createFramebuffer()!
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.bindTexture(gl.TEXTURE_2D, null)
return { fbo, texture }
}
private _compilePassProgram(fragSrc: string): WebGLProgram {
const gl = this.ctx
const compile = (type: number, src: string) => {
const s = gl.createShader(type)!
gl.shaderSource(s, src)
gl.compileShader(s)
return s
}
const prog = gl.createProgram()!
gl.attachShader(prog, compile(gl.VERTEX_SHADER, PASS_VERT))
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc))
gl.linkProgram(prog)
return prog
}
private _initPostProcess() {
const w = this.el.width
const h = this.el.height
this._mainTarget = this._createTarget(w, h)
this._pingTarget = this._createTarget(w, h)
this._pongTarget = this._createTarget(w, h)
this._extraTarget = this._createTarget(w, h)
this._extractTarget = this._createTarget(w, h)
this._pixelateTarget = this._createTarget(w, h)
this._blurProgram = this._compilePassProgram(BLUR_FRAG)
this._extractProgram = this._compilePassProgram(EXTRACT_FRAG)
this._pixelateProgram = this._compilePassProgram(PIXELATE_FRAG)
this._compositeProgram = this._compilePassProgram(COMPOSITE_FRAG)
this._quadBuffer = this.ctx.createBuffer()!
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this._quadBuffer)
this.ctx.bufferData(this.ctx.ARRAY_BUFFER, QUAD, this.ctx.STATIC_DRAW)
}
// ─── Blur helpers ────────────────────────────────────────────────────────────
private _bindQuad(program: WebGLProgram) {
const gl = this.ctx
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadBuffer)
const loc = gl.getAttribLocation(program, 'aPosition')
gl.enableVertexAttribArray(loc)
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0)
}
private _blurPass(src: RenderTarget, dst: RenderTarget, dir: [number, number], radius: number, stepMult = 1) {
const gl = this.ctx
gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo)
gl.useProgram(this._blurProgram)
this._bindQuad(this._blurProgram)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, src.texture)
gl.uniform1i(gl.getUniformLocation(this._blurProgram, 'uTexture'), 0)
gl.uniform2f(gl.getUniformLocation(this._blurProgram, 'uResolution'), this.el.width, this.el.height)
gl.uniform2f(gl.getUniformLocation(this._blurProgram, 'uDirection'), dir[0], dir[1])
gl.uniform1f(gl.getUniformLocation(this._blurProgram, 'uRadius'), radius)
gl.uniform1f(gl.getUniformLocation(this._blurProgram, 'uStepMult'), stepMult)
gl.drawArrays(gl.TRIANGLES, 0, 6)
}
private _runBlur(src: RenderTarget, passes: number, radius: number, useExtra: boolean, stepMult = 1): RenderTarget {
const a = this._pingTarget
const b = useExtra ? this._extraTarget : this._pongTarget
let cur: RenderTarget = src
let scratch: [RenderTarget, RenderTarget] = [a, b]
let idx = 0
for (let i = 0; i < passes; i++) {
const dst0 = scratch[idx % 2]!
const dst1 = scratch[(idx + 1) % 2]!
this._blurPass(cur, dst0, [1, 0], radius, stepMult)
this._blurPass(dst0, dst1, [0, 1], radius, stepMult)
cur = dst1
idx += 2
}
return cur
}
private _extractPass(src: RenderTarget, threshold: number) {
const gl = this.ctx
gl.bindFramebuffer(gl.FRAMEBUFFER, this._extractTarget.fbo)
gl.useProgram(this._extractProgram)
this._bindQuad(this._extractProgram)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, src.texture)
gl.uniform1i(gl.getUniformLocation(this._extractProgram, 'uTexture'), 0)
gl.uniform1f(gl.getUniformLocation(this._extractProgram, 'uThreshold'), threshold)
gl.drawArrays(gl.TRIANGLES, 0, 6)
}
private _pixelatePass(src: RenderTarget) {
const gl = this.ctx
gl.bindFramebuffer(gl.FRAMEBUFFER, this._pixelateTarget.fbo)
gl.useProgram(this._pixelateProgram)
this._bindQuad(this._pixelateProgram)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, src.texture)
gl.uniform1i(gl.getUniformLocation(this._pixelateProgram, 'uTexture'), 0)
gl.uniform2f(gl.getUniformLocation(this._pixelateProgram, 'uResolution'), this.el.width, this.el.height)
gl.uniform1f(gl.getUniformLocation(this._pixelateProgram, 'uPixelSize'), this.ppOptions.pixelSize)
gl.drawArrays(gl.TRIANGLES, 0, 6)
}
// ─── Resize ─────────────────────────────────────────────────────────────────
override resize() {
super.resize()
if (!this._mainTarget) return
const w = this.el.width
const h = this.el.height
for (const t of [this._mainTarget, this._pingTarget, this._pongTarget, this._extraTarget, this._extractTarget, this._pixelateTarget]) {
this.ctx.bindTexture(this.ctx.TEXTURE_2D, t.texture)
this.ctx.texImage2D(this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, w, h, 0, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, null)
}
this.ctx.bindTexture(this.ctx.TEXTURE_2D, null)
}
// ─── Render ─────────────────────────────────────────────────────────────────
override render() {
if (!this.running) return
requestAnimationFrame(this.render.bind(this))
const gl = this.ctx
// Bypass: render directly to screen without any post-processing
if (!this.ppEnabled) {
gl.useProgram(this.program)
this.initBuffers(this.positions)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.viewport(0, 0, this.el.width, this.el.height)
if (!this.renderOptions.externalTimeUse) {
this.time = (Date.now() - this.startTime) / 1000
gl.uniform1f(this.programInfo.uniforms.time, this.time)
}
if (this.renderHook) this.renderHook()
gl.drawArrays(gl.TRIANGLES, 0, 6)
return
}
const { passesA, radiusA, intensityA, stepMultA, passesB, radiusB, intensityB, stepMultB, thresholdB, pixelSize } = this.ppOptions
// Pass 1: render main shader to offscreen texture
gl.useProgram(this.program)
this.initBuffers(this.positions)
gl.bindFramebuffer(gl.FRAMEBUFFER, this._mainTarget.fbo)
gl.viewport(0, 0, this.el.width, this.el.height)
if (!this.renderOptions.externalTimeUse) {
this.time = (Date.now() - this.startTime) / 1000
gl.uniform1f(this.programInfo.uniforms.time, this.time)
}
if (this.renderHook) this.renderHook()
gl.drawArrays(gl.TRIANGLES, 0, 6)
// Optional pixelation applied before bloom so the effect covers both base and glow
const renderBase = pixelSize > 1
? (this._pixelatePass(this._mainTarget), this._pixelateTarget)
: this._mainTarget
const bloomA = this._runBlur(renderBase, passesA, radiusA, false, stepMultA)
// If thresholdB > 0: extract bright pixels first so bloom B scatters only from light sources
const bloomBSrc = thresholdB > 0
? (this._extractPass(this._mainTarget, thresholdB), this._extractTarget)
: this._mainTarget
const bloomB = this._runBlur(bloomBSrc, passesB, radiusB, true, stepMultB)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.useProgram(this._compositeProgram)
this._bindQuad(this._compositeProgram)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, renderBase.texture)
gl.uniform1i(gl.getUniformLocation(this._compositeProgram, 'uBase'), 0)
gl.activeTexture(gl.TEXTURE1)
gl.bindTexture(gl.TEXTURE_2D, bloomA.texture)
gl.uniform1i(gl.getUniformLocation(this._compositeProgram, 'uBloom1'), 1)
gl.activeTexture(gl.TEXTURE2)
gl.bindTexture(gl.TEXTURE_2D, bloomB.texture)
gl.uniform1i(gl.getUniformLocation(this._compositeProgram, 'uBloom2'), 2)
gl.uniform1f(gl.getUniformLocation(this._compositeProgram, 'uIntensity1'), intensityA)
gl.uniform1f(gl.getUniformLocation(this._compositeProgram, 'uIntensity2'), intensityB)
gl.drawArrays(gl.TRIANGLES, 0, 6)
}
}