Welcome to the second a part of Be taught OpenGL with Rust tutorial. In the final article we discovered a little bit of OpenGL principle and found find out how to create a window, initialize OpenGL context and name some fundamental api to clear a window with a coloration of our selection.
On this article we’ll briefly focus on trendy OpenGL graphics pipeline and find out how to configure it utilizing shaders. All of the supply code for the article yow will discover on my github.
Graphics pipeline
We take into account display of the devise as a 2D array of pixels, however often we wish to draw objects in 3D house, so a big a part of OpenGL’s work is about remodeling 3D coordinates to 2D pixels that match on a display. The method of reworking 3D coordinates to 2D pixels is managed by the graphics pipeline
.
The graphics pipeline may be divided into a number of steps the place every step requires the output of the earlier step as its enter. All of those steps have their very own particular operate and may be executed in parallel. Every step of the pipeline runs small applications on the GPU known as shader
.
As enter to the graphics pipeline we cross vertices, record of factors from which shapes like triangles shall be constructed later. Every of those factors is saved with sure attributes and it is as much as programmer to resolve what sort of attributes they wish to retailer. Generally used attributes are 3D place and coloration worth.
The primary a part of the pipeline is the vertex shader
that takes as enter a single vertex. The principle function of the vertex shader is transformation of 3D coordinates. It additionally passes essential attributes like coloration and texture coordinates additional down the pipeline.
The primitive meeting
stage takes as enter all of the vertices from the vertex shader and assembles all of the factors right into a primitive form.
The output of the primitive meeting stage is handed to the geometry shader
. The geometry shader takes the primitives from the earlier stage as enter and might both cross a primitive all the way down to the remainder of the pipeline, modify it, utterly discard and even substitute it with different primitives.
After that last record of shapes consists and transformed to display coordinates, the rasterization stage
turns the seen elements of the shapes into pixel-sized fragments.
The principle function of the fragment shader
is to calculate the ultimate coloration of a pixel. In additional superior situations, there is also calculations associated to lighting and shadowing and particular results on this program.
Lastly, the top result’s composed from all these form fragments by mixing them collectively and performing depth and stencil testing
. So even when a pixel output coloration is calculated within the fragment shader, the ultimate pixel coloration may nonetheless be one thing totally different when rendering a number of triangles one over one other.
The graphics pipeline is sort of complicated and incorporates many configurable elements. In trendy OpenGL we’re required to outline at the very least a vertex and fragment shader (geometry shader is elective).
Shaders
As mentioned earlier in trendy OpenGL, it is as much as us to instruct the graphics card what to do with the information. And we will do it writing shader applications. We are going to configure two quite simple shaders to render our first triangle.
Shaders are written in a C-style language known as GLSL (OpenGL Shading Language). OpenGL will compile your program from supply at runtime and duplicate it to the graphics card. Under yow will discover the supply code of a vertex shader in GLSL:
#model 330
in vec2 place;
in vec3 coloration;
out vec3 vertexColor;
void major() {
gl_Position = vec4(place, 0.0, 1.0);
vertexColor = coloration;
}
Every shader begins with a declaration of its model. Since OpenGL 3.3 and better the model numbers of GLSL match the model of OpenGL.
Subsequent we declare all of the enter vertex attributes within the vertex shader with the in
key phrase. Now we have two vertex attributes: one for vertex place and one other one for vertex coloration. Aside from the common C sorts, GLSL has built-in vector and matrix sorts: vec
and mat
with a quantity on the finish which stands for variety of elements. The ultimate place of the vertex is assigned to the particular gl_Position
variable.
The output from the vertex shader is interpolated over all of the pixels on the display coated by a primitive. These pixels are known as fragments and that is what the fragment shader operates on. The fragment shader solely requires one output variable and that could be a vector of dimension 4 that defines the ultimate coloration output, FragColor
in our case. Right here is an instance of our easy fragment shader:
#model 330
out vec4 FragColor;
in vec3 vertexColor;
void major() {
FragColor = vec4(vertexColor, 1.0);
}
Shader can specify inputs and outputs utilizing in
and out
key phrases. If we wish to ship knowledge from one shader to a different now we have to declare an output within the first shader and an analogous enter within the second shader. OpenGL will hyperlink these variables collectively and ship knowledge between shaders. In our case we cross vertexColor
from vertex shader to the fragment shader. This worth shall be interpolated amongst all of the fragments of our triangle.
To ensure that OpenGL to make use of the shader it has to dynamically compile it at run-time from a supply code. However first we declare shader struct which can retailer shader object id
:
pub struct Shader {
pub id: GLuint,
}
To create an object we’ll use gl::CreateShader
operate which takes sort of shader (gl::VERTEX_SHADER
or gl::FRAGMENT_SHADER
) as a primary argument. Then we connect the shader supply code to the shader object and compile the shader:
let source_code = CString::new(source_code)?;
let shader = Self {
id: gl::CreateShader(shader_type),
};
gl::ShaderSource(shader.id, 1, &source_code.as_ptr(), ptr::null());
gl::CompileShader(shader.id);
To examine if compilation was profitable and to retrieving the compile log we will use gl::GetShaderiv
and gl::GetShaderInfoLog
accordingly. The ultimate model of shader creation operate seems to be like this:
impl Shader {
pub unsafe fn new(source_code: &str, shader_type: GLenum) -> End result<Self, ShaderError> {
let source_code = CString::new(source_code)?;
let shader = Self {
id: gl::CreateShader(shader_type),
};
gl::ShaderSource(shader.id, 1, &source_code.as_ptr(), ptr::null());
gl::CompileShader(shader.id);
// examine for shader compilation errors
let mut success: GLint = 0;
gl::GetShaderiv(shader.id, gl::COMPILE_STATUS, &mut success);
if success == 1 {
Okay(shader)
} else {
let mut error_log_size: GLint = 0;
gl::GetShaderiv(shader.id, gl::INFO_LOG_LENGTH, &mut error_log_size);
let mut error_log: Vec<u8> = Vec::with_capacity(error_log_size as usize);
gl::GetShaderInfoLog(
shader.id,
error_log_size,
&mut error_log_size,
error_log.as_mut_ptr() as *mut _,
);
error_log.set_len(error_log_size as usize);
let log = String::from_utf8(error_log)?;
Err(ShaderError::CompilationError(log))
}
}
}
To delete a shader as soon as we do not want it anymore we implement trait Drop
and can name gl::DeleteShader
operate with shader id
as an argument:
impl Drop for Shader {
fn drop(&mut self) {
unsafe {
gl::DeleteShader(self.id);
}
}
}
Shader program
Thus far vertex and fragment shaders have been two separate objects. We are going to use shader program
to hyperlink them collectively. When linking the shaders right into a program it hyperlinks the outputs of every shader to the inputs of the subsequent shader. Therefore we will have program linking errors if outputs and inputs don’t match.
Just like Shader
we’ll declare Program
struct, which holds program id
generated by gl::CreateProgram
operate. To hyperlink all shaders collectively we have to connect them first with gl::AttachShader
after which use gl::LinkProgram
for linking. Like with shaders we will examine for and retrieve linking errors if now we have any.
pub struct ShaderProgram {
pub id: GLuint,
}
impl ShaderProgram {
pub unsafe fn new(shaders: &[Shader]) -> End result<Self, ShaderError> {
let program = Self {
id: gl::CreateProgram(),
};
for shader in shaders {
gl::AttachShader(program.id, shader.id);
}
gl::LinkProgram(program.id);
let mut success: GLint = 0;
gl::GetProgramiv(program.id, gl::LINK_STATUS, &mut success);
if success == 1 {
Okay(program)
} else {
let mut error_log_size: GLint = 0;
gl::GetProgramiv(program.id, gl::INFO_LOG_LENGTH, &mut error_log_size);
let mut error_log: Vec<u8> = Vec::with_capacity(error_log_size as usize);
gl::GetProgramInfoLog(
program.id,
error_log_size,
&mut error_log_size,
error_log.as_mut_ptr() as *mut _,
);
error_log.set_len(error_log_size as usize);
let log = String::from_utf8(error_log)?;
Err(ShaderError::LinkingError(log))
}
}
}
To stop assets liking we implement Drop
trait for shader program as effectively:
impl Drop for ShaderProgram {
fn drop(&mut self) {
unsafe {
gl::DeleteProgram(self.id);
}
}
}
Lastly we will use our declared sorts to compile shaders and hyperlink them right into a program, which we’ll use throughout rendering:
let vertex_shader = Shader::new(VERTEX_SHADER_SOURCE, gl::VERTEX_SHADER)?;
let fragment_shader = Shader::new(FRAGMENT_SHADER_SOURCE, gl::FRAGMENT_SHADER)?;
let program = ShaderProgram::new(&[vertex_shader, fragment_shader])?;
To make use of this system whereas rendering we declare operate apply
for Program
sort that makes use of gl::UseProgram
underneath the hood:
pub unsafe fn apply(&self) {
gl::UseProgram(self.id);
}
Each rendering name after apply
will use this program object.
Abstract
At this time we have discovered how graphics pipeline of recent OpenGL works and the way we will use shaders to configure it.
Subsequent time we’re going to lean what vertex buffer and vertex array objects are and the way we will use information we have thus far to render a primary triangle.
If you happen to discover the article fascinating take into account hit the like button and subscribe for updates.