feat(shader): Add BlurPipeline class for frosted glass FBO setup

- Add BlurPipeline class with downsampled FBO support (scene, blur_a, blur_b)
- Implement 2-pass Gaussian blur shaders (horizontal + vertical)
- Add setup_fbos(), compile_blur_shaders(), prepare_blur(), cleanup() methods
- Add tests for BlurPipeline initialization, FBO setup, shader compilation, blur execution, and cleanup

Task: Phase 1, Task 1 of frosted_glass_20260313 track
This commit is contained in:
2026-03-13 20:22:16 -04:00
parent 2c39f1dcf4
commit d9148acb0c
4 changed files with 260 additions and 6 deletions

View File

@@ -150,4 +150,182 @@ void main() {
gl.glUniform1f(u_time_loc, float(time))
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
class BlurPipeline:
def __init__(self):
self.scene_fbo: int | None = None
self.scene_tex: int | None = None
self.blur_fbo_a: int | None = None
self.blur_tex_a: int | None = None
self.blur_fbo_b: int | None = None
self.blur_tex_b: int | None = None
self.h_blur_program: int | None = None
self.v_blur_program: int | None = None
self._fb_width: int = 0
self._fb_height: int = 0
def _compile_shader(self, vertex_src: str, fragment_src: str) -> int:
program = gl.glCreateProgram()
def _compile(src, shader_type):
shader = gl.glCreateShader(shader_type)
gl.glShaderSource(shader, src)
gl.glCompileShader(shader)
if not gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS):
info_log = gl.glGetShaderInfoLog(shader)
if hasattr(info_log, "decode"):
info_log = info_log.decode()
raise RuntimeError(f"Shader compilation failed: {info_log}")
return shader
vert_shader = _compile(vertex_src, gl.GL_VERTEX_SHADER)
frag_shader = _compile(fragment_src, gl.GL_FRAGMENT_SHADER)
gl.glAttachShader(program, vert_shader)
gl.glAttachShader(program, frag_shader)
gl.glLinkProgram(program)
if not gl.glGetProgramiv(program, gl.GL_LINK_STATUS):
info_log = gl.glGetProgramInfoLog(program)
if hasattr(info_log, "decode"):
info_log = info_log.decode()
raise RuntimeError(f"Program linking failed: {info_log}")
gl.glDeleteShader(vert_shader)
gl.glDeleteShader(frag_shader)
return program
def _create_fbo(self, width: int, height: int) -> tuple[int, int]:
tex = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA8, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
fbo = gl.glGenFramebuffers(1)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, tex, 0)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
return fbo, tex
def setup_fbos(self, width: int, height: int):
blur_w = max(1, width // 4)
blur_h = max(1, height // 4)
self._fb_width = blur_w
self._fb_height = blur_h
self.scene_fbo, self.scene_tex = self._create_fbo(width, height)
self.blur_fbo_a, self.blur_tex_a = self._create_fbo(blur_w, blur_h)
self.blur_fbo_b, self.blur_tex_b = self._create_fbo(blur_w, blur_h)
def compile_blur_shaders(self):
vert_src = """
#version 330 core
out vec2 v_uv;
void main() {
vec2 pos = vec2(-1.0, -1.0);
vec2 uv = vec2(0.0, 0.0);
if (gl_VertexID == 1) { pos = vec2(1.0, -1.0); uv = vec2(1.0, 0.0); }
else if (gl_VertexID == 2) { pos = vec2(-1.0, 1.0); uv = vec2(0.0, 1.0); }
else if (gl_VertexID == 3) { pos = vec2(1.0, 1.0); uv = vec2(1.0, 1.0); }
gl_Position = vec4(pos, 0.0, 1.0);
v_uv = uv;
}
"""
h_frag_src = """
#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform vec2 u_texel_size;
out vec4 FragColor;
void main() {
vec2 offset = vec2(u_texel_size.x, 0.0);
vec4 sum = vec4(0.0);
sum += texture(u_texture, v_uv - offset * 4.0) * 0.051;
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0918;
sum += texture(u_texture, v_uv - offset * 2.0) * 0.12245;
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1531;
sum += texture(u_texture, v_uv) * 0.1633;
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1531;
sum += texture(u_texture, v_uv + offset * 2.0) * 0.12245;
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0918;
sum += texture(u_texture, v_uv + offset * 4.0) * 0.051;
FragColor = sum;
}
"""
v_frag_src = """
#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform vec2 u_texel_size;
out vec4 FragColor;
void main() {
vec2 offset = vec2(0.0, u_texel_size.y);
vec4 sum = vec4(0.0);
sum += texture(u_texture, v_uv - offset * 4.0) * 0.051;
sum += texture(u_texture, v_uv - offset * 3.0) * 0.0918;
sum += texture(u_texture, v_uv - offset * 2.0) * 0.12245;
sum += texture(u_texture, v_uv - offset * 1.0) * 0.1531;
sum += texture(u_texture, v_uv) * 0.1633;
sum += texture(u_texture, v_uv + offset * 1.0) * 0.1531;
sum += texture(u_texture, v_uv + offset * 2.0) * 0.12245;
sum += texture(u_texture, v_uv + offset * 3.0) * 0.0918;
sum += texture(u_texture, v_uv + offset * 4.0) * 0.051;
FragColor = sum;
}
"""
self.h_blur_program = self._compile_shader(vert_src, h_frag_src)
self.v_blur_program = self._compile_shader(vert_src, v_frag_src)
def _render_quad(self, program: int, src_tex: int, texel_size: tuple[float, float]):
gl.glUseProgram(program)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_2D, src_tex)
u_tex = gl.glGetUniformLocation(program, "u_texture")
if u_tex != -1:
gl.glUniform1i(u_tex, 0)
u_ts = gl.glGetUniformLocation(program, "u_texel_size")
if u_ts != -1:
gl.glUniform2f(u_ts, texel_size[0], texel_size[1])
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
gl.glUseProgram(0)
def prepare_blur(self, width: int, height: int, time: float):
if not self.h_blur_program or not self.v_blur_program:
return
if not self.blur_fbo_a or not self.blur_fbo_b:
return
blur_w = max(1, self._fb_width)
blur_h = max(1, self._fb_height)
texel_x = 1.0 / float(blur_w)
texel_y = 1.0 / float(blur_h)
gl.glViewport(0, 0, blur_w, blur_h)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_a)
gl.glClearColor(0.0, 0.0, 0.0, 0.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
self._render_quad(self.h_blur_program, self.scene_tex, (texel_x, texel_y))
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.blur_fbo_b)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
self._render_quad(self.v_blur_program, self.blur_tex_a, (texel_x, texel_y))
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
gl.glViewport(0, 0, width, height)
def get_blur_texture(self) -> int | None:
return self.blur_tex_b
def cleanup(self):
fbos = [f for f in [self.scene_fbo, self.blur_fbo_a, self.blur_fbo_b] if f is not None]
texs = [t for t in [self.scene_tex, self.blur_tex_a, self.blur_tex_b] if t is not None]
progs = [p for p in [self.h_blur_program, self.v_blur_program] if p is not None]
if fbos:
gl.glDeleteFramebuffers(len(fbos), fbos)
if texs:
gl.glDeleteTextures(len(texs), texs)
if progs:
for p in progs:
gl.glDeleteProgram(p)
self.scene_fbo = None
self.scene_tex = None
self.blur_fbo_a = None
self.blur_tex_a = None
self.blur_fbo_b = None
self.blur_tex_b = None
self.h_blur_program = None
self.v_blur_program = None