As we discussed before about How GPU works and the processes involved when playing a game, what are the process goes through to render. In this article lets dive into OpenGL Rendering pipeline stages that goes through to rendering an object in a 3D/2D space.
First let's understand what is OpenGL?
OpenGL is a cross-platform API that allows developers to create 2D and 3D graphic applications. It's used to interact with a graphics processing unit (GPU) to achieve hardware-accelerated rendering. And what it basically is that it's a bunch of functions that developers can call to handle graphics rendering. Specifically, OpenGL enables us to access the GPU and instruct it to perform rendering tasks.
What OpenGL Is (and isn't):
Let's clear up some common misconceptions about OpenGL. A lot of people refer to OpenGL as a library, an engine, or a framework - but it's none of those things. At its core, OpenGL is a specification, similar to how the C++ language is defined. It does not include any code or implementation itself but outlines functions, parameters, and expected outputs.
For instance, the specification defines that a function should exist, accept certain parameters, and return specific values. The actual implementation of OpenGL comes from GPU manufacturers, such as NVIDIA, AMD, or Intel. If you're wondering where to download OpenGL, it's included in your GPU drivers provided by the manufacturer. Each vendor creates its own implementation of OpenGL based on the specification.
Why Use OpenGL?
OpenGL has been a popular choice for rendering graphics due to its cross-platform support and extensive functionality. While newer APIs like Vulkan and DirectX exist, OpenGL remains relevant for many applications, especially those that prioritize simplicity and widespread support.
The Stages:
Vertex Specification
The vertex specification stage is the first step in the OpenGL rendering pipeline, where vertex data is defined and sent to the GPU for processing. This stage is crucial because it lays the foundation for all subsequent steps in the rendering pipeline. It involves preparing and organizing the data that represents the geometric structure of the objects you want to render.
Technically, each vertex retrieved from the vertex arrays (defined as VAO, VBO, and IBO) is acted upon by a vertex shader. The vertex data is stored in arrays, often structured as matrices, and contains all the necessary information for rendering. Each vertex may include:
· Position
· Color
· Normal
· Texture Coordinates
· Custom AttributesNote: Attributes are small data sets that describe an object to be rendered.
Buffers and Their Roles:
Vertex Buffer Objects (VBOs):
VBOs store vertex data in GPU memory, allowing efficient data transfer and access during rendering. They ensure that the data is readily available for the GPU.
Vertex Array Objects (VAOs):
VAOs act as containers that store the state of vertex attribute configurations. In simple terms, they act as a container for VBOs (attributes). This organization simplifies switching between different sets of vertex data without reconfiguring attributes.
Element Buffer Objects (EBOs) / Index Buffer Objects (IBOs):
EBOs or IBOs store indices for indexed drawing. They allow the reuse of vertex data for multiple primitives, reducing memory usage and improving performance. For example, when defining a 3D triangle with multiple sides, instead of redefining vertices for each side, IBOs can reference existing vertex data efficiently.
Practical Example:
Vertex Processing:
Vertex processing is the stage of the graphics pipeline where individual vertices of a 3D/2D model are transformed and prepared for rendering. As explained in vertex specification, each vertex retrieved from the vertex arrays (VAOs) is processed by a vertex shader. Vertex processing includes multiple stages, some fixed functions and other programmable, depending on the graphics API, (e.g. OpenGL, DirectX, Vulkan). In OpenGL there are vertex shader, tessellation, & geometry shader.
Vertex Shader:
The vertex shader performs basic processing of each individual vertex. It's a programmable stage in the graphics rendering pipeline. Vertex shaders receive attribute inputs from vertex specification and process each incoming vertex to produce a single outgoing vertex.
In simple terms, the vertex shader's primary role is:
Transformation: Transforming a vertex's position from the object's local space to clip space.
Attribute Handling: Passing vertex-specific attributes (e.g., color, texture coordinates, normals) to subsequent pipeline stages.
Example:
Vertex shaders can have user-defined outputs, but they must also output a special value representing the final position of the vertex. This clip-space position is crucial for rendering and is mandatory, as vertex shaders are not optional in the pipeline.
Key Features of Vertex Shaders:
** Programmability:**
Developers can write custom vertex shaders to implement specific transformations or effects.Parallel Processing:
Each vertex is processed independently, making vertex shaders highly parallelizable on modern GPUs.Flexible Outputs:
Apart from clip-space positions, vertex shaders can output other interpolated data, such as texture coordinates or lighting calculations, for use in later stages.
Tessellation:
Tessellation is a process in the graphics pipeline that subdivides polygons into smaller, finer primitives, such as triangles. This allows for the creation of smoother surfaces, detailed geometry, or adaptive level-of-detail rendering without requiring high-detail models directly from artists.
Tessellation in OpenGL consists of two programmable shader stages and a fixed-function tessellator:
Tessellation Control Shader (TCS):
· Determines the amount of tessellation to apply to a primitive.
· Ensures connectivity between adjacent tessellated primitives.
· Dynamically adjusts the level of detail based on factors like distance from the camera or application-specific criteria.
Tessellation Evaluation Shader (TES):
· Computes the final positions and attributes of the subdivided primitives.
· Applies interpolation and other user-defined operations to generate smooth and detailed geometry.
Applications of Tessellation:
Terrain Rendering:
· Generate highly detailed terrains from a coarse height map
Character Models:
· Adding fine details to character models, like wrinkles or muscle definition
** Adaptive Detail:**
· Automatically adjusting the level of detail based on camera proximity to optimize performance
Geometry Shader:
The geometry shader is a programmable and optional stage in the graphics pipeline that processes entire primitives (points, lines, or triangles) as input. Unlike vertex shaders, which operate on individual vertices, the geometry shader works on entire primitives and can:
Generate New Primitives:
· For example, creating additional triangles for tessellation or adding detail.
Modify Existing Primitives:
· Changing the shape or attributes of input primitives.
Discard Primitives Entirely:
· Eliminating unnecessary primitives to optimize rendering.
The input primitives for geometry shaders are the output primitives from a subset of the Primitive Assembly process. So, if you send a triangle strip as a single primitive, what the geometry shaders will see is a series of triangles.
However, there are a number of input primitive types that are defined specifically for geometry shaders. These adjacency primitives give geometry shaders a larger view of the primitives, they provide access to vertices of primitives adjacent to the current one.
The output of a geometry shader is zero or more simple primitives, much like the output of the primitive assembly. The geometry shader is able to remove primitives, or tessellate them by outputting many primitives for a single input. The geometry shader can also tinker with the vertex values themselves, either doing some of the work for the vertex shader, or just to interpolate the value when tessellating them. Geometry shaders can even convert primitives to different types: input point primitives can become triangles, or line can become points.
Vertex post-processing:
The vertex post-processing step is primarily responsible for preparing the processed vertex data for the rasterization process. The key operations in vertex post-processing are:
Perspective Division:
· After the vertex shader processes a vertex, it outputs the position in clip space, represented in homogeneous coordinates (x, y, z, w).
· To transform clip space coordinates into normalized device coordinates (NDC), the GPU performs perspective division
· This ensures that vertices closer to the camera appear larger, creating the effect of perspective.
Viewport Transformation:
· NDC ranges from [-1, 1] along all axes. These coordinates need to be mapped to window space or screen space (pixel coordinates) based on the viewport settings.
· This transformation involves scaling and translating the coordinates to fit the current viewport.
Clipping:
· Clipping removes vertices or primitives that fall outside the view frustum (the visible region of the 3D scene defined by the camera's perspective or orthographic projection).
· Clipping ensures that only the visible portions of the geometry are processed further. If a primitive partially intersects the view frustum, the GPU may generate new vertices at the intersection points (clipping the primitives).
Face Culling:
· Triangle primitives can be culled (i.e., discarded without rendering) based on the triangle's orientation in window space. Back-face culling is an optional operation that removes faces of geometry not visible to the camera (e.g., the back sides of triangles).
· This helps improve performance by not rendering unnecessary geometry.
Depth Range Mapping:
Depth range mapping in OpenGL is a crucial concept that deals with how depth values are mapped from normalized device coordinates (NDC) to the depth buffer. It ensures proper depth testing and determines visibility within a 3D scene.
· In NDC, after vertex processing and perspective division, the depth values range between -1 (near clip plane) and 1 (far clip plane).
· OpenGL requires depth values to be clamped between 0 and 1 for storage in the depth buffer.
Primitive Assembly:
Primitive assembly is a critical stage in the graphics pipeline that occurs just before rasterization. During this stage, the GPU takes the processed vertices and assembles them into primitives, such as points, lines, or triangles, based on the rendering mode specified in the drawing command.
Why Primitive Assembly is Important:
Defines Shape Connectivity:
· Determines how vertices are grouped into meaningful shapes.
** Handles Optimizations:**
· Reuses vertices with indexed drawing, reducing redundant calculations.
· Eliminates unnecessary primitives through clipping and culling.
Prepares for Rasterization:
· Outputs fully assembled and valid primitives, ready for fragment generation.
Primitive assembly ensures the integrity and connectivity of geometry before it moves to the rasterization stage, where it's converted into fragments for pixel shading.
Rasterization:
Rasterization is responsible for converting geometric primitives into fragments that can be processed further to produce the final image on the screen. A fragment is a set of state that is used to compute the final data for a pixel in the output framebuffer. The state for a fragment includes its position in screen-space, the sample coverage if multisampling is enabled, and a list of arbitrary data that was output from the previous vertex or geometry shader.
In basic, it bridges the gap between the vector-based representation of objects in 3D space and the grid of pixels in 2D screen space.
Fragment Shader:
The fragment shader is a programmable stage in the rendering pipeline and the Fragments are the data elements produced during the rasterization stage when primitives are converted into pixel-sized chunks.
Each fragment corresponds to a pixel on the screen but includes additional data such as:
· Interpolated attributes (e.g. colors, texture coordinates, normals) from the vertices of the primitives.
· Depth (z-coordinates in window space).
Fragment Shaders are not able to set the stencil data for fragment, but they do have control over the color and depth values.
This shader is an optional. If you render without a fragment shader, the depth values of the fragment get their usual values. But the values of all the colors that a fragment could have been undefined. Rendering without a fragment shader is useful when rendering only a primitive's default depth information to the depth buffer.
Per-Sample Operations:
Per-sample operations are the final set of stages in the graphics pipeline that occur after the fragment shader has executed and before the rendered results are written to the framebuffer. These operations are essential for refining the final appearance of the image and ensuring it adheres to rendering requirements like depth, stencil, blending and anti-aliasing.
What are Samples:
A sample represents a point within a pixel that is used to calculate the final color of that pixel.
For multi-sampling anti-aliasing (MSAA), each pixel can have multiple samples to capture more detail about overlapping primitives. Each sample may have its own depth, stencil, and color data.
Steps in Per-Sample Operations:
Pixel Ownership Test:
· Determines whether a pixel belongs to the current window or render target.
· If the pixels fail this test (e.g. due to being outside the window or overlapping an obscured area), it is discarded. Always passes when using a Framebuffer Object. Failure means that the pixel contains undefined values.
Scissor Test:
· When enabled, the test fails if the fragment's pixel lies outside of a specified rectangle of the screen (the scissor box).
· Only fragments inside the scissor box are processed further.
Stencil Test:
· When enabled, Compares the fragments stencil value with a reference value using a stencil function.
· Depending on the result, the fragment can be discarded, or the stencil buffer can be updated.
· Useful for making areas of the screen or implementing effects like outlining or decals.
Depth Test (Z-Test):
· When enabled, compares the fragment's depth value (output by the fragment shader on modified during rasterization) with the existing value in the depth buffer.
· If the test fails, the fragment is discarded.
· Ensures correct occlusion and depth-based ordering of primitives.
After this color blending happens. For each fragment color value, there is a specific blending operation between it and the color already in the framebuffer at that location.
Logical Operations may also take place in lieu of blending, which perform bitwise operations between the fragment colors and framebuffer colors.
This article provides an overview of the OpenGL Rendering Pipeline. It emphasizes the importance of each stage, the flexibility of OpenGL as a specification, and its relevance in graphics programming.
For more detailed look at the code, I've created GitHub repository where you can access and explore the entire basic project. You can find the repository here: Github
You can further explore on OpenGL here: OpenGL
Finally, I thank whoever reading, for spending your valuable time on my article.
Top comments (0)