OpenGL, learning log
// status: seedling
OpenGL is the API I reach for when I want to get a triangle on screen in an afternoon. It's showing its age — the state machine is a historical accident and "modern" OpenGL still carries 1992's baggage — but the iteration time is unbeatable for prototyping. These are the bits I keep relearning. For the quiet-but-explicit alternative, see Vulkan - learning log.
Versions, profiles, and what you should actually use
- OpenGL 1.x / fixed-function —
glBegin/glEnd. Don't. Every OpenGL tutorial older than 2012 teaches this. Ignore them. - OpenGL 3.3 core — the earliest version where the code you write today looks reasonable. Widely supported. Good floor.
- OpenGL 4.5+ — direct state access (DSA), compute shaders, bindless textures, SPIR-V ingest. If your target supports it, use it. DSA alone is worth the upgrade.
- OpenGL ES 3.x / WebGL 2 — subset for mobile/web. Roughly GL 3.3-flavoured with some GL 4 features.
On macOS, Apple froze OpenGL at 4.1 (no compute, no DSA) and deprecated it entirely in favour of Metal. If I'm targeting Mac, I reach for Vulkan via MoltenVK or just write Metal.
The pipeline in one paragraph
Vertex data lives in a VBO (Vertex Buffer Object). A VAO (Vertex Array Object) remembers the attribute layout that binds those buffers to shader inputs. An optional EBO (Element Buffer Object) holds indices. glDrawElements triggers the pipeline: vertex shader → optional tessellation → optional geometry shader → rasterizer → fragment shader → framebuffer.
GLuint vao, vbo;
;
;
;
;
;
// Attribute 0: vec3 position at offset 0
;
;With DSA (GL 4.5+) the same thing doesn't bind anything globally:
;
;
;
;
;
;
;Shaders
GLSL. Compiled at runtime (or ingested as SPIR-V in 4.6). The shader stages:
- Vertex — one invocation per vertex; writes
gl_Position. - Tessellation control / evaluation — optional; subdivide patches.
- Geometry — optional; emit new primitives. Notoriously slow on many drivers, often better done via compute or instancing.
- Fragment — one invocation per sample (with MSAA) or pixel; writes colour and depth.
- Compute — not part of the draw pipeline; see Compute shaders.
Uniforms send data from CPU to shader. Three main flavours:
glUniform*— individual values per draw. Fine for small stuff.- Uniform Buffer Object (UBO) — for blocks of uniforms shared across draws. Watch the
std140layout:vec3is padded tovec4, and this will bite you. - Shader Storage Buffer Object (SSBO) — larger, writeable from shaders,
std430layout (less padding surprises). Compute-friendly.
Textures and framebuffers
Textures are the other side of state. Samplers bind texture units to shader sampler uniforms. Mipmaps are usually worth generating (glGenerateMipmap) unless you built them externally. Anisotropic filtering is still an extension in spirit but ubiquitous.
Render to texture = create a Framebuffer Object, attach colour/depth textures, draw into it, then sample it from a later pass. That's the whole foundation of post-processing, shadow maps (Shadow mapping), and deferred shading.
The state machine, gently
OpenGL is a giant global State struct. Every function either modifies it or reads from it. This is where most of the pain lives:
- A stray
glBindTexturefrom the previous frame breaks your next draw silently. - Depth test, blend mode, scissor test, cull face — all global, all persist until changed.
- Debug contexts (
glDebugMessageCallback) are the single best quality-of-life upgrade. Turn them on in dev.
DSA (4.5+) fixes most of this: operations take the object explicitly, no bind-to-edit. Use it everywhere you can.
Extensions
The extension mechanism is how the ecosystem experiments. glad or GLEW load them for you. Useful ones:
GL_ARB_bindless_texture— pass texture handles as uniforms instead of binding texture units. Massively reduces draw-call overhead.GL_NV_mesh_shader/GL_EXT_mesh_shader— modern mesh/task shader model.GL_ARB_multi_draw_indirect— batch thousands of draws into one CPU call.GL_KHR_debug— the debug callback mentioned above.
Check the extension before using it; the fallback path matters on Intel/old-driver setups.
A sensible starter stack
- GLFW — windowing and input. Tiny, cross-platform, no politics.
- glad — loader, generated for whatever GL version you target.
- GLM — GLSL-ish math in C++.
- stb_image — loading PNG/JPG/HDR.
- Dear ImGui — debug UI. Ship without it, develop with it.
- RenderDoc — frame debugger. Non-negotiable.
Things that tripped me up
std140padding — avec3 position;is sized asvec4. If your C++ struct mirrors the UBO layout, pad it or usestd430/ per-field queries.- Y axis — OpenGL NDC has +Y up; most image formats have +Y down. Flip in the texture load or in the shader, not both.
- Depth range —
[-1, 1]by default.glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE)(4.5+) switches to[0, 1], which is better for precision and matches Vulkan. - sRGB — textures are usually sRGB-encoded, shaders want linear. Ask the driver to decode on sample by using an
*_SRGBinternal format, and render to an sRGB framebuffer. Don'tpow(2.2)by hand. - Driver differences — NVIDIA is permissive, AMD is stricter, Intel does its own thing. Test on all three before declaring anything "works".
glGetErroris slow — use the debug callback, not a polling loop.
References
- Khronos OpenGL
- LearnOpenGL — the single best modern tutorial
- docs.gl — searchable reference for every entry point
- OpenGL Insights — collection of deeper articles
Back to Notes · see also Vulkan - learning log · Compute shaders · GPU-Driven Visibility for Large Worlds · Debugging Graphics Without Guessing