ikuzakの備忘録

数学関連のツールTeX,Tikz,Manim,Geogebraなどの備忘録です。

【Manim】2次元Mobjectのブーリアン演算

例えば円とか長方形とかのMobjectに関する、集合の演算みたいなもん。
以下の4つ。

  • Difference (図形のクリップ)
  • Exclusion (いわゆる排他的論理和
  • Intersection(言わずもがな、共通部分)
  • Union(言わずもがな、和集合)

これらはboolean_opsとして、まとめられているようだ。

ぶっちゃけ、本当にやりたかったこと

本当はね、2023年の東工大の第4問の立体をManim使って描けないかなー?と思ってたんだけど。

穴の空いた立体

件の問題がこちら↓引用

xyz空間において, x軸を軸とする半径2の円柱から,  |y|\lt1かつ |z|\lt 1で表される角柱の内部を取り除いたものをAとする. また, Ax軸のまわりに45^{\circ}回転してからz軸のまわりに90^{\circ}回転したものをBとする. ABの共通部分の体積を求めよ.

まー、数学Ⅲあるあるだと思うけど、円柱を交差させたものの体積を求めさせるの、好きだよね。

この問題を見た瞬間、
(Manim...使えるか!?)
ってなったけど、結局今回のタイトルのブーリアン演算だと、できなかったんだよね。

最初の発想

まあ、大体の人は想像つくかもしれないけど、円柱に穴空けて、なおかつ、共通部分まで考えるんなら、
DifferenceIntersectionが使えるんじゃね?」
と思ってて、考えが浅はかだったと即出鼻をくじかれたのだけど。

ま、タイトルにもある通り、最初に挙げた4つ

  • Difference
  • Exclusion
  • Intersection
  • Union

たちは2次元のMobjectにしか対応してないわけで、3次元のMobjectには使えなかったわけだ。

無知ながらの予想

まず、Manimで円柱を描こうとするなら、Cylinderというクラスがあるので、それが使えるわけだけど、
描画したところで、どうやらSurfaceクラスと同様に媒介変数で処理したようなメッシュで描かれる
Resolutionを落とせば昔のゲームのポリゴンを彷彿とさせるカクカクした面になるから、「複数の面」を作ってVGroupでまとめてあるってことなんだろう。
CubePrismなんかもVGroupを継承してるんで構成要素は「複数の面」(まぁこいつらの場合は6面で固定だが)ってことなんだろう。

一方、例えばIntersectionの項の引数に注目すると、*vmobjectsとあるので、既にVGroupで複数の面をまとめているのを引数にすると、その「複数の面」の共通部分になってしまって、当然のごとく、演算結果が空になってしまうってことなんじゃないかな?
あ、ちなみにVGroupでまとめるとリスト形式として1つのVMobjectと扱われて、そのままIntersectionの引数にはできないので、 "*(アスタリスク)"でアンパックして渡さないと駄目みたい。(試そうと思ってエラーでた)

とりあえず、やってみた

試行錯誤しまくった、自分のベストエフォートは以下に示す。
立体に穴を空けるのができないんなら、実際のこの問題の解答よろしく、断面を積み重ねるような形で作成。
穴が空くように長方形を積み重ねてあげたけど、それだけだと、穴がまったくわからんかったので、
別の色で穴部分を表現することに。

from manim import *

def construct(self):
        self.set_camera_orientation(phi=60*DEGREES,theta=60*DEGREES)
        axes = (
            ThreeDAxes(
                x_range=(-3,3,1),
                y_range=(-3,3,1),
                z_range=(-3,3,1),
                x_length=6,
                y_length=6,
                z_length=6
            )
        )
        segment_num = 80
        obj = VGroup()
        hole = VGroup()
        for i in range(1,segment_num):
            k = i / segment_num * 2
            fullRect = Rectangle(
                height=np.sqrt(4-k**2)*2*axes.get_y_unit_size(),
                width=np.sqrt(4-k**2)*2*axes.get_x_unit_size()
            )
            if k < 1:
                subA = Rectangle(
                    height=np.sqrt(4-k**2)*2*axes.get_y_unit_size(),
                    width=2*axes.get_x_unit_size()
                )
                subB = Rectangle(
                    height=(np.sqrt(2)-k)*2*axes.get_y_unit_size(),
                    width=np.sqrt(4-k**2)*2*axes.get_x_unit_size(),
                )

            elif k < np.sqrt(2):
                subA = VMobject()
                subB = Rectangle(
                    height=(np.sqrt(2)-k)*2*axes.get_y_unit_size(),
                    width=np.sqrt(4-k**2)*2*axes.get_x_unit_size(),
                )
            else:
                subA = VMobject()
                subB = VMobject()

            remRect = Union(subA,subB,color=BLACK,stroke_width=1,fill_opacity=0.01)
            sliceArea = Difference(
                fullRect,remRect,
                color=BLUE,
                fill_opacity=0.01
            ).move_to(axes.c2p(0,0,k))
            remRect.move_to(axes.c2p(0,0,k))
            obj.add(sliceArea)
            hole.add(remRect)

        obj.add(
            obj.copy().rotate_about_origin(180*DEGREES,axis=X_AXIS)
        )
        hole.add(
            hole.copy().rotate_about_origin(180*DEGREES,axis=X_AXIS)
        )
        obj_edge = VGroup()
        obj_edge.add(
            ParametricFunction(
                lambda k: np.array([
                    np.sqrt(4-k**2),
                    np.sqrt(4-k**2),
                    k
                ]),
                color=BLUE_E,
                t_range=[-2,2,1/60]
            )
        )
        obj_edge.add(
            obj_edge.copy().rotate_about_origin(90*DEGREES,Z_AXIS)
        )
        obj_edge.add(
            obj_edge.copy().rotate_about_origin(180*DEGREES,Z_AXIS)
        )
        axes.add(obj,hole,obj_edge)
        self.add(axes)
        self.begin_ambient_camera_rotation(1)
        self.wait(3)
        self.move_camera(phi=90*DEGREES)
        self.wait(3)
        self.stop_ambient_camera_rotation()

youtu.be

分かりにくいからエッジもいれたけど、回転させたときに端々にギザギザ見えるのなんとかならんかな…?

時間があれば、YMMとかに挑戦して、ずんだもんが入試問題を解説する動画とか作れたら楽しそうよね。

2024.2.7 追記 あろうことか、問題の内容が脳内で変なフィルターかかったのか、 間違ったコードを書いて動画にしてた。 動画含めて訂正した。ついでに穴が分かりやすいように穴部分を黒に。
/* -----codeの行番号----- */