blender まとめ20260821



bl_info = {
"name": "Sphere Cut & Hollow (4 Colors & Detach)",
"version": (2, 6),
"blender": (5, 0, 0),
"category": "Object"
}
# ===================================
# 4. 共通定義 (ここを変更するだけで完全に競合しない別アドオンになります)
# ===================================
ADDON_TITLE = "Sphere Cut & Hollow" # ツールチップ等に表示される名前
ADDON_CATEGORY = "Sphere_Cut" # Nパネルのタブ名
PREFIX_VAL = "SPHERE_CUT_TOOLS" # 内部データ・オブジェクトの接頭辞 (※必ず英数字・アンダースコアのみ)
PREFIX = PREFIX_VAL.lower() # bl_idname用の小文字版
# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = ""
LINK_URL_4 = ""
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =============================================================================
# 【ユーザーパラメータ】
# =============================================================================
# --- 球体 ---
SPHERE_RADIUS = 5.0
SPHERE_SEGMENTS = 96
SPHERE_RING_COUNT = 64
SHELL_THICKNESS = 0.5 # 「内部空洞(殻)」モード時の、殻の厚み
# --- 円柱穴 ---
CYLINDER_RADIUS = 1.5
CYLINDER_DEPTH = 15.0
CYLINDER_VERTICES = 96
CYLINDER_AXIS = "Z" # 対応: "X", "Y", "Z"
# --- 半球断面のカット位置 ---
SECTION_AXIS = "X" # 対応: "X", "Y", "Z"
SECTION_POSITION = 0.0
SECTION_KEEP = "POSITIVE" # "POSITIVE" なら SECTION_AXIS >= 0 を残す
SECTION_CUTTER_SIZE = 30.0 # 断面カッターのサイズ
# --- デフォルト色 (4色構成) ---
OUTER_SURFACE_COLOR = (0.05, 0.20, 0.90, 1.0) # 青 (外表面)
CYLINDER_HOLE_COLOR = (0.90, 0.08, 0.03, 1.0) # 赤 (円柱との接触面)
SECTION_COLOR = (0.05, 0.80, 0.15, 1.0) # 緑 (半球切断面)
HOLLOW_WALL_COLOR = (0.80, 0.60, 0.10, 1.0) # 黄 (空洞の内壁 ※殻モード時のみ)
# --- Collection & Naming (PREFIXを自動適用) ---
COLLECTION_NAME = f"{PREFIX_VAL}_Collection"
SPHERE_NAME = f"{PREFIX_VAL}_Target"
CYL_CUTTER_NAME = f"{PREFIX_VAL}_Cyl_Cutter"
SEC_CUTTER_NAME = f"{PREFIX_VAL}_Sec_Cutter"
# =============================================================================
# 【コアロジック】
# =============================================================================
import bpy
import math
import webbrowser
from bpy.props import FloatVectorProperty, StringProperty, PointerProperty
def get_props():
"""動的生成したSceneプロパティを取得する"""
return getattr(bpy.context.scene, f"{PREFIX}_props")
def clear_objects():
"""古い生成物を完全に削除する"""
for name in [SPHERE_NAME, CYL_CUTTER_NAME, SEC_CUTTER_NAME]:
obj = bpy.data.objects.get(name)
if obj:
bpy.data.objects.remove(obj, do_unlink=True)
def move_to_collection(obj, coll_name):
"""オブジェクトを指定のコレクションに配置する"""
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
for c in obj.users_collection:
c.objects.unlink(obj)
coll.objects.link(obj)
def get_or_create_material(mat_name, color):
"""マテリアルを取得または新規作成して色を設定する"""
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
def get_materials_list():
"""UIのカラー設定を反映した4つのマテリアルを固定順序のリストで返す"""
props = get_props()
# スロット順序を固定: 0=外側, 1=円柱, 2=断面, 3=内壁
return [
get_or_create_material(f"{PREFIX_VAL}_Outer_Mat", props.outer_color),
get_or_create_material(f"{PREFIX_VAL}_Cyl_Mat", props.cylinder_color),
get_or_create_material(f"{PREFIX_VAL}_Section_Mat", props.section_color),
get_or_create_material(f"{PREFIX_VAL}_Hollow_Mat", props.hollow_color),
]
def update_colors(self, context):
get_materials_list()
def apply_shared_materials(obj, mats, default_index):
"""オブジェクトに共通の4色を割り当て、全面のインデックスを指定の番号に強制する"""
for m in mats:
obj.data.materials.append(m)
for p in obj.data.polygons:
p.material_index = default_index
def create_cylinder_cutter(name):
"""円柱カッターを作成する"""
rot = (0, 0, 0)
if CYLINDER_AXIS == "X": rot = (0, math.radians(90), 0)
elif CYLINDER_AXIS == "Y": rot = (math.radians(90), 0, 0)
bpy.ops.mesh.primitive_cylinder_add(vertices=CYLINDER_VERTICES, radius=CYLINDER_RADIUS, depth=CYLINDER_DEPTH, rotation=rot)
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def create_section_cutter(name):
"""半球にするための断面カッターを作成する"""
loc = [0, 0, 0]
half = SECTION_CUTTER_SIZE / 2.0
if SECTION_AXIS == "X":
loc[0] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Y":
loc[1] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Z":
loc[2] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
bpy.ops.mesh.primitive_cube_add(size=SECTION_CUTTER_SIZE, location=tuple(loc))
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def apply_boolean(target, cutter, mod_name):
"""Boolean Differenceを適用する"""
bpy.context.view_layer.objects.active = target
mod = target.modifiers.new(name=mod_name, type='BOOLEAN')
mod.object = cutter
mod.operation = 'DIFFERENCE'
mod.solver = 'EXACT'
if hasattr(mod, "material_mode"):
mod.material_mode = 'TRANSFER'
bpy.ops.object.modifier_apply(modifier=mod_name)
# =============================================================================
# 【プロパティ定義】
# =============================================================================
class DynamicProps(bpy.types.PropertyGroup):
current_mode_label: StringProperty(default="なし")
error_message: StringProperty(default="")
outer_color: FloatVectorProperty(name="外表面", subtype='COLOR', size=4, default=OUTER_SURFACE_COLOR, update=update_colors)
cylinder_color: FloatVectorProperty(name="円柱との接触面", subtype='COLOR', size=4, default=CYLINDER_HOLE_COLOR, update=update_colors)
section_color: FloatVectorProperty(name="半球の切断面", subtype='COLOR', size=4, default=SECTION_COLOR, update=update_colors)
hollow_color: FloatVectorProperty(name="空洞の内壁", subtype='COLOR', size=4, default=HOLLOW_WALL_COLOR, update=update_colors)
# =============================================================================
# 【オペレーター (機能)】
# =============================================================================
class OT_create_tunnel(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_create_tunnel"
bl_label = "トンネル壁面 (無垢)"
bl_description = "中身が詰まった球体に穴を開け、半分に切断します"
def execute(self, context):
props = get_props()
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 無垢の球体を作成
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. カッターを作成
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 3. Boolean適用
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 4. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "トンネル壁面 (無垢の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
class OT_create_hollow(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_create_hollow"
bl_label = "内部空洞 (殻)"
bl_description = "中身が空っぽの殻に穴を開け、半分に切断します"
def execute(self, context):
props = get_props()
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 球体を作成
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 殻(シェル)にする
sol = sphere.modifiers.new(name="Solidify", type='SOLIDIFY')
sol.thickness = SHELL_THICKNESS
sol.offset = -1.0
sol.material_offset = 3 # 厚みの内側
sol.material_offset_rim = 2 # 縁の部分
bpy.ops.object.modifier_apply(modifier="Solidify")
# 3. カッターを作成
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 4. Boolean適用
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 5. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "内部空洞 (殻の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
class OT_detach_object(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_detach_object"
bl_label = "確定して切り離す"
bl_description = "モディファイアを適用し、マテリアルを独立させ、アドオンの管理下から外します"
def execute(self, context):
obj = bpy.data.objects.get(SPHERE_NAME)
if not obj:
self.report({'WARNING'}, "切り離す対象のオブジェクトが見つかりません。先に生成してください。")
return {'CANCELLED'}
context.view_layer.objects.active = obj
# 1. モディファイアを全て適用 (形の独立)
for mod in list(obj.modifiers):
try:
bpy.ops.object.modifier_apply(modifier=mod.name)
except:
pass
# 2. マテリアルを複製して独立化 (色の独立)
for slot in obj.material_slots:
if slot.material:
slot.material = slot.material.copy()
# 3. オブジェクト名を変更 (アドオンの削除・再生成対象から外す)
obj.name = f"{PREFIX_VAL}_Detached"
self.report({'INFO'}, "オブジェクトを切り離しました (マテリアルと形状を確定)")
return {'FINISHED'}
# =============================================================================
# 【UI・その他】
# =============================================================================
class OT_focus_selected(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_focus_selected"
bl_label = "選択オブジェクトを中央に表示"
def execute(self, context):
if context.selected_objects:
bpy.ops.view3d.view_selected()
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_open_url"
bl_label = "Open URL"
url: StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# --- 1. メインパネル ---
class VIEW3D_PT_main_panel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"VIEW3D_PT_{PREFIX}_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = get_props()
layout.operator(f"view3d.{PREFIX}_focus_selected", icon='ZOOM_SELECTED')
layout.separator()
layout.label(text="生成モード (どちらも半球になります)")
row = layout.row(align=True)
row.operator(f"view3d.{PREFIX}_create_tunnel", text="トンネル (無垢)")
row.operator(f"view3d.{PREFIX}_create_hollow", text="内部空洞 (殻)")
layout.separator()
layout.label(text=f"現在:{props.current_mode_label}")
if props.error_message:
layout.label(text=props.error_message, icon='ERROR')
layout.separator()
layout.label(text="面の色")
row = layout.row(); row.prop(props, "outer_color", text="球の外側表面")
row = layout.row(); row.prop(props, "cylinder_color", text="円柱との接触面")
row = layout.row(); row.prop(props, "section_color", text="半球の切断面")
row = layout.row(); row.prop(props, "hollow_color", text="空洞の内壁 (※殻モード時)")
layout.separator()
layout.operator(f"view3d.{PREFIX}_detach_object", icon='CHECKMARK')
# --- 2. リンクパネル ---
class VIEW3D_PT_links_panel(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"VIEW3D_PT_{PREFIX}_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
if LINK_NAME_1: layout.operator(f"wm.{PREFIX}_open_url", text=f"・{LINK_NAME_1}").url = LINK_URL_1
if LINK_NAME_2: layout.operator(f"wm.{PREFIX}_open_url", text=f"・{LINK_NAME_2}").url = LINK_URL_2
if LINK_NAME_3: layout.operator(f"wm.{PREFIX}_open_url", text=f"・{LINK_NAME_3}").url = LINK_URL_3
if LINK_NAME_4: layout.operator(f"wm.{PREFIX}_open_url", text=f"・{LINK_NAME_4}").url = LINK_URL_4
if LINK_NAME_5: layout.operator(f"wm.{PREFIX}_open_url", text=f"・{LINK_NAME_5}").url = LINK_URL_5
# --- 3. Adminパネル ---
class VIEW3D_PT_admin_panel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = f"VIEW3D_PT_{PREFIX}_admin"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# =============================================================================
# 【登録処理】
# =============================================================================
def initialize_viewport():
wm = bpy.context.window_manager
if not wm: return 0.2
for window in wm.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
if space:
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
classes = (
DynamicProps,
OT_create_tunnel,
OT_create_hollow,
OT_detach_object,
OT_focus_selected,
OT_open_url,
OT_remove_addon,
VIEW3D_PT_main_panel,
VIEW3D_PT_links_panel,
VIEW3D_PT_admin_panel,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
setattr(bpy.types.Scene, f"{PREFIX}_props", PointerProperty(type=DynamicProps))
if not bpy.app.timers.is_registered(initialize_viewport):
bpy.app.timers.register(initialize_viewport, first_interval=0.2)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
prop_name = f"{PREFIX}_props"
if hasattr(bpy.types.Scene, prop_name):
delattr(bpy.types.Scene, prop_name)
if __name__ == "__main__":
register()
# =============================================================================
# 【AIへの依頼用プロンプト(完全再現用)】
# =============================================================================
"""
Blender 5.x 専用のPythonアドオンを作成してください。
【目的】
球体に「円柱状の穴」を開け、さらに「半球状にカット」して断面を見せるツールを作ります。
「トンネル (無垢)」と「内部空洞 (殻)」の2つのモードを備え、4つの面を確実に色分け(マテリアル分け)します。
【形式・共通定義】
コードの冒頭に以下の形式を必ず組み込み、PREFIXによる動的ID管理を行ってください。
この部分を変更するだけで、他のアドオンと競合しない設計にします。
# ===================================
# 4. 共通定義 (ここを変更するだけで完全に競合しない別アドオンになります)
# ===================================
ADDON_TITLE = "Sphere Cut & Hollow" # ツールチップ等に表示される名前
ADDON_CATEGORY = "Sphere_Cut" # Nパネルのタブ名
PREFIX_VAL = "SPHERE_CUT_TOOLS" # 内部データ・オブジェクトの接頭辞
PREFIX = PREFIX_VAL.lower() # bl_idname用の小文字版
# ===================================
# 5. リンク定義 (そのまま使用する)
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"
# ... (指示された5つのリンク定義を配置) ...
※ オペレーターの `bl_idname` や保存プロパティ、オブジェクト名、マテリアル名には、必ずこの `PREFIX` や `PREFIX_VAL` を組み込んでください。
【色分け(マテリアル)の完全な仕様】
Booleanで面ごとに正確に色分けするため、以下のロジックを必ず守ってください。
スロットの固定順序(4色):
Index 0: 外表面 (初期値: 青)
Index 1: 円柱との接触面 (初期値: 赤)
Index 2: 半球の切断面 (初期値: 緑)
Index 3: 空洞の内壁 (初期値: 黄)
適用手順:
1. 上記4つのマテリアルを生成し、リスト化します。
2. メインの球体、円柱カッター、断面カッターの「すべてのオブジェクト」に対して、この4つのマテリアルを「全く同じ順番」でappendします。
3. 各オブジェクトの全ポリゴンの material_index を以下のように強制上書きします。
- 球体の全ポリゴン: 0
- 円柱カッターの全ポリゴン: 1
- 断面カッターの全ポリゴン: 2
4. 殻モードの場合、球体のSolidifyモディファイアで以下を設定します。
- offset = -1.0
- material_offset = 3
- material_offset_rim = 2
5. Booleanモディファイアで operation = 'DIFFERENCE' と material_mode = 'TRANSFER' を適用します。
【UI (Nパネル) の構成】
Nパネルの指定したタブ内に、独立した「3つのパネル(クラス)」を作成してください。
1. メインパネル (デフォルトで開く)
- 「選択オブジェクトを中央に表示」ボタン
- 「トンネル (無垢)」「内部空洞 (殻)」の実行ボタン
- 現在のモード名とエラーメッセージ
- 4色のカラーピッカー
- 「確定して切り離す」ボタン (Detach)
2. Links パネル (デフォルトは閉じる : bl_options = {'DEFAULT_CLOSED'})
- 指定したリンク先を開くボタンリスト
3. Admin パネル (デフォルトは閉じる : bl_options = {'DEFAULT_CLOSED'})
- 「アドオン解除」ボタン
【切り離し (Detach) 機能の仕様】
「確定して切り離す」ボタンを押した際、以下の処理を行ってオブジェクトを独立させてください。
1. 対象のオブジェクト(球体)を選択・アクティブにする。
2. 適用されているすべてのモディファイア(Solidify, Boolean等)をループで適用 (Apply) し、メッシュ形状を確定させる。
3. 対象オブジェクトのマテリアルスロットをループし、`slot.material = slot.material.copy()` を実行して、マテリアルを複製・独立させる。
4. 対象オブジェクトの名前を変更し(例: `PREFIX_VAL_Detached`)、以降の「削除・再生成(clear_objects)」の対象から外れるようにする。
【その他仕様】
- ボタンを押すたびに古いオブジェクトを削除してから新規生成すること。
- Boolean適用後、カッターオブジェクトは削除すること。
"""
bl_info = {
"name": "Cam Sphere Boolean",
"version": (2, 4),
"blender": (5, 0, 0),
"category": "Object"
}
# =============================================================================
# 【ユーザーパラメータ(コード先頭)】
# =============================================================================
# --- 球体 ---
SPHERE_RADIUS = 5.0
SPHERE_SEGMENTS = 96
SPHERE_RING_COUNT = 64
SHELL_THICKNESS = 0.5 # 「内部空洞(殻)」モード時の、殻の厚み
# --- 円柱穴 ---
CYLINDER_RADIUS = 1.5
CYLINDER_DEPTH = 15.0
CYLINDER_VERTICES = 96
CYLINDER_AXIS = "Z" # 対応: "X", "Y", "Z"
# --- 半球断面のカット位置 ---
SECTION_AXIS = "X" # 対応: "X", "Y", "Z"
SECTION_POSITION = 0.0
SECTION_KEEP = "POSITIVE" # "POSITIVE" なら SECTION_AXIS >= 0 を残す
SECTION_CUTTER_SIZE = 30.0 # 断面カッターのサイズ
# --- デフォルト色 (4色構成) ---
OUTER_SURFACE_COLOR = (0.05, 0.20, 0.90, 1.0) # 青 (外表面)
CYLINDER_HOLE_COLOR = (0.90, 0.08, 0.03, 1.0) # 赤 (円柱との接触面)
SECTION_COLOR = (0.05, 0.80, 0.15, 1.0) # 緑 (半球切断面)
HOLLOW_WALL_COLOR = (0.80, 0.60, 0.10, 1.0) # 黄 (空洞の内壁 ※殻モード時のみ)
# --- Collection & Naming ---
COLLECTION_NAME = "CAM_Sphere_Boolean"
SPHERE_NAME = "CAM_Sphere"
CYL_CUTTER_NAME = "CAM_Cylinder_Cutter"
SEC_CUTTER_NAME = "CAM_Section_Cutter"
# =============================================================================
# 【コアロジック】
# =============================================================================
import bpy
import math
from bpy.props import FloatVectorProperty, StringProperty, PointerProperty
def clear_objects():
"""古い生成物を完全に削除する"""
for name in [SPHERE_NAME, CYL_CUTTER_NAME, SEC_CUTTER_NAME]:
obj = bpy.data.objects.get(name)
if obj:
bpy.data.objects.remove(obj, do_unlink=True)
def move_to_collection(obj, coll_name):
"""オブジェクトを指定のコレクションに配置する"""
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
for c in obj.users_collection:
c.objects.unlink(obj)
coll.objects.link(obj)
def get_or_create_material(mat_name, color):
"""マテリアルを取得または新規作成して色を設定する"""
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
def get_materials_list():
"""UIのカラー設定を反映した4つのマテリアルを固定順序のリストで返す"""
props = bpy.context.scene.cam_sphere_props
# スロット順序を固定: 0=外側, 1=円柱, 2=断面, 3=内壁
return [
get_or_create_material("CAM_Outer_Mat", props.outer_color),
get_or_create_material("CAM_Cyl_Mat", props.cylinder_color),
get_or_create_material("CAM_Section_Mat", props.section_color),
get_or_create_material("CAM_Hollow_Mat", props.hollow_color),
]
def update_colors(self, context):
get_materials_list()
def apply_shared_materials(obj, mats, default_index):
"""オブジェクトに共通の4色を割り当て、全面のインデックスを指定の番号に強制する"""
# 4色すべてを同じ順番でスロットに登録する(Booleanでの色ズレを防ぐため必須)
for m in mats:
obj.data.materials.append(m)
# オブジェクトの全面を指定した色(インデックス)にする
for p in obj.data.polygons:
p.material_index = default_index
def create_cylinder_cutter(name):
"""円柱カッターを作成する"""
rot = (0, 0, 0)
if CYLINDER_AXIS == "X": rot = (0, math.radians(90), 0)
elif CYLINDER_AXIS == "Y": rot = (math.radians(90), 0, 0)
bpy.ops.mesh.primitive_cylinder_add(vertices=CYLINDER_VERTICES, radius=CYLINDER_RADIUS, depth=CYLINDER_DEPTH, rotation=rot)
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def create_section_cutter(name):
"""半球にするための断面カッターを作成する"""
loc = [0, 0, 0]
half = SECTION_CUTTER_SIZE / 2.0
if SECTION_AXIS == "X":
loc[0] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Y":
loc[1] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Z":
loc[2] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
bpy.ops.mesh.primitive_cube_add(size=SECTION_CUTTER_SIZE, location=tuple(loc))
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def apply_boolean(target, cutter, mod_name):
"""Boolean Differenceを適用する"""
bpy.context.view_layer.objects.active = target
mod = target.modifiers.new(name=mod_name, type='BOOLEAN')
mod.object = cutter
mod.operation = 'DIFFERENCE'
mod.solver = 'EXACT'
if hasattr(mod, "material_mode"):
mod.material_mode = 'TRANSFER'
bpy.ops.object.modifier_apply(modifier=mod_name)
# =============================================================================
# 【プロパティ定義】
# =============================================================================
class CAMSphereProps(bpy.types.PropertyGroup):
current_mode_label: StringProperty(default="なし")
error_message: StringProperty(default="")
outer_color: FloatVectorProperty(name="外表面", subtype='COLOR', size=4, default=OUTER_SURFACE_COLOR, update=update_colors)
cylinder_color: FloatVectorProperty(name="円柱との接触面", subtype='COLOR', size=4, default=CYLINDER_HOLE_COLOR, update=update_colors)
section_color: FloatVectorProperty(name="半球の切断面", subtype='COLOR', size=4, default=SECTION_COLOR, update=update_colors)
hollow_color: FloatVectorProperty(name="空洞の内壁", subtype='COLOR', size=4, default=HOLLOW_WALL_COLOR, update=update_colors)
# =============================================================================
# 【オペレーター (機能)】
# =============================================================================
class OT_cam_create_tunnel(bpy.types.Operator):
bl_idname = "view3d.cam_create_tunnel"
bl_label = "トンネル壁面 (無垢)"
bl_description = "中身が詰まった球体に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 無垢の球体を作成 (インデックス0: 青色)
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 円柱カッターを作成 (インデックス1: 赤色)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
# 3. 断面カッターを作成 (インデックス2: 緑色)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 4. Boolean適用 (カッターの色が正確に転送される)
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 5. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "トンネル壁面 (無垢の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
class OT_cam_create_hollow(bpy.types.Operator):
bl_idname = "view3d.cam_create_hollow"
bl_label = "内部空洞 (殻)"
bl_description = "中身が空っぽの殻に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 球体を作成 (インデックス0: 青色)
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 殻(シェル)にする (Solidifyで厚みの内側と縁に色を付ける)
sol = sphere.modifiers.new(name="Solidify", type='SOLIDIFY')
sol.thickness = SHELL_THICKNESS
sol.offset = -1.0
sol.material_offset = 3 # 厚みの内側をインデックス3 (黄色)にする
sol.material_offset_rim = 2 # 縁の部分をインデックス2 (緑色)にする
bpy.ops.object.modifier_apply(modifier="Solidify")
# 3. 円柱カッターを作成 (インデックス1: 赤色)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
# 4. 断面カッターを作成 (インデックス2: 緑色)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 5. Boolean適用
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 6. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "内部空洞 (殻の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
# =============================================================================
# 【UI・その他】
# =============================================================================
class OT_cam_focus_selected(bpy.types.Operator):
bl_idname = "view3d.cam_focus_selected"
bl_label = "選択オブジェクトを中央に表示"
def execute(self, context):
if context.selected_objects:
bpy.ops.view3d.view_selected()
return {'FINISHED'}
class OT_cam_remove_addon(bpy.types.Operator):
bl_idname = "wm.cam_remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
class VIEW3D_PT_cam_sphere(bpy.types.Panel):
bl_label = "Cam Sphere"
bl_idname = "VIEW3D_PT_cam_sphere"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cam Sphere"
def draw(self, context):
layout = self.layout
props = context.scene.cam_sphere_props
layout.operator("view3d.cam_focus_selected", icon='ZOOM_SELECTED')
layout.separator()
layout.label(text="生成モード (どちらも半球になります)")
row = layout.row(align=True)
row.operator("view3d.cam_create_tunnel", text="トンネル (無垢)")
row.operator("view3d.cam_create_hollow", text="内部空洞 (殻)")
layout.separator()
layout.label(text=f"現在:{props.current_mode_label}")
if props.error_message:
layout.label(text=props.error_message, icon='ERROR')
layout.separator()
layout.label(text="面の色")
row = layout.row()
row.prop(props, "outer_color", text="球の外側表面")
row = layout.row()
row.prop(props, "cylinder_color", text="円柱との接触面")
row = layout.row()
row.prop(props, "section_color", text="半球の切断面")
row = layout.row()
row.prop(props, "hollow_color", text="空洞の内壁 (※殻モード時)")
layout.separator()
box = layout.box()
box.operator("wm.cam_remove_addon", icon='CANCEL')
# =============================================================================
# 【登録処理】
# =============================================================================
def initialize_viewport():
wm = bpy.context.window_manager
if not wm: return 0.2
for window in wm.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
if space:
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
classes = (
CAMSphereProps,
OT_cam_create_tunnel,
OT_cam_create_hollow,
OT_cam_focus_selected,
OT_cam_remove_addon,
VIEW3D_PT_cam_sphere,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.cam_sphere_props = PointerProperty(type=CAMSphereProps)
if not bpy.app.timers.is_registered(initialize_viewport):
bpy.app.timers.register(initialize_viewport, first_interval=0.2)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "cam_sphere_props"):
del bpy.types.Scene.cam_sphere_props
if __name__ == "__main__":
register()
# =============================================================================
# 【AIへの依頼用プロンプト(完全再現用)】
# =============================================================================
"""
Blender 5.x 専用のPythonアドオンを作成してください。
【目的】
球体に「円柱状の穴」を開け、さらに「半球状にカット」して断面を見せるツールを作ります。
「トンネル (無垢)」と「内部空洞 (殻)」の2つのモードを備え、4つの面を確実に色分け(マテリアル分け)します。
【機能・モード】
1. トンネル (無垢) モード
中身が詰まった球体に円柱で穴を開け、半分に切断します。
2. 内部空洞 (殻) モード
Solidifyモディファイアを使って球体を「厚みのある殻」にし、そこに円柱で穴を開け、半分に切断します。
【色分け(マテリアル)の完全な仕様】※最重要
Booleanで面ごとに正確に色分けするため、以下のロジックを必ず守ってください。
スロットの固定順序(4色):
Index 0: 外表面 (初期値: 青)
Index 1: 円柱との接触面 (初期値: 赤)
Index 2: 半球の切断面 (初期値: 緑)
Index 3: 空洞の内壁 (初期値: 黄)
適用手順:
1. 上記4つのマテリアルを生成し、リスト化します。
2. メインの球体、円柱カッター、断面カッターの「すべてのオブジェクト」に対して、この4つのマテリアルを「全く同じ順番」でappendします。
3. 各オブジェクトの全ポリゴンの material_index を以下のように強制上書きします。
- 球体の全ポリゴン: 0
- 円柱カッターの全ポリゴン: 1
- 断面カッターの全ポリゴン: 2
4. 殻モードの場合、球体のSolidifyモディファイアで以下を設定します。
- offset = -1.0
- material_offset = 3 (内壁をインデックス3にする)
- material_offset_rim = 2 (縁をインデックス2にする)
5. Booleanモディファイアで operation = 'DIFFERENCE' と material_mode = 'TRANSFER' を適用します。
【UI (Nパネル)】
タブ名: "Cam Sphere"
- 「選択オブジェクトを中央に表示」ボタン
- 「トンネル (無垢)」「内部空洞 (殻)」の実行ボタン
- 現在のモード名とエラーメッセージの表示
- 上記4色のカラーピッカー (FloatVectorProperty)
- アドオン解除ボタン
【その他仕様】
- ボタンを押すたびに、古いオブジェクト(球体、カッター類)を削除してから新規生成すること。
- Boolean適用後、カッターオブジェクトは削除すること。
- パラメータ(半径、サイズ、色など)はコード先頭に定数としてまとめること。
"""
bl_info = {
"name": "Cam Sphere Boolean",
"version": (2, 3),
"blender": (5, 0, 0),
"category": "Object"
}
# =============================================================================
# 【ユーザーパラメータ(コード先頭)】
# =============================================================================
# --- 球体 ---
SPHERE_RADIUS = 5.0
SPHERE_SEGMENTS = 96
SPHERE_RING_COUNT = 64
SHELL_THICKNESS = 0.5 # 「内部空洞(殻)」モード時の、殻の厚み
# --- 円柱穴 ---
CYLINDER_RADIUS = 1.5
CYLINDER_DEPTH = 15.0
CYLINDER_VERTICES = 96
CYLINDER_AXIS = "Z" # 対応: "X", "Y", "Z"
# --- 半球断面のカット位置 ---
SECTION_AXIS = "X" # 対応: "X", "Y", "Z"
SECTION_POSITION = 0.0
SECTION_KEEP = "POSITIVE" # "POSITIVE" なら SECTION_AXIS >= 0 を残す
SECTION_CUTTER_SIZE = 30.0 # 断面カッターのサイズ
# --- デフォルト色 (4色構成に拡張) ---
OUTER_SURFACE_COLOR = (0.05, 0.20, 0.90, 1.0) # 青 (外表面)
CYLINDER_HOLE_COLOR = (0.90, 0.08, 0.03, 1.0) # 赤 (円柱との接触面)
SECTION_COLOR = (0.05, 0.80, 0.15, 1.0) # 緑 (半球切断面)
HOLLOW_WALL_COLOR = (0.80, 0.60, 0.10, 1.0) # 黄 (空洞の内壁 ※殻モード時のみ)
# --- Collection & Naming ---
COLLECTION_NAME = "CAM_Sphere_Boolean"
SPHERE_NAME = "CAM_Sphere"
CYL_CUTTER_NAME = "CAM_Cylinder_Cutter"
SEC_CUTTER_NAME = "CAM_Section_Cutter"
# =============================================================================
# 【コアロジック】
# =============================================================================
import bpy
import math
from bpy.props import FloatVectorProperty, StringProperty, PointerProperty
def clear_objects():
"""古い生成物を完全に削除する"""
for name in [SPHERE_NAME, CYL_CUTTER_NAME, SEC_CUTTER_NAME]:
obj = bpy.data.objects.get(name)
if obj:
bpy.data.objects.remove(obj, do_unlink=True)
def move_to_collection(obj, coll_name):
"""オブジェクトを指定のコレクションに配置する"""
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
for c in obj.users_collection:
c.objects.unlink(obj)
coll.objects.link(obj)
def get_or_create_material(mat_name, color):
"""マテリアルを取得または新規作成して色を設定する"""
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
def get_materials_list():
"""UIのカラー設定を反映した4つのマテリアルを固定順序のリストで返す"""
props = bpy.context.scene.cam_sphere_props
# スロット順序を固定: 0=外側, 1=円柱, 2=断面, 3=内壁
return [
get_or_create_material("CAM_Outer_Mat", props.outer_color),
get_or_create_material("CAM_Cyl_Mat", props.cylinder_color),
get_or_create_material("CAM_Section_Mat", props.section_color),
get_or_create_material("CAM_Hollow_Mat", props.hollow_color),
]
def update_colors(self, context):
get_materials_list()
def apply_shared_materials(obj, mats, default_index):
"""オブジェクトに共通の4色を割り当て、全面のインデックスを指定の番号に強制する"""
# 4色すべてを同じ順番でスロットに登録する(Booleanでの色ズレを防ぐため必須)
for m in mats:
obj.data.materials.append(m)
# オブジェクトの全面を指定した色(インデックス)にする
for p in obj.data.polygons:
p.material_index = default_index
def create_cylinder_cutter(name):
"""円柱カッターを作成する"""
rot = (0, 0, 0)
if CYLINDER_AXIS == "X": rot = (0, math.radians(90), 0)
elif CYLINDER_AXIS == "Y": rot = (math.radians(90), 0, 0)
bpy.ops.mesh.primitive_cylinder_add(vertices=CYLINDER_VERTICES, radius=CYLINDER_RADIUS, depth=CYLINDER_DEPTH, rotation=rot)
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def create_section_cutter(name):
"""半球にするための断面カッターを作成する"""
loc = [0, 0, 0]
half = SECTION_CUTTER_SIZE / 2.0
if SECTION_AXIS == "X":
loc[0] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Y":
loc[1] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Z":
loc[2] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
bpy.ops.mesh.primitive_cube_add(size=SECTION_CUTTER_SIZE, location=tuple(loc))
cutter = bpy.context.active_object
cutter.name = name
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def apply_boolean(target, cutter, mod_name):
"""Boolean Differenceを適用する"""
bpy.context.view_layer.objects.active = target
mod = target.modifiers.new(name=mod_name, type='BOOLEAN')
mod.object = cutter
mod.operation = 'DIFFERENCE'
mod.solver = 'EXACT'
if hasattr(mod, "material_mode"):
mod.material_mode = 'TRANSFER'
bpy.ops.object.modifier_apply(modifier=mod_name)
# =============================================================================
# 【プロパティ定義】
# =============================================================================
class CAMSphereProps(bpy.types.PropertyGroup):
current_mode_label: StringProperty(default="なし")
error_message: StringProperty(default="")
outer_color: FloatVectorProperty(name="外表面", subtype='COLOR', size=4, default=OUTER_SURFACE_COLOR, update=update_colors)
cylinder_color: FloatVectorProperty(name="円柱との接触面", subtype='COLOR', size=4, default=CYLINDER_HOLE_COLOR, update=update_colors)
section_color: FloatVectorProperty(name="半球の切断面", subtype='COLOR', size=4, default=SECTION_COLOR, update=update_colors)
hollow_color: FloatVectorProperty(name="空洞の内壁", subtype='COLOR', size=4, default=HOLLOW_WALL_COLOR, update=update_colors)
# =============================================================================
# 【オペレーター (機能)】
# =============================================================================
class OT_cam_create_tunnel(bpy.types.Operator):
bl_idname = "view3d.cam_create_tunnel"
bl_label = "トンネル壁面 (無垢)"
bl_description = "中身が詰まった球体に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 無垢の球体を作成 (インデックス0: 青色)
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 円柱カッターを作成 (インデックス1: 赤色)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
# 3. 断面カッターを作成 (インデックス2: 緑色)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 4. Boolean適用 (カッターの色が正確に転送される)
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 5. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "トンネル壁面 (無垢の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
class OT_cam_create_hollow(bpy.types.Operator):
bl_idname = "view3d.cam_create_hollow"
bl_label = "内部空洞 (殻)"
bl_description = "中身が空っぽの殻に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mats = get_materials_list()
# 1. 球体を作成 (インデックス0: 青色)
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
apply_shared_materials(sphere, mats, default_index=0)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 殻(シェル)にする (Solidifyで厚みの内側と縁に色を付ける)
sol = sphere.modifiers.new(name="Solidify", type='SOLIDIFY')
sol.thickness = SHELL_THICKNESS
sol.offset = -1.0
sol.material_offset = 3 # 厚みの内側をインデックス3 (黄色)にする
sol.material_offset_rim = 2 # 縁の部分をインデックス2 (緑色)にする
bpy.ops.object.modifier_apply(modifier="Solidify")
# 3. 円柱カッターを作成 (インデックス1: 赤色)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME)
apply_shared_materials(cyl, mats, default_index=1)
# 4. 断面カッターを作成 (インデックス2: 緑色)
sec = create_section_cutter(SEC_CUTTER_NAME)
apply_shared_materials(sec, mats, default_index=2)
# 5. Boolean適用
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 6. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "内部空洞 (殻の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
# =============================================================================
# 【UI・その他】
# =============================================================================
class OT_cam_focus_selected(bpy.types.Operator):
bl_idname = "view3d.cam_focus_selected"
bl_label = "選択オブジェクトを中央に表示"
def execute(self, context):
if context.selected_objects:
bpy.ops.view3d.view_selected()
return {'FINISHED'}
class OT_cam_remove_addon(bpy.types.Operator):
bl_idname = "wm.cam_remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
class VIEW3D_PT_cam_sphere(bpy.types.Panel):
bl_label = "Cam Sphere"
bl_idname = "VIEW3D_PT_cam_sphere"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cam Sphere"
def draw(self, context):
layout = self.layout
props = context.scene.cam_sphere_props
layout.operator("view3d.cam_focus_selected", icon='ZOOM_SELECTED')
layout.separator()
layout.label(text="生成モード (どちらも半球になります)")
row = layout.row(align=True)
row.operator("view3d.cam_create_tunnel", text="トンネル (無垢)")
row.operator("view3d.cam_create_hollow", text="内部空洞 (殻)")
layout.separator()
layout.label(text=f"現在:{props.current_mode_label}")
if props.error_message:
layout.label(text=props.error_message, icon='ERROR')
layout.separator()
layout.label(text="面の色")
row = layout.row()
row.prop(props, "outer_color", text="球の外側表面")
row = layout.row()
row.prop(props, "cylinder_color", text="円柱との接触面")
row = layout.row()
row.prop(props, "section_color", text="半球の切断面")
row = layout.row()
row.prop(props, "hollow_color", text="空洞の内壁 (※殻モード時)")
layout.separator()
box = layout.box()
box.operator("wm.cam_remove_addon", icon='CANCEL')
# =============================================================================
# 【登録処理】
# =============================================================================
def initialize_viewport():
wm = bpy.context.window_manager
if not wm: return 0.2
for window in wm.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
if space:
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
classes = (
CAMSphereProps,
OT_cam_create_tunnel,
OT_cam_create_hollow,
OT_cam_focus_selected,
OT_cam_remove_addon,
VIEW3D_PT_cam_sphere,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.cam_sphere_props = PointerProperty(type=CAMSphereProps)
if not bpy.app.timers.is_registered(initialize_viewport):
bpy.app.timers.register(initialize_viewport, first_interval=0.2)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "cam_sphere_props"):
del bpy.types.Scene.cam_sphere_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Cam Sphere Boolean",
"version": (2, 1),
"blender": (5, 0, 0),
"category": "Object"
}
# =============================================================================
# 【ユーザーパラメータ(コード先頭)】
# =============================================================================
# --- 球体 ---
SPHERE_RADIUS = 5.0
SPHERE_SEGMENTS = 96
SPHERE_RING_COUNT = 64
SHELL_THICKNESS = 0.5 # 「内部空洞(殻)」モード時の、殻の厚み
# --- 円柱穴 ---
CYLINDER_RADIUS = 1.5
CYLINDER_DEPTH = 15.0
CYLINDER_VERTICES = 96
CYLINDER_AXIS = "Z" # 対応: "X", "Y", "Z"
# --- 半球断面のカット位置 ---
SECTION_AXIS = "X" # 対応: "X", "Y", "Z"
SECTION_POSITION = 0.0
SECTION_KEEP = "POSITIVE" # "POSITIVE" なら SECTION_AXIS >= 0 を残す
SECTION_CUTTER_SIZE = 30.0 # 断面カッターのサイズ
# --- デフォルト色 ---
OUTER_SURFACE_COLOR = (0.05, 0.20, 0.90, 1.0) # 青 (外表面)
INNER_WALL_COLOR = (0.90, 0.08, 0.03, 1.0) # 赤 (空洞内壁)
SECTION_COLOR = (0.05, 0.80, 0.15, 1.0) # 緑 (切断面・トンネル壁面)
# --- Collection & Naming ---
COLLECTION_NAME = "CAM_Sphere_Boolean"
SPHERE_NAME = "CAM_Sphere"
CYL_CUTTER_NAME = "CAM_Cylinder_Cutter"
SEC_CUTTER_NAME = "CAM_Section_Cutter"
# =============================================================================
# 【コアロジック】
# =============================================================================
import bpy
import math
from bpy.props import FloatVectorProperty, StringProperty, PointerProperty
def clear_objects():
"""古い生成物を完全に削除する"""
for name in [SPHERE_NAME, CYL_CUTTER_NAME, SEC_CUTTER_NAME]:
obj = bpy.data.objects.get(name)
if obj:
bpy.data.objects.remove(obj, do_unlink=True)
def move_to_collection(obj, coll_name):
"""オブジェクトを指定のコレクションに配置する"""
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
for c in obj.users_collection:
c.objects.unlink(obj)
coll.objects.link(obj)
def get_or_create_material(mat_name, color):
"""マテリアルを取得または新規作成して色を設定する"""
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
def setup_materials():
"""UIのカラー設定をマテリアルに反映させる"""
props = bpy.context.scene.cam_sphere_props
mat_outer = get_or_create_material("CAM_Outer_Mat", props.outer_color)
mat_inner = get_or_create_material("CAM_Inner_Mat", props.inner_color)
mat_sec = get_or_create_material("CAM_Section_Mat", props.section_color)
return mat_outer, mat_inner, mat_sec
def update_colors(self, context):
setup_materials()
def create_cylinder_cutter(name, mat):
"""円柱カッターを作成し、指定マテリアルを付与する"""
rot = (0, 0, 0)
if CYLINDER_AXIS == "X": rot = (0, math.radians(90), 0)
elif CYLINDER_AXIS == "Y": rot = (math.radians(90), 0, 0)
bpy.ops.mesh.primitive_cylinder_add(vertices=CYLINDER_VERTICES, radius=CYLINDER_RADIUS, depth=CYLINDER_DEPTH, rotation=rot)
cutter = bpy.context.active_object
cutter.name = name
cutter.data.materials.append(mat)
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def create_section_cutter(name, mat):
"""半球にするための断面カッター(立方体)を作成し、指定マテリアルを付与する"""
loc = [0, 0, 0]
half = SECTION_CUTTER_SIZE / 2.0
if SECTION_AXIS == "X":
loc[0] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Y":
loc[1] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
elif SECTION_AXIS == "Z":
loc[2] = SECTION_POSITION - half if SECTION_KEEP == "POSITIVE" else SECTION_POSITION + half
bpy.ops.mesh.primitive_cube_add(size=SECTION_CUTTER_SIZE, location=tuple(loc))
cutter = bpy.context.active_object
cutter.name = name
cutter.data.materials.append(mat)
move_to_collection(cutter, COLLECTION_NAME)
return cutter
def apply_boolean(target, cutter, mod_name):
"""Boolean Differenceを適用する"""
bpy.context.view_layer.objects.active = target
mod = target.modifiers.new(name=mod_name, type='BOOLEAN')
mod.object = cutter
mod.operation = 'DIFFERENCE'
mod.solver = 'EXACT'
bpy.ops.object.modifier_apply(modifier=mod_name)
# =============================================================================
# 【プロパティ定義】
# =============================================================================
class CAMSphereProps(bpy.types.PropertyGroup):
current_mode_label: StringProperty(default="なし")
error_message: StringProperty(default="")
outer_color: FloatVectorProperty(name="外表面", subtype='COLOR', size=4, default=OUTER_SURFACE_COLOR, update=update_colors)
inner_color: FloatVectorProperty(name="内部 (空洞内壁)", subtype='COLOR', size=4, default=INNER_WALL_COLOR, update=update_colors)
section_color: FloatVectorProperty(name="断面・削り口", subtype='COLOR', size=4, default=SECTION_COLOR, update=update_colors)
# =============================================================================
# 【オペレーター (機能)】
# =============================================================================
class OT_cam_create_tunnel(bpy.types.Operator):
bl_idname = "view3d.cam_create_tunnel"
bl_label = "トンネル壁面 (半球)"
bl_description = "中身が詰まった球体に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mat_outer, mat_inner, mat_sec = setup_materials()
# 1. 無垢の球体を作成
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
sphere.data.materials.append(mat_outer)
move_to_collection(sphere, COLLECTION_NAME)
# 2. カッターを作成(★トンネルの壁面も「切断面」の色にする)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME, mat_sec)
sec = create_section_cutter(SEC_CUTTER_NAME, mat_sec)
# 3. Boolean適用 (カッター側のマテリアルが自動的に切り口に転送される)
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 4. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "トンネル壁面 (無垢の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
class OT_cam_create_hollow(bpy.types.Operator):
bl_idname = "view3d.cam_create_hollow"
bl_label = "内部空洞 (半球)"
bl_description = "中身が空っぽの殻に穴を開け、半分に切断します"
def execute(self, context):
props = context.scene.cam_sphere_props
props.error_message = ""
clear_objects()
try:
mat_outer, mat_inner, mat_sec = setup_materials()
# 1. 球体を作成
bpy.ops.mesh.primitive_uv_sphere_add(radius=SPHERE_RADIUS, segments=SPHERE_SEGMENTS, ring_count=SPHERE_RING_COUNT)
sphere = context.active_object
sphere.name = SPHERE_NAME
# Solidifyでの割り当て順序として 0:外側 1:内側 2:縁(断面) を用意する
sphere.data.materials.append(mat_outer)
sphere.data.materials.append(mat_inner)
sphere.data.materials.append(mat_sec)
move_to_collection(sphere, COLLECTION_NAME)
# 2. 殻(シェル)にする
sol = sphere.modifiers.new(name="Solidify", type='SOLIDIFY')
sol.thickness = SHELL_THICKNESS
sol.offset = -1.0 # 内側へ厚み付け
sol.material_offset = 1 # 内側をSlot 1(inner)にする
sol.material_offset_rim = 2 # 縁をSlot 2(sec)にする
bpy.ops.object.modifier_apply(modifier="Solidify")
# 3. カッターを作成(切り口は全て断面色になる)
cyl = create_cylinder_cutter(CYL_CUTTER_NAME, mat_sec)
sec = create_section_cutter(SEC_CUTTER_NAME, mat_sec)
# 4. Boolean適用
apply_boolean(sphere, cyl, "Bool_Cyl")
apply_boolean(sphere, sec, "Bool_Sec")
# 5. カッター削除
bpy.data.objects.remove(cyl, do_unlink=True)
bpy.data.objects.remove(sec, do_unlink=True)
props.current_mode_label = "内部空洞 (殻の半球)"
except Exception as e:
props.error_message = f"エラー: {str(e)}"
return {'CANCELLED'}
return {'FINISHED'}
# =============================================================================
# 【UI・その他】
# =============================================================================
class OT_cam_focus_selected(bpy.types.Operator):
bl_idname = "view3d.cam_focus_selected"
bl_label = "選択オブジェクトを中央に表示"
def execute(self, context):
if context.selected_objects:
bpy.ops.view3d.view_selected()
return {'FINISHED'}
class OT_cam_remove_addon(bpy.types.Operator):
bl_idname = "wm.cam_remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
class VIEW3D_PT_cam_sphere(bpy.types.Panel):
bl_label = "Cam Sphere"
bl_idname = "VIEW3D_PT_cam_sphere"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cam Sphere"
def draw(self, context):
layout = self.layout
props = context.scene.cam_sphere_props
layout.operator("view3d.cam_focus_selected", icon='ZOOM_SELECTED')
layout.separator()
layout.label(text="生成モード (どちらも半球になります)")
row = layout.row(align=True)
row.operator("view3d.cam_create_tunnel", text="トンネル (無垢)")
row.operator("view3d.cam_create_hollow", text="内部空洞 (殻)")
layout.separator()
layout.label(text=f"現在:{props.current_mode_label}")
if props.error_message:
layout.label(text=props.error_message, icon='ERROR')
layout.separator()
layout.label(text="面の色")
row = layout.row()
row.prop(props, "outer_color", text="球の外側表面")
row = layout.row()
row.prop(props, "inner_color", text="内部(空洞の内壁)")
row = layout.row()
row.prop(props, "section_color", text="断面・削り口")
layout.separator()
box = layout.box()
box.operator("wm.cam_remove_addon", icon='CANCEL')
# =============================================================================
# 【登録処理】
# =============================================================================
def initialize_viewport():
wm = bpy.context.window_manager
if not wm: return 0.2
for window in wm.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
if space:
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
classes = (
CAMSphereProps,
OT_cam_create_tunnel,
OT_cam_create_hollow,
OT_cam_focus_selected,
OT_cam_remove_addon,
VIEW3D_PT_cam_sphere,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.cam_sphere_props = PointerProperty(type=CAMSphereProps)
if not bpy.app.timers.is_registered(initialize_viewport):
bpy.app.timers.register(initialize_viewport, first_interval=0.2)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "cam_sphere_props"):
del bpy.types.Scene.cam_sphere_props
if __name__ == "__main__":
register()