Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

ManimGL (3b1b/manim) Administrator and Developer Guide

Generated April 2026 from 3b1b/manim at c5e23d9

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 by Tex / TexText mobjects.

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):

  1. pre_play — Update skipping status (jump-to-N, etc.), tell the SceneFileWriter to begin_animation() (creates a partial movie file if subdivide_output is on, opens a new FFmpeg pipe, and writes leading audio padding).
  2. begin_animations — Call animation.begin() on each. Each Animation saves a starting_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.
  3. progress_through_animations — Iterate dt-spaced timestamps from 0 to run_time. For each t:
    • Each animation's update_mobjects(dt) runs updaters on auxiliary copies (e.g. target_mobject for Transform).
    • Each animation's interpolate(alpha) runs (where alpha = t / run_time).
    • The scene advances time by dt, runs every mobject's updaters, and calls camera.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.
  4. finish_animations — Each animation's finish() runs (sets alpha=final_alpha_value, default 1.0), then clean_up_from_scene (removes the mobject if is_remover()).
  5. post_play — Increment num_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, then post_play), but the inner loop only updates mobjects/frames; no animation interpolation.
  • If presenter_mode is on and not skipping, the loop blocks on hold_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 with samples=4, this can exceed 1 GB of VRAM transiently. Reduce camera.resolution or samples if 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 by ast.literal_eval in update_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_ks plus -t (transparent) emits a .mov with alpha. ManimGL automatically applies this combination when only -t is passed. Override pixel_format only 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, plus ecf_*, 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. Browse manimlib/tex_templates.yml for 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-run to 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 (calls manimlib.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 --version runs and prints a green banner.
  • [ ] ffmpeg -version runs (and the codec you need is listed under ffmpeg -codecs).
  • [ ] latex --version runs (only if any scene uses Tex/TexText).
  • [ ] custom_config.yml resolved: 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.yml directory 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_progress to get per-animation tqdm bars.
  • Use --prerun to 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 Scene with a custom subclass that overrides play to 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. Prefer mobject.add_updater(...) that mutates an existing mobject in place.
  • TransformMatchingStrings / TransformMatchingShapes do per-submobject pairing and longest-substring matching at begin(). Cost is O(n*m) in submobject counts; cap source/target string length.
  • VShaderWrapper.get_fill_canvas allocates a 2x-resolution floating-point texture for fill compositing. Consider lowering camera.resolution for previews.
  • Tex first-render triggers a LaTeX compile + dvisvgm + parse + glyph triangulation. After the first render, results are cached.
  • Scene.assemble_render_groups runs after every add / remove to batch mobjects with the same shader. Adding hundreds of mobjects one at a time is slower than self.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_sound and 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 / apparmor profiles 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:

  1. Camera setup. Camera.__init__ builds a CameraFrame (a Mobject subclass with Euler-angle uniforms), a moderngl context (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) and draw_fbo (the read-back FBO for grabbing pixels).
  2. Per-mobject shader programs. Each Mobject type declares which shader_folder to use (e.g. "quadratic_bezier", "surface", "true_dot"). ShaderWrapper.init_program compiles the GLSL files in that folder into an OpenGL program. VShaderWrapper is a specialized subclass for vector mobjects: it compiles three programs (stroke, fill, depth) and uses two passes per frame.
  3. Render groups. Scene.assemble_render_groups clusters adjacent mobjects of the same (type, shader_id, z_index) so they can share a draw call.
  4. Frame capture. Scene.update_frame calls self.camera.capture(*self.render_groups). Each render group's pre_render updates uniforms, binds textures, sets depth-test and clip-plane state; then render issues the draw call.
  5. Output. If a movie is being written, Scene.emit_frame calls SceneFileWriter.write_frame(camera), which reads pixels from draw_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:

  1. Stops skipping animations.
  2. Forces one frame draw so the window has something to show.
  3. Builds an InteractiveSceneEmbed, which:
    • Creates an IPython terminal InteractiveShell and 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 in construct, and a curated set of helpers (play, add, remove, clear, wait, save_state, restore, touch, reload_scene, auto_reload, checkpoint_paste).
    • Hooks pre_run_cell so each cell runs Scene.update_frame between interactions, keeping the window responsive.
  4. Calls shell.embed() to enter the REPL.
  5. Raises EndScene when 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 corresponding MousePress.
  • Key events: fire on every listener for the key event type.
  • A listener returns False to 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-escape to 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 every Tex string the renderer will see.

8.12 Code Patterns and Conventions

  • from __future__ import annotations at the top of nearly every module. Type hints are PEP 604 / generic syntax; many are lazy-loaded in if TYPE_CHECKING: blocks to avoid circular imports.
  • addict.Dict is used in place of dict-with-defaults for config. Reading a missing attribute returns an empty Dict(), not a KeyError. 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_mobject copy in begin() and interpolate from starting_mobject -> mobject. Don't mutate starting_mobject after begin returns.
  • @affects_mobject_list (a method decorator on Scene) calls self.assemble_render_groups() after the wrapped method returns, refreshing the renderer's batches. Apply it to any Scene method that changes self.mobjects.
  • Method chaining returns self on most setters. mob.set_color(RED).shift(UP) works.
  • No @property setters on Mobjects — use set_* 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 .glsl files loaded by manimlib/utils/shaders.py:get_shader_code_from_file from manimlib/shaders/. The path-resolution rules support #include "common.glsl" directives via the inserts/ folder.

8.13 Testing Strategy

The repo does not ship a comprehensive automated test suite. Validation is essentially:

  • Running example_scenes.py end-to-end on each release.
  • Visual review of rendered output.
  • The 3b1b/videos companion 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 -w to a PNG and image-diff against a golden.
  • Lint with ruff or flake8. The codebase doesn't enforce a style but PEP 8 fits.
  • mypy --check-untyped-defs runs but produces many warnings — ManimGL uses type hints liberally without enforcing them strictly.

8.14 Extending ManimGL

Add a new Mobject

  1. Subclass Mobject (point cloud), VMobject (vector graphics), or Surface (3D mesh).
  2. Implement init_points(self) to populate self.points.
  3. Optionally override init_colors, init_uniforms, init_data.
  4. If you need a custom shader, add a folder under manimlib/shaders/ with vert.glsl, geom.glsl (optional), frag.glsl and pass its name to Mobject.shader_folder.
  5. Re-export from manimlib/__init__.py if you want it imported by from manimlib import *.

Add a new Animation

  1. Subclass Animation, Transform, AnimationGroup, or another base.
  2. For the simplest case, override interpolate_submobject(self, sub, sub_starting, alpha) to define how each submobject changes.
  3. For animations that need a per-frame setup, override interpolate_mobject(self, alpha). Compute alpha via self.time_spanned_alpha(alpha) if you want time_span to work.
  4. Override begin, finish, or clean_up_from_scene if you need lifecycle hooks.
  5. Place the file under manimlib/animation/ and re-export.

Add a new CLI flag

  1. Add parser.add_argument(...) in manimlib/config.py:parse_cli.
  2. Decide where the flag should land in manim_config. Typically you add an update_<section>_config(config, args) block near the existing ones.
  3. Reference the new key from the relevant subsystem (camera, file writer, scene). Updates are visible to all Scene instances 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.play exposes no hook for external tools; a job-queue integration must parse stdout. A simple progress_callback: Callable[[int, int], None] parameter on Scene would unblock orchestration tooling.
  • Move the fill canvas to a smaller resolution for previews. The 2x-resolution f2 texture is overkill for a 480p preview and costs significant VRAM and bandwidth.
  • Replace the regex-based GLSL #include resolver in utils/shaders.py:get_shader_code_from_file with 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.Dict usage. It silently materializes empty dicts for missing keys, which masks typos like manim_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 a directories.allowed_hosts allowlist for multi-tenant deployments (relates to issue #2434).
  • Add bash-completion for the manimgl CLI. The argparse spec is large enough that tab completion would meaningfully help.

8.16 Contributing

The repository's high-level workflow:

  1. Fork https://github.com/3b1b/manim on GitHub.
  2. Branch off master. Keep PRs small and focused.
  3. Match the existing style: PEP 8 spacing, type hints with from __future__ import annotations, import ordering by stdlib / third-party / first-party / local, no unnecessary docstrings on trivial methods (the codebase favors expressive method names over docstrings).
  4. 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.
  5. 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".