Posted on

Table of Contents

First OpenGL Program

To start, we need to import the necessary libraries: glium for handling OpenGL in Rust, and winit for creating windows and managing events.

cargo add glium winit

The first step is to prepare the window. We'll need an event_loop, which we then attach to our window, providing us with both a window and a display surface.

   let event_loop = EventLoop::new().expect("Failed to create event loop");
   let (window,display) = SimpleWindowBuilder::new().with_title("Chapter2 - program1").build(&event_loop);

We might also want to set an initial color for the window.

OpenGL utilizes a technique known as double buffering. This means that instead of drawing directly onto the window, we draw onto a buffer (referred to as a frame) first. We then dispose of this frame, allowing glium to copy its contents to the window. The term "clear_color" is similar to the OpenGL function glClearColor.

    let mut frame = display.draw();
    frame.clear_color(1.0, 0.0, 0.0, 0.0); // Fill with red // glClearColor
    frame.finish().unwrap();

Upon running this code, we'll notice the window closes immediately. To keep it open until we decide to exit, we need to manage the event_loop effectively.

    event_loop.run(move |event, window_target|
    {
        match event
        {
            Event::WindowEvent {event,..} => match event {
                WindowEvent::CloseRequested => window_target.exit(),
                _=>(),
            },
            _=>(),
        };
    }).unwrap();

With this setup, running the program will display a small window with a red background.

display

Vertex Shader and Fragment Shader

Similar to C++, we need to use GLSL to write vertex shaders and fragment shaders. These shaders will instruct the hardware on how to color vertices and fragment pixels.

    let vertex_shader_src = r#"
    #version 460

    in vec2 position;

    void main() {
        gl_Position = vec4(position, 0.0, 1.0);
    }
    "#;

    let fragment_shader_src = r#"
    #version 460

    out vec4 color;

    void main() {
        color = vec4(0.0, 0.0, 1.0, 1.0);
    }
    "#;

Next, we need to compile these two shaders and initialize a Render Program. For the geometry shader, we'll temporarily set it to 'none' for now, as we will consider 3D later. At the moment, we are focusing solely on 2D rendering.

    let program =
        Program::from_source(&display, vertex_shader_src, fragment_shader_src, None).unwrap(); // glCreateShader & glShaderSource & glCompileShader & glAttachShader

OpenGL requires a Vertex Array Object (VAO) to serve as the buffer for vertex objects. We can directly define the structure of the point set and generate it.

#[derive(Copy, Clone)]
struct Vertex {
    position: [f32; 2],
}
implement_vertex!(Vertex, position);

    let vertex1 = Vertex {
        position: [-0.5, -0.5],
    };
    let vertex2 = Vertex {
        position: [0.0, 0.5],
    };
    let vertex3 = Vertex {
        position: [0.5, -0.25],
    };
    let shape = vec![vertex1, vertex2, vertex3];

    let vertex_buffer = VertexBuffer::new(&display, &shape).unwrap(); // glGenVertexArray & glBindVertexArray
	let indices = glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList); 

Next, we can begin the drawing process. Pass our Vertex Buffer, Indices and Program.

    frame
        .draw(
            &vertex_buffer,
            &indices,
            &program,
            &EmptyUniforms,
            &Default::default(),
        )
        .unwrap();

shader-render

Wireframe Mode

We can also draw this in wireframe mode, but we need to modify the DrawParameters:

    let draw_parameters = DrawParameters{
        polygon_mode:glium::PolygonMode::Line, // glPolygonMode
        ..Default::default()
    };
    
    frame
        .draw(
            &vertex_buffer,
            &indices,
            &program,
            &EmptyUniforms,
            &draw_parameters,
        )
        .unwrap();
    frame.finish().unwrap();

wireframe

Colour by Position

We can also modify the fragment shader, so we can change the color based on position:

    let fragment_shader_src = r#"
    #version 460

    out vec4 color;

    void main() {
        if (gl_FragCoord.x < 300) color = vec4(0.0,1.0,0.0,1.0);
        else color = vec4(0.0,0.0,1.0,1.0);
    }
    "#;

colour-by-position

Animation

Do you still remember, there is a EmptyUniforms we used in draw. We can use it to change the position of our triangle.

First, we need allow our vertex shader to accept uniform

    let vertex_shader_src_animation = r#"
    #version 460

    in vec2 position;
    uniform vec2 offset

    void main() {
        gl_Position = vec4(position + offset, 0.0, 1.0);
    }
    "#;

Then, in event loop, we can change the offset and apply it to draw

    // outside the event loop
    let mut offset = [0.0, 0.0]; // init offset
	let mut direction = [0.01, 0.01]; // speed and direction
    
	// inside the event loop
    // update offset
    offset[0] += direction[0];
    offset[1] += direction[1];
    
    if offset[0] > 1.0 || offset[0] < -1.0 {
        direction[0] *= -1.0; // change direction
    }
    if offset[1] > 1.0 || offset[1] < -1.0 {
        direction[1] *= -1.0; // change direction
    }

    let mut frame = display.draw();
    frame.clear_color(1.0, 0.0, 0.0, 0.0); // Fill with red

    frame
   .draw(
        &vertex_buffer,
        &indices,
        &program,
        &uniforms,
        &draw_parameters,
    )
    .unwrap();

    frame.finish().unwrap();

But you will see, the animation will move very fast, even faster than computer can show. That's because the animation will move by frame. Each frame the triangle will move by offset,the frame rate will be hundreds or even thousands frame per second.

We can use Event Loop, after draw a new frame, the AboutToWait will be send, we can use that to draw our next frame.

    event_loop
        .run(move |event, window_target| {
            match event {
                Event::WindowEvent { event, .. } => match event {
                    WindowEvent::RedrawRequested => {
                        offset[0] += direction[0];
                        offset[1] += direction[1];
            
                        if offset[0] > 1.0 || offset[0] < -1.0 {
                            direction[0] *= -1.0; // change direction
                        }
                        if offset[1] > 1.0 || offset[1] < -1.0 {
                            direction[1] *= -1.0; // change direction
                        }
            
                        let uniforms = uniform! {
                            offset: offset,
                        };
            
                        let mut frame = display.draw();
                        frame.clear_color(1.0, 0.0, 0.0, 0.0); // Fill with red
            
                        frame
                        .draw(
                            &vertex_buffer,
                            &indices,
                            &program,
                            &uniforms,
                            &draw_parameters,
                        )
                        .unwrap();
            
                        frame.finish().unwrap();
                    },
                    WindowEvent::CloseRequested => window_target.exit(),
                    _ => (),
                },
                Event::AboutToWait => {
                     window.request_redraw();
                }
                _ => (),
            };  

        })
        .unwrap();

animation