#include #include #include "rendering/gl_renderer.hpp" #include "utils/frame_profiler.hpp" namespace Nimbin { // ───────────────────────────────────────────────────────────────────────────── // GLSL shaders // ───────────────────────────────────────────────────────────────────────────── static const char* kBlitVertSrc = R"GLSL( #version 330 core layout(location = 0) in vec2 aPos; out vec2 vUV; void main() { vUV = aPos * 0.5 + 0.5; // [-1,1] → [0,1] gl_Position = vec4(aPos, 0.0, 1.0); } )GLSL"; static const char* kBlitFragSrc = R"GLSL( #version 330 core in vec2 vUV; out vec4 FragColor; uniform sampler2D u_SceneTex; void main() { FragColor = texture(u_SceneTex, vUV); } )GLSL"; static const char* kCrosshairFragSrc = R"GLSL( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); } )GLSL"; static const char* kExtractFragSrc = R"GLSL( #version 330 core in vec2 vUV; out vec4 FragColor; uniform sampler2D u_SceneTex; void main() { vec4 s = texture(u_SceneTex, vUV); // The alpha channel carries the emissive_strength written by the main // shader. Multiply the scene RGB by that mask: pixels with alpha=0 // produce zero bright output; pixels with alpha=1 contribute their // full color to the bloom. FragColor = vec4(s.rgb * s.a, 1.0); } )GLSL"; static const char* kBlurFragSrc = R"GLSL( #version 330 core in vec2 vUV; out vec4 FragColor; uniform sampler2D u_BlurTex; uniform vec2 u_BlurDir; uniform vec2 u_BlurTexelSize; // 3-tap separable Gaussian using hardware bilinear filtering. // Equivalent to a 5-tap discrete kernel with sigma ≈ 1.0 // (texels -2..+2 with the standard binomial weights 1,4,6,4,1 / 16). // Center weight = 6/16 = 0.375 // Outer weight = (4+1)/16 = 0.3125 per side // Outer offset = (1*4 + 2*1) / (4+1) = 1.2 texels const float W_CENTER = 0.375; const float W_OFFSET = 0.3125; const float OFFSET = 1.2; void main() { vec2 off = u_BlurDir * u_BlurTexelSize; vec3 col = texture(u_BlurTex, vUV).rgb * W_CENTER; col += texture(u_BlurTex, vUV + off * OFFSET).rgb * W_OFFSET; col += texture(u_BlurTex, vUV - off * OFFSET).rgb * W_OFFSET; FragColor = vec4(col, 1.0); } )GLSL"; static const char* kCompositeFragSrc = R"GLSL( #version 330 core in vec2 vUV; out vec4 FragColor; uniform sampler2D u_SceneTex; uniform sampler2D u_BloomTex; uniform float u_BloomStrength; void main() { vec3 scene = texture(u_SceneTex, vUV).rgb; vec3 bloom = texture(u_BloomTex, vUV).rgb; FragColor = vec4(scene + bloom * u_BloomStrength, 1.0); } )GLSL"; static const char* kVertSrc = R"GLSL( #version 330 core layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNormal; layout(location = 2) in vec3 aColor; uniform mat4 u_MVP; uniform mat4 u_Model; // NEW — for world-space position uniform mat3 u_NormalMat; out vec3 vNormal; out vec3 vColor; out vec3 vWorldPos; // NEW void main() { vec4 worldPos = u_Model * vec4(aPos, 1.0); vWorldPos = worldPos.xyz; gl_Position = u_MVP * vec4(aPos, 1.0); vNormal = normalize(u_NormalMat * aNormal); vColor = aColor; } )GLSL"; static const char* kFragSrc = R"GLSL( #version 330 core in vec3 vNormal; in vec3 vColor; in vec3 vWorldPos; out vec4 FragColor; uniform vec3 u_Color; uniform vec3 u_BodyColor; uniform float u_BackDim; uniform bool u_UseFront; uniform bool u_Wireframe; uniform bool u_UseVertexColor; uniform bool u_Mandelbrot; uniform bool u_WallPaper; uniform vec3 u_LightDir; uniform vec3 u_SunColor; uniform float u_Intensity; uniform vec3 u_Ambient; uniform vec3 u_FlashPos; uniform vec3 u_FlashDir; uniform vec3 u_FlashColor; uniform float u_FlashRange; uniform float u_FlashInnerCos; uniform float u_FlashOuterCos; uniform float u_FlashIntensity; uniform vec3 u_EmissiveColor; uniform float u_EmissiveStrength; const float LINE_SPACING_M = 0.7; const float LINE_THICKNESS = 0.015; const vec3 LINE_COLOR = vec3(0.45, 0.55, 0.78); vec3 paper_color(float world_y, vec3 paper_base) { float t = world_y / LINE_SPACING_M; float frac = abs(t - floor(t + 0.5)); float line_dist = frac * LINE_SPACING_M; float line = 1.0 - smoothstep(LINE_THICKNESS * 0.5, LINE_THICKNESS * 1.5, line_dist); return mix(paper_base, LINE_COLOR, line); } const vec3 BAND_PALETTE[8] = vec3[8]( vec3(0.85, 0.40, 0.55), vec3(0.95, 0.55, 0.30), vec3(0.95, 0.85, 0.35), vec3(0.55, 0.85, 0.40), vec3(0.30, 0.80, 0.75), vec3(0.40, 0.55, 0.95), vec3(0.65, 0.40, 0.90), vec3(0.85, 0.45, 0.85) ); const vec3 INSIDE_SET = vec3(0.04, 0.03, 0.08); const int MAX_ITER = 20; vec3 mandelbrot_color(vec2 c) { vec2 z = vec2(0.0); int i = 0; for (int k = 0; k < MAX_ITER; ++k) { if (dot(z, z) > 1.0) break; z = vec2(z.x*z.x - z.y*z.y + c.x, 2.0*z.x*z.y + c.y); i = k + 1; } if (i >= MAX_ITER) { float angle = atan(z.y, z.x); float t = (angle + 3.14159) / 6.28318; int idx = int(t * 8.0) & 7; return BAND_PALETTE[idx] * 0.35; } return BAND_PALETTE[i & 7]; } vec3 mandelbrot_smooth(vec2 c) { vec2 dx = dFdx(c) * 0.5; vec2 dy = dFdy(c) * 0.5; vec3 col = vec3(0.0); col += mandelbrot_color(c + (-dx - dy) * 0.25); col += mandelbrot_color(c + ( dx - dy) * 0.25); col += mandelbrot_color(c + (-dx + dy) * 0.25); col += mandelbrot_color(c + ( dx + dy) * 0.25); return col * 0.25; } void main() { vec3 baseColor; if (u_Mandelbrot) baseColor = mandelbrot_smooth(vColor.xy); else if (u_WallPaper) baseColor = paper_color(vColor.x, u_Color); else if (u_UseFront) baseColor = (vColor.x > 0.5) ? u_BodyColor * u_BackDim : u_BodyColor; else baseColor = u_UseVertexColor ? vColor : u_Color; if (u_Wireframe) { FragColor = vec4(baseColor, 0.0); return; } vec3 N = normalize(vNormal); float sun_d = max(dot(N, -u_LightDir), 0.0); vec3 sun = u_SunColor * sun_d * u_Intensity; vec3 flash = vec3(0.0); if (u_FlashIntensity > 0.0) { vec3 to_surface = vWorldPos - u_FlashPos; float dist = length(to_surface); vec3 beam = to_surface / max(dist, 0.0001); float forward = dot(u_FlashDir, beam); // 1=on-axis, 0=perp, <0=behind if (forward > u_FlashOuterCos) { float diff = max(dot(N, -beam), 0.0); // Smooth cone: full brightness inside inner_cos, fades to zero at outer_cos. float cone = smoothstep(u_FlashOuterCos, u_FlashInnerCos, forward); // Linear distance falloff; fully dark at flashlight_range. float atten = clamp(1.0 - dist / u_FlashRange, 0.0, 1.0); flash = u_FlashColor * diff * cone * atten * u_FlashIntensity; } } vec3 light_factor = u_Ambient + sun + flash; light_factor = min(light_factor, vec3(1.0)); // Add emissive contribution: makes the surface bright regardless of lighting. vec3 lit = baseColor * light_factor + u_EmissiveColor * u_EmissiveStrength; // Final color. Alpha is the bloom mask — extraction reads this. FragColor = vec4(clamp(lit, 0.0, 1.0), u_EmissiveStrength); } )GLSL"; // ───────────────────────────────────────────────────────────────────────────── // box_Mesh (internal helper for draw_Box) // // Generates a lit, face-normal box centred at origin scaled by hx/hy/hz. // Layout: [px,py,pz, nx,ny,nz] per vertex, 4 verts per face, 2 tris per face. // ───────────────────────────────────────────────────────────────────────────── static void box_Mesh(float hx, float hy, float hz, DynArray& verts, DynArray& indices) { struct Face { float nx,ny,nz; float v[4][3]; }; const Face faces[6] = { // +X face (right): vertices at x=+hx, normal points +X { 1, 0, 0, {{ hx,-hy,-hz },{ hx, hy,-hz },{ hx, hy, hz },{ hx,-hy, hz }} }, // -X face (left): vertices at x=-hx, normal points -X {-1, 0, 0, {{-hx,-hy, hz },{-hx, hy, hz },{-hx, hy,-hz },{-hx,-hy,-hz }} }, // +Y face (top): vertices at y=+hy, normal points +Y { 0, 1, 0, {{-hx, hy,-hz },{ hx, hy,-hz },{ hx, hy, hz },{-hx, hy, hz }} }, // -Y face (bottom):vertices at y=-hy, normal points -Y { 0, -1, 0, {{-hx,-hy, hz },{ hx,-hy, hz },{ hx,-hy,-hz },{-hx,-hy,-hz }} }, // +Z face (front): vertices at z=+hz, normal points +Z { 0, 0, 1, {{-hx,-hy, hz },{-hx, hy, hz },{ hx, hy, hz },{ hx,-hy, hz }} }, // -Z face (back): vertices at z=-hz, normal points -Z { 0, 0, -1, {{ hx,-hy,-hz },{ hx, hy,-hz },{-hx, hy,-hz },{-hx,-hy,-hz }} }, }; for (int f = 0; f < 6; f++) { int base = static_cast(verts.size()) / 6; for (int v = 0; v < 4; v++) { verts.push_back(faces[f].v[v][0]); verts.push_back(faces[f].v[v][1]); verts.push_back(faces[f].v[v][2]); verts.push_back(faces[f].nx); verts.push_back(faces[f].ny); verts.push_back(faces[f].nz); } indices.push_back(base); indices.push_back(base+1); indices.push_back(base+2); indices.push_back(base); indices.push_back(base+2); indices.push_back(base+3); } } // ───────────────────────────────────────────────────────────────────────────── // GlRenderer::draw_Box (new — draws a solid lit box at current MVP) // ───────────────────────────────────────────────────────────────────────────── void GlRenderer::draw_Box(float hx, float hy, float hz) { DynArray verts; verts.reserve(6 * 4 * 6); DynArray indices; indices.reserve(6 * 6); box_Mesh(hx, hy, hz, verts, indices); draw_Mesh(verts.data(), static_cast(verts.size()) / 6, indices.data(), static_cast(indices.size())); } // ───────────────────────────────────────────────────────────────────────────── // Mat4 // ───────────────────────────────────────────────────────────────────────────── Mat4 Mat4::identity() { Mat4 m; m.m[0]=m.m[5]=m.m[10]=m.m[15]=1.0f; return m; } Mat4 Mat4::perspective(float fovY, float aspect, float near, float far) { Mat4 m; float f = 1.0f / SDL_tanf(fovY * 0.5f); m.m[0] = f / aspect; m.m[5] = f; m.m[10] = (far + near) / (near - far); m.m[11] = -1.0f; m.m[14] = (2.0f * far * near) / (near - far); return m; } Mat4 Mat4::mul(const Mat4& a, const Mat4& b) { Mat4 r; for (int col = 0; col < 4; col++) for (int row = 0; row < 4; row++) { float s = 0; for (int k = 0; k < 4; k++) s += a.m[k*4+row] * b.m[col*4+k]; r.m[col*4+row] = s; } return r; } Mat4 buildViewMat4(const Camera& cam) { double yaw = Config::Physics::binrad * (double)cam.yaw; double pitch = Config::Physics::binrad * (double)cam.pitch; double cy = SDL_cos(yaw), sy = SDL_sin(yaw); double cp = SDL_cos(pitch), sp = SDL_sin(pitch); float rx=(float)cy, ry=0.0f, rz=(float)(-sy); float ux=(float)(sy*sp), uy=(float)cp, uz=(float)(cy*sp); float fx=(float)(sy*cp), fy=(float)(-sp),fz=(float)(cy*cp); float tx=(float)cam.pos.x, ty=(float)cam.pos.y, tz=(float)cam.pos.z; float ttx=-(rx*tx+ry*ty+rz*tz); float tty=-(ux*tx+uy*ty+uz*tz); float ttz=-(fx*tx+fy*ty+fz*tz); Mat4 v; v.m[0]=rx; v.m[4]=ry; v.m[8] =rz; v.m[12]=ttx; v.m[1]=ux; v.m[5]=uy; v.m[9] =uz; v.m[13]=tty; v.m[2]=fx; v.m[6]=fy; v.m[10]=fz; v.m[14]=ttz; v.m[3]=0; v.m[7]=0; v.m[11]=0; v.m[15]=1; return v; } Mat4 buildModelMat4(const Vec3& pos, float yaw) { float cy=SDL_cosf(yaw), sy=SDL_sinf(yaw); Mat4 m; m.m[0]= cy; m.m[4]=0.0f; m.m[8] = sy; m.m[12]=(float)pos.x; m.m[1]=0.0f; m.m[5]=1.0f; m.m[9] =0.0f; m.m[13]=(float)pos.y; m.m[2]=-sy; m.m[6]=0.0f; m.m[10]= cy; m.m[14]=(float)pos.z; m.m[3]=0.0f; m.m[7]=0.0f; m.m[11]=0.0f; m.m[15]=1.0f; return m; } Mat4 buildModelMat4(const Vec3& pos, float yaw, float pitch) { float cy = SDL_cosf(yaw), sy = SDL_sinf(yaw); float cp = SDL_cosf(pitch), sp = SDL_sinf(pitch); Mat4 m; // column 0 (rotated X axis) m.m[0] = cy; m.m[1] = 0.0f; m.m[2] = -sy; m.m[3] = 0.0f; // column 1 (rotated Y axis) m.m[4] = sy * sp; m.m[5] = cp; m.m[6] = cy * sp; m.m[7] = 0.0f; // column 2 (rotated Z axis) m.m[8] = sy * cp; m.m[9] = -sp; m.m[10] = cy * cp; m.m[11] = 0.0f; // translation m.m[12] = (float)pos.x; m.m[13] = (float)pos.y; m.m[14] = (float)pos.z; m.m[15] = 1.0f; return m; } // ───────────────────────────────────────────────────────────────────────────── // GlRenderer::init // ───────────────────────────────────────────────────────────────────────────── void GlRenderer::pre_Config() { SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); } bool GlRenderer::init(SDL_Window* window) { _ctx = SDL_GL_CreateContext(window); if (!_ctx) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GL context creation failed: %s", SDL_GetError()); return false; } SDL_GL_MakeCurrent (window, _ctx); SDL_GL_SetSwapInterval(0); int samples = 0; SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES, &samples); SDL_Log("MSAA samples: %d", samples); if (!gladLoadGLES2Loader((GLADloadproc)SDL_GL_GetProcAddress)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize GLAD"); return false; } GLuint vert = compile_Shader(GL_VERTEX_SHADER, kVertSrc); GLuint frag = compile_Shader(GL_FRAGMENT_SHADER, kFragSrc); if (!vert || !frag) return false; if (!link_Program(vert, frag)) return false; glDeleteShader(vert); glDeleteShader(frag); _uMVP = glGetUniformLocation(_prog, "u_MVP"); _uNormalMat = glGetUniformLocation(_prog, "u_NormalMat"); _uColor = glGetUniformLocation(_prog, "u_Color"); _uBodyColor = glGetUniformLocation(_prog, "u_BodyColor"); _uBackDim = glGetUniformLocation(_prog, "u_BackDim"); _uUseFront = glGetUniformLocation(_prog, "u_UseFront"); _uLightDir = glGetUniformLocation(_prog, "u_LightDir"); _uAmbient = glGetUniformLocation(_prog, "u_Ambient"); _uIntensity = glGetUniformLocation(_prog, "u_Intensity"); _uWireframe = glGetUniformLocation(_prog, "u_Wireframe"); _uUseVertexColor = glGetUniformLocation(_prog, "u_UseVertexColor"); _uMandelbrot = glGetUniformLocation(_prog, "u_Mandelbrot"); _uWallPaper = glGetUniformLocation(_prog, "u_WallPaper"); _uTextLineSpacing= glGetUniformLocation(_prog, "u_TextLineSpacing"); _uModel = glGetUniformLocation(_prog, "u_Model"); _uSunColor = glGetUniformLocation(_prog, "u_SunColor"); _uFlashPos = glGetUniformLocation(_prog, "u_FlashPos"); _uFlashDir = glGetUniformLocation(_prog, "u_FlashDir"); _uFlashColor = glGetUniformLocation(_prog, "u_FlashColor"); _uFlashRange = glGetUniformLocation(_prog, "u_FlashRange"); _uFlashInnerCos = glGetUniformLocation(_prog, "u_FlashInnerCos"); _uFlashOuterCos = glGetUniformLocation(_prog, "u_FlashOuterCos"); _uFlashIntensity = glGetUniformLocation(_prog, "u_FlashIntensity"); glGenVertexArrays (1, &_vao); glGenBuffers(1, &_vbo); glGenBuffers(1, &_ebo); glBindVertexArray (_vao); glBindBuffer (GL_ARRAY_BUFFER, _vbo); glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer (1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray (0); glGenVertexArrays (1, &_line_vao); glGenBuffers(1, &_line_vbo); glBindVertexArray (_line_vao); glBindBuffer (GL_ARRAY_BUFFER, _line_vbo); glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttrib3f (1, 0.0f, 0.0f, 1.0f); glBindVertexArray (0); glGenVertexArrays(1, &_vao_c); glGenBuffers (1, &_vbo_c); glGenBuffers (1, &_ebo_c); glBindVertexArray(_vao_c); glBindBuffer(GL_ARRAY_BUFFER, _vbo_c); // 9 floats per vertex: pos(3) + normal(3) + color(3) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); glBindVertexArray(0); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // Bloom infrastructure setup. create_FullscreenQuad_(); create_BlitProgram_(); create_ExtractProgram_(); create_BlurProgram_(); create_CompositeProgram_(); { // crosshair: reuse the blit vertex shader (vec2 aPos @ loc 0) GLuint v = compile_Shader(GL_VERTEX_SHADER, kBlitVertSrc); GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kCrosshairFragSrc); _crosshair_prog = glCreateProgram(); glAttachShader(_crosshair_prog, v); glAttachShader(_crosshair_prog, f); glLinkProgram (_crosshair_prog); glDeleteShader(v); glDeleteShader(f); glGenVertexArrays(1, &_crosshair_vao); glGenBuffers (1, &_crosshair_vbo); glBindVertexArray(_crosshair_vao); glBindBuffer (GL_ARRAY_BUFFER, _crosshair_vbo); glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); } // Lookup the new emissive uniforms on the main program. _uEmissiveColor = glGetUniformLocation(_prog, "u_EmissiveColor"); _uEmissiveStrength = glGetUniformLocation(_prog, "u_EmissiveStrength"); { // thats for the menu static const float tri[] = { 0.0f, 0.5f, 0.0f, -0.5f,-0.5f, 0.0f, 0.5f,-0.5f, 0.0f }; glGenVertexArrays(1, &_menu_vao); glGenBuffers (1, &_menu_vbo); glBindVertexArray(_menu_vao); glBindBuffer (GL_ARRAY_BUFFER, _menu_vbo); glBufferData (GL_ARRAY_BUFFER, sizeof(tri), tri, GL_STATIC_DRAW); glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttrib3f (1, 0.0f, 0.0f, 1.0f); glBindVertexArray(0); } SDL_Log("OpenGL %s, GLSL %s", glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION)); return true; } void GlRenderer::begin_Frame(int w, int h) { _view_w = w; _view_h = h; _aspect = (h != 0) ? (float)w/(float)h : 1.0f; // Lazy-create or resize the FBO to match the window. if (_scene_fbo == 0) create_FBO_(w, h); else resize_FBO_(w, h); // Bind the FBO as the render target. glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo_ms); glViewport (0, 0, w, h); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(_prog); glUniform1i (_uMandelbrot, 0); glUniform1i (_uWallPaper, 0); glUniform1i (_uUseFront, 0); Mat4 ident = Mat4::identity(); glUniformMatrix4fv(_uModel, 1, GL_FALSE, ident.m); glUniform1i (_uUseVertexColor, 0); glUniform3f (_uEmissiveColor, 0.0f, 0.0f, 0.0f); glUniform1f (_uEmissiveStrength, 0.0f); _proj = Mat4::perspective(SDL_PI_F/3.0f, _aspect, 0.1f, 200.0f); } void GlRenderer::end_Frame(SDL_Window* window) { glBindFramebuffer(GL_READ_FRAMEBUFFER, _scene_fbo_ms); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _scene_fbo); glBlitFramebuffer(0, 0, _fbo_w, _fbo_h, 0, 0, _fbo_w, _fbo_h, GL_COLOR_BUFFER_BIT, GL_NEAREST); // MS resolve requires NEAREST glBindFramebuffer(GL_FRAMEBUFFER, 0); // Run bloom passes (extract + blur). { FP_ZONE("end_Frame.bloom"); run_BloomPipeline_(); // Composite scene + bloom to screen. glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, _view_w, _view_h); glDisable(GL_DEPTH_TEST); glUseProgram (_composite_prog); glActiveTexture(GL_TEXTURE0); glBindTexture (GL_TEXTURE_2D, _scene_color_tex); glUniform1i (_uComposeScene, 0); glActiveTexture(GL_TEXTURE1); glBindTexture (GL_TEXTURE_2D, _blur_tex[1]); // final bloom glUniform1i (_uComposeBloom, 1); glUniform1f (_uComposeStrength, _bloom_strength); } { FP_ZONE("end_Frame.composite"); glBindVertexArray(_fullscreen_vao); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); glActiveTexture(GL_TEXTURE0); // reset to default unit glEnable(GL_DEPTH_TEST); glUseProgram(_prog); } draw_Crosshair_(); { FP_ZONE("end_Frame.swap"); SDL_GL_SwapWindow(window); } } void GlRenderer::clear_Depth() { glClear(GL_DEPTH_BUFFER_BIT); } // ───────────────────────────────────────────────────────────────────────────── // Uniform setters // ───────────────────────────────────────────────────────────────────────────── void GlRenderer::set_MVP(const Mat4& model, const Mat4& mvp) { glUniformMatrix4fv(_uMVP, 1, GL_FALSE, mvp.m); glUniformMatrix4fv(_uModel, 1, GL_FALSE, model.m); } void GlRenderer::set_NormalMat(const Mat4& model) { // Upper-left 3×3 of model IS the normal matrix for orthogonal transforms // (rotation + translation only). Normals land in world space, which matches // the world-space lighting in the fragment shader. // If non-uniform scale is ever added, replace with transpose(inverse(model_3x3)). float n[9] = { model.m[0], model.m[1], model.m[2], model.m[4], model.m[5], model.m[6], model.m[8], model.m[9], model.m[10] }; glUniformMatrix3fv(_uNormalMat, 1, GL_FALSE, n); } void GlRenderer::set_Color(float r, float g, float b) { glUniform3f(_uColor, r, g, b); } void GlRenderer::set_Light(const Light& light) { // Sun glUniform3f(_uLightDir, (float)light.sun_direction.x, (float)light.sun_direction.y, (float)light.sun_direction.z); glUniform3f(_uSunColor, (float)light.sun_color.x, (float)light.sun_color.y, (float)light.sun_color.z); glUniform1f(_uIntensity, light.sun_intensity); // Ambient glUniform3f(_uAmbient, (float)light.ambient.x, (float)light.ambient.y, (float)light.ambient.z); // Flashlight glUniform3f(_uFlashPos, (float)light.flashlight_pos.x, (float)light.flashlight_pos.y, (float)light.flashlight_pos.z); glUniform3f(_uFlashDir, (float)light.flashlight_dir.x, (float)light.flashlight_dir.y, (float)light.flashlight_dir.z); glUniform3f(_uFlashColor, (float)light.flashlight_color.x, (float)light.flashlight_color.y, (float)light.flashlight_color.z); glUniform1f(_uFlashRange, light.flashlight_range); glUniform1f(_uFlashInnerCos, light.flashlight_inner_cos); glUniform1f(_uFlashOuterCos, light.flashlight_outer_cos); glUniform1f(_uFlashIntensity, light.flashlight_intensity); } void GlRenderer::set_Wireframe(bool on) { glUniform1i(_uWireframe, on ? 1 : 0); } void GlRenderer::set_ColorUseVertex(bool on) { glUniform1i(_uUseVertexColor, on ? 1 : 0); } void GlRenderer::set_TextLineSpacing(float m) { glUniform1f(_uTextLineSpacing, m); } void GlRenderer::set_Emissive(float r, float g, float b, float strength) { glUniform3f(_uEmissiveColor, r, g, b); glUniform1f(_uEmissiveStrength, strength); } void GlRenderer::set_FaceShade(float r, float g, float b, float back_dim, bool use_front) { glUniform3f(_uBodyColor, r, g, b); glUniform1f(_uBackDim, back_dim); glUniform1i(_uUseFront, use_front ? 1 : 0); } void GlRenderer::set_DepthTest(bool on) { if (on) glEnable (GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); } void GlRenderer::set_CullBack(bool on) { if (on) { glEnable(GL_CULL_FACE); glCullFace(GL_FRONT); } else glDisable(GL_CULL_FACE); } ///////////////// /// DRAW void GlRenderer::draw_Crosshair_() { if (!_crosshair_enabled || _view_w <= 0 || _view_h <= 0) return; // Pixel dimensions → NDC half-extents (per-axis, so it stays square). const float gap = 4.0f, len = 9.0f, thick = 1.5f; const float gx = gap / static_cast(_view_w) * 2.0f, gy = gap / static_cast(_view_h) * 2.0f; const float lx = len / static_cast(_view_w) * 2.0f, ly = len / static_cast(_view_h) * 2.0f; const float tx = thick / static_cast(_view_w) * 2.0f, ty = thick / static_cast(_view_h) * 2.0f; // Four arms, center gap, NO overlap (overlap would invert twice = no-op). float v[48]; int k = 0; auto quad = [&](float x0, float y0, float x1, float y1) { v[k++]=x0; v[k++]=y0; v[k++]=x1; v[k++]=y0; v[k++]=x1; v[k++]=y1; v[k++]=x0; v[k++]=y0; v[k++]=x1; v[k++]=y1; v[k++]=x0; v[k++]=y1; }; quad( gx, -ty, gx + lx, ty); // right quad(-(gx + lx), -ty, -gx, ty); // left quad( -tx, gy, tx, gy + ly); // up quad( -tx, -(gy + ly), tx, -gy); // down glDisable (GL_DEPTH_TEST); glEnable (GL_BLEND); glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); // out = (1 - dst) → invert glUseProgram(_crosshair_prog); glBindVertexArray(_crosshair_vao); glBindBuffer (GL_ARRAY_BUFFER, _crosshair_vbo); glBufferData (GL_ARRAY_BUFFER, sizeof(v), v, GL_STREAM_DRAW); glDrawArrays (GL_TRIANGLES, 0, 24); glBindVertexArray(0); glDisable (GL_BLEND); glEnable (GL_DEPTH_TEST); glUseProgram(_prog); } // ───────────────────────────────────────────────────────────────────────────── // draw_Mesh / draw_Lines // ───────────────────────────────────────────────────────────────────────────── void GlRenderer::draw_Mesh(const float* verts, int vert_count, const int* indices, int index_count) { glBindVertexArray(_vao); glBindBuffer (GL_ARRAY_BUFFER, _vbo); glBufferData (GL_ARRAY_BUFFER, static_cast(vert_count)*6*sizeof(float), verts, GL_STREAM_DRAW); glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, _ebo); glBufferData (GL_ELEMENT_ARRAY_BUFFER, static_cast(index_count)*sizeof(int), indices, GL_STREAM_DRAW); glDrawElements (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); } void GlRenderer::draw_MeshColored(const float* verts, int vert_count, const int* indices, int index_count) { glBindVertexArray(_vao_c); glBindBuffer (GL_ARRAY_BUFFER, _vbo_c); glBufferData (GL_ARRAY_BUFFER, static_cast (vert_count) * 9 * sizeof(float), verts, GL_STATIC_DRAW); // Re-point attributes at _vbo_c — draw_MeshCached may have repointed // this shared VAO at a per-Text::Token::Data VBO. glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(3*sizeof(float))); glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(6*sizeof(float))); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo_c); glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast(index_count) * sizeof(int), indices, GL_STATIC_DRAW); glDrawElements (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); } void GlRenderer::draw_MeshCached(unsigned int& vbo, unsigned int& ebo, int& index_count, bool dirty, const float* verts, int vert_count, const int* indices, int idx_count) { glBindVertexArray(_vao_c); if (vbo == 0) glGenBuffers(1, &vbo); if (ebo == 0) glGenBuffers(1, &ebo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); if (dirty) { glBufferData(GL_ARRAY_BUFFER, (size_t)vert_count * 9 * sizeof(float), verts, GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (size_t)idx_count * sizeof(int), indices, GL_STATIC_DRAW); index_count = idx_count; } // _vao_c's attribute pointers were bound to _vbo_c at init; rebind to ours. glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(3*sizeof(float))); glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(6*sizeof(float))); glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); } void GlRenderer::free_MeshBuffers(unsigned int vbo, unsigned int ebo) { if (!SDL_GL_GetCurrentContext()) return; if (vbo) glDeleteBuffers(1, &vbo); if (ebo) glDeleteBuffers(1, &ebo); } void GlRenderer::draw_Lines(const float* points, int point_count) { glBindVertexArray(_line_vao); glBindBuffer(GL_ARRAY_BUFFER, _line_vbo); glBufferData(GL_ARRAY_BUFFER, static_cast(point_count)*3*sizeof(float), points, GL_STREAM_DRAW); glDrawArrays(GL_LINES, 0, point_count); glBindVertexArray(0); } void GlRenderer::draw_Menu() { glUseProgram(_prog); Mat4 identity = Mat4::identity(); glUniformMatrix4fv(_uMVP, 1, GL_FALSE, identity.m); float n[9] = {1,0,0, 0,1,0, 0,0,1}; glUniformMatrix3fv(_uNormalMat, 1, GL_FALSE, n); glUniform3f(_uColor, 0.2f, 0.4f, 0.8f); glUniform3f(_uLightDir, 0.0f, 0.0f, 1.0f); glUniform3f(_uAmbient, 1.0f, 1.0f, 1.0f); glUniform1f(_uIntensity, 1.0f); glUniform1i(_uWireframe, 1); glUniform3f(_uSunColor, 1.0f, 1.0f, 1.0f); glUniform1i(_uMandelbrot, 0); glUniform1i(_uWallPaper, 0); glUniform3f(_uFlashPos, 0.0f, 0.0f, 0.0f); glUniform3f(_uFlashDir, 0.0f, 0.0f, -1.0f); glUniform3f(_uFlashColor, 0.0f, 0.0f, 0.0f); glUniform1f(_uFlashRange, 1.0f); glUniform1f(_uFlashInnerCos, 0.97f); glUniform1f(_uFlashOuterCos, 0.90f); glUniform1f(_uFlashIntensity, 0.0f); Mat4 ident_m = Mat4::identity(); glUniformMatrix4fv(_uModel, 1, GL_FALSE, ident_m.m); glBindVertexArray(_menu_vao); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); } void GlRenderer::draw_Emissive(const Camera& cam, const float* verts, int vert_count, const int* indices, int index_count, float r, float g, float b, float strength) { if (!_emissive_prog || vert_count == 0 || index_count == 0) return; // Push state: switch program, enable additive blending, disable depth // write (we still depth-TEST so halos respect occluders, but we don't // want halos blocking each other in the depth buffer). glUseProgram(_emissive_prog); // The vertex shader needs an MVP. We use the current proj * view * I — // verts are already in world space. Mat4 view = buildViewMat4(cam); Mat4 mvp = Mat4::mul(_proj, view); glUniformMatrix4fv(_uEmissiveMVP, 1, GL_FALSE, mvp.m); glUniform3f(_uEmissiveColor, r, g, b); glUniform1f(_uEmissiveStrength, strength); glEnable (GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); // additive glDepthMask(GL_FALSE); // no depth write glBindVertexArray(_emissive_vao); glBindBuffer(GL_ARRAY_BUFFER, _emissive_vbo); glBufferData(GL_ARRAY_BUFFER, static_cast(vert_count) * 5 * sizeof(float), verts, GL_STREAM_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _emissive_ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast(index_count) * sizeof(int), indices, GL_STREAM_DRAW); glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); // Pop state. glDisable (GL_BLEND); glDepthMask (GL_TRUE); glUseProgram(_prog); // restore main program } // ───────────────────────────────────────────────────────────────────────────── // Shader helpers // ───────────────────────────────────────────────────────────────────────────── GLuint GlRenderer::compile_Shader(GLenum type, const char* src) { GLuint s = glCreateShader(type); glShaderSource (s, 1, &src, nullptr); glCompileShader(s); GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(s,512,nullptr,log); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader compile error: %s", log); glDeleteShader(s); return 0; } return s; } bool GlRenderer::link_Program(GLuint vert, GLuint frag) { _prog = glCreateProgram(); glAttachShader(_prog, vert); glAttachShader(_prog, frag); glLinkProgram (_prog); GLint ok; glGetProgramiv(_prog, GL_LINK_STATUS, &ok); if (!ok) { char log[512]; glGetProgramInfoLog(_prog,512,nullptr,log); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Program link error: %s", log); glDeleteProgram(_prog); _prog=0; return false; } return true; } float GlRenderer::get_Aspect() const { return _aspect; } const Mat4& GlRenderer::get_Projection() const { return _proj; } void GlRenderer::enable_MandelbrotMode(bool on) { glUniform1i(_uMandelbrot, on ? 1 : 0); } void GlRenderer::enable_WallPaperMode(bool on) { glUniform1i(_uWallPaper, on ? 1 : 0); } void GlRenderer::create_FullscreenQuad_() { static const float quad_verts[] = { -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, }; glGenVertexArrays(1, &_fullscreen_vao); glGenBuffers(1, &_fullscreen_vbo); glBindVertexArray(_fullscreen_vao); glBindBuffer(GL_ARRAY_BUFFER, _fullscreen_vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindVertexArray(0); } void GlRenderer::create_BlitProgram_() { GLuint v = compile_Shader(GL_VERTEX_SHADER, kBlitVertSrc); GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kBlitFragSrc); _blit_prog = glCreateProgram(); glAttachShader(_blit_prog, v); glAttachShader(_blit_prog, f); glLinkProgram(_blit_prog); GLint ok=0; glGetProgramiv(_blit_prog, GL_LINK_STATUS, &ok); if (!ok) { char log[512]; glGetProgramInfoLog(_blit_prog, 512, nullptr, log); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Blit program link error: %s", log); } glDeleteShader(v); glDeleteShader(f); _uBlitTex = glGetUniformLocation(_blit_prog, "u_SceneTex"); } void GlRenderer::create_FBO_(int w, int h) { GLint max_samples = 0; glGetIntegerv(GL_MAX_SAMPLES, &max_samples); if (_msaa_samples > max_samples) _msaa_samples = max_samples; if (_msaa_samples < 1) _msaa_samples = 1; // ── Multisampled FBO: the scene renders here ──────────────────────── glGenFramebuffers(1, &_scene_fbo_ms); glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo_ms); glGenRenderbuffers(1, &_scene_color_rbo_ms); glBindRenderbuffer(GL_RENDERBUFFER, _scene_color_rbo_ms); glRenderbufferStorageMultisample(GL_RENDERBUFFER, _msaa_samples, GL_RGBA8, w, h); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _scene_color_rbo_ms); glGenRenderbuffers(1, &_scene_depth_rbo_ms); glBindRenderbuffer(GL_RENDERBUFFER, _scene_depth_rbo_ms); glRenderbufferStorageMultisample(GL_RENDERBUFFER, _msaa_samples, GL_DEPTH_COMPONENT24, w, h); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _scene_depth_rbo_ms); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "MSAA scene FBO incomplete"); // ── Single-sample resolve FBO: bloom + composite read this texture ── glGenFramebuffers(1, &_scene_fbo); glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo); glGenTextures(1, &_scene_color_tex); glBindTexture(GL_TEXTURE_2D, _scene_color_tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _scene_color_tex, 0); // Depth now lives on the MS FBO, so the resolve target needs none. if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Resolve FBO incomplete"); glBindFramebuffer(GL_FRAMEBUFFER, 0); _fbo_w = w; _fbo_h = h; } void GlRenderer::create_ExtractProgram_() { GLuint v = compile_Shader(GL_VERTEX_SHADER, kBlitVertSrc); GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kExtractFragSrc); _extract_prog = glCreateProgram(); glAttachShader(_extract_prog, v); glAttachShader(_extract_prog, f); glLinkProgram(_extract_prog); glDeleteShader(v); glDeleteShader(f); _uExtractTex = glGetUniformLocation(_extract_prog, "u_SceneTex"); } void GlRenderer::create_BlurProgram_() { GLuint v = compile_Shader(GL_VERTEX_SHADER, kBlitVertSrc); GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kBlurFragSrc); _blur_prog = glCreateProgram(); glAttachShader(_blur_prog, v); glAttachShader(_blur_prog, f); glLinkProgram(_blur_prog); glDeleteShader(v); glDeleteShader(f); _uBlurTex = glGetUniformLocation(_blur_prog, "u_BlurTex"); _uBlurDir = glGetUniformLocation(_blur_prog, "u_BlurDir"); _uBlurTexelSize = glGetUniformLocation(_blur_prog, "u_BlurTexelSize"); } void GlRenderer::create_BloomFBOs_(int scene_w, int scene_h) { int w = scene_w / 4; int h = scene_h / 4; if (w < 1) w = 1; if (h < 1) h = 1; _blur_w = w; _blur_h = h; auto make_fbo = [&](GLuint& fbo, GLuint& tex) { glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); GLenum s = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (s != GL_FRAMEBUFFER_COMPLETE) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Bloom FBO incomplete: 0x%x", s); }; make_fbo(_bright_fbo, _bright_tex); make_fbo(_blur_fbo[0], _blur_tex[0]); make_fbo(_blur_fbo[1], _blur_tex[1]); glBindFramebuffer(GL_FRAMEBUFFER, 0); } void GlRenderer::destroy_BloomFBOs_() { if (_bright_tex) { glDeleteTextures (1, &_bright_tex); _bright_tex = 0; } if (_bright_fbo) { glDeleteFramebuffers(1, &_bright_fbo); _bright_fbo = 0; } for (int i = 0; i < 2; i++) { if (_blur_tex[i]) { glDeleteTextures (1, &_blur_tex[i]); _blur_tex[i] = 0; } if (_blur_fbo[i]) { glDeleteFramebuffers(1, &_blur_fbo[i]); _blur_fbo[i] = 0; } } _blur_w = 0; _blur_h = 0; } void GlRenderer::resize_BloomFBOs_(int scene_w, int scene_h) { int want_w = scene_w / 4; if (want_w < 1) want_w = 1; int want_h = scene_h / 4; if (want_h < 1) want_h = 1; if (want_w == _blur_w && want_h == _blur_h) return; destroy_BloomFBOs_(); create_BloomFBOs_(scene_w, scene_h); } void GlRenderer::run_BloomPipeline_() { { FP_ZONE("bloom.resize"); resize_BloomFBOs_(_fbo_w, _fbo_h); } glDisable(GL_DEPTH_TEST); glBindVertexArray(_fullscreen_vao); { FP_ZONE("bloom.extract"); glBindFramebuffer(GL_FRAMEBUFFER, _bright_fbo); glViewport(0, 0, _blur_w, _blur_h); glUseProgram(_extract_prog); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _scene_color_tex); glUniform1i(_uExtractTex, 0); glDrawArrays(GL_TRIANGLES, 0, 6); } // ── Blur ping-pong ────────────────────────────────────────────────── // First iteration reads from _bright_tex, then ping-pongs between // _blur_tex[0] and _blur_tex[1]. 4 full iterations = 8 passes. { FP_ZONE("bloom.blur"); const int ITERATIONS = 2; glUseProgram(_blur_prog); glUniform2f(_uBlurTexelSize, 1.0f / static_cast(_blur_w), 1.0f / static_cast(_blur_h)); GLuint src_tex = _bright_tex; for (int i = 0; i < ITERATIONS; i++) { // Horizontal pass: src → blur_tex[0] via blur_fbo[0] glBindFramebuffer(GL_FRAMEBUFFER, _blur_fbo[0]); glBindTexture(GL_TEXTURE_2D, src_tex); glUniform1i(_uBlurTex, 0); glUniform2f(_uBlurDir, 1.0f, 0.0f); glDrawArrays(GL_TRIANGLES, 0, 6); // Vertical pass: blur_tex[0] → blur_tex[1] via blur_fbo[1] glBindFramebuffer(GL_FRAMEBUFFER, _blur_fbo[1]); glBindTexture(GL_TEXTURE_2D, _blur_tex[0]); glUniform1i(_uBlurTex, 0); glUniform2f(_uBlurDir, 0.0f, 1.0f); glDrawArrays(GL_TRIANGLES, 0, 6); // Next iteration's input is blur_tex[1]. We swap the names so the // final result always ends up in blur_tex[0] for compositing. src_tex = _blur_tex[1]; } } // After loop: final bloom is in _blur_tex[1]. We'll sample it directly. glBindVertexArray(0); glEnable(GL_DEPTH_TEST); } void GlRenderer::create_CompositeProgram_() { GLuint v = compile_Shader(GL_VERTEX_SHADER, kBlitVertSrc); GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kCompositeFragSrc); _composite_prog = glCreateProgram(); glAttachShader(_composite_prog, v); glAttachShader(_composite_prog, f); glLinkProgram(_composite_prog); glDeleteShader(v); glDeleteShader(f); _uComposeScene = glGetUniformLocation(_composite_prog, "u_SceneTex"); _uComposeBloom = glGetUniformLocation(_composite_prog, "u_BloomTex"); _uComposeStrength = glGetUniformLocation(_composite_prog, "u_BloomStrength"); } void GlRenderer::destroy_FBO_() { if (_scene_color_tex) {glDeleteTextures (1, &_scene_color_tex); _scene_color_tex = 0;} if (_scene_depth_rbo) {glDeleteRenderbuffers(1, &_scene_depth_rbo); _scene_depth_rbo = 0;} if (_scene_fbo) {glDeleteFramebuffers (1, &_scene_fbo); _scene_fbo = 0;} if (_scene_color_rbo_ms){glDeleteRenderbuffers(1, &_scene_color_rbo_ms);_scene_color_rbo_ms= 0;} if (_scene_depth_rbo_ms){glDeleteRenderbuffers(1, &_scene_depth_rbo_ms);_scene_depth_rbo_ms= 0;} if (_scene_fbo_ms) {glDeleteFramebuffers (1, &_scene_fbo_ms); _scene_fbo_ms = 0;} _fbo_w = 0; _fbo_h = 0; } void GlRenderer::resize_FBO_(int w, int h) { if (w == _fbo_w && h == _fbo_h) return; destroy_FBO_(); create_FBO_(w, h); } } // namespace Nimbin