Why I nonetheless educate OpenGL ES 3.0


Little C++ OpenGL ES 3.0 pattern compiled to the net with emscripten

Introduction

It’s been a number of years that I’ve been giving this Introduction to Computer Graphics module (a trimester) at SAE Institute Geneva and I’ve at all times been questioning if and after I would wish to transition my course from OpenGL ES 3.0 to extra fashionable API.

Back in November, I used to be on the Graphics Programming Conference (my weblog publish here), and this topic reemerged for me in the course of the conversations with individuals from the business in addition to from the talks. From the Teardown discuss (all of the convention’s talks can be found on the website), the unique sport labored in OpenGL 3.3 and from the X-Plane discuss, the rendering engine was utilizing OpenGL 2.1. But each these talks have been about how they transitioned to extra fashionable API to have higher graphics. This sounds just like the sundown of OpenGL.

Two talks actually caught my consideration about schooling for Computer Graphics: the discuss Bridging Pixels and Code: Teaching Computer Graphics to Technical Artists from Matthieu Delaere that target college students creating their very own rasterizer from scratch and the discuss from Mike Shah that focuses on the following steps after the tutorial. The latter discuss truly made me take into consideration if the precise concept to do the transition to fashionable API for my course made nonetheless sense.

What need to be taken into consideration for a transition is that every one the programs have to be translated to this new API/framework and up to date relying on the brand new idea added. So if there’s a transition to be made, it must be actually value it evaluate to OpenGL ES 3.0.

The course content material

The objective of the introduction in pc graphics module is for the scholars to implement a 3d scene with a graphics API. What is vital to know is that they need to not implement a generic 3d renderer, however particularly a 3d scene, so the ultimate executable just isn’t purported to be data-driven. For instance, it will be utterly alright to have such implementation:

void DrawScene(const Pipeline& pipeline, const Camera& digital camera)
{
  //Cull the scene from the digital camera POV

  //Draw every 3d object by hand

}

Since after I began to present this module, I’ve been following the learnopengl.com construction however utilizing OpenGL ES 3.0 as an alternative. Each chapter is sort of a record of samples that need to be applied and examined (Hello Triangle, Hello Texture, and many others…)

