How to Create and Manipulate 3D Objects and Surfaces in Manim: A Complete Guide

Use ThreeDScene as your base class, instantiate primitives like Sphere or Cube from manimlib.mobject.three_dimensions, and apply transformations using standard Mobject methods while enabling depth testing for proper occlusion.

Manim (3b1b/manim) provides a dedicated framework for creating and manipulating 3D objects and surfaces within mathematical animations. The library implements a layered architecture spanning from low-level UV-mesh generation in manimlib/mobject/types/surface.py to high-level scene management via ThreeDScene, enabling precise control over parametric surfaces, textured models, and primitive geometries.

Core Architecture for 3D Rendering in Manim

ThreeDScene and Camera Configuration

The entry point for any 3D animation is ThreeDScene, defined in manimlib/scene/scene.py around line 30. This class extends the standard Scene with a ThreeDCamera (sourced from manimlib/camera/camera.py at line 259) that maintains a view matrix and projection state. When you instantiate a ThreeDScene, the constructor automatically sets always_depth_test = True, enabling hidden-face removal via depth testing so that objects occlude each other correctly based on their Z-depth.

The Surface Base Class and UV Parameterization

All 3D geometry in Manim descends from Surface, located in manimlib/mobject/types/surface.py at line 34. This base class constructs a triangular mesh from a UV-parameterization function uv_func(u, v) that maps 2D coordinates to 3D points. The implementation:

  • Samples the UV grid according to u_range, v_range, and resolution parameters
  • Computes vertex normals via cross products of tangent vectors (utilizing utilities in manimlib/utils/space_ops.py)
  • Generates triangle indices for rendering
  • Provides always_sort_to_camera for dynamic face sorting when the camera moves

ParametricSurface (line 68 in the same file) offers a convenient wrapper that accepts a lambda or callable directly, eliminating the need to subclass for custom shapes.

Creating Primitive 3D Objects

Built-in Primitives: Sphere, Cube, and Torus

Manim ships with ready-made subclasses of Surface for common geometries, all defined in manimlib/mobject/three_dimensions.py. Key implementations include:

  • Sphere (line 93): Implements spherical coordinates via uv_func, accepting radius, u_range, and v_range to control completeness (e.g., hemispheres).
  • Cube (line 70): Constructs a rectangular prism with side_length parameters, mapping UV coordinates to the six faces.
  • Torus, Cylinder, Line3D, Disk3D: Additional primitives for rings, tubes, and planar disks in 3D space.

These classes handle the init_points sampling internally, so instantiation requires only geometric parameters and styling arguments like color or shading.

Custom Parametric Surfaces

For surfaces not covered by primitives, use ParametricSurface with a custom uv_func. The function must accept two scalar parameters (u, v) and return a NumPy array [x, y, z]. Control sampling density via resolution=(u_res, v_res) and define the domain with u_range and v_range tuples.

This approach allows visualization of mathematical manifolds, graphs of functions z = f(x,y), or exotic geometries like the Möbius strip without subclassing.

Advanced 3D Surface Manipulation

Real-Time Surface Updates and Animation

Because Surface inherits from VMobject, it supports Manim’s updater pattern for dynamic behavior. Attach a function via add_updater to regenerate geometry each frame based on self.time or other variables.

For surfaces that must maintain correct face ordering as the camera rotates, invoke always_sort_to_camera(self.camera). This method, defined in Surface, re-sorts triangle indices every frame based on distance to the camera position, ensuring transparent or complex meshes render without artifacts.

Textured Surfaces and External Model Import

Manim supports image-based texturing via TexturedSurface (line 96 in manimlib/mobject/types/surface.py). This class wraps a base Surface and maps a PIL Image or image path onto the UV coordinates, enabling realistic rendering of 3D models with photographic detail.

For importing existing assets, ThreeDModel (defined in surface.py around line 19) parses OBJ files into Manim Surface objects. Provide the file path and desired height or width to scale the imported geometry to your scene units.

Practical Code Examples for 3D Manipulation

Rotating Primitives in ThreeDScene

This example demonstrates basic instantiation and animation using Cube and Sphere:

from manimlib import *

class RotatingObjects(ThreeDScene):
    def construct(self):
        # Axes

        axes = ThreeDAxes()
        self.add(axes)

        # Cube and sphere

        cube = Cube(side_length=2, color=BLUE_D).shift(LEFT * 2)
        sphere = Sphere(radius=1.2, color=RED_E).shift(RIGHT * 2)

        # Add with depth-testing automatically enabled

        self.add(cube, sphere)

        # Animate a continuous rotation

        self.begin_ambient_camera_rotation(rate=0.02)  # camera circles around

        self.play(
            Rotate(cube, angle=TAU, axis=OUT, run_time=6, rate_func=linear),
            Rotate(sphere, angle=TAU, axis=IN, run_time=6, rate_func=linear),
        )
        self.wait()

Key classes used: ThreeDScenescene.py#L30, ThreeDAxescoordinate_systems.py#L35, Cubethree_dimensions.py#L70, Spherethree_dimensions.py#L93.

Custom Parametric Surfaces (Möbius Strip)

