Skip to content

Internals

Tagin T edited this page Sep 4, 2026 · 1 revision

Internals

Deep dive into how real-time-manim renders a frame, how shapes map to the DLL, and the design decisions behind it. For the public surface see API Reference.

Frame lifecycle

flowchart TD
    P[play animations] --> B[begin each animation]
    B --> LOOP{per frame}
    LOOP --> ST[advance time + interpolate alpha]
    ST --> SYNC[sync scene]
    SYNC --> TICK[tick — process Win32 messages]
    TICK --> SEND[dispatch each mobject to a sender]
    SEND --> CS[ClearShapes]
    SEND --> D[DLL Render_DrawScene]
    D --> PRES[present to window]
    PRES --> OPT{recording?}
    OPT -- yes --> SS[SaveScreenshot -> ffmpeg]
    OPT -- no --> LOOP
    SS --> LOOP
Loading

For each frame, MLWindow:

  1. advances time and calls each active animation's interpolate(alpha);
  2. traverses scene.mobjects (sync) and dispatches each leaf mobject to a type-specific sender;
  3. clears the DLL command buffer (ClearShapes) and submits the scene;
  4. calls the native Render_DrawScene, which builds vertices and draws;
  5. optionally read-backs the frame for recording.

Coordinate system

Manim uses a centered, Y-up frame. Screen pixels are Y-down.

Manim  (-7.1 → +7.1 , -4.0 → +4.0)     Screen (0 → 1920, 0 → 1080)
        Y-axis UP                              Y-axis DOWN

manim_to_screen() (in real_time_manim/vulkan_util.py) maps between them:

sx = cx + x * (w / frame_width)     # frame_width = w * 8.0 / h
sy = cy - y * (h / 8.0)             # Manim default frame height = 8 units

Each GPU vertex is 6 floats — [ndc_x, ndc_y, r, g, b, alpha] — in NDC where ndc = pixel/px_extent * 2 - 1.

Shape dispatch

MLWindow._send walks the mobject tree and routes each leaf by type:

def _send(self, mob, angle, parent_alpha, ...):
    if isinstance(mob, Text):        # -> AddText via DLL (TrueType)
    elif isinstance(mob, VGroup):    # -> recurse into submobjects
    elif isinstance(mob, Square):    # -> AddRect fill + AddLine stroke
    elif isinstance(mob, Circle):    # -> AddCircle fill + tessellated stroke
    elif isinstance(mob, Arrow):     # -> AddLine shaft + AddPolygon tip
    elif isinstance(mob, Polygon):   # -> AddPolygon(vertices)
    elif isinstance(mob, VMobject):  # -> AddBezierPath(control points)
    ...

Shape → DLL mapping

Manim type DLL function Notes
Square, Rectangle AddRect fill rect + line edges for stroke
Circle AddCircle fill circle + tessellated stroke segments
Ellipse AddEllipse fill + tessellated stroke
Line, DashedLine AddLine / AddDashedLine honour draw progress for Create
Arrow AddLine + AddPolygon shaft + triangular tip
Polygon, Polygram AddPolygon vertex array, closed path
Arc AddArc arc segment
Dot, Point AddCircle / AddPoint small filled circle
Text AddText TrueType via stb_truetype
generic VMobject AddBezierPath bezier tessellation

Transform handling

When a mobject has _transforming = True, the renderer bypasses the shape senders and renders it as a generic bezier path (_send_vmobject). That is what makes Transform (points morphing between shapes), point-wise warps and ClockwiseTransform work, because a transform edits the mobject's points rather than its high-level shape properties.

Exception: axis-aligned quads (Square/Rectangle) during rotation still use a polygon path for solid fills.

Animation bookkeeping

All animations extend a base Animation with begin / interpolate / finish and Manim-compatible run_time/lag_ratio. To keep concurrent fades and spins isolated from Manim's internal state, real-time-manim tracks two per-mobject values in module-level dicts:

_anim_opacity[id(mob)]  = 0.0..1.0     # current fade level
_anim_rotation[id(mob)] = radians      # accumulated rotation

These propagate through VGroup hierarchies during sync(). Rotation deltas are computed each frame and applied as a rotation matrix around the mobject's pivot.

Mobjects also carry animation-driven attributes the senders read:

Attribute Type Meaning
_vulkan_progress float draw progress 0→1 (e.g. Create)
_transforming bool render as bezier path, not a shape sender
_grow_scale float scale factor for GrowFrom*
_grow_point tuple origin point for scale animations
_rotation_about_point tuple rotation pivot
_letter_alphas list per-character opacity for Write

Design decisions

  1. Shape-specific dispatch. Emit dedicated vertices for simple primitives instead of tessellating everything through a bezier path — less overhead, better batching, keeps it real-time.
  2. Transform routing through bezier. Transforms mutate points, so they take the generic path; the exception is axis-aligned quads during rotation.
  3. Per-frame rotation delta. VGroup spin is an accumulated angle; each frame the delta is applied as a rotation matrix to submobject points.
  4. Opacity isolation. Per-mobject fade state is tracked separately from Manim's internals so concurrent animations don't interfere and fades are clean.
  5. The DLL owns the GPU. Vulkan instance/device/swapchain, vertex buffers, shaders and the Win32 window all live in the native DLL; Python stays focused on animation logic and mobject state.

Clone this wiki locally