Sidebar (Nパネル)", "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール", "category": "Object", } import bpy import bmesh import webbrowser import math import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Camera 20260704" PREFIX_NAME = "camrigdon" RIG_COLLECTION_NAME = "CamRig_Collection" PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_") # ========================================================================= # 【カメラ軌道制御用アップデート関数】 # ========================================================================= def get_curve_target(cam, track_name): for const in cam.constraints: if const.type == 'FOLLOW_PATH' and const.target: if const.target"> Sidebar (Nパネル)", "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール", "category": "Object", } import bpy import bmesh import webbrowser import math import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Camera 20260704" PREFIX_NAME = "camrigdon" RIG_COLLECTION_NAME = "CamRig_Collection" PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_") # ========================================================================= # 【カメラ軌道制御用アップデート関数】 # ========================================================================= def get_curve_target(cam, track_name): for const in cam.constraints: if const.type == 'FOLLOW_PATH' and const.target: if const.target"> Sidebar (Nパネル)", "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール", "category": "Object", } import bpy import bmesh import webbrowser import math import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Camera 20260704" PREFIX_NAME = "camrigdon" RIG_COLLECTION_NAME = "CamRig_Collection" PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_") # ========================================================================= # 【カメラ軌道制御用アップデート関数】 # ========================================================================= def get_curve_target(cam, track_name): for const in cam.constraints: if const.type == 'FOLLOW_PATH' and const.target: if const.target">
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 13),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type in ['TRACK_TO', 'COPY_ROTATION']), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
if cutter_back:
r_back = max(0.001, self.cam_shield_hole_radius_back)
cutter_back.scale = (r_back, r_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
def update_target_empty_visibility(self, context):
for obj in bpy.data.objects:
if obj.name.startswith("CameraTarget_") or obj.name.startswith("CamTrack_") or obj.name.startswith("CamRotRef_"):
obj.hide_viewport = not self.show_target_empty
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=10.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=5.0, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【情報更新&コピー用オペレーター】
# =========================================================================
class CAMRIG_OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_update_info"
bl_label = "情報を更新"
bl_description = "ビューを動かした後の最新の座標を再計算して表示します"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
return {'FINISHED'}
class CAMRIG_OT_copy_view_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
bl_label = "ビュー情報をコピー"
def execute(self, context):
rv3d = get_rv3d(context)
space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
if not rv3d or not space: return {'CANCELLED'}
view_loc = rv3d.view_location
view_dist = rv3d.view_distance
view_rot = rv3d.view_rotation
cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
c_start = space.clip_start
c_end = space.clip_end
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
lines = []
if rv3d.view_perspective == 'PERSP':
lines.append("【 透視投影 (Perspective) ビュー情報 】")
lines.append(f"架空カメラ位置 : X: {cam_pos.x:.3f}, Y: {cam_pos.y:.3f}, Z: {cam_pos.z:.3f}")
lines.append(f"画面中央(注視点) : X: {view_loc.x:.3f}, Y: {view_loc.y:.3f}, Z: {view_loc.z:.3f}")
fov_h = 2.0 * math.atan(sensor / (2.0 * space.lens))
w = 2.0 * view_dist * math.tan(fov_h / 2.0)
h = w * aspect
lines.append(f"表示範囲 (左右の幅) : {w:.3f}")
lines.append(f"表示範囲 (上下の幅) : {h:.3f}")
lines.append(f"表示範囲 (奥行き) : Clip Start {c_start:.3f} 〜 Clip End {c_end:.3f}")
else:
lines.append("【 平行投影 (Orthographic) ビュー情報 】")
lines.append(f"架空カメラ位置 : X: {cam_pos.x:.3f}, Y: {cam_pos.y:.3f}, Z: {cam_pos.z:.3f}")
lines.append(f"面中央(注視点) : X: {view_loc.x:.3f}, Y: {view_loc.y:.3f}, Z: {view_loc.z:.3f}")
w = view_dist * (sensor / space.lens)
h = w * aspect
lines.append(f"表示範囲 (左右の幅) : {w:.3f}")
lines.append(f"表示範囲 (上下の幅) : {h:.3f}")
lines.append(f"表示範囲 (奥行き) : Clip Start {c_start:.3f} 〜 Clip End {c_end:.3f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class CAMRIG_OT_copy_cam_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_copy_cam_info"
bl_label = "軌道カメラ情報をコピー"
def execute(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_" not in cam.name:
return {'CANCELLED'}
cam_loc = cam.matrix_world.to_translation()
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO': target_obj = c.target; break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO': target_obj = sub_c.target; break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
lines = []
lines.append(f"【 軌道カメラ ({cam.name}) 情報 】")
lines.append(f"カメラ位置 : X: {cam_loc.x:.3f}, Y: {cam_loc.y:.3f}, Z: {cam_loc.z:.3f}")
if target_obj:
tgt_loc = target_obj.matrix_world.to_translation()
dist = (cam_loc - tgt_loc).length
lines.append(f"注視点位置 : X: {tgt_loc.x:.3f}, Y: {tgt_loc.y:.3f}, Z: {tgt_loc.z:.3f}")
lines.append(f"注視点までの距離 : {dist:.3f}")
else:
lines.append("注視点 : なし")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
return {'FINISHED'}
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
rot_ref_name = f"CamRotRef_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name, rot_ref_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_radius_back": c.cam_shield_hole_radius_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
rot_ref_obj = None
if self.rig_type == 'LINE':
rot_ref_obj = bpy.data.objects.new(rot_ref_name, None)
rot_ref_obj.empty_display_type = 'ARROWS'
rot_ref_obj.hide_viewport = True
rot_ref_obj.hide_render = True
rig_col.objects.link(rot_ref_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_radius_back = old_cam_props_dict["cam_shield_hole_radius_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
if self.rig_type == 'CIRCLE':
cprops.cam_shield_color = (1.0, 0.0, 0.0)
elif self.rig_type == 'SPHERE':
cprops.cam_shield_color = (1.0, 1.0, 0.0)
elif self.rig_type == 'LINE':
cprops.cam_shield_color = (0.0, 1.0, 1.0)
elif self.rig_type == 'FIXED':
cprops.cam_shield_color = (1.0, 0.5, 0.0)
else:
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター生成
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type == 'LINE' and rot_ref_obj:
ref_path = rot_ref_obj.constraints.new(type='FOLLOW_PATH')
ref_path.target = curve_obj
ref_path.use_curve_follow = False
ref_path.use_fixed_location = True
ref_path.offset_factor = 0.0
ref_track = rot_ref_obj.constraints.new(type='TRACK_TO')
ref_track.target = target_empty
ref_track.track_axis = 'TRACK_NEGATIVE_Z'
ref_track.up_axis = 'UP_Y'
const_copy_rot = cam_obj.constraints.new(type='COPY_ROTATION')
const_copy_rot.target = rot_ref_obj
elif self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
update_target_empty_visibility(props, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平视野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
rig_type_name = "不明"
if "CIRCLE" in cam.name: rig_type_name = "円周軌道"
elif "LINE" in cam.name: rig_type_name = "線分軌道"
elif "SPHERE" in cam.name: rig_type_name = "球面軌道"
elif "FIXED" in cam.name: rig_type_name = "固定 (定点)"
box_shield.label(text=f"表示設定 (個別) - {rig_type_name}:", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
row_disp.prop(cprops, "show_cam_shield", text="包み")
box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円柱の半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
# =================================================================
# ★ 位置情報・座標の取得 (クリップボードコピペ用)
# =================================================================
box_info = layout.box()
row_info_head = box_info.row()
row_info_head.label(text="位置・座標・範囲情報の取得:", icon='INFO')
row_info_head.operator(f"view3d.{PREFIX_SAFE}_update_info", text="", icon='FILE_REFRESH')
rv3d = get_rv3d(context)
space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
if rv3d and space:
view_dist = rv3d.view_distance
view_loc = rv3d.view_location
cam_pos = view_loc + rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, view_dist))
col_v = box_info.column(align=True)
if rv3d.view_perspective == 'PERSP':
col_v.label(text="[ 透視投影ビュー ]", icon='VIEW_PERSPECTIVE')
col_v.label(text=f"架空カメラ: X:{cam_pos.x:.1f} Y:{cam_pos.y:.1f} Z:{cam_pos.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
else:
col_v.label(text="[ 平行投影ビュー ]", icon='VIEW_ORTHO')
col_v.label(text=f"架空カメラ: X:{cam_pos.x:.1f} Y:{cam_pos.y:.1f} Z:{cam_pos.z:.1f}")
col_v.label(text=f"面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括コピー", icon='COPYDOWN')
if cam and "TrackingCamera_" in cam.name:
box_info.separator()
col_c = box_info.column(align=True)
col_c.label(text=f"[ 軌道カメラ ({cam.name}) ]", icon='CAMERA_DATA')
cam_loc = cam.matrix_world.to_translation()
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO': target_obj = c.target; break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO': target_obj = sub_c.target; break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
tgt_loc = target_obj.matrix_world.to_translation()
dist = (cam_loc - tgt_loc).length
col_c.label(text=f"位置: X:{cam_loc.x:.1f} Y:{cam_loc.y:.1f} Z:{cam_loc.z:.1f}")
col_c.label(text=f"注視点: X:{tgt_loc.x:.1f} Y:{tgt_loc.y:.1f} Z:{tgt_loc.z:.1f}")
col_c.label(text=f"距離: {dist:.2f}")
else:
col_c.label(text=f"位置: X:{cam_loc.x:.1f} Y:{cam_loc.y:.1f} Z:{cam_loc.z:.1f}")
col_c.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括コピー", icon='COPYDOWN')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
CAMRIG_OT_update_info, CAMRIG_OT_copy_view_info, CAMRIG_OT_copy_cam_info
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
更新ボタンを情報パネルに作って
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 12),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type in ['TRACK_TO', 'COPY_ROTATION']), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
if cutter_back:
r_back = max(0.001, self.cam_shield_hole_radius_back)
cutter_back.scale = (r_back, r_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
def update_target_empty_visibility(self, context):
for obj in bpy.data.objects:
if obj.name.startswith("CameraTarget_") or obj.name.startswith("CamTrack_") or obj.name.startswith("CamRotRef_"):
obj.hide_viewport = not self.show_target_empty
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=10.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=5.0, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【情報コピー用オペレーター】
# =========================================================================
class CAMRIG_OT_copy_view_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
bl_label = "ビュー情報をコピー"
def execute(self, context):
rv3d = get_rv3d(context)
space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
if not rv3d or not space: return {'CANCELLED'}
view_loc = rv3d.view_location
view_dist = rv3d.view_distance
view_rot = rv3d.view_rotation
cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
c_start = space.clip_start
c_end = space.clip_end
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0 # 3D Viewportのセンサー基準概算値
lines = []
if rv3d.view_perspective == 'PERSP':
lines.append("【 透視投影 (Perspective) ビュー情報 】")
lines.append(f"架空カメラ位置 : X: {cam_pos.x:.3f}, Y: {cam_pos.y:.3f}, Z: {cam_pos.z:.3f}")
lines.append(f"画面中央(注視点) : X: {view_loc.x:.3f}, Y: {view_loc.y:.3f}, Z: {view_loc.z:.3f}")
fov_h = 2.0 * math.atan(sensor / (2.0 * space.lens))
w = 2.0 * view_dist * math.tan(fov_h / 2.0)
h = w * aspect
lines.append(f"表示範囲 (左右の幅) : {w:.3f}")
lines.append(f"表示範囲 (上下の幅) : {h:.3f}")
lines.append(f"表示範囲 (奥行き) : Clip Start {c_start:.3f} 〜 Clip End {c_end:.3f}")
else:
lines.append("【 平行投影 (Orthographic) ビュー情報 】")
lines.append(f"架空カメラ位置 : X: {cam_pos.x:.3f}, Y: {cam_pos.y:.3f}, Z: {cam_pos.z:.3f}")
lines.append(f"面中央(注視点) : X: {view_loc.x:.3f}, Y: {view_loc.y:.3f}, Z: {view_loc.z:.3f}")
w = view_dist * (sensor / space.lens)
h = w * aspect
lines.append(f"表示範囲 (左右の幅) : {w:.3f}")
lines.append(f"表示範囲 (上下の幅) : {h:.3f}")
lines.append(f"表示範囲 (奥行き) : Clip Start {c_start:.3f} 〜 Clip End {c_end:.3f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class CAMRIG_OT_copy_cam_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_copy_cam_info"
bl_label = "軌道カメラ情報をコピー"
def execute(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_" not in cam.name:
return {'CANCELLED'}
cam_loc = cam.matrix_world.to_translation()
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO': target_obj = c.target; break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO': target_obj = sub_c.target; break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
lines = []
lines.append(f"【 軌道カメラ ({cam.name}) 情報 】")
lines.append(f"カメラ位置 : X: {cam_loc.x:.3f}, Y: {cam_loc.y:.3f}, Z: {cam_loc.z:.3f}")
if target_obj:
tgt_loc = target_obj.matrix_world.to_translation()
dist = (cam_loc - tgt_loc).length
lines.append(f"注視点位置 : X: {tgt_loc.x:.3f}, Y: {tgt_loc.y:.3f}, Z: {tgt_loc.z:.3f}")
lines.append(f"注視点までの距離 : {dist:.3f}")
else:
lines.append("注視点 : なし")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
return {'FINISHED'}
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
rot_ref_name = f"CamRotRef_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name, rot_ref_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_radius_back": c.cam_shield_hole_radius_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
rot_ref_obj = None
if self.rig_type == 'LINE':
rot_ref_obj = bpy.data.objects.new(rot_ref_name, None)
rot_ref_obj.empty_display_type = 'ARROWS'
rot_ref_obj.hide_viewport = True
rot_ref_obj.hide_render = True
rig_col.objects.link(rot_ref_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_radius_back = old_cam_props_dict["cam_shield_hole_radius_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
if self.rig_type == 'CIRCLE':
cprops.cam_shield_color = (1.0, 0.0, 0.0)
elif self.rig_type == 'SPHERE':
cprops.cam_shield_color = (1.0, 1.0, 0.0)
elif self.rig_type == 'LINE':
cprops.cam_shield_color = (0.0, 1.0, 1.0)
elif self.rig_type == 'FIXED':
cprops.cam_shield_color = (1.0, 0.5, 0.0)
else:
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター生成
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type == 'LINE' and rot_ref_obj:
ref_path = rot_ref_obj.constraints.new(type='FOLLOW_PATH')
ref_path.target = curve_obj
ref_path.use_curve_follow = False
ref_path.use_fixed_location = True
ref_path.offset_factor = 0.0
ref_track = rot_ref_obj.constraints.new(type='TRACK_TO')
ref_track.target = target_empty
ref_track.track_axis = 'TRACK_NEGATIVE_Z'
ref_track.up_axis = 'UP_Y'
const_copy_rot = cam_obj.constraints.new(type='COPY_ROTATION')
const_copy_rot.target = rot_ref_obj
elif self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
update_target_empty_visibility(props, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包み & 軌道の表示設定 (軌道名つき)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
rig_type_name = "不明"
if "CIRCLE" in cam.name: rig_type_name = "円周軌道"
elif "LINE" in cam.name: rig_type_name = "線分軌道"
elif "SPHERE" in cam.name: rig_type_name = "球面軌道"
elif "FIXED" in cam.name: rig_type_name = "固定 (定点)"
box_shield.label(text=f"表示設定 (個別) - {rig_type_name}:", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
row_disp.prop(cprops, "show_cam_shield", text="包み")
box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円柱の半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
# =================================================================
# ★ 位置情報・座標の取得 (クリップボードコピペ用)
# =================================================================
box_info = layout.box()
box_info.label(text="位置・座標・範囲情報の取得:", icon='INFO')
rv3d = get_rv3d(context)
space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
if rv3d and space:
view_dist = rv3d.view_distance
view_loc = rv3d.view_location
cam_pos = view_loc + rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, view_dist))
col_v = box_info.column(align=True)
if rv3d.view_perspective == 'PERSP':
col_v.label(text="[ 透視投影ビュー ]", icon='VIEW_PERSPECTIVE')
col_v.label(text=f"架空カメラ: X:{cam_pos.x:.1f} Y:{cam_pos.y:.1f} Z:{cam_pos.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
else:
col_v.label(text="[ 平行投影ビュー ]", icon='VIEW_ORTHO')
col_v.label(text=f"架空カメラ: X:{cam_pos.x:.1f} Y:{cam_pos.y:.1f} Z:{cam_pos.z:.1f}")
col_v.label(text=f"面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括コピー", icon='COPYDOWN')
if cam and "TrackingCamera_" in cam.name:
box_info.separator()
col_c = box_info.column(align=True)
col_c.label(text=f"[ 軌道カメラ ({cam.name}) ]", icon='CAMERA_DATA')
cam_loc = cam.matrix_world.to_translation()
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO': target_obj = c.target; break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO': target_obj = sub_c.target; break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
tgt_loc = target_obj.matrix_world.to_translation()
dist = (cam_loc - tgt_loc).length
col_c.label(text=f"位置: X:{cam_loc.x:.1f} Y:{cam_loc.y:.1f} Z:{cam_loc.z:.1f}")
col_c.label(text=f"注視点: X:{tgt_loc.x:.1f} Y:{tgt_loc.y:.1f} Z:{tgt_loc.z:.1f}")
col_c.label(text=f"距離: {dist:.2f}")
else:
col_c.label(text=f"位置: X:{cam_loc.x:.1f} Y:{cam_loc.y:.1f} Z:{cam_loc.z:.1f}")
col_c.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括コピー", icon='COPYDOWN')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
CAMRIG_OT_copy_view_info, CAMRIG_OT_copy_cam_info
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
現在のモニター画面の
透視投影 架空カメラ 位置と 画面中央位置座標
左右 上下 奥行き前後の 表示範囲情報を
UIに 表示と コピーボタンを作って
情報は 一括コピーで 1行ずつのペーストになってるのを
平行投影の 画面中央位置座標と
面中央位置座標
左右 上下 奥行き前後の 表示範囲情報を
UIに 表示と コピーボタンを作って
情報は 一括コピーで 1行ずつのペーストになってるのを
選択中の軌道カメラの位置情報と
注視点情報と 注視点までの距離情報
これもUI表示と コピーとペースト設定
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 11),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
# ターゲットオブジェクトの取得 (LINE軌道用のダミーも考慮)
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type in ['TRACK_TO', 'COPY_ROTATION']), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
if cutter_back:
r_back = max(0.001, self.cam_shield_hole_radius_back)
cutter_back.scale = (r_back, r_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
def update_target_empty_visibility(self, context):
for obj in bpy.data.objects:
if obj.name.startswith("CameraTarget_") or obj.name.startswith("CamTrack_") or obj.name.startswith("CamRotRef_"):
obj.hide_viewport = not self.show_target_empty
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=10.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=5.0, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
rot_ref_name = f"CamRotRef_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name, rot_ref_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_radius_back": c.cam_shield_hole_radius_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
# ★ 線分軌道の場合のみ、視線固定用のダミーを生成
rot_ref_obj = None
if self.rig_type == 'LINE':
rot_ref_obj = bpy.data.objects.new(rot_ref_name, None)
rot_ref_obj.empty_display_type = 'ARROWS'
rot_ref_obj.hide_viewport = True
rot_ref_obj.hide_render = True
rig_col.objects.link(rot_ref_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_radius_back = old_cam_props_dict["cam_shield_hole_radius_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
if self.rig_type == 'CIRCLE':
cprops.cam_shield_color = (1.0, 0.0, 0.0) # 赤
elif self.rig_type == 'SPHERE':
cprops.cam_shield_color = (1.0, 1.0, 0.0) # 黄
elif self.rig_type == 'LINE':
cprops.cam_shield_color = (0.0, 1.0, 1.0) # 水色
elif self.rig_type == 'FIXED':
cprops.cam_shield_color = (1.0, 0.5, 0.0) # オレンジ
else:
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター生成
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
# ★ 線分(LINE)軌道の場合は、視線をオフセット0の方向に固定する
if self.rig_type == 'LINE' and rot_ref_obj:
ref_path = rot_ref_obj.constraints.new(type='FOLLOW_PATH')
ref_path.target = curve_obj
ref_path.use_curve_follow = False
ref_path.use_fixed_location = True
ref_path.offset_factor = 0.0 # 常に始点にいる
ref_track = rot_ref_obj.constraints.new(type='TRACK_TO')
ref_track.target = target_empty
ref_track.track_axis = 'TRACK_NEGATIVE_Z'
ref_track.up_axis = 'UP_Y'
# カメラ自体はダミーの回転をそのままコピーする (平行移動になる)
const_copy_rot = cam_obj.constraints.new(type='COPY_ROTATION')
const_copy_rot.target = rot_ref_obj
elif self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
update_target_empty_visibility(props, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
# UI描画時用のターゲット取得処理
target_obj = None
for c in cam.constraints:
if c.type == 'TRACK_TO':
target_obj = c.target
break
elif c.type == 'COPY_ROTATION' and c.target and c.target.name.startswith("CamRotRef_"):
for sub_c in c.target.constraints:
if sub_c.type == 'TRACK_TO':
target_obj = sub_c.target
break
break
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包み & 軌道の表示設定 (軌道名つき)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
rig_type_name = "不明"
if "CIRCLE" in cam.name: rig_type_name = "円周軌道"
elif "LINE" in cam.name: rig_type_name = "線分軌道"
elif "SPHERE" in cam.name: rig_type_name = "球面軌道"
elif "FIXED" in cam.name: rig_type_name = "固定 (定点)"
box_shield.label(text=f"表示設定 (個別) - {rig_type_name}:", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
row_disp.prop(cprops, "show_cam_shield", text="包み")
box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円柱の半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
線分軌道(LINEリグ)において、「カメラが軌道上を移動してもターゲットをキョロキョロと追従せず、移動0(始点)の時の視線方向を維持したまま平行移動する」ように仕組みを改良しました。
これを実現するために、内部で「常に移動0の位置にいてターゲットを見続けるダミーオブジェクト」を配置し、カメラはそのダミーの回転角度だけをコピーする仕様に変更しています。
以下のコードで上書きしてご利用ください。
線分軌道では 軌道上の移動0から1するとき
注視点
オブジェクト
指定位置
オブジェクト2つの中間位置を
移動0のときの視線方向 初期値として
その視線方向を維持したまま
移動0から 1をする
つまり 0のときの視線方向だけ使って
注視点の初期値を 以後 追わない
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 10),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
# 前方の円錐カッター (角度ベース)
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
# 後方の円柱カッター (半径ベース)
if cutter_back:
r_back = max(0.001, self.cam_shield_hole_radius_back)
cutter_back.scale = (r_back, r_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
def update_target_empty_visibility(self, context):
# すべての軌道と注視点の表示を一括切り替え
for obj in bpy.data.objects:
if obj.name.startswith("CameraTarget_") or obj.name.startswith("CamTrack_"):
obj.hide_viewport = not self.show_target_empty
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ (色、表示状態など)"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=10.0, min=0.1, update=update_cam_shield)
# 前方は角度(円錐)、後方は半径(円柱)で制御する
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=5.0, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
# ★ まとめて軌道と注視点(Empty)を隠すプロパティ
show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_radius_back": c.cam_shield_hole_radius_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_radius_back = old_cam_props_dict["cam_shield_hole_radius_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
# 新規作成時の初期値
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
if self.rig_type == 'CIRCLE':
cprops.cam_shield_color = (1.0, 0.0, 0.0) # 赤
elif self.rig_type == 'SPHERE':
cprops.cam_shield_color = (1.0, 1.0, 0.0) # 黄
elif self.rig_type == 'LINE':
cprops.cam_shield_color = (0.0, 1.0, 1.0) # 水色
elif self.rig_type == 'FIXED':
cprops.cam_shield_color = (1.0, 0.5, 0.0) # オレンジ
else:
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター生成 (前方は円錐、後方は円柱)
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
# 前方:円錐カッター (先端を少し太くし、中心を貫通させる)
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
# 後方:円柱カッター (中心を貫通させて+Z側に伸ばす)
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
update_target_empty_visibility(props, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包み & 軌道の表示設定 (軌道名つき)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
rig_type_name = "不明"
if "CIRCLE" in cam.name: rig_type_name = "円周軌道"
elif "LINE" in cam.name: rig_type_name = "線分軌道"
elif "SPHERE" in cam.name: rig_type_name = "球面軌道"
elif "FIXED" in cam.name: rig_type_name = "固定 (定点)"
box_shield.label(text=f"表示設定 (個別) - {rig_type_name}:", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
row_disp.prop(cprops, "show_cam_shield", text="包み")
box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円柱の半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
包みパネル内に「カメラの軌道名(円周・線分など)」を表示するようにしました。
包みの初期設定をカメラ種類ごとに変更しました。
円周軌道:赤色
球面軌道:黄色
線分軌道:水色
固定:オレンジ
包みの半径の初期値:10
後方(裏穴)の半径の初期値:5
新しく**「軌道 & 注視点 をまとめて表示 / 非表示にするチェックボックス」**を追加しました(Emptyやカーブが邪魔なときに一括で隠せます)。
包の設定してるとき パネル内に カメラ軌道名を表示して
包み 初期設定
円周軌道 赤
球体軌道 黄色
線分 水色
固定 オレンジ
包み半径10
裏穴あけ 5
あと 軌道のempty まとめて 非表示 表示のチェックボックス
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 9),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
# 前方の円錐カッター (角度ベース)
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
# 後方の円柱カッター (半径ベース)
if cutter_back:
r_back = max(0.001, self.cam_shield_hole_radius_back)
cutter_back.scale = (r_back, r_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ (色、表示状態など)"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
# 前方は角度(円錐)、後方は半径(円柱)で制御する
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=0.5, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=0.5, min=0.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_radius_back": c.cam_shield_hole_radius_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_radius_back = old_cam_props_dict["cam_shield_hole_radius_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 1.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 0.5
cprops.show_cam_obj = True
cprops.show_cam_shield = True
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター生成 (前方は円錐、後方は円柱)
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
# 前方:円錐カッター (先端を少し太くし、中心を貫通させる)
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
# 後方:円柱カッター (中心を貫通させて+Z側に伸ばす)
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包みの表示・非表示・前後穴あけ設定 (個別化)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
box_shield.label(text="ビューポート上の表示設定 (個別):", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体を表示")
row_disp.prop(cprops, "show_cam_shield", text="包みを表示")
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円柱の半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
包の カメラ浦和が 181度から360度は 円柱で球体をくり抜いて
角度じゃなく 円柱の半径で
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 8),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
if cutter_back:
back_deg = self.cam_shield_hole_angle_back - 180.0
if back_deg < 0.1: back_deg = 0.1
if back_deg > 179.9: back_deg = 179.9
back_angle = math.radians(back_deg)
r_scale_back = 2.0 * z_scale * math.tan(back_angle / 2.0)
cutter_back.scale = (r_scale_back, r_scale_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ (色、表示状態など)"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_angle_back: bpy.props.FloatProperty(name="後方の穴 (反対側)", default=200.0, min=181.0, max=360.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
# カメラ軌道設定
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
# 背景・ワールド
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
# ★ 既存の「同じ種類のカメラ」からのみ設定を退避する
old_cam = bpy.data.objects.get(cam_name)
old_cam_props_dict = None
if old_cam and hasattr(old_cam, "cam_rig_props"):
c = old_cam.cam_rig_props
old_cam_props_dict = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_angle_back": c.cam_shield_hole_angle_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
# 新しいカメラに以前の設定を復元
cprops = cam_obj.cam_rig_props
if old_cam_props_dict:
cprops.show_cam_obj = old_cam_props_dict["show_cam_obj"]
cprops.show_cam_shield = old_cam_props_dict["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props_dict["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props_dict["cam_shield_hole_angle"]
cprops.cam_shield_hole_angle_back = old_cam_props_dict["cam_shield_hole_angle_back"]
cprops.cam_shield_color = old_cam_props_dict["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props_dict["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 1.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_angle_back = 200.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター同士をめり込ませて空洞を貫通させる
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=0.001, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
# 個別カメラ用のマテリアルを設定
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
# ★ 軌道の中心や半径などのリセットは「初回作成時」のみ行う (設定維持)
if not old_cam_props_dict:
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
# 値の更新適用
if self.rig_type == 'CIRCLE': update_cam_circle(props, context)
elif self.rig_type == 'LINE': update_cam_line(props, context)
elif self.rig_type == 'SPHERE': update_cam_sphere(props, context)
elif self.rig_type == 'FIXED': update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包みの表示・非表示・前後穴あけ設定 (個別化)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
box_shield.label(text="ビューポート上の表示設定 (個別):", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体を表示")
row_disp.prop(cprops, "show_cam_shield", text="包みを表示")
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_angle_back", text="後方の穴 (181〜360度)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
bl_info = {
"name": "Camera & View Rig Tools",
"author": "Your Name",
"version": (11, 7),
"blender": (4, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
for const in cam.constraints:
if const.type == 'FOLLOW_PATH' and const.target:
if const.target.name == track_name:
return const.target
return None
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Circle")
if curve_obj:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = get_curve_target(cam, "CamTrack_Line")
if curve_obj:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (*self.cam_line_start, 1.0)
spline.points[1].co = (*self.cam_line_end, 1.0)
def update_cam_sphere(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_SPHERE" not in cam.name:
return
track_obj = bpy.data.objects.get("CamTrack_Sphere")
if track_obj:
track_obj.location = self.cam_sphere_center
track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
track_obj.rotation_euler = self.cam_sphere_rotation
if getattr(self, "mute_target_tracking", False):
return
r = self.cam_sphere_radius
lon = math.radians(self.cam_sphere_lon)
lat = math.radians(self.cam_sphere_lat)
lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
vec = mathutils.Vector((lx, ly, lz))
vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
cx, cy, cz = self.cam_sphere_center
cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)
def update_cam_fixed(self, context):
cam = context.scene.camera
if not cam or "TrackingCamera_FIXED" not in cam.name:
return
cam.location = self.cam_fixed_location
cam.rotation_mode = 'XYZ'
cam.rotation_euler = (
math.radians(self.cam_fixed_pitch),
math.radians(self.cam_fixed_roll),
math.radians(self.cam_fixed_yaw)
)
def update_cam_target(self, context):
cam = context.scene.camera
if not cam: return
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if not target_obj: return
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = context.scene.camera
if not cam: return
track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
mat = eval_cam.matrix_world.copy()
loc = mat.to_translation()
rot = mat.to_euler(cam.rotation_mode)
if track_const: track_const.mute = True
if path_const: path_const.mute = True
cam.location = loc
cam.rotation_euler = rot
else:
if track_const: track_const.mute = False
if path_const: path_const.mute = False
if "SPHERE" in cam.name:
update_cam_sphere(self, context)
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
# =========================================================================
# 【カメラ個別 (オブジェクト単位) の包み制御関数】
# =========================================================================
def update_cam_obj_visibility(self, context):
cam = self.id_data
if cam and cam.type == 'CAMERA':
cam.hide_viewport = not self.show_cam_obj
def update_cam_shield_visibility(self, context):
cam = self.id_data
if not cam: return
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
child.hide_viewport = not self.show_cam_shield
def update_cam_shield(self, context):
cam = self.id_data
if not cam: return
shield = None
cutter = None
cutter_back = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"): shield = child
elif child.name.startswith("CamShield_Cutter_"): cutter = child
elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
if shield:
shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
z_scale = self.cam_shield_radius * 1.5
if cutter:
front_deg = self.cam_shield_hole_angle
if front_deg < 0.1: front_deg = 0.1
if front_deg > 179.9: front_deg = 179.9
angle = math.radians(front_deg)
r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
cutter.scale = (r_scale, r_scale, z_scale)
if cutter_back:
back_deg = self.cam_shield_hole_angle_back - 180.0
if back_deg < 0.1: back_deg = 0.1
if back_deg > 179.9: back_deg = 179.9
back_angle = math.radians(back_deg)
r_scale_back = 2.0 * z_scale * math.tan(back_angle / 2.0)
cutter_back.scale = (r_scale_back, r_scale_back, z_scale)
def update_cam_shield_material(self, context):
cam = self.id_data
if not cam: return
shield = None
for child in cam.children:
if child.name.startswith("CamShield_Sphere_"):
shield = child
break
if not shield: return
if shield.data.materials:
mat = shield.data.materials[0]
if mat:
mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
shield.color = (*self.cam_shield_color, self.cam_shield_alpha)
# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
def update_viewport_color(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.background_type = 'VIEWPORT'
space.shading.background_color = self.viewport_bg_color
def setup_world_nodes(context):
world = context.scene.world
if not world:
world = bpy.data.worlds.new("World")
context.scene.world = world
world.use_nodes = True
tree = world.node_tree
out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
return tree, out_node, bg_node, sky_node
def update_world_mode(self, context):
tree, out_node, bg_node, sky_node = setup_world_nodes(context)
for link in bg_node.inputs['Color'].links: tree.links.remove(link)
if not bg_node.outputs['Background'].links:
tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
if self.world_mode == 'SKY':
context.scene.render.film_transparent = False
sky_node.sky_type = 'NISHITA'
tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
update_sky_texture(self, context)
update_world_settings(self, context)
elif self.world_mode == 'COLOR':
context.scene.render.film_transparent = False
update_world_settings(self, context)
elif self.world_mode == 'TRANSPARENT':
context.scene.render.film_transparent = True
update_world_settings(self, context)
def update_sky_texture(self, context):
if self.world_mode != 'SKY': return
_, _, _, sky_node = setup_world_nodes(context)
sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
sky_node.sun_intensity = self.sky_sun_intensity
def update_world_settings(self, context):
tree, _, bg_node, _ = setup_world_nodes(context)
if self.world_mode != 'SKY':
bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
bg_node.inputs[1].default_value = self.world_bg_strength
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
if bpy.context.active_object:
rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
"""個別のカメラオブジェクトに紐づくプロパティ (色、表示状態など)"""
show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
cam_shield_hole_angle_back: bpy.props.FloatProperty(name="後方の穴 (反対側)", default=200.0, min=181.0, max=360.0, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)
class CamRigProperties(bpy.types.PropertyGroup):
"""シーン全体の操作プロパティ"""
obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
# カメラ軌道設定
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
default='OBJECT', update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
mute_target_tracking: bpy.props.BoolProperty(name="軌道と注視を解除 (完全手動)", default=False, update=update_cam_mute)
# 背景・ワールド
viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
world_mode: bpy.props.EnumProperty(
name="ワールド背景モード",
items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
default='COLOR', update=update_world_mode
)
sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)
# =========================================================================
# 【オペレーター】
# =========================================================================
class CAMRIG_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
target_name = f"CameraTarget_{self.rig_type}"
track_name = f"CamTrack_{self.rig_type.capitalize()}"
cam_name = f"TrackingCamera_{self.rig_type}"
cam_data_name = f"CamData_{self.rig_type}"
shield_name = f"CamShield_Sphere_{self.rig_type}"
cutter_name = f"CamShield_Cutter_{self.rig_type}"
cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
if target and target.name in names_to_delete: target = None
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
# 既存のカメラから包みの設定(色や大きさなど)を引き継ぐために保存
old_cam_props = {}
for obj in bpy.data.objects:
if obj.name.startswith("TrackingCamera_") and hasattr(obj, "cam_rig_props"):
c = obj.cam_rig_props
old_cam_props = {
"show_cam_obj": c.show_cam_obj,
"show_cam_shield": c.show_cam_shield,
"cam_shield_radius": c.cam_shield_radius,
"cam_shield_hole_angle": c.cam_shield_hole_angle,
"cam_shield_hole_angle_back": c.cam_shield_hole_angle_back,
"cam_shield_color": list(c.cam_shield_color),
"cam_shield_alpha": c.cam_shield_alpha
}
break
for name in names_to_delete:
obj = bpy.data.objects.get(name)
if obj:
try:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and getattr(data, "users", 1) == 0:
if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
except ReferenceError: pass
cdata = bpy.data.cameras.get(cam_data_name)
if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
if not rig_col:
rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
context.scene.collection.children.link(rig_col)
target_empty = bpy.data.objects.new(target_name, None)
target_empty.empty_display_type = 'PLAIN_AXES'
target_empty.location = loc
rig_col.objects.link(target_empty)
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = track_name
for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new(track_name, type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new(track_name, curve_data)
rig_col.objects.link(curve_obj)
elif self.rig_type == 'SPHERE':
curve_obj = bpy.data.objects.new(track_name, None)
curve_obj.empty_display_type = 'SPHERE'
rig_col.objects.link(curve_obj)
cam_data = bpy.data.cameras.new(name=cam_data_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
cam_obj.rotation_mode = 'XYZ'
rig_col.objects.link(cam_obj)
# 新しいカメラに以前の設定を復元
cprops = cam_obj.cam_rig_props
if old_cam_props:
cprops.show_cam_obj = old_cam_props["show_cam_obj"]
cprops.show_cam_shield = old_cam_props["show_cam_shield"]
cprops.cam_shield_radius = old_cam_props["cam_shield_radius"]
cprops.cam_shield_hole_angle = old_cam_props["cam_shield_hole_angle"]
cprops.cam_shield_hole_angle_back = old_cam_props["cam_shield_hole_angle_back"]
cprops.cam_shield_color = old_cam_props["cam_shield_color"]
cprops.cam_shield_alpha = old_cam_props["cam_shield_alpha"]
else:
cprops.cam_shield_radius = 1.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_angle_back = 200.0
cprops.show_cam_obj = True
cprops.show_cam_shield = True
cprops.cam_shield_color = (0.0, 0.8, 1.0)
cprops.cam_shield_alpha = 0.3
# -------------------------------------------------------------
# ★ カッター同士をめり込ませて空洞を貫通させる
# -------------------------------------------------------------
mesh_sphere = bpy.data.meshes.new(shield_name)
shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
rig_col.objects.link(shield_obj)
shield_obj.parent = cam_obj
shield_obj.hide_render = True
shield_obj.hide_select = True
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh_sphere)
bm.free()
mesh_cone = bpy.data.meshes.new(cutter_name)
cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
rig_col.objects.link(cutter_obj)
cutter_obj.parent = cam_obj
cutter_obj.hide_viewport = True
cutter_obj.hide_render = True
cutter_obj.hide_select = True
cutter_obj.display_type = 'BOUNDS'
bm_cone = bmesh.new()
bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
for v in bm_cone.verts: v.co.z -= 0.95
bm_cone.to_mesh(mesh_cone)
bm_cone.free()
mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
rig_col.objects.link(cutter_back_obj)
cutter_back_obj.parent = cam_obj
cutter_back_obj.hide_viewport = True
cutter_back_obj.hide_render = True
cutter_back_obj.hide_select = True
cutter_back_obj.display_type = 'BOUNDS'
bm_cone_back = bmesh.new()
bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=0.001, radius2=1.0, depth=2.0)
for v in bm_cone_back.verts: v.co.z += 0.95
bm_cone_back.to_mesh(mesh_cone_back)
bm_cone_back.free()
mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
mod_front.operation = 'DIFFERENCE'
mod_front.object = cutter_obj
mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
mod_back.operation = 'DIFFERENCE'
mod_back.object = cutter_back_obj
# 個別カメラ用のマテリアルを設定
mat_name = f"CamShield_Material_{self.rig_type}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
# -------------------------------------------------------------
if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
if self.rig_type != 'FIXED':
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
props.cam_target_mode = 'OBJECT'
try:
if target and target.type in ['CAMERA', 'LIGHT']: target = None
except ReferenceError: target = None
props.cam_target_obj1 = target
if props.mute_target_tracking: props.mute_target_tracking = False
update_cam_target(props, context)
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
props.cam_circle_radius = 15.0
props.cam_circle_rotation = (0, 0, 0)
update_cam_circle(props, context)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
update_cam_line(props, context)
elif self.rig_type == 'SPHERE':
props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
props.cam_sphere_radius = 15.0
props.cam_sphere_rotation = (0, 0, 0)
props.cam_sphere_lon = 0.0
props.cam_sphere_lat = 0.0
update_cam_sphere(props, context)
elif self.rig_type == 'FIXED':
props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
props.cam_fixed_pitch = 90.0
props.cam_fixed_yaw = 0.0
props.cam_fixed_roll = 0.0
update_cam_fixed(props, context)
update_cam_shield(cprops, context)
update_cam_obj_visibility(cprops, context)
update_cam_shield_visibility(cprops, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
return {'FINISHED'}
class CAMRIG_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_reset_view"
bl_label = "選択したオブジェクトを中心にビュー初期化"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class CAMRIG_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=90.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class CAMRIG_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
bl_label = "画面を指定軸で回転"
angle: bpy.props.FloatProperty(name="Angle", default=15.0)
def execute(self, context):
axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
rv3d = get_rv3d(context)
if rv3d:
vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
return {'FINISHED'}
class CAMRIG_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class CAMRIG_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class CAMRIG_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
bl_label = "カメラ & ビュー操作ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
if context.active_object:
try:
axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
row = box_rot.row(align=True)
for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
except ReferenceError:
box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
row_v = box_vrot.row(align=True)
for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
layout.separator(factor=1.5)
box_cam = layout.box()
box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
col_cam_btn = box_cam.column(align=True)
row1 = col_cam_btn.row(align=True)
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
row2 = col_cam_btn.row(align=True)
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
cam = context.scene.camera
if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
box_cam.separator()
box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
if "SPHERE" in cam.name:
box_sp = box_cam.box()
box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
col_sp = box_sp.column(align=True)
col_sp.enabled = not props.mute_target_tracking
col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_sp.separator()
col_sp.prop(props, "cam_sphere_center", text="球の中心")
col_sp.prop(props, "cam_sphere_radius", text="球の半径")
col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
elif "FIXED" in cam.name:
box_fix = box_cam.box()
box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
col_fix = box_fix.column(align=True)
col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
col_fix.separator()
col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
else:
curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
if curve_obj:
col_offset = box_cam.column(align=True)
col_offset.enabled = not props.mute_target_tracking
col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
box_curve = box_cam.box()
box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
col_crv = box_curve.column(align=True)
col_crv.enabled = not props.mute_target_tracking
if "Circle" in curve_obj.name:
col_crv.prop(props, "cam_circle_center", text="円の中心")
col_crv.prop(props, "cam_circle_radius", text="円の半径")
col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "Line" in curve_obj.name:
col_crv.prop(props, "cam_line_start", text="始点")
col_crv.prop(props, "cam_line_end", text="終点")
target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
if not target_obj and "FIXED" in cam.name:
target_obj = cam.data.dof.focus_object
if target_obj:
box_target = box_cam.box()
if "FIXED" in cam.name:
box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
else:
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
col_tgt_main = box_target.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", expand=True)
col_tgt = col_tgt_main.column(align=True)
if props.cam_target_mode == 'OBJECT': col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif props.cam_target_mode == 'POINT': col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif props.cam_target_mode == 'MIDPOINT':
col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
col_lens.separator()
col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
box_cam.separator()
col_pp = box_cam.column(align=True)
col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする (Passepartout)")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
box_cam.separator()
if "FIXED" not in cam.name:
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = props.mute_target_tracking
col_sight.prop(cam, "location", text="位置 (XYZ)")
col_sight.separator()
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# ★ カメラ本体 & 包みの表示・非表示・前後穴あけ設定 (個別化)
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = box_cam.box()
box_shield.label(text="ビューポート上の表示設定 (個別):", icon='RESTRICT_VIEW_OFF')
row_disp = box_shield.row()
row_disp.prop(cprops, "show_cam_obj", text="カメラ本体を表示")
row_disp.prop(cprops, "show_cam_shield", text="包みを表示")
col_sh = box_shield.column(align=True)
col_sh.enabled = cprops.show_cam_shield
col_sh.separator()
col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
col_sh.prop(cprops, "cam_shield_hole_angle_back", text="後方の穴 (181〜360度)")
col_sh.separator()
col_sh.prop(cprops, "cam_shield_color", text="包みの色")
col_sh.prop(cprops, "cam_shield_alpha", text="透明度 (アルファ)", slider=True)
box_cam.separator()
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
else:
box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
layout.separator(factor=1.5)
box_render = layout.box()
box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
row_eng = box_render.row(align=True)
row_eng.prop(context.scene.render, "engine", expand=True)
box_render.separator()
box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
box_render.separator()
box_render.label(text="ワールド (背景) の設定:", icon='WORLD')
box_render.prop(props, "world_mode", expand=True)
col_world = box_render.column(align=True)
if props.world_mode == 'SKY':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
elif props.world_mode == 'COLOR':
col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
col_world.prop(props, "world_bg_strength", text="明るさ")
elif props.world_mode == 'TRANSPARENT':
col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig,
CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view,
CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=CamRigProperties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX_SAFE}_props"): delattr(bpy.types.Scene, f"{PREFIX_SAFE}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()