Blenderを改造する

はじめに

この記事は東京大学工学部電気電子工学科・電子情報工学科で開講されている「電気電子情報実験・演習第二 /大規模ソフトウェアを手探る」の成果報告レポートです。

当実験における「ソースを眺めて全容を把握できるわけがない程大きなソフトウェアをいかに扱い,必要なだけ動作を理解し,変更する」という趣旨に基づき、オープンソースの3DCGソフトであるBlenderに対してアプローチをかけました。記事内では、実際にBlenderを手探った過程や結果を記述しています。

実行環境

作業はすべて下に示すような環境で行いました。

Blenderについて

まずBlenderについて簡単に説明します。Blenderとはオープンソースのソフトウェアの一つであり、3DCG作成から2Dアニメーション作成、さらには動画編集などの機能があります。非常に多機能ですが軽量であり、ライセンス料が無料であることも特徴の一つです。

ソースコードは主にc++,Pythonで記述されており、総行数は約400万行に及びます。c++は主に論理を、Pythonは主にUIを担当しており、Pythonに関しては、ビルド後の実行ファイルの入ったフォルダにファイルが存在するものはその変更をビルドなしで動作に反映させることができます。

Blenderではこれを利用して様々なPythonアドオンが開発されていますが、今回はPythonのみならずc++にも手を加えていくため、基本的にソースコードを編集していくことになります。

ビルドの方法

Developer向けのページを参考にしてビルドを行いました。

  1. まず、ビルドに必要なgcc、g++のバージョンを確認する。

    Building Blenderのページによると、コンパイルに必要なバージョンはgcc-11なので、手元の環境が条件を満たしているかを確認します。 手元のターミナルでgcc --versiong++ --versionを実行します。gcc (Ubuntu 11.4.0-2ubuntu1~20.04) 11.4.0などと11.0.0より大きければ問題なしです。小さい場合は、以下の手順を行います。

    gcc-11とg++-11をインストールします。

     sudo apt install gcc-11 g++-11
    

    次に、ターミナルのgccとg++バージョンを変更します。そのために、update-alternativesを使います。

     sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 11 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11
    

    他のバージョンもあれば、以下のように登録します。

     sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10
    

    登録しているリストは、以下のコマンドで確認できます。

     sudo update-alternatives --list gcc
    

    例えば、私の環境だと以下のようになります。

     /usr/bin/gcc-11
     /usr/bin/gcc-8
     /usr/bin/gcc-9
    

    続いて、バージョンの切り替えを行います。

     sudo update-alternatives --config gcc
    

    これを実行すると、

     alternative gcc (/usr/bin/gcc を提供) には 3 個の選択肢があります。
    
       選択肢    パス                              優先度  状態
     ------------------------------------------------------------
     * 0            /usr/bin/gcc-11                      120       自動モード
       1            /usr/bin/gcc-11                      120       手動モード
       2            /usr/bin/gcc-8                       80        手動モード
       3            /usr/bin/gcc-9                       90        手動モード
    
     現在の選択 [*] を保持するには <Enter>、さもなければ選択肢の番号のキーを押してください: 
    

    と出てくるので、操作してgcc-11を選びます。

    参考にしたページ

  2. Developer向けのページに従いビルドする。

    ここで、Developer向けのページに従いビルドします。デバッグできるようにするために、Update and Buildの項目でmake updateの後に、makeではなくmake debugをするようにしましょう。

  3. 起動

    ターミナル上で、~/blender-git/build_linux_debug/bin/blenderを実行することで起動できます。 vscodeでdebugを行うのに、このページを参考にしました。 デバッグ設定の3.のjsonファイルは、以下のようにしました。

     {
     // Use IntelliSense to learn about possible attributes.
     // Hover to view descriptions of existing attributes.
     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "configurations": [
         {
             "name": "C/C++: debug",
             "type": "cppdbg",
             "request": "launch",
             "program": "/home/denjo/blender-git/build_linux_debug/bin/blender", // ビルドしたバイナリへの絶対パス
             "stopAtEntry": true,            // 実行後すぐに一旦止める
             "cwd": "${workspaceFolder}",    // 実行時の作業ディレクトリ
             "MIMode": "gdb" 
         }
     ]
     }
    

    あらかじめソースコードブレークポイントを打っておくと、GUIで操作したときにその場所で止まり、デバッグを行うことができます。

取り組んだ課題

今回私たちは以下の三つの課題に取り組み、それぞれ機能を実装しました。

  • ヒストリー機能の追加
  • 多角形描画機能の追加
  • Mac,Linux版におけるクリップボードからの画像ペースト機能の追加

