Vertex input

Vertex buffer

The first part of drawing an object with the graphics pipeline is to describe the shape of this object. When you think "shape", you may think of squares, circles, etc., but in graphics programming the most common shapes that one will need to work with are triangles.

Note: Tessellation shaders and alternative PrimitiveTopology values unlock the possibility to use other polygons, but this is a more advanced topic.

Each triangle is made of three vertices, and the shape of an object is just a collection of vertices linked together to form triangles. For the purpose of this book, we are only going to draw a single triangle first.

The first step to describe a shape with vulkano is to create a struct named MyVertex (the actual name doesn't matter) whose purpose is to describe the properties of a single vertex. Once this is done, the shape of our triangle is going to be a buffer whose content is an array of three MyVertex objects.

#![allow(unused)]
fn main() {
use vulkano::buffer::BufferContents;
use vulkano::pipeline::graphics::vertex_input::Vertex;

#[derive(BufferContents, Vertex)]
#[repr(C)]
struct MyVertex {
    #[format(R32G32_SFLOAT)]
    position: [f32; 2],
}
}

Our struct contains a position field which we will use to store the position of the vertex on the image we are drawing to. Being a vectorial renderer, Vulkan doesn't use coordinates in pixels. Instead it considers that the image has a width and a height of 2 units (-1.0 to 1.0), and that the origin is at the center of the image.

When we give positions to Vulkan, we need to use its coordinate system.

In this book we are going to draw only a single triangle for now. Let's pick a shape for it, for example this one:

Which translates into this code:

#![allow(unused)]
fn main() {
let vertex1 = MyVertex { position: [-0.5, -0.5] };
let vertex2 = MyVertex { position: [ 0.0,  0.5] };
let vertex3 = MyVertex { position: [ 0.5, -0.25] };
}

Note: The field that contains the position is named position, but note that this name is arbitrary. We will see below how to actually pass that position to the GPU.

Now all we have to do is create a buffer that contains these three vertices. This buffer will be passed as a parameter when we start the drawing operation.

#![allow(unused)]
fn main() {
let vertex_buffer = Buffer::from_iter(
    memory_allocator.clone(),
    BufferCreateInfo {
        usage: BufferUsage::VERTEX_BUFFER,
        ..Default::default()
    },
    AllocationCreateInfo {
        memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
            | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
        ..Default::default()
    },
    vec![vertex1, vertex2, vertex3],
)
.unwrap();
}

A buffer that contains a collection of vertices is commonly named a vertex buffer. Because we know the specific use of this buffer is for storing vertices, we specify the usage flag VERTEX_BUFFER.

Note: Vertex buffers are not special in any way. The term vertex buffer indicates the way the programmer intends to use the buffer, and it is not a property of the buffer.

Vertex shader

At the start of the drawing operation, the GPU is going to pick each element from this buffer one by one and call a vertex shader on them.

Here is what the source code of a vertex shader looks like:

#version 460

layout(location = 0) in vec2 position;

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

The line layout(location = 0) in vec2 position; declares that each vertex has an attribute named position and of type vec2. This corresponds to the definition of the MyVertex struct we created.

Note: The Vertex trait is used to describe the attributes of an individual vertex that can be read by a vertex shader. It provides methods for specifying the format of the vertex's fields, which can be done using field attributes like format and name when deriving the trait using the Vertex derive macro.

The main function is called once for each vertex, and sets the value of the gl_Position variable to a vec4 whose first two components are the position of the vertex.

gl_Position is a special "magic" global variable that exists only in the context of a vertex shader and whose value must be set to the position of the vertex on the surface. This is how the GPU knows how to position our shape.

After the vertex shader

After the vertex shader has run on each vertex, the GPU knows where our shape is located on the screen. It then proceeds to call the fragment shader.