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
This commit is contained in:
2026-03-13 20:25:26 -04:00
parent 2947948ac6
commit d85dc3a1b3
3 changed files with 123 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
## Phase 1: Robust Shader & FBO Foundation ## Phase 1: Robust Shader & FBO Foundation
- [x] Task: Implement: Create `ShaderManager` methods for downsampled FBO setup (scene, temp, blur). [d9148ac] - [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: 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) - [ ] Task: Conductor - User Manual Verification 'Phase 1: Robust Foundation' (Protocol in workflow.md)

View File

@@ -161,6 +161,7 @@ class BlurPipeline:
self.blur_tex_b: int | None = None self.blur_tex_b: int | None = None
self.h_blur_program: int | None = None self.h_blur_program: int | None = None
self.v_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_width: int = 0
self._fb_height: 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.h_blur_program = self._compile_shader(vert_src, h_frag_src)
self.v_blur_program = self._compile_shader(vert_src, v_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]): def _render_quad(self, program: int, src_tex: int, texel_size: tuple[float, float]):
gl.glUseProgram(program) gl.glUseProgram(program)
gl.glActiveTexture(gl.GL_TEXTURE0) gl.glActiveTexture(gl.GL_TEXTURE0)
@@ -313,7 +402,7 @@ void main() {
def cleanup(self): def cleanup(self):
fbos = [f for f in [self.scene_fbo, self.blur_fbo_a, self.blur_fbo_b] if f is not None] 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] 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: if fbos:
gl.glDeleteFramebuffers(len(fbos), fbos) gl.glDeleteFramebuffers(len(fbos), fbos)
if texs: if texs:
@@ -329,3 +418,4 @@ void main() {
self.blur_tex_b = None self.blur_tex_b = None
self.h_blur_program = None self.h_blur_program = None
self.v_blur_program = None self.v_blur_program = None
self.deepsea_program = None

View File

@@ -43,6 +43,37 @@ def test_blur_pipeline_compile_shaders():
assert pipeline.h_blur_program is not None assert pipeline.h_blur_program is not None
assert pipeline.v_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(): def test_blur_pipeline_prepare_blur():
with patch("src.shader_manager.gl") as mock_gl: with patch("src.shader_manager.gl") as mock_gl:
mock_gl.glGenFramebuffers.return_value = None mock_gl.glGenFramebuffers.return_value = None