This example uses ParametricSurface to render a non-orientable surface:

class MobiusStrip(ThreeDScene):
    def construct(self):
        axes = ThreeDAxes()
        self.add(axes)

        # UV-parameterisation of a Möbius strip

        mobius = ParametricSurface(
            lambda u, v: np.array([
                (1 + v/2 * np.cos(u/2)) * np.cos(u),
                (1 + v/2 * np.cos(u/2)) * np.sin(u),
                v/2 * np.sin(u/2)
            ]),
            u_range=(0, TAU),
            v_range=(-0.5, 0.5),
            resolution=(50, 20),
            color=GREEN,
            shading=(0.4, 0.4, 0.2),
        )
        # Align it with the axes (optional)

        mobius.shift(OUT * 0.5)

        self.add(mobius)
        self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES)
        self.begin_ambient_camera_rotation(rate=0.02)
        self.wait(5)

Key class: ParametricSurfacesurface.py#L68.

Importing Textured OBJ Models

Load external 3D assets using ThreeDModel:

class TexturedModel(ThreeDScene):
    def construct(self):
        axes = ThreeDAxes()
        self.add(axes)

        # Loads "teapot.obj" from the repository's assets folder

        model = ThreeDModel("teapot.obj", height=3)
        self.add(model)

        self.set_camera_orientation(phi=60 * DEGREES, theta=-30 * DEGREES)
        self.begin_ambient_camera_rotation(rate=0.01)
        self.wait(6)

Key class: ThreeDModelsurface.py#L19.

Real-Time Deforming Surfaces

Animate surface geometry dynamically using updaters:

class WavyPlane(ThreeDScene):
    def construct(self):
        axes = ThreeDAxes()
        self.add(axes)

        # Simple sine-wave surface

        plane = ParametricSurface(
            lambda u, v: np.array([
                u,                     # x

                v,                     # y

                0.3 * np.sin(2 * u) * np.cos(2 * v)   # z

            ]),
            u_range=(-3, 3),
            v_range=(-3, 3),
            resolution=(30, 30),
            color=YELLOW,
        )
        self.add(plane)

        # Animate the wave by moving the phase

        def update_wave(mob, dt):
            mob.shift(0)  # dummy to force refresh

            mob.become(
                ParametricSurface(
                    lambda u, v: np.array([
                        u,
                        v,
                        0.3 * np.sin(2 * (u + self.time)) * np.cos(2 * (v + self.time))
                    ]),
                    u_range=(-3, 3),
                    v_range=(-3, 3),
                    resolution=(30, 30),
                    color=YELLOW,
                )
            )
        plane.add_updater(update_wave)

        self.wait(5)
        plane.remove_updater(update_wave)

Key methods: add_updater (inherited from Mobject), self.time (exposed by Scene), ParametricSurface as above.

Summary

  • Use ThreeDScene as your base class to automatically enable depth testing and camera controls for 3D rendering.
  • Instantiate primitives like Sphere, Cube, and Torus from manimlib/mobject/three_dimensions.py for standard geometries.
  • Define custom surfaces by passing a uv_func to ParametricSurface in manimlib/mobject/types/surface.py, controlling resolution and domain ranges.
  • Manipulate surfaces using standard VMobject transformations; add updaters for real-time deformation and always_sort_to_camera for dynamic face ordering.
  • Import textures and models via TexturedSurface and ThreeDModel to integrate external assets into your 3D scenes.

Frequently Asked Questions

What is the difference between ThreeDScene and Scene?

ThreeDScene extends the base Scene class specifically for three-dimensional rendering. According to the source in manimlib/scene/scene.py, it automatically instantiates a ThreeDCamera (sourced from manimlib/camera/camera.py) and sets always_depth_test = True. This enables hidden-face removal and camera orientation controls like set_camera_orientation, which are not available in the standard 2D Scene class.

How do I enable depth testing for proper 3D occlusion?

When using ThreeDScene, depth testing is enabled by default via the always_depth_test attribute. If you manually add 3D objects to a regular Scene, you must call object.apply_depth_test() or set object.always_depth_test = True to ensure faces are sorted by depth. For dynamic camera movements, call surface.always_sort_to_camera(self.camera) on your Surface objects to re-sort triangle indices every frame based on camera distance, preventing rendering artifacts.

Can I import custom OBJ files into Manim?

Yes. The ThreeDModel class in manimlib/mobject/types/surface.py (around line 19) parses Wavefront OBJ files into Manim Surface objects. Instantiate it with the file path and optional height or width parameters to scale the geometry: model = ThreeDModel("model.obj", height=3). The class handles vertex parsing and normal generation, integrating the external mesh into Manim's rendering pipeline with full support for transformations and depth testing.

How do I animate a surface changing shape over time?

Use Manim's updater pattern to regenerate surface geometry each frame. Because Surface inherits from VMobject, you can attach a function via add_updater that calls mob.become() with a new ParametricSurface instance calculated using self.time. For example, to create a waving plane, define an updater function that recalculates the Z-coordinate based on self.time and applies it to the surface. Remove the updater with remove_updater when the animation completes to stop the regeneration cycle.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →