From d85dc3a1b3360c7fbd4398a615d2735a77567ead Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 13 Mar 2026 20:25:26 -0400 Subject: [PATCH] feat(shader): Add Deep Sea background shader for BlurPipeline - Add compile_deepsea_shader() with animated underwater-like GLSL shader - Add render_deepsea_to_fbo() to render background to scene FBO - Deep Sea shader features: FBM noise, animated blobs, caustic lines, vignette - Add deepsea_program to cleanup() for proper resource management - Add 2 new tests for Deep Sea shader compilation and FBO rendering Task: Phase 1, Task 2 of frosted_glass_20260313 track --- .../tracks/frosted_glass_20260313/plan.md | 2 +- src/shader_manager.py | 92 ++++++++++++++++++- tests/test_shader_manager.py | 31 +++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/conductor/tracks/frosted_glass_20260313/plan.md b/conductor/tracks/frosted_glass_20260313/plan.md index bbe2769..e94875e 100644 --- a/conductor/tracks/frosted_glass_20260313/plan.md +++ b/conductor/tracks/frosted_glass_20260313/plan.md @@ -2,7 +2,7 @@ ## Phase 1: Robust Shader & FBO Foundation - [x] Task: Implement: Create `ShaderManager` methods for downsampled FBO setup (scene, temp, blur). [d9148ac] -- [ ] Task: Implement: Develop the "Deep Sea" background shader and integrate it as the FBO source. +- [~] Task: Implement: Develop the "Deep Sea" background shader and integrate it as the FBO source. - [ ] Task: Implement: Develop the 2-pass Gaussian blur shaders with a wide tap distribution. - [ ] Task: Conductor - User Manual Verification 'Phase 1: Robust Foundation' (Protocol in workflow.md) diff --git a/src/shader_manager.py b/src/shader_manager.py index 89509d4..53a9e13 100644 --- a/src/shader_manager.py +++ b/src/shader_manager.py @@ -161,6 +161,7 @@ class BlurPipeline: 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._fb_width: int = 0 self._fb_height: int = 0 @@ -273,6 +274,94 @@ void main() { 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 +void main() { + vec2 pos = vec2(-1.0, -1.0); + if (gl_VertexID == 1) pos = vec2(1.0, -1.0); + else if (gl_VertexID == 2) pos = vec2(-1.0, 1.0); + else if (gl_VertexID == 3) pos = vec2(1.0, 1.0); + gl_Position = vec4(pos, 0.0, 1.0); +} +""" + frag_src = """ +#version 330 core +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 = gl_FragCoord.xy / u_resolution.xy; + 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) + + def render_deepsea_to_fbo(self, width: int, height: int, time: float): + if not self.deepsea_program or not self.scene_fbo: + return + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.scene_fbo) + gl.glViewport(0, 0, width, height) + gl.glClearColor(0.01, 0.05, 0.12, 1.0) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + gl.glUseProgram(self.deepsea_program) + u_time = gl.glGetUniformLocation(self.deepsea_program, "u_time") + if u_time != -1: + gl.glUniform1f(u_time, time) + u_res = gl.glGetUniformLocation(self.deepsea_program, "u_resolution") + if u_res != -1: + gl.glUniform2f(u_res, float(width), float(height)) + gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) + 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) @@ -313,7 +402,7 @@ void main() { 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] + 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: @@ -329,3 +418,4 @@ void main() { self.blur_tex_b = None self.h_blur_program = None self.v_blur_program = None + self.deepsea_program = None diff --git a/tests/test_shader_manager.py b/tests/test_shader_manager.py index 2b729bf..cc81656 100644 --- a/tests/test_shader_manager.py +++ b/tests/test_shader_manager.py @@ -43,6 +43,37 @@ def test_blur_pipeline_compile_shaders(): assert pipeline.h_blur_program is not None assert pipeline.v_blur_program is not None +def test_blur_pipeline_render_deepsea_to_fbo(): + with patch("src.shader_manager.gl") as mock_gl: + tex_counter = iter([10, 20, 30]) + fbo_counter = iter([1, 2, 3]) + mock_gl.glGenTextures.side_effect = lambda n: next(tex_counter) + mock_gl.glGenFramebuffers.side_effect = lambda n: next(fbo_counter) + mock_gl.glCreateProgram.return_value = 300 + mock_gl.glCreateShader.return_value = 400 + mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE + mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE + from src.shader_manager import BlurPipeline + pipeline = BlurPipeline() + pipeline.setup_fbos(800, 600) + pipeline.compile_deepsea_shader() + pipeline.render_deepsea_to_fbo(800, 600, 0.0) + assert mock_gl.glBindFramebuffer.called + assert mock_gl.glUseProgram.called + assert mock_gl.glDrawArrays.called + +def test_blur_pipeline_deepsea_shader_compilation(): + with patch("src.shader_manager.gl") as mock_gl: + mock_gl.glCreateProgram.return_value = 500 + mock_gl.glCreateShader.return_value = 600 + mock_gl.glGetShaderiv.return_value = mock_gl.GL_TRUE + mock_gl.glGetProgramiv.return_value = mock_gl.GL_TRUE + from src.shader_manager import BlurPipeline + pipeline = BlurPipeline() + pipeline.compile_deepsea_shader() + assert mock_gl.glCreateProgram.called + assert pipeline.deepsea_program is not None + def test_blur_pipeline_prepare_blur(): with patch("src.shader_manager.gl") as mock_gl: mock_gl.glGenFramebuffers.return_value = None