以下でこれらについて、現状を説明します。

  • ヒストリー機能

    現状Blenderでは、トップバーに存在する[Edit]->[Undo history]と選択することで操作履歴すなわちヒストリーが確認でき、並んだ履歴から任意の時点を選択することでその時点での状態に回帰することができます。

    しかし同種の機能を備えた他ソフトのように常時表示させておくことはできず、「ヒストリーを見ながら作業」ということができません。そしていざ戻る際にも上記のような数段の手順を踏まなければならない仕様となっています。

  • 多角形描画機能

    例えば六角形を描画したい時、標準では円を描画した後にその円の頂点数を(デフォルトなら32から)6に減らすことで描くことになります。幾何学的なモデルを作成する際などには、いちいちこの手順を踏むのが面倒くさいと感じることがあります。

  • クリップボードからの画像ペースト機能

    WindowsBlenderでは実装済みの機能ですが、Mac,Linux版では現状クリップボードから画像をBlender上にペーストすることができません。例えば3Dモデルに画像のテクスチャを貼り付けたい時、ネットのフリー画像をコピーしてきてもペーストが出来ないとなると不便に感じるでしょう。

    これはBlenderの開発者コミュニティに挙げられていたissueの一つです。

ヒストリー機能

操作履歴のメニューをサイドバーに追加することにしました。 またPythonから操作履歴にアクセスする方法がなかったので、全てC++で実装することにしました。

既存の操作履歴のソースを探る

既存の操作履歴がどのように実装されているか確認してみます。 VSCodeBlenderソースコードのフォルダを開き、Shift+Ctrl+Fを押します。 左上に表示された検索欄にundo_historyと入力します。

いくつか候補のファイルが表示されます。既存の操作履歴はトップバーに実装されているのでspace_topbar.ccundo_history_draw_menuが怪しそうです。 undo_history_draw_menuの中身を覗いてみると

static void undo_history_draw_menu(const bContext *C, Menu *menu)
{
  wmWindowManager *wm = CTX_wm_manager(C);
  if (wm->undo_stack == nullptr) {
    return;
  }

  int undo_step_count = 0;
  int undo_step_count_all = 0;
  LISTBASE_FOREACH_BACKWARD (UndoStep *, us, &wm->undo_stack->steps) {
    undo_step_count_all += 1;
    if (us->skip) {
      continue;
    }
    undo_step_count += 1;
  }

  uiLayout *split = uiLayoutSplit(menu->layout, 0.0f, false);
  uiLayout *column = nullptr;

  const int col_size = 20 + (undo_step_count / 12);

  undo_step_count = 0;

  /* Reverse the order so the most recent state is first in the menu. */
  int i = undo_step_count_all - 1;
  for (UndoStep *us = static_cast<UndoStep *>(wm->undo_stack->steps.last); us; us = us->prev, i--)
  {
    if (us->skip) {
      continue;
    }
    if (!(undo_step_count % col_size)) {
      column = uiLayoutColumn(split, false);
    }
    const bool is_active = (us == wm->undo_stack->step_active);
    uiLayout *row = uiLayoutRow(column, false);
    uiLayoutSetEnabled(row, !is_active);
    uiItemIntO(row,
               CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, us->name),
               is_active ? ICON_LAYER_ACTIVE : ICON_NONE,
               "ED_OT_undo_history",
               "item",
               i);
    undo_step_count += 1;
  }
}

for文でUndoStepという型を回していることから、ここに操作履歴の情報があると思われます。デバッガでus->nameを見てみると、確かにこのfor文でメニューに操作履歴を並べていることがわかるでしょう。

C++のサイドバー実装方法

C++でサイドバーがどのように実装されているか探ってみます。Blenderのサイドバーのアイテムというタブを押すと、トランスフォームというパネルが開きます。 というわけでVSCodeShift+Ctrl+Fで開く検索欄にtransform_panelと入力します。するとview3d_buttons.ccMOD_uvwarp.ccというファイルがヒットします。 MOD_uvwarp.ccのほうにはtransform_panel_drawといういかにもな関数がありますが、中身を見てみると

static void transform_panel_draw(const bContext * /*C*/, Panel *panel)
{
  uiLayout *layout = panel->layout;

  PointerRNA *ptr = modifier_panel_get_property_pointers(panel, nullptr);

  uiLayoutSetPropSep(layout, true);

  uiItemR(layout, ptr, "offset", UI_ITEM_NONE, nullptr, ICON_NONE);
  uiItemR(layout, ptr, "scale", UI_ITEM_NONE, nullptr, ICON_NONE);
  uiItemR(layout, ptr, "rotation", UI_ITEM_NONE, nullptr, ICON_NONE);
}

という具合で

トランスフォームのパネルにある「寸法」がなく、実際には使われていない、もしくは関係がない関数だと考えられます。 そこでview3d_buttons.ccのほうを見てみます。

transformでファイル内を検索してひとつずつ見ていくとview3d_panel_transformという関数が見つかります。

static void view3d_panel_transform(const bContext *C, Panel *panel)
{
  uiBlock *block;
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  BKE_view_layer_synced_ensure(scene, view_layer);
  Object *ob = BKE_view_layer_active_object_get(view_layer);
  Object *obedit = OBEDIT_FROM_OBACT(ob);
  uiLayout *col;

  block = uiLayoutGetBlock(panel->layout);
  UI_block_func_handle_set(block, do_view3d_region_buttons, nullptr);

  col = uiLayoutColumn(panel->layout, false);

  if (ob == obedit) {
    if (ob->type == OB_ARMATURE) {
      v3d_editarmature_buts(col, ob);
    }
    else if (ob->type == OB_MBALL) {
      v3d_editmetaball_buts(col, ob);
    }
    else {
      View3D *v3d = CTX_wm_view3d(C);
      v3d_editvertex_buts(col, v3d, ob, FLT_MAX);
    }
  }
  else if (ob->mode & OB_MODE_POSE) {
    v3d_posearmature_buts(col, ob);
  }
  else {
    PointerRNA obptr = RNA_id_pointer_create(&ob->id);
    v3d_transform_butsR(col, &obptr);

    /* Dimensions and editmode are mostly the same check. */
    if (OB_TYPE_SUPPORT_EDITMODE(ob->type) || ELEM(ob->type, OB_VOLUME, OB_CURVES, OB_POINTCLOUD))
    {
      View3D *v3d = CTX_wm_view3d(C);
      v3d_object_dimension_buts(nullptr, col, v3d, ob);
    }
  }
}

こちらは内部で様々な関数を呼んでおり、最後の方にはdimension(寸法)の文字も見えます。 念の為もう少し確認すると、関数内で呼ばれているv3d_transform_butsRのコード中にlocation, rotation. scaleという文字列が見つかります。 これからこれがパネルの描画関数だろうと推測されます。

パネルの描画関数を定義するだけではパネルを表示させることはできないのでどこかで関数を登録する必要があるはずです。 そこでファイル内をregisterで検索するとview3d_buttons_register関数が見つかります。

void view3d_buttons_register(ARegionType *art)
{
  PanelType *pt;

  pt = static_cast<PanelType *>(MEM_callocN(sizeof(PanelType), "spacetype view3d panel object"));
  STRNCPY(pt->idname, "VIEW3D_PT_transform");
  STRNCPY(pt->label, N_("Transform")); /* XXX C panels unavailable through RNA bpy.types! */
  STRNCPY(pt->category, "Item");
  STRNCPY(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = view3d_panel_transform;
  pt->poll = view3d_panel_transform_poll;
  BLI_addtail(&art->paneltypes, pt);

  pt = static_cast<PanelType *>(MEM_callocN(sizeof(PanelType), "spacetype view3d panel vgroup"));
  STRNCPY(pt->idname, "VIEW3D_PT_vgroup");
  STRNCPY(pt->label, N_("Vertex Weights")); /* XXX C panels unavailable through RNA bpy.types! */
  STRNCPY(pt->category, "Item");
  STRNCPY(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = view3d_panel_vgroup;
  pt->poll = view3d_panel_vgroup_poll;
  BLI_addtail(&art->paneltypes, pt);

  MenuType *mt;

  mt = static_cast<MenuType *>(MEM_callocN(sizeof(MenuType), "spacetype view3d menu collections"));
  STRNCPY(mt->idname, "VIEW3D_MT_collection");
  STRNCPY(mt->label, N_("Collection"));
  STRNCPY(mt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  mt->draw = hide_collections_menu_draw;
  WM_menutype_add(mt);
}

pt->drawに描画関数を入れ、pt->pollにも自身で関数を定義して入れればよさそうです。

実装

以上のことを踏まえると実装すべきものは描画用関数とpt->pollに入れる関数、それらを登録する処理であることがわかります。

描画用関数は以下のような実装となりました。

static void hex_panel_undo_history(const bContext *C, Panel *panel)
{
  uiBlock *block;
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  BKE_view_layer_synced_ensure(scene, view_layer);
  Object *ob = BKE_view_layer_active_object_get(view_layer);
  Object *obedit = OBEDIT_FROM_OBACT(ob);
  uiLayout *col;

  block = uiLayoutGetBlock(panel->layout);
  UI_block_func_handle_set(block, do_view3d_region_buttons, nullptr);

  col = uiLayoutColumn(panel->layout, false);
  wmWindowManager *wm = CTX_wm_manager(C);
  if (wm->undo_stack == nullptr) {
    return;
  }
  int undo_step_count = 0;
  int undo_step_count_all = 0;
  LISTBASE_FOREACH_BACKWARD (UndoStep *, us, &wm->undo_stack->steps) {
    undo_step_count_all += 1;
    if (us->skip) {
      continue;
    }
    undo_step_count += 1;
  }

  uiLayout *split = uiLayoutSplit(panel->layout, 0.0f, false);
  uiLayout *column = nullptr;

  const int col_size = 20 + (undo_step_count / 12);

  undo_step_count = 0;

  /* Reverse the order so the most recent state is first in the menu. */
  int i = undo_step_count_all - 1;

  for (UndoStep *us = static_cast<UndoStep *>(wm->undo_stack->steps.last); us; us = us->prev, i--)
  {
    if (us->skip) {
      continue;
    }
    if (!(undo_step_count % col_size)) {
      column = uiLayoutColumn(split, false);
    } 

    const bool is_active = (us == wm->undo_stack->step_active);
    uiLayout *row = uiLayoutRow(col, false);
    uiLayoutSetEnabled(row, !is_active);
   // char buff[100];
   // sprintf(buff, "HEX_ED_OT_undo_history{}", i);
    uiItemIntO(row,
               CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, us->name),
               is_active ? ICON_LAYER_ACTIVE : ICON_NONE,
               "ED_OT_undo_history",
               "item",
               i);
    undo_step_count += 1;
  }
}

トランスフォームのパネルのソースコードに操作履歴のソースコードを切り貼りしただけです。 サイドバー、既存の操作履歴メニューのいずれもがuiLayoutという構造体を操作してUIを作成していることから、既存のコードをコピーすれば動くだろうと考えました。

pt->pollの関数は以下のようになりました。

static bool hex_panel_undo_history_poll(const bContext *C, PanelType * /*pt*/)
{
  const Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  BKE_view_layer_synced_ensure(scene, view_layer);
  return (BKE_view_layer_active_base_get(view_layer) != nullptr);
}

メニューの内容に関わるものではないと判断し、view3d_panel_transform_pollのコードそのままです。

view3d_buttons_registerに以下のコードを追加し、これら2つの関数を登録します。

  pt = static_cast<PanelType *>(MEM_callocN(sizeof(PanelType), "spacetype view3d panel object"));
  STRNCPY(pt->idname, "VIEW3D_PT_undo_history");
  STRNCPY(pt->label, N_("Undo History")); /* XXX C panels unavailable through RNA bpy.types! */
  STRNCPY(pt->category, "Undo History");
  STRNCPY(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
  pt->draw = hex_panel_undo_history;
  pt->poll = hex_panel_undo_history_poll;
  BLI_addtail(&art->paneltypes, pt);

以上で操作履歴のメニューの実装は終わりです。

多角形描画機能

ここでは、3Dビュー上に六角形などの多角形をワンボタンで描画することができるような機能の実装に関して記述します。この機能に関してはc++よりもPythonのファイルを編集する割合が大きくなっており、後述しますが機能だけならPythonのみで実装が可能です。

Pythonアドオン開発について

はじめに、BlenderにおけるPythonアドオン開発の主な方法について簡単に説明します。

そもそもBlenderはその内部構造に多様な構造体を使用しており、それらを用いずゼロから実用的な機能を実装するのはPythonからではほぼ不可能と言っていいでしょう。すなわちPythonアドオン開発では、既に用意されている関数(実体はc++で記述されている)を組み合わせて所望の機能の実現を目指すことになります。

今回の多角形描画機能の実装においては、円を作成する既存の関数を使っていくことになります。円作成関数の引数を調整することで、数種類の多角形をワンボタンで追加できるようにしていく方針です。


機能の実装

今回は以下の5ステップに分けて解説していきたいと思います。

  1. Operatorクラスを追加

  2. クラスの中身を既存関数で記述し、登録

  3. Menuクラスを追加

  4. クラスの中身を記述し、登録

  5. UIに組み込む

初出の言葉が出てきていますが、都度解説しますのでご心配なく。

1. Operatorクラスを追加

まずOperatorクラスとはなんぞやと言う話ですが、これは何らかの操作を実現するボタンについてのクラスです。少し分かりにくいので例を示します。以下は3DビューのUIを定義しているspace_view3d.pyから、メッシュを追加するボタン(Object Mode左上のAdd->Mesh)の中身を定義しているクラスです。

class VIEW3D_MT_mesh_add(Menu):
    bl_idname = "VIEW3D_MT_mesh_add"
    bl_label = "Mesh"

    def draw(self, _context):
        layout = self.layout

        layout.operator_context = 'INVOKE_REGION_WIN'

        layout.operator("mesh.primitive_plane_add", text="Plane", icon='MESH_PLANE')
        layout.operator("mesh.primitive_cube_add", text="Cube", icon='MESH_CUBE')
        layout.operator("mesh.primitive_circle_add", text="Circle", icon='MESH_CIRCLE')
        layout.operator("mesh.primitive_uv_sphere_add", text="UV Sphere", icon='MESH_UVSPHERE')
        layout.operator("mesh.primitive_ico_sphere_add", text="Ico Sphere", icon='MESH_ICOSPHERE')
        layout.operator("mesh.primitive_cylinder_add", text="Cylinder", icon='MESH_CYLINDER')
        layout.operator("mesh.primitive_cone_add", text="Cone", icon='MESH_CONE')
        layout.operator("mesh.primitive_torus_add", text="Torus", icon='MESH_TORUS')

        layout.separator()

        layout.operator("mesh.primitive_grid_add", text="Grid", icon='MESH_GRID')
        layout.operator("mesh.primitive_monkey_add", text="Monkey", icon='MESH_MONKEY')

        layout.template_node_operator_asset_menu_items(catalog_path="Add")

Meshを選択した時に出てくるボタンがそれぞれlayout.operator()という関数で呼ばれています。第一引数はそれぞれのOperator固有の名前(idname)で、第二引数はボタンの名前、第三引数はアイコンを指定しています(後述)。

ここにあるOperatorは全てc++で実装されているため実体を参照することはしませんが、OperatorはPythonで書くこともできます。アドオン開発におけるOperatorの記述方法はこちらをご参考ください。このページを見ればOperatorの中身について大体理解ができると思います。

つまり自前でOperatorを用意し、このクラスのようなUI表示をしているクラスに追記すれば簡単に新しいボタンが追加できます。今回は以下の8つのOperatorクラスをspace_view3d.py内に作成しました。

  • TriangleAdd (三角形)
  • PentagonAdd (五角形)
  • HexagonAdd (六角形)
  • OctagonAdd (八角形)
  • TriangleAdd_fill
  • PentagonAdd_fill
  • HexagonAdd_fill
  • OctagonAdd_fill

_fillのついたOperatorは内部の埋まった多角形を描画します。

2. クラスの中身を既存関数で記述し、登録

以下がHexagonAddクラスの実装です。

class HexagonAdd(bpy.types.Operator):
    bl_idname = "mesh.add_hexagon"
    bl_label = "add hexagon"
    bl_description = "Construct a hexagon(6) mesh"
    
    def execute(self,context):
        bpy.ops.mesh.primitive_circle_add(vertices=6, radius=1.0, fill_type='NOTHING', calc_uvs=True, enter_editmode=False, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(1.0, 1.0, 1.0))
        return {"FINISHED"}
    
bpy.utils.register_class(HexagonAdd)

内容はいたって単純で、execute関数内ではボタンを押した時に実行される内容=六頂点の円を描画する既存関数を呼び出しています。最後の一文でユーティリティにこのクラスを登録することで、既にあったOperatorと同様の使い方ができるようになります。

3. Menuクラスを追加

MenuクラスはOperatorクラス並びに下位のMenuクラスを束ねるクラスのようなものです。例としてspace_view3d.pyから、3Dビューに何かを追加するボタン(Object Mode左上のAdd)の中身を定義しているMenuクラスの一部です。

class VIEW3D_MT_add(Menu):
    bl_label = "Add"
    bl_translation_context = i18n_contexts.operator_default

    def draw(self, context):
        layout = self.layout

        # NOTE: don't use 'EXEC_SCREEN' or operators won't get the `v3d` context.

        # NOTE: was `EXEC_AREA`, but this context does not have the `rv3d`, which prevents
        #       "align_view" to work on first call (see #32719).
        layout.operator_context = 'EXEC_REGION_WIN'

        # layout.operator_menu_enum("object.mesh_add", "type", text="Mesh", icon='OUTLINER_OB_MESH')
        layout.menu("VIEW3D_MT_mesh_add", icon='OUTLINER_OB_MESH')

        # layout.operator_menu_enum("object.curve_add", "type", text="Curve", icon='OUTLINER_OB_CURVE')
        layout.menu("VIEW3D_MT_curve_add", icon='OUTLINER_OB_CURVE')
        # layout.operator_menu_enum("object.surface_add", "type", text="Surface", icon='OUTLINER_OB_SURFACE')
        layout.menu("VIEW3D_MT_surface_add", icon='OUTLINER_OB_SURFACE')
        layout.menu("VIEW3D_MT_metaball_add", text="Metaball", icon='OUTLINER_OB_META')
        layout.operator("object.text_add", text="Text", icon='OUTLINER_OB_FONT')

draw関数内でlayout.menu()という文によって下位のMenuを呼び出したり、layout.operator()でOperatorを呼び出したりしています。察しの良い方なら気づいているかもしれませんが、先程例に挙げたVIEW3D_MT_mesh_add()もMenuであり、このMenuクラスで呼び出されています。

目指すのはMeshなどと同列にPolygon(多角形追加ボタン)を表示させることです。すなわちVIEW3D_MT_mesh_add()を参考にしてMenuクラスを追加します。

4. クラスの中身を記述し、登録

以下が先程作成したOperatorをまとめるMenuの実装です。

class VIEW3D_MT_polygon_add(Menu):
    bl_idname = "VIEW3D_MT_polygon_add"
    bl_label = "polygon"
    
    def draw(self,context):
        layout = self.layout
        
        layout.operator(TriangleAdd.bl_idname,text="Triangle",icon="MESH_TRIANGLE")
        layout.operator(PentagonAdd.bl_idname,text="Pentagon",icon="MESH_PENTAGON")
        layout.operator(HexagonAdd.bl_idname,text="Hexagon",icon="MESH_HEXAGON")
        layout.operator(OctagonAdd.bl_idname,text="Octagon",icon="MESH_OCTAGON")
        
        layout.separator()
        
        layout.operator(TriangleAdd_fill.bl_idname,text="Triangle(filled)",icon="MESH_TRIANGLE")
        layout.operator(PentagonAdd_fill.bl_idname,text="Pentagon(filled)",icon="MESH_PENTAGON")
        layout.operator(HexagonAdd_fill.bl_idname,text="Hexagon(filled)",icon="MESH_HEXAGON")
        layout.operator(OctagonAdd_fill.bl_idname,text="Octagon(filled)",icon="MESH_OCTAGON")
        
        
bpy.utils.register_class(VIEW3D_MT_polygon_add)

8つのOperatorを呼び出しているだけの単純な構造で、定義後に登録するところは先程のOperatorと同じです。

5. UIに組み込む

あとは先程例に出したVIEW3D_MT_add()内の適切な位置に以下の一文を追加することで、機能の実装が完了です。

layout.menu("VIEW3D_MT_polygon_add",text="Polygon",icon="MESH_PENTAGON")

ここまでの実装は全てPythonファイルに対して行ったものなので、ソースコードをビルドし直さなくともBlender上から実現可能です。このように簡単な機能の追加であればPythonで気軽に実現できるのがBlenderの強みと言えるかもしれません。


アイコンの追加

機能自体は実装されましたが、新規の機能なので他のボタンにあるようなアイコンはありません。無くても実用上問題はないですが、せっかくなので新たに作成し、先程実装したボタンに追加したいと思います。アイコン追加の方法はこちらを参考にしました。

まず、Blender上に現れるアイコンは全てblender_icons.svgというベクター形式のファイルに描かれています。一枚の画像がいくつもの領域に分かれていて、その領域ごとに切り出された部分が一つのアイコンになる、といった形です。  

blender_icons.svg

上の画像はinkscapeでそのファイルを開いた時のものです。たくさんのアイコンの行列の中にいくつか空いている区域があり、そこにアイコンになるデザインを描き込みます。画像では中央右あたりに三角形〜八角形のデザインを追加しています。

アイコンを描いたら、そのアイコンを使用できるように登録する必要があります。UI_icons.hhというヘッダファイルで全てのアイコンが登録されているので、自分が先程描き込んだ区域に該当する数字の行を変更し任意の名前をつけます。

DEF_ICON_BLANK(674)
DEF_ICON(RIGID_BODY)
DEF_ICON(RIGID_BODY_CONSTRAINT)
DEF_ICON_BLANK(677)
//
DEF_ICON(MESH_TRIANGLE)
DEF_ICON(MESH_PENTAGON)
DEF_ICON(MESH_HEXAGON)
DEF_ICON(MESH_OCTAGON)
///
DEF_ICON_BLANK(682)
DEF_ICON(IMAGE_PLANE)
DEF_ICON(IMAGE_BACKGROUND)
DEF_ICON(IMAGE_REFERENCE)

最後に、ビルドを行う前に make icons を実行することでアイコンのデータが更新され、自作のアイコンが登録されます。UI_icons.hhでつけた名前をlayout.operator()layout.menu()で指定することで、自作のアイコンを反映させることができます。

これで下の画像のように、まるで元からあった機能かのようなボタンを追加することに成功しました。

polygon menuの追加に成功!

クリップボードから画像ペースト

Windowsでどのように実現されているかを確かめる。

Linuxの環境で、クリップボードから画像をペーストすることを可能にするために、まずWindows環境での画像ペーストの実装を参考にしました。このページによると、 scripts/startup/bl_ui/space_image.pyで、環境がWindowsの時にペーストボタンとコピーボタンを表示するように書かれています。 ここで、ペーストボタンを押すと、"image.clipboard_paste"が呼ばれるようなので、Linuxの場合は以下のように追加すれば、ボタンを表示できます。

    if sys.platform[:3] == "win":
        layout.operator("image.clipboard_copy", text="Copy")
        layout.operator("image.clipboard_paste", text="Paste")
        layout.separator()

    #以下を追加
    if sys.platform[:3] =="lin":
        layout.operator("image.clipboard_paste", text="Paste")
        layout.separator()

適切な場所にブレークポイントを打てば、これによって、Linux環境でもDebugできるようになります。 Windows環境での画像ペーストの実装をみると、intern/ghost/intern/GHOST_C-api.ccで、以下のような関数があります。

uint *GHOST_getClipboardImage(int *r_width, int *r_height)
{
  const GHOST_ISystem *system = GHOST_ISystem::getSystem();
  return system->getClipboardImage(r_width, r_height);
}

return system->getClipboardImage(r_width, r_height);の行にブレークポイントを追加し、step inすると、Debugがしやすかったです。 ここで、systemは、現在のシステムオブジェクトへのポインタであり、systemオブジェクトのgetClipboardImageメソッドを呼び出しています。 Linux環境でもgetClipboardImage(r_width, r_height)の関数を定義する必要があります。 また、intern/ghost/intern/GHOST_SystemX11.ccにあるGHOST_TCapabilityFlag GHOST_SystemX11のClipboardImagesのフラグに関する行をコメントアウトしておきました。

GHOST_TCapabilityFlag GHOST_SystemX11::getCapabilities() const
{
  return GHOST_TCapabilityFlag(GHOST_CAPABILITY_FLAG_ALL &
                               ~(
                                   /* No support yet for desktop sampling. */
                                   GHOST_kCapabilityDesktopSample |
                                   /* No support yet for image copy/paste. */
                                   //GHOST_kCapabilityClipboardImages | //ここをコメントアウト
                                   /* No support yet for IME input methods. */
                                   GHOST_kCapabilityInputIME));
}

GHOST_SystemX11.ccに変更を加えて、画像ペースト機能を追加する。

  1. GHOST_SystemX11.hhに必要な関数を定義する。

    GHOST_SystemX11.ccのヘッダファイルであるGHOST_SystemX11.hhに、実装する関数を定義します。getClipboardImageを、class GHOST_SystemX11 : public GHOST_System内に実装します。clipboard_img_propgetClipboardImageの実装に際して、定義した関数です。

    void clipboard_img_prop(Atom sel, Atom target, uchar **img, unsigned long *len,unsigned int *context) const;

    uint *getClipboardImage(int *r_width, int *r_height) const;

    GHOST_TSuccess hasClipboardImage(void) const;
  1. getClipboardImage関数の仕様を理解する。

    getClipbboardImage関数の実装に際して、GHOST_SystemWin32.ccのgetClipboardImageを参考にしました。

    GHOST_SystemWin32.ccのgetClipboardImage関数の内容は

    • 引数は、int *r_width, int *r_height
    • クリップボードのフォーマットに応じて、各々の関数を呼ぶ
    • r_widthr_heightの指すアドレスの値を更新
    • 画像データをrgbaとしてreturnする。

    です。これをGHOST_SystemX11.ccでも実装すれば良いことがわかりました。 WindowsLinuxでは、クリップボードの仕組みが違うので、クリップボードからデータをもらう方法は、独自で実装する必要がありますが、それ以外の部分はWindowsと同じ方法で実装します。

  2. X11のSelectionの仕組みを理解する。

    XとはX Window Systemのことで、UNIX系で標準的に用いられており、GUIを構築するのに使用されています。Xはクライアント・サーバーシステムを採用しており、ユーザーが利用しているコンピュータ上にXサーバーが存在し、アプリケーションがクライアントです。

    ここでは、Selectionと呼ばれる機能について補足しておきます。例えば、あるアプリケーションAで、textをコピーしたり、画像をコピーしたりしたとします。そのウィンドウを所有しているクライアント、すなわち、アプリケーションAが、selectionを所有していることを知らせるリクエストをXサーバーに送ります。そして、ユーザが別のウィンドウにペーストしようとすると、そのウィンドウを所有するクライアントをアプリケーションBとすると、アプリケーションBが、Aによって選択されたtextや画像を取得するためのプロトコルを開始します。このプロトコルは、Xサーバーに誰がselectionを所有しているかを尋ねることから始まり、2つのクライアントがサーバーを経由してデータの転送を行います。

    • Bがウィンドウのプロパティを指定して、selectionのデータ変換を要求
    • XサーバーがAにSelectionRequestを送る
    • AはChangePropertyを送ることによって、指定されたウィンドウのプロパティにデータを送る
    • Aは、Bにselectionが送られたことを知らせるSelectionNotifyを送るためにXサーバーにリクエストを送る。
    • Bは、一つ以上のGetPropertyをXサーバーに送ることで、ウィンドウのプロパティにselectionを読み取ることができる。
  3. GHOST_SystemX11.ccに実装する

    Blenderで、どのようにtextのコピーペーストが実装されているかは、GHOST_SystemX11.ccのGHOST_SystemX11::getClipboardで確認できます。 実装の際には、X11: How does “the” clipboard work?を参考にしました。 このページのProgram 2では、クリップボードからtextをUTF-8で取得するプログラムが書かれています。データ型を指定する部分はXInternAtom(dpy, "UTF8_STRING", False);なので、XInternAtom(dpy, "image/png",False)にすることで、pngを指定できました。

実装したgetClipboardImage関数は以下です。

    uint *GHOST_SystemX11::getClipboardImage(int *r_width, int *r_height) const {
        Atom sseln,img_png;
        Window owner;
        XEvent evt;
        uchar *sel_buf;
        ulong sel_len = 0;
        uint context = XCLIB_XCOUT_NONE;
        uint *rgba = nullptr;
        XSelectionEvent *sev;

        const vector<GHOST_IWindow *> &win_vec = m_windowManager->getWindows();
        vector<GHOST_IWindow *>::const_iterator win_it = win_vec.begin();
        GHOST_WindowX11 *window = static_cast<GHOST_WindowX11 *>(*win_it);
        Window win = window->getXWindow(); //target_window
        sseln = XInternAtom(m_display,"CLIPBOARD",False);
        img_png = XInternAtom(m_display,"image/png",False);
        owner = XGetSelectionOwner(m_display,sseln);

    
        XConvertSelection(m_display, sseln, img_png, m_atom.XCLIP_OUT, win, CurrentTime);
        if (owner == None){
            return nullptr;
        }
        for (;;)
        {
            XNextEvent(m_display,&evt);
            switch (evt.type){
                case SelectionNotify:
                    sev = (XSelectionEvent*)&evt.xselection;
                    if (sev->property == None)
                    {
                        return nullptr;
                    }
                    else
                    {
                        clipboard_img_prop(sseln, m_atom.XCLIP_OUT,(uchar**)&sel_buf,&sel_len,&context);
                        ImBuf *ibuf = IMB_ibImageFromMemory(sel_buf,sel_len,IB_rect,nullptr,"<clipboard>");
                        if(ibuf){
                            *r_width = ibuf->x;
                            *r_height = ibuf->y;
                            rgba = (uint *)malloc(4*ibuf->x*ibuf->y);
                            memcpy(rgba,ibuf->byte_buffer.data,4*ibuf->x*ibuf->y);
                            IMB_freeImBuf(ibuf);
                        }
                        return rgba;
                    }
                    break;
            }
        }
    }

簡単にgetClipboardImage関数のポイントを補足しておきます。 - ターゲットWindow winは、getClipboard関数を参考にして、代入します。 - Display型の変数は、GHOST_SystemX11.hhに定義されているDisplay *m_displayを使います。 - clipboard_img_prop関数で、クリップボードからデータをsel_bufにコピーし、そのバイト数をsel_lenに代入します。 - IMB_ibImageFromMemory関数は、画像の情報が入った構造体ImBuf型を返します。

この関数内で呼び出しているclipboard_img_prop関数は以下です。

    void GHOST_SystemX11::clipboard_img_prop(Atom sel, Atom target, uchar **img, unsigned long *len,unsigned int *context) const{
        Atom da,incr,pty_type;
        int pty_format;
        uchar *buffer = NULL;
        ulong pty_size, pty_items;
        uchar *limg = *img;
        const vector<GHOST_IWindow *> &win_vec = m_windowManager->getWindows();
        vector<GHOST_IWindow *>::const_iterator win_it = win_vec.begin();
        GHOST_WindowX11 *window = static_cast<GHOST_WindowX11 *>(*win_it);
        Window win = window->getXWindow();
        XGetWindowProperty(m_display,win,m_atom.XCLIP_OUT,0,0,False,AnyPropertyType,
                      &pty_type,&pty_format,&pty_items,&pty_size,&buffer);
        XFree(buffer);
        incr = XInternAtom(m_display,"INCR",False);
        if(pty_type == incr){
            return ;
        }
        XGetWindowProperty(m_display,win,m_atom.XCLIP_OUT,0,pty_size,False,AnyPropertyType,
                      &pty_type,&pty_format,&pty_items,&pty_size,&buffer);
  

        /* copy the buffer to the pointer for returned data */
        limg = (uchar *)malloc(pty_items);
        memcpy(limg, buffer, pty_items);
        *len = pty_items;
        *img = limg;
        XFree(buffer);
        *context = XCLIB_XCOUT_NONE;
        XDeleteProperty(m_display, win, m_atom.XCLIP_OUT);
        /* complete contents of selection fetched, return 1 */
        return;
  
    }

また、hasClipboardImage関数は以下です。

    GHOST_TSuccess GHOST_SystemX11::hasClipboardImage(void) const{
        return GHOST_kSuccess;
    }

最後に

今回はBlenderに対して全部で三つの機能を追加することに成功しました。作業には大変な困難が伴うものであったがそれゆえの成長を感じることができました。

最後にこの記事が、同じような悩みを持った開発者に届くことを願って終わりとしたいと思います。