【Blender】特定の方向に選択範囲を拡大する

投稿者: | 2024-04-12

座標系と軸を指定して、選択範囲を拡大するアドオンを作ってみました。

スクリプト

__init__.py

# -*- coding: utf-8 -*-

bl_info = {
    "name": "Axis Direction Selector",
    "blender": (2, 93, 0),
    "category": "Object",
    "description": "Select faces based on axis direction in specified coordinate system.",
    "author": "Author",
    "version": (1, 0, 0),
    "location": "View3D > Sidebar > Tool"
}

import bpy
from .directional_select_logic import select_faces_based_on_direction


class AxisDirectionSettings(bpy.types.PropertyGroup):
    coord_system: bpy.props.EnumProperty(
        name="Coordinate System",
        description="Choose coordinate system",
        items=[('WORLD', "World", "World coordinate system"),
               ('LOCAL', "Local", "Local coordinate system")],
        default='WORLD'
    )
    
    axis: bpy.props.EnumProperty(
        name="Axis",
        description="Select axis",
        items=[
            ('X', "X", "Positive X axis"),
            ('Y', "Y", "Positive Y axis"),
            ('Z', "Z", "Positive Z axis"),
            ('-X', "-X", "Negative X axis"),
            ('-Y', "-Y", "Negative Y axis"),
            ('-Z', "-Z", "Negative Z axis")
        ],
        default='Z'
    )

class DirectionalSelectOperator(bpy.types.Operator):
    bl_idname = "object.directional_select_op"
    bl_label = "Select Faces"
    bl_description = "Select faces based on the specified axis and coordinate system"

    def execute(self, context):
        settings = context.scene.axis_direction_settings
        select_faces_based_on_direction(context, settings.axis, settings.coord_system)
        self.report({'INFO'}, "Directional face selection done.")
        return {'FINISHED'}


class AxisDirectionSelectorPanel(bpy.types.Panel):
    bl_label = "Axis Direction Selector"
    bl_idname = "AXIS_PT_DirectionSelector"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tool'

    def draw(self, context):
        layout = self.layout
        settings = context.scene.axis_direction_settings
        
        layout.label(text="Coordinate System:")
        row = layout.row()
        row.prop(settings, "coord_system", expand=True)

        layout.label(text="Axis:")
        row = layout.row()
        row.prop(settings, "axis", expand=True)

        layout.operator("object.directional_select_op")


def register():
    bpy.utils.register_class(AxisDirectionSelectorPanel)
    bpy.utils.register_class(DirectionalSelectOperator)
    bpy.utils.register_class(AxisDirectionSettings)
    bpy.types.Scene.axis_direction_settings = bpy.props.PointerProperty(type=AxisDirectionSettings)

def unregister():
    bpy.utils.unregister_class(AxisDirectionSelectorPanel)
    bpy.utils.unregister_class(DirectionalSelectOperator)
    del bpy.types.Scene.axis_direction_settings

if __name__ == "__main__":
    register()

directional_select_logic.py

import bpy
import bmesh
from mathutils import Vector

def get_direction_vector(settings, obj):
    # 座標系に基づいて方向ベクトルを設定
    axis_directions = {
        'X': Vector((1, 0, 0)),
        'Y': Vector((0, 1, 0)),
        'Z': Vector((0, 0, 1)),
        '-X': Vector((-1, 0, 0)),
        '-Y': Vector((0, -1, 0)),
        '-Z': Vector((0, 0, -1))
    }
    direction_vector = axis_directions[settings.axis]

    # 座標系がローカルの場合、オブジェクトのモデル変換を適用
    if settings.coord_system == 'LOCAL':
        direction_vector = obj.matrix_world.to_3x3() @ direction_vector

    return direction_vector


def select_faces_based_on_direction(context, axis, coordinate_system):
    # 編集モードに切り替える
    if context.object.mode != 'EDIT':
        bpy.ops.object.mode_set(mode='EDIT')

    # アクティブなメッシュを取得
    edit_obj = context.edit_object
    
    if edit_obj is None:
        raise RuntimeError("編集モードにあるアクティブなオブジェクトがありません。")
    elif edit_obj.type != 'MESH':
        raise TypeError("選択されたオブジェクトはメッシュタイプではありません。")
    
    me = edit_obj.data

    # BMesh表現を取得
    bm = bmesh.from_edit_mesh(me)
    bm.faces.ensure_lookup_table()

    # 選択されている面を収集
    selected_faces = [f for f in bm.faces if f.select]
    
    # アクティブなシーンから設定を取得
    settings = context.scene.axis_direction_settings
    
    # 面選択のための方向ベクトルを取得
    direction_vector = get_direction_vector(settings, edit_obj)

    # 各選択面の中心点のワールド座標における内積を計算
    selected_faces_dots = {f: direction_vector.dot(edit_obj.matrix_world @ f.calc_center_median()) for f in selected_faces}

    # 基準の方向ベクトルに沿って位置する隣接面を選択
    for face in selected_faces:
        dot_selected = selected_faces_dots[face]
        for edge in face.edges:
            for adjacent_face in edge.link_faces:
                # 隣接面が選択されている面でなく、基準の方向ベクトルに沿って選択されている面よりも前方に位置していることを確認
                if adjacent_face != face and adjacent_face not in selected_faces:
                    dot_adjacent = direction_vector.dot(edit_obj.matrix_world @ adjacent_face.calc_center_median())
                    if dot_adjacent > dot_selected:
                        adjacent_face.select = True

    bmesh.update_edit_mesh(me)

設定

上の2つのファイルだけが含まれたフォルダを作ります。

フォルダを圧縮してzipファイルを作ります。

Blenderで、Edit > Preferences… を開きます。

Add-onsで「Install…」ボタンからzipファイルをインストールします。

アドオンの名前の横のチェックボックスを入れてアドオンを有効化します。

その横の▷アイコンを押すとアドオンの情報が開きます。「Remove」ボタンでアドオンを削除できます。

3Dビューポートのサイドバーの「Tool」タブにパネルが表示されます。

使い方

編集モードで面を選択します。

ラジオボタンでワールド座標系のZ軸を選択します。ボタンをクリックすると、上方向に選択範囲が拡大します。

さらにボタンを2回クリック

軸によって選択範囲が拡大する方向が変わります。

ローカル座標

オブジェクトモードでCubeを回転してみます。

軸が同じでも、座標系を変えると選択範囲の拡大する方向が変わります。

ワールド座標
ローカル座標

スクリプト詳細

プロパティグループに2つの列挙体プロパティを定義しています。

class AxisDirectionSettings(bpy.types.PropertyGroup):
    coord_system: bpy.props.EnumProperty(
        name="Coordinate System",
        description="Choose coordinate system",
        items=[('WORLD', "World", "World coordinate system"),
               ('LOCAL', "Local", "Local coordinate system")],
        default='WORLD'
    )
    
    axis: bpy.props.EnumProperty(
        name="Axis",
        description="Select axis",
        items=[
            ('X', "X", "Positive X axis"),
            ('Y', "Y", "Positive Y axis"),
            ('Z', "Z", "Positive Z axis"),
            ('-X', "-X", "Negative X axis"),
            ('-Y', "-Y", "Negative Y axis"),
            ('-Z', "-Z", "Negative Z axis")
        ],
        default='Z'
    )

列挙体の値を選択するUIを表示するときに、引数expandにTrueを渡すと、ドロップダウンではなくラジオボタンが表示されます。

    def draw(self, context):
        layout = self.layout
        settings = context.scene.axis_direction_settings
        
        layout.label(text="Coordinate System:")
        row = layout.row()
        row.prop(settings, "coord_system", expand=True)

        layout.label(text="Axis:")
        row = layout.row()
        row.prop(settings, "axis", expand=True)

        layout.operator("object.directional_select_op")
座標系のみ「expand=False」にした場合

編集中のオブジェクトのメッシュデータからBMeshを作成します。faces.ensure_lookup_tableメソッドで面を簡単に編集できるようになります。

    # アクティブなメッシュを取得
    edit_obj = context.edit_object
    
    if edit_obj is None:
        raise RuntimeError("編集モードにあるアクティブなオブジェクトがありません。")
    elif edit_obj.type != 'MESH':
        raise TypeError("選択されたオブジェクトはメッシュタイプではありません。")
    
    me = edit_obj.data

    # BMesh表現を取得
    bm = bmesh.from_edit_mesh(me)
    bm.faces.ensure_lookup_table()

BMeshに変更を加えたあと、update_edit_meshメソッドでメッシュを更新します。

    bmesh.update_edit_mesh(me)

選択中の面と隣接している面の中から、条件に合った面を選択に含めます。今回は基準となる方向ベクトルを作り、選択面と隣接面の2つの面の中心位置との内積を比較しています。

まず基準の方向ベクトルを作り、全ての選択面の内積をリストに入れます。

    # 選択されている面を収集
    selected_faces = [f for f in bm.faces if f.select]
    
    # アクティブなシーンから設定を取得
    settings = context.scene.axis_direction_settings
    
    # 面選択のための方向ベクトルを取得
    direction_vector = get_direction_vector(settings, edit_obj)

    # 各選択面の中心点のワールド座標における内積を計算
    selected_faces_dots = {f: direction_vector.dot(edit_obj.matrix_world @ f.calc_center_median()) for f in selected_faces}

基準のベクトルはプロパティが表す座標系と軸によって決定します。軸によって方向ベクトルが定義されています。ローカル座標系を選択した場合はこのベクトルをワールド座標系に座標変換します。

def get_direction_vector(settings, obj):
    # 座標系に基づいて方向ベクトルを設定
    axis_directions = {
        'X': Vector((1, 0, 0)),
        'Y': Vector((0, 1, 0)),
        'Z': Vector((0, 0, 1)),
        '-X': Vector((-1, 0, 0)),
        '-Y': Vector((0, -1, 0)),
        '-Z': Vector((0, 0, -1))
    }
    direction_vector = axis_directions[settings.axis]

    # 座標系がローカルの場合、オブジェクトのモデル変換を適用
    if settings.coord_system == 'LOCAL':
        direction_vector = obj.matrix_world.to_3x3() @ direction_vector

    return direction_vector

各面を作る辺のlink_facesメソッドで、その辺と接続している面が得られます。基準のベクトルと隣接面の中心位置との内積が、選択面との内積より大きい場合に隣接面を選択します。

    # 基準の方向ベクトルに沿って位置する隣接面を選択
    for face in selected_faces:
        dot_selected = selected_faces_dots[face]
        for edge in face.edges:
            for adjacent_face in edge.link_faces:
                # 隣接面が選択されている面でなく、基準の方向ベクトルに沿って選択されている面よりも前方に位置していることを確認
                if adjacent_face != face and adjacent_face not in selected_faces:
                    dot_adjacent = direction_vector.dot(edit_obj.matrix_world @ adjacent_face.calc_center_median())
                    if dot_adjacent > dot_selected:
                        adjacent_face.select = True

これで特定の方向にそって選択範囲を拡大できました。

コメントを残す

メールアドレスが公開されることはありません。