Introducing Magnum, a multiplatform 2D/3D graphics engine

After nearly three years of semi-pub­lic hy­per­act­ive de­vel­op­ment, I think it’s the time to re­lease this beast in­to wild. Say hello to Mag­num, mod­u­lar graph­ics en­gine writ­ten in C++11 and OpenGL.

Mag­num star­ted as a simple wrap­per to sim­pli­fy vec­tor/mat­rix op­er­a­tions so I could learn and play with OpenGL API without writ­ing too much boil­er­plate code. Over the time it ex­pan­ded in­to ac­tu­ally us­able graph­ics en­gine. Its goal is to sim­pli­fy low-level graph­ics de­vel­op­ment and in­ter­ac­tion with OpenGL us­ing re­cent C++11 fea­tures and to ab­stract away plat­form-spe­cif­ic is­sues.

Mag­num is cur­rently por­ted to these plat­forms:

  • OpenGL 2.1 through 4.4, core pro­file func­tion­al­ity and mod­ern ex­ten­sions
  • OpenGL ES 2.0, 3.0 and ex­ten­sions to match desktop OpenGL func­tion­al­ity
  • Linux and em­bed­ded Linux (nat­ively us­ing GLX/EGL and Xlib or through GLUT or SDL2 toolkit)
  • Win­dows (through GLUT or SDL2 toolkit)
  • Google Chrome (through Nat­ive Cli­ent, both newlib and glibc tool­chains are sup­por­ted)

What it of­fers you

Use C++11 to sim­pli­fy com­mon work­flow and OpenGL in­ter­ac­tion

Mag­num makes ex­tens­ive use of C++11. Most of the new fea­tures are used in the in­tern­als to make the lib­rary more power­ful and you can only guess their pres­ence, but the best fea­tures are on every corner to sim­pli­fy your life.

C++11 list-ini­tial­iz­a­tion and com­pile-time checks al­low for easi­er and safer struc­ture ini­tial­iz­a­tion. With constexpr you can even do some lim­ited vec­tor math at com­pile-time.

Int* data = new Int{2, 5, -1, 10, 0,                          /* using C++03 */
                    3, 53, -60, -27, // oops
                    9, 0, 4, 7, 135};

Math::Matrix<3, 5, Int> a;
a.assignFrom(data);
Math::Matrix<3, 5, Int> a({2, 5, -1, 10, 0},                  /* using C++11 */
                          {3, 53, -60, -27, 25},
                          {9, 0, 4, 7, 135});

Vari­ad­ic func­tion tem­plates greatly sim­pli­fy re­pet­it­ive things and avoid mis­takes, how­ever you are not lim­ited to do this at com­pile-time only. It is pos­sible to do the equi­val­ent in run-time, but at the cost of more verb­ose code.

/* Shader properties using C++03 and pure OpenGL */
const int SHADER_POSITION = 0; // three-component
const int SHADER_NORMAL = 1; // three-component
const int SHADER_TEXCOORDS = 2; // two-component
const int SHADER_WEIGHT = 3; // one-component

/* Mesh configuration */
glEnableVertexAttribArray(SHADER_POSITION);
glEnableVertexAttribArray(SHADER_NORMAL);
glEnableVertexAttribArray(SHADER_TEXCOORDS);
glEnableVertexAttribArray(SHADER_WEIGHT);

glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
int offset = 4238;
glVertexAttribPointer(SHADER_POSITION, 3, GL_FLOAT, GL_FALSE, 40, static_cast<GLvoid*>(offset));
glVertexAttribPointer(SHADER_NORMAL, 3, GL_FLOAT, GL_FALSE, 40, static_cast<GLvoid*>(offset + 12));
glVertexAttribPointer(SHADER_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 40, static_cast<GLvoid*>(offset + 24));
glVertexAttribPointer(SHADER_WEIGHT, 2, GL_FLOAT, GL_FALSE, 40, static_cast<GLvoid*>(offset + 32)); // oops
/* Type-safe shader definition using C++11 and Magnum */
class Shader: public AbstractShaderProgram {
    public:
        typedef Attribute<0, Vector3> Position;
        typedef Attribute<1, Vector2> Normal;
        typedef Attribute<2, Vector3> TextureCoordinates;
        typedef Attribute<3, Float> Weight;

