import OpenGL.GL as gl class ShaderManager: def __init__(self): self.program = None self.bg_program = None self.pp_program = None 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) self.program = program return program def update_uniforms(self, uniforms: dict): if self.program is None: return for name, value in uniforms.items(): loc = gl.glGetUniformLocation(self.program, name) if loc == -1: continue if isinstance(value, float): gl.glUniform1f(loc, value) elif isinstance(value, int): gl.glUniform1i(loc, value) elif isinstance(value, (list, tuple)): if len(value) == 2: gl.glUniform2f(loc, value[0], value[1]) elif len(value) == 3: gl.glUniform3f(loc, value[0], value[1], value[2]) elif len(value) == 4: gl.glUniform4f(loc, value[0], value[1], value[2], value[3]) def setup_background_shader(self): vertex_src = """ #version 330 core const vec2 positions[4] = vec2[]( vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2(-1.0, 1.0), vec2( 1.0, 1.0) ); void main() { gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); } """ fragment_src = """ #version 330 core uniform float u_time; uniform vec2 u_resolution; out vec4 FragColor; void main() { vec2 uv = gl_FragCoord.xy / u_resolution.xy; vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0, 2, 4)); FragColor = vec4(col, 1.0); } """ self.bg_program = self.compile_shader(vertex_src, fragment_src) def render_background(self, width, height, time): if not self.bg_program: return gl.glUseProgram(self.bg_program) u_time_loc = gl.glGetUniformLocation(self.bg_program, "u_time") if u_time_loc != -1: gl.glUniform1f(u_time_loc, float(time)) u_res_loc = gl.glGetUniformLocation(self.bg_program, "u_resolution") if u_res_loc != -1: gl.glUniform2f(u_res_loc, float(width), float(height)) gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) gl.glUseProgram(0) def setup_post_process_shader(self): vertex_src = """ #version 330 core const vec2 positions[4] = vec2[]( vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2(-1.0, 1.0), vec2( 1.0, 1.0) ); const vec2 uvs[4] = vec2[]( vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 1.0) ); out vec2 v_uv; void main() { gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); v_uv = uvs[gl_VertexID]; } """ fragment_src = """ #version 330 core in vec2 v_uv; uniform sampler2D u_texture; uniform float u_time; out vec4 FragColor; void main() { vec4 color = texture(u_texture, v_uv); float scanline = sin(v_uv.y * 800.0 + u_time * 2.0) * 0.04; color.rgb -= scanline; FragColor = color; } """ self.pp_program = self.compile_shader(vertex_src, fragment_src) def render_post_process(self, texture_id, width, height, time): if not self.pp_program: return gl.glUseProgram(self.pp_program) gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id) u_tex_loc = gl.glGetUniformLocation(self.pp_program, "u_texture") if u_tex_loc != -1: gl.glUniform1i(u_tex_loc, 0) u_time_loc = gl.glGetUniformLocation(self.pp_program, "u_time") if u_time_loc != -1: 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.deepsea_program: int | None = None self._quad_vao: int | None = None self._fb_width: int = 0 self._fb_height: int = 0 self._fb_scale: int = 1 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]: if width <= 0 or height <= 0: raise ValueError(f"Invalid FBO dimensions: {width}x{height}") 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 _create_quad_vao(self) -> int: import ctypes vao = gl.glGenVertexArrays(1) gl.glBindVertexArray(vao) vertices = (ctypes.c_float * 16)( -1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0 ) vbo = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, ctypes.sizeof(vertices), vertices, gl.GL_STATIC_DRAW) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 16, None) gl.glEnableVertexAttribArray(1) gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 16, ctypes.c_void_p(8)) gl.glBindVertexArray(0) return vao def setup_fbos(self, width: int, height: int, fb_scale: float = 1.0): scale = max(1, int(fb_scale)) blur_w = max(1, (width * scale) // 4) blur_h = max(1, (height * scale) // 4) self._fb_width = blur_w self._fb_height = blur_h self._fb_scale = scale scene_w = width * scale scene_h = height * scale self.scene_fbo, self.scene_tex = self._create_fbo(scene_w, scene_h) 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 layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; out vec2 v_uv; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_uv = a_texcoord; } """ 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 * 6.0) * 0.0152; sum += texture(u_texture, v_uv - offset * 5.0) * 0.0300; sum += texture(u_texture, v_uv - offset * 4.0) * 0.0525; sum += texture(u_texture, v_uv - offset * 3.0) * 0.0812; sum += texture(u_texture, v_uv - offset * 2.0) * 0.1110; sum += texture(u_texture, v_uv - offset * 1.0) * 0.1342; sum += texture(u_texture, v_uv) * 0.1432; sum += texture(u_texture, v_uv + offset * 1.0) * 0.1342; sum += texture(u_texture, v_uv + offset * 2.0) * 0.1110; sum += texture(u_texture, v_uv + offset * 3.0) * 0.0812; sum += texture(u_texture, v_uv + offset * 4.0) * 0.0525; sum += texture(u_texture, v_uv + offset * 5.0) * 0.0300; sum += texture(u_texture, v_uv + offset * 6.0) * 0.0152; 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 * 6.0) * 0.0152; sum += texture(u_texture, v_uv - offset * 5.0) * 0.0300; sum += texture(u_texture, v_uv - offset * 4.0) * 0.0525; sum += texture(u_texture, v_uv - offset * 3.0) * 0.0812; sum += texture(u_texture, v_uv - offset * 2.0) * 0.1110; sum += texture(u_texture, v_uv - offset * 1.0) * 0.1342; sum += texture(u_texture, v_uv) * 0.1432; sum += texture(u_texture, v_uv + offset * 1.0) * 0.1342; sum += texture(u_texture, v_uv + offset * 2.0) * 0.1110; sum += texture(u_texture, v_uv + offset * 3.0) * 0.0812; sum += texture(u_texture, v_uv + offset * 4.0) * 0.0525; sum += texture(u_texture, v_uv + offset * 5.0) * 0.0300; sum += texture(u_texture, v_uv + offset * 6.0) * 0.0152; 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 compile_deepsea_shader(self): vert_src = """ #version 330 core layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; out vec2 v_uv; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_uv = a_texcoord; } """ frag_src = """ #version 330 core in vec2 v_uv; uniform float u_time; uniform vec2 u_resolution; out vec4 FragColor; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } float fbm(vec2 p) { float v = 0.0; float a = 0.5; for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; } return v; } void main() { vec2 uv = v_uv; float t = u_time * 0.3; vec3 col = vec3(0.01, 0.05, 0.12); for (int i = 0; i < 3; i++) { float phase = t * (0.1 + float(i) * 0.05); vec2 blob_uv = uv + vec2(sin(phase), cos(phase * 0.8)) * 0.3; float blob = fbm(blob_uv * 3.0 + t * 0.2); col = mix(col, vec3(0.02, 0.20, 0.40), blob * 0.4); } float line_alpha = 0.0; for (int i = 0; i < 12; i++) { float fi = float(i); float offset = mod(t * 15.0 + fi * (u_resolution.x / 12.0), u_resolution.x); float line_x = offset / u_resolution.x; float dist = abs(uv.x - line_x); float alpha = smoothstep(0.02, 0.0, dist) * (0.1 + 0.05 * sin(t + fi)); line_alpha += alpha; } col += vec3(0.04, 0.35, 0.55) * line_alpha; float vignette = 1.0 - length(uv - 0.5) * 0.8; col *= vignette; FragColor = vec4(col, 1.0); } """ self.deepsea_program = self._compile_shader(vert_src, frag_src) self._quad_vao = self._create_quad_vao() def render_deepsea_to_fbo(self, width: int, height: int, time: float): if not self.deepsea_program or not self.scene_fbo or not self._quad_vao: return scene_w = width * self._fb_scale scene_h = height * self._fb_scale gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo) gl.glViewport(0, 0, scene_w, scene_h) gl.glClearColor(0.01, 0.05, 0.12, 1.0) gl.glClear(gl.GL_COLOR_BUFFER_BIT) gl.glUseProgram(self.deepsea_program) u_time_loc = gl.glGetUniformLocation(self.deepsea_program, "u_time") if u_time_loc != -1: gl.glUniform1f(u_time_loc, time) u_res_loc = gl.glGetUniformLocation(self.deepsea_program, "u_resolution") if u_res_loc != -1: gl.glUniform2f(u_res_loc, float(scene_w), float(scene_h)) gl.glBindVertexArray(self._quad_vao) gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) gl.glBindVertexArray(0) gl.glUseProgram(0) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) 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.glBindVertexArray(self._quad_vao) gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) gl.glBindVertexArray(0) 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) restore_w = width * self._fb_scale restore_h = height * self._fb_scale gl.glViewport(0, 0, restore_w, restore_h) def prepare_global_blur(self, width: int, height: int, time: float, fb_scale: float = 1.0): if not self.scene_fbo: if self._fb_scale != int(fb_scale): self.setup_fbos(width, height, fb_scale) self.render_deepsea_to_fbo(width, height, time) self.prepare_blur(width, height, time) 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, self.deepsea_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) if self._quad_vao: gl.glDeleteVertexArrays(1, [self._quad_vao]) 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 self.deepsea_program = None self._quad_vao = None