Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
083271a
Load OpenGL class only if OpenGL renderer is used
mayanksuman Apr 4, 2026
69317ef
Added wgpu-py as a dependency
mayanksuman Apr 4, 2026
ef9d044
Structural interface shared by all Manim renderers.
mayanksuman Apr 4, 2026
b80ee61
Basic WebGPU renderer for fill, stroke and surface
mayanksuman Apr 4, 2026
55102f7
Batch GPU buffers support
mayanksuman Apr 4, 2026
699c0e5
Implemented efficient Fill shader based on Slug Algorithm
mayanksuman Apr 5, 2026
95e00f7
Added Analytic SDF Anti-Aliasing for strokes
mayanksuman Apr 5, 2026
bd56ca9
3D scene and animation is working in WebGPU Renderer
mayanksuman Apr 7, 2026
f39cb68
Added prospective projection support to WebGPUCamera
mayanksuman Apr 7, 2026
18f1c2a
Correct z-placement of 2D object by WebGPU Renderer
mayanksuman Apr 7, 2026
5d7e1e1
Combine fill and stroke for planar object.
mayanksuman Apr 8, 2026
da69060
Optimization: Surface shader and mesh shader is now combined into one…
mayanksuman Apr 8, 2026
bde0b2c
Removed unused WebGPU Shaders
mayanksuman Apr 8, 2026
fec7583
Added uv.lock file to .gitignore
mayanksuman Apr 8, 2026
bc8463e
WebGPU Renderer can not render ManimBanner scene
mayanksuman Apr 8, 2026
1804441
Static Frame Optimization
mayanksuman Apr 8, 2026
40d9241
Added image support to WebGPU Renderer
mayanksuman Apr 9, 2026
1f3d511
Added TrueDot support to WebGPU and Cairo Renderers
mayanksuman Apr 9, 2026
9d77452
Added support for per surface diffuse and specular strength
mayanksuman Apr 9, 2026
7c76c35
Added different kind of light source for WebGPU renderer
mayanksuman Apr 10, 2026
03e1889
Added support for ImageMobjectFromCamera and ZoomedScene in WebGPU re…
mayanksuman Apr 10, 2026
0ddf1d3
Enabled per surface reflection parameters in WebGPU render
mayanksuman Apr 10, 2026
d77196d
Added support for complex SVG in WebGPU renderer
mayanksuman Apr 10, 2026
4545e0a
Better support for gradient in webGPU VMobject Renderer
mayanksuman Apr 11, 2026
f80c4b8
Export Light Classes from Manim
mayanksuman Apr 11, 2026
2b5714c
Color in Surface.__init__() is not ignored now
mayanksuman Apr 11, 2026
a6c7281
Fixed test_plot_suface with Surface Color Support
mayanksuman Apr 11, 2026
1c6e2ad
Added configurable MSAA in WebGPU renderer
mayanksuman Apr 11, 2026
2476ec0
WebGPU Renderer Window is now interactive
mayanksuman Apr 11, 2026
69cba9e
File watcher / rerun() is enabled in WebGPU Renderer Interactive Mode
mayanksuman Apr 11, 2026
4e1e190
WebGPU Renderer window now respect window configuration
mayanksuman Apr 12, 2026
5fb7949
Optimization of WebGPU renderer for interactive mode
mayanksuman Apr 14, 2026
9f06a1f
Merge branch 'main' of github.com:mayanksuman/manim
mayanksuman Aug 27, 2026
0a9259d
Merge branch 'main' into webgpu_render
mayanksuman Aug 27, 2026
2aeeec1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,9 @@ dist/
/media_dir.txt
# ^TODO: Remove the need for this with a proper config file

#uv lock file
uv.lock
*.lock

