The goal of the gfx-rs
project is to make a high-performance, easy to use,
robust graphics API for the Rust programming language. Later posts will detail
show how we achieve that, but this one serves as a high-level introduction and
tutorial to gfx-rs
. A basic familiarity with 3D graphics in general is
assumed (know what a vertex is). We’ll walk through the triangle
example.
Basic Setup
First we need some boilerplate to link to the libraries we need.
#![feature(phase)]
#[phase(plugin)]
extern crate gfx_macros;
extern crate gfx;
extern crate glfw;
Next, we define our vertex format. For this simple example, a 2D colored
triangle, we only need 2D coordinates and the color. The #[vertex_format]
attribute makes gfx-rs
generate all of the glue code necessary for using our
custom type with the underlying graphics API:
#[vertex_format]
struct Vertex {
pos: [f32, ..2],
color: [f32, ..3]
}
Creating a renderer
The first thing any gfx-rs
program needs to do is get a window to render to.
Right now, only the glfw
library is supported. We create an 800x600 window
using the lastest OpenGL version the graphics driver supports:
let glfw = glfw::init(glfw::FAIL_ON_ERRORS).unwrap();
let (mut window, events) = gfx::glfw::WindowBuilder::new(&glfw)
.title("Welcome to gfx-rs!")
.try_modern_context_hints()
.create()
.expect("Could not make window :(");
The next thing we need to do is create a renderer and a device:
let (renderer, mut device) = {
let (context, provider) = gfx::glfw::Platform::new(window.render_context(), &glfw);
gfx::start(context, provider, 1).unwrap()
};
The context provides buffer swapping, and the provider exposes GL extension
querying and function loading. The magic 1
is how many frames the renderer
will send before it blocks on the device. When renderer.end_frame()
is
called, it will wait for a corresponding device.update()
to finish
processing the frame.
The device
abstracts over a specific graphics API and isn’t that
interesting, but the renderer
provides a high-level, easy to use interface.
Let’s create a thread that will drive the renderer
. This will allow the
device to still make progress on the sent draw calls while the renderer is
preparing more to send:
spawn(proc() {
let mut renderer = renderer;
In order to begin drawing we’ll need to prepare:
- A
Frame
to render into. - A
DrawState
. We don’t customize it here, but this tracks things like vertex winding order and how to do depth testing. - A mesh to draw.
- A shader program to draw with.
- The description of how we want to clear the
Frame
before we draw into it.
let frame = gfx::Frame::new();
let state = gfx::DrawState::new();
let vertex_data = vec![
Vertex { pos: [ -0.5, -0.5 ], color: [1.0, 0.0, 0.0] },
Vertex { pos: [ 0.5, 0.5 ], color: [0.0, 1.0, 0.0] },
Vertex { pos: [ 0.0, 0.5 ], color: [0.0, 0.0, 1.0] }
];
let mesh = renderer.create_mesh(vertex_data);
let program = renderer.create_program(...);
let bundle = renderer.bundle_program(program, ()).unwrap();
let clear = gfx::ClearData {
color: Some(gfx::Color([0.3, 0.3, 0.3, 1.0])),
depth: None,
stencil: None
};
The details of creating a shader program are skipped here, to be shown in a later post.
We are now ready to write the renderer loop. This is a simple demo, so there won’t be much here. We’ll clear the frame, draw the mesh, and then tell the device that we have finished:
while !renderer.should_finish() {
renderer.clear(clear, frame);
renderer.draw(&mesh, gfx::mesh::VertexSlice(0, 3), frame, &bundle, state)
.unwrap();
renderer.end_frame();
for err in renderer.errors() {
println!("Render error: {}", err);
}
}
})
Back in the main thread, we need to process the commands that the renderer
is sending:
while !window.should_close() {
glfw.poll_events();
// any event handling
device.update();
}
device.close();
The update
method will process any commands it has received from the
renderer. Compile and run, and you get: