How to draw a red square in Magnum — in one statement

After read­ing the “How to draw a red square in Qt Quick” blog post show­cas­ing the sim­pli­city of Qt API I thought it would be in­ter­est­ing to try some­thing sim­il­ar in Mag­num for com­par­is­on.

The fol­low­ing state­ment cre­ates and renders a red square in Mag­num. The state­ment is wrapped on four lines for bet­ter read­ab­il­ity:

MeshTools::compile(Primitives::squareSolid()))
    .draw(Shaders::Flat2D{}
        .setTransformationProjectionMatrix(Matrix3::scaling({0.2f, 0.3f}))
        .setColor(0xff0000_rgbf));

Copy-past­ing it in­to draw event of the ba­sic boostrap ap­plic­a­tion and do­ing a bunch of #include and lib­rary link­ing bur­eau­cracy will res­ult in this im­age:

Magnum One Statement Red Square
import QtQuick 2.0

Item {
    Rectangle {
        color: "red"
        width: 100
        height: 100
        x: 100
        y: 100
    }
}
Draw­ing a red square in Qt Quick

Be sure to read the ori­gin­al blog post as well so you can do a fair com­par­is­on. The code in ques­tion is above.

The Qt Quick code, shown on the right, is slightly short­er, but the C++ ver­sion of com­par­able per­form­ance shown in the post is far more verb­ose in Qt than in Mag­num. A couple things worth not­ing:

  • Un­like in the Qt code we aren’t us­ing any scene graph but do everything dir­ectly. In this par­tic­u­lar case the scene graph would com­plic­ate things rather than make them sim­pler.
  • Thanks to ex­pres­sion-ori­ented (and not state­ment-ori­ented) API and meth­od chain­ing it’s pos­sible to con­cat­en­ate many calls on one line.
  • In C++ all tem­por­ar­ies are kept un­til the end of the state­ment (the ; char­ac­ter), so the code doesn’t rely on any un­defined be­ha­vi­or, even though it might look like that.
  • This is ob­vi­ously only per­form­ant as an one-off draw, re­cre­at­ing all ob­jects every frame wouldn’t be ex­actly ef­fi­cient.
  • This ex­act thing can be done even sim­pler (and faster) us­ing scis­sor test and frame­buf­fer clear, but that’s not the point.

The fol­low­ing code is the ex­act equi­val­ent of the above, with ex­pli­cit tem­por­ar­ies for bet­ter un­der­stand­ing:

Trade::MeshData2D meshData = Primitives::squareSolid();
GL::Mesh mesh = MeshTools::compile(meshData);

Shaders::Flat2D shader;
shader.setTransformationProjectionMatrix(Matrix3::scaling({0.2f, 0.3f}))
      .setColor(0xff0000_rgbf);

mesh.draw(shader);

The setup and ac­tu­al draw­ing is now clearly sep­ar­ated. You can now see that we ab­used meth­od chain­ing to cre­ate, con­fig­ure and pass the shader to GL::Mesh::draw() in a single ex­pres­sion, but that’s a per­fectly leg­al thing to do. Hav­ing 2D equi­val­ents of everything also makes things a bit sim­pler, on the oth­er hand dis­play­ing a 3D cube would only need a dif­fer­ent prim­it­ive, a dif­fer­ent shader with more in­volved con­fig­ur­a­tion and en­abling the depth test. The code is also as fast as it could get, un­less you have a very spe­cif­ic use case (like draw­ing thou­sands of squares in a particle sys­tem).

Go­ing deep­er

As noted be­fore, there is pre­cisely no lower level in which we could do things more ef­fi­cit­ently. The only lower level are raw OpenGL calls, which would have com­par­able per­form­ance but with far more verb­os­ity and less er­ror check­ing. The only thing we can do is to re­cre­ate parts of the setup by hand.

Manu­ally cre­at­ing the mesh

The MeshTools::com­pile() func­tion is an all-in-one tool for cre­at­ing gen­er­ic meshes from im­por­ted data. In this case the pre­par­a­tion is very simple, so we can re­place it with the fol­low­ing. Note that we need only the ver­tex buf­fer (the in­dex buf­fer above was nullptr as it was also not needed).

constexpr Vector2 data[]{{ 1.0f, -1.0f}, { 1.0f,  1.0f},
                         {-1.0f, -1.0f}, {-1.0f,  1.0f}};

GL::Buffer buffer;
buffer.setData(data);

GL::Mesh mesh{MeshPrimitive::TriangleStrip};
mesh.setCount(4)
    .addVertexBuffer(buffer, 0, Shaders::Flat2D::Position{});

Manu­ally cre­at­ing the shader

The stock Shaders::Flat2D shader in­tern­ally em­ploys a bunch of com­pat­ib­il­ity stuff to make it work­ing on all sup­por­ted OpenGL, OpenGL ES and WebGL sys­tems. To make things sim­pler we will re­strict our shader to GLSL 4.30 only. Also all er­ror check­ing is omit­ted for brev­ity:

struct FlatShader: GL::AbstractShaderProgram {
    typedef GL::Attribute<0, Vector2> Position;

    FlatShader() {
        GL::Shader vert{GL::Version::GL430, GL::Shader::Type::Vertex};
        vert.addSource(R"GLSL(
layout(location = 0) uniform mat3 matrix;
layout(location = 0) in vec4 position;

void main() {
    gl_Position = vec4(matrix*position.xyw, 0.0).xywz;
}
)GLSL").compile();

        GL::Shader frag{GL::Version::GL430, GL::Shader::Type::Fragment};
        frag.addSource(R"GLSL(
layout(location = 1) uniform vec4 color;
out vec4 fragmentColor;

void main() {
    fragmentColor = color;
}
)GLSL").compile();

        attachShader(vert);
        attachShader(frag);
        link();
    }

    FlatShader& setTransformationProjectionMatrix(const Matrix3& matrix) {
        setUniform(0, matrix);
        return *this;
    }

    FlatShader& setColor(const Color4& color) {
        setUniform(1, color);
        return *this;
    }
};

The ac­tu­al code is then just slightly mod­i­fied to use our shader, i.e. FlatShader in­stead of Shaders::Flat2D:

// ...

GL::Mesh mesh{MeshPrimitive::TriangleStrip};
mesh.setCount(4)
    .addVertexBuffer(buffer, 0, FlatShader::Position{});

FlatShader shader;
shader.setTransformationProjectionMatrix(Matrix3::scaling({0.2f, 0.3f}))
      .setColor(0xff0000_rgbf);

// ...

Con­clu­sion

The code above shows that:

  • The lib­rary is low-level, but low-level doesn’t nec­cessar­ily mean verb­ose.
  • High­er-level con­cepts are built on top of lower-level fea­tures, they are not re­pla­cing them. So when you want to e.g. use scene graph, you just take your lower-level code as is, wrap it in the SceneGraph API and you are done.
  • Many fea­tures are there to sim­pli­fy com­mon tasks (such as pre-made prim­it­ives or stock shaders), but it doesn’t mean that they will get in the way when you want to do some­thing more in­volved.
  • Sig­ni­fic­ant por­tions of the lib­rary can be re­placed with cus­tom or lower-level solu­tions and the rest of the code will just work with them.

That’s all. Happy hack­ing!