Skip to content

Commit 99a5a5e

Browse files
authored
Merge pull request #1113 from eulertour/moderngl-interaction
Add interaction to the OpenGL renderer
2 parents 7760675 + 3c4709d commit 99a5a5e

File tree

4 files changed

+132
-33
lines changed

4 files changed

+132
-33
lines changed

manim/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,5 @@
177177

178178
DEFAULT_QUALITY: str = "high_quality"
179179
DEFAULT_QUALITY_SHORT = QUALITIES[DEFAULT_QUALITY]["flag"]
180+
SHIFT_VALUE = 65505
181+
CTRL_VALUE = 65507

manim/renderer/opengl_renderer.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,10 @@ def refresh_rotation_matrix(self):
9191
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
9292
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
9393
)
94-
self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(
95-
quat
96-
)
94+
self.inverse_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
9795

9896
def rotate(self, angle, axis=OUT, **kwargs):
99-
curr_rot_T = self.inverse_camera_rotation_matrix
97+
curr_rot_T = self.inverse_rotation_matrix
10098
added_rot_T = rotation_matrix_transpose(angle, axis)
10199
new_rot_T = np.dot(curr_rot_T, added_rot_T)
102100
Fz = new_rot_T[2]
@@ -182,22 +180,33 @@ class OpenGLRenderer:
182180
def __init__(self):
183181
# Measured in pixel widths, used for vector graphics
184182
self.anti_alias_width = 1.5
185-
186183
self.num_plays = 0
187184
self.skip_animations = False
188-
189185
self.camera = OpenGLCamera()
186+
self.pressed_keys = set()
187+
188+
# Initialize shader map.
189+
self.id_to_shader_program = {}
190+
191+
# Initialize texture map.
192+
self.path_to_texture_id = {}
190193

194+
def init_scene(self, scene):
195+
self.partial_movie_files = []
196+
self.file_writer = SceneFileWriter(
197+
self,
198+
scene.__class__.__name__,
199+
)
200+
self.scene = scene
191201
if config["preview"]:
192-
self.window = Window()
202+
self.window = Window(self)
193203
self.context = self.window.ctx
194204
self.frame_buffer_object = self.context.detect_framebuffer()
195205
else:
196206
self.window = None
197207
self.context = moderngl.create_standalone_context()
198208
self.frame_buffer_object = self.get_frame_buffer_object(self.context, 0)
199209
self.frame_buffer_object.use()
200-
201210
self.context.enable(moderngl.BLEND)
202211
self.context.blend_func = (
203212
moderngl.SRC_ALPHA,
@@ -206,14 +215,6 @@ def __init__(self):
206215
moderngl.ONE,
207216
)
208217

209-
# Initialize shader map.
210-
self.id_to_shader_program = {}
211-
212-
# Initialize texture map.
213-
self.path_to_texture_id = {}
214-
215-
self.partial_movie_files = []
216-
217218
def update_depth_test(self, context, shader_wrapper):
218219
if shader_wrapper.depth_test:
219220
self.context.enable(moderngl.DEPTH_TEST)
@@ -223,24 +224,24 @@ def update_depth_test(self, context, shader_wrapper):
223224
def get_pixel_shape(self):
224225
return self.frame_buffer_object.viewport[2:4]
225226

226-
def refresh_perspective_uniforms(self, camera_frame):
227+
def refresh_perspective_uniforms(self, camera):
227228
pw, ph = self.get_pixel_shape()
228-
fw, fh = camera_frame.get_shape()
229+
fw, fh = camera.get_shape()
229230
# TODO, this should probably be a mobject uniform, with
230231
# the camera taking care of the conversion factor
231232
anti_alias_width = self.anti_alias_width / (ph / fh)
232233
# Orient light
233-
rotation = camera_frame.inverse_camera_rotation_matrix
234-
light_pos = camera_frame.light_source.get_location()
234+
rotation = camera.inverse_rotation_matrix
235+
light_pos = camera.light_source.get_location()
235236
light_pos = np.dot(rotation, light_pos)
236237

