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.VertexColor2D.POSITION)
        self._mesh.add_vertex_buffer(buffer, 2*4, 5*4,
            shaders.VertexColor2D.COLOR3)

        self._shader = shaders.VertexColor2D()

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

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

exit(TriangleExample().exec())

Textured Triangle

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 TexturedTriangleShader(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 TexturedTriangleExample(Application):
    def __init__(self):
        configuration = self.Configuration()
        configuration.title = "Magnum Python Textured Triangle Example"
        Application.__init__(self, configuration)

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

        self._mesh = gl.Mesh()
        self._mesh.count = 3
        self._mesh.add_vertex_buffer(buffer, 0, 4*4,
            TexturedTriangleShader.POSITION)
        self._mesh.add_vertex_buffer(buffer, 2*4, 4*4,
            TexturedTriangleShader.TEXTURE_COORDINATES)

        importer = trade.ImporterManager().load_and_instantiate('TgaImporter')
        importer.open_file(os.path.join(os.path.dirname(__file__),
                                        '../textured-triangle/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.Flat2D(shaders.Flat2D.Flags.TEXTURED)
        self._shader = TexturedTriangleShader()

    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._mesh.draw(self._shader)

        self.swap_buffers()

exit(TexturedTriangleExample().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.Phong()

        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)]
        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._mesh.draw(self._shader)
        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.Phong, 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 = [
            camera.camera_matrix.transform_point((7.0, 5.0, 2.5))
        ]
        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._mesh.draw(self._shader)

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.Phong(),
            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.Phong, 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 = [
            camera.camera_matrix.transform_point((-3.0, 10.0, 10.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._mesh.draw(self._shader)

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.Phong()
        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.mesh3d(importer.mesh3d_for_name('Suzanne')))
        suzanne_eyes_mesh = meshtools.compile(
            importer.mesh3d(importer.mesh3d_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())