Skip to content

Refactor: separate camera state from renderer-owned rasterization (cairo) + consolidate renderer related modules - #4989

Open
behackl wants to merge 13 commits into
mainfrom
refactor/camera-renderer-ownership
Open

Refactor: separate camera state from renderer-owned rasterization (cairo) + consolidate renderer related modules#4989
behackl wants to merge 13 commits into
mainfrom
refactor/camera-renderer-ownership

Conversation

@behackl

@behackl behackl commented Sep 4, 2026

Copy link
Copy Markdown
Member

Overview: What does this pull request change?

This PR makes the camera/renderer ownership boundary explicit: Cairo cameras describe semantic views, while CairoRenderer owns raster targets, PyCairo drawing, composition, static-image reuse, readback, and cleanup.

It also packages concrete implementations under manim.renderer.cairo and manim.renderer.opengl, gives the default Cairo Camera an animatable frame and auto_zoom(), and preserves ThreeD, ZoomedScene, nested-camera, image, point-cloud, background, and static-cache behavior.

This change assumes the renderer-independent writer boundary introduced by #4976. Renderer-owned play loops, clocks, cache decisions, and writer scheduling remain unchanged for the later Manager execution refactor.

Motivation and Explanation: Why and how do your changes improve the library?

Cairo cameras previously combined semantic view state with pixel buffers, PyCairo contexts, drawing, composition, and readback. That made camera movement a special case and made render-resource ownership difficult to reason about.

After this change:

  • Camera, MovingCamera, MultiCamera, and ThreeDCamera own semantic frame, background, ordering, projection, shading, tracker, and nested-view state;
  • CairoRenderer and private helpers own all Cairo resources and raster work;
  • only production modules in the Cairo backend import PyCairo;
  • nested views use renderer-owned secondary targets, detect cycles, exclude their own display family, and clean up with the parent renderer;
  • background-colored vector composition reuses a renderer-owned scratch target; and
  • both backend implementations and OpenGL shader assets live under canonical backend packages.

For ordinary Cairo scenes, camera movement no longer requires a specialized scene class:

class CameraExample(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        self.play(self.camera.frame.animate.move_to(square).scale(0.5))
        self.play(self.camera.auto_zoom([square], margin=1))

MovingCamera and MovingCameraScene remain supported as thin named subclasses.

Breaking changes and migration

Supported top-level imports remain unchanged:

from manim import Camera, CairoRenderer, MovingCamera, MultiCamera, ThreeDCamera

Implementation-module imports are intentionally a clean break; no duplicate compatibility modules are retained.

Module paths

Removed path Replacement
manim.camera.camera manim.renderer.cairo.camera
manim.camera.moving_camera manim.renderer.cairo.camera
manim.camera.multi_camera manim.renderer.cairo.camera
manim.camera.three_d_camera manim.renderer.cairo.camera
manim.renderer.cairo_renderer manim.renderer.cairo.renderer or manim.renderer.cairo
manim.renderer.opengl_renderer manim.renderer.opengl.renderer, manim.renderer.opengl.camera, or manim.renderer.opengl
manim.renderer.opengl_renderer_window manim.renderer.opengl.window
manim.renderer.shader manim.renderer.opengl.shader
manim.renderer.shader_wrapper manim.renderer.opengl.shader_wrapper
manim.renderer.vectorized_mobject_rendering manim.renderer.opengl.vectorized_mobject_rendering

OpenGL shader resources move from manim/renderer/shaders to manim/renderer/opengl/shaders. Plugins loading those resources directly must update their package-relative paths.

Camera construction and raster access

Cairo camera constructors now accept semantic view settings only. The following raster-oriented arguments are removed:

  • pixel_width, pixel_height, and frame_rate;
  • image_mode, n_channels, and pixel_array_dtype;
  • background pixel arrays and cairo_line_width_multiple; and
  • MovingCamera.fixed_dimension.

Use config.pixel_width, config.pixel_height, and config.frame_rate for render execution settings. Cairo output is standardized on renderer-owned uint8 RGBA targets. Use background_image, background_color, and background_opacity for camera backgrounds. Configure logical view geometry with frame=, frame_width=, frame_height=, or the public camera.frame mobject. Unknown camera keywords now raise TypeError instead of being retained silently.

Camera-owned raster APIs are also removed, including camera.pixel_array, capture_mobjects(), get_image(), Cairo-context caches, and display_* / apply_* drawing helpers. Direct raster users should render through CairoRenderer:

renderer = CairoRenderer(camera=camera)
try:
    renderer.render_mobjects(mobjects)
    pixels = renderer.get_frame()
    image = renderer.get_image()
finally:
    renderer.close()

For one-off mobject images, mobject.get_image(camera=camera) manages that temporary renderer automatically.

Custom camera subclasses should implement semantic hooks such as object ordering, point projection, shading, and movement controls. Backend drawing extensions belong in a renderer integration rather than a camera subclass.

Removed camera implementations

MappingCamera, OldMultiCamera, and SplitScreenCamera are removed.

  • Nonlinear mapping should be applied explicitly to sufficiently subdivided geometry with apply_function() rather than through MappingCamera.
  • Nested or split views should use MultiCamera with positioned ImageMobjectFromCamera display mobjects.

ImageMobjectFromCamera remains available, but it is now a geometry-bearing Mobject rather than an AbstractImageMobject. It stores no pixel_array and has no get_pixel_array() API; CairoRenderer renders the source camera into a secondary target and composites it into the display geometry. Existing positioning, display-frame, and resampling usage remains supported.

For ThreeD camera positioning, use camera.frame or camera.frame_center; the private _frame_center mobject is removed.

Links to added or changed documentation pages

  • docs/source/guides/deep_dive.rst
  • docs/source/reference.rst
  • docs/source/reference_index/cameras.rst

Preview links can be added after the PR is opened.

Further Information and Comments

This PR does not move scheduling or resource lifecycle into Manager, redesign OpenGL context lifetime, or unify Cairo and OpenGL cameras. Tests etc. pass locally.

AI assistance was used during implementation and review. The resulting design, code, documentation, and tests were reviewed by the author.

margin: float = 0,
only_mobjects_in_frame: bool = False,
animate: Literal[False] = False,
) -> Mobject: ...
margin: float = 0,
only_mobjects_in_frame: bool = False,
animate: Literal[True] = True,
) -> _AnimationBuilder: ...
@behackl behackl added refactor Refactor or redesign of existing code breaking changes This PR introduces breaking changes labels Sep 4, 2026
Retire unused nested targets, release retained buffers, and guard closed operations. Reject unused renderer keywords and build dispatch tables once per draw.
Expose Scene.get_image() and Scene.show() through Manager without stepping animations or emitting movie frames. Preserve active raster targets and capture OpenGL meshes on the context-owning thread.
Compute default frame bounds directly without a semantic cache, preserve custom frame hooks, and share perspective scaling between axes. Add mutation, boundary, and projection parity tests.

