sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzsrc/rendering/gl_renderer.cpp raw
#include <SDL3/SDL.h>
#include <cstring>
#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<float>& verts,
DynArray<int>& 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<int>(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<float> verts;
verts.reserve(6 * 4 * 6);
DynArray<int> indices;
indices.reserve(6 * 6);
box_Mesh(hx, hy, hz, verts, indices);
draw_Mesh(verts.data(), static_cast<int>(verts.size()) / 6,
indices.data(), static_cast<int>(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<float>(_view_w) * 2.0f, gy = gap / static_cast<float>(_view_h) * 2.0f;
const float lx = len / static_cast<float>(_view_w) * 2.0f, ly = len / static_cast<float>(_view_h) * 2.0f;
const float tx = thick / static_cast<float>(_view_w) * 2.0f, ty = thick / static_cast<float>(_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<size_t>(vert_count)*6*sizeof(float), verts, GL_STREAM_DRAW);
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, _ebo);
glBufferData (GL_ELEMENT_ARRAY_BUFFER, static_cast<size_t>(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<size_t> (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<size_t>(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<size_t>(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<size_t>(vert_count) * 5 * sizeof(float),
verts, GL_STREAM_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _emissive_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
static_cast<size_t>(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<float>(_blur_w),
1.0f / static_cast<float>(_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