    // ...
};

/* Mesh configuration */
Buffer vertexBuffer;
Mesh mesh;
mesh.addVertexBuffer(vertexBuffer, 4238, Shader::Position(), Shader::Normal(),
    Shader::TextureCoordinates(), Shader::Weight(), 3);

Ini­tial­izer lists and user-defined lit­er­als will save you typ­ing and avoid nasty mis­takes with units in un­ob­trus­ive way:

Object3D object;                                              /* using C++03 */
object.translate(Vector3(1.5f, 0.3f, -1.0f))
    .rotate(35.0f); // this function accepts degrees, right? (it doesn't)
Object3D object;                                              /* using C++11 */
object.translate({1.5f, 0.3f, -1.0f})
    .rotate(35.0_degf);

Strongly typed enums and type-safe Enum­Set class pre­vent hard-to-spot er­rors with im­prop­er enum val­ues and en­able prop­er IDE auto­com­ple­tion for enu­mer­a­tion val­ues, sav­ing pre­cious time:

/* Using pure OpenGL, the errors are catched at run-time */
glClear(GL_COLOR|GL_DEPTH); // oops
/* Using C++11 and Magnum, the errors are catched at compile-time */
framebuffer.clear(FramebufferClear::Color|FramebufferClear::Depth);

Mag­num uses RAII prin­ciple, has OpenGL state track­ing and trans­par­ent sup­port for EX­T_­dir­ec­t_state_ac­cess. With auto­mat­ic fall­back to core func­tion­al­ity for un­sup­por­ted ex­ten­sions it al­lows you to just cre­ate an ob­ject and call a func­tion on it without any boil­er­plate code. You don’t need to handle any ex­pli­cit ini­tial­iz­a­tion and fi­nal­iz­a­tion, save and re­store the pre­vi­ous state or both­er about ex­ten­sion avail­ab­il­ity:

GLint texture;                                          /* using pure OpenGL */
glGenTextures(1, &texture);
GLint previous;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous);
glBindTexture(GL_TEXTURE_2D, texture);

if(/* ARB_texture_storage supported, faster code path */) {
    glTexStorage2D(GL_TEXTURE_2D, 4, GL_RGBA8, 256, 256);
} else {
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
}

glBindTexture(GL_TEXTURE_2D, previous);

// ...

glDeleteTextures(1, &texture);
Texture2D texture;                                           /* using Magnum */
texture.setStorage(4, TextureFormat::RGBA8, {256, 256});

These fea­tures re­quire com­piler with good enough sup­port for C++11. Of­fi­cia­ly sup­por­ted ones are GCC 4.6+ and Clang 3.1+. There is also com­pat­ib­il­ity branch with sup­port for GCC 4.4 and 4.5 (and prob­ably Visu­al Stu­dio 2012, when I get to test it). Some­times the miss­ing fea­tures are heav­ily worked around, which might case some is­sues, thus this com­pat­ib­il­ity is not part of the main­line code.

Mod­u­lar and ex­tens­ible scene graph

On top of core lib­rary tak­ing care of math and OpenGL there are vari­ous op­tion­al lib­rar­ies, which you can, but don’t have to use. One of them is scene graph im­ple­ment­a­tion for both 2D and 3D scenes. The scene graph is tem­plated on trans­form­a­tion im­ple­ment­a­tion, thus you are free to use matrices, dual qua­ternions, dual com­plex num­bers or even roll your own trans­form­a­tion im­ple­ment­a­tion. Ob­jects in scene graph are not in any lin­ear fea­ture hier­archy and par­tic­u­lar fea­tures are at­tached to giv­en ob­ject in­stead, either dy­nam­ic­ally or us­ing mul­tiple in­her­itace. This ap­proach al­lows great­er flex­ib­il­ity com­pared to lin­ear hier­archy and avoids bubble-up ef­fect (like hav­ing func­tion for set­ting wheel count on base ob­ject).

You can learn more about scene graph in the doc­u­ment­a­tion.

