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 ();