Ultramodern OpenGL. Part 1
Hello. Everyone who knows a little about the OpenGL topic knows that there are a large number of articles and courses on this topic, but many do not affect the modern API, and some of them generally talk about glBegin and glEnd . I will try to cover some of the nuances of the new API starting from version 4. Link to the second part of the article
This time I will try to write an interesting and informative article, and what happened is up to the good habra residents. Please forgive me for my poor grammar (I will be grateful for the corrections).
If you like it, I’ll write about optimizing OpenGL and reducing DrawCalls.
Let's get started!
What will be in this article - the functionality of modern OpenGL
What will not be in this article - modern approaches to rendering on OpenGL
Content:
- Direct state access
- Debug
- Separate Shader Objects
- Texture arrays
- Texture view
- Single buffer for index and vertex
- Tessellation and compute shading
- Path rendering
DSA (Direct State Access)
Direct State Access - Direct access to state. A tool for modifying OpenGL objects without having to snap them to context. This allows you to change the state of an object in a local context without affecting the global state shared by all parts of the application. It also makes the API a little more object oriented, as functions that change the state of objects can be clearly defined. This is what the OpenGL Wiki tells us .
As we know, OpenGL is an API with many radio buttons - glActiveTexture , glBindTexture , etc.
From here we have some problems:
- The selector and current states can make a deeper change in state.
- It may be necessary to bind / change the active unit to set a filter for textures
- State management becomes problematic as a result of which the complexity of the application grows
- Unknown state leads to additional settings
- Attempts to save / restore state can be problematic
What did the Khronos group offer us and how does the DSA help?
- Adds functions that work directly with the object / objects
- Sets the texture filter for the specified texture object, not the current one.
- Binds a texture to a specific unit, not to the active
- Adds a very large number of new features
- Covers things up to OpenGL 1.x
- Adds extra features.
In theory, a DSA can help reduce the number of non-rendering and state-changing operations to zero ... But that's not accurate.
Now I’ll briefly go over some of the new functions, I won’t dwell on the parameters in detail, I will leave links on the wiki.
- glCreateTextures replaces glGenTextures + glBindTexture (initialization).
It was:
It became:glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name);
glCreateTextures(GL_TEXTURE_2D, 1, &name);
- glTextureParameterX equivalent to glTexParameterX
Now we will write it like this:glGenTextures(1, &name); glBindTexture(GL_TEXTURE_2D, name); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glCreateTextures(GL_TEXTURE_2D, 1, &name); glTextureParameteri(name, GL_TEXTURE_WRAP_S, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_WRAP_T, GL_CLAMP); glTextureParameteri(name, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(name, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTextureStorage2D(name, 1, GL_RGBA8, width, height); glTextureSubImage2D(name, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
- glBindTextureUnit replaces glActiveTexture + glBindTexture
Here's how we did it:
Now:glActiveTexture(GL_TEXTURE0 + 3); glBindTexture(GL_TEXTURE_2D, name);
glBindTextureUnit(3, name);
The changes also affected glTextureImage , it is no longer used and this is why:
glTexImage is rather unsafe, it is very easy to get invalid textures, because the function does not check values when called, the driver does this while drawing. GlTexStorage was added to replace it .
glTexStorage provides a way to create textures with checks performed during a call, which minimizes errors. The texture repository is solved by most, if not all, problems caused by mutable textures, although immutable textures are more reliable.
The changes also affected the frame buffer:
- glCreateFramebuffers instead glGenFramebuffers
- glNamedFramebufferTexture replace glFramebufferTexture
These are not all the changed features. The next in line are the functions for the buffers:
- glCreateBuffers instead glGenBuffers + glBindBuffer (initialization)
- glNamedBufferData replace glBufferData
- glVertexAttribFormat and glBindVertexBuffer instead of glVertexAttribPointer
Here is a list of what is now included in DSA support:
- Vertex array objects
- Framebuffer objects
- Program objects
- Buffer objects
- Matrix stacks
- A lot of obsolete things
Debug
Since version 4.3, new functionality has been added for debug, in my opinion very useful and convenient. Now OpenGL will call our callback for errors and debug messages, the level of which we can adjust.
We need to call only two functions to enable: glEnable & glDebugMessageCallback , nowhere is easier.
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(message_callback, nullptr);
Now we will write a callback function to get the message:
void callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, GLchar const* message, void const* user_param)
{
auto source_str = [source]() -> std::string {
switch (source)
{
case GL_DEBUG_SOURCE_API: return "API";
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: return "WINDOW SYSTEM";
case GL_DEBUG_SOURCE_SHADER_COMPILER: return "SHADER COMPILER";
case GL_DEBUG_SOURCE_THIRD_PARTY: return "THIRD PARTY";
case GL_DEBUG_SOURCE_APPLICATION: return "APPLICATION";
case GL_DEBUG_SOURCE_OTHER: return "OTHER";
default: return "UNKNOWN";
}
}();
auto type_str = [type]() {
switch (type)
{
case GL_DEBUG_TYPE_ERROR: return "ERROR";
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: return "DEPRECATED_BEHAVIOR";
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: return "UNDEFINED_BEHAVIOR";
case GL_DEBUG_TYPE_PORTABILITY: return "PORTABILITY";
case GL_DEBUG_TYPE_PERFORMANCE: return "PERFORMANCE";
case GL_DEBUG_TYPE_MARKER: return "MARKER";
case GL_DEBUG_TYPE_OTHER: return "OTHER";
default: return "UNKNOWN";
}
}();
auto severity_str = [severity]() {
switch (severity) {
case GL_DEBUG_SEVERITY_NOTIFICATION: return "NOTIFICATION";
case GL_DEBUG_SEVERITY_LOW: return "LOW";
case GL_DEBUG_SEVERITY_MEDIUM: return "MEDIUM";
case GL_DEBUG_SEVERITY_HIGH: return "HIGH";
default: return "UNKNOWN";
}
}();
std::cout << source_str << ", "
<< type_str << ", "
<< severity_str << ", "
<< id << ": "
<< message << std::endl;
}
We can also configure the filter using glDebugMessageControl . The filter can work in filtering mode by source / type / importance or a set of messages using their identifiers.
Filter messages in a specific scope:
glPushDebugGroup( GL_DEBUG_SOURCE_APPLICATION, DEPTH_FILL_ID, 11, “Depth Fill”); //Добавляем маркер
Render_Depth_Only_Pass(); //Выполняем рендеринг
glPopDebugGroup(); //Убираем маркер
It will be very useful to disable the asynchronous call so that we can determine the order of function calls, as well as find the place of the error on the stack when debugging. This is done quite simply:
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
It is worth remembering that it is not safe to call OpenGL or window functions in a wrapped call function, as well as callbacks are not free and you should not leave them included in the release build.
More details about these and other little things - gimmicks, you can read here .
SSO (Separate Shader Objects)
Once OpenGL worked as a “fixed pipeline” - this meant that pre-programmed processing was applied to all the data transferred for visualization. The next step was the “programmable pipeline” - where the programmable part implements shaders, written in GLSL, the classic GLSL program consisted of vertex and fragment shaders, but in modern OpenGL some new types of shaders were added, namely the shaders of geometry, weighting and calculations (about them I I will tell in the next part).
SSOs allow us to change shader steps on the fly without re-linking them. Creating and setting up a simple software pipeline without debugging is as follows:
GLuint pipe = GL_NONE;
// Create shaders
GLuint fprog = glCreateShaderProgramv( GL_FRAGMENT_SHADER, 1, &text);
GLuint vprog = glCreateShaderProgramv( GL_VERTEX_SHADER, 1, &text);
// Bind pipeline
glGenProgramPipelines( 1, &pipe);
glBindProgramPipelines( pipe);
// Bind shaders
glUseProgramStages( pipe, GL_FRAGMENT_SHADER_BIT, fprog);
glUseProgramStages( pipe, GL_VERTEX_SHADER_BIT, vprog);
As we see glCreateProgramPipelines generates a descriptor and initializes the object, glCreateShaderProgramv generates, initializes, compiles and links the shader program using the specified sources, and glUseProgramStages attaches the program steps to the pipeline object. glBindProgramPipeline - Associates a pipeline with a context.
But there is one caveat, now the input and output parameters of the shaders must match. We can declare the input / output parameters in the same order, with the same names, or we make their location clearly coincide with the help of qualifiers.
I recommend the latter option, this will allow us to configure a clearly defined interface, as well as be flexible with respect to names and order.
To provide a more rigorous interface, we also need to declare the built-in input and output blocks that we want to use for each stage.
The built-in block interfaces are defined as ( from a wiki ):
Vertex:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
Tesselation Control:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_out[];
Tesselation Evaluation:
out gl_PerVertex {
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
Geometry:
out gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
An example of re-declaring a built-in module and using attribute location in a regular vertex shader:
#version 450
out gl_PerVertex { vec4 gl_Position; };
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 0) out v_out
{
vec3 color;
} v_out;
void main()
{
v_out.color = color;
gl_Position = vec4(position, 1.0);
}