237238
self.perspective_uniforms = {
238-
"frame_shape": camera_frame.get_shape(),
239+
"frame_shape": camera.get_shape(),
239240
"anti_alias_width": anti_alias_width,
240-
"camera_center": tuple(camera_frame.get_center()),
241+
"camera_center": tuple(camera.get_center()),
241242
"camera_rotation": tuple(np.array(rotation).T.flatten()),
242243
"light_source_position": tuple(light_pos),
243-
"focal_distance": camera_frame.get_focal_distance(),
244+
"focal_distance": camera.get_focal_distance(),
244245
}
245246

246247
def render_mobjects(self, mobs):
@@ -347,12 +348,6 @@ def set_shader_uniforms(self, shader, shader_wrapper):
347348
except KeyError:
348349
pass
349350

350-
def init_scene(self, scene):
351-
self.file_writer = SceneFileWriter(
352-
self,
353-
scene.__class__.__name__,
354-
)
355-
356351
def play(self, scene, *args, **kwargs):
357352
if len(args) == 0:
358353
logger.warning("Called Scene.play with no animations")
@@ -389,7 +384,6 @@ def update_frame():
389384
if self.window is not None:
390385
self.window.swap_buffers()
391386
while self.animation_elapsed_time < frame_offset:
392-
# TODO: Just sleep?
393387
update_frame()
394388
self.window.swap_buffers()
395389

@@ -429,3 +423,15 @@ def get_raw_frame_buffer_object_data(self, dtype="f1"):
429423
dtype=dtype,
430424
)
431425
return ret
426+
427+
# Returns offset from the bottom left corner in pixels.
428+
def pixel_coords_to_space_coords(self, px, py, relative=False):
429+
pw, ph = config["pixel_width"], config["pixel_height"]
430+
fw, fh = config["frame_width"], config["frame_height"]
431+
fc = self.camera.get_center()
432+
if relative:
433+
return 2 * np.array([px / pw, py / ph, 0])
434+
else:
435+
# Only scale wrt one axis
436+
scale = fh / ph
437+
return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0])

manim/renderer/opengl_renderer_window.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,50 @@ class Window(PygletWindow):
1111
vsync = True
1212
cursor = True
1313

14-
def __init__(self, size=None, **kwargs):
14+
def __init__(self, renderer, size=None, **kwargs):
1515
if size is None:
1616
size = (config["pixel_width"], config["pixel_height"])
1717
super().__init__(size=size)
1818

19-
self.pressed_keys = set()
20-
2119
self.title = f"Manim Community {__version__}"
2220
self.size = size
21+
self.renderer = renderer
2322

2423
mglw.activate_context(window=self)
2524
self.timer = Timer()
2625
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
2726
self.timer.start()
2827

2928
self.swap_buffers()
29+
30+
# Delegate event handling to scene.
31+
def on_mouse_motion(self, x, y, dx, dy):
32+
super().on_mouse_motion(x, y, dx, dy)
33+
point = self.renderer.pixel_coords_to_space_coords(x, y)
34+
d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True)
35+
self.renderer.scene.on_mouse_motion(point, d_point)
36+
37+
def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float):
38+
super().on_mouse_scroll(x, y, x_offset, y_offset)
39+
point = self.renderer.pixel_coords_to_space_coords(x, y)
40+
offset = self.renderer.pixel_coords_to_space_coords(
41+
x_offset, y_offset, relative=True
42+
)
43+
self.renderer.scene.on_mouse_scroll(point, offset)
44+
45+
def on_key_press(self, symbol, modifiers):
46+
self.renderer.pressed_keys.add(symbol)
47+
super().on_key_press(symbol, modifiers)
48+
self.renderer.scene.on_key_press(symbol, modifiers)
49+
50+
def on_key_release(self, symbol, modifiers):
51+
if symbol in self.renderer.pressed_keys:
52+
self.renderer.pressed_keys.remove(symbol)
53+
super().on_key_release(symbol, modifiers)
54+
self.renderer.scene.on_key_release(symbol, modifiers)
55+
56+
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
57+
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
58+
point = self.renderer.pixel_coords_to_space_coords(x, y)
59+
d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True)
60+
self.renderer.scene.on_mouse_drag(point, d_point, buttons, modifiers)