But why particularly this model of OpenGL? OpenGL ES 3.0 is a cell graphics API, launched in August 2012, so about 14 years in the past (which is a LONG time in pc graphics). I selected it as a result of it’s the most cross-platform model of OpenGL for my part, with this model you’ll be able to run the identical C++ program (with some #ifdef) and the identical shaders for:

  • Windows: whereas I encountered some driver points all year long, OpenGL ES 3.0 is appropriate with OpenGL 4.3, so I simply assume OpenGL 4.3 on Windows to not fear.
  • Linux: a few of my workstations boot on Linux by default, and it’s good observe to compile the identical C++ code with one other compiler.
  • Android: this one is a bit difficult because the precise android app boots from Java and calls the native lib containing the code.
  • Nintendo Switch: we now have two devkits at college and besides eradicating glew or glad and changing it with the embedding OpenGL wrapper, it really works wonderful.
  • WebGL2: my favourite purpose of utilizing OpenGL ES 3.0 is that it’s appropriate with WebGL2 with emscripten (besides float shade attachment that’s an extension…), which permits my college students to port their samples to their very own web site/weblog. It is the case with this weblog publish the place I merely put the demo out of emscripten on the internet web page.
  • iOS or MacOSX: sadly not supported anymore, however none of my college students work on a Mac as it is a Games Programming bachelor.

The identical demo however operating on the Switch

Compare to extra fashionable API, synchronization is taken care of by the driving force in OpenGL. This permits to skip many of the setup of loads of boilerplate initialization. This is good, however is paid with the price of the driving force overhead added for this.

However, one of many hassles of the primary course is having the scholars’ laptop computer chooses the flawed GPU on laptop computer (utilizing the Intel built-in GPU as an alternative of the Nvidia GPU). Last yr, you simply wanted to set the entire Nvidia configuration to be on “Performant” mode, however with a brand new Windows replace, you must set individually the applying that’s to be performant within the Windows settings, not the Nvidia settings anymore. Also, there may be this trick for Windows:

#ifdef __cplusplus
extern "C" {
#endif

__declspec(dllexport) DWORD NvOptimusEnablement = 1;
__declspec(dllexport) int AmdPowerXpressRequestHighEfficiency = 1;

#ifdef __cplusplus
}
#endif

And this (supposedely) allows the discrete GPU and never the built-in one (I’m glad it is a solved drawback in fashionable API like Vulkan). Let’s see how we sort out different topics!

Core ideas

Every week, the scholars want to complete about 2-3 samples on a particular function (for instance: implement a pattern that makes use of SSAO utilizing the earlier concept from the deferred rendering pattern).
When engaged on their samples, the scholars normally begin to construct their very own abstractions. This yr, we determined to work collectively and construct the abstraction in a typical git repository such that these abstraction could possibly be utilized by all the scholars of the category as quickly because it was applied. This is one thing that I need to proceed within the subsequent years.

Pipeline

I actually dislike OpenGL naming… Pipelines are known as program, so when my college students comes from learnopengl.com, they identify their pipelines shaderProgram. Anyway, we created an abstraction known as Pipeline that hundreds with a vertex shader and a fraction shader (additionally a particular abstraction) that appears like this:

class Shader
{
public:
//shader goal 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 permits to examine for syntax errors throughout compile-time as an alternative of seeing a bizarre error within the run-time compilation of the glsl shader by the driving force.

Because of OpenGL implementation of uniform buffer, we added a bunch of operate to set these uniforms and to retrieve the uniform areas (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);
    }
personal:
    GLint GetUniformLocation(std::string_view identify);
    std::unordered_map<std::string, GLint, StringHash, StringEqual> uniform_location_map_;

Several issues to unpack right here:

  • SetVec3 makes use of the idea IsVector3 that examine if the sort has the three attributes x, y, z on the right kind.
  • StringHash and StringEqual permits using std::string_view with out the necessity to create a std::string when looking for a uniform location within the map.

Vertex inputs

One of the bizarre factor for me going into OpenGL is how reminiscence is allotted (the glGen, glBind, after which add the info and doubtless uplaod on the identical time). For instance, for a vertex enter buffer:

int vbo_;
glGenBuffers(vboCount, &vbo_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferKnowledge(GL_ARRAY_BUFFER, vertex_buffer.measurement, vertex_buffer.knowledge, GL_STATIC_DRAW);

For vertex inputs, you have got these buffers which are to be setup in a Vertex Array Object, form of like a container of vertex buffers, to outline how the pipeline goes to learn the vertex buffers, with one thing like this:

int vao_;
glGenVertexArrays(1, &vao_);
glBindVertexArray(vao_);

glBindBuffer(GL_ARRAY_BUFFER, vbo_);

glVertexAttribPointer(
  vertex_input_location, // within the shader (location = 0) 
  vertexAttribData.measurement, // how lots of the primitive: float => 1, vec2 => 2, vec3 => 3, vec4 => 4
  vertexAttribData.kind,
  GL_FALSE,
		vertexAttribData.stride,
		(void*)vertexAttribData.offset);
glEnableVertexAttribArray(vertexAttribData.index);
//when utilizing instancing
glVertexDivisor(vertexAttribData.index, 1);

All that is abstracted within the VertexInput class with the Buffer abstraction:


template<GLenum goal>
class Buffer {
public:
  void Bind() const;
  void Load();
  /**
   * @temporary Allocate and add the info within the buffer
   */

  template<typename T>
  void AddRange(std::span<const T> vary, GLenum utilization = GL_STATIC_DRAW);
  /**
   * @temporary Update the info within the buffer (for use after utilizing AddRange
   */
  template <typename T>
  void UpdateRange(std::span<const T> vary, GLintptr offset = 0, GLenum utilization = GL_STATIC_DRAW);
};

utilizing VertexBuffer = Buffer<GL_ARRAY_BUFFER>;
utilizing IndexBuffer = Buffer<GL_ELEMENT_ARRAY_BUFFER>;

struct VertexBufferAttribute {
  GLuint location;
  GLint measurement;
  GLenum kind;
  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 permits Buffer to be binded as per occasion buffer as we don’t have help for SSBO in OpenGL ES 3.0 (part beneath).

Textures

The logic for texture in OpenGL is counter-intuitive. One has to know the distinction between texture identify (the deal with given by glGenTextures) and the feel unit (GL_TEXTURE0 for instance). It confuses loads of my college students annually. So binding a texture to a uniform pattern appears like this:

glUniform1i(uniform_location, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE2D, my_texture);

This is put in a operate within the pipeline to look this:

pipeline_.SetTexture("uniform_name", my_texture, 0);

//with implementation wanting like this:
void Pipeline::SetTexture(std::string_view uniform_name, const Texture& texture, int texture_unit)
{
    const auto uniformLocation = GetUniformLocation(uniformName);
    glUniform1i(uniformLocation, textureUnit);
    glActiveTexture(GL_TEXTURE0 + textureUnit);
    texture.Bind(); 
}

For loading, to start with of the module, we begin with stb_image to load JPG, PNG, BMP, and many others… This header-only library decompresses the photographs such that we will add them to the GPU, one thing that appears like this:

picture.pixels = stbi_load(imagePath.knowledge(), &picture.width, &picture.peak, &picture.comp, 0);
	
glGenTextures(1, &my_texture);
glBindTexture(GL_TEXTURE2D, name_);
change (picture.comp)
	{
	case 3:
		glTexImage2D(GL_TEXTURE2D, 
      0, GL_RGB, picture.width, picture.peak, 
      0, GL_RGB, GL_UNSIGNED_BYTE, picture.pixels);
		break;
	case 4:
		glTexImage2D(GL_TEXTURE2D, 
      0, GL_RGBA, picture.width, picture.peak, 
      0, GL_RGBA, GL_UNSIGNED_BYTE, picture.pixels);
		break;

All that is course hidden in a Texture abstraction with Load and Bind. Then for cubemaps, we add:

class Texture
{
  public:
  //[...]
  void LoadCubemaps(std::span paths);
}

The Texture holds the “goal” (GL_TEXTURE2D or GL_TEXTURE_CUBE_MAP, why not texture3d?) after which the Bind will bind the proper goal.

Model Loading

We use Assimp because it simplifies the setup of the loading of 3d fashions and likewise permits to import and export in loads of totally different codecs. This would appear to be this:


void Model::LoadModel(std::string_view path)
{
  Assimp::Importer importer;
  scene = importer.ReadFile(path.knowledge(),
                                aiProcess_Triangulate | aiProcess_FlipUVs |
                                aiProcess_GenNormals |
                                aiProcess_CalcTangentSpace);

  if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
  {
      ...
  }
  ProcessNode(scene->mRootNode, scene);
}

void Model::ProcessNode(aiNode* node, const aiScene* scene)
{
    // course of all of the node's meshes (if any)
    for (unsigned int i = 0; i < node->mNumMeshes; i++)
    {
        aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
        meshes_.push_back(Course ofMesh(mesh, scene));
    }
    // then do the identical for every of its youngsters
    for (unsigned int i = 0; i < node->mNumChildren; i++)
    {
        ProcessNode(node->mChildren[i], scene);
    }
}

Mesh Model::Course ofMesh(aiMesh* mesh, const aiScene* scene)
{
  // Processing the meshes

It can be utilized in the identical approach in learnopengl.com, so the scholars can have a working implementation to examine after they encounter a mistake.

Draw instructions

In OpenGL, the driving force is including our instructions into its personal command buffer. This forbids multi-threading draw instructions era (evaluate to extra fashionable API) and it offers this sense that drawing occurs instantly. Because of learnopengl.com, most of my college students put their draw name within the Mesh contained of their Model. So it appears like this:

void Model::Draw(const Pipeline& pipeline)
{
  pipeline.Bind();
  for(auto& mesh: meshes)
  {
    mesh.Draw(pipeline);
  }
}

void Mesh::Draw(const Pipeline& pipeline)
{
  material_.Bind(pipeline); //binding all textures to their samples
  glDrawComponents(GL_TRIANGLES, indices_count, GL_UNSIGNED_INT, nullptr);
  //or relying on the abstraction
  vao_.Draw();
}

After engaged on my pc graphics editor (I wrote a weblog publish here), I actually favor a separated strategy the place a scene has supplies (pipeline + texture reference) and meshes, and that fashions don’t exist as scene abstraction. However once more, it isn’t about making a data-driven renderer, however simply implement a 3d scene.

Render passes & Framebuffer

Finally, the final large OpenGL core idea is about multi passes. The first half of the module is spent in a single cross. Post-processing is the primary instance of multi-pass, the place my college students have to make use of a framebuffer to attract their scene, after which use one other pipeline and the render targets as a sampled texture. The first instance is a number of variations of post-processing (like grayscale, inversing the colour). Setting a sure framebuffer in OpenGL is beginning a brand new render cross (RenderDoc will normally write it as Color Pass). With the scholars, we labored out an Framebuffer abstraction that use a FramebufferCreateInfo struct (similar to Vulkan) with the colour attachments and the depth shade:


struct ColorAttachmentInfo {
  GLenum goal = GL_TEXTURE_2D;
  GLint internal_format = GL_RGB8;
  bool is_active = false;
};

struct DepthAttachmentInfo {
  GLenum goal = GL_TEXTURE_2D;
  GLenum internal_format = GL_DEPTH24_STENCIL8;
  bool is_active = false;
  bool is_rbo = true;
};

struct FramebufferCreateInfo {
  //OpenGL has most 16 shade attachments
  std::array color_attachment_infos{};
  DepthAttachmentInfo depth_stencil_attachment_info{};
  core::Vec2I measurement{};
};

class Framebuffer
{
void Load(const FramebufferCreateInfo& information);
void Bind();
static void BindBackbuffer();
const Texture& color_attachments(int index);
const Texture& depth_attachment();
};
  • RBO (render buffer object, yet one more kind of buffers) is form of like a GPU write-only sort of useful resource.

Usually, we wish one or a number of shade attachments as Texture and a depth-buffer as RBO, aside from a shadow cross, the place we wish a depth-only cross (that outputs to a Texture).

An enormous step in utilizing framebuffer within the module comes with the introduction to HDR (High-Dynamic Range), principally the truth that we will use shade in a much bigger vary than [0 – 255]. One of such purposes is deferred rendering and the creation of G-Buffer (Geometry Buffer that accommodates place, normals and baseColor for instance) to reduce the quantity of lighting calculation per pixel. So first, you render a shade cross on a G-Buffer with a number of shade attachments every containing vital geometry worth (place, regular, base shade) after which there’s a gentle cross the place lighting is calculated per pixel. This method see an enormous efficiency enchancment when drawing loads of lights on the display. With our abstraction, you simply want to make use of a HDR kind of inside format (for instance GL_RGB16F).

Of course, not all monitor outputs HDR, so we want a approach to return to LDR (Low-Dynamic Range, [0 – 255]) and that is accomplished on the finish of the lighting cross fragment shader:

//Reinhard tonemapping
vec3 consequence = lighting / (lighting + vec3(1.0));
FragColor = vec4(consequence, 1.0);

Missing fashionable OpenGL options in ES 3.0

Using OpenGL ES 3.0 (to permit use of WebGL 2.0) has nonetheless a price although. Some core ideas of contemporary rendering are defined in course content material however not applied (besides if the scholars need to do away with WebGL2 compatibility).

SSBO

As of OpenGL 4.3 and OpenGL ES 3.1, we will outline shader storage buffer object. This is beneficial to take away the necessity to cross by the vertex inputs when doing instancing. Of course, we have to had one other kind of buffer that may be binded to an uniform. This would appear to be this:

//in C++
glBufferKnowledge(GL_SHADER_STORAGE_BUFFER, instancing_buffer.measurement, instancing_buffer.knowledge, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_point, ssbo);

//in shader
format(std430, binding = 3) buffer layoutName
{
    vec4 data_SSBO[];
};

void most important()
{
  vec4 instancing_data = data_SSBO[gl_InstanceID];
}

Compute shader

Compute shader solely appeared in OpenGL ES 3.1 (so not WebGL2). Coming again to the post-processing instance defined earlier than, as an alternative of giving the colour attachment of the scene as a sampler2D, we might give it as an image2D and truly do the post-processing in compute. Typically, each the-whole-screen sort of shader are simpler to implement with a compute pipeline with a single shader, quite than a graphics pipeline with a aircraft in NDC (Normal Device Coordinate) going from [-1, 1] simply to recuperate the entire display within the vertex shader and implementing the transformation within the fragment shader.

SPIR-V

Starting with OpenGL 4.6 (not even out there in OpenGL ES 3.2), we will load SPIR-V as an alternative of plain textual content GLSL. This enable for fast loading of shader because the format is binary and it permits so as to add a compilation step to shaders (one thing we form of do anyway with glslandValidator).

Bindless

Modern OpenGL desktop permits for “bindless” rendering the place textures will not be binded to a texture unit however might be referenced by way of a 64-bit deal with saved in a buffer (enabling entry to just about a vast textures with out costly state adjustments) with the extension ARB_bindless_texture (supported on Nvidia and AMD, however not on Intel).

For buffers, ARB_buffer_storage (core in OpenGL 4.4 and as an extension EXT_buffer_storage since OpenGL ES 3.1) permits to maintain the GPU pointer to allows persistent mapped buffers, avoiding repeated map/unmap cycles.

This just isn’t out there in OpenGL ES 3.0, so the workarounds are normally texture atlases or texture arrays, mixed with batching (draw calls with totally different vertex inputs, however the identical textures) to keep away from the pricey change of textures.

Indirect Drawing

Another highly effective function that’s unavailable in OpenGL ES 3.0 is oblique drawing. In our module, the CPU is issuing all of the draw calls with glDrawArrays and glDrawComponents or their instanced variations.
glDrawArraysIndirect and glDrawComponentsIndirect are a part of core since OpenGL 4.0 and OpenGL ES 3.1 permitting draw parameters to return from a GL_DRAW_INDIRECT_BUFFER. More energy comes with Multi-Draw Indirect (core in OpenGL 4.3 and extension EXT_multi_draw_indirect since OpenGL ES 3.1), which permits by means of glMultiDrawArraysIndirect and glMultiDrawComponentsIndirect calls to dispatch whole arrays of draw instructions which mixed with compute shaders that permits totally GPU-driven rendering pipelines the place the GPU performs frustum culling, LOD choice, and occlusion culling. ARB_indirect_parameters permits even the draw depend itself be GPU-determined.

Debugging

For my college students, what was essentially the most time they have been spending was with RenderDoc. Now I see extra college students simply utilizing LLM to examine their code and see what’s flawed (even certainly one of my college students who stated “they advised me there was an error there”). However, the examination explicitly forbids LLM to resolve, so my college students nonetheless need to undergo the debugging course of by themselves in the event that they need to cross.

Starting with OpenGL 4.3, KHR_debug is a part of core and permits to have the driving force sends error, warning, debug messages by means of a callback. As OpenGL 4.3 is totally appropriate, I noticed whereas scripting this weblog publish that we might apply it to Desktop to have higher debug capabilities (registering OpenGL objects with glObjectLabel with a macro for instance).

Missing options from Modern API

Sticking with OpenGL means additionally lacking loads about newer options of contemporary API like Vulkan and DX12. Of course, fashionable API are extra than simply options, they’re additionally one other psychological mannequin. But let’s deal with the options.

Raytracing

With all their advertising on RTX, Nvidia certainly introduced raytracing in each gamer’s mouth when speaking about pc graphics. Of course, raytracing exists since earlier than the appearance of graphics card. However in 2018, DXR was launched, permitting for {hardware} raytracing on the GPU utilizing new sort of shaders (closest-hit, miss, raygen, and many others…) and Vulkan adopted with the Nvidia extension after which the generic extension.

Most video games are nonetheless residing in a hybrid rendering planet the place some components are accomplished in rasterization and a few are accomplished in raytracing, with customized choices for gamers. In its State of GPU Hardware, Dmytro “Boolka” Bulatov mentions that solely video games like Indiana Jones and the Great Circle (launched in late 2024) that places DXR as a requirement. So for me, as a instructor, I nonetheless showcase the {hardware} raytracing API, its benefits and downsides, and the way the long run appears like, however as it’s an introduction to pc graphics, I can’t ask all my college students to need to implement a {hardware} raytracing pipeline.

Multithreaded rendering

When interested by fashionable CPU structure, we live in a multi-core planet now, however most of what we program in video games continues to be taking place in a single thread on one core. To actually unlock the ability of our CPU, we will divide our work between a number of threads. A standard sample is to have a threading mannequin with Game/Cull/Render on the CPU, or having the loading accomplished async and people nonetheless work with OpenGL.

However, sure video games (like Halo Infinite) require using a number of threads to generate all of the GPU instructions. This is certainly potential with DX12 and Vulkan, however not in OpenGL the place the render states are within the driver implementation in a single thread.

The outcomes

Here are among the works of my college students throughout this module over time:

What a couple of framework?

I like SDL GPU API. I’m presently porting my pc graphics editor to it, and it permits to focus on Vulkan, DirectX and Metal on the identical time. However, there may be nonetheless a little bit of bloat for an introduction course for me to make use of SDL GPU. I heard some good issues about bgfx additionally and raylib as properly. WebGPU, on high of including a brand new shader language, is similar to Vulkan 1.0 that precipitated me extra ache that pleasure.

When having college students engaged on their samples they usually do a mistake in OpenGL, it may typically be fairly clear why there’s a mistake, however dealing with a framework, the code of the library has additionally to be taken into consideration. And my objective is to show them a graphics API and construct abstractions on high of it.

Conclusion

OpenGL ES 3.0 has nonetheless loads of benefits to be taught even in 2026 (particularly WebGL2), at the price of going by means of the battle of the legacy of this API. 14 years is loads of time in Computer Graphics. For instance, on the finish of his graphics API blog post, Adam Sawicki doesn’t suggest OpenGL as a alternative (as it’s hardly ever used anymore).

This course has allowed all through the years all of my college students to find how graphics programming work. For a few of them, it was their first step of their pc graphics profession and I’m glad that I helped them in that.
I need to write one other weblog publish about why we aren’t but switching to Vulkan (speaking about synchronization, PSO, driver overhead), as I’ve a number of issues to say about this particular API. Don’t hesitate to touch upon the social media or simply ship me an e-mail (I’m to understand how different academics are doing and what individuals working within the business need to see from graphics programming college students). Here the rotating cubes demo operating on Android:

Showing rotating cubes on Android



Source link