Aug
30
2013

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

IT

After nearly three years of semi-public hyperactive development, I think it's the time to release this beast into wild. Say hello to Magnum, modular graphics engine written in C++11 and OpenGL.

Magnum started as a simple wrapper to simplify vector/matrix operations so I could learn and play with OpenGL API without writing too much boilerplate code. Over the time it expanded into actually usable graphics engine. Its goal is to simplify low-level graphics development and interaction with OpenGL using recent C++11 features and to abstract away platform-specific issues.

Magnum is currently ported to these platforms:

  • OpenGL 2.1 through 4.4, core profile functionality and modern extensions
  • OpenGL ES 2.0, 3.0 and extensions to match desktop OpenGL functionality
  • Linux and embedded Linux (natively using GLX/EGL and Xlib or through GLUT or SDL2 toolkit)
  • Windows (through GLUT or SDL2 toolkit)
  • Google Chrome (through Native Client, both newlib and glibc toolchains are supported)

What it offers you

Use C++11 to simplify common workflow and OpenGL interaction

Magnum makes extensive use of C++11. Most of the new features are used in the internals to make the library more powerful and you can only guess their presence, but the best features are on every corner to simplify your life.

C++11 list-initialization and compile-time checks allow for easier and safer structure initialization. With constexpr you can even do some limited vector math at compile-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});

Variadic function templates greatly simplify repetitive things and avoid mistakes, however you are not limited to do this at compile-time only. It is possible to do the equivalent in run-time, but at the cost of more verbose 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);

Initializer lists and user-defined literals will save you typing and avoid nasty mistakes with units in unobtrusive way:

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

Strongly typed enums and type-safe EnumSet class prevent hard-to-spot errors with improper enum values and enable proper IDE autocompletion for enumeration values, saving precious 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);

Magnum uses RAII principle, has OpenGL state tracking and transparent support for EXT_direct_state_access. With automatic fallback to core functionality for unsupported extensions it allows you to just create an object and call a function on it without any boilerplate code. You don't need to handle any explicit initialization and finalization, save and restore the previous state or bother about extension availability:

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 features require compiler with good enough support for C++11. Officialy supported ones are GCC 4.6+ and Clang 3.1+. There is also compatibility branch with support for GCC 4.4 and 4.5 (and probably Visual Studio 2012, when I get to test it). Sometimes the missing features are heavily worked around, which might case some issues, thus this compatibility is not part of the mainline code.

Modular and extensible scene graph

On top of core library taking care of math and OpenGL there are various optional libraries, which you can, but don't have to use. One of them is scene graph implementation for both 2D and 3D scenes. The scene graph is templated on transformation implementation, thus you are free to use matrices, dual quaternions, dual complex numbers or even roll your own transformation implementation. Objects in scene graph are not in any linear feature hierarchy and particular features are attached to given object instead, either dynamically or using multiple inheritace. This approach allows greater flexibility compared to linear hierarchy and avoids bubble-up effect (like having function for setting wheel count on base object).

You can learn more about scene graph in the documentation.

Integration with other software, plugins for data manipulation

Magnum library itself is kept lightweight and without any external dependencies to make porting and usage in embedded systems easier. However, in real world usage, you often need the ability to import data in various formats. Magnum has support for both static and dynamic plugins and contains plugin interface for importing meshes, images, audio files and for doing format conversions. Separate plugin repository contains JPEG, PNG, TGA, COLLADA and WAV importer plugins.

Magnum has also builtin plugin-based text layouting and rendering library. Plugin repository contains FreeType font engine support, HarfBuzz text layouter, raster font support and also ability to convert between font formats.

It is often desirable to use external (math, physics) library. I'm not going to boast, Magnum's math library is pretty limited in comparison with most other math libraries. Magnum provides interface for converting from and to external representation of mathematic structures, which in the end is presented to user as simple explicit conversion. Integration repository contains initial integration of Bullet Physics library.

Magnum doesn't contain its own full-featured window and event handling abstraction library, instead it is able to hook into various multiplatform toolkits like GLUT or SDL2 and also lightweight platform-specific toolkits such as Xlib with GLX/EGL or PPAPI.

Extensive documentation and examples

