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

Af­ter read­ing the “How to draw a red square in Qt Quick” blog post show­cas­ing the sim­plic­i­ty of Qt API I thought it would be in­ter­est­ing to try some­thing sim­i­lar in Mag­num for com­par­i­son.

The fol­low­ing state­ment cre­ates and ren­ders red square in Mag­num. The state­ment is wrapped on four lines for bet­ter read­abil­i­ty:

std::get<0>(MeshTools::compile(Primitives::Square::solid(), BufferUsage::StaticDraw))
    .draw(Shaders::Flat2D{}
        .setTransformationProjectionMatrix(Matrix3::scaling({0.2f, 0.3f}))
        .setColor(Color3::red()));

Copy-past­ing it in­to draw event of ba­sic boos­t­rap ap­pli­ca­tion and do­ing a bunch of #include and li­brary link­ing bu­reau­cra­cy will re­sult in this im­age:

Magnum One Statement Red Square

The Qt Quick code is much short­er, but the C++ ver­sion of com­pa­ra­ble per­for­mance is far more ver­bose in Qt than in Mag­num. A cou­ple things worth not­ing:

  • Un­like in the Qt code we aren’t us­ing any scene graph but do ev­ery­thing di­rect­ly. In this par­tic­u­lar case the scene graph would com­pli­cate things rather than make them sim­pler.
  • Thanks to ex­pres­sion-ori­ent­ed (and not state­ment-ori­ent­ed) API and method chain­ing it’s pos­si­ble to con­cate­nate many calls on one line.
  • In C++ all tem­po­raries are kept un­til the end of the state­ment (the ; char­ac­ter), so the code doesn’t re­ly on any un­de­fined be­hav­ior, even though it might look like that.
  • This is ob­vi­ous­ly per­for­mant on­ly as one-off draw, recre­at­ing all ob­jects ev­ery frame wouldn’t be ex­act­ly ef­fi­cient.
  • This ex­act thing can be done even sim­pler (and faster) us­ing scis­sor test and frame­buffer clear, but that’s not the point.

The fol­low­ing code is the ex­act equiv­a­lent of the above, with ex­plic­it tem­po­raries for bet­ter un­der­stand­ing:

const Trade::MeshData2D meshData = Primitives::Square::solid();

Mesh mesh;
std::unique_ptr<Buffer> vertices, indices;
std::tie(mesh, vertices, indices) = MeshTools::compile(meshData, BufferUsage::StaticDraw);

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

mesh.draw(shader);

The set­up and ac­tu­al draw­ing is now clear­ly sep­a­rat­ed. You can now see that we abused method chain­ing to cre­ate, con­fig­ure and pass the shad­er to Mesh::draw() in sin­gle ex­pres­sion, but that’s per­fect­ly le­gal thing to do. Hav­ing 2D equiv­a­lents of ev­ery­thing al­so makes things a bit sim­pler, on the oth­er hand dis­play­ing a 3D cube would on­ly need dif­fer­ent prim­i­tive, dif­fer­ent shad­er with more in­volved con­fig­u­ra­tion and en­abling depth test. The code is al­so 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 par­ti­cle sys­tem).

Go­ing deep­er

As not­ed be­fore, there is pre­cise­ly no low­er lev­el in which we could do things more ef­fici­tent­ly. The on­ly low­er lev­el are raw OpenGL calls, which would have com­pa­ra­ble per­for­mance but with far more ver­bosi­ty and less er­ror check­ing. The on­ly thing we can do is to recre­ate parts of the set­up by hand.

Man­u­al­ly cre­at­ing the mesh

The Mesh­Tools::com­pile() func­tion is all-in-one tool for cre­at­ing gener­ic mesh­es from im­port­ed da­ta. In this case the prepa­ra­tion is very sim­ple, so we can re­place it with the fol­low­ing. Note that we need on­ly the ver­tex buf­fer (the in­dex buf­fer above was nullptr as it was al­so not need­ed).

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

Buffer buffer;
buffer.setData(data, BufferUsage::StaticDraw);
Mesh mesh;
mesh.setPrimitive(MeshPrimitive::TriangleStrip)
    .setVertexCount(4)
    .addVertexBuffer(buffer, 0, Shaders::Flat2D::Position());

Man­u­al­ly cre­at­ing the shad­er

The stock Shaders::Flat2D shad­er in­ter­nal­ly em­ploys a bunch of com­pat­i­bil­i­ty stuff to make it work­ing on all sup­port­ed OpenGL, OpenGL ES and We­bGL sys­tems. To make things sim­pler we will re­strict our shad­er to GLSL 4.30 on­ly. Al­so all er­ror check­ing is omit­ted for brevi­ty:

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

    FlatShader() {
        Shader vert{Version::GL430, 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();

        Shader frag{Version::GL430, 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 slight­ly mod­i­fied to use our shad­er, i.e. FlatShader in­stead of Shaders::Flat2D:

// ...

Mesh mesh;
mesh.setPrimitive(MeshPrimitive::TriangleStrip)
    .setVertexCount(4)
    .addVertexBuffer(buffer, 0, FlatShader::Position());

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

// ...

Con­clu­sion

The code above shows that:

  • The li­brary is low-lev­el, but low-lev­el doesn’t nec­ces­sar­i­ly mean ver­bose.
  • High­er-lev­el con­cepts are built on top of low­er-lev­el fea­tures, they are not re­plac­ing them. So when you want to e.g. use scene graph, you just take your low­er-lev­el code as is, wrap it in Scene­Graph API and you are done.
  • Many fea­tures are there to sim­pli­fy com­mon tasks (such as pre-made prim­i­tives 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­nif­i­cant por­tions of the li­brary can be re­placed with cus­tom or low­er-lev­el so­lu­tions and the rest of the code will just work with them.

That’s all. Hap­py hack­ing!