@nikolajmunk nikolajmunk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a superficial deslopping pass of some of the documentation and comments.

More generally, I think it would be useful to consciously choose some language for the terms used in a camera/render context. I don't like "logical/semantic" "frame/view". I would suggest that we adopt camera frame (or just frame when the context is clear) to describe the rectangle which controls the camera view (the position, angle, and size of the view), and I would suggest that the rectangle which defines the output image is called the viewport - in graphics engine circles, this is the usual nomenclature for the "screen rectangle". Another choice might be canvas but that doesn't work quite as well for 3D.

So for example, you might say "When both the width and height of the camera frame are provided, you must ensure that the pixel dimensions of the viewport have the same aspect ratio of the frame, otherwise (bad things happen)."

Happy to discuss more! I would also be happy to give the new camera guide a more thorough rewrite in a later PR once things settle down a bit.

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +43 to +44
configured pixel aspect ratio. Square and portrait output therefore preserve ordinary
geometry. One explicit camera dimension determines the other using that aspect ratio;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does Square and portrait output therefore preserve ordinary geometry mean?

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +162 to +163
Run this example with ``--renderer=cairo``. Selecting ``MultiCamera`` in the constructor
ensures it is installed before the scene's renderer is initialized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest a separate "changing camera class" section which explains this, since this is the current intended approach for all custom cameras. Also "installed" is weird phrasing here, is there a more pythonesque word you could use instead?

Comment thread docs/source/guides/cameras.rst Outdated
self.play(self.camera.auto_zoom([square]))
self.play(Restore(self.camera.frame))

:class:`.MovingCameraScene` remains available as a descriptive name for this behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would either remove this entirely or make explicit that MovingCameraScene exists for backwards compatibility and will be deleted in the future.

Comment thread docs/source/guides/cameras.rst Outdated
Comment thread docs/source/guides/cameras.rst Outdated
Comment thread docs/source/guides/cameras.rst Outdated
Run this example with ``--renderer=cairo``. Selecting ``MultiCamera`` in the constructor
ensures it is installed before the scene's renderer is initialized.

Both registration and scene membership matter: registering a display tells MultiCamera

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Both registration and scene membership matter" is mega-slopspeak :D

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +188 to +190
The renderer retires unused secondary targets on the next draw. A scene image requested
with ``self.get_image()`` includes all currently registered and visible views; there is
no need to copy camera pixels or refresh each inset yourself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another instance of the doc specifying what the user doesn't need to do.

Comment thread manim/renderer/cairo/camera.py Outdated


class _CameraFrame(ScreenRectangle):
"""Default frame with a direct bounding-box center query, not a cached center."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrite so the docstring doesn't say what the frame doesn't do.

Comment thread manim/renderer/cairo/camera.py Outdated
class Camera:
"""Describe the logical view used by a rendering backend.

