Sunday, August 28, 2022
HomeWordPress DevelopmentBe taught OpenGL with Rust: shaders

Be taught OpenGL with Rust: shaders


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.

Image description

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;
}
Enter fullscreen mode

Exit fullscreen mode

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);
}
Enter fullscreen mode

Exit fullscreen mode

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,
}
Enter fullscreen mode

Exit fullscreen mode

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);
Enter fullscreen mode

Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode



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))
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

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])?;
Enter fullscreen mode

Exit fullscreen mode

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);
}
Enter fullscreen mode

Exit fullscreen mode

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.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments