Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix numpy reshape error in opengl_vectorized_mobject for transforming mobjects with different number of subpaths #3278

Closed
wants to merge 4 commits into from

Conversation

glingy
Copy link

@glingy glingy commented Jul 9, 2023

Overview: What does this pull request change?

This is related to #2972. opengl_vectorized_mobject returns a python array rather than a numpy array of points if the subpath lengths are different which causes rendering errors with the OpenGL renderer. This wraps the null subpath array in a numpy array to fix the issue.

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

Currently, if any mobject is transformed into another mobject using the OpenGL renderer, the OpenGL renderer crashes with

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /usr/local/lib/python3.8/site-packages/manim/cli/render/commands.py:97 in render                 │
│                                                                                                  │
│    94 │   │   │   │   for SceneClass in scene_classes_from_file(file):                           │
│    95 │   │   │   │   │   with tempconfig({}):                                                   │
│    96 │   │   │   │   │   │   scene = SceneClass(renderer)                                       │
│ ❱  97 │   │   │   │   │   │   rerun = scene.render()                                             │
│    98 │   │   │   │   │   if rerun or config["write_all"]:                                       │
│    99 │   │   │   │   │   │   renderer.num_plays = 0                                             │
│   100 │   │   │   │   │   │   continue                                                           │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:223 in render                        │
│                                                                                                  │
│    220 │   │   """                                                                               │
│    221 │   │   self.setup()                                                                      │
│    222 │   │   try:                                                                              │
│ ❱  223 │   │   │   self.construct()                                                              │
│    224 │   │   except EndSceneEarlyException:                                                    │
│    225 │   │   │   pass                                                                          │
│    226 │   │   except RerunSceneException as e:                                                  │
│                                                                                                  │
│ /workspaces/c_overview/test.py:15 in construct                                                   │
│                                                                                                  │
│   12                                                                                             │
│   13 class Test(Scene):                                                                          │
│   14   def construct(self):                                                                      │
│ ❱ 15 │   test(self)                                                                              │
│   16                                                                                             │
│                                                                                                  │
│ /workspaces/c_overview/test.py:9 in test                                                         │
│                                                                                                  │
│    6   svg1 = InlineSVG("M0,0 h0.8 v1.2 h-0.6 l-0.2,-0.2 v-1")                                   │
│    7   print("SVG2")                                                                             │
│    8   svg2 = InlineSVG("M0,0 h0.8 v1.2 h-0.6 l-0.2,-0.2 v-1 m1,1 h0.1")                         │
│ ❱  9   self.play(                                                                                │
│   10 │   Transform(svg1, svg2),                                                                  │
│   11   )                                                                                         │
│   12                                                                                             │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:1080 in play                         │
│                                                                                                  │
│   1077 │   │   │   return                                                                        │
│   1078 │   │                                                                                     │
│   1079 │   │   start_time = self.renderer.time                                                   │
│ ❱ 1080 │   │   self.renderer.play(self, *args, **kwargs)                                         │
│   1081 │   │   run_time = self.renderer.time - start_time                                        │
│   1082 │   │   if subcaption:                                                                    │
│   1083 │   │   │   if subcaption_duration is None:                                               │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/utils/caching.py:65 in wrapper                      │
│                                                                                                  │
│   62 │   │   │   "List of the first few animation hashes of the scene: %(h)s",                   │
│   63 │   │   │   {"h": str(self.animations_hashes[:5])},                                         │
│   64 │   │   )                                                                                   │
│ ❱ 65 │   │   func(self, scene, *args, **kwargs)                                                  │
│   66 │                                                                                           │
│   67 │   return wrapper                                                                          │
│   68                                                                                             │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/renderer/opengl_renderer.py:424 in play             │
│                                                                                                  │
│   421 │   │   self.file_writer.begin_animation(not self.skip_animations)                         │
│   422 │   │                                                                                      │
│   423 │   │   scene.compile_animation_data(*args, **kwargs)                                      │
│ ❱ 424 │   │   scene.begin_animations()                                                           │
│   425 │   │   if scene.is_current_animation_frozen_frame():                                      │
│   426 │   │   │   self.update_frame(scene)                                                       │
│   427                                                                                            │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/scene/scene.py:1208 in begin_animations             │
│                                                                                                  │
│   1205 │   │   """Start the animations of the scene."""                                          │
│   1206 │   │   for animation in self.animations:                                                 │
│   1207 │   │   │   animation._setup_scene(self)                                                  │
│ ❱ 1208 │   │   │   animation.begin()                                                             │
│   1209 │   │                                                                                     │
│   1210 │   │   if config.renderer == RendererType.CAIRO:                                         │
│   1211 │   │   │   # Paint all non-moving objects onto the screen, so they don't                 │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/animation/transform.py:201 in begin                 │
│                                                                                                  │
│   198 │   │   # Note, this potentially changes the structure                                     │
│   199 │   │   # of both mobject and target_mobject                                               │
│   200 │   │   if config.renderer == RendererType.OPENGL:                                         │
│ ❱ 201 │   │   │   self.mobject.align_data_and_family(self.target_copy)                           │
│   202 │   │   else:                                                                              │
│   203 │   │   │   self.mobject.align_data(self.target_copy)                                      │
│   204 │   │   super().begin()                                                                    │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/opengl/opengl_mobject.py:2306 in            │
│ align_data_and_family                                                                            │
│                                                                                                  │
│   2303 │                                                                                         │
│   2304 │   def align_data_and_family(self, mobject):                                             │
│   2305 │   │   self.align_family(mobject)                                                        │
│ ❱ 2306 │   │   self.align_data(mobject)                                                          │
│   2307 │                                                                                         │
│   2308 │   def align_data(self, mobject):                                                        │
│   2309 │   │   # In case any data arrays get resized when aligned to shader data                 │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/opengl/opengl_mobject.py:2314 in align_data │
│                                                                                                  │
│   2311 │   │   for mob1, mob2 in zip(self.get_family(), mobject.get_family()):                   │
│   2312 │   │   │   # Separate out how points are treated so that subclasses                      │
│   2313 │   │   │   # can handle that case differently if they choose                             │
│ ❱ 2314 │   │   │   mob1.align_points(mob2)                                                       │
│   2315 │   │   │   for key in mob1.data.keys() & mob2.data.keys():                               │
│   2316 │   │   │   │   if key == "points":                                                       │
│   2317 │   │   │   │   │   continue                                                              │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/opengl/opengl_vectorized_mobject.py:1220 in │
│ align_points                                                                                     │
│                                                                                                  │
│   1217 │   │   │   sp2 = get_nth_subpath(subpaths2, n)                                           │
│   1218 │   │   │   diff1 = max(0, (len(sp2) - len(sp1)) // nppc)                                 │
│   1219 │   │   │   diff2 = max(0, (len(sp1) - len(sp2)) // nppc)                                 │
│ ❱ 1220 │   │   │   sp1 = self.insert_n_curves_to_point_list(diff1, sp1)                          │
│   1221 │   │   │   sp2 = self.insert_n_curves_to_point_list(diff2, sp2)                          │
│   1222 │   │   │   new_subpaths1.append(sp1)                                                     │
│   1223 │   │   │   new_subpaths2.append(sp2)                                                     │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/opengl/opengl_vectorized_mobject.py:1271 in │
│ insert_n_curves_to_point_list                                                                    │
│                                                                                                  │
│   1268 │   │   if len(points) == 1:                                                              │
│   1269 │   │   │   return np.repeat(points, nppc * n, 0)                                         │
│   1270 │   │                                                                                     │
│ ❱ 1271 │   │   bezier_groups = self.get_bezier_tuples_from_points(points)                        │
│   1272 │   │   norms = np.array([np.linalg.norm(bg[nppc - 1] - bg[0]) for bg in bezier_groups])  │
│   1273 │   │   total_norm = sum(norms)                                                           │
│   1274 │   │   # Calculate insertions per curve (ipc)                                            │
│                                                                                                  │
│ /usr/local/lib/python3.8/site-packages/manim/mobject/opengl/opengl_vectorized_mobject.py:717 in  │
│ get_bezier_tuples_from_points                                                                    │
│                                                                                                  │
│    714 │   │   nppc = self.n_points_per_curve                                                    │
│    715 │   │   remainder = len(points) % nppc                                                    │
│    716 │   │   points = points[: len(points) - remainder]                                        │
│ ❱  717 │   │   return points.reshape((-1, nppc, 3))                                              │
│    718 │                                                                                         │
│    719 │   def get_bezier_tuples(self):                                                          │
│    720 │   │   return self.get_bezier_tuples_from_points(self.points)                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'list' object has no attribute 'reshape'

Using this animation as an example:

from manim import *
import svgelements as se

config.renderer = RendererType.OPENGL

def InlineSVG(svg_str: str, **kwargs):
  return VMobjectFromSVGPath(se.Path(svg_str), **kwargs)

class Test(Scene):
  def construct(self):
    svg1 = InlineSVG("M0,0 h0.8")
    print("SVG2")
    svg2 = InlineSVG("M0,0 h0.8 M1,1 h0.1")
    self.play(Transform(svg1, svg2))

svg2 consists of two subpaths, but svg1 has only one subpath, so the OpenGL renderer adds a null path to svg1 to match. However, that null path was made using a python array instead of a numpy array causing a crash. I expect this error to also exist with any mobjects which include subpaths, but I couldn't find any other concrete examples easily.

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

@glingy
Copy link
Author

glingy commented Jul 9, 2023

Update, this also occurs with plain text in OpenGL:

from manim import *
config.renderer = RendererType.OPENGL

class Test(Scene):
  def construct(self):
    c = Text('c')
    o = Text('o')
    self.play(Transform(c, o))

Also has the same error caused by different numbers of subpaths and is also fixed by this PR.

@behackl behackl self-requested a review July 16, 2023 08:23
Copy link

@lukechu10 lukechu10 left a comment

Choose a reason for hiding this comment

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

This seems to be a very small and easy fix to a pretty common problem. Any chance this can be merged soon?

@JasonGrace2282
Copy link
Member

Any chance this can be merged soon?

In order to merge the CI needs to pass.

@glingy
Copy link
Author

glingy commented Feb 27, 2024

I've mostly stopped using manim for now, if you'd like to pick up the pr and fix the CI errors, go for it. It passed when I created the pr and I forgot about it.

@behackl
Copy link
Member

behackl commented Feb 27, 2024

We need a suitable test for this. The case I've tried to add (and was suggested above) does not behave consistently across OSes.

@behackl
Copy link
Member

behackl commented Jun 30, 2024

In the meantime, the underlying issue has been fixed by #3790, closing this due to being superseded.

@behackl behackl closed this Jun 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Rejected
Development

Successfully merging this pull request may close these issues.

4 participants