Camera owns an animatable frame, semantic background settings, display ordering,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference: I don't like the heavy usage of "owns". "Has" works perfectly fine and aligns with how we generally talk about classes and their attributes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in some contexts "owns" is used to mean "is responsible for" rather than "has-a". In these cases I would also argue that "is responsible for" is clearer than "owns".

Comment thread manim/renderer/cairo/camera.py Outdated
)

def get_mobjects_indicating_movement(self) -> list[Mobject]:
"""Camera controls whose animation changes every projected pixel."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this docstring? Maybe give it another pass.

behackl and others added 2 commits September 8, 2026 12:17
Co-authored-by: nikolajmunk <28557236+nikolajmunk@users.noreply.github.com>
Address review feedback on camera terminology, backend controls, custom camera selection, snapshot behavior, and inset views. Preserve the earlier camera test cleanup.

@nikolajmunk nikolajmunk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything reads much more nicely now! I nevertheless went through and remarked on a few LLM-isms.

As a general note, I'm not personally convinced that "semantic (camera) view" and "owned" are useful terms to the reader.

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +137 to +140
The ``camera`` parameter allows for a different camera to be used to generate
the image. Without it, a new default :class:`.Camera` is created. Only the
mobject and its submobjects are drawn; pass ``camera=self.camera`` to use the
scene's current view.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The ``camera`` parameter allows for a different camera to be used to generate
the image. Without it, a new default :class:`.Camera` is created. Only the
mobject and its submobjects are drawn; pass ``camera=self.camera`` to use the
scene's current view.
These methods render an image from the view of a camera such that only the
chosen mobject and its submobjects are drawn; anything else in the scene is
ignored.
The ``camera`` parameter allows for a different camera to be used to generate
the image. Without it, a new default :class:`.Camera` is created. To use the
view of the current camera, pass ``camera=self.camera``.

Separate information about what is drawn and how the camera param works.

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +152 to +153
For an inset magnified view, :class:`.ZoomedScene` provides the camera and display
relationship::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not crazy about "the camera and display relationship" but I'm unsure what a better phrasing would look like.

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +215 to +217
``view.add_display_frame()`` adds the visible border around the display. To also
show the region that the secondary camera looks at, give its ``frame`` a visible
stroke and add it to the scene::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
``view.add_display_frame()`` adds the visible border around the display. To also
show the region that the secondary camera looks at, give its ``frame`` a visible
stroke and add it to the scene::
``view.add_display_frame()`` adds a visible border around the display.
To show the region which the secondary camera is currently looking at, give its
``frame`` a visible stroke and add it to the scene::

Comment thread docs/source/guides/cameras.rst Outdated
Comment on lines +222 to +225
A display initially matches its source camera's aspect ratio. Scale it uniformly to
preserve that ratio; stretching only its width or height can distort the image.
Each inset's pixel resolution follows its display size relative to the primary
camera frame.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A display initially matches its source camera's aspect ratio. Scale it uniformly to
preserve that ratio; stretching only its width or height can distort the image.
Each inset's pixel resolution follows its display size relative to the primary
camera frame.
A display initially matches the aspect ratio of its source camera. When resizing
this display, make sure it is scaled uniformly to preserve its aspect ratio;
stretching only its width or height can distort the image.
Each inset's pixel resolution follows its display size relative to the primary
camera frame.

Comment thread docs/source/guides/deep_dive.rst Outdated
Comment on lines +1005 to +1006
dimensions. The camera itself does not own pixels or a Cairo context; its constructor
accepts semantic view settings rather than pixel dimensions or frame-rate options.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The camera itself does not own pixels or a Cairo context; its constructor accepts semantic view settings rather than pixel dimensions or frame-rate options." should probably be removed; the user doesn't care what the camera doesn't do.

Comment thread docs/source/guides/deep_dive.rst Outdated
to target pixel coordinates and composited by renderer helpers.
- A :class:`.MultiCamera` describes nested camera-backed views. Their
:class:`.ImageMobjectFromCamera` display mobjects contain geometry and sampling
settings but no placeholder or live pixels. The renderer creates secondary targets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear what it means for a camera to (not) contain "placeholder or live pixels".

Comment thread docs/source/guides/deep_dive.rst Outdated
this array to the file writer.
- Then the scene asks the renderer to do its job. The only mobject that needs to
be processed at this point is the main mobject attached to the transformation.
The camera supplies its semantic view transform, while renderer-owned Cairo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps just "supplies its view transform"?

Comment thread docs/source/guides/deep_dive.rst Outdated
be processed at this point is the main mobject attached to the transformation.
The camera supplies its semantic view transform, while renderer-owned Cairo
helpers draw the current mobject state into the renderer's raster target. The
renderer reads that target and passes an owned array to the file writer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"im not owned! im not owned!!", i continue to insist as i slowly shrink and transform into a corn cob

I think it should be more clear exactly which array is being passed to the file writer; it tells the user nothing that it is "owned".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changes This PR introduces breaking changes refactor Refactor or redesign of existing code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants