Skip to content

Commit bc16210

Browse files
authored
Add OpenGL Renderer (#1075)
Adds and OpenGL renderer, OpenGL-enabled Mobjects, and a Scene.embed() method.
2 parents edcdeb1 + a549c2a commit bc16210

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+7365
-308
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
run: |
5151
sudo apt update
5252
sudo apt install -y ffmpeg
53-
sudo apt-get -y install texlive texlive-latex-extra texlive-fonts-extra texlive-latex-recommended texlive-science texlive-fonts-extra tipa
53+
sudo apt-get -y install texlive texlive-latex-extra texlive-fonts-extra texlive-latex-recommended texlive-science texlive-fonts-extra tipa python-opengl libpango1.0-dev
5454
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
5555
5656
- name: Install system dependencies (MacOS)
@@ -118,7 +118,7 @@ jobs:
118118
run: poetry run pytest
119119

120120
- name: Run module doctests
121-
run: poetry run pytest --doctest-modules manim
121+
run: poetry run pytest --doctest-modules --ignore-glob="*opengl*" manim
122122

123123
- name: Run doctests in rst files
124124
run: cd docs && pip install -r requirements.txt && poetry run make doctest O=-tskip-manim

docs/source/tutorials/configuration.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ A list of all config options
357357
'preview', 'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
358358
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
359359
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
360-
'upto_animation_number', 'use_webgl_renderer', 'verbosity', 'video_dir',
361-
'webgl_renderer_path', 'webgl_updater_fps', 'write_all', 'write_to_movie']
360+
'upto_animation_number', 'use_opengl_renderer', 'use_webgl_renderer', 'verbosity',
361+
'video_dir', 'webgl_renderer_path', 'webgl_updater_fps', 'write_all',
362+
'write_to_movie']
362363

363364

364365
A list of all CLI flags
@@ -430,6 +431,8 @@ A list of all CLI flags
430431
-n FROM_ANIMATION_NUMBER, --from_animation_number FROM_ANIMATION_NUMBER
431432
Start rendering at the specified animation index, instead of the first animation. If you pass in two comma separated values, e.g. '3,6', it will end
432433
the rendering at the second value
434+
--use_opengl_renderer
435+
Render animations using the OpenGL renderer
433436
--use_webgl_renderer Render animations using the WebGL frontend
434437
--webgl_renderer_path WEBGL_RENDERER_PATH
435438
Path to the WebGL frontend
69.1 KB
Loading
108 KB
Loading

example_scenes/opengl.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from manim import *
2+
from manim.opengl import *
3+
import os
4+
from pathlib import Path
5+
6+
7+
# Copied from https://3b1b.github.io/manim/getting_started/example_scenes.html#surfaceexample.
8+
# Lines that do not yet work with the Community Version are commented.
9+
10+
11+
class InteractiveDevelopment(Scene):
12+
def construct(self):
13+
circle = OpenGLCircle()
14+
circle.set_fill(BLUE, opacity=0.5)
15+
circle.set_stroke(BLUE_E, width=4)
16+
square = OpenGLSquare()
17+
18+
self.play(ShowCreation(square))
19+
self.wait()
20+
21+
# This opens an iPython termnial where you can keep writing
22+
# lines as if they were part of this construct method.
23+
# In particular, 'square', 'circle' and 'self' will all be
24+
# part of the local namespace in that terminal.
25+
self.embed()
26+
27+
# Try copying and pasting some of the lines below into
28+
# the interactive shell
29+
self.play(ReplacementTransform(square, circle))
30+
self.wait()
31+
self.play(circle.animate.stretch(4, 0))
32+
self.play(Rotate(circle, 90 * DEGREES))
33+
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
34+
35+
# text = Text("""
36+
# In general, using the interactive shell
37+
# is very helpful when developing new scenes
38+
# """)
39+
# self.play(Write(text))
40+
41+
# # In the interactive shell, you can just type
42+
# # play, add, remove, clear, wait, save_state and restore,
43+
# # instead of self.play, self.add, self.remove, etc.
44+
45+
# # To interact with the window, type touch(). You can then
46+
# # scroll in the window, or zoom by holding down 'z' while scrolling,
47+
# # and change camera perspective by holding down 'd' while moving
48+
# # the mouse. Press 'r' to reset to the standard camera position.
49+
# # Press 'q' to stop interacting with the window and go back to
50+
# # typing new commands into the shell.
51+
52+
# # In principle you can customize a scene to be responsive to
53+
# # mouse and keyboard interactions
54+
# always(circle.move_to, self.mouse_point)
55+
56+
57+
class SurfaceExample(Scene):
58+
def construct(self):
59+
# surface_text = Text("For 3d scenes, try using surfaces")
60+
# surface_text.fix_in_frame()
61+
# surface_text.to_edge(UP)
62+
# self.add(surface_text)
63+
# self.wait(0.1)
64+
65+
torus1 = OpenGLTorus(r1=1, r2=1)
66+
torus2 = OpenGLTorus(r1=3, r2=1)
67+
sphere = OpenGLSphere(radius=3, resolution=torus1.resolution)
68+
# You can texture a surface with up to two images, which will
69+
# be interpreted as the side towards the light, and away from
70+
# the light. These can be either urls, or paths to a local file
71+
# in whatever you've set as the image directory in
72+
# the custom_config.yml file
73+
74+
script_location = Path(os.path.realpath(__file__)).parent
75+
day_texture = (
76+
script_location / "assets" / "1280px-Whole_world_-_land_and_oceans.jpg"
77+
)
78+
night_texture = script_location / "assets" / "1280px-The_earth_at_night.jpg"
79+
80+
surfaces = [
81+
OpenGLTexturedSurface(surface, day_texture, night_texture)
82+
for surface in [sphere, torus1, torus2]
83+
]
84+
85+
for mob in surfaces:
86+
mob.shift(IN)
87+
mob.mesh = OpenGLSurfaceMesh(mob)
88+
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
89+
90+
# Set perspective
91+
frame = self.renderer.camera
92+
frame.set_euler_angles(
93+
theta=-30 * DEGREES,
94+
phi=70 * DEGREES,
95+
)
96+
97+
surface = surfaces[0]
98+
99+
self.play(
100+
FadeIn(surface),
101+
ShowCreation(surface.mesh, lag_ratio=0.01, run_time=3),
102+
)
103+
for mob in surfaces:
104+
mob.add(mob.mesh)
105+
surface.save_state()
106+
self.play(Rotate(surface, PI / 2), run_time=2)
107+
for mob in surfaces[1:]:
108+
mob.rotate(PI / 2)
109+
110+
self.play(Transform(surface, surfaces[1]), run_time=3)
111+
112+
self.play(
113+
Transform(surface, surfaces[2]),
114+
# Move camera frame during the transition
115+
frame.animate.increment_phi(-10 * DEGREES),
116+
frame.animate.increment_theta(-20 * DEGREES),
117+
run_time=3,
118+
)
119+
# Add ambient rotation
120+
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
121+
122+
# Play around with where the light is
123+
# light_text = Text("You can move around the light source")
124+
# light_text.move_to(surface_text)
125+
# light_text.fix_in_frame()
126+
127+
# self.play(FadeTransform(surface_text, light_text))
128+
light = self.camera.light_source
129+
self.add(light)
130+
light.save_state()
131+
self.play(light.animate.move_to(3 * IN), run_time=5)
132+
self.play(light.animate.shift(10 * OUT), run_time=5)
133+
134+
# drag_text = Text("Try moving the mouse while pressing d or s")
135+
# drag_text.move_to(light_text)
136+
# drag_text.fix_in_frame()

manim/__main__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,18 @@ def main():
7373
else:
7474
config.digest_args(args)
7575
input_file = config.get_dir("input_file")
76-
if config["use_webgl_renderer"]:
76+
77+
if config["use_opengl_renderer"]:
78+
from manim.renderer.opengl_renderer import OpenGLRenderer
79+
80+
for SceneClass in scene_classes_from_file(input_file):
81+
try:
82+
renderer = OpenGLRenderer()
83+
scene = SceneClass(renderer)
84+
scene.render()
85+
except Exception:
86+
console.print_exception()
87+
elif config["use_webgl_renderer"]:
7788
try:
7889
from manim.grpc.impl import frame_server_impl
7990

manim/_config/default.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ webgl_renderer_path =
9393
# --webgl_updater_fps
9494
webgl_updater_fps = 15
9595

96+
# --use_opengl_renderer
97+
use_opengl_renderer = False
98+
9699
# If the -t (--transparent) flag is used, these will be replaced with the
97100
# values specified in the [TRANSPARENT] section later in this file.
98101
png_mode = RGB

manim/_config/main_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@ def _parse_args_no_subcmd(args: list) -> argparse.Namespace:
410410
"the rendering at the second value",
411411
)
412412

413+
parser.add_argument(
414+
"--use_opengl_renderer",
415+
help="Render animations using the OpenGL renderer",
416+
action="store_const",
417+
const=True,
418+
)
419+
413420
parser.add_argument(
414421
"--use_webgl_renderer",
415422
help="Render animations using the WebGL frontend",

manim/_config/utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class MyScene(Scene):
287287
"tex_template_file",
288288
"text_dir",
289289
"upto_animation_number",
290+
"use_opengl_renderer",
290291
"use_webgl_renderer",
291292
"webgl_updater_fps",
292293
"verbosity",
@@ -510,6 +511,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig":
510511
"disable_caching",
511512
"flush_cache",
512513
"custom_folders",
514+
"use_opengl_renderer",
513515
"use_webgl_renderer",
514516
]:
515517
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))
@@ -629,6 +631,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
629631
"scene_names",
630632
"verbosity",
631633
"background_color",
634+
"use_opengl_renderer",
632635
"use_webgl_renderer",
633636
"webgl_updater_fps",
634637
]:
@@ -705,6 +708,11 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
705708
if args.tex_template:
706709
self.tex_template = TexTemplateFromFile(tex_filename=args.tex_template)
707710

711+
if self.use_opengl_renderer:
712+
if getattr(args, "write_to_movie") is None:
713+
# --write_to_movie was not passed on the command line, so don't generate video.
714+
self["write_to_movie"] = False
715+
708716
return self
709717

710718
def digest_file(self, filename: str) -> "ManimConfig":
@@ -1046,9 +1054,20 @@ def dry_run(self, val: bool) -> None:
10461054
"save_last_frame, save_pngs, or save_as_gif)"
10471055
)
10481056

1057+
@property
1058+
def use_opengl_renderer(self):
1059+
"""Whether or not to use the OpenGL renderer."""
1060+
return self._d["use_opengl_renderer"]
1061+
1062+
@use_opengl_renderer.setter
1063+
def use_opengl_renderer(self, val: bool) -> None:
1064+
self._d["use_opengl_renderer"] = val
1065+
if val:
1066+
self["disable_caching"] = True
1067+
10491068
@property
10501069
def use_webgl_renderer(self):
1051-
"""Whether to use WebGL renderer or not (default)."""
1070+
"""Whether or not to use WebGL renderer."""
10521071
return self._d["use_webgl_renderer"]
10531072

10541073
@use_webgl_renderer.setter

manim/animation/animation.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
from manim.scene.scene import Scene
1515

1616
from .. import logger
17-
from ..mobject.mobject import Mobject, _AnimationBuilder
17+
from ..mobject import mobject
18+
from ..mobject import opengl_mobject
19+
from ..mobject.mobject import Mobject
1820
from ..utils.rate_functions import smooth
21+
from ..mobject.opengl_mobject import OpenGLMobject
1922

2023
DEFAULT_ANIMATION_RUN_TIME: float = 1.0
2124
DEFAULT_ANIMATION_LAG_RATIO: float = 0.0
@@ -61,7 +64,9 @@ def __init__(
6164
def _typecheck_input(self, mobject: Mobject) -> None:
6265
if mobject is None:
6366
logger.debug("creating dummy animation")
64-
elif not isinstance(mobject, Mobject):
67+
elif not isinstance(mobject, Mobject) and not isinstance(
68+
mobject, OpenGLMobject
69+
):
6570
raise TypeError("Animation only works on Mobjects")
6671

6772
def __str__(self) -> str:
@@ -195,7 +200,9 @@ def is_remover(self) -> bool:
195200
return self.remover
196201

197202

198-
def prepare_animation(anim: Union["Animation", "_AnimationBuilder"]) -> "Animation":
203+
def prepare_animation(
204+
anim: Union["Animation", "mobject._AnimationBuilder"]
205+
) -> "Animation":
199206
r"""Returns either an unchanged animation, or the animation built
200207
from a passed animation factory.
201208
@@ -222,7 +229,10 @@ def prepare_animation(anim: Union["Animation", "_AnimationBuilder"]) -> "Animati
222229
TypeError: Object 42 cannot be converted to an animation
223230
224231
"""
225-
if isinstance(anim, _AnimationBuilder):
232+
if isinstance(anim, mobject._AnimationBuilder):
233+
return anim.build()
234+
235+
if isinstance(anim, opengl_mobject._AnimationBuilder):
226236
return anim.build()
227237

228238
if isinstance(anim, Animation):

0 commit comments

Comments
 (0)