In­teg­ra­tion with oth­er soft­ware, plu­gins for data ma­nip­u­la­tion

Mag­num lib­rary it­self is kept light­weight and without any ex­tern­al de­pend­en­cies to make port­ing and us­age in em­bed­ded sys­tems easi­er. How­ever, in real world us­age, you of­ten need the abil­ity to im­port data in vari­ous formats. Mag­num has sup­port for both stat­ic and dy­nam­ic plu­gins and con­tains plu­gin in­ter­face for im­port­ing meshes, im­ages, au­dio files and for do­ing format con­ver­sions. Sep­ar­ate plu­gin re­pos­it­ory con­tains JPEG, PNG, TGA, COL­LADA and WAV im­port­er plu­gins.

Mag­num has also built­in plu­gin-based text lay­out­ing and ren­der­ing lib­rary. Plu­gin re­pos­it­ory con­tains Free­Type font en­gine sup­port, Harf­Buzz text lay­outer, ras­ter font sup­port and also abil­ity to con­vert between font formats.

It is of­ten de­sir­able to use ex­tern­al (math, phys­ics) lib­rary. I’m not go­ing to boast, Mag­num’s math lib­rary is pretty lim­ited in com­par­is­on with most oth­er math lib­rar­ies. Mag­num provides in­ter­face for con­vert­ing from and to ex­tern­al rep­res­ent­a­tion of math­em­at­ic struc­tures, which in the end is presen­ted to user as simple ex­pli­cit con­ver­sion. In­teg­ra­tion re­pos­it­ory con­tains ini­tial in­teg­ra­tion of Bul­let Phys­ics lib­rary.

Mag­num doesn’t con­tain its own full-fea­tured win­dow and event hand­ling ab­strac­tion lib­rary, in­stead it is able to hook in­to vari­ous mul­ti­plat­form toolkits like GLUT or SDL2 and also light­weight plat­form-spe­cif­ic toolkits such as Xlib with GLX/EGL or PPAPI.

Ex­tens­ive doc­u­ment­a­tion and ex­amples

Doc­u­ment­a­tion is es­sen­tial part of the en­gine. Each mod­ule and class has in­tro­duc­tion­al chapter and ex­ample us­age, each OpenGL sup­port class provides de­tailed in­form­a­tion about re­lated OpenGL calls and ex­ten­sion de­pend­ence. There is also ex­ample re­pos­it­ory con­tain­ing fully doc­u­mented ex­amples to ease your learn­ing even more. The doc­u­ment­a­tion also has a thor­ough guide how to start us­ing Mag­num in your pro­ject, provid­ing even ready-to-build boot­strap code.

More fea­tures

There are many more things worth men­tion­ing, you can read through the nearly ex­haust­ive fea­ture list for more in­form­a­tion. The pro­ject page con­tains also ra­tionale and design goals.

What it won’t do

Mag­num is de­signed for people who love cod­ing and stands upon in­teg­ra­tion with ex­tern­al tools. Don’t ex­pect any Game­Maker-like GUI, visu­al shaders, built­in ed­it­ors or ded­ic­ated IDE. Spe­cial­ized soft­ware will al­ways be bet­ter at that job than any in­teg­rated ed­it­or and this way you can use any tool you want.

Mag­num tries to be mod­u­lar, light­weight and doesn’t want to put any re­stric­tions or lim­it­a­tions on the user. There is no en­gine-spe­cif­ic mesh format or ef­fect frame­work, as it is nearly im­possible to cre­ate a format which will suit all ima­gin­able use cases.

Show­case

Mag­num is cur­rently used in one small game and one big­ger, yet un­na­nounced one and the func­tion­al­ity is demon­strated in vari­ous ex­amples. See show­case page for im­ages and live ap­plic­a­tions.

Push The Box

Where can you get it

Be­cause the lib­rary is meant to be used by de­velopers and not end users, it is dis­trib­uted purely as source code, avail­able on Git­Hub. The doc­u­ment­a­tion is avail­able for on­line view­ing, you can also gen­er­ate it dir­ectly from the source code, see the build­ing doc­u­ment­a­tion for more in­form­a­tion. Be sure to read also the thor­ough Get­ting Star­ted Guide.