mirror of
https://github.com/vulkano-rs/vulkano.git
synced 2024-11-26 16:54:16 +00:00
Some work on the tutorial
This commit is contained in:
parent
3fc8bb615d
commit
bcff47b20a
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -11,4 +11,4 @@ them.
|
||||
|
||||
## Creating a window
|
||||
|
||||
|
||||
Creating a window is out of the scope of Vulkan.
|
||||
|
@ -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 .
|
||||
|
218
www/guide/05-first-triangle.md
Normal file
218
www/guide/05-first-triangle.md
Normal 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:
|
Loading…
Reference in New Issue
Block a user