-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Developer documentation (WIP)
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
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.
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 attributeshader_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 isOpenGLPMobject
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 arestroke_shader_folder
that defaults toquadratic_bezier_stroke
andfill_shader_folder
that defaults toquadratic_bezier_fill
. When extending this class these attributes can be set for subclasses to use different shaders for example the below class will extendOpenGLVMobject
and set its own shaders
class MyCustomClass(OpenGLVMobject):
stroke_shader_folder = "vectorized_mobject_stroke"
fill_shader_folder = "vectorized_mobject_fill"
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.
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.
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.
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
When working with graphs in Manim, you will mainly be relying on these files: coordianate_systems.py
, number_line.py
and optionally, functions.py
/scale.py
.
CoordinateSystem
is the parent class of Axes
. It initalizes the following information: x_range
/y_range
/x_length
/y_length
. It's an abstract class and stores attributes/methods that are meant to be shared among all graphing-related classes. Although, all current classes inherit from Axes
instead, so there isn't a clear need for this class. Mostly exists for organization purposes
Axes
is the primary graphing class in Manim. Its job is to create the axes via _create_axis
(which creates NumberLine
and positions them appropiately). It's the class from which the methods defined in CoordinateSystem
are used. Axes
offers many useful methods, such as specifying points along the graph (coords_to_point
) and plotting functions (plot
). It's important to remember that the axes of an Axes
mobject are NumberLine
s. Therefore, you can use any methods defined in NumberLine
when dealing with them via Axes.x_axis.<method>
, for example.
NumberLine
is the true backbone of Manim's graphing infrastructure. It handles everything from creating the ticks, labels, lines and configuration for an axis. It supports different scales (logarithmic, linear) and its methods account for these. Almost everything it generates can be accessed after creation and modified. It inherits from Line
to create the actual NumberLine
.
Here is an outline of its key methods:
-
get_tick_range
: Generates the a list of the position of the ticks. So, with anx_range
of[1, 10, 2]
, this method would generate five evenly spaced ticks. These ticks values are then adjusted depending on the scaling viaNumberLine.scaling.function
, but remain evenly spaced due tonumber_to_point
accounting for this scaling. -
get_tick
. Generates aLine
mobject that acts as a tick. Called on byadd_ticks
(which then calls onget_tick_range
). -
get_number_mobject
: Accepts anx-value
and generates aDecimalNumber
mob for the number and positions it withnumber_to_point
. This is how the numbers are generated byadd_numbers
. -
add_numbers
: Calls onget_tick_range
and iterates through the range while calling onget_number_mobject
to generate the numbers. -
number_to_point
: The main tool for putting things on the NumberLine. It interpolates between the min/max values of the line and determines where a specific "x-value" belongs. Accounts for the scaling inget_tick_range
viaNumberLine.scaling.inverse_function
.
So, here's how a NumberLine
is made:
Make the line (length determined by x_range
/length
) -> get_tick_range
determines where the ticks/numbers will be -> add_ticks
/get_tick
define the ticks -> get_number_mobject
/add_numbers
add the numbers and number_to_point
determines where everything should be placed.
Scaling classes /add_labels
can generate custom label mobjects, but the general idea remains the same.
_ScaleBase
is an abstract base class which allows more configuration for NumberLine
/ParametricFunction
. For example, LogBase
allows a user to define a custom base and offers the get_custom_labels
method for generating labels for a NumberLine
in the form of base^exponent.
Each scaling class must define a function
(used in get_tick_range
/ParametricFunction
) and an inverse_function
, which is simply the inverse of function
and is used when plotting on a NumberLine
.
For instance, the function
for LogBase
is simply an exponential in the form of base^{value}
, whereas the inverse function is the logarithmic function with the same base. A custom rule for generating labels on the graph can optionally be defined for use with graphing. See the get_custom_labels
implementation in LogBase
for inspiration.
NOTE: function
/inverse_function
may not be valid everywhere in the domain, e.g. log(0) is undefined.
Apart from some minor stylistic changes, NumberPlane
is effectively equivalent to Axes
in terms of generating the axes. However, the background lines introduce more complexity to this class.
Here's the breakdown for the background lines: _init_background_lines
--> _get_lines
--> _get_lines_parallel_to_axis
.
-
_init_background_lines
: Applies the styling defined for the background lines and calls_get_lines
. -
_get_lines
: An intermediary method which passes in parameters into_get_lines_parallel_to_axis
. It calls the method twice, once to generate the horizontal lines, and once for the vertical lines. It puts these lines into two separateVGroups
and returns them to_init_background_lines
. -
_get_lines_parallel_to_axis
: Actually generates the lines. It iterates over thex_step
of the perpendicular axis:y_range[2]
for the horizontal lines andx_range[2]
for the vertical lines. There are some precautions taken when0
is not included in the range and depending on the scaling function of the axis. 🚧More description needed🚧
ThreeDAxes
is really just an Axes
that creates a third NumberLine
mobject and rotates it to position it along the z-axis. Its only unique method is get_z_axis_label
.
🚧Under construction🚧
A NumberPlane
which has support for complex numbers.
🚧Under construction🚧
🚧Under Construction🚧