Some work on the tutorial

This commit is contained in:
Pierre Krieger 2016-05-17 21:42:35 +02:00
parent 3fc8bb615d
commit bcff47b20a
5 changed files with 297 additions and 11 deletions

View File

@ -9,19 +9,30 @@ title: "Tutorial 1: getting started"
vulkano = "0.1"
{% endhighlight %}
## Creating an instance
The first thing any Vulkan program should do is create an instance. Creating an instance checks
whether Vulkan is supported on the system and loads the list of available devices from the
environment.
{% highlight rust %}
extern crate vulkano;
// Put this at the top of the file.
use vulkano::instance::Instance;
fn main() {
let instance = Instance::new(None, &Default::default(), None)
.expect("failed to create instance");
}
// Put this inside the main function.
let instance = Instance::new(None, &Default::default(), None)
.expect("failed to create instance");
{% endhighlight %}
This code does the first thing any Vulkan program should do: create an instance.
There are three optional parameters that we can pass to the `new` functions: a description of your
application, a list of extensions to enable, and a list of layers to enable. We don't need any of
these for the moment.
Like many other functions in vulkano, creating an instance returns a `Result`. If Vulkan is not
installed on the system, this result will contain an error. For the sake of this example we call
`expect` on the `Result`, which prints a message to stderr and terminates the application. In a
real game or application you should handle that situation in a nicer way, for example by opening
a dialog box with an explanation.
You can now try your code by running:
@ -29,4 +40,28 @@ You can now try your code by running:
cargo run
{% endhighlight %}
# Enumerating physical devices
## Enumerating physical devices
The machine you run your program on may have multiple devices that support Vulkan. Before we can
ask a video card to perform some operations, we have to enumerate all the physical devices that
support Vulkan and choose which one we are going to use.
As of the writing of this tutorial, it is not possible to share resources between multiple
physical devices. The consequence is that you would probably gain nothing from using multiple
devices at once. At the moment everybody chooses the "best" device and uses it exclusively, like
this:
{% highlight rust %}
use vulkano::instance::PhysicalDevice;
let physical = PhysicalDevice::enumerate(&instance).next().expect("no device available");
{% endhighlight %}
The `enumerate` function returns an iterator to the list of available physical devices.
We call `next` on it to return the first device, if any. Note that the first device is not
necessarily the best device. In a real program you probably want to choose in a better way
or leave the choice to the user.
It is possible for this iterator to be empty, in which case the code above will panic. This
happens if Vulkan is installed on the system, but none of the physical devices are capable
of supporting Vulkan.

View File

