-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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
For each frame, MLWindow:
- advances time and calls each active animation's
interpolate(alpha); - traverses
scene.mobjects(sync) and dispatches each leaf mobject to a type-specific sender; - clears the DLL command buffer (
ClearShapes) and submits the scene; - calls the native
Render_DrawScene, which builds vertices and draws; - optionally read-backs the frame for recording.
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 unitsEach GPU vertex is 6 floats — [ndc_x, ndc_y, r, g, b, alpha] — in NDC where
ndc = pixel/px_extent * 2 - 1.
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)
...| 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 |
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.
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 rotationThese 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
|
- 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.
- Transform routing through bezier. Transforms mutate points, so they take the generic path; the exception is axis-aligned quads during rotation.
- Per-frame rotation delta. VGroup spin is an accumulated angle; each frame the delta is applied as a rotation matrix to submobject points.
- Opacity isolation. Per-mobject fade state is tracked separately from Manim's internals so concurrent animations don't interfere and fades are clean.
- 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.