ふと思い立ち、Blender + Pythonの勉強がてらアドオンを作りを始めました。

作りきれてちょ〜嬉しいので、はじめて作ったObject系アドオン「TQ Circular Array」をお披露目します。

なお、記事執筆時点では「Blender 2.8 beta(macOS版)April 24, 03:02:20 – 1b839e85e142」で動作確認しています。

TQ Circular Arrayの紹介

機能

  • 対象となるオブジェクト位置を中心として(Cursorではない)
  • X/Y/Z軸に対し
  • 指定した半径の円周上に
  • オブジェクトを同じ大きさで複製する

その名も「TQ Circular Array」です。TQは「Takashi Q. Hanamura Photography」からTとQを持ってきただけ(笑)

同じことを標準機能でやるには手間がかかります。しかも、Array前に拡大縮小や回転していると(状態を確定していないと)、Array数を増やすたびに縮小したり回転してしまいます。

標準機能はおそらく「そういう機能」なのでしょうが、僕は単純に複製されるのがしっくりくるので、ボタンひとつで単純に複製されるアドオンを作りました。

紹介画像

▼円盤状の平面に

sc_190426-2

▼楔を等間隔に打ち込む、みたいな使い方ができます。

sc_190426-3

紹介動画

ソースコード(2019.4.26版)

クリックすると見れます
'''
 対象のオブジェクト位置を中心として
 X/Y/Z軸に対して
 指定した半径の円周上にオブジェクトを
 同じ大きさで複製する
'''
import bpy
from bpy.props import (
    EnumProperty,
    FloatProperty,
    IntProperty,
    PointerProperty
)
from bpy.types import Panel
import math


bl_info = {
    "name": "TQ Circular Array",
    "description": "Make an array in a circular shape",
    "author": "Takashi Q. Hanamura",
    "version": (0, 1, 0, 0),
    "blender": (2, 80, 0),
    "support": "TESTING",  # テスト版
    "category": "Object",
    "location": "View3D > Sidebar",
    "warning": "",
    "wiki_url": "",
    "tracker_url": ""
}

# Array対象オブジェクト
bpy.types.Scene.CircularArray_target = PointerProperty(
    type=bpy.types.Object)
# 回転軸(デフォルト:X軸)
bpy.types.Scene.CircularArray_axis = bpy.props.EnumProperty(
    items=[("x", "X", "", 1), ("y", "Y", "", 2), ("z", "Z", "", 3)],
    description="Axis of rotation to make an array")
# 回転半径
bpy.types.Scene.CircularArray_radius = bpy.props.FloatProperty(
    default=5,
    min=0.001,
    description="Array radius")
# 複製する個数
bpy.types.Scene.CircularArray_count = bpy.props.IntProperty(
    default=2,
    min=1,
    max=50,
    description="Array count")

# 定数
EMPTY = "tq_circle_empty"  # Array用Emptyオブジェクト名


# Operationクラス
class TQ_OT_CircularArray_Operator(bpy.types.Operator):
    bl_idname = "object.circular_array"
    bl_label = "Circular Array"
    bl_description = "Make an array in a circular shape"
    bl_options = {'REGISTER', 'UNDO'}

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "CircularArray_target")

    def execute(self, context):
        # Array対象オブジェクト
        target_ob = bpy.context.scene.CircularArray_target

        if target_ob is not None:
            # Array回転軸
            axis = bpy.context.scene.CircularArray_axis
            # Array半径
            radius = bpy.context.scene.CircularArray_radius
            # Array数
            count = bpy.context.scene.CircularArray_count

            # Array対象オブジェクトをアクティブ
            bpy.ops.object.select_all(action="DESELECT")
            target_ob.select_set(state=True)
            bpy.context.view_layer.objects.active = target_ob

            # Array対象オブジェクトが等倍で複製されるよう現在の変形状態を確定
            bpy.ops.object.transform_apply(
                location=False,
                rotation=True,
                scale=True)

            # Array対象オブジェクトをEDITモードで変形(移動)
            trans_x, trans_y, trans_z = 0, 0, 0
            if axis == 'x':
                trans_y = radius
                rotation_axis = 0  # X軸
            elif axis == 'y':
                trans_z = radius
                rotation_axis = 1  # Y軸
            else:
                trans_x = radius
                rotation_axis = 2  # Z軸

            bpy.ops.object.mode_set(mode="EDIT")
            bpy.ops.transform.translate(
                value=(trans_x, trans_y, trans_z),
                orient_type="GLOBAL",
                orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
                orient_matrix_type="GLOBAL",
                constraint_axis=(False, False, False),
                mirror=True,
                proportional="DISABLED",
                proportional_edit_falloff="SMOOTH",
                proportional_size=1
            )

            # Array対象オブジェクトをOBJECTモードで変形(回転)
            bpy.ops.object.mode_set(mode="OBJECT")
            rad = float(360 / count) * (math.pi / 180)
            bpy.context.object.rotation_euler[rotation_axis] = rad

            # Array Modifierを追加
            bpy.context.scene.cursor.location = bpy.context.object.location
            bpy.ops.object.origin_set(
                type="ORIGIN_CURSOR",
                center="MEDIAN"
            )
            array_mod = target_ob.modifiers.new(
                type="ARRAY",
                name="TQ_Circular_Array"
            )
            array_mod.use_relative_offset = False
            array_mod.use_object_offset = True
            array_mod.count = count

            # Emptyオブジェクトを追加
            circle_empty = bpy.data.objects.new(EMPTY, None)
            bpy.context.scene.collection.objects.link(circle_empty)
            circle_empty.location = target_ob.location

            circle_empty.empty_display_size = 1
            circle_empty.empty_display_type = "ARROWS"
            array_mod.offset_object = circle_empty

            # 親子設定
            bpy.data.objects[circle_empty.name].select_set(state=True)
            bpy.ops.object.parent_set(type="OBJECT", keep_transform=False)
            bpy.data.objects[circle_empty.name].select_set(state=False)
        else:
            # エラー
            self.report({'WARNING'}, "Please select object.")

        return {'FINISHED'}


# Panelクラス
class TQ_PT_CircularArray_Panel(bpy.types.Panel):
    bl_label = "Circular Array"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Circular Array"
    bl_context = "objectmode"

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

        # Array対象のオブジェクト
        row = layout.row()
        row.prop_search(context.scene, "CircularArray_target",
                        context.scene, "objects", text="Target")
        # Array回転軸
        row = layout.row()
        row.prop(context.scene, "CircularArray_axis",
                 text="Mirror Axis", expand=True)
        # Array半径
        row = layout.row()
        row.prop(context.scene, "CircularArray_radius",
                 text="Radius")
        # Array数
        row = layout.row()
        row.prop(context.scene, "CircularArray_count",
                 text="Count")

        # 実行ボタン
        layout.separator()
        row = layout.row()
        row.operator(TQ_OT_CircularArray_Operator.bl_idname,
                     text="Array")


# クラス一覧
classes = (
    TQ_OT_CircularArray_Operator,
    TQ_PT_CircularArray_Panel
)


# クラスの登録
def register():
    for cls in classes:
        bpy.utils.register_class(cls)


# クラスの解除
def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


# Add-On Entry
if __name__ == "__main__":
    register()

課題

(1)リアルタイム反映させたい

▼元ネタとなったFast CarveのCircle Arrayでは専用のウィジェットでArray数を増減できます。それが即座に結果に反映されます。

sc_190427

これやりたいんですが今の自分の力ではできず・・・要調査です。

(2)複数つなげられない

BlenderのModifierは複数のModifierをつなげられます。たとえばWireframe → Subdivision Surface → Boolean・・・のように。

TQ Circular Arrayは標準のArray Modifierを使っていますが、TQ Circular Array → TQ Circular Array・・・とつなげていくと意図しない複製がなされます。

▼一度TQ Circular Arrayして作られたオブジェクト。これをZ軸に対して複製すると

sc_190426-4
Cylinderを小さく円盤状に。Wireframe modifierをかけたものにY軸に対しTQ Circular Arrayしたもの。

▼こうなるのが僕の想定する動作です。

sc_190426-8
上記オブジェクトが単純に複製されるはずが・・・

▼しかし、実際にはしっちゃかめっちゃかになります(笑)

sc_190426-6
OMG!!!

TQ Circular Arrayを連続で使いたい場合は一度Applyしてからにしてください。

(これ、なんとかできるのかしら・・・)

偶然の産物

ちなみに、しっちゃかめっちゃか状態でチョチョイとやるとこのような動画が作れます。偶然見つけたんですが、Generative Artとしては使えるかも。

おわりに

sc_190426-9

課題はあるもののひとまず作り切って大満足!

アドオンを使ってモデリングするというより、「ただ作りたい」という気持ちの方が強かったりします(笑)そんなんでもいいよね。

数年前、松本生活時代にPythonを始めたものの、具体的にこういうのが作りたい → 作ったにまで達したものは実は少ないのです。

今回ず〜っとやりたいと思っていたCGに活かすことができました。長い間点と点だったものが繋がった瞬間でもあるんです。

そういうことも含め、思い出深いアドオン開発になりました。

参考サイト

元ネタとなったオブジェクト系多機能アドオン。これに含まれるCircle Arrayを自分用にアレンジしました。
関連記事含めて大変参考になりました。続きも気になります。

この記事を書いた人

花村貴史|Takashi Hanamura

◆Photographer|木漏れ日や水、空が魅せるきらきらが好き|写真を通じて「やさしい世界」を伝えてゆく ◆Software Engineer|Private: Go/C++/WebGL|Work: SAP/SAC

写真素材note発売中

継続課金マガジンで販売しています。ブログに載せるもOK、壁紙に設定するもOK、使い方は自由です。フリー素材も良いけれど、人と被りたくないという方に人気です。

人物写真|Photo Session

お話しながらのポートレイト撮影です。「今」そして「これから」のあなたにフォーカスして撮影します。SNSやサイト用のプロフィール写真に。

イベント写真|Photo Shooting

「友達と楽しんでいる」「セミナーで講演している」「パーティーを主催している」など、皆さんが大切な時間を過ごしている瞬間をスナップします。