Skip to content

Developer documentation (WIP)

Laith Bahodi edited this page Nov 6, 2021 · 7 revisions

The goal of this page is to give developers interested in contributing to manim a deeper look into its inner workings and hopefully make it easier to understand the codebase. The target audience is therefore those interested in developing manim and may not be as useful to users. If you fall into the latter category and want to learn how to create animations with manim we have documentation for that here

Rendering logic

CLI

OpenGLRenderer and Shaders

The Opengl rendering pipeline in manim involves logic in both python code and programs called shaders. Shaders are programs designed to run on graphics cards. They are written in a language called glsl that is similar in syntax to c++. Manim uses moderngl, a python library that acts as a wrapper around opengl.

Shaders can be broken into three categories Vertex Shaders, Geometry Shaders and Fragment Shaders. Generally speaking each OpenGLMobject will be assigned to a vertex shader, a fragment shader and optionally a geometry shader. In some cases multiple of a given type of shader can also be used.

The order of processing is vertex shader -> geometry shader -> fragment shader.

Assigning Manim Shaders

In this section we will discuss how manim's existing shaders are assigned. There are also options to use custom shaders.

Manim's shaders are stored in manim/renderer/shaders. You will notice that shaders are stored in groups containing at least a vertex shader, a fragment shader and optionally a geometry shader. This makes up the opengl pipeline. In manim we only need to point it to the directory and it will detect the shaders based on the following naming convention:

  • Vertex shader -> vert.glsl
  • Geometry shader -> geom.glsl
  • Fragment shader -> frag.glsl

Manim knows what shader folder to look by class attributes. There are two base classes that are relevant here:

  • OpenGLMobject uses a single class attribute shader_folder to define the shader that should be used. By default no shader is defined, however subclasses that require a shader should set this. An example of a class that defines this attribute is OpenGLPMobject as shown below
class OpenGLPMobject(OpenGLMobject):
    shader_folder = "true_dot"
  • OpenGLVMobject uses two groups of shaders, one for stroke and one for fills and so two class attributes can be set to define the shaders that are to be used. These attributes are stroke_shader_folder that defaults to quadratic_bezier_stroke and fill_shader_folder that defaults to quadratic_bezier_fill. When extending this class these attributes can be set for subclasses to use different shaders for example the below class will extend OpenGLVMobject and set its own shaders
class MyCustomClass(OpenGLVMobject):
    stroke_shader_folder = "vectorized_mobject_stroke"
    fill_shader_folder = "vectorized_mobject_fill"

Rendering Flow

The entry point for the opengl rendering flow is in the render method in the OpenglRenderer. This is called for each time step from the Scene object. This will call update_frame and cycle through each OpenGLMObject in the scene, rendering each one in the render_mobject method. Objects may be assigned different shaders and so each object will have its own ShaderWrapper. This is a container that holds what it needs to render that given object such as the name of the folder containing the shader, the data to be passed to the shader etc. Most of the render_mobject involves preprocessing such as updating data before the opengl stage. After this preprocessing stage the Mesh object's render method is called. This method contains the main logic bridging manim and opengl. It takes the data that has been created with manim and passes it to moderngl with vertex buffers and vertex arrays.

Passing Data to Shaders

We need to pass data to the shaders to be processed and rendered on the graphics card. There are different types of data that can be used by shaders, the first type we will discuss is data that can vary for each vertex, in opengl these are known as attributes.

Attributes

If we take a simple triangle as an example, it can be defined by 3 vertices. We need a way to pass data such as color and position of each vertex. We do this using a flexible descriptor _Data that can be found here. This allows us to use keys such as 'points' to hold our position data and map them to the shader input 'point' attribute for a each vertex. Let's look at an example of how this is set up.

Taking OpenGLMobject as an example we initialise points as at the class level as below:

class OpenGLMobject:
    ...
    points = _Data()

This will create the key in our descriptor and we can now treat it like an instance attribute, and in this case this assign positions to each vertex as below.

self.points = points # numpy array containing xyz points with shape (n, 3)

Now that we have our attribute created and all our points are ready, however this array isn't passed directly to the vertex shader. The vertex shader may use different keys, for example it may have an input such as in vec3 point;. The reason for this is that the vertex shader will only take in a single vertex (point) at a time, so if we have three vertices for our triangle the vertex shader will only have access to one at a time. OpenGLMObject contains a method read_data_to_shader that will map from manim's data keys to the shaders keys, in other words map the 'points' key to the vertex shader's 'point' key.

After some processing the actual calls to a moderngl context happens in the Mesh object's render method.

vertex_buffer_object = self.shader.context.buffer(shader_attributes.tobytes())

This creates a buffer with the data that originated in the _Data descriptor that is passed to the shader.

Uniforms

The next type of data we can pass to shaders are uniforms. Unlike attributes, uniforms are not set per vertex, instead they are constant over a single render. Therefore the data in a uniform will be constant over a single draw call. This is not the same as an actual constant however, a real constant will be the same across all draw calls. Taking a triangle with three vertices again as an example, the uniform will not change as these vertices are rendered, however we can update the uniforms each time the whole triangle is rendered.

Uniforms are set in a similar way to attributes - by a descriptor called _Uniforms. An example of initialising a uniform can be seen below:

class OpenGLMobject:
    ...
    is_fixed_in_frame = _Uniforms()
    gloss = _Uniforms()
    shadow = _Uniforms()

They can they be set as normal python attributes self.gloss=0.0. As you can see uniforms follow a similar pattern to attributes. Where they diverge is how they are passed to the shaders. Unlike attributes they are set in the Mesh object's set_uniforms method. Each moderngl context has a program and uniform's are set using dict-like syntax self.shader_program[name] = value. Taking the gloss uniform as an example in the shader we will have:

uniform float gloss;

Manim's shader class would be assigning this by self.shader_program['gloss'] = 0.0

Using Custom Shaders

Vertex Shaders

Geometry Shaders

Fragment Shaders

Testing logic

Graphing in Manim

Clone this wiki locally