@ -5,9 +5,23 @@ title: "Tutorial 2: the first operation"
# The first operation
Now that we have chosen a physical device, it is time to ask it to do something.
But before we start, a few things need to be explained. Just like it is possible to use multiple
threads in your program running on the CPU, it is also possible to run multiple operations in
parallel on the physical device. The Vulkan equivalent of a CPU core is a *queue*.
When we ask the device to perform an operation, we have to submit the command to a specific queue.
Some queues support only graphical operations, some others support only compute operations, and
some others support both.
The reason why this is important is that at initialization we need to tell the device which queues
we are going to use.
## Creating a device
Now that we have chosen a physical device, we can create a `Device` object.
A `Device` object is an open channel of communication with a physical device. It is probably the
most important object of the Vulkan API.
{% highlight rust %}
let (device, mut queues) = {
@ -41,8 +55,25 @@ let destination = CpuAccessibleBuffer::new(&device, 3, &BufferUsage::all(), Some
## Copying
Now that we
In Vulkan you can't just submit a command to a queue. Instead you must create a *command buffer*
which contains one or more commands, and submit the command buffer.
{% highlight rust %}
let cb_pool = CommandPool::new(&device);
{% endhighlight %}
{% highlight rust %}
let cmd = PrimaryCommandBuffer::new().copy(&source, &destination).build();
{% endhighlight %}
We now have our command buffer! The last thing we need to do is submit it to a
queue for execution.
{% highlight rust %}
{% endhighlight %}
Note: there are several things that we can do in a more optimal way.
This code asks the GPU to execute the command buffer that contains our copy command and waits for
it to be finished.

View File

@ -11,4 +11,4 @@ them.
## Creating a window
Creating a window is out of the scope of Vulkan.

View File

@ -12,6 +12,8 @@ In order to fully optimize and parallelize commands execution, we can't just add
to draw a shape whenever we want. Instead we first have to enter "rendering mode" by entering
a *render pass*, then draw, and then leave the render pass.
This will serve as a foundation for the next tutorial, which is about drawing a triangle.
## What is a render pass?
A render pass describes how .

View File

@ -0,0 +1,218 @@
---
layout: page
title: "Tutorial 5: the first triangle"
---
# The first triangle
With some exceptions, Vulkan doesn't provide any function to easily draw shapes. There is
no draw_rectangle, draw_cube or draw_text function for example. Instead everything is handled
the same way: through the graphics pipeline. It doesn't matter whether you draw a simple
triangle or a 3D model with thousands of polygons and advanced shadowing techniques, everything
uses the same mechanics.
This is the point where the learning curve becomes very steep, as you need to learn how the
graphics pipeline works even if you just want to draw a single triangle. However once you have
passed that step, it will become easier to understand the rest.
Before we can draw a triangle, we need to prepare two things during the initialization:
- A shape that describes our triangle.
- A graphics pipeline object that will be executed by the GPU.
## Shape
A shape represents the geometry of an object. When you think "geometry", you may think of squares,
circles, etc., but in graphics programming the only shapes that we are going to manipulate are
triangles (note: tessellation unlocks the possibility to use other polygons, but this is an
advanced topic).
Here is an example of an object's shape. As you can see, it is made of hundreds of triangles and
only triangles.
TODO: The famous Utah Teapot
Each triangle is made of three vertices, which means that a shape is just a collection of vertices
linked together to form triangles. The first step to describe a shape like this with vulkano is to
create a struct named `Vertex` (the actual name doesn't matter) whose purpose is to describe each
individual vertex. Our collection of vertices can later be represented by a collection of `Vertex`
objects.
{% highlight rust %}
#[derive(Copy, Clone)]
struct Vertex {
position: [f32; 2],
}
impl_vertex!(Vertex, position);
{% endhighlight %}
Our struct contains a position field which we will use to store the position of each vertex on
the window. Being a true vectorial renderer, Vulkan doesn't use coordinates in pixels. Instead it
considers that the window has a width and a height of 2 units, and that the origin is at the
center of the window.
TODO: The windows coordinates system
When we give positions to Vulkan, we need to use this coordinate system. Let's pick a shape for
our triangle, for example this one:
TODO: Finding the coordinates of our triangle
Which translates into this code:
{% highlight rust %}
let vertex1 = Vertex { position: [-0.5, 0.5] };
let vertex2 = Vertex { position: [ 0.0, -0.5] };
let vertex3 = Vertex { position: [ 0.5, 0.25] };
{% endhighlight %}
Since we are going to pass this data to the video card, we have to put it in a buffer. This is
done in the same way as we did earlier.
{% highlight rust %}
{% endhighlight %}
## The graphics pipeline
### Shaders
In the 1990s, drawing an object with a video card consisted in sending a shape alongside with
various parameters like the color, lightning direction, fog distance, etc. Over time these
parameters became too limiting for game creators, and in the year 2000s a more flexible system
was introduced with what are called shaders. A few years later, all these parameters were removed
and totally replaced with shaders.
In order to draw a triangle, you will need some basic understanding about how the drawing process
(also called the pipeline) works.
TODO: The graphics pipeline
The list of coordinates at the left of the schema represents the vertices of the shape that we
have created earlier. When we will ask the GPU to draw this shape, it will first execute what is
called a vertex shader, once for each vertex (that means three times here). A vertex shader is
a small program whose purpose is to tell the GPU what the screen coordinates of each vertex is.
Then the GPU builds our triangle and determines which pixels of the screen are inside of it. It
will then execute a fragment shader once for each of these pixels. A fragment shader is a small
program whose purpose is to tell the GPU what the color of each pixel needs to be.
The tricky part is that we need to write the vertex and fragment shaders. To do so, we are going
to write it using a programming language named GLSL, which is very similar to the C programming
language. Teaching you GLSL would be a bit too complicated for now, so I will just give you the
source codes. Here is the source code that we will use for the vertex shader:
{% highlight glsl %}
#version 450
layout(location = 0) in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
{% endhighlight %}
When we defined the `Vertex` struct in our shape, we created a field named position which
contains the position of our vertex. But contrary to what I let you think, this struct doesn't
contain the actual position of the vertex but only a attribute whose value is passed to the vertex
shader. Vulkan doesn't care about the name of the attribute, all it does is passing its value
to the vertex shader. The `in vec2 position;` line of our shader is here to declare that we are
expected to be passed an attribute named position whose type is `vec2` (which corresponds to
`[f32; 2]` in Rust).
The main function of our shader is called once per vertex, which means three times for our
triangle. The first time, the value of position will be `[-0.5, -0.5]`, the second time it will
be `[0, 0.5]`, and the third time `[0.5, -0.25]`. It is in this function that we actually tell
Vulkan what the position of our vertex is, thanks to the `gl_Position = vec4(position, 0.0, 1.0);`
line. We need to do a small conversion because Vulkan doesn't expect two-dimensional coordinates,
but four-dimensional coordinates (the reason for this will be covered in a later tutorial).
The second shader is called the fragment shader (sometimes also named pixel shader in other APIs).
{% highlight glsl %}
#version 450
layout(location = 0) out vec4 color;
void main() {
color = vec4(1.0, 0.0, 0.0, 1.0);
}
{% endhighlight %}
This source code is very similar to our vertex shader above. This time the `main` function is
executed once per pixel and has to return the color of this pixel, which we do with the
`color = vec4(1.0, 0.0, 0.0, 1.0);` line. Just like when clearing the image, we need to pass the
red, green, blue and alpha components of the pixel. Here we are returning an opaque red color.
In a real application you most likely want to return different values depending on the pixel,
but this will be covered in later tutorials.
### Compiling the shaders
Before we can pass our shaders to Vulkan, we have to compile them in a format named **SPIR-V**.
{% highlight rust %}
mod vs { include!{concat!(env!("OUT_DIR"), "/shaders/src/bin/triangle_vs.glsl")} }
let vs = vs::Shader::load(&device).expect("failed to create shader module");
mod fs { include!{concat!(env!("OUT_DIR"), "/shaders/src/bin/triangle_fs.glsl")} }
let fs = fs::Shader::load(&device).expect("failed to create shader module");
{% endhighlight %}
### Building the graphics pipeline object
{% highlight rust %}
use vulkano::descriptor::pipeline_layout::EmptyPipeline;
use vulkano::framebuffer::Subpass;
use vulkano::pipeline::GraphicsPipeline;
use vulkano::pipeline::GraphicsPipelineParams;
use vulkano::pipeline::blend::Blend;
use vulkano::pipeline::depth_stencil::DepthStencil;
use vulkano::pipeline::input_assembly::InputAssembly;
use vulkano::pipeline::multisample::Multisample;
use vulkano::pipeline::vertex::SingleBufferDefinition;
use vulkano::pipeline::viewport::ViewportsState;
use vulkano::pipeline::viewport::Viewport;
use vulkano::pipeline::viewport::Scissor;
let pipeline = GraphicsPipeline::new(&device, GraphicsPipelineParams {
vertex_input: SingleBufferDefinition::new(),
vertex_shader: vs.main_entry_point(),
input_assembly: InputAssembly::triangle_list(),
geometry_shader: None,
viewport: ViewportsState::Fixed {
data: vec![(
Viewport {
origin: [0.0, 0.0],
depth_range: 0.0 .. 1.0,
dimensions: [images[0].dimensions()[0] as f32,
images[0].dimensions()[1] as f32],
},
Scissor::irrelevant()
)],
},
raster: Default::default(),
multisample: Multisample::disabled(),
fragment_shader: fs.main_entry_point(),
depth_stencil: DepthStencil::disabled(),
blend: Blend::pass_through(),
layout: &EmptyPipeline::new(&device).unwrap(),
render_pass: Subpass::from(&render_pass, 0).unwrap(),
}).unwrap();
{% endhighlight %}
This big struct contains all the parameters required to describe the draw operation to Vulkan.
## Drawing
Now that we have prepared our shape and graphics pipeline object, we can finally draw this
triangle!
Remember the target object? We will need to use it to start a draw operation.
Starting a draw operation needs several things: a source of vertices (here we use our vertex_buffer), a source of indices (we use our indices variable), a program, the program's uniforms, and some draw parameters. We will explain what uniforms and draw parameters are in the next tutorials, but for the moment we will just ignore them by passing a EmptyUniforms marker and by building the default draw parameters.
target.draw(&vertex_buffer, &indices, &program, &glium::uniforms::EmptyUniforms,
&Default::default()).unwrap();
The "draw command" designation could make you think that drawing is a heavy operation that takes a lot of time. In reality drawing a triangle takes less than a few microseconds, and if everything goes well you should see a nice little triangle: