Commitb8423477Recorded27 Apr 2026Repositorysigil-graphics

v0.10.1: separable two-pass Gaussian bloom in test-postprocess-demo

Message

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.

Changed
 package.sgl                                                   |   2 +-
 test/test-postprocess-demo/src/test-postprocess-demo/main.sgl | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------
 2 files changed, 102 insertions(+), 52 deletions(-)
Diff
package.sglmodified
@@ -5,7 +5,7 @@
5
6
(package
7
name: "sigil-graphics"
8
version: "0.10.0"
+8
version: "0.10.1"
9
sigil: "^0.14"
10
description: "2D graphics, image loading, and font rendering for Sigil"
11
url: "https://codeberg.org/sigil/sigil-graphics"
test/test-postprocess-demo/src/test-postprocess-demo/main.sglmodified
@@ -1,11 +1,17 @@
1
;;; test-postprocess-demo / main — end-to-end bloom demo
+1
;;; test-postprocess-demo / main — separable two-pass Gaussian bloom
2
;;;
3
;;; Proves the sigil-graphics post-processing surface end-to-end:
4
;;; 1. Render scene into a 960×540 offscreen target (a few bright
5
;;; moving circles + a dim backdrop).
6
;;; 2. Composite the target onto the swap chain through a one-pass
7
;;; bloom shader. Bright pixels (the circles) gain a visible glow
8
;;; halo; dim pixels (the backdrop) pass through unmodified.
+3
;;; Pipeline:
+4
;;; 1. Render scene into RT_scene (a few bright orbiting circles + a
+5
;;; dim navy backdrop).
+6
;;; 2. RT_scene -> RT_h via horizontal-blur shader. The shader fuses
+7
;;; a smoothstep bright-extract with a 21-tap 1D Gaussian; only
+8
;;; bright pixels survive into RT_h, and they're already blurred
+9
;;; horizontally.
+10
;;; 3. RT_h -> RT_v via vertical-blur shader. RT_h is bright-only, so
+11
;;; this pass is a plain 1D Gaussian. After this, RT_v contains a
+12
;;; true 2D Gaussian blur of the bright regions.
+13
;;; 4. Present pass: draw RT_scene as a passthrough, then additively
+14
;;; composite RT_v on top.
15
;;;
16
;;; Quits after ~5 seconds or when escape is pressed.
17
@@ -34,65 +40,83 @@
40
" iColor = color;\n"
41
"}\n"))
42
37
;; One-pass bloom: 7x7 Gaussian-weighted blur of the
38
;; threshold-extracted bright regions, added back on top of the
39
;; original scene. Threshold uses smoothstep so it doesn't
40
;; alias against pixel-aligned bright shapes; intensity
41
;; multiplies the bloom contribution so bright pixels get a
42
;; visible halo. Not production-quality (a real pipeline would
43
;; downsample to a half-res target first) but enough to prove
44
;; the surface works end to end.
45
(define BLOOM-FRAG
+43
;; Pass 1 — bright extract + 1D horizontal Gaussian, fused.
+44
;;
+45
;; 21-tap kernel (i in -10..10) with sigma = 3.3 in tap units; the
+46
;; corner weight is exp(-100/21.78) ~ 0.010, soft enough to avoid
+47
;; truncation banding without padding the kernel further. u_radius
+48
;; scales the per-tap pixel stride: at u_radius=1.0 the kernel
+49
;; reaches +/-10 px; at u_radius=2.0 it reaches +/-20 px (mild
+50
;; aliasing on high-frequency content but a wider halo). The
+51
;; smoothstep around u_threshold (typ. 0.7) gives a soft knee so
+52
;; the dark playfield doesn't bloom.
+53
(define BLOOM-H-FRAG
54
(string-append
55
"#version 410\n"
56
"uniform sampler2D iTexChannel0_iSmpChannel0;\n"
57
"uniform float u_time;\n"
58
"uniform vec2 u_resolution;\n"
51
"uniform float u_intensity;\n"
59
"uniform float u_threshold;\n"
60
"uniform float u_radius;\n"
61
"in vec2 texUV;\n"
62
"in vec4 iColor;\n"
63
"out vec4 fragColor;\n"
64
"\n"
58
"vec3 sample_bright(vec2 uv) {\n"
+65
"vec3 bright(vec2 uv) {\n"
66
" vec3 c = texture(iTexChannel0_iSmpChannel0, uv).rgb;\n"
67
" float luma = max(max(c.r, c.g), c.b);\n"
68
" return c * smoothstep(u_threshold, u_threshold + 0.15, luma);\n"
69
"}\n"
70
"\n"
71
"void main() {\n"
65
" vec3 base = texture(iTexChannel0_iSmpChannel0, texUV).rgb;\n"
66
" vec2 px_step = u_radius / u_resolution;\n"
67
" vec3 bloom = vec3(0.0);\n"
+72
" vec2 step = vec2(u_radius / u_resolution.x, 0.0);\n"
+73
" vec3 sum = vec3(0.0);\n"
74
" float wsum = 0.0;\n"
69
" // 7x7 Gaussian-ish kernel — sigma ~ 1.5 cells.\n"
70
" for (int j = -3; j <= 3; j++) {\n"
71
" for (int i = -3; i <= 3; i++) {\n"
72
" float w = exp(-float(i*i + j*j) * 0.22);\n"
73
" bloom += sample_bright(texUV + vec2(i, j) * px_step) * w;\n"
74
" wsum += w;\n"
75
" }\n"
+75
" for (int i = -10; i <= 10; i++) {\n"
+76
" float w = exp(-float(i*i) / 21.78);\n"
+77
" sum += bright(texUV + float(i) * step) * w;\n"
+78
" wsum += w;\n"
79
" }\n"
77
" bloom /= wsum;\n"
78
" // Slight time-driven pulse so the auto-uniform u_time is\n"
79
" // clearly exercised on capture.\n"
80
" float pulse = 0.85 + 0.15 * sin(u_time * 1.7);\n"
81
" vec3 outc = base + bloom * (u_intensity * pulse);\n"
+80
" fragColor = vec4(sum / wsum, 1.0) * iColor;\n"
+81
"}\n"))
+82
+83
;; Pass 2 — 1D vertical Gaussian. No threshold; the input RT_h is
+84
;; already bright-only. u_intensity scales the final halo amplitude
+85
;; so callers can pick how punchy the bloom should be.
+86
(define BLOOM-V-FRAG
+87
(string-append
+88
"#version 410\n"
+89
"uniform sampler2D iTexChannel0_iSmpChannel0;\n"
+90
"uniform float u_time;\n"
+91
"uniform vec2 u_resolution;\n"
+92
"uniform float u_intensity;\n"
+93
"uniform float u_radius;\n"
+94
"in vec2 texUV;\n"
+95
"in vec4 iColor;\n"
+96
"out vec4 fragColor;\n"
+97
"\n"
+98
"void main() {\n"
+99
" vec2 step = vec2(0.0, u_radius / u_resolution.y);\n"
+100
" vec3 sum = vec3(0.0);\n"
+101
" float wsum = 0.0;\n"
+102
" for (int j = -10; j <= 10; j++) {\n"
+103
" float w = exp(-float(j*j) / 21.78);\n"
+104
" sum += texture(iTexChannel0_iSmpChannel0, texUV + float(j) * step).rgb * w;\n"
+105
" wsum += w;\n"
+106
" }\n"
+107
" vec3 outc = (sum / wsum) * u_intensity;\n"
108
" fragColor = vec4(outc, 1.0) * iColor;\n"
109
"}\n"))
110
85
;; Three orbiting circles in saturated colors — saturation is what
86
;; makes the bloom halo unambiguous. The backdrop stays dim so it
87
;; doesn't compete with the bloom.
+111
;; Three orbiting circles in saturated colors plus a pulsing white
+112
;; core. Saturation makes the bloom halo unambiguous; the dim
+113
;; backdrop stays below threshold so it doesn't bloom.
114
(define (draw-bright-scene t)
115
(set-color 0.04 0.05 0.10 1.0)
116
(draw-filled-rect 0.0 0.0 (inexact WINDOW-W) (inexact WINDOW-H))
117
(let* ((cx (* 0.5 (inexact WINDOW-W)))
118
(cy (* 0.5 (inexact WINDOW-H)))
119
(r 140.0))
94
;; Three orbiting circles, 120° apart, drawn additively so
95
;; overlap regions get extra bright (= more bloom).
120
(with-additive-blend
121
(set-color 1.0 0.35 0.35 1.0)
122
(draw-filled-circle (+ cx (* r (cos t)))
@@ -105,27 +129,50 @@
129
(let ((a (+ t (/ (* 4.0 3.14159265) 3.0))))
130
(draw-filled-circle (+ cx (* r (cos a)))
131
(+ cy (* r (sin a))) 28.0 32))
108
;; A small, very bright white core in the middle pulsing on
109
;; t — gives the bloom a steady focal point.
132
(set-color 1.0 1.0 1.0 (+ 0.6 (* 0.3 (sin (* t 2.5)))))
133
(draw-filled-circle cx cy 16.0 32))))
134
135
(define (game-loop)
136
(gfx-setup)
137
(set-viewport WINDOW-W WINDOW-H)
116
(let* ((rt (make-render-target WINDOW-W WINDOW-H))
117
(bloom (load-shader VERT-SOURCE BLOOM-FRAG)))
118
(set-shader-uniform bloom 'u_intensity 1.4)
119
(set-shader-uniform bloom 'u_threshold 0.55)
120
(set-shader-uniform bloom 'u_radius 3.5)
+138
(let* ((rt-scene (make-render-target WINDOW-W WINDOW-H))
+139
(rt-h (make-render-target WINDOW-W WINDOW-H))
+140
(rt-v (make-render-target WINDOW-W WINDOW-H))
+141
(bloom-h (load-shader VERT-SOURCE BLOOM-H-FRAG))
+142
(bloom-v (load-shader VERT-SOURCE BLOOM-V-FRAG)))
+143
(set-shader-uniform bloom-h 'u_threshold 0.70)
+144
(set-shader-uniform bloom-h 'u_radius 1.5)
+145
(set-shader-uniform bloom-v 'u_radius 1.5)
+146
(set-shader-uniform bloom-v 'u_intensity 1.6)
147
(let loop ((elapsed 0.0))
148
(let ((dt (wait-frame)))
123
(with-render-target rt
+149
;; Pass 0: render scene into RT_scene.
+150
(with-render-target rt-scene
151
(draw-bright-scene elapsed))
+152
;; Pass 1: bright extract + horizontal blur into RT_h.
+153
(with-render-target rt-h
+154
(set-color 1.0 1.0 1.0 1.0)
+155
(with-shader bloom-h
+156
(draw-render-target rt-scene 0.0 0.0
+157
(inexact WINDOW-W)
+158
(inexact WINDOW-H))))
+159
;; Pass 2: vertical blur into RT_v.
+160
(with-render-target rt-v
+161
(set-color 1.0 1.0 1.0 1.0)
+162
(with-shader bloom-v
+163
(draw-render-target rt-h 0.0 0.0
+164
(inexact WINDOW-W)
+165
(inexact WINDOW-H))))
+166
;; Present: passthrough scene + additive bloom layer.
167
(with-frame
168
(clear-screen 0.0 0.0 0.0 1.0)
127
(with-shader bloom
128
(draw-render-target rt 0.0 0.0
+169
(set-color 1.0 1.0 1.0 1.0)
+170
(draw-render-target rt-scene 0.0 0.0
+171
(inexact WINDOW-W)
+172
(inexact WINDOW-H))
+173
(with-additive-blend
+174
(set-color 1.0 1.0 1.0 1.0)
+175
(draw-render-target rt-v 0.0 0.0
176
(inexact WINDOW-W)
177
(inexact WINDOW-H))))
178
(cond
@@ -133,11 +180,14 @@
180
((quit-requested?) #t)
181
((> elapsed 5.0) (request-quit))
182
(else (loop (+ elapsed dt))))))
136
(shader-free! bloom)
137
(render-target-free! rt))
+183
(shader-free! bloom-v)
+184
(shader-free! bloom-h)
+185
(render-target-free! rt-v)
+186
(render-target-free! rt-h)
+187
(render-target-free! rt-scene))
188
(gfx-shutdown))
189
190
(define (main . _args)
141
(run-game "sigil-graphics bloom postprocess demo"
+191
(run-game "sigil-graphics separable bloom postprocess demo"
192
WINDOW-W WINDOW-H game-loop)
193
0)))