manim/scene/scene.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
from ..constants import *
2323
from ..container import Container
2424
from ..mobject.mobject import Mobject, _AnimationBuilder
25+
from ..mobject.opengl_mobject import OpenGLPoint
2526
from ..utils.iterables import list_update, list_difference_update
2627
from ..utils.family import extract_mobject_family_members
2728
from ..renderer.cairo_renderer import CairoRenderer
2829
from ..utils.exceptions import EndSceneEarlyException
2930
from ..utils.family_ops import restructure_list_to_exclude_certain_family_members
31+
from ..utils.space_ops import rotate_vector
3032

3133

3234
class Scene(Container):
@@ -81,6 +83,11 @@ def __init__(
8183
self.duration = None
8284
self.last_t = None
8385

86+
if config["use_opengl_renderer"]:
87+
# Items associated with interaction
88+
self.mouse_point = OpenGLPoint()
89+
self.mouse_drag_point = OpenGLPoint()
90+
8491
if renderer is None:
8592
self.renderer = CairoRenderer(
8693
camera_class=self.camera_class,
@@ -873,6 +880,16 @@ def play_internal(self, skip_rendering=False):
873880
self.update_mobjects(0)
874881
self.renderer.static_image = None
875882

883+
def interact(self):
884+
self.quit_interaction = False
885+
while not (self.renderer.window.is_closing or self.quit_interaction):
886+
self.renderer.animation_start_time = 0
887+
dt = 1 / config["frame_rate"]
888+
self.renderer.render(self, dt, self.moving_mobjects)
889+
self.update_mobjects(dt)
890+
if self.renderer.window.is_closing:
891+
self.renderer.window.destroy()
892+
876893
def embed(self):
877894
if not config["preview"]:
878895
logger.warning("Called embed() while no preview window is available.")
@@ -903,6 +920,7 @@ def embed(self):
903920
"wait",
904921
"add",
905922
"remove",
923+
"interact",
906924
# "clear",
907925
# "save_state",
908926
# "restore",
@@ -950,3 +968,45 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
950968
return
951969
time = self.renderer.time + time_offset
952970
self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs)
971+
972+
def on_mouse_motion(self, point, d_point):
973+
self.mouse_point.move_to(point)
974+
if SHIFT_VALUE in self.renderer.pressed_keys:
975+
shift = -d_point
976+
shift[0] *= self.camera.get_width() / 2
977+
shift[1] *= self.camera.get_height() / 2
978+
transform = self.camera.inverse_rotation_matrix
979+
shift = np.dot(np.transpose(transform), shift)
980+
self.camera.shift(shift)
981+
982+
def on_mouse_scroll(self, point, offset):
983+
if CTRL_VALUE in self.renderer.pressed_keys:
984+
factor = 1 + np.arctan(-20 * offset[1])
985+
self.camera.scale(factor, about_point=point)
986+
987+
transform = self.camera.inverse_rotation_matrix
988+
shift = np.dot(np.transpose(transform), offset)
989+
if SHIFT_VALUE in self.renderer.pressed_keys:
990+
self.camera.shift(20.0 * np.array(rotate_vector(shift, PI / 2)))
991+
else:
992+
self.camera.shift(20.0 * shift)
993+
994+
def on_key_press(self, symbol, modifiers):
995+
try:
996+
char = chr(symbol)
997+
except OverflowError:
998+
logger.warning("The value of the pressed key is too large.")
999+
return
1000+
1001+
if char == "r":
1002+
self.camera.to_default_state()
1003+
elif char == "q":
1004+
self.quit_interaction = True
1005+
1006+
def on_key_release(self, symbol, modifiers):
1007+
pass
1008+
1009+
def on_mouse_drag(self, point, d_point, buttons, modifiers):
1010+
self.mouse_drag_point.move_to(point)
1011+
self.camera.increment_theta(-d_point[0])
1012+
self.camera.increment_phi(d_point[1])

0 commit comments

Comments
 (0)