Skip to content
HN On Hacker News ↗

Why I still teach OpenGL ES 3.0

▲ 157 points • 93 comments • by majorchord • 1w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,525
PEAK AI % 0% · §1
Analyzed
Oct 27
backend: pangram/v3.3
Segments scanned
1 windows
avg 1525 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,525 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

The Wayback Machine - http://web.archive.org/web/20261027021542/https://eliasfarhan.ch/graphics/cpp/2026/01/27/why-i-teach-opengles.html Little C++ OpenGL ES 3.0 sample compiled to the web with emscripten Introduction It’s been a few years that I have been giving this Introduction to Computer Graphics module (a trimester) at SAE Institute Geneva and I have always been wondering if and when I would need to transition my course from OpenGL ES 3.0 to more modern API. Back in November, I was at the Graphics Programming Conference (my blog post here), and this subject reemerged for me during the conversations with people from the industry as well as from the talks. From the Teardown talk (all the conference’s talks are available on the website), the original game worked in OpenGL 3.3 and from the X-Plane talk, the rendering engine was using OpenGL 2.1. But both those talks were about how they transitioned to more modern API to have better graphics. This sounds like the sunset of OpenGL. Two talks really caught my attention about education for Computer Graphics: the talk Bridging Pixels and Code: Teaching Computer Graphics to Technical Artists from Matthieu Delaere that focus on students creating their own rasterizer from scratch and the talk from Mike Shah that focuses on the next steps after the tutorial. The latter talk actually made me think about if the actual idea to do the transition to modern API for my course made still sense. What have to be taken into account for a transition is that all the courses need to be translated to this new API/framework and updated depending on the new concept added. So if there is a transition to be made, it needs to be really worth it compare to OpenGL ES 3.0. The course content The goal of the introduction in computer graphics module is for the students to implement a 3d scene with a graphics API. What is important to understand is that they should not implement a generic 3d renderer, but specifically a 3d scene, so the final executable is not supposed to be data-driven. For example, it would be completely ok to have such implementation: void DrawScene(const Pipeline& pipeline, const Camera& camera) { //Cull the scene from the camera POV //Draw each 3d object by hand } Since when I started to give this module, I have been following the learnopengl.com structure but using OpenGL ES 3.0 instead. Each chapter is like a list of samples that have to be implemented and tested (Hello Triangle, Hello Texture, etc…) But why specifically this version of OpenGL? OpenGL ES 3.0 is a mobile graphics API, released in August 2012, so about 14 years ago (which is a LONG time in computer graphics). I chose it because it is the most cross-platform version of OpenGL in my opinion, with this version you can run the same C++ program (with some #ifdef) and the same shaders for: Windows: while I encountered some driver issues throughout the year, OpenGL ES 3.0 is compatible with OpenGL 4.3, so I just assume OpenGL 4.3 on Windows not to worry. Linux: some of my workstations boot on Linux by default, and it is good practice to compile the same C++ code with another compiler. Android: this one is a bit complicated as the actual android app boots from Java and calls the native lib containing the code. Nintendo Switch: we have two devkits at school and except removing glew or glad and replacing it with the embedding OpenGL wrapper, it works fine. WebGL2: my favorite reason of using OpenGL ES 3.0 is that it is compatible with WebGL2 with emscripten (except float color attachment that is an extension…), which allows my students to port their samples to their own website/blog. It is the case with this blog post where I simply put the demo out of emscripten on the web page. iOS or MacOSX: unfortunately not supported anymore, but none of my students work on a Mac as this is a Games Programming bachelor. The same demo but running on the Switch Compare to more modern API, synchronization is taken care of by the driver in OpenGL. This allows to skip most of the setup of a lot of boilerplate initialization. This is nice, but is paid with the cost of the driver overhead added for this. However, one of the hassles of the first course is having the students’ laptop chooses the wrong GPU on laptop (using the Intel integrated GPU instead of the Nvidia GPU). Last year, you just needed to set the whole Nvidia configuration to be on “Performant” mode, but with a new Windows update, you need to set individually the application that is to be performant in the Windows settings, not the Nvidia settings anymore. Also, there is this trick for Windows: #ifdef __cplusplus extern "C" { #endif __declspec(dllexport) DWORD NvOptimusEnablement = 1; __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #ifdef __cplusplus } #endif And this (supposedely) enables the discrete GPU and not the integrated one (I am happy this is a solved problem in modern API like Vulkan). Let’s see how we tackle other subjects! Core concepts Every week, the students need to finish about 2-3 samples on a specific feature (for example: implement a sample that uses SSAO using the previous idea from the deferred rendering sample). When working on their samples, the students usually start to build their own abstractions. This year, we decided to work together and build the abstraction in a common git repository such that those abstraction could be used by all the students of the class as soon as it was implemented. This is something that I want to continue in the next years. Pipeline I really dislike OpenGL naming… Pipelines are called program, so when my students comes from learnopengl.com, they name their pipelines shaderProgram. Anyway, we created an abstraction called Pipeline that loads with a vertex shader and a fragment shader (also a specific abstraction) that looks like this: class Shader { public: //shader target being GL_VERTEX_SHADER or GL_FRAGMENT_SHADER void Load(std::string_view path, GLenum shader_target); //[...] class Pipeline { public: void Load(const Shader& vertex_shader, const Shader& fragment_shader); //[...] Shaders are “compiled” with glslangValidator that I configure with CMake. This allows to check for syntax errors during compile-time instead of seeing a weird error in the run-time compilation of the glsl shader by the driver. Because of OpenGL implementation of uniform buffer, we added a bunch of function to set those uniforms and to retrieve the uniform locations (and cache them for future retrieval): class Pipeline { public: //[...] void SetInt(std::string_view uniform_name, int new_value); void SetFloat(std::string_view uniform_name, float new_value); template<typename T> requires core::IsVector3<T, float> void SetVec3(const std::string_view uniform_name, const T& v) { const GLint loc = GetUniformLocation(uniform_name); glUniform3f(loc, v.x, v.y, v.z); } private: GLint GetUniformLocation(std::string_view name); std::unordered_map<std::string, GLint, StringHash, StringEqual> uniform_location_map_; Several things to unpack here: SetVec3 uses the concept IsVector3 that check if the type has the three attributes x, y, z at the correct type. StringHash and StringEqual allows the use of std::string_view without the need to create a std::string when trying to find a uniform location in the map. Vertex inputs One of the weird thing for me going into OpenGL is how memory is allocated (the glGen, glBind, and then upload the data and probably uplaod at the same time). For example, for a vertex input buffer: int vbo_; glGenBuffers(vboCount, &vbo_); glBindBuffer(GL_ARRAY_BUFFER, vbo_); glBufferData(GL_ARRAY_BUFFER, vertex_buffer.size, vertex_buffer.data, GL_STATIC_DRAW); For vertex inputs, you have those buffers that are to be setup in a Vertex Array Object, sort of like a container of vertex buffers, to define how the pipeline is going to read the vertex buffers, with something like this: int vao_; glGenVertexArrays(1, &vao_); glBindVertexArray(vao_); glBindBuffer(GL_ARRAY_BUFFER, vbo_); glVertexAttribPointer( vertex_input_location, // in the shader (location = 0) vertexAttribData.size, // how many of the primitive: float => 1, vec2 => 2, vec3 => 3, vec4 => 4 vertexAttribData.type, GL_FALSE, vertexAttribData.stride, (void*)vertexAttribData.offset); glEnableVertexAttribArray(vertexAttribData.index); //when using instancing glVertexDivisor(vertexAttribData.index, 1); All this is abstracted in the VertexInput class with the Buffer abstraction: template<GLenum target> class Buffer { public: void Bind() const; void Load(); /** * @brief Allocate and upload the data in the buffer */ template<typename T> void UploadRange(std::span<const T> range, GLenum usage = GL_STATIC_DRAW); /** * @brief Update the data in the buffer (to be used after using UploadRange */ template <typename T> void UpdateRange(std::span<const T> range, GLintptr offset = 0, GLenum usage = GL_STATIC_DRAW); }; using VertexBuffer = Buffer<GL_ARRAY_BUFFER>; using IndexBuffer = Buffer<GL_ELEMENT_ARRAY_BUFFER>; struct VertexBufferAttribute { GLuint location; GLint size; GLenum type; GLsizei stride; size_t offset; bool is_instanced = false; }; class VertexInput { public: void Load(); void BindVertexBuffer(const VertexBuffer& vbo, std::span<const VertexBufferAttribute> attributes); void BindIndexBuffer(const IndexBuffer& ebo); void Bind(); }; VertexBufferAttribute::is_instanced allows Buffer to be binded as per instance buffer as we don’t have support for SSBO in OpenGL ES 3.0 (section below). Textures The logic for texture in OpenGL is counter-intuitive. One has to understand the difference between texture name (the handle given by glGenTextures) and the texture unit (GL_TEXTURE0 for example). It confuses a lot of my students each year. So binding a texture to a uniform sample looks like this: glUniform1i(uniform_location, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE2D, my_texture); This is put in a function in the pipeline to look this: