Welcome to Python-flavored Magnum! Please note that, while already being rather stable, this functionality is still considered experimental and some APIs might get changed without preserving full backwards compatibility.

Examples

Examples for the Python bindings.

The magnum-examples repository contains a few examples in pure Python in the src/python/ directory. These currently mirror the C++ examples and show how to achieve the same in Python.

Your First Triangle

Basic rendering with builtin shaders. Fully equivalent to the C++ version.

import array

from magnum import *
from magnum import gl, shaders
from magnum.platform.sdl2 import Application

class TriangleExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Triangle Example"
        Application.__init__(self, configuration)

        buffer = gl.Buffer()
        buffer.set_data(array.array('f', [
            -0.5, -0.5, 1.0, 0.0, 0.0,
             0.5, -0.5, 0.0, 1.0, 0.0,
             0.0,  0.5, 0.0, 0.0, 1.0
        ]))

        self._mesh = gl.Mesh()
        self._mesh.count = 3
        self._mesh.add_vertex_buffer(buffer, 0, 5*4,
            shaders.VertexColorGL2D.POSITION)
        self._mesh.add_vertex_buffer(buffer, 2*4, 5*4,
            shaders.VertexColorGL2D.COLOR3)

        self._shader = shaders.VertexColorGL2D()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR)

        self._shader.draw(self._mesh)
        self.swap_buffers()

exit(TriangleExample().exec())

Textured Quad

Importing image data, texturing and custom shaders. Fully equivalent to the C++ version.

import os
import array

from magnum import *
from magnum import gl, shaders, trade
from magnum.platform.sdl2 import Application

class TexturedQuadShader(gl.AbstractShaderProgram):
    POSITION = gl.Attribute(
        gl.Attribute.Kind.GENERIC, 0,
        gl.Attribute.Components.TWO,
        gl.Attribute.DataType.FLOAT)
    TEXTURE_COORDINATES = gl.Attribute(
        gl.Attribute.Kind.GENERIC, 1,
        gl.Attribute.Components.TWO,
        gl.Attribute.DataType.FLOAT)

    _texture_unit = 0

    def __init__(self):
        super().__init__()

        vert = gl.Shader(gl.Version.GL330, gl.Shader.Type.VERTEX)
        vert.add_source("""
layout(location = 0) in vec4 position;
layout(location = 1) in vec2 textureCoordinates;

out vec2 interpolatedTextureCoordinates;

void main() {
    interpolatedTextureCoordinates = textureCoordinates;

    gl_Position = position;
}
""".lstrip())
        vert.compile()
        self.attach_shader(vert)

        frag = gl.Shader(gl.Version.GL330, gl.Shader.Type.FRAGMENT)
        frag.add_source("""
uniform vec3 color = vec3(1.0, 1.0, 1.0);
uniform sampler2D textureData;

in vec2 interpolatedTextureCoordinates;

out vec4 fragmentColor;

void main() {
    fragmentColor.rgb = color*texture(textureData, interpolatedTextureCoordinates).rgb;
    fragmentColor.a = 1.0;
}
""".lstrip())
        frag.compile()
        self.attach_shader(frag)

        self.link()

        self._color_uniform = self.uniform_location('color')
        self.set_uniform(self.uniform_location('textureData'), self._texture_unit)

    def color(self, color: Color3):
        self.set_uniform(self._color_uniform, color)
    color = property(None, color)

    def bind_texture(self, texture: gl.Texture2D):
        texture.bind(self._texture_unit)

class TexturedQuadExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Textured Quad Example"
        Application.__init__(self, configuration)

        vertices = gl.Buffer()
        vertices.set_data(array.array('f', [
             0.5, -0.5, 1.0, 0.0,
             0.5,  0.5, 1.0, 1.0,
            -0.5, -0.5, 0.0, 0.0,
            -0.5,  0.5, 0.0, 1.0
        ]))
        indices = gl.Buffer()
        indices.set_data(array.array('I', [
            0, 1, 2,
            2, 1, 3
        ]))

        self._mesh = gl.Mesh()
        self._mesh.count = 6
        self._mesh.add_vertex_buffer(vertices, 0, 4*4,
            TexturedQuadShader.POSITION)
        self._mesh.add_vertex_buffer(vertices, 2*4, 4*4,
            TexturedQuadShader.TEXTURE_COORDINATES)
        self._mesh.set_index_buffer(indices, 0, gl.MeshIndexType.UNSIGNED_INT)

        importer = trade.ImporterManager().load_and_instantiate('TgaImporter')
        importer.open_file(os.path.join(os.path.dirname(__file__),
                                        '../texturedquad/stone.tga'))
        image = importer.image2d(0)

        self._texture = gl.Texture2D()
        self._texture.wrapping = gl.SamplerWrapping.CLAMP_TO_EDGE
        self._texture.minification_filter = gl.SamplerFilter.LINEAR
        self._texture.magnification_filter = gl.SamplerFilter.LINEAR
        self._texture.set_storage(1, gl.TextureFormat.RGB8, image.size)
        self._texture.set_sub_image(0, Vector2i(), image)

        # or self._shader = shaders.FlatGL2D(shaders.FlatGL2D.Flags.TEXTURED)
        self._shader = TexturedQuadShader()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR)

        self._shader.color = (1.0, 0.7, 0.7)
        self._shader.bind_texture(self._texture)
        self._shader.draw(self._mesh)

        self.swap_buffers()

exit(TexturedQuadExample().exec())

Primitives

Importing mesh data, 3D transformations and input handling. Equivalent to the C++ version except that it uses meshtools.compile() instead of interleaving the data by hand — the low-level MeshTools APIs are not exposed yet.

from magnum import *
from magnum import gl, meshtools, primitives, shaders
from magnum.platform.sdl2 import Application

class PrimitivesExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Primitives Example"
        Application.__init__(self, configuration)

        gl.Renderer.enable(gl.Renderer.Feature.DEPTH_TEST)
        gl.Renderer.enable(gl.Renderer.Feature.FACE_CULLING)

        self._mesh = meshtools.compile(primitives.cube_solid())
        self._shader = shaders.PhongGL()

        self._transformation = (
            Matrix4.rotation_x(Deg(30.0))@
            Matrix4.rotation_y(Deg(40.0)))
        self._projection = (
            Matrix4.perspective_projection(
                fov=Deg(35.0), aspect_ratio=1.33333, near=0.01, far=100.0)@
            Matrix4.translation(Vector3.z_axis(-10.0)))
        self._color = Color3.from_hsv(Deg(35.0), 1.0, 1.0)
        self._previous_mouse_position = Vector2i()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR|
                                     gl.FramebufferClear.DEPTH)

        self._shader.light_positions = [(7.0, 5.0, 2.5, 0.0)]
        self._shader.light_colors = [Color3(1.0)]
        self._shader.diffuse_color = self._color
        self._shader.ambient_color = Color3.from_hsv(self._color.hue(), 1.0, 0.3)
        self._shader.transformation_matrix = self._transformation
        self._shader.normal_matrix = self._transformation.rotation_scaling()
        self._shader.projection_matrix = self._projection
        self._shader.draw(self._mesh)

        self.swap_buffers()

    def mouse_release_event(self, event: Application.MouseEvent):
        self._color = Color3.from_hsv(self._color.hue() + Deg(50.0), 1.0, 1.0)
        self.redraw()

    def mouse_move_event(self, event: Application.MouseMoveEvent):
        if event.buttons & self.MouseMoveEvent.Buttons.LEFT:
            delta = 1.0*(
                Vector2(event.position - self._previous_mouse_position)/
                Vector2(self.window_size))
            self._transformation = (
                Matrix4.rotation_x(Rad(delta.y))@
                self._transformation@
                Matrix4.rotation_y(Rad(delta.x)))
            self.redraw()

        self._previous_mouse_position = event.position

exit(PrimitivesExample().exec())

Primitives, using a scene graph

Same behavior as above, but this time handling transformations using the scene graph. Compared to doing the same in C++ there’s less worrying about data ownership, as the reference counting handles most of it.

from magnum import *
from magnum import gl, meshtools, primitives, scenegraph, shaders
from magnum.platform.sdl2 import Application
from magnum.scenegraph.matrix import Scene3D, Object3D

class CubeDrawable(scenegraph.Drawable3D):
    def __init__(self, object: Object3D, drawables: scenegraph.DrawableGroup3D,
                 mesh: gl.Mesh, shader: shaders.PhongGL, color: Color4):
        scenegraph.Drawable3D.__init__(self, object, drawables)

        self._mesh = mesh
        self._shader = shader
        self.color = color # Settable from outside

    def draw(self, transformation_matrix: Matrix4, camera: scenegraph.Camera3D):
        self._shader.light_positions = [
            Vector4(camera.camera_matrix.transform_point((-3.0, 5.0, 10.0)), 0.0)
        ]
        self._shader.light_colors = [Color3(1.0)]
        self._shader.diffuse_color = self.color
        self._shader.ambient_color = Color3.from_hsv(self.color.hue(), 1.0, 0.3)
        self._shader.transformation_matrix = transformation_matrix
        self._shader.normal_matrix = transformation_matrix.rotation_scaling()
        self._shader.projection_matrix = camera.projection_matrix
        self._shader.draw(self._mesh)

class PrimitivesSceneGraphExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Primitives + SceneGraph Example"
        Application.__init__(self, configuration)

        gl.Renderer.enable(gl.Renderer.Feature.DEPTH_TEST)
        gl.Renderer.enable(gl.Renderer.Feature.FACE_CULLING)

        # Scene and drawables
        self._scene = Scene3D()
        self._drawables = scenegraph.DrawableGroup3D()

        # Camera setup
        camera_object = Object3D(parent=self._scene)
        camera_object.translate(Vector3.z_axis(10.0))
        self._camera = scenegraph.Camera3D(camera_object)
        self._camera.projection_matrix = Matrix4.perspective_projection(
            fov=Deg(35.0), aspect_ratio=1.33333, near=0.01, far=100.0)

        # Cube object and drawable
        self._cube = Object3D(parent=self._scene)
        self._cube.rotate_y(Deg(40.0))
        self._cube.rotate_x(Deg(30.0))
        self._cube_drawable = CubeDrawable(self._cube, self._drawables,
            meshtools.compile(primitives.cube_solid()), shaders.PhongGL(),
            Color3.from_hsv(Deg(35.0), 1.0, 1.0))

        self._previous_mouse_position = Vector2i()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR|
                                     gl.FramebufferClear.DEPTH)

        self._camera.draw(self._drawables)
        self.swap_buffers()

    def mouse_release_event(self, event: Application.MouseEvent):
        self._cube_drawable.color = Color3.from_hsv(
            self._cube_drawable.color.hue() + Deg(50.0), 1.0, 1.0)
        self.redraw()

    def mouse_move_event(self, event: Application.MouseMoveEvent):
        if event.buttons & self.MouseMoveEvent.Buttons.LEFT:
            delta = 1.0*(
                Vector2(event.position - self._previous_mouse_position)/
                Vector2(self.window_size))
            self._cube.rotate_y_local(Rad(delta.x))
            self._cube.rotate_x(Rad(delta.y))
            self.redraw()

        self._previous_mouse_position = event.position

exit(PrimitivesSceneGraphExample().exec())

Model viewer

Scene graph, resource management and model importing. Goal is to be equivalent to the C++ version except that right now it imports the meshes directly by name as the full scene hierarchy import APIs from Trade::AbstractImporter are not exposed yet.

import os

from magnum import *
from magnum import gl, meshtools, scenegraph, shaders, trade
from magnum.platform.sdl2 import Application
from magnum.scenegraph.matrix import Scene3D, Object3D

class ColoredDrawable(scenegraph.Drawable3D):
    def __init__(self, object: Object3D, drawables: scenegraph.DrawableGroup3D,
                 mesh: gl.Mesh, shader: shaders.PhongGL, color: Color4):
        scenegraph.Drawable3D.__init__(self, object, drawables)

        self._mesh = mesh
        self._shader = shader
        self._color = color

    def draw(self, transformation_matrix: Matrix4, camera: scenegraph.Camera3D):
        self._shader.light_positions = [
            Vector4(camera.camera_matrix.transform_point((-3.0, 10.0, 10.0)), 0.0)
        ]
        self._shader.diffuse_color = self._color
        self._shader.transformation_matrix = transformation_matrix
        self._shader.normal_matrix = transformation_matrix.rotation_scaling()
        self._shader.projection_matrix = camera.projection_matrix
        self._shader.draw(self._mesh)

class ViewerExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Viewer Example"
        Application.__init__(self, configuration)

        gl.Renderer.enable(gl.Renderer.Feature.DEPTH_TEST)
        gl.Renderer.enable(gl.Renderer.Feature.FACE_CULLING)

        # Scene and drawables
        self._scene = Scene3D()
        self._drawables = scenegraph.DrawableGroup3D()

        # Every scene needs a camera
        camera_object = Object3D(parent=self._scene)
        camera_object.translate(Vector3.z_axis(5.0))
        self._camera = scenegraph.Camera3D(camera_object)
        self._camera.aspect_ratio_policy = scenegraph.AspectRatioPolicy.EXTEND
        self._camera.projection_matrix = Matrix4.perspective_projection(
            fov=Deg(35.0), aspect_ratio=1.0, near=0.01, far=100.0)
        self._camera.viewport = self.framebuffer_size

        # Base object, parent of all (for easy manipulation)
        self._manipulator = Object3D(parent=self._scene)

        # Setup renderer and shader defaults
        gl.Renderer.enable(gl.Renderer.Feature.DEPTH_TEST)
        gl.Renderer.enable(gl.Renderer.Feature.FACE_CULLING)
        colored_shader = shaders.PhongGL()
        colored_shader.ambient_color = Color3(0.06667)
        colored_shader.shininess = 80.0

        # Import Suzanne head and eyes (yes, sorry, it's all hardcoded here)
        importer = trade.ImporterManager().load_and_instantiate('TinyGltfImporter')
        importer.open_file(os.path.join(os.path.dirname(__file__),
                                        '../viewer/scene.glb'))
        suzanne_object = Object3D(parent=self._manipulator)
        suzanne_mesh = meshtools.compile(
            importer.mesh(importer.mesh_for_name('Suzanne')))
        suzanne_eyes_mesh = meshtools.compile(
            importer.mesh(importer.mesh_for_name('Eyes')))
        self._suzanne = ColoredDrawable(suzanne_object, self._drawables,
            suzanne_mesh, colored_shader, Color3(0.15, 0.49, 1.0))
        self._suzanne_eyes = ColoredDrawable(suzanne_object, self._drawables,
            suzanne_eyes_mesh, colored_shader, Color3(0.95))

        self._previous_mouse_position = Vector2i()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR|
                                     gl.FramebufferClear.DEPTH)

        self._camera.draw(self._drawables)
        self.swap_buffers()

    def mouse_move_event(self, event: Application.MouseMoveEvent):
        if event.buttons & self.MouseMoveEvent.Buttons.LEFT:
            delta = 1.0*(
                Vector2(event.position - self._previous_mouse_position)/
                Vector2(self.window_size))
            self._manipulator.rotate_y_local(Rad(delta.x))
            self._manipulator.rotate_x(Rad(delta.y))
            self.redraw()

        self._previous_mouse_position = event.position

    def mouse_scroll_event(self, event: Application.MouseScrollEvent):
        if not event.offset.y: return

        # Distance to origin
        distance = self._camera.object.transformation.translation.z

        # Move 15% of the distance back or forward
        self._camera.object.translate(Vector3.z_axis(
            distance*(1.0 - (1.0/0.85 if event.offset.y > 0 else 0.85))))

        self.redraw()

exit(ViewerExample().exec())

Text

Distance-field text rendering. Fully equivalent to the C++ version.

import os

from magnum import *
from magnum import gl, shaders, text
from magnum.platform.sdl2 import Application

class TextExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.window_flags |= self.Configuration.WindowFlag.RESIZABLE
        configuration.title = "Magnum Python Text Example"
        Application.__init__(self, configuration)

        # Load a TrueTypeFont plugin and open the font
        self._font = text.FontManager().load_and_instantiate('TrueTypeFont')
        self._font.open_file(os.path.join(os.path.dirname(__file__),
            '../text/SourceSansPro-Regular.ttf'), 180.0)

        # Glyphs we need to render everything
        self._cache = text.DistanceFieldGlyphCache(Vector2i(2048), Vector2i(512), 22)
        self._font.fill_glyph_cache(self._cache,
            "abcdefghijklmnopqrstuvwxyz"
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "0123456789:-+,.!°ěäПривітСΓειασουκόμ ")

        # Text that rotates using mouse wheel. Size relative to the window size
        # (1/10 of it) -- if you resize the window, it gets bigger
        self._rotating_text = text.Renderer2D(self._font, self._cache, 0.2, text.Alignment.MIDDLE_CENTER)
        self._rotating_text.reserve(128)
        self._rotating_text.render(
            "Hello, world!\n"
            "Ahoj, světe!\n"
            "Привіт Світ!\n"
            "Γεια σου κόσμε!\n"
            "Hej Världen!")

        # Dynamically updated text that shows rotation/zoom of the other. Size
        # in points that stays the same if you resize the window. Aligned so
        # top right of the bounding box is at mesh origin, and then transformed
        # so the origin is at the top right corner of the window.
        self._dynamic_text = text.Renderer2D(self._font, self._cache, 32.0, text.Alignment.TOP_RIGHT)
        self._dynamic_text.reserve(40)
        self._transformation_projection_dynamic_text =\
            Matrix3.projection(Vector2(self.window_size))@\
            Matrix3.translation(Vector2(self.window_size)*0.5)

        self._transformation_rotating_text = Matrix3.rotation(Deg(-10.0))
        self._projection_rotating_text = Matrix3.projection(
            Vector2.x_scale(Vector2(self.window_size).aspect_ratio()))

        self._shader = shaders.DistanceFieldVectorGL2D()

        gl.Renderer.enable(gl.Renderer.Feature.BLENDING)
        gl.Renderer.set_blend_function(gl.Renderer.BlendFunction.ONE, gl.Renderer.BlendFunction.ONE_MINUS_SOURCE_ALPHA)
        gl.Renderer.set_blend_equation(gl.Renderer.BlendEquation.ADD, gl.Renderer.BlendEquation.ADD)

        self.update_text()

    def draw_event(self):
        gl.default_framebuffer.clear(gl.FramebufferClear.COLOR|
                                     gl.FramebufferClear.DEPTH)

        self._shader.bind_vector_texture(self._cache.texture)

        self._shader.transformation_projection_matrix = \
            self._projection_rotating_text @ self._transformation_rotating_text
        self._shader.color = [0.184, 0.514, 0.8]
        self._shader.outline_color = [0.863, 0.863, 0.863]
        self._shader.outline_range = (0.45, 0.35)
        self._shader.smoothness = 0.025/self._transformation_rotating_text.uniform_scaling()
        self._shader.draw(self._rotating_text.mesh)

        self._shader.transformation_projection_matrix = \
            self._transformation_projection_dynamic_text
        self._shader.color = [1.0, 1.0, 1.0]
        self._shader.outline_range = (0.5, 1.0)
        self._shader.smoothness = 0.075
        self._shader.draw(self._dynamic_text.mesh)

        self.swap_buffers()

    def viewport_event(self, event: Application.ViewportEvent):
        gl.default_framebuffer.viewport = ((Vector2i(), event.framebuffer_size))

        self._projection_rotating_text = Matrix3.projection(
            Vector2.x_scale(Vector2(self.window_size).aspect_ratio()))
        self._transformation_projection_dynamic_text =\
            Matrix3.projection(Vector2(self.window_size))@\
            Matrix3.translation(Vector2(self.window_size)*0.5)

    def mouse_scroll_event(self, event: Application.MouseScrollEvent):
        if not event.offset.y: return

        if event.offset.y > 0:
            self._transformation_rotating_text =\
                Matrix3.rotation(Deg(1.0)) @\
                Matrix3.scaling(Vector2(1.1)) @\
                self._transformation_rotating_text
        else:
            self._transformation_rotating_text =\
                Matrix3.rotation(Deg(-1.0)) @\
                Matrix3.scaling(Vector2(1.0/1.1)) @\
                self._transformation_rotating_text

        self.update_text()

        event.accepted = True
        self.redraw()

    def update_text(self):
        # TODO show rotation once Complex.from_matrix() is a thing
        self._dynamic_text.render("Scale: {:.2}".format(self._transformation_rotating_text.uniform_scaling()))

exit(TextExample().exec())