v0.10.1: separable two-pass Gaussian bloom in test-postprocess-demo
Replaces the v0.10.0 single-pass 7x7 Gaussian-on-square-grid kernel with a separable two-pass Gaussian (21-tap horizontal + 21-tap vertical, sigma ~3.3 in tap units). Pass 1 fuses smoothstep bright-extract with the horizontal blur into RTh; pass 2 is a plain 1D vertical blur into RTv; present pass passes the scene through and additively blends RT_v on top.
The single-pass kernel left a square-support imprint on bright sources because total radius (~12 px effective) was small relative to typical shape size and corner samples carried ~2% weight. The separable pipeline reaches +/-10 px of soft Gaussian falloff per axis at 42 samples total (vs 49 for the old grid) - cheaper, wider, and visually circular.
API surface is unchanged; this lands purely as a test-demo upgrade plus the version bump.
package.sgl | 2 +-
test/test-postprocess-demo/src/test-postprocess-demo/main.sgl | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------
2 files changed, 102 insertions(+), 52 deletions(-)package.sglmodified
(package name: "sigil-graphics" version: "0.10.0" version: "0.10.1" sigil: "^0.14" description: "2D graphics, image loading, and font rendering for Sigil" url: "https://codeberg.org/sigil/sigil-graphics"test/test-postprocess-demo/src/test-postprocess-demo/main.sglmodified
;;; test-postprocess-demo / main — end-to-end bloom demo;;; test-postprocess-demo / main — separable two-pass Gaussian bloom;;;;;; Proves the sigil-graphics post-processing surface end-to-end:;;; 1. Render scene into a 960×540 offscreen target (a few bright;;; moving circles + a dim backdrop).;;; 2. Composite the target onto the swap chain through a one-pass;;; bloom shader. Bright pixels (the circles) gain a visible glow;;; halo; dim pixels (the backdrop) pass through unmodified.;;; Pipeline:;;; 1. Render scene into RT_scene (a few bright orbiting circles + a;;; dim navy backdrop).;;; 2. RT_scene -> RT_h via horizontal-blur shader. The shader fuses;;; a smoothstep bright-extract with a 21-tap 1D Gaussian; only;;; bright pixels survive into RT_h, and they're already blurred;;; horizontally.;;; 3. RT_h -> RT_v via vertical-blur shader. RT_h is bright-only, so;;; this pass is a plain 1D Gaussian. After this, RT_v contains a;;; true 2D Gaussian blur of the bright regions.;;; 4. Present pass: draw RT_scene as a passthrough, then additively;;; composite RT_v on top.;;;;;; Quits after ~5 seconds or when escape is pressed. " iColor = color;\n" "}\n")) ;; One-pass bloom: 7x7 Gaussian-weighted blur of the ;; threshold-extracted bright regions, added back on top of the ;; original scene. Threshold uses smoothstep so it doesn't ;; alias against pixel-aligned bright shapes; intensity ;; multiplies the bloom contribution so bright pixels get a ;; visible halo. Not production-quality (a real pipeline would ;; downsample to a half-res target first) but enough to prove ;; the surface works end to end. (define BLOOM-FRAG ;; Pass 1 — bright extract + 1D horizontal Gaussian, fused. ;; ;; 21-tap kernel (i in -10..10) with sigma = 3.3 in tap units; the ;; corner weight is exp(-100/21.78) ~ 0.010, soft enough to avoid ;; truncation banding without padding the kernel further. u_radius ;; scales the per-tap pixel stride: at u_radius=1.0 the kernel ;; reaches +/-10 px; at u_radius=2.0 it reaches +/-20 px (mild ;; aliasing on high-frequency content but a wider halo). The ;; smoothstep around u_threshold (typ. 0.7) gives a soft knee so ;; the dark playfield doesn't bloom. (define BLOOM-H-FRAG (string-append "#version 410\n" "uniform sampler2D iTexChannel0_iSmpChannel0;\n" "uniform float u_time;\n" "uniform vec2 u_resolution;\n" "uniform float u_intensity;\n" "uniform float u_threshold;\n" "uniform float u_radius;\n" "in vec2 texUV;\n" "in vec4 iColor;\n" "out vec4 fragColor;\n" "\n" "vec3 sample_bright(vec2 uv) {\n" "vec3 bright(vec2 uv) {\n" " vec3 c = texture(iTexChannel0_iSmpChannel0, uv).rgb;\n" " float luma = max(max(c.r, c.g), c.b);\n" " return c * smoothstep(u_threshold, u_threshold + 0.15, luma);\n" "}\n" "\n" "void main() {\n" " vec3 base = texture(iTexChannel0_iSmpChannel0, texUV).rgb;\n" " vec2 px_step = u_radius / u_resolution;\n" " vec3 bloom = vec3(0.0);\n" " vec2 step = vec2(u_radius / u_resolution.x, 0.0);\n" " vec3 sum = vec3(0.0);\n" " float wsum = 0.0;\n" " // 7x7 Gaussian-ish kernel — sigma ~ 1.5 cells.\n" " for (int j = -3; j <= 3; j++) {\n" " for (int i = -3; i <= 3; i++) {\n" " float w = exp(-float(i*i + j*j) * 0.22);\n" " bloom += sample_bright(texUV + vec2(i, j) * px_step) * w;\n" " wsum += w;\n" " }\n" " for (int i = -10; i <= 10; i++) {\n" " float w = exp(-float(i*i) / 21.78);\n" " sum += bright(texUV + float(i) * step) * w;\n" " wsum += w;\n" " }\n" " bloom /= wsum;\n" " // Slight time-driven pulse so the auto-uniform u_time is\n" " // clearly exercised on capture.\n" " float pulse = 0.85 + 0.15 * sin(u_time * 1.7);\n" " vec3 outc = base + bloom * (u_intensity * pulse);\n" " fragColor = vec4(sum / wsum, 1.0) * iColor;\n" "}\n")) ;; Pass 2 — 1D vertical Gaussian. No threshold; the input RT_h is ;; already bright-only. u_intensity scales the final halo amplitude ;; so callers can pick how punchy the bloom should be. (define BLOOM-V-FRAG (string-append "#version 410\n" "uniform sampler2D iTexChannel0_iSmpChannel0;\n" "uniform float u_time;\n" "uniform vec2 u_resolution;\n" "uniform float u_intensity;\n" "uniform float u_radius;\n" "in vec2 texUV;\n" "in vec4 iColor;\n" "out vec4 fragColor;\n" "\n" "void main() {\n" " vec2 step = vec2(0.0, u_radius / u_resolution.y);\n" " vec3 sum = vec3(0.0);\n" " float wsum = 0.0;\n" " for (int j = -10; j <= 10; j++) {\n" " float w = exp(-float(j*j) / 21.78);\n" " sum += texture(iTexChannel0_iSmpChannel0, texUV + float(j) * step).rgb * w;\n" " wsum += w;\n" " }\n" " vec3 outc = (sum / wsum) * u_intensity;\n" " fragColor = vec4(outc, 1.0) * iColor;\n" "}\n")) ;; Three orbiting circles in saturated colors — saturation is what ;; makes the bloom halo unambiguous. The backdrop stays dim so it ;; doesn't compete with the bloom. ;; Three orbiting circles in saturated colors plus a pulsing white ;; core. Saturation makes the bloom halo unambiguous; the dim ;; backdrop stays below threshold so it doesn't bloom. (define (draw-bright-scene t) (set-color 0.04 0.05 0.10 1.0) (draw-filled-rect 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H)) (let* ((cx (* 0.5 (inexact WINDOW-W))) (cy (* 0.5 (inexact WINDOW-H))) (r 140.0)) ;; Three orbiting circles, 120° apart, drawn additively so ;; overlap regions get extra bright (= more bloom). (with-additive-blend (set-color 1.0 0.35 0.35 1.0) (draw-filled-circle (+ cx (* r (cos t))) (let ((a (+ t (/ (* 4.0 3.14159265) 3.0)))) (draw-filled-circle (+ cx (* r (cos a))) (+ cy (* r (sin a))) 28.0 32)) ;; A small, very bright white core in the middle pulsing on ;; t — gives the bloom a steady focal point. (set-color 1.0 1.0 1.0 (+ 0.6 (* 0.3 (sin (* t 2.5))))) (draw-filled-circle cx cy 16.0 32)))) (define (game-loop) (gfx-setup) (set-viewport WINDOW-W WINDOW-H) (let* ((rt (make-render-target WINDOW-W WINDOW-H)) (bloom (load-shader VERT-SOURCE BLOOM-FRAG))) (set-shader-uniform bloom 'u_intensity 1.4) (set-shader-uniform bloom 'u_threshold 0.55) (set-shader-uniform bloom 'u_radius 3.5) (let* ((rt-scene (make-render-target WINDOW-W WINDOW-H)) (rt-h (make-render-target WINDOW-W WINDOW-H)) (rt-v (make-render-target WINDOW-W WINDOW-H)) (bloom-h (load-shader VERT-SOURCE BLOOM-H-FRAG)) (bloom-v (load-shader VERT-SOURCE BLOOM-V-FRAG))) (set-shader-uniform bloom-h 'u_threshold 0.70) (set-shader-uniform bloom-h 'u_radius 1.5) (set-shader-uniform bloom-v 'u_radius 1.5) (set-shader-uniform bloom-v 'u_intensity 1.6) (let loop ((elapsed 0.0)) (let ((dt (wait-frame))) (with-render-target rt ;; Pass 0: render scene into RT_scene. (with-render-target rt-scene (draw-bright-scene elapsed)) ;; Pass 1: bright extract + horizontal blur into RT_h. (with-render-target rt-h (set-color 1.0 1.0 1.0 1.0) (with-shader bloom-h (draw-render-target rt-scene 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H)))) ;; Pass 2: vertical blur into RT_v. (with-render-target rt-v (set-color 1.0 1.0 1.0 1.0) (with-shader bloom-v (draw-render-target rt-h 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H)))) ;; Present: passthrough scene + additive bloom layer. (with-frame (clear-screen 0.0 0.0 0.0 1.0) (with-shader bloom (draw-render-target rt 0.0 0.0 (set-color 1.0 1.0 1.0 1.0) (draw-render-target rt-scene 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H)) (with-additive-blend (set-color 1.0 1.0 1.0 1.0) (draw-render-target rt-v 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H)))) (cond ((quit-requested?) #t) ((> elapsed 5.0) (request-quit)) (else (loop (+ elapsed dt)))))) (shader-free! bloom) (render-target-free! rt)) (shader-free! bloom-v) (shader-free! bloom-h) (render-target-free! rt-v) (render-target-free! rt-h) (render-target-free! rt-scene)) (gfx-shutdown)) (define (main . _args) (run-game "sigil-graphics bloom postprocess demo" (run-game "sigil-graphics separable bloom postprocess demo" WINDOW-W WINDOW-H game-loop) 0)))