骨格 立方体作成リスト
円柱の組み合わせ 一体化で 骨格立方体を
初期値000中心んに 作る
xyz 平行移動
xyz 回転
円柱 太さ 色 透明度
円柱1辺の長さ
骨格立方体になる 円柱12本の一体化
リアルタイムで 長さとかを 変更
作成ボタンと
アドオンからの切り離し ボタン
アドオン切り離し後に Objectは 変更を受けない 形も色も


# =========================================================
# Blender 5+ Skeleton Cube Generator
# 12本の円柱を一体化した骨格立方体
# =========================================================
bl_info = {
"name": "Skeleton Cube Generator",
"author": "AI Assistant",
"version": (1, 0, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Skeleton Cube",
"description": "Generates a skeletal cube from 12 integrated cylinders.",
"category": "3D View",
}
# =========================================================
# Code Created
# =========================================================
# Code Created : 2026_0818_2100
# =========================================================
# 1. import
# =========================================================
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
# =========================================================
# 2. ユーザー設定パラメータ
# =========================================================
DEFAULT_CENTER_X = 0.0
DEFAULT_CENTER_Y = 0.0
DEFAULT_CENTER_Z = 0.0
DEFAULT_ROTATION_X = 0.0
DEFAULT_ROTATION_Y = 0.0
DEFAULT_ROTATION_Z = 0.0
DEFAULT_EDGE_LENGTH = 2.0
DEFAULT_CYLINDER_RADIUS = 0.05
DEFAULT_COLOR_R = 0.2
DEFAULT_COLOR_G = 0.6
DEFAULT_COLOR_B = 1.0
DEFAULT_ALPHA = 1.0
DEFAULT_CYLINDER_SEGMENTS = 32
# =========================================================
# 3. 共通定義
# =========================================================
ADDON_TITLE = "Skeleton Cube"
PREFIX_VAL = "SKELETON_CUBE"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Skeleton Cube"
OBJECT_NAME = f"{PREFIX_VAL}_OBJECT"
MESH_NAME = f"{PREFIX_VAL}_MESH"
MATERIAL_NAME = f"{PREFIX_VAL}_MATERIAL"
# =========================================================
# 4. リンク定義
# =========================================================
# 1番はユーザー指定用として空ける
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
# =========================================================
# 5. グローバル状態
# =========================================================
_IS_UPDATING = False
_IS_DETACHING = False
# =========================================================
# 6. Utility
# =========================================================
def get_skeleton_object():
"""このアドオンが生成したSkeleton Cubeを取得"""
obj = bpy.data.objects.get(OBJECT_NAME)
if obj and obj.type == 'MESH':
return obj
return None
# =========================================================
# 7. Material
# =========================================================
def get_or_create_material():
mat = bpy.data.materials.get(MATERIAL_NAME)
if mat is None:
mat = bpy.data.materials.new(MATERIAL_NAME)
mat.use_nodes = True
nodes = mat.node_tree.nodes
principled = nodes.get("Principled BSDF")
if principled is None:
principled = nodes.new("ShaderNodeBsdfPrincipled")
return mat
def update_material(props):
"""
色と透明度をMaterialへ反映
"""
obj = get_skeleton_object()
if obj is None:
return
mat = get_or_create_material()
color = (
props.color[0],
props.color[1],
props.color[2],
props.opacity,
)
principled = mat.node_tree.nodes.get("Principled BSDF")
if principled:
# Blender 5.x のPrincipled BSDF
if "Base Color" in principled.inputs:
principled.inputs["Base Color"].default_value = color
if "Alpha" in principled.inputs:
principled.inputs["Alpha"].default_value = props.opacity
# 透明度を実際に表示できるようにTransmission
# ではなくAlpha設定を利用
if "Roughness" in principled.inputs:
principled.inputs["Roughness"].default_value = 0.35
# Material Settings
try:
mat.surface_render_method = 'DITHERED'
except Exception:
pass
try:
mat.diffuse_color = color
except Exception:
pass
# 既存マテリアルを整理
obj.data.materials.clear()
obj.data.materials.append(mat)
# =========================================================
# 8. 円柱メッシュ生成
# =========================================================
def add_cylinder_between(bm, start, end, radius, segments):
"""
start → end の間に円柱を生成して
bmeshへ直接追加する。
Objectは生成しない。
したがって最終的に12本すべてが
1つのMeshになる。
"""
start = Vector(start)
end = Vector(end)
direction = end - start
length = direction.length
if length <= 0.000001:
return
midpoint = (start + end) * 0.5
# bmesh.ops.create_coneで円柱を生成
result = bmesh.ops.create_cone(
bm,
cap_ends=True,
cap_tris=False,
segments=segments,
radius1=radius,
radius2=radius,
depth=length,
)
verts = result["verts"]
# 円柱は最初Z軸方向
z_axis = Vector((0.0, 0.0, 1.0))
direction_normalized = direction.normalized()
rotation = z_axis.rotation_difference(direction_normalized)
for v in verts:
v.co = rotation @ v.co
v.co += midpoint
# =========================================================
# 9. 12本の円柱による骨格立方体生成
# =========================================================
def build_skeleton_mesh(edge_length, radius):
"""
12本の円柱を1つのbmeshへ直接生成する。
"""
bm = bmesh.new()
h = edge_length * 0.5
# -----------------------------------------------------
# 8頂点
# -----------------------------------------------------
v0 = Vector((-h, -h, -h))
v1 = Vector(( h, -h, -h))
v2 = Vector(( h, h, -h))
v3 = Vector((-h, h, -h))
v4 = Vector((-h, -h, h))
v5 = Vector(( h, -h, h))
v6 = Vector(( h, h, h))
v7 = Vector((-h, h, h))
edges = [
# 下
(v0, v1),
(v1, v2),
(v2, v3),
(v3, v0),
# 上
(v4, v5),
(v5, v6),
(v6, v7),
(v7, v4),
# 縦
(v0, v4),
(v1, v5),
(v2, v6),
(v3, v7),
]
# -----------------------------------------------------
# 12本生成
# -----------------------------------------------------
for start, end in edges:
add_cylinder_between(
bm,
start,
end,
radius,
DEFAULT_CYLINDER_SEGMENTS,
)
# -----------------------------------------------------
# Meshへ変換
# -----------------------------------------------------
mesh = bpy.data.meshes.new(MESH_NAME)
bm.to_mesh(mesh)
bm.free()
mesh.update()
return mesh
# =========================================================
# 10. Skeleton Cube生成
# =========================================================
def create_or_update_skeleton(context):
global _IS_UPDATING
if _IS_UPDATING:
return
if _IS_DETACHING:
return
props = context.scene.skeleton_cube_props
_IS_UPDATING = True
try:
obj = get_skeleton_object()
# -------------------------------------------------
# Objectがない場合
# -------------------------------------------------
if obj is None:
mesh = build_skeleton_mesh(
props.edge_length,
props.cylinder_radius,
)
obj = bpy.data.objects.new(
OBJECT_NAME,
mesh,
)
context.collection.objects.link(obj)
# -------------------------------------------------
# 既存Objectがある場合
# Meshを作り直す
# -------------------------------------------------
else:
new_mesh = build_skeleton_mesh(
props.edge_length,
props.cylinder_radius,
)
old_mesh = obj.data
obj.data = new_mesh
# 古いMeshを削除
if old_mesh and old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
# -------------------------------------------------
# Transform
# -------------------------------------------------
obj.location = (
props.center_x,
props.center_y,
props.center_z,
)
obj.rotation_mode = 'XYZ'
obj.rotation_euler = (
math.radians(props.rotation_x),
math.radians(props.rotation_y),
math.radians(props.rotation_z),
)
# -------------------------------------------------
# Material
# -------------------------------------------------
update_material(props)
# -------------------------------------------------
# 表示
# -------------------------------------------------
obj.hide_viewport = False
obj.hide_render = False
obj.select_set(True)
context.view_layer.objects.active = obj
finally:
_IS_UPDATING = False
# =========================================================
# 11. Property Update
# =========================================================
def property_update(self, context):
"""
UIの数値変更時にリアルタイム更新
"""
if _IS_DETACHING:
return
create_or_update_skeleton(context)
# =========================================================
# 12. PropertyGroup
# =========================================================
class SKELETON_CUBE_PG_Properties(bpy.types.PropertyGroup):
# -----------------------------------------------------
# Position
# -----------------------------------------------------
center_x: bpy.props.FloatProperty(
name="位置 X",
default=DEFAULT_CENTER_X,
update=property_update,
)
center_y: bpy.props.FloatProperty(
name="位置 Y",
default=DEFAULT_CENTER_Y,
update=property_update,
)
center_z: bpy.props.FloatProperty(
name="位置 Z",
default=DEFAULT_CENTER_Z,
update=property_update,
)
# -----------------------------------------------------
# Rotation
# -----------------------------------------------------
rotation_x: bpy.props.FloatProperty(
name="回転 X",
default=DEFAULT_ROTATION_X,
update=property_update,
)
rotation_y: bpy.props.FloatProperty(
name="回転 Y",
default=DEFAULT_ROTATION_Y,
update=property_update,
)
rotation_z: bpy.props.FloatProperty(
name="回転 Z",
default=DEFAULT_ROTATION_Z,
update=property_update,
)
# -----------------------------------------------------
# Cube
# -----------------------------------------------------
edge_length: bpy.props.FloatProperty(
name="立方体1辺",
default=DEFAULT_EDGE_LENGTH,
min=0.001,
soft_max=1000.0,
update=property_update,
)
# -----------------------------------------------------
# Cylinder
# -----------------------------------------------------
cylinder_radius: bpy.props.FloatProperty(
name="円柱半径",
default=DEFAULT_CYLINDER_RADIUS,
min=0.0001,
soft_max=10.0,
update=property_update,
)
# -----------------------------------------------------
# Color
# -----------------------------------------------------
color: bpy.props.FloatVectorProperty(
name="色",
subtype='COLOR',
size=4,
default=(
DEFAULT_COLOR_R,
DEFAULT_COLOR_G,
DEFAULT_COLOR_B,
DEFAULT_ALPHA,
),
min=0.0,
max=1.0,
update=property_update,
)
# -----------------------------------------------------
# Opacity
# -----------------------------------------------------
opacity: bpy.props.FloatProperty(
name="透明度",
default=DEFAULT_ALPHA,
min=0.0,
max=1.0,
update=property_update,
)
# =========================================================
# 13. Create Operator
# =========================================================
class SKELETON_CUBE_OT_Create(bpy.types.Operator):
bl_idname = f"{PREFIX}.create"
bl_label = "骨格立方体を作成"
bl_description = "12本の円柱から骨格立方体を生成します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
create_or_update_skeleton(context)
self.report(
{'INFO'},
"骨格立方体を作成しました",
)
return {'FINISHED'}
# =========================================================
# 14. Link Operator
# =========================================================
class SKELETON_CUBE_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
try:
webbrowser.open(self.url)
except Exception as e:
self.report(
{'ERROR'},
f"URLを開けません: {e}",
)
return {'CANCELLED'}
return {'FINISHED'}
# =========================================================
# 15. Addon Detach / Remove
# =========================================================
class SKELETON_CUBE_OT_RemoveAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "アドオンから切り離す"
bl_description = (
"現在の骨格立方体を残したまま、"
"アドオンを解除します"
)
def execute(self, context):
global _IS_DETACHING
# -------------------------------------------------
# 切り離し開始
# -------------------------------------------------
_IS_DETACHING = True
obj = get_skeleton_object()
# -------------------------------------------------
# 現在状態を確定
# -------------------------------------------------
if obj:
obj.select_set(True)
context.view_layer.objects.active = obj
# 現在のGeometryを確定
if obj.data:
obj.data.update()
# -------------------------------------------------
# リアルタイムCallbackを停止
# -------------------------------------------------
# PropertyGroup解除前に状態を保持する
scene = context.scene
# -------------------------------------------------
# 安全な遅延unregister
# -------------------------------------------------
try:
bpy.app.timers.register(
unregister,
first_interval=0.01,
)
except Exception as e:
_IS_DETACHING = False
self.report(
{'ERROR'},
f"アドオン解除に失敗しました: {e}",
)
return {'CANCELLED'}
self.report(
{'INFO'},
"アドオンから切り離しました。Objectは保持されています。",
)
return {'FINISHED'}
# =========================================================
# 16. Main Panel
# =========================================================
class SKELETON_CUBE_PT_Main(bpy.types.Panel):
bl_label = "Skeleton Cube"
bl_idname = f"{PREFIX_VAL}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_order = 0
def draw(self, context):
layout = self.layout
props = context.scene.skeleton_cube_props
# -------------------------------------------------
# Position
# -------------------------------------------------
box = layout.box()
box.label(
text="中心位置",
icon='OBJECT_ORIGIN',
)
col = box.column(align=True)
col.prop(
props,
"center_x",
)
col.prop(
props,
"center_y",
)
col.prop(
props,
"center_z",
)
# -------------------------------------------------
# Rotation
# -------------------------------------------------
box = layout.box()
box.label(
text="回転",
icon='ORIENTATION_GIMBAL',
)
col = box.column(align=True)
col.prop(
props,
"rotation_x",
)
col.prop(
props,
"rotation_y",
)
col.prop(
props,
"rotation_z",
)
# -------------------------------------------------
# Geometry
# -------------------------------------------------
box = layout.box()
box.label(
text="骨格立方体",
icon='CUBE',
)
box.prop(
props,
"edge_length",
)
box.prop(
props,
"cylinder_radius",
)
# -------------------------------------------------
# Material
# -------------------------------------------------
box = layout.box()
box.label(
text="マテリアル",
icon='MATERIAL',
)
box.prop(
props,
"color",
text="色",
)
box.prop(
props,
"opacity",
text="透明度",
)
# -------------------------------------------------
# Create
# -------------------------------------------------
layout.separator()
row = layout.row()
row.scale_y = 1.5
row.operator(
SKELETON_CUBE_OT_Create.bl_idname,
icon='ADD',
)
# =========================================================
# 17. Links Panel
# =========================================================
class SKELETON_CUBE_PT_LinksPanel(bpy.types.Panel):
bl_label = "リンク"
bl_idname = f"{PREFIX_VAL}_PT_links_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_order = 1
def draw(self, context):
layout = self.layout
box = layout.box()
if LINK_NAME_2:
op2 = box.operator(
SKELETON_CUBE_OT_OpenUrl.bl_idname,
text=LINK_NAME_2,
icon='URL',
)
op2.url = LINK_URL_2
if LINK_NAME_3:
op3 = box.operator(
SKELETON_CUBE_OT_OpenUrl.bl_idname,
text=LINK_NAME_3,
icon='URL',
)
op3.url = LINK_URL_3
if LINK_NAME_4:
op4 = box.operator(
SKELETON_CUBE_OT_OpenUrl.bl_idname,
text=LINK_NAME_4,
icon='URL',
)
op4.url = LINK_URL_4
# =========================================================
# 18. Remove / Detach Panel
# =========================================================
class SKELETON_CUBE_PT_RemovePanel(bpy.types.Panel):
bl_label = "アドオンから切り離し"
bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 2
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(
text="Objectを残してアドオンを解除",
icon='ERROR',
)
box.label(
text="現在の状態で完全に固定されます。",
icon='INFO',
)
box.separator()
row = box.row()
row.scale_y = 1.5
row.operator(
SKELETON_CUBE_OT_RemoveAddon.bl_idname,
icon='UNLINKED',
)
# =========================================================
# 19. Classes
# =========================================================
classes = (
SKELETON_CUBE_PG_Properties,
SKELETON_CUBE_OT_Create,
SKELETON_CUBE_OT_OpenUrl,
SKELETON_CUBE_OT_RemoveAddon,
SKELETON_CUBE_PT_Main,
SKELETON_CUBE_PT_LinksPanel,
SKELETON_CUBE_PT_RemovePanel,
)
# =========================================================
# 20. Register
# =========================================================
def register():
for cls in classes:
try:
bpy.utils.register_class(cls)
except ValueError:
# 二重登録対策
try:
bpy.utils.unregister_class(cls)
except Exception:
pass
bpy.utils.register_class(cls)
bpy.types.Scene.skeleton_cube_props = bpy.props.PointerProperty(
type=SKELETON_CUBE_PG_Properties
)
# =========================================================
# 21. Unregister
# =========================================================
def unregister():
global _IS_DETACHING
_IS_DETACHING = True
# -----------------------------------------------------
# Property解除
# -----------------------------------------------------
if hasattr(
bpy.types.Scene,
"skeleton_cube_props",
):
del bpy.types.Scene.skeleton_cube_props
# -----------------------------------------------------
# Class解除
# -----------------------------------------------------
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
except Exception:
pass
# -----------------------------------------------------
# 重要
#
# 生成Objectは削除しない。
#
# bpy.data.objects.remove()
# bpy.data.meshes.remove()
#
# はここでは実行しない。
# -----------------------------------------------------
# =========================================================
# 22. Run
# =========================================================
if __name__ == "__main__":
register()