Rugl - Declarative Stateless OpenGL


#1

I’ve been working on a side project to build a clone of my favorite WebGL framework regl in Rust. It seemed like a nice problem to work through. I’ve finally got it to the point where it’s actually moving things across the screen.

This tweet has a video of the triangles moving:

And this is what the API looks like so far for creating the draw call:

extern crate rugl;

fn main() {
    let mut rugl = rugl::init();
    let count = 1000;

    let draw = rugl.draw()
        .vert("
            #version 150
            in vec2 position;
            in float id;

            uniform float time;
            uniform float count;

            float TRIANGLE_SIZE = 0.02;

            void main() {
                float unit_id = id / count;
                vec2 offset = vec2(
                    2.0 * (unit_id - 0.5) + 0.1 * sin(unit_id * 30.0 + time * 2.5) + 0.5 * sin(unit_id * 50.0 + time * 5.234),
                    (sin(unit_id * 20.0 + time) + sin(unit_id * 7.0 + time * 2.83)) / 3.0 + 0.5 * sin(unit_id * 66.0 + time * 7.234)
                );
                gl_Position = vec4(
                    position * TRIANGLE_SIZE + offset,
                    0.0,
                    1.0
                );
            }
        ")
        .frag("
            #version 150
            out vec4 out_color;
            void main() {
                out_color = vec4(1.0, 1.0, 1.0, 1.0);
            }
        ")
        .uniform("time", Box::new(|env| Box::new(env.time as f32)))
        .uniform("count", {
            let count_in = count.clone() as f32;
            Box::new(move |env| Box::new(count_in))
        })
        .attribute("position", {
            &((0..(count * 3)).map(|i| {
                // Equilateral triangle
                match i % 3 {
                    0 => [0.0, 0.5],
                    1 => [0.36056, -0.5],
                    2 => [-0.36056, -0.5],
                    _ => panic!("Math is hard.")
                }
            }).collect::<Vec<[f32; 2]>>())
        })
        .attribute("id", {
            &((0..(count * 3)).map(|i| {
                (i as f32 / 3.0).floor()
            }).collect::<Vec<f32>>())
        })
        .count(count * 3)
        .finalize();

    rugl.frame(|env| {
        draw(env);
    });
}

So far the experience in Rust has been interesting. The biggest challenges have been dealing with generics and traits for polymorphic methods. I come from more of a dynamic language background, so dealing with code at the systems level has been a nice challenge. There are quite a few unsafe blocks dealing with the gl-side of things, so I had to spend a lot of time working on understanding how Rust lays things out in memory.

Next steps will be to add more support for different data types on uniforms and attributes, and maybe try to write a few macros to cut down on the verbosity of some of the draw calls. Testing seems like an interesting problem to solve as well.


#2

This looks like the perfect crate for rendering glTF 1.0.


#3

Ah, that would be nice to get it far along enough to be able to support glTF.


#4

This looks great and I love everything stateless!

I had a look through your code and was wondering about the vec3 and mat4 implementations. You are implementing .add(..) and .substract(..) directly instead of through traits, which would allow you to use + and other operators on your types. Is there a particular reason for that or did you just not get around to converting it to traits?

(Edit) More thoughts:

Here is a guess: With type Vec3 = [f64; 3] orphan rules probably prevent implementing traits, and wrapping it inside a struct might not be ergonomic enough.

I mostly wonder about your choice to implement vector math yourself. Is that something that you intend to keep or do you hope to migrate to a different crate? That sounds like criticism and I don’t know how to rephrase it :confused:. I totally understand implementing it yourself to avoid dependencies and keep tight control over it. I would have done it myself. Polishing it (traits, more tests) is of course only needed if you intend to keep it.


#5

I definitely do not intend on supporting a matrix math library inside of this. I’m currently writing some of one myself mainly to be able to control the ergonomics of the examples (for myself really). I may spin it off into its own crate if it’s useful at all. I generally prefer simplicity of API design for writing my own graphics code and doing math operations.

For this project, I’m trying to find the most basic set of values I can use to represent the GL types. I’d much rather have as inputs simple arrays and vectors so that I can interop easily with existing libraries. I would assume that they would all support wrapping and unwrapping basic data types from their interfaces.

I’m trying to keep user-facing API for creating draw calls as simple as possible, so I’m doing things like interfacing with the gl-side of things by implementing traits for different basic value types. For instance I can take a [f32; 3] and then set a uniform value by calling gl::Uniform3f, and then with some additional hints from the shader I can chose the correct gl call for a given data type. I haven’t done as much testing on this side, but it would be great if I can fail early and often when you pass the wrong data in. The GL-side of things is pretty unsafe, so the more I can do the right thing, the better.

As for the orphan rules, doing something like pub struct Mat4([f64; 16]); gives you some nice overloading traits, but then getting the actual values out seems a bit tedious. I’m still at the wait and see stage to see the direction I want to go for my personal code, but for this library I definitely don’t want to couple it to a single math library implementation.