# Ignore the built dependencies
third_party/*
1 change: 1 addition & 0 deletions manim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
from .mobject.text.tex_mobject import *
from .mobject.text.text_mobject import *
from .mobject.text.typst_mobject import *
from .mobject.three_d.light_source import *
from .mobject.three_d.polyhedra import *
from .mobject.three_d.three_d_utils import *
from .mobject.three_d.three_dimensions import *
Expand Down
5 changes: 4 additions & 1 deletion manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,10 @@ def digest_args(self, args: argparse.Namespace) -> Self:
if args.tex_template:
self.tex_template = TexTemplate.from_file(args.tex_template)

if self.renderer == RendererType.OPENGL and args.write_to_movie is None:
if (
self.renderer in (RendererType.OPENGL, RendererType.WEBGPU)
and args.write_to_movie is None
):
# --write_to_movie was not passed on the command line, so don't generate video.
self["write_to_movie"] = False

Expand Down
12 changes: 12 additions & 0 deletions manim/cli/render/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
except Exception:
error_console.print_exception()
sys.exit(1)
elif config.renderer == RendererType.WEBGPU:
from manim.renderer.webgpu.webgpu_renderer import WebGPURenderer

renderer = WebGPURenderer()
for SceneClass in scene_classes_from_file(file):
try:
with tempconfig({}):
scene = SceneClass(renderer)
scene.render()
except Exception:
error_console.print_exception()
sys.exit(1)
else:
for SceneClass in scene_classes_from_file(file):
try:
Expand Down
1 change: 1 addition & 0 deletions manim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ class RendererType(Enum):

CAIRO = "cairo" #: A renderer based on the cairo backend.
OPENGL = "opengl" #: An OpenGL-based renderer.
WEBGPU = "webgpu" #: A WebGPU-based renderer (wgpu-py).


class LineJointType(Enum):
Expand Down
9 changes: 5 additions & 4 deletions manim/mobject/opengl/opengl_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
from typing import Any

from manim import config
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurface
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject

from ...constants import RendererType

Expand All @@ -26,6 +22,11 @@ def __new__(
mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
) -> type:
if config.renderer == RendererType.OPENGL:
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurface
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject

# Must check class names to prevent
# cyclic importing.
base_names_to_opengl: dict[str, type] = {
Expand Down
3 changes: 3 additions & 0 deletions manim/mobject/svg/svg_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ def apply_style_to_mobject(mob: VMobject, shape: se.GraphicObject) -> VMobject:
fill_color=shape.fill.hexrgb,
fill_opacity=shape.fill.opacity,
)
# Extract fill-rule for WebGPU renderer (0=nonzero, 1=evenodd).
fill_rule_str = shape.values.get("fill-rule", "nonzero")
mob.fill_rule = 1 if fill_rule_str == "evenodd" else 0
return mob

def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
Expand Down
1 change: 1 addition & 0 deletions manim/mobject/three_d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.. autosummary::
:toctree: ../reference

~light_source
~polyhedra
~three_d_utils
~three_dimensions
Expand Down
193 changes: 193 additions & 0 deletions manim/mobject/three_d/dot_cloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Point-cloud mobject for WebGPU TrueDot rendering.

``PointDot`` is a single dot rendered as a 3-D lit sphere.
``DotCloud3D`` is an N-point cloud rendered as N lit spheres.

These classes are ``Mobject``-based (not ``OpenGLMobject``-based) so they
work transparently with both Cairo scenes (skipped silently) and WebGPU
scenes (routed to the TrueDot pipeline).
"""

from __future__ import annotations

__all__ = ["DotCloud3D", "PointDot"]

from typing import Any

import numpy as np

from manim.constants import ORIGIN
from manim.mobject.mobject import Mobject
from manim.typing import Point3DLike
from manim.utils.color import WHITE, ParsableManimColor, color_to_rgba


class DotCloud3D(Mobject):
"""A cloud of points, each rendered as a lit sphere by the WebGPU renderer.

In Cairo / OpenGL renderers, ``DotCloud3D`` objects are silently ignored
(they produce no geometry for those pipelines).

Parameters
----------
points
Array of world-space positions, shape (N, 3).
color
Base colour of all dots (can be overridden per-point via ``set_rgbas``).
radius
World-space radius of each sphere in scene units.
gloss
Specular shininess (Cairo-style): 0 = matte, 1 = very shiny.
shadow
Diffuse darkening strength: 0 = no shadow, 1 = full Lambert shading.
"""

def __init__(
self,
points: np.ndarray | list | None = None,
color: ParsableManimColor = WHITE,
radius: float = 0.05,
gloss: float = 0.3,
shadow: float = 0.3,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
pts = (
np.zeros((0, 3), dtype=np.float32)
if points is None
else np.asarray(points, dtype=np.float32)
)
if pts.ndim == 1:
pts = pts.reshape(1, 3)
self._cloud_points: np.ndarray = pts.astype(np.float32)
self._rgbas: np.ndarray = np.tile(
np.asarray(color_to_rgba(color), dtype=np.float32), (max(len(pts), 1), 1)
)
self.dot_radius: float = float(radius)
self.gloss: float = float(gloss)
self.shadow: float = float(shadow)
# Set Mobject.points to the cloud positions so bounding-box helpers work.
if len(pts) > 0:
self.set_points(pts)

# ------------------------------------------------------------------
# Cloud-specific API
# ------------------------------------------------------------------

def get_cloud_points(self) -> np.ndarray:
"""Return the (N, 3) float32 array of dot centres."""
return self._cloud_points

def set_cloud_points(self, points: np.ndarray) -> DotCloud3D:
pts = np.asarray(points, dtype=np.float32)
if pts.ndim == 1:
pts = pts.reshape(1, 3)
self._cloud_points = pts
if len(pts) > 0:
self.set_points(pts)
return self

def get_rgbas(self) -> np.ndarray:
"""Return the (N, 4) float32 RGBA array for all dots."""
return self._rgbas

def set_rgbas(self, rgbas: np.ndarray) -> DotCloud3D:
self._rgbas = np.asarray(rgbas, dtype=np.float32)
return self

def set_color(self, color: ParsableManimColor, family: bool = True) -> DotCloud3D: # type: ignore[override]
rgba = np.asarray(color_to_rgba(color), dtype=np.float32)
self._rgbas = np.tile(rgba, (max(len(self._cloud_points), 1), 1))
if family:
for sub in self.submobjects:
if isinstance(sub, DotCloud3D):
sub.set_color(color, family=False)
return self

def set_opacity(self, opacity: float, family: bool = True) -> DotCloud3D: # type: ignore[override]
self._rgbas[:, 3] = float(opacity)
if family:
for sub in self.submobjects:
if isinstance(sub, DotCloud3D):
sub.set_opacity(opacity, family=False)
return self

# ------------------------------------------------------------------
# Animation support — required Mobject overrides
# ------------------------------------------------------------------

def align_points_with_larger(self, larger_mobject: Mobject) -> None:
"""Tile _cloud_points and _rgbas to match the size of *larger_mobject*."""
if not isinstance(larger_mobject, DotCloud3D):
return
n_target = len(larger_mobject._cloud_points)
n_self = len(self._cloud_points)
if n_self == 0 or n_self >= n_target:
return
reps = -(-n_target // n_self) # ceiling division
self._cloud_points = np.tile(self._cloud_points, (reps, 1))[:n_target]
self._rgbas = np.tile(self._rgbas, (reps, 1))[:n_target]
self.set_points(self._cloud_points)

def interpolate_color(
self, mobject1: Mobject, mobject2: Mobject, alpha: float
) -> None:
"""Linearly interpolate _rgbas between *mobject1* and *mobject2*."""
if not isinstance(mobject1, DotCloud3D) or not isinstance(mobject2, DotCloud3D):
return
self._rgbas = ((1 - alpha) * mobject1._rgbas + alpha * mobject2._rgbas).astype(
np.float32
)

def interpolate(
self,
mobject1: Mobject,
mobject2: Mobject,
alpha: float,
path_func: Any = None,
) -> DotCloud3D:
"""Interpolate position and colour; keep _cloud_points in sync with points."""
from manim.utils.bezier import interpolate as lerp

if path_func is None:
path_func = lerp
super().interpolate(mobject1, mobject2, alpha, path_func)
# Mobject.interpolate writes into self.points; mirror that into _cloud_points.
self._cloud_points = np.asarray(self.points, dtype=np.float32)
return self


class PointDot(DotCloud3D):
"""A single dot at *center* rendered as a lit sphere by the WebGPU renderer.

Parameters
----------
center
World-space position of the dot.
color
Base colour.
radius
World-space radius of the sphere in scene units.
gloss
Specular shininess: 0 = matte, 1 = very shiny.
shadow
Diffuse darkening: 0 = flat, 1 = full Lambert shading.
"""

def __init__(
self,
center: Point3DLike = ORIGIN,
color: ParsableManimColor = WHITE,
radius: float = 0.05,
gloss: float = 0.3,
shadow: float = 0.3,
**kwargs: Any,
) -> None:
super().__init__(
points=np.asarray(center, dtype=np.float32).reshape(1, 3),
color=color,
radius=radius,
gloss=gloss,
shadow=shadow,
**kwargs,
)
Loading
Loading