mirror of
https://github.com/vulkano-rs/vulkano.git
synced 2024-10-30 05:52:44 +00:00
Some random work on the tutorial
This commit is contained in:
parent
a6ea28f7cf
commit
07bd5d4696
@ -47,27 +47,40 @@ To do so, let's create two buffers first: one source and one destination. There
|
||||
ways to create a buffer in vulkano, but for now we're going to use a `CpuAccessibleBuffer`.
|
||||
|
||||
{% highlight rust %}
|
||||
let source = CpuAccessibleBuffer::new(&device, 3, &BufferUsage::all(), Some(queue.family()))
|
||||
let source = CpuAccessibleBuffer::array(&device, 3, &BufferUsage::all(), Some(queue.family()))
|
||||
.expect("failed to create buffer");
|
||||
let destination = CpuAccessibleBuffer::new(&device, 3, &BufferUsage::all(), Some(queue.family()))
|
||||
let destination = CpuAccessibleBuffer::array(&device, 3, &BufferUsage::all(), Some(queue.family()))
|
||||
.expect("failed to create buffer");
|
||||
{% endhighlight %}
|
||||
|
||||
Creating a buffer in Vulkan requires passing several informations.
|
||||
|
||||
The first parameter is the device to use. Most objects in Vulkan and in vulkano are linked to a
|
||||
specific device, and only objects that belong to the same device can interact with each other.
|
||||
Most of the time you will only have one `Device` object alive, so it's not a problem.
|
||||
|
||||
The second parameter is present only because we use `CpuAccessibleBuffer::array` and corresponds
|
||||
to the capacity of the array in number of elements.
|
||||
|
||||
The third parameter tells the Vulkan implementation in which ways the buffer is going to be used.
|
||||
Thanks to this, the implementation may be capable of performing some optimizations. Here we just
|
||||
pass a dummy value, but in a real code you should indicate.
|
||||
|
||||
The final parameter is the queue family which are going to perform operations on the buffer.
|
||||
|
||||
## Copying
|
||||
|
||||
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.
|
||||
which contains one or more commands, and then submit the command buffer.
|
||||
|
||||
{% highlight rust %}
|
||||
let cb_pool = CommandPool::new(&device);
|
||||
{% endhighlight %}
|
||||
That sounds complicated, but it is not:
|
||||
|
||||
{% 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.
|
||||
We now have our command buffer! It is ready to be executed. The last thing we need to do is
|
||||
submit it to a queue for execution.
|
||||
|
||||
{% highlight rust %}
|
||||
|
||||
|
@ -11,14 +11,15 @@ the result.
|
||||
|
||||
## Creating a window
|
||||
|
||||
Creating a window is out of the scope of Vulkan. Instead we have to use platform-specific
|
||||
functionnalities dedicated to opening a window.
|
||||
Creating a window is out of the scope of Vulkan. Instead, just like for OpenGL and other
|
||||
graphical APIs we have to use platform-specific functionnalities dedicated to opening a window.
|
||||
|
||||
For the purpose of this tutorial, we are going to use the `winit` and the `vulkano-win` crates.
|
||||
The former will be used to open a window and handle keyboard and mouse input, and the latter
|
||||
is used as a glue between `winit` and `vulkano`.
|
||||
is used as a glue between `winit` and `vulkano`. It is possible to manipulate windows in vulkano
|
||||
without using any third-party crate, but doing so would require unsafe code.
|
||||
|
||||
To do so, let's add these dependencies to our Cargo.toml:
|
||||
Let's add these dependencies to our Cargo.toml:
|
||||
|
||||
{% highlight toml %}
|
||||
winit = "0.5"
|
||||
@ -50,8 +51,8 @@ The reason is that surfaces are actually not part of Vulkan itself, but of sever
|
||||
to the Vulkan API. These extensions are disabled by default and need to be manually enabled when
|
||||
creating the instance before one can use their capabilities.
|
||||
|
||||
Fortunately the `vulkano_win` provides a function named `required_extensions()` that will return
|
||||
a list of the extensions that are needed.
|
||||
To make this task easier, the `vulkano_win` provides a function named `required_extensions()` that
|
||||
will return a list of the extensions that are needed on the current platform.
|
||||
|
||||
In order to make this work, we need to modify the way the instance is created:
|
||||
|
||||
@ -68,3 +69,11 @@ a window.
|
||||
## Events handling
|
||||
|
||||
## Creating a swapchain
|
||||
|
||||
Since the window is ultimately on the screen, things are a bit special.
|
||||
|
||||
## Clearing the image
|
||||
|
||||
{% highlight rust %}
|
||||
let cmd = PrimaryCommandBuffer::new().copy(&source, &destination).build();
|
||||
{% endhighlight %}
|
||||
|
@ -10,9 +10,11 @@ However our ultimate goal is to draw some shapes on that surface, not just clear
|
||||
|
||||
In order to fully optimize and parallelize commands execution, we can't just add ask the GPU
|
||||
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.
|
||||
what is called 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.
|
||||
In this section we are just going to enter a render pass and leave it immediately. This is not
|
||||
very useful per se, but it will serve as a foundation for the next tutorial, which is about
|
||||
drawing a triangle.
|
||||
|
||||
## What is a render pass?
|
||||
|
||||
@ -23,19 +25,20 @@ The term "render pass" describes two things:
|
||||
|
||||
- It also designates a kind of object that describes this rendering mode.
|
||||
|
||||
In this section, we are going to create a render pass object, and then modify our command buffer
|
||||
to enter the render pass.
|
||||
Entering a render pass (as in "the rendering mode") requires passing a render pass object.
|
||||
|
||||
## Creating a render pass
|
||||
|
||||
In this tutorial, the only thing we want to do is draw to a window. This is the most simple case
|
||||
possible, and we only need to provide two informations to a render pass: the format of the images
|
||||
of our swapchain, and the fact that we don't use multisampling (which is an advanced anti-aliasing
|
||||
technique).
|
||||
For the moment, the only thing we want to do is draw some color to an image that corresponds to
|
||||
our window. This is the most simple case possible, and we only need to provide two informations
|
||||
to a render pass: the format of the images of our swapchain, and the fact that we don't use
|
||||
multisampling (which is an advanced anti-aliasing technique).
|
||||
|
||||
However complex games can use render passes in very complex ways, with multiple subpasses and
|
||||
multiple attachments, and with various micro-optimizations. In order to accomodate for these
|
||||
complex usages, vulkano's API to create a render pass is a bit complex.
|
||||
complex usages, vulkano's API to create a render pass is a bit particular.
|
||||
|
||||
TODO: provide a simpler way in vulkano to do that?
|
||||
|
||||
{% highlight rust %}
|
||||
mod render_pass {
|
||||
@ -61,23 +64,48 @@ let render_pass = render_pass::CustomRenderPass::new(&device, &render_pass::Form
|
||||
}).unwrap();
|
||||
{% endhighlight %}
|
||||
|
||||
## Entering the render pass
|
||||
|
||||
A render pass only describes the format and the way we load and store the image we are going to
|
||||
draw upon. However we also need to indicate the actual list of attachments.
|
||||
draw upon. It is enough to initialize all the objects we need.
|
||||
|
||||
But before we can draw, we also need to indicate the actual list of attachments. This is done
|
||||
by creating a *framebuffer*.
|
||||
|
||||
Creating a framebuffer is typically done as part of the rendering process. Although it is not a
|
||||
bad idea to keep the framebuffer objects alive between frames, but it won't kill your
|
||||
performances to create and destroy a few framebuffer objects during each frame.
|
||||
|
||||
{% highlight rust %}
|
||||
let framebuffers = images.iter().map(|image| {
|
||||
let framebuffer = {
|
||||
let image = &images[image_num];
|
||||
let dimensions = [image.dimensions()[0], image.dimensions()[1], 1];
|
||||
Framebuffer::new(&render_pass, dimensions, render_pass::AList {
|
||||
color: image
|
||||
}).unwrap()
|
||||
}).collect::<Vec<_>>();
|
||||
};
|
||||
{% endhighlight %}
|
||||
|
||||
We are now ready the enter drawing mode!
|
||||
|
||||
This is done by calling the `draw_inline` function on the primary command buffer builder.
|
||||
This function takes as parameter the render pass object, the framebuffer, and a struct that
|
||||
contains the colors to fill the attachments with.
|
||||
|
||||
This struct is created by the `single_pass_renderpass!` macro and contains one field for
|
||||
each attachment that was defined with `load: Clear`.
|
||||
|
||||
Clearing our attachment has exactly the same effect as `clear_color_foo`, except that this
|
||||
time it is done by the rendering engine.
|
||||
|
||||
{% highlight rust %}
|
||||
let command_buffer = PrimaryCommandBufferBuilder::new(&cb_pool)
|
||||
.draw_inline(&render_pass, &framebuffers[image_num], render_pass::ClearValues {
|
||||
.draw_inline(&render_pass, &framebuffer, render_pass::ClearValues {
|
||||
color: [0.0, 0.0, 1.0, 1.0]
|
||||
})
|
||||
.draw_end()
|
||||
.build();
|
||||
{% endhighlight %}
|
||||
|
||||
We enter the render pass and immediately leave it afterward. In the next section, we are going
|
||||
to insert a function call between `draw_inline` and `draw_end`.
|
||||
|
@ -5,15 +5,15 @@ 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.
|
||||
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.
|
||||
If you are not familiar with other graphical APIs, 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:
|
||||
|
||||
@ -27,8 +27,7 @@ circles, etc., but in graphics programming the only shapes that we are going to
|
||||
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.
|
||||
Here is an example of an object's shape. It is made of hundreds of triangles and only triangles.
|
||||
|
||||
TODO: The famous Utah Teapot
|
||||
|
||||
@ -47,15 +46,20 @@ struct Vertex {
|
||||
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.
|
||||
In order for the struct to be processed by vulkano, it must implement the `Vertex` trait provided
|
||||
by vulkano. This can be done automatically by calling the `impl_vertex!` macro whose parameters
|
||||
are the name of the struct and its fields. In the future it will be possible to simply add
|
||||
`#[derive(VulkanoVertex)]` instead, but this is not yet available in stable Rust.
|
||||
|
||||
The struct contains a field named `position` 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:
|
||||
When we give positions to Vulkan, we need to use the coordinate system described by this image.
|
||||
Let's pick a shape for our triangle, for example this one:
|
||||
|
||||
TODO: Finding the coordinates of our triangle
|
||||
|
||||
@ -67,10 +71,19 @@ 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
|
||||
But since this data is going to be read by the video card, we have to put it in a buffer. This is
|
||||
done in the same way as we did earlier.
|
||||
|
||||
{% highlight rust %}
|
||||
let shape = CpuAccessibleBuffer::array(&device, 3, &BufferUsage::all(), Some(queue.family()))
|
||||
.expect("failed to create buffer");
|
||||
|
||||
{
|
||||
let mut content = shape.write(Duration::new(0, 0)).unwrap();
|
||||
content[0] = Vertex { position: [-0.5, 0.5] };
|
||||
content[1] = Vertex { position: [ 0.0, -0.5] };
|
||||
content[2] = Vertex { position: [ 0.5, 0.25] };
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
## The graphics pipeline
|
||||
@ -78,10 +91,10 @@ done in the same way as we did earlier.
|
||||
### 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.
|
||||
various parameters like the color of the shape, direction of the lighting, 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
|
||||
predefined 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.
|
||||
@ -90,17 +103,19 @@ 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
|
||||
called a vertex shader, once for each vertex (which 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:
|
||||
The tricky part is that we need to write the vertex and fragment shaders ourselves. To do, we are
|
||||
going to write them using a programming language named GLSL, which is very similar to the C
|
||||
programming language. The shaders that we pass to Vulkan have to be in a specific format named
|
||||
SPIR-V, which GLSL can compile to. 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
|
||||
@ -149,16 +164,75 @@ 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**.
|
||||
This can be done through yet-another crate named `vulkano-shaders`.
|
||||
|
||||
To use it, we have to tweak our Cargo.toml:
|
||||
|
||||
{% highlight toml %}
|
||||
[build-dependencies]
|
||||
vulkano-shaders = "0.1"
|
||||
{% endhighlight %}
|
||||
|
||||
Note that this is not a regular dependency, but a *build dependency*. We are not going to use
|
||||
the vulkano-shaders crate in the example itself, but in the *build script* of the example.
|
||||
|
||||
{% highlight toml %}
|
||||
build = "build.rs"
|
||||
{% endhighlight %}
|
||||
|
||||
Let's create a file named `build.rs` which will contain our build script.
|
||||
|
||||
{% highlight rust %}
|
||||
extern crate vulkano_shaders;
|
||||
|
||||
fn main() {
|
||||
vulkano_shaders::build_glsl_shaders([
|
||||
("src/vs.glsl", vulkano_shaders::ShaderType::Vertex),
|
||||
("src/fs.glsl", vulkano_shaders::ShaderType::Fragment),
|
||||
].iter().cloned());
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
This code will be compiled and executed before our real code, and will compile the `vs.glsl` and
|
||||
`fs.glsl` files into SPIR-V and put the result in the `target` directory of Cargo.
|
||||
|
||||
But the vulkano-shaders crate does more than just compile the shaders. It also analyzes their code
|
||||
and generates several Rust structs and functions that will provide information to vulkano about
|
||||
the shaders. The consequence of this, is that the files generated by vulkano-shaders are in fact
|
||||
not raw SPIR-V, but Rust code. In order to import them, we have to use the standard `include!`
|
||||
macro:
|
||||
|
||||
{% highlight rust %}
|
||||
mod vs { include!{concat!(env!("OUT_DIR"), "/shaders/src/vs.glsl")} }
|
||||
mod fs { include!{concat!(env!("OUT_DIR"), "/shaders/src/fs.glsl")} }
|
||||
{% endhighlight %}
|
||||
|
||||
The paths are the same as what we passed (including the extension), except that they are
|
||||
prefixed with `/shaders/`.
|
||||
|
||||
For better isolation, we put the code inside modules.
|
||||
|
||||
The Rust code generated for each shader always contains a struct named `Shader` with a `load`
|
||||
function. This is the glue between vulkano-shaders and vulkano.
|
||||
|
||||
{% 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 %}
|
||||
|
||||
We now have a `vs` variable that represents our vertex shader, and a `fs` variable that represents
|
||||
our fragment shader.
|
||||
|
||||
Note that in the future this whole process will be available through a procedural macro provided
|
||||
by vulkano itself, which will greatly simplify things. However this is not yet possible in stable
|
||||
Rust.
|
||||
|
||||
### Building the graphics pipeline object
|
||||
|
||||
But the shaders are not enough. Before we can draw, we also need to build a pipeline object that
|
||||
contains our two shaders but also a lot of additional parameters that describe how the rendering
|
||||
process will need to be performed.
|
||||
|
||||
{% highlight rust %}
|
||||
use vulkano::descriptor::pipeline_layout::EmptyPipeline;
|
||||
use vulkano::framebuffer::Subpass;
|
||||
@ -202,18 +276,45 @@ let pipeline = GraphicsPipeline::new(&device, GraphicsPipelineParams {
|
||||
}).unwrap();
|
||||
{% endhighlight %}
|
||||
|
||||
This big struct contains all the parameters required to describe the draw operation to Vulkan.
|
||||
A few noteworthy elements:
|
||||
|
||||
- The `vertex_input` field describes how the GPU will load our vertices. This is where we specify
|
||||
the format of our vertices (the `Vertex` struct). TODO: talk about the fact that the vertex type is inferred
|
||||
|
||||
- The `vertex_shader` and `fragment_shader` fields contain our shaders.
|
||||
|
||||
- The `viewport` field contains the dimensions of the final image. This parameter can be used to
|
||||
ask the GPU to only draw to a specific location of the image. You also have the possibility (not
|
||||
covered here) to pass the value `Dynamic`, which means that you will instead specify these
|
||||
dimensions when adding the draw command to the command buffer. Passing `Dynamic` can be slower
|
||||
on some implementations.
|
||||
|
||||
- `input_assembly` tells the implementation how vertices are linked together to form triangles.
|
||||
Since we have only a single triangle, this isn't really relevant here.
|
||||
|
||||
- `render_pass` must link to our render pass object. The pipeline will only be usable in the
|
||||
corresponding render pass.
|
||||
|
||||
## 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.
|
||||
Let's modify our command buffer building code again.
|
||||
|
||||
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.
|
||||
The draw command can only be added between `draw_inline` and `draw_end`.
|
||||
|
||||
target.draw(&vertex_buffer, &indices, &program, &glium::uniforms::EmptyUniforms,
|
||||
&Default::default()).unwrap();
|
||||
The five parameters are the pipeline object, the source of vertices, any additional customization
|
||||
for our pipeline object (like the viewport dimensions if you pass `Dynamic`), and two parameters
|
||||
that contain the external resources to pass to the shaders. We will cover everything later. For
|
||||
now only the first two parameters are relevant.
|
||||
|
||||
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:
|
||||
{% highlight rust %}
|
||||
let command_buffer = PrimaryCommandBufferBuilder::new(&cb_pool)
|
||||
.draw_inline(&render_pass, &framebuffer, render_pass::ClearValues {
|
||||
color: [0.0, 0.0, 1.0, 1.0]
|
||||
})
|
||||
.draw(&pipeline, &vertex_buffer, &DynamicState::none(), (), &())
|
||||
.draw_end()
|
||||
.build();
|
||||
{% endhighlight %}
|
||||
|
Loading…
Reference in New Issue
Block a user