Documentation is essential part of the engine. Each module and class has introductional chapter and example usage, each OpenGL support class provides detailed information about related OpenGL calls and extension dependence. There is also example repository containing fully documented examples to ease your learning even more. The documentation also has a thorough guide how to start using Magnum in your project, providing even ready-to-build bootstrap code.

More features

There are many more things worth mentioning, you can read through the nearly exhaustive feature list for more information. The project page contains also rationale and design goals.

What it won't do

Magnum is designed for people who love coding and stands upon integration with external tools. Don't expect any GameMaker-like GUI, visual shaders, builtin editors or dedicated IDE. Specialized software will always be better at that job than any integrated editor and this way you can use any tool you want.

Magnum tries to be modular, lightweight and doesn't want to put any restrictions or limitations on the user. There is no engine-specific mesh format or effect framework, as it is nearly impossible to create a format which will suit all imaginable use cases.

Showcase

Magnum is currently used in one small game and one bigger, yet unnanounced one and the functionality is demonstrated in various examples. See showcase page for images and live applications.

Where can you get it

Because the library is meant to be used by developers and not end users, it is distributed purely as source code, available on GitHub. The documentation is available for online viewing, you can also generate it directly from the source code, see instructions on download page for more information. Be sure to read also the thorough Getting Started Guide.

MagnumDownload latest » Add comment

Discussion: 6 comments

Wouldn't it have been nice to use Eigen instead of rolling your own math library? Any particular reason for not using Eigen?
I didn't choose to use Eigen (or GLM or any particular math library) for these reasons:

1) C++11 and backwards compatibility. All popular math libraries need to maintain backwards compatibility, which means that they cannot use C++11 features extensively. Using C++11 from the very beginning (and no need to worry about backward compatibility) allowed me to use new approach to some common use cases, which might make some things more intuitive.

2) OpenGL interoperability. I wanted to have lightweight and well-defined types, which can be sent directly to OpenGL without potentially costly conversions (e.g. removing extra data in vector/matrix structures, converting matrices from row-major to column-major etc.). While I know that GLM can be mapped directly to OpenGL types, I don't know that about Eigen and even if it would work today, I cannot guarantee it for future versions.

3) Additional dependencies. I struggle to make core Magnum library as small and independent as possible and adding huge math library as main dependency will hurt the portability. I'm currently working on Emscripten port and having to spend extra time porting some external math library first would significantly slow things down. Also without the dependencies I have more control when something breaks up.

4) Not forcing users to use any particular library. Someone wants to use GLM, someone Eigen, someone doesn't care. I don't want to put restrictions on anything :-)

On the other hand, it is not impossible to conveniently use Eigen (or GLM) with Magnum. When some particular conversion structures are implemented (see below), the user can convert the data using just an explicit conversion, e.g.:

Magnum::Vector3 a;
Eigen::Vector3f b(a);
Magnum::Vector3 c(b);

The conversion can be implicit as well and then it would be absolutely transparent to the user. The user could then use Eigen exclusively and Magnum will still use its own math library in the internals, but as the core library it is just a tiny wrapper around OpenGL, no heavy computations are performed under the hood and the data are in most cases just passed to OpenGL API. Even better, Magnum's scene graph can be set to use these ultra-optimized Eigen math structures internally for object transformations.

See https://github.com/mosra/magnum-integration for an example, implemented is initial integration of Bullet Physics math library, the structures (e.g. btVector3) can be explicitly converted from and to Magnum's types, as shown above.

Sorry for the lengthy post, hope it will clarify some things :-)
» Reply 03.09.2013 18:25 | ayongwust_sjtu |
I totally agree with such concern. There no common standard for these common libs, causing coding in mess.
» Reply 04.09.2013 14:22 | Séverin Lemaignan |
Interesting to know you're working on getting the engine compile with emscripten. Any progress to share on that side?
The underlying utility library, Corrade, was ported there some time ago (see e.g. http://mosra.cz/...orrade.html#building-emscripten for more information), it is possible to even run the unit tests using Node.js. However the building toolchain currently seems to be broken somehow, I need to look into it.

I ported SDL2 support and the initial "Triangle" example to Emscripten (nothing publicly available yet), but didn't get much farther than that because of some weird floating-point issues. It's on my TODO list :-)
» Reply 18.09.2013 15:36 | 3d graphics | web |
You have made some good points here. I searched around the web to find out more about this matter and found most people will go along with these ideas of yours.