Hide pixelation post-process

main
Warinyourself 3 weeks ago
parent 820de8539d
commit 61bea1b774

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const DESTRUCTION_SETTINGS: AppTheme = {
name: 'Destruction',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox } from '@/utils/theme-inputs'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox,
} from '@/utils/theme-inputs'
export const FLOW_SETTINGS: AppTheme = {
name: 'Flow',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const PLANET_SETTINGS: AppTheme = {
name: 'Planet',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, hueSlider, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, hueSlider, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const PLASMA_SETTINGS: AppTheme = {
name: 'Plasma',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox } from '@/utils/theme-inputs'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox,
} from '@/utils/theme-inputs'
export const RANDOM_SETTINGS: AppTheme = {
name: 'Random',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const RINGS_SETTINGS: AppTheme = {
name: 'Rings',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider, buildInputColor } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider, buildInputColor,
} from '@/utils/theme-inputs'
export const RINGS3D_SETTINGS: AppTheme = {
name: 'Rings3D',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox } from '@/utils/theme-inputs'
import { pxratio, hueSlider, brightnessSlider, randomButton, resetButton, buildInputSlider, buildInvertCheckbox,
} from '@/utils/theme-inputs'
export const SPHERE_SETTINGS: AppTheme = {
name: 'Sphere',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const TENDERNESS_SETTINGS: AppTheme = {
name: 'Tenderness',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const TUNNEL_SETTINGS: AppTheme = {
name: 'Tunnel',

@ -1,5 +1,6 @@
import type { AppTheme } from '@/models/app'
import { pxratio, randomButton, resetButton, buildInputSlider } from '@/utils/theme-inputs'
import { pxratio, randomButton, resetButton, buildInputSlider,
} from '@/utils/theme-inputs'
export const ZAPPY_SETTINGS: AppTheme = {
name: 'Zappy',

@ -30,8 +30,15 @@ export default class GL {
height!: number
innerRunning = false
pixelSize = 1 // >1 enables pixelation post-pass (e.g. 6 = 6×6 pixel blocks)
positions!: number[]
// ─── Pixelation resources (lazy-initialised on first use) ───────────────────
private _pxFbo: WebGLFramebuffer | null = null
private _pxTex: WebGLTexture | null = null
private _pxProg: WebGLProgram | null = null
private _pxQuad: WebGLBuffer | null = null
positionBuffer!: WebGLBuffer | null
listeners: WindowListenerCallback[] = []
@ -172,16 +179,38 @@ export default class GL {
render() {
this.running && requestAnimationFrame(this.render)
const gl = this.ctx
const usePixelate = this.pixelSize > 1
if (usePixelate) {
if (!this._pxFbo) this._initPixelate()
gl.bindFramebuffer(gl.FRAMEBUFFER, this._pxFbo)
}
if (!this.renderOptions.externalTimeUse) {
this.time = (Date.now() - this.startTime) / 1000
this.ctx.uniform1f(this.programInfo.uniforms.time, this.time)
gl.uniform1f(this.programInfo.uniforms.time, this.time)
}
if (this.renderHook) {
this.renderHook()
if (this.renderHook) this.renderHook()
gl.drawArrays(gl.TRIANGLES, 0, 6)
if (usePixelate) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.useProgram(this._pxProg)
gl.bindBuffer(gl.ARRAY_BUFFER, this._pxQuad)
const loc = gl.getAttribLocation(this._pxProg!, 'aPos')
gl.enableVertexAttribArray(loc)
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, this._pxTex)
gl.uniform1i(gl.getUniformLocation(this._pxProg!, 'uTex'), 0)
gl.uniform2f(gl.getUniformLocation(this._pxProg!, 'uRes'), this.el.width, this.el.height)
gl.uniform1f(gl.getUniformLocation(this._pxProg!, 'uSize'), this.pixelSize)
gl.drawArrays(gl.TRIANGLES, 0, 6)
gl.useProgram(this.program)
this.initBuffers(this.positions)
}
this.ctx.drawArrays(this.ctx.TRIANGLES, 0, 6)
}
initBuffers(positions: number[]) {
@ -235,6 +264,56 @@ export default class GL {
this.ctx.uniform2fv(this.programInfo.uniforms.resolution, [width * this.pxratio, height * this.pxratio])
this.initBuffers(this.positions)
if (this._pxTex) {
this.ctx.bindTexture(this.ctx.TEXTURE_2D, this._pxTex)
this.ctx.texImage2D(this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.el.width, this.el.height, 0, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, null)
this.ctx.bindTexture(this.ctx.TEXTURE_2D, null)
}
}
private _initPixelate() {
const gl = this.ctx
const w = this.el.width
const h = this.el.height
this._pxTex = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, this._pxTex)
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.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
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)
this._pxFbo = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, this._pxFbo)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._pxTex, 0)
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
gl.bindTexture(gl.TEXTURE_2D, null)
const compile = (type: number, src: string) => {
const s = gl.createShader(type)!
gl.shaderSource(s, src)
gl.compileShader(s)
return s
}
this._pxProg = gl.createProgram()!
gl.attachShader(this._pxProg, compile(gl.VERTEX_SHADER, `#version 300 es
in vec2 aPos; out vec2 vUv;
void main() { vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`))
gl.attachShader(this._pxProg, compile(gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
uniform sampler2D uTex; uniform vec2 uRes; uniform float uSize;
in vec2 vUv; out vec4 fragColor;
void main() {
vec2 b = floor(gl_FragCoord.xy / uSize) * uSize + uSize * 0.5;
fragColor = texture(uTex, b / uRes);
}`))
gl.linkProgram(this._pxProg)
this._pxQuad = gl.createBuffer()!
gl.bindBuffer(gl.ARRAY_BUFFER, this._pxQuad)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW)
}
createShaderOfType(ctx: ctxType, type: GLenum, source: string) {

@ -56,6 +56,21 @@ void main() {
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)
@ -91,6 +106,8 @@ export interface PostProcessOptions {
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 {
@ -101,9 +118,11 @@ export default class PostProcessGL extends GL {
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
@ -122,6 +141,7 @@ export default class PostProcessGL extends GL {
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()
@ -169,8 +189,10 @@ export default class PostProcessGL extends GL {
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)
@ -231,6 +253,19 @@ export default class PostProcessGL extends GL {
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() {
@ -238,7 +273,7 @@ export default class PostProcessGL extends GL {
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]) {
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)
}
@ -268,7 +303,7 @@ export default class PostProcessGL extends GL {
return
}
const { passesA, radiusA, intensityA, stepMultA, passesB, radiusB, intensityB, stepMultB, thresholdB } = this.ppOptions
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)
@ -282,7 +317,12 @@ export default class PostProcessGL extends GL {
if (this.renderHook) this.renderHook()
gl.drawArrays(gl.TRIANGLES, 0, 6)
const bloomA = this._runBlur(this._mainTarget, passesA, radiusA, false, stepMultA)
// 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
@ -295,7 +335,7 @@ export default class PostProcessGL extends GL {
this._bindQuad(this._compositeProgram)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, this._mainTarget.texture)
gl.bindTexture(gl.TEXTURE_2D, renderBase.texture)
gl.uniform1i(gl.getUniformLocation(this._compositeProgram, 'uBase'), 0)
gl.activeTexture(gl.TEXTURE1)

@ -83,3 +83,10 @@ export const buildInvertCheckbox = (): AppInputThemeGeneral => ({
type: 'checkbox',
value: false
})
export const buildCheckbox = (name: string, value = false): AppInputThemeGeneral => ({
name,
label: `input.${name}`,
type: 'checkbox' as const,
value
})

Loading…
Cancel
Save