This guide covers the internals of 3b1b/manim (ManimGL): the rendering
pipeline, the OpenGL/shader stack, configuration layering, the package
layout on disk, the interactive shell internals, and what every module
contributes. It is intended for developers who want to extend ManimGL,
operators who want to run it at scale (classrooms, CI, video farms), and
advanced users who need to understand failure modes that the User Guide
doesn't reach.
Important
This is the original Grant Sanderson library, not ManimCE (
manim-community/manim). The two share an ancestor but have diverged on module layout, class names, default configuration, the renderer implementation, and the CLI binary. Everything in this guide describes ManimGL only.
1. Architecture Overview
ManimGL is a Python animation framework with a hard dependency on OpenGL
3.3+ via moderngl. The high-level execution flow:
+-------------------------+
| manimgl <file> <Scene> |
+------------+------------+
|
v
+-------------------------+
| manimlib.__main__ |
| parse_cli, run loop |
+------------+------------+
|
v
+-------------------------+ +-----------------+
| manim_config (Dict) |<-----+ default_config |
| (yaml + cli + custom) | | custom_config |
+------------+------------+ +-----------------+
|
v
+-------------------------+
| ModuleLoader.get | import the user's .py file
| _module(file) | (with optional reload tracking)
+------------+------------+
|
v
+-------------------------+
| extract_scene.main | discover Scene classes
| -> get_scenes_to_run | instantiate them
+------------+------------+
|
v
+-------------------------+
| Scene.run() loop |
| setup -> construct |
| -> interact -> tear |
+-----+--------------+----+
| |
v v
+------------------+ +-------------------+
| Camera | | SceneFileWriter |
| OpenGL context, | | ffmpeg subprocess|
| FBOs, light src | | partial movies, |
+--------+---------+ | audio mux |
| +-------------------+
v
+------------------+
| ShaderWrapper |
| / VShaderWrapper |
| per-mob program |
+--------+---------+
|
v
+------------------+
| moderngl |
| OpenGL 3.3 |
+------------------+
A scene is a Python class that, on .run(), calls setup(), then
construct(), then enters an idle event loop (interact()) if a window
is attached. Every self.play(...) invocation feeds a list of Animation
objects through pre_play -> begin_animations -> progress_through_animations -> finish_animations -> post_play. Inside the inner loop, the scene
advances time by 1 / fps seconds per iteration, calls each animation's
interpolate(alpha), calls every mobject's updaters, and asks the camera
to capture a frame. If write_to_movie=True, that frame is piped to FFmpeg
via stdin.
Note
ManimGL has no message bus and no parallelism inside a scene. A scene runs single-threaded. The only out-of-process work is the FFmpeg encoder subprocess (
SceneFileWriter.writing_process) and the LaTeX compiler subprocess invoked byTex/TexTextmobjects.
1.1 Component Inventory
| Component | Path | Role |
|---|---|---|
| Entry point | manimlib/__main__.py |
Parses CLI, builds config, runs the scene loop. |
| Config | manimlib/config.py, manimlib/default_config.yml |
Argparse spec, YAML merge, global manim_config. |
| Constants | manimlib/constants.py |
Frame size, vectors, colors, angles. |
| Logger | manimlib/logger.py |
rich-backed logger named "manimgl". |
| Scene discovery | manimlib/extract_scene.py |
Imports the user file, finds Scene subclasses, prompts if needed. |
| Module loader | manimlib/module_loader.py |
Import-with-tracking for hot reload. |
| Window | manimlib/window.py |
Pyglet OpenGL window, event delegation to Scene. |
| Camera | manimlib/camera/camera.py, camera_frame.py |
OpenGL context, FBOs, light source, CameraFrame mobject. |
| Shaders | manimlib/shader_wrapper.py, manimlib/shaders/*.glsl |
ShaderWrapper / VShaderWrapper, GLSL programs. |
| Mobject base | manimlib/mobject/mobject.py |
Mobject, Group, Point, animation/updater builders. |
| Mobjects | manimlib/mobject/**/*.py |
All shape, text, 3D, matrix, vector-field classes. |
| Animations | manimlib/animation/**/*.py |
Animation base + every concrete animation class. |
| Scene | manimlib/scene/scene.py |
Scene, SceneState, EndScene, ThreeDScene. |
| Interactive scene | manimlib/scene/interactive_scene.py |
Drag-select, palette, undo/redo. |
| Embedded shell | manimlib/scene/scene_embed.py |
InteractiveSceneEmbed, CheckpointManager (IPython integration). |
| File writer | manimlib/scene/scene_file_writer.py |
FFmpeg pipeline, partial movies, audio. |
| Event handler | manimlib/event_handler/ |
Singleton EVENT_DISPATCHER, EventListener, EventType enum. |
| Utilities | manimlib/utils/*.py |
Bezier, color, file ops, rate functions, space ops, TeX writing, sounds. |
| Type aliases | manimlib/typing.py |
ManimColor, Vect3, Vect3Array, etc. |
| Examples | example_scenes.py, logo/logo.py |
Runnable demonstrations. |
1.2 Technology Stack
| Layer | Implementation |
|---|---|
| Language | Python 3.8+ (modern type hints, from __future__ import annotations ubiquitous) |
| Window/event loop | moderngl_window / pyglet |
| OpenGL bindings | moderngl 5.x + PyOpenGL |
| Shaders | GLSL 3.30 core profile (in manimlib/shaders/*.glsl) |
| Vector math | numpy 1.x; scipy.linalg for solvers |
| Geometry | mapbox-earcut (triangulation), skia-pathops (booleans), isosurfaces (implicit functions), trimesh, pywavefront, fontTools.cu2qu |
| LaTeX | External latex / xelatex binaries; output piped through dvisvgm to SVG; parsed by svgelements |
| Text shaping | manimpango>=0.6.0 (Pango / HarfBuzz wrapper) |
| Color | colour package |
| Audio | pydub (loaded files); piped to FFmpeg for mux |
| Config | pyyaml, wrapped in addict.Dict for attribute access |
| Cache | diskcache, plus a custom SVG cache for compiled Tex / Text |
| CLI | Stdlib argparse |
| Logging | Stdlib logging + rich.logging.RichHandler |
| Interactive shell | IPython.terminal.embed |
| Subprocess | subprocess.Popen for FFmpeg |
| Clipboard | pyperclip for InteractiveScene copy/paste |
| Multi-monitor | screeninfo |
1.3 Data Flow for the Render Pipeline
For each self.play(*animations):
pre_play— Update skipping status (jump-to-N, etc.), tell theSceneFileWritertobegin_animation()(creates a partial movie file ifsubdivide_outputis on, opens a new FFmpeg pipe, and writes leading audio padding).begin_animations— Callanimation.begin()on each. EachAnimationsaves astarting_mobject = self.mobject.copy(), marks the mobject as animating, and zips together the families of source and target mobjects so per-submobject interpolation stays aligned.progress_through_animations— Iteratedt-spaced timestamps from0torun_time. For eacht:- Each animation's
update_mobjects(dt)runs updaters on auxiliary copies (e.g.target_mobjectforTransform). - Each animation's
interpolate(alpha)runs (wherealpha = t / run_time). - The scene advances time by
dt, runs every mobject's updaters, and callscamera.capture(*self.render_groups)to draw. - If a window is attached, the renderer sleeps to keep wall-clock synchronized to virtual time.
- If
write_to_movie, the captured frame is piped to FFmpeg.
- Each animation's
finish_animations— Each animation'sfinish()runs (setsalpha=final_alpha_value, default1.0), thenclean_up_from_scene(removes the mobject ifis_remover()).post_play— Incrementnum_plays, end the partial movie file, and (in preview-while-skipping mode) draw one quick frame.
For self.wait(duration, stop_condition=None):
- Same envelope (
pre_play, then a time loop, thenpost_play), but the inner loop only updates mobjects/frames; no animation interpolation. - If
presenter_modeis on and not skipping, the loop blocks onhold_loop()until the user presses Space or right-arrow.
Note
Inside an animation, frame timestamps are virtual time, not wall-clock. Wall-clock sleeping happens only when a window is attached to keep the preview real-time. When writing to file, ManimGL renders as fast as it can.
2. System Requirements
| Resource | Recommended |
|---|---|
| CPU | Modern x86_64 / ARM64. Single-threaded, so clock speed matters more than core count. |
| RAM | 4 GB minimum. Heavy 3D + LaTeX scenes can push 8 GB. |
| GPU | OpenGL 3.3+ core profile. Integrated GPUs work. Low VRAM (~256 MB) is fine for 1080p. |
| Disk | ~200 MB for the package + dependencies; rendered videos run anywhere from 5 MB/min (low quality) to 200 MB/min (4K, low-compression codecs). |
| Python | 3.8 or newer. Tested on CPython; PyPy is not supported. |
| External binaries | ffmpeg (mandatory for video output), latex + dvisvgm (only for Tex/TexText). |
Caution
ManimGL allocates a multi-megapixel float framebuffer per fill draw (see
VShaderWrapper.get_fill_canvas— it builds a texture at(2 * resolution[0], 2 * resolution[1])for fill compositing). At 4K withsamples=4, this can exceed 1 GB of VRAM transiently. Reducecamera.resolutionorsamplesif you hit GL_OUT_OF_MEMORY.
3. Configuration Reference
3.1 Configuration Files
| File | Format | Location | Purpose |
|---|---|---|---|
default_config.yml |
YAML | manimlib/default_config.yml (in package) |
Hard-coded defaults shipped with ManimGL. |
custom_config.yml |
YAML | Current working directory at runtime | Per-project overrides. |
<custom>.yml |
YAML | Path passed to --config_file |
Ad-hoc overrides. |
tex_templates.yml |
YAML | manimlib/tex_templates.yml |
LaTeX preamble bundles indexed by name. |
config.initialize_manim_config() merges the three YAML sources via
merge_dicts_recursively (later sources override earlier ones), then
mutates the resulting addict.Dict based on parsed CLI args via
update_directory_config, update_window_config, update_camera_config,
update_file_writer_config, update_scene_config, update_run_config,
and update_embed_config. The final dict is exported as the module-level
manim_config.
3.2 Top-Level Schema
directories:
mirror_module_path: false
base: ""
subdirs:
output: "videos"
raster_images: "raster_images"
vector_images: "vector_images"
three_d_models: "three_d_models"
sounds: "sounds"
data: "data"
downloads: "downloads"
latex_cache: "latex_cache"
cache: ""
window:
position_string: UR
monitor_index: 0
full_screen: false
# position: (500, 500) # optional, pixel coords for upper-right corner
# size: (1920, 1080) # optional, exact pixel size
camera:
resolution: (1920, 1080)
background_color: "#333333"
fps: 30
background_opacity: 1.0
file_writer:
ffmpeg_bin: "ffmpeg"
video_codec: "libx264"
pixel_format: "yuv420p"
saturation: 1.0
gamma: 1.0
scene:
show_animation_progress: false
leave_progress_bars: false
preview_while_skipping: true
default_wait_time: 1.0
vmobject:
default_stroke_width: 4.0
default_stroke_color: "#DDDDDD" # GREY_A
default_fill_color: "#888888" # GREY_C
mobject:
default_mobject_color: "#FFFFFF" # WHITE
default_light_color: "#BBBBBB" # GREY_B
tex:
template: "default"
font_size_for_unit_height: 144
text:
font: "Consolas"
alignment: "LEFT"
font_size_for_unit_height: 144
embed:
exception_mode: "Verbose"
autoreload: false
resolution_options:
low: (854, 480)
med: (1280, 720)
high: (1920, 1080)
4k: (3840, 2160)
sizes:
frame_height: 8.0
small_buff: 0.1
med_small_buff: 0.25
med_large_buff: 0.5
large_buff: 1.0
default_mobject_to_edge_buff: 0.5
default_mobject_to_mobject_buff: 0.25
key_bindings:
pan_3d: "d"
pan: "f"
reset: "r"
quit: "q" # Together with command/ctrl
select: "s"
unselect: "u"
grab: "g"
x_grab: "h"
y_grab: "v"
z_grab: "z"
resize: "t"
color: "c"
information: "i"
cursor: "k"
colors: { ...full named-color palette... }
log_level: "INFO"
universal_import_line: "from manimlib import *"
ignore_manimlib_modules_on_reload: true
Important
Tuple-typed values (
resolution,position,size,resolution_options.*) are written as YAML strings of the form(W, H)and parsed byast.literal_evalinupdate_window_config/update_camera_config/get_resolution_from_args. A bare YAML list ([1920, 1080]) will not work for these keys.
3.3 directories
| Key | Type | Default | Description |
|---|---|---|---|
mirror_module_path |
bool | False |
When True, output paths mirror the source .py file's path (with _ prefix in path components stripped). |
base |
str | "" |
Project root prepended to every subdir. Empty string means current working directory. |
subdirs.output |
str | "videos" |
Where rendered videos and images are written. |
subdirs.raster_images |
str | "raster_images" |
Directory Mobjects search when given a raster image filename. |
subdirs.vector_images |
str | "vector_images" |
SVG asset search path. |
subdirs.three_d_models |
str | "three_d_models" |
OBJ / GLTF model search path. |
subdirs.sounds |
str | "sounds" |
Audio file search path for Scene.add_sound. |
subdirs.data |
str | "data" |
Generic project data (CSVs, NumPy arrays). |
subdirs.downloads |
str | "downloads" |
Where remote URL-fetched assets land. |
subdirs.latex_cache |
str | "latex_cache" |
Compiled LaTeX SVG cache; relative to base. |
cache |
str | "" |
Override path for Tex / Text SVG cache. Empty string means use appdirs.user_cache_dir("manim"). |
update_directory_config resolves each subdir to os.path.join(base, sub)
in place, so after init the same dict has keys directories.output,
directories.raster_images, etc., as absolute paths.
3.4 window
| Key | Type | Default | Description |
|---|---|---|---|
position_string |
str | "UR" |
Anchor on screen: two characters, where each is U/O/D (vertical) and L/O/R (horizontal). "UR" is upper-right. |
monitor_index |
int | 0 |
Which monitor (0-based) to open the window on. |
full_screen |
bool | False |
Forced full-screen on the chosen monitor. |
position |
str | unset | Pixel (x, y) of upper-left corner; overrides position_string. |
size |
str | unset | Pixel (w, h); default is half of monitor width. |
3.5 camera
| Key | Type | Default | Description |
|---|---|---|---|
resolution |
str (tuple) | "(1920, 1080)" |
Default render resolution. CLI overrides via -l/-m/--hd/--uhd/-r. |
background_color |
str | "#333333" |
Hex or named color used by Camera.clear. |
fps |
int | 30 |
Render frame rate. CLI overrides via --fps. |
background_opacity |
float | 1.0 |
0.0 = transparent. CLI -t sets this to 0.0 and switches codec to prores_ks. |
Camera.__init__ also accepts samples (multisample anti-aliasing
levels), which Scene controls via the class-level attribute
Scene.samples (default 0). Set this to 4 on a ThreeDScene if you
see jaggy edges on 3D surfaces.
3.6 file_writer
| Key | Type | Default | Description |
|---|---|---|---|
ffmpeg_bin |
str | "ffmpeg" |
Binary name or absolute path. |
video_codec |
str | "libx264" |
FFmpeg codec name. CLI --vcodec overrides. |
pixel_format |
str | "yuv420p" |
FFmpeg pixel format. CLI --pix_fmt overrides. CLI -t clears it. |
saturation |
float | 1.0 |
Post-render saturation adjustment via FFmpeg's eq filter. |
gamma |
float | 1.0 |
Post-render gamma. |
Important
Setting
--vcodec prores_ksplus-t(transparent) emits a.movwith alpha. ManimGL automatically applies this combination when only-tis passed. Overridepixel_formatonly if you know what you're doing — yuv420p is required by most consumer players.
3.7 scene
| Key | Type | Default | Description |
|---|---|---|---|
show_animation_progress |
bool | False |
Show a tqdm progress bar per animation. |
leave_progress_bars |
bool | False |
Keep tqdm bars on screen after they finish. |
preview_while_skipping |
bool | True |
When skipping animations (e.g. with -s), still draw one frame at the end of each play so the window stays alive. |
default_wait_time |
float | 1.0 |
Default duration of Scene.wait() when called with no argument. |
Several CLI flags also write to this section indirectly:
-s/--skip_animations->scene.skip_animations = True.-n N[,M]->scene.start_at_animation_number = N,end_at_animation_number = M.-p/--presenter_mode->scene.presenter_mode = True.--leave_progress_bars->scene.leave_progress_bars = True.--show_animation_progress->scene.show_animation_progress = True.
3.8 vmobject and mobject
These two stanzas seed module-level constants in manimlib/constants.py:
vmobject.default_stroke_width->DEFAULT_STROKE_WIDTH(4.0⇒ ~4 px at 1080p).vmobject.default_stroke_color/default_fill_color->DEFAULT_VMOBJECT_STROKE_COLOR/DEFAULT_VMOBJECT_FILL_COLOR.mobject.default_mobject_color/default_light_color->DEFAULT_MOBJECT_COLOR/DEFAULT_LIGHT_COLOR.
Override these on a per-mobject basis by passing stroke_color=...,
fill_color=... to constructors.
3.9 tex and text
tex.template selects from manimlib/tex_templates.yml. The shipped
templates include:
- Classical:
default(LaTeX),ctex(xeLaTeX with Chinese support),basic,basic_ctex,empty,empty_ctex. - Font-specific (xelatex / lualatex / latex with mathastext):
american_typewriter,antykwa,apple_chancery,auriocus_kalligraphicus,baskervald_adf_fourier,baskerville_it,biolinum,brushscriptx,chalkboard_se,chalkduster,comfortaa,comic_sans,droid_sans,droid_sans_it,droid_serif,droid_serif_px_it, plusecf_*,electrum_adf,epigrafica,fourier_utopia,french_cursive,gfs_bodoni,gfs_didot,gfs_neohellenic,gnu_freesans_tx,gnu_freeserif_freesans,helvetica_fourier_it,latin_modern_tw, and many more — over 90 templates total. Browsemanimlib/tex_templates.ymlfor the full list.
Each template has fields:
<name>:
description: "..."
compiler: latex | xelatex | lualatex
preamble: |-
\usepackage{...}
text.font is passed to Pango via manimpango; the system font lookup
applies. text.alignment is one of LEFT / CENTER / RIGHT.
3.10 embed
| Key | Type | Default | Description |
|---|---|---|---|
exception_mode |
str | "Verbose" |
IPython's xmode setting. Plain/Verbose/Context are the standard values. |
autoreload |
bool | False |
Re-import the user module on every IPython cell. CLI --autoreload sets this. |
3.11 key_bindings
Single-character key bindings consumed by Scene and InteractiveScene
event handlers. The defaults (d for 3D pan, f for 2D pan, r to
reset, g/h/v/z for grab and axis-lock, etc.) are read directly
from this dict at scene init — change them in custom_config.yml to
remap.
3.12 colors
64 named colors available as Python constants after from manimlib import *. The standard 11-hue palette is BLUE, TEAL, GREEN, YELLOW,
GOLD, RED, MAROON, PURPLE, GREY, plus WHITE, BLACK. Each
hue has 5 shades suffixed _A (lightest) to _E (darkest). Bare
BLUE, TEAL, etc. alias to _C (median). Specials: GREY_BROWN,
DARK_BROWN, LIGHT_BROWN, PINK, LIGHT_PINK, GREEN_SCREEN,
ORANGE, PURE_RED, PURE_GREEN, PURE_BLUE. The full palette is
exposed as MANIM_COLORS. The COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
is the channel's signature gradient.
3.13 Top-Level Settings
| Key | Type | Default | Description |
|---|---|---|---|
log_level |
str | "INFO" |
One of DEBUG/INFO/WARNING/ERROR/CRITICAL. CLI --log-level overrides. |
universal_import_line |
str | "from manimlib import *" |
Code executed by BlankScene.construct() and on manimgl with no file. |
ignore_manimlib_modules_on_reload |
bool | True |
Skip ManimGL itself when reload_scene() deep-reloads user modules. Avoids slow re-imports. |
4. Deployment and Scaling
ManimGL is a desktop / single-machine renderer. There is no built-in client-server mode, no GPU offload, no distributed task queue. To run it at scale, treat each scene as a CLI invocation and orchestrate externally.
4.1 Headless Rendering
Camera.init_context calls moderngl.create_standalone_context() when no
window is attached, so ManimGL renders without an X server. On Linux
headless boxes you typically still need:
- A working OpenGL driver (Mesa via
libgl1-mesa-glx+libegl1). - Either
xvfb-runto provide a virtual display or EGL — moderngl's standalone context picks the right backend per platform. - FFmpeg, LaTeX (
texlive-full), and a font set installed.
A typical Dockerfile for batch rendering:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg texlive-full dvisvgm \
libegl1 libgl1 libgles2 \
libpango-1.0-0 libpangocairo-1.0-0 \
fonts-dejavu fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir manimgl
WORKDIR /work
ENTRYPOINT ["manimgl"]
Then:
docker run --rm -v "$PWD":/work my-manimgl scene.py MyScene -w
4.2 CI Pipelines
Render on push or schedule:
- name: Render scenes
run: |
pip install manimgl
for scene in MyScene OtherScene; do
manimgl scenes.py "$scene" -w --hd --fps 30
done
- uses: actions/upload-artifact@v4
with:
name: videos
path: videos/
Tip
CI runners rarely have hardware acceleration. EGL software rasterization still works but is much slower than a real GPU. Budget 5x-10x the wall-clock you see locally.
4.3 Render Farms
A simple parallel pattern: one scene per worker. Each worker invocation
is independent (the global manim_config is rebuilt from scratch on each
process start). Use the same custom_config.yml across workers and a
shared output volume.
# GNU parallel
parallel -j 8 manimgl scenes.py {} -w --hd ::: SceneA SceneB SceneC ...
4.4 Embedding in a Larger Application
ManimGL is a Python library — you can drive it from any Python program without going through the CLI:
from manimlib import *
from manimlib.config import manim_config
# Mutate config in place
manim_config.camera.fps = 60
manim_config.file_writer.write_to_movie = True
manim_config.file_writer.output_directory = "/tmp/out"
# Build a scene
class Programmatic(Scene):
def construct(self):
self.play(ShowCreation(Circle()))
# Run it
scene = Programmatic()
scene.run()
This bypasses parse_cli entirely — set every config key yourself before
instantiating the scene.
5. Operations
5.1 Output Layout
After a successful render, expect:
{base}/
+- videos/
| +- {scene_name}.mp4
| +- {scene_name}.png (when -s -w used)
| +- {scene_name}/ (when --subdivide)
| +- 00001_{anim_name}.mp4
| +- 00002_{anim_name}.mp4
| +- ...
+- latex_cache/
+- raster_images/ (asset search path)
+- vector_images/
+- sounds/
+- ...
Plus the system cache at appdirs.user_cache_dir("manim") (or
directories.cache) holding compiled LaTeX SVGs.
5.2 Logging
Logs go to stderr via rich.logging.RichHandler (defined in
manimlib/logger.py). The logger name is "manimgl". Set
log_level: "DEBUG" in custom_config.yml to see module-loader and
shader-compilation messages.
5.3 Cache Maintenance
The Tex/Text SVG cache lives at directories.cache (or
appdirs.user_cache_dir("manim") if empty). On a long-running production
machine, the cache can grow into the GB range over months. Two options:
- Wipe interactively:
manimgl --clear-cache(callsmanimlib.utils.cache.clear_cache). - Wipe scripted: delete the directory directly. ManimGL recreates it on next run.
5.4 Pre-flight Checklist
Before a long batch render:
- [ ]
manimgl --versionruns and prints a green banner. - [ ]
ffmpeg -versionruns (and the codec you need is listed underffmpeg -codecs). - [ ]
latex --versionruns (only if any scene usesTex/TexText). - [ ]
custom_config.ymlresolved:python -c "from manimlib.config import manim_config; print(manim_config.camera, manim_config.directories)"shows the merged values. - [ ] Disk has free space for expected output (1080p @ 30fps libx264 averages ~5-15 MB/min; 4K can be 10x that).
- [ ] No two parallel workers share the same
custom_config.ymldirectory unless they write to disjoint output subdirs. - [ ] LaTeX cache pre-warmed: render a small representative scene first to populate it.
5.5 Monitoring a Running Render
Each Scene.play(...) call is a single FFmpeg pipe. While it runs:
- Use
--show_animation_progressto get per-animation tqdm bars. - Use
--prerunto compute total framecount up front and get a global progress bar from the first frame. - The OpenGL window (when present) shows the current frame in real-time.
Note
ManimGL does not expose a programmatic progress hook. To integrate with an external job tracker (e.g. Celery beat, GitHub Actions step summaries), parse stdout for tqdm output or wrap the
Scenewith a custom subclass that overridesplayto update a side channel.
6. Performance
6.1 Where Time Goes
| Stage | Typical share of frame time |
|---|---|
| Animation interpolation (Python) | 10-30% |
| Updater callbacks | 5-25% (depends on user code) |
| Mobject -> shader uniform packing | 5-15% |
| OpenGL draw calls | 30-60% (dominates 3D scenes) |
| FBO blit + read for FFmpeg | 5-15% (only when writing to file) |
| FFmpeg encode | runs concurrently with the next frame; rarely a bottleneck for libx264 at 1080p30 |
6.2 Known Hot Spots
always_redraw(Constructor, *args)rebuilds a fresh mobject every frame. For deep mobjects (Tex with hundreds of glyphs), this is expensive. Prefermobject.add_updater(...)that mutates an existing mobject in place.TransformMatchingStrings/TransformMatchingShapesdo per-submobject pairing and longest-substring matching atbegin(). Cost is O(n*m) in submobject counts; cap source/target string length.VShaderWrapper.get_fill_canvasallocates a 2x-resolution floating-point texture for fill compositing. Consider loweringcamera.resolutionfor previews.Texfirst-render triggers a LaTeX compile + dvisvgm + parse + glyph triangulation. After the first render, results are cached.Scene.assemble_render_groupsruns after everyadd/removeto batch mobjects with the same shader. Adding hundreds of mobjects one at a time is slower thanself.add(*mobjects)once.
6.3 Tuning Levers
| Lever | Where | Effect |
|---|---|---|
-l / -m / --hd / --uhd |
CLI | Resolution preset. Lower = faster. |
camera.fps / --fps |
YAML / CLI | 30 -> 60 doubles work. |
Scene.samples |
Subclass attribute | MSAA level for 3D. 0 = off. |
--prerun |
CLI | Adds one full pre-pass; use only when you need a global progress bar. |
--subdivide |
CLI | One movie file per animation. Slightly slower (more file opens) but resilient — a crash mid-scene loses only one animation. |
| Resolution of 3D surfaces | Mobject kwarg | Sphere(resolution=(50, 100)) controls subdivisions. |
Tex(...) cache |
OS cache dir | First render is slow; subsequent are instant. |
6.4 Profiling
ManimGL has no built-in profiler. Wrap a scene in cProfile:
import cProfile, pstats
from manimlib import *
class Bench(Scene):
def construct(self):
...
scene = Bench()
prof = cProfile.Profile()
prof.runcall(scene.run)
pstats.Stats(prof).sort_stats("cumulative").print_stats(40)
Hot frames usually show Mobject.update, Animation.interpolate_submobject,
VShaderWrapper.render_fill, and FFmpeg pipe writes near the top.
7. Security
ManimGL is a developer tool intended to run trusted Python source. It
imports user files via importlib.util.spec_from_file_location and
executes them. Treat any .py you pass to manimgl as code you would
run via python <file> — there is no sandbox.
7.1 Threat Model
| Vector | Status |
|---|---|
| Untrusted scene file | Not safe. A scene file can do anything any Python program can do. Don't render .py files from strangers. |
| Untrusted YAML config | Mostly safe. ManimGL uses yaml.safe_load, so !python/object injection is prevented. However, several values ((W, H) tuples) go through ast.literal_eval — that allows literal int/float/string/tuple/list/dict only, not calls. |
| Untrusted assets (images, sounds, SVGs) | Probably safe. Image and sound paths are loaded via Pillow / pydub. Both have had security advisories — keep them updated. |
| Untrusted LaTeX strings | Variable risk. LaTeX itself can read/write files via \write18 if --shell-escape is enabled. ManimGL does not pass -shell-escape to the LaTeX binary (see manimlib/utils/tex_file_writing.py), so the standard exploit chain is broken, but a malicious package included in the preamble could still misbehave. |
| Network access | Possible when a Mobject (e.g. TexturedSurface) is given a URL. Files are downloaded to directories.downloads. There is currently no allowlist or signature check. |
| Sound playback via shell | Historic concern. Issue #2434 (closed) reported a command-injection vector via PowerShell sound playback on Windows and SSRF via unrestricted URL fetching. Verify your version postdates the fix. |
Warning
Issue #2434 surfaced a command-injection in Windows sound playback and an SSRF in URL-asset fetching. The closed-status of the issue suggests a fix landed, but if you operate ManimGL on a multi-tenant / shared system, audit
manimlib/utils/sounds.py:play_soundand the URL-fetching code path before exposing it to externally-supplied scene files.
7.2 Hardening for Multi-Tenant Use
If you must run user-supplied scenes (e.g. a "render-as-a-service" product), put each render inside an isolated container or sandbox:
- Run ManimGL as a non-root user with no write access outside a per-job temp dir.
- Drop network access (
--network=none) unless you specifically allow remote-asset URLs. - Enforce a wall-clock and memory limit on the FFmpeg + Python processes.
- Use
seccomp/apparmorprofiles to block/etc/passwd-style reads if you treat the source as untrusted.
7.3 Cache Poisoning
The Tex/Text cache hashes the source string and template into a content key. A rogue scene file that produces the same hash as a previously rendered cache entry is an unlikely (~2^256-bit) collision concern, not a realistic attack. There is no hostname, user, or signing data in the cache key beyond content.
8. Developer Internals
8.1 Repository Layout
manim/ (3b1b/manim repo root)
+- example_scenes.py All canonical demos in one file
+- requirements.txt Runtime Python deps
+- pyproject.toml Build metadata (setuptools)
+- setup.py Trivial setuptools.setup() shim
+- setup.cfg Static metadata (name=manimgl, etc.)
+- MANIFEST.in Includes shaders, YAMLs, examples in sdist
+- LICENSE.md MIT
+- README.md Brief — points users to ManimCE for general use
+- logo/
| +- logo.py The 3B1B logo as a runnable scene
| +- *.png Pre-rendered logo assets
+- docs/
| +- Makefile / make.bat Sphinx wrappers
| +- requirements.txt Sphinx==3.0.3, sphinx-copybutton, furo, Jinja2
| +- example.py
| +- source/
| +- conf.py
| +- index.rst
| +- manim_example_ext.py Custom Sphinx directive: ManimExampleDirective
| +- documentation/*.rst Per-module API reference
| +- getting_started/*.rst Quickstart, install, configuration, examples
| +- development/*.rst Changelog, contributing, about
| +- _static/icon.png
+- manimlib/ The package
+- __init__.py Re-exports the entire public API
+- __main__.py `python -m manimlib` entry point
+- config.py CLI parsing + YAML merge -> manim_config
+- default_config.yml Hard-coded defaults
+- constants.py Frame size, vectors, named colors, angles
+- logger.py rich-backed logger "manimgl"
+- module_loader.py Hot-reload aware module import
+- extract_scene.py Scene discovery + BlankScene
+- shader_wrapper.py ShaderWrapper, VShaderWrapper, fill canvas
+- tex_templates.yml 100+ LaTeX preamble bundles
+- typing.py Type aliases (ManimColor, Vect3, ...)
+- window.py Pyglet OpenGL window + event delegation
+- animation/ 13 files; Animation base + every concrete subclass
+- camera/ Camera, ThreeDCamera, CameraFrame
+- event_handler/ EventDispatcher singleton
+- mobject/ 17 top-level files + svg/, types/ subpackages
| +- mobject.py Mobject base, Group, Point, _AnimationBuilder
| +- types/ VMobject, Surface, DotCloud, ImageMobject, PMobject
| +- svg/ Tex, TexText, Text, Brace, drawings
| +- ... Geometry, matrix, numbers, etc.
+- scene/ Scene, InteractiveScene, embed shell, file writer
+- shaders/ GLSL programs (vert/geom/frag) + helper inserts
| +- quadratic_bezier/ Stroke / fill / depth shaders for VMobject
| +- surface/ 3D surface shaders
| +- textured_surface/ UV-textured surface shaders
| +- mandelbrot_fractal/ Two named fractal demos
| +- newton_fractal/
| +- true_dot/ High-quality round-dot shader
| +- image/ 2D image shader
| +- inserts/ Reusable GLSL snippets included via #include
+- utils/ 19 files; bezier, color, file ops, rate funcs, ...
8.2 The Scene Lifecycle
scene = SceneClass(**scene_config)
scene.run()
+- file_writer.begin()
+- setup() # subclass hook
+- construct() # user-supplied animation script
+- interact() # only if window != None; idle event loop
+- tear_down()
+- stop_skipping()
+- file_writer.finish()
+- if window: window.destroy()
construct() is what users override. It can call self.play(...),
self.wait(...), self.add(...), self.remove(...), self.embed(),
and any custom logic.
Inside play:
play(*proto_animations, run_time=None, rate_func=None, lag_ratio=None)
-> animations = [prepare_animation(a) for a in proto_animations]
-> for anim in animations: anim.update_rate_info(...)
-> pre_play()
-> begin_animations(animations)
-> progress_through_animations(animations) # the inner time loop
-> finish_animations(animations)
-> post_play()
8.3 Mobject Class Hierarchy
object
+- Mobject mobject/mobject.py
| +- Group mobject/mobject.py
| +- Point mobject/mobject.py
| +- VMobject mobject/types/vectorized_mobject.py
| | +- VGroup mobject/types/vectorized_mobject.py
| | +- VHighlight mobject/types/vectorized_mobject.py
| | +- TipableVMobject mobject/geometry.py
| | | +- Arc -> Circle -> Dot, Ellipse, SmallDot
| | | +- Arc -> ArcBetweenPoints -> CurvedArrow -> CurvedDoubleArrow
| | | +- Line -> DashedLine, TangentLine, StrokeArrow, Arrow -> Vector
| | +- Polygon -> Rectangle -> Square -> ...
| | +- ParametricCurve -> FunctionGraph
| | +- ImplicitFunction
| | +- StringMobject (svg/string_mobject.py)
| | | +- Tex (svg/tex_mobject.py)
| | | | +- TexText
| | | +- Text (svg/text_mobject.py)
| | +- DecimalNumber -> Integer
| | +- Matrix -> DecimalMatrix -> IntegerMatrix; TexMatrix; MobjectMatrix
| | +- VectorField -> TimeVaryingVectorField
| | +- ...
| +- Surface mobject/types/surface.py
| | +- Sphere, Torus, Cylinder, Cone, Disk3D, Square3D, ...
| | +- TexturedSurface
| +- DotCloud mobject/types/dot_cloud.py
| | +- TrueDot
| +- ImageMobject mobject/types/image_mobject.py
| +- PMobject mobject/types/point_cloud_mobject.py
| +- ValueTracker mobject/value_tracker.py
| +- ExponentialValueTracker, ComplexValueTracker
+- CoordinateSystem (ABC) mobject/coordinate_systems.py
+- Axes (also inherits VGroup)
+- ThreeDAxes
+- NumberPlane
+- ComplexPlane
+- _AnimationBuilder (internal, returned by mobject.animate)
+- _UpdaterBuilder (internal, returned by mobject.always)
+- _FunctionalUpdaterBuilder (internal, returned by mobject.f_always)
The CameraFrame class (in manimlib/camera/camera_frame.py) inherits
from Mobject so the camera itself is animatable.
8.4 Animation Class Hierarchy
object
+- Animation animation/animation.py
+- AnimationGroup animation/composition.py
| +- Succession
| +- LaggedStart -> LaggedStartMap
| +- Flash, AnimationOnSurroundingRectangle, FlashyFadeIn, ...
+- ShowPartial animation/creation.py
| +- ShowCreation -> Uncreate
| +- ShowPassingFlash
+- DrawBorderThenFill -> Write
+- ShowIncreasingSubsets -> ShowSubmobjectsOneByOne, AddTextWordByWord
+- Transform animation/transform.py
| +- ReplacementTransform
| +- TransformFromCopy
| +- MoveToTarget -> _MethodAnimation
| +- ApplyMethod
| | +- ApplyPointwiseFunction
| | | +- ApplyMatrix
| | +- FadeToColor
| | +- ScaleInPlace -> ShrinkToCenter
| | +- ApplyComplexFunction
| +- Restore
| +- ApplyFunction
| +- ApplyPointwiseFunctionToCenter
| +- CyclicReplace -> Swap
| +- FocusOn, Indicate, CircleIndicate, TurnInsideOut
+- Fade -> FadeIn -> FadeInFromPoint
| `- FadeOut -> FadeOutToPoint
+- FadeTransform -> FadeTransformPieces
+- VFadeIn -> VFadeOut, VFadeInThenOut
+- GrowFromPoint -> GrowFromCenter, GrowFromEdge, GrowArrow
+- VShowPassingFlash -> FlashAround -> FlashUnder
+- ShowCreationThenDestruction
+- ShowCreationThenFadeOut (Succession)
+- WiggleOutThenIn
+- ApplyWave (Homotopy)
+- Homotopy animation/movement.py
| +- SmoothedVectorizedHomotopy, ComplexHomotopy
+- PhaseFlow
+- MoveAlongPath
+- Rotating -> Rotate animation/rotation.py
+- ChangingDecimal animation/numbers.py
| +- ChangeDecimalToValue, CountInFrom
+- Broadcast (LaggedStart) animation/specialized.py
+- TransformMatchingParts animation/transform_matching_parts.py
| +- TransformMatchingShapes
| +- TransformMatchingStrings -> TransformMatchingTex
+- UpdateFromFunc, UpdateFromAlphaFunc animation/update.py
+- MaintainPositionRelativeTo
8.5 The Renderer
ManimGL's renderer is shader-based and pure-OpenGL. The lifecycle:
- Camera setup.
Camera.__init__builds aCameraFrame(aMobjectsubclass with Euler-angle uniforms), amodernglcontext (window-shared if a window is attached, otherwise a standalone EGL/WGL context), and two FBOs —fbo_for_files(the offscreen one used when writing video) anddraw_fbo(the read-back FBO for grabbing pixels). - Per-mobject shader programs. Each Mobject type declares which
shader_folderto use (e.g."quadratic_bezier","surface","true_dot").ShaderWrapper.init_programcompiles the GLSL files in that folder into an OpenGL program.VShaderWrapperis a specialized subclass for vector mobjects: it compiles three programs (stroke, fill, depth) and uses two passes per frame. - Render groups.
Scene.assemble_render_groupsclusters adjacent mobjects of the same(type, shader_id, z_index)so they can share a draw call. - Frame capture.
Scene.update_framecallsself.camera.capture(*self.render_groups). Each render group'spre_renderupdates uniforms, binds textures, sets depth-test and clip-plane state; thenrenderissues the draw call. - Output. If a movie is being written,
Scene.emit_framecallsSceneFileWriter.write_frame(camera), which reads pixels fromdraw_fbo(Camera.get_raw_fbo_data) and writes them to FFmpeg's stdin.
8.5.1 The Fill Compositing Trick
VShaderWrapper.get_fill_canvas is one of ManimGL's signature techniques.
It constructs a separate floating-point texture (size: 2x camera
resolution, components=4, dtype f2) used as an intermediate fill canvas.
The fill shader writes signed alpha values, and the composite shader
resolves overlapping triangles via a winding-number computation:
if (color.a < 0) {
color.a = -color.a / (1.0 - color.a);
color.rgb *= (color.a - 1);
}
This is what enables ManimGL to render arbitrary self-intersecting polygons with correct winding fill, rather than relying on stencil tricks or libtess.
8.5.2 Stroke Rendering
Strokes use the quadratic_bezier/stroke GLSL programs. Each VMobject
is decomposed into quadratic Bezier curves, packed into a vertex buffer
with attributes (point, stroke_rgba, stroke_width, joint_angle, unit_normal), and rendered through a geometry shader that emits triangles
along the curve. Joint angles let the geometry shader produce correct
miter / bevel joints without a CPU pre-pass.
8.5.3 Depth Test
Set on a per-mobject basis via ShaderWrapper.set_ctx_depth_test. 3D
mobjects pass depth_test=True; 2D mobjects skip it. Mobjects can also
set up to four GLSL clip planes by writing clip_plane0..clip_plane3
into their uniform dict.
8.6 Configuration Layering Algorithm
def initialize_manim_config() -> Dict:
args = parse_cli()
global_defaults = load_yaml(<package>/manimlib/default_config.yml)
project_overrides = load_yaml("custom_config.yml") # CWD
file_overrides = load_yaml(args.config_file) if args.config_file else {}
config = Dict(merge_dicts_recursively(
global_defaults, project_overrides, file_overrides
))
log.setLevel(args.log_level or config["log_level"])
update_directory_config(config)
update_window_config(config, args)
update_camera_config(config, args)
update_file_writer_config(config, args)
update_scene_config(config, args)
update_run_config(config, args)
update_embed_config(config, args)
return config
merge_dicts_recursively (in manimlib/utils/dict_ops.py) is a deep
dict merge — child dicts merge key-by-key, scalars in later sources
overwrite earlier ones, lists overwrite (not concatenate).
The Dict (from addict) wrapping turns every key into an attribute,
so config.camera.fps works as well as config["camera"]["fps"].
8.7 Module Loader and Hot Reload
manimlib/module_loader.py:ModuleLoader is the layer that imports
user-supplied scene files. The non-trivial part is supporting
reload_scene() from inside the IPython shell.
ModuleLoader.get_module(file_name, is_during_reload):
if file_name is None: # `manimgl` with no file
return None # caller will use BlankScene
spec = importlib.util.spec_from_file_location(...)
module = importlib.util.module_from_spec(spec)
if is_during_reload:
# Track every import the module triggers,
# then deep-reload all user-defined ones.
imported = _exec_module_and_track_imports(spec, module)
_reload_modules(imported, set())
spec.loader.exec_module(module)
return module
_is_user_defined_module filters out stdlib (anything under
sysconfig.get_path("stdlib")), site-packages / dist-packages, and
ManimGL itself if ignore_manimlib_modules_on_reload=True. The deep
reload walks the imported module's __dict__ for nested modules and
class-attached modules, calling importlib.reload on each.
8.8 Interactive Shell (InteractiveSceneEmbed)
Defined in manimlib/scene/scene_embed.py. On Scene.embed() it:
- Stops skipping animations.
- Forces one frame draw so the window has something to show.
- Builds an
InteractiveSceneEmbed, which:- Creates an IPython terminal
InteractiveShelland registers a custom exception handler that flashes a red full-screen rectangle when a cell raises. - Pre-populates the shell namespace with
self, every local inconstruct, and a curated set of helpers (play,add,remove,clear,wait,save_state,restore,touch,reload_scene,auto_reload,checkpoint_paste). - Hooks
pre_run_cellso each cell runsScene.update_framebetween interactions, keeping the window responsive.
- Creates an IPython terminal
- Calls
shell.embed()to enter the REPL. - Raises
EndScenewhen the user exits, ending the run loop cleanly.
reload_scene() validates syntax of the user file, sets
run_config.is_reload = True, and runs the exit_raise IPython magic to
break out of the shell. The outer run_scenes loop in __main__.py
catches KillEmbedded and starts the scene again from a clean slate
(but with the same embed_line, optionally overridden).
CheckpointManager provides "checkpoint paste":
- Read a Python snippet from the system clipboard (
pyperclip.paste()). - If the snippet's first non-whitespace line is a comment, use that comment as a checkpoint key. Save the scene state on first use; on subsequent uses, restore to the saved state before running the snippet.
- This lets you iterate on a block of scene code without rewinding the whole scene.
8.9 The File Writer Subprocess
SceneFileWriter (in manimlib/scene/scene_file_writer.py) manages the
FFmpeg subprocess. Its lifecycle:
begin_animation() # opens a new ffmpeg pipe (or partial movie)
+- writing_process = subprocess.Popen([
"ffmpeg",
"-y", "-loglevel", "error",
"-f", "rawvideo",
"-s", f"{w}x{h}",
"-pix_fmt", "rgba",
"-r", str(fps),
"-i", "-", # video frames from stdin
*audio_inputs,
"-c:v", video_codec,
"-pix_fmt", pixel_format,
movie_path,
], stdin=PIPE)
write_frame(camera) # one .write() per frame
+- writing_process.stdin.write(camera.get_raw_fbo_data())
end_animation() # close pipe, optionally append to master file
finish() # final cleanup
If --subdivide is set, each animation gets its own movie file under a
folder named after the scene. Otherwise all animations share a single
{scene}.mp4.
add_sound(sound_file, time, gain=None, gain_to_background=None):
if time < 0:
raise Exception("Adding sound at timestamp < 0")
Audio is loaded via pydub.AudioSegment, stamped with time_offset,
mixed via pydub.effects.normalize, and the result is written as a
sidecar WAV that FFmpeg picks up at finalization time.
8.10 Event Handler Internals
manimlib/event_handler/event_dispatcher.py defines a singleton
EVENT_DISPATCHER (constructed at module import). Mobjects can subscribe
to events by attaching listeners:
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType
listener = EventListener(my_mobject, EventType.MousePressEvent, my_callback)
EVENT_DISPATCHER.add_listner(listener)
EventType is an enum.Enum with values: MouseMotionEvent,
MousePressEvent, MouseReleaseEvent, MouseDragEvent,
MouseScrollEvent, KeyPressEvent, KeyReleaseEvent.
Dispatch logic in EventDispatcher.dispatch:
- Mouse events: only fire on listeners whose mobject's bounding box
contains the mouse point (
is_point_touching). Drag events fire only on the listeners that were touching at the correspondingMousePress. - Key events: fire on every listener for the key event type.
- A listener returns
Falseto stop propagation; otherwise other listeners get called.
Window.on_mouse_motion / on_mouse_drag / on_key_press etc. forward
to the active scene, which forwards (via EVENT_DISPATCHER.dispatch) to
the mobject listeners and then handles built-in interactions
(camera pan, scroll-zoom, undo/redo).
Concrete usage: the MotionMobject, Button, Checkbox,
LinearNumberSlider, ColorSliders, Textbox, and ControlPanel
classes in manimlib/mobject/interactive.py all install event listeners
in their __init__.
8.11 LaTeX Pipeline
manimlib/utils/tex_file_writing.py orchestrates Tex / TexText
compilation:
latex_to_svg(tex_string, template_name="default"):
template = get_tex_template_config(template_name) # from tex_templates.yml
full_tex = wrap_in_template(tex_string, template) # adds preamble
...build full_tex.tex, .aux, .log...
sp.run([template.compiler, "-no-pdf", ...]) # latex / xelatex / lualatex
sp.run(["dvisvgm", "--no-fonts", ...]) # converts dvi -> svg
return svg_path
Errors raise LatexError (the only public custom exception in the utils
package). The cache key is hash_string(full_tex), and the cached SVG
lives at <cache_dir>/<hash>.svg. Cache hits skip the compile step.
get_tex_template_config(name) reads the requested template from
manimlib/tex_templates.yml. If you maintain your own preamble bundles,
either add them to a fork of tex_templates.yml or pass a custom
tex.template value pointing to a key you've added.
Important
ManimGL does not pass
--shell-escapeto the LaTeX binary. This blocks the standard\write18{...}command-injection vector. If you add a custom template, do not enable shell-escape unless you control everyTexstring the renderer will see.
8.12 Code Patterns and Conventions
from __future__ import annotationsat the top of nearly every module. Type hints are PEP 604 / generic syntax; many are lazy-loaded inif TYPE_CHECKING:blocks to avoid circular imports.addict.Dictis used in place of dict-with-defaults for config. Reading a missing attribute returns an emptyDict(), not aKeyError. This is convenient but can mask typos — be careful.- Classes prefer
__init__-time configuration to subclass attributes for "user-tweakable" behavior (Scene.random_seed,Scene.samples,InteractiveScene.palette_colors). Callers override these as class attributes on their subclass; constructor args are reserved for per-instance values. - Animations construct a
starting_mobjectcopy inbegin()and interpolate fromstarting_mobject->mobject. Don't mutatestarting_mobjectafterbeginreturns. @affects_mobject_list(a method decorator onScene) callsself.assemble_render_groups()after the wrapped method returns, refreshing the renderer's batches. Apply it to anyScenemethod that changesself.mobjects.- Method chaining returns
selfon most setters.mob.set_color(RED).shift(UP)works. - No
@propertysetters on Mobjects — useset_*methods. This is an explicit choice so that setters can do extra work (resampling bezier curves, refreshing bounding boxes) without surprising users. - GLSL shaders are textual
.glslfiles loaded bymanimlib/utils/shaders.py:get_shader_code_from_filefrommanimlib/shaders/. The path-resolution rules support#include "common.glsl"directives via theinserts/folder.
8.13 Testing Strategy
The repo does not ship a comprehensive automated test suite. Validation is essentially:
- Running
example_scenes.pyend-to-end on each release. - Visual review of rendered output.
- The
3b1b/videoscompanion repo serving as a real-world regression surface (Grant uses ManimGL daily).
If you fork ManimGL and want CI coverage, consider:
- Snapshot-test individual mobjects: render a scene with
-s -wto a PNG and image-diff against a golden. - Lint with
rufforflake8. The codebase doesn't enforce a style but PEP 8 fits. mypy --check-untyped-defsruns but produces many warnings — ManimGL uses type hints liberally without enforcing them strictly.
8.14 Extending ManimGL
Add a new Mobject
- Subclass
Mobject(point cloud),VMobject(vector graphics), orSurface(3D mesh). - Implement
init_points(self)to populateself.points. - Optionally override
init_colors,init_uniforms,init_data. - If you need a custom shader, add a folder under
manimlib/shaders/withvert.glsl,geom.glsl(optional),frag.glsland pass its name toMobject.shader_folder. - Re-export from
manimlib/__init__.pyif you want it imported byfrom manimlib import *.
Add a new Animation
- Subclass
Animation,Transform,AnimationGroup, or another base. - For the simplest case, override
interpolate_submobject(self, sub, sub_starting, alpha)to define how each submobject changes. - For animations that need a per-frame setup, override
interpolate_mobject(self, alpha). Computealphaviaself.time_spanned_alpha(alpha)if you wanttime_spanto work. - Override
begin,finish, orclean_up_from_sceneif you need lifecycle hooks. - Place the file under
manimlib/animation/and re-export.
Add a new CLI flag
- Add
parser.add_argument(...)inmanimlib/config.py:parse_cli. - Decide where the flag should land in
manim_config. Typically you add anupdate_<section>_config(config, args)block near the existing ones. - Reference the new key from the relevant subsystem (camera, file
writer, scene). Updates are visible to all
Sceneinstances created during that run.
8.15 Potential Improvements
These are gaps an interested contributor might address. They are observations, not promises:
- Add a frozen-time progress callback.
Scene.playexposes no hook for external tools; a job-queue integration must parse stdout. A simpleprogress_callback: Callable[[int, int], None]parameter onScenewould unblock orchestration tooling. - Move the fill canvas to a smaller resolution for previews. The
2x-resolution
f2texture is overkill for a 480p preview and costs significant VRAM and bandwidth. - Replace the regex-based GLSL
#includeresolver inutils/shaders.py:get_shader_code_from_filewith a proper preprocessor pass — the current one cannot handle conditional includes or nested defines. - Add a deterministic test surface (snapshot tests for at least every example scene at a low resolution) to catch shader regressions.
- Tighten
addict.Dictusage. It silently materializes empty dicts for missing keys, which masks typos likemanim_config.cammera.fps. A wrapping class that whitelists known keys would catch these at startup. - Document the asset URL fetching path.
TexturedSurface(url, url)silently downloads — there should be adirectories.allowed_hostsallowlist for multi-tenant deployments (relates to issue #2434). - Add
bash-completionfor themanimglCLI. The argparse spec is large enough that tab completion would meaningfully help.
8.16 Contributing
The repository's high-level workflow:
- Fork
https://github.com/3b1b/manimon GitHub. - Branch off
master. Keep PRs small and focused. - Match the existing style: PEP 8 spacing, type hints with
from __future__ import annotations,importordering by stdlib / third-party / first-party / local, no unnecessary docstrings on trivial methods (the codebase favors expressive method names over docstrings). - Open a PR. Grant reviews them as time permits — bear in mind the project is a small group's tool, not a community staffed library.
- For visual changes, attach a rendered before/after MP4 or GIF.
docs/source/development/contributing.rst has the canonical instructions
and any current style nits.
9. Integration Points
ManimGL is a self-contained desktop application but it does interact with the host environment in a few well-defined ways:
| Integration | How | Source |
|---|---|---|
| FFmpeg | Subprocess pipe (raw RGBA frames over stdin) | manimlib/scene/scene_file_writer.py |
LaTeX (latex / xelatex / lualatex) |
Subprocess (one call per Tex cache miss) |
manimlib/utils/tex_file_writing.py |
dvisvgm |
Subprocess (DVI -> SVG) | manimlib/utils/tex_file_writing.py |
| Pango / HarfBuzz | In-process via manimpango for Text glyph layout |
manimlib/mobject/svg/text_mobject.py |
| OpenGL driver | Via moderngl (moderngl_window for windowed; standalone EGL/WGL for headless) |
manimlib/camera/camera.py, manimlib/window.py |
| System clipboard | pyperclip (read in CheckpointManager, write in InteractiveScene) |
manimlib/scene/scene_embed.py, manimlib/scene/interactive_scene.py |
| System cache dir | appdirs.user_cache_dir("manim") |
manimlib/utils/cache.py, tex_file_writing.py |
| Filesystem | YAML config from CWD; assets from directories.* paths |
manimlib/utils/file_ops.py, directories.py |
| HTTP(S) | Asset URL downloads via validators + stdlib urllib |
manimlib/utils/file_ops.py (and surrounding utils) |
| Audio | pydub reads sound files; FFmpeg muxes them onto the final movie |
manimlib/utils/sounds.py |
| IPython | Embedded shell for interactive development | manimlib/scene/scene_embed.py |
There is no:
- Built-in HTTP server, API, or RPC layer.
- Plugin system (the package re-exports
manimlib.*; "plugins" in the ManimGL world are just Python modules you import alongside your scenes). - Database or persistent state beyond the on-disk cache.
- Telemetry, analytics, or call-home behavior.
Note
If you need to integrate ManimGL with another system (a web service, an LMS, a video editor), wrap the CLI. The library's API is stable enough to script directly, but the boundary is "import the package and instantiate a Scene", not "call into an exposed service".