カメラ機能の追加 改変型
bl_info = {
"name": "Sync Sphere Camera & View Tools",
"author": "Your Name",
"version": (4, 1),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "球体軌道カメラ、アニメ自動移動完全対応、包み、背景/Grid設定",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Sphere Cam"
PREFIX_NAME = "SyncSphereCamRig"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
COLLECTION_NAME = "CamRig_Sphere_Collection"
OBJ_NAME_CAM_TRACK = "CamTrack_Sphere"
OBJ_NAME_ROTATOR = "CamRotator_Sphere"
OBJ_NAME_RADIUS = "CamRadius_Sphere"
OBJ_NAME_CAMERA = "TrackingCamera_Sphere"
OBJ_NAME_TARGET = "CameraTarget_Sphere"
SHIELD_NAME = "CamShield_Sphere"
CUTTER_NAME = "CamShield_Cutter_Sphere"
CUTTER_BACK_NAME = "CamShield_CutterBack_Sphere"
LINK_URL = "<https://app.notion.com/p/20260710-399f5dacaf4380f3bd2bcea570b2b686>"
# =========================================================================
# 【ビュー・情報取得用 関数群】
# =========================================================================
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()
# =========================================================================
# 【カメラ・注視点 制御 関数 (ドライバ・球面)】
# =========================================================================
def setup_driver(obj, path, index, prop_path, is_angle=False, is_negative=False):
d = obj.driver_add(path, index).driver
d.type = 'SCRIPTED'
v = d.variables.new()
v.name = "val"
v.type = 'SINGLE_PROP'
v.targets[0].id_type = 'SCENE'
v.targets[0].id = bpy.context.scene
v.targets[0].data_path = f"{PREFIX_SAFE}_props.{prop_path}"
expr = "val"
if is_angle:
expr = "val * 0.017453292519943295" # math.radians()
if is_negative:
expr = f"-({expr})"
d.expression = expr
def update_cam_target(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not tgt: return
for c in tgt.constraints:
if c.type == 'COPY_LOCATION':
tgt.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT' and self.cam_target_obj1:
c1 = tgt.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
tgt.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = tgt.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = tgt.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if not cam: return
copy_loc = next((c for c in cam.constraints if c.type == 'COPY_LOCATION'), None)
cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam.location = eval_cam.matrix_world.to_translation()
cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
if copy_loc: copy_loc.mute = True
if cam_track: cam_track.mute = True
else:
if copy_loc: copy_loc.mute = False
if cam_track: cam_track.mute = False
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, cutter, cutter_back = None, None, 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 = max(0.1, min(179.9, self.cam_shield_hole_angle))
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_scale_back = max(0.001, self.cam_shield_hole_radius_back)
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 = next((c for c in cam.children if c.name.startswith("CamShield_Sphere")), None)
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 in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_ROTATOR, OBJ_NAME_RADIUS]:
obj.hide_viewport = not self.show_target_empty
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
# =========================================================================
# 【プロパティ定義】
# =========================================================================
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=(1.0, 1.0, 0.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 SphereCamRigProperties(bpy.types.PropertyGroup):
# 球面軌道用 (Driverで連動するためupdate関数は不要)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5))
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0))
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0)
# アニメーション用
anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
anim_start_lon: bpy.props.FloatProperty(name="開始 経度", default=0.0)
anim_end_lon: bpy.props.FloatProperty(name="終了 経度", default=360.0)
anim_start_lat: bpy.props.FloatProperty(name="開始 緯度", default=0.0)
anim_end_lat: bpy.props.FloatProperty(name="終了 緯度", default=0.0)
# 注視点
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)
# その他設定
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_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=10.0, min=0.001)
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 SPHERECAM_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 SPHERECAM_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 SPHERECAM_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 SPHERECAM_OT_set_grid_scale(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_set_grid_scale"
bl_label = "Grid間隔を設定"
scale_val: bpy.props.FloatProperty(name="Scale")
use_custom: bpy.props.BoolProperty(name="カスタム値", default=False)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
val = props.custom_grid_scale if self.use_custom else self.scale_val
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.overlay.grid_scale = val
self.report({'INFO'}, f"Grid間隔を {val:.1f} に設定しました")
return {'FINISHED'}
class SPHERECAM_OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_update_info"
bl_label = "更新"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
return {'FINISHED'}
class SPHERECAM_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))
view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
lines = []
if rv3d.view_perspective == 'PERSP':
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("投影モード: 透視投影 (Perspective)")
lines.append(f"架空の視座位置 X: {cam_pos.x:.1f}, Y: {cam_pos.y:.1f}, Z: {cam_pos.z:.1f}")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離: {view_dist:.1f}")
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
lines.append("投影モード: 平行投影 (Orthographic)")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class SPHERECAM_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 OBJ_NAME_CAMERA not in cam.name: return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam_loc = eval_cam.matrix_world.to_translation()
cam_rot = eval_cam.matrix_world.to_quaternion()
view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = res_y / res_x if res_x > 0 else 1.0
fov_h = cam.data.angle
w = 2.0 * dist * math.tan(fov_h / 2.0)
h = w * aspect
lines = []
lines.append(f"投影モード: カメラ ({cam.name})")
lines.append(f"架空の視座位置 X: {cam_loc.x:.1f}, Y: {cam_loc.y:.1f}, Z: {cam_loc.z:.1f}")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
if tgt:
lines.append(f"画面中央位置 X: {tgt_loc.x:.1f}, Y: {tgt_loc.y:.1f}, Z: {tgt_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離: {dist:.1f}")
else:
lines.append(f"画面中央位置 X: なし, Y: なし, Z: なし")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離 (Focus): {dist:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
return {'FINISHED'}
# =========================================================================
# 【初期化(リセット) オペレーター】
# =========================================================================
class SPHERECAM_OT_reset_props(bpy.types.Operator):
bl_idname = f"scene.{PREFIX_SAFE}_reset_props"
bl_label = "初期値にリセット"
bl_description = "設定をアドオンの初期値に戻します"
bl_options = {'REGISTER', 'UNDO'}
reset_type: bpy.props.EnumProperty(
items=[
('ALL', "全て", ""),
('CAMERA', "カメラ・軌道", ""),
('WORLD', "環境・背景", ""),
('VIEW', "ビュー・操作", "")
],
default='ALL'
)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
if self.reset_type in {'ALL', 'WORLD'}:
props.world_mode = 'COLOR'
props.sky_sun_elevation = 15.0
props.sky_sun_rotation = 0.0
props.sky_sun_intensity = 1.0
props.world_bg_color = (0.53, 0.81, 0.92)
props.world_bg_strength = 1.0
if self.reset_type in {'ALL', 'VIEW'}:
props.obj_rot_axis = 'Z'
props.view_rot_axis = 'Z'
props.custom_grid_scale = 10.0
if self.reset_type in {'ALL', 'CAMERA'}:
props.cam_sphere_center = (0, 0, 5)
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
props.anim_start_frame = 1
props.anim_end_frame = 250
props.anim_start_lon = 0.0
props.anim_end_lon = 360.0
props.anim_start_lat = 0.0
props.anim_end_lat = 0.0
props.cam_target_mode = 'OBJECT'
props.cam_target_obj1 = None
props.cam_target_obj2 = None
props.cam_target_loc = (0, 0, 0)
props.mute_target_tracking = False
props.show_target_empty = True
props.cam_fov = 50.0
cam = context.scene.camera
if cam and OBJ_NAME_CAMERA in cam.name:
cam.data.clip_start = 0.1
cam.data.clip_end = 1000.0
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.5
cam.data.dof.use_dof = True
cam.data.dof.aperture_fstop = 1.8
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
cprops.show_cam_obj = True
cprops.show_cam_shield = True
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.cam_shield_color = (1.0, 1.0, 0.0)
cprops.cam_shield_alpha = 0.3
self.report({'INFO'}, "設定を初期値にリセットしました")
return {'FINISHED'}
class SPHERECAM_OT_auto_animate(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_auto_animate"
bl_label = "経度・緯度のアニメーションを自動設定"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
prefs = context.preferences
prev_interp = prefs.edit.keyframe_new_interpolation_type
try:
prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
props.cam_sphere_lon = props.anim_start_lon
props.cam_sphere_lat = props.anim_start_lat
props.keyframe_insert(data_path="cam_sphere_lon", frame=props.anim_start_frame)
props.keyframe_insert(data_path="cam_sphere_lat", frame=props.anim_start_frame)
props.cam_sphere_lon = props.anim_end_lon
props.cam_sphere_lat = props.anim_end_lat
props.keyframe_insert(data_path="cam_sphere_lon", frame=props.anim_end_frame)
props.keyframe_insert(data_path="cam_sphere_lat", frame=props.anim_end_frame)
finally:
prefs.edit.keyframe_new_interpolation_type = prev_interp
self.report({'INFO'}, f"アニメ設定完了: {props.anim_start_frame}F 〜 {props.anim_end_frame}F")
return {'FINISHED'}
class SPHERECAM_OT_create_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_rig"
bl_label = "球面リグ作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_ROTATOR, OBJ_NAME_RADIUS, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
for name in names:
obj = bpy.data.objects.get(name)
if obj: bpy.data.objects.remove(obj, do_unlink=True)
main_col = bpy.data.collections.get(COLLECTION_NAME)
if not main_col:
main_col = bpy.data.collections.new(COLLECTION_NAME)
context.scene.collection.children.link(main_col)
# 軌道の中心(Center/Rotation)をDriverで連動
track_obj = bpy.data.objects.new(OBJ_NAME_CAM_TRACK, None)
track_obj.empty_display_type = 'SPHERE'
main_col.objects.link(track_obj)
for i in range(3):
setup_driver(track_obj, "location", i, f"cam_sphere_center[{i}]")
setup_driver(track_obj, "rotation_euler", i, f"cam_sphere_rotation[{i}]")
# 回転用 (Lon, Lat) をDriverで連動
rot_obj = bpy.data.objects.new(OBJ_NAME_ROTATOR, None)
rot_obj.empty_display_type = 'ARROWS'
rot_obj.rotation_mode = 'ZYX'
rot_obj.parent = track_obj
main_col.objects.link(rot_obj)
setup_driver(rot_obj, "rotation_euler", 2, "cam_sphere_lon", is_angle=True)
setup_driver(rot_obj, "rotation_euler", 1, "cam_sphere_lat", is_angle=True, is_negative=True)
# 半径用 (Radius) をDriverで連動
rad_obj = bpy.data.objects.new(OBJ_NAME_RADIUS, None)
rad_obj.empty_display_type = 'PLAIN_AXES'
rad_obj.parent = rot_obj
main_col.objects.link(rad_obj)
setup_driver(rad_obj, "location", 0, "cam_sphere_radius")
# ターゲット
tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_empty.empty_display_type = 'PLAIN_AXES'
main_col.objects.link(tgt_empty)
# カメラ
cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
cam_obj.rotation_mode = 'XYZ'
main_col.objects.link(cam_obj)
# カメラの制約
cam_copy_loc = cam_obj.constraints.new(type='COPY_LOCATION')
cam_copy_loc.target = rad_obj
cam_track = cam_obj.constraints.new(type='TRACK_TO')
cam_track.target = tgt_empty
cam_track.track_axis = 'TRACK_NEGATIVE_Z'
cam_track.up_axis = 'UP_Y'
# 包み(シールド)作成
mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
main_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)
main_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)
main_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 = bpy.data.materials.new("CamShield_Mat_Sphere")
mat.use_nodes = True
mat.blend_method = 'BLEND'
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
cprops = cam_obj.cam_rig_props
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.cam_shield_color = (1.0, 1.0, 0.0)
cprops.cam_shield_alpha = 0.3
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = tgt_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
props.mute_target_tracking = False
props.cam_target_mode = 'OBJECT'
props.cam_target_obj1 = context.active_object if context.active_object and context.active_object.type not in ['CAMERA', 'LIGHT'] else None
update_cam_target(props, context)
update_cam_shield(cprops, context)
update_cam_shield_material(cprops, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, "球面軌道カメラをセットアップしました (完全アニメーション対応)")
return {'FINISHED'}
class SPHERECAM_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "リンク"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SPHERECAM_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル1】 ビュー & 環境操作ツール
# =========================================================================
class SPHERECAM_PT_view_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_view_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:
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
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_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
view_rot = rv3d.view_rotation
cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
col_v = box_info.column(align=True)
if rv3d.view_perspective == 'PERSP':
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
col_v.label(text="[ 透視投影 (Perspective) ]", 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_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.label(text=f"視座からの距離: {view_dist:.1f}")
col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
col_v.label(text="[ 平行投影 (Orthographic) ]", icon='VIEW_ORTHO')
col_v.label(text=f"視線方向: X:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
col_v.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括コピー", icon='COPYDOWN')
layout.separator(factor=1.5)
# Grid / 背景設定
box_grid = layout.box()
box_grid.label(text="Grid / 背景 設定", icon='GRID')
row_preset = box_grid.row(align=True)
row_preset.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="間隔 10").scale_val = 10.0
row_preset.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="間隔 100").scale_val = 100.0
row_custom = box_grid.row(align=True)
row_custom.prop(props, "custom_grid_scale")
op_custom = row_custom.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="指定値にする")
op_custom.use_custom = True
box_grid.separator()
prefs = context.preferences
if len(prefs.themes) > 0:
theme_view3d = prefs.themes[0].view_3d
col_theme = box_grid.column(align=True)
col_theme.prop(theme_view3d, "grid", text="Grid 色変更")
col_theme.separator()
col_theme.prop(theme_view3d.space.gradients, "high_gradient", text="背景色 (上/単色)")
col_theme.prop(theme_view3d.space.gradients, "low_gradient", text="背景色 (下)")
layout.separator(factor=1.5)
box_render = layout.box()
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)")
# ワールド設定リセット
box_render.operator(f"scene.{PREFIX_SAFE}_reset_props", text="ワールド設定を初期化", icon='FILE_REFRESH').reset_type = 'WORLD'
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"scene.{PREFIX_SAFE}_reset_props", text="すべて初期化 (アドオン全設定)", icon='FILE_REFRESH').reset_type = 'ALL'
box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオンのWebリンク", icon='URL').url = LINK_URL
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【UIパネル2】 球面カメラ軌道
# =========================================================================
class SPHERECAM_PT_camera_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_camera_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_rig = layout.box()
box_rig.label(text="専用カメラ (同期球面軌道):", icon='MESH_UVSPHERE')
row_rig = box_rig.row(align=True)
row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="球面リグ作成", icon='ADD')
row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
# カメラ設定リセット
box_rig.operator(f"scene.{PREFIX_SAFE}_reset_props", text="カメラ・軌道設定を初期化", icon='FILE_REFRESH').reset_type = 'CAMERA'
cam = context.scene.camera
if cam and OBJ_NAME_CAMERA in cam.name:
box_cam = layout.box()
box_cam.label(text="軌道の操作 (経度・緯度):", icon='PLAY')
col_pos = box_cam.column(align=True)
col_pos.enabled = not props.mute_target_tracking
col_pos.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_pos.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_pos.separator()
col_pos.prop(props, "cam_sphere_center", text="球の中心")
col_pos.prop(props, "cam_sphere_radius", text="球の半径")
col_pos.prop(props, "cam_sphere_rotation", text="球の傾き")
box_cam.separator()
box_cam.label(text="アニメーション自動設定:", icon='REC')
col_anim = box_cam.column(align=True)
col_anim.enabled = not props.mute_target_tracking
row_f = col_anim.row(align=True)
row_f.prop(props, "anim_start_frame", text="開始F")
row_f.prop(props, "anim_end_frame", text="終了F")
row_lon = col_anim.row(align=True)
row_lon.prop(props, "anim_start_lon", text="開始 経度")
row_lon.prop(props, "anim_end_lon", text="終了 経度")
row_lat = col_anim.row(align=True)
row_lat.prop(props, "anim_start_lat", text="開始 緯度")
row_lat.prop(props, "anim_end_lat", text="終了 緯度")
col_anim.operator(f"object.{PREFIX_SAFE}_auto_animate", text="キーフレームを打つ", icon='REC')
box_cam.separator()
box_cam.label(text="注視点の設定:", icon='EMPTY_DATA')
col_tgt_main = box_cam.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="カメラ枠外を暗くする")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", 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, "aperture_fstop", text="F値 (小さいとボケる)")
# 手動操作 (Location, Pitch, Roll, Yaw) と情報表示
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
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)")
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_cam.separator()
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="包み")
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.prop(cprops, "cam_shield_radius", text="半径")
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)
layout.separator(factor=1.5)
# 軌道カメラの個別情報
box_info = layout.box()
row_info_head = box_info.row()
row_info_head.label(text="カメラ位置・座標・範囲情報:", icon='CAMERA_DATA')
row_info_head.operator(f"view3d.{PREFIX_SAFE}_update_info", text="", icon='FILE_REFRESH')
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam_loc = eval_cam.matrix_world.to_translation()
cam_rot = eval_cam.matrix_world.to_quaternion()
view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = res_y / res_x if res_x > 0 else 1.0
fov_h = cam.data.angle
w = 2.0 * dist * math.tan(fov_h / 2.0)
h = w * aspect
col_c = box_info.column(align=True)
col_c.label(text=f"[ 軌道カメラ ({cam.name}) ]", icon='CAMERA_DATA')
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:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
if tgt:
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:.1f}")
col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
else:
col_c.label(text="注視点: なし")
col_c.label(text=f"視座からの距離(Focus): {dist:.1f}")
col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
col_c.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括コピー", icon='COPYDOWN')
else:
layout.label(text="※専用カメラがアクティブではありません", icon='INFO')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, SphereCamRigProperties,
SPHERECAM_OT_reset_view, SPHERECAM_OT_rotate_selected, SPHERECAM_OT_rotate_view, SPHERECAM_OT_set_grid_scale,
SPHERECAM_OT_update_info, SPHERECAM_OT_copy_view_info, SPHERECAM_OT_copy_cam_info,
SPHERECAM_OT_reset_props, SPHERECAM_OT_auto_animate, SPHERECAM_OT_create_rig,
SPHERECAM_OT_open_url, SPHERECAM_OT_remove_addon,
SPHERECAM_PT_view_panel, SPHERECAM_PT_camera_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=SphereCamRigProperties))
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": "Sync Sphere Camera & View Tools",
"author": "Your Name",
"version": (4, 1),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "球体軌道カメラ、アニメ自動移動完全対応、包み、背景/Grid設定",
"category": "Object",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Sphere Cam"
PREFIX_NAME = "SyncSphereCamRig"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")
COLLECTION_NAME = "CamRig_Sphere_Collection"
OBJ_NAME_CAM_TRACK = "CamTrack_Sphere"
OBJ_NAME_ROTATOR = "CamRotator_Sphere"
OBJ_NAME_RADIUS = "CamRadius_Sphere"
OBJ_NAME_CAMERA = "TrackingCamera_Sphere"
OBJ_NAME_TARGET = "CameraTarget_Sphere"
SHIELD_NAME = "CamShield_Sphere"
CUTTER_NAME = "CamShield_Cutter_Sphere"
CUTTER_BACK_NAME = "CamShield_CutterBack_Sphere"
LINK_URL = "<https://app.notion.com/p/20260710-399f5dacaf4380f3bd2bcea570b2b686>"
# =========================================================================
# 【ビュー・情報取得用 関数群】
# =========================================================================
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()
# =========================================================================
# 【カメラ・注視点 制御 関数 (ドライバ・球面)】
# =========================================================================
def setup_driver(obj, path, index, prop_path, is_angle=False, is_negative=False):
d = obj.driver_add(path, index).driver
d.type = 'SCRIPTED'
v = d.variables.new()
v.name = "val"
v.type = 'SINGLE_PROP'
v.targets[0].id_type = 'SCENE'
v.targets[0].id = bpy.context.scene
v.targets[0].data_path = f"{PREFIX_SAFE}_props.{prop_path}"
expr = "val"
if is_angle:
expr = "val * 0.017453292519943295" # math.radians()
if is_negative:
expr = f"-({expr})"
d.expression = expr
def update_cam_target(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not tgt: return
for c in tgt.constraints:
if c.type == 'COPY_LOCATION':
tgt.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT' and self.cam_target_obj1:
c1 = tgt.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
tgt.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = tgt.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = tgt.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
c2.influence = 0.5
def update_cam_mute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if not cam: return
copy_loc = next((c for c in cam.constraints if c.type == 'COPY_LOCATION'), None)
cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam.location = eval_cam.matrix_world.to_translation()
cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
if copy_loc: copy_loc.mute = True
if cam_track: cam_track.mute = True
else:
if copy_loc: copy_loc.mute = False
if cam_track: cam_track.mute = False
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, cutter, cutter_back = None, None, 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 = max(0.1, min(179.9, self.cam_shield_hole_angle))
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_scale_back = max(0.001, self.cam_shield_hole_radius_back)
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 = next((c for c in cam.children if c.name.startswith("CamShield_Sphere")), None)
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 in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_ROTATOR, OBJ_NAME_RADIUS]:
obj.hide_viewport = not self.show_target_empty
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
# =========================================================================
# 【プロパティ定義】
# =========================================================================
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=(1.0, 1.0, 0.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 SphereCamRigProperties(bpy.types.PropertyGroup):
# 球面軌道用 (Driverで連動するためupdate関数は不要)
cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5))
cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1)
cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0))
cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0)
cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0)
# アニメーション用
anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
anim_start_lon: bpy.props.FloatProperty(name="開始 経度", default=0.0)
anim_end_lon: bpy.props.FloatProperty(name="終了 経度", default=360.0)
anim_start_lat: bpy.props.FloatProperty(name="開始 緯度", default=0.0)
anim_end_lat: bpy.props.FloatProperty(name="終了 緯度", default=0.0)
# 注視点
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)
# その他設定
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_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=10.0, min=0.001)
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 SPHERECAM_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 SPHERECAM_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 SPHERECAM_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 SPHERECAM_OT_set_grid_scale(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_set_grid_scale"
bl_label = "Grid間隔を設定"
scale_val: bpy.props.FloatProperty(name="Scale")
use_custom: bpy.props.BoolProperty(name="カスタム値", default=False)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
val = props.custom_grid_scale if self.use_custom else self.scale_val
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.overlay.grid_scale = val
self.report({'INFO'}, f"Grid間隔を {val:.1f} に設定しました")
return {'FINISHED'}
class SPHERECAM_OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_SAFE}_update_info"
bl_label = "更新"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
return {'FINISHED'}
class SPHERECAM_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))
view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
lines = []
if rv3d.view_perspective == 'PERSP':
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("投影モード: 透視投影 (Perspective)")
lines.append(f"架空の視座位置 X: {cam_pos.x:.1f}, Y: {cam_pos.y:.1f}, Z: {cam_pos.z:.1f}")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離: {view_dist:.1f}")
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
lines.append("投影モード: 平行投影 (Orthographic)")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class SPHERECAM_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 OBJ_NAME_CAMERA not in cam.name: return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam_loc = eval_cam.matrix_world.to_translation()
cam_rot = eval_cam.matrix_world.to_quaternion()
view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = res_y / res_x if res_x > 0 else 1.0
fov_h = cam.data.angle
w = 2.0 * dist * math.tan(fov_h / 2.0)
h = w * aspect
lines = []
lines.append(f"投影モード: カメラ ({cam.name})")
lines.append(f"架空の視座位置 X: {cam_loc.x:.1f}, Y: {cam_loc.y:.1f}, Z: {cam_loc.z:.1f}")
lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
if tgt:
lines.append(f"画面中央位置 X: {tgt_loc.x:.1f}, Y: {tgt_loc.y:.1f}, Z: {tgt_loc.z:.1f}")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離: {dist:.1f}")
else:
lines.append(f"画面中央位置 X: なし, Y: なし, Z: なし")
lines.append(f"画面水平の大きさ: {w:.1f}")
lines.append(f"画面上下の大きさ: {h:.1f}")
lines.append(f"視座からの距離 (Focus): {dist:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
return {'FINISHED'}
class SPHERECAM_OT_auto_animate(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_auto_animate"
bl_label = "経度・緯度のアニメーションを自動設定"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
prefs = context.preferences
prev_interp = prefs.edit.keyframe_new_interpolation_type
try:
prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
props.cam_sphere_lon = props.anim_start_lon
props.cam_sphere_lat = props.anim_start_lat
props.keyframe_insert(data_path="cam_sphere_lon", frame=props.anim_start_frame)
props.keyframe_insert(data_path="cam_sphere_lat", frame=props.anim_start_frame)
props.cam_sphere_lon = props.anim_end_lon
props.cam_sphere_lat = props.anim_end_lat
props.keyframe_insert(data_path="cam_sphere_lon", frame=props.anim_end_frame)
props.keyframe_insert(data_path="cam_sphere_lat", frame=props.anim_end_frame)
finally:
prefs.edit.keyframe_new_interpolation_type = prev_interp
self.report({'INFO'}, f"アニメ設定完了: {props.anim_start_frame}F 〜 {props.anim_end_frame}F")
return {'FINISHED'}
class SPHERECAM_OT_create_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_SAFE}_create_rig"
bl_label = "球面リグ作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_SAFE}_props")
names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_ROTATOR, OBJ_NAME_RADIUS, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
for name in names:
obj = bpy.data.objects.get(name)
if obj: bpy.data.objects.remove(obj, do_unlink=True)
main_col = bpy.data.collections.get(COLLECTION_NAME)
if not main_col:
main_col = bpy.data.collections.new(COLLECTION_NAME)
context.scene.collection.children.link(main_col)
# 軌道の中心(Center/Rotation)をDriverで連動
track_obj = bpy.data.objects.new(OBJ_NAME_CAM_TRACK, None)
track_obj.empty_display_type = 'SPHERE'
main_col.objects.link(track_obj)
for i in range(3):
setup_driver(track_obj, "location", i, f"cam_sphere_center[{i}]")
setup_driver(track_obj, "rotation_euler", i, f"cam_sphere_rotation[{i}]")
# 回転用 (Lon, Lat) をDriverで連動
rot_obj = bpy.data.objects.new(OBJ_NAME_ROTATOR, None)
rot_obj.empty_display_type = 'ARROWS'
rot_obj.rotation_mode = 'ZYX'
rot_obj.parent = track_obj
main_col.objects.link(rot_obj)
setup_driver(rot_obj, "rotation_euler", 2, "cam_sphere_lon", is_angle=True)
setup_driver(rot_obj, "rotation_euler", 1, "cam_sphere_lat", is_angle=True, is_negative=True)
# 半径用 (Radius) をDriverで連動
rad_obj = bpy.data.objects.new(OBJ_NAME_RADIUS, None)
rad_obj.empty_display_type = 'PLAIN_AXES'
rad_obj.parent = rot_obj
main_col.objects.link(rad_obj)
setup_driver(rad_obj, "location", 0, "cam_sphere_radius")
# ターゲット
tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_empty.empty_display_type = 'PLAIN_AXES'
main_col.objects.link(tgt_empty)
# カメラ
cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
cam_obj.rotation_mode = 'XYZ'
main_col.objects.link(cam_obj)
# カメラの制約
cam_copy_loc = cam_obj.constraints.new(type='COPY_LOCATION')
cam_copy_loc.target = rad_obj
cam_track = cam_obj.constraints.new(type='TRACK_TO')
cam_track.target = tgt_empty
cam_track.track_axis = 'TRACK_NEGATIVE_Z'
cam_track.up_axis = 'UP_Y'
# 包み(シールド)作成
mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
main_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)
main_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)
main_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 = bpy.data.materials.new("CamShield_Mat_Sphere")
mat.use_nodes = True
mat.blend_method = 'BLEND'
shield_obj.data.materials.append(mat)
shield_obj.show_transparent = True
cprops = cam_obj.cam_rig_props
cprops.cam_shield_radius = 10.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 5.0
cprops.cam_shield_color = (1.0, 1.0, 0.0)
cprops.cam_shield_alpha = 0.3
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = tgt_empty
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
props.mute_target_tracking = False
props.cam_target_mode = 'OBJECT'
props.cam_target_obj1 = context.active_object if context.active_object and context.active_object.type not in ['CAMERA', 'LIGHT'] else None
update_cam_target(props, context)
update_cam_shield(cprops, context)
update_cam_shield_material(cprops, context)
rv3d = get_rv3d(context)
if rv3d: rv3d.view_perspective = 'CAMERA'
self.report({'INFO'}, "球面軌道カメラをセットアップしました (完全アニメーション対応)")
return {'FINISHED'}
class SPHERECAM_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_open_url"
bl_label = "リンク"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SPHERECAM_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_SAFE}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル1】 ビュー & 環境操作ツール
# =========================================================================
class SPHERECAM_PT_view_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_view_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:
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
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_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
view_rot = rv3d.view_rotation
cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
area = context.area
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
col_v = box_info.column(align=True)
if rv3d.view_perspective == 'PERSP':
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
col_v.label(text="[ 透視投影 (Perspective) ]", 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_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.label(text=f"視座からの距離: {view_dist:.1f}")
col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
col_v.label(text="[ 平行投影 (Orthographic) ]", icon='VIEW_ORTHO')
col_v.label(text=f"視線方向: X:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
col_v.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括コピー", icon='COPYDOWN')
layout.separator(factor=1.5)
# Grid / 背景設定
box_grid = layout.box()
box_grid.label(text="Grid / 背景 設定", icon='GRID')
row_preset = box_grid.row(align=True)
row_preset.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="間隔 10").scale_val = 10.0
row_preset.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="間隔 100").scale_val = 100.0
row_custom = box_grid.row(align=True)
row_custom.prop(props, "custom_grid_scale")
op_custom = row_custom.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="指定値にする")
op_custom.use_custom = True
box_grid.separator()
prefs = context.preferences
if len(prefs.themes) > 0:
theme_view3d = prefs.themes[0].view_3d
col_theme = box_grid.column(align=True)
col_theme.prop(theme_view3d, "grid", text="Grid 色変更")
col_theme.separator()
col_theme.prop(theme_view3d.space.gradients, "high_gradient", text="背景色 (上/単色)")
col_theme.prop(theme_view3d.space.gradients, "low_gradient", text="背景色 (下)")
layout.separator(factor=1.5)
box_render = layout.box()
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="アドオンのWebリンク", icon='URL').url = LINK_URL
box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【UIパネル2】 球面カメラ軌道
# =========================================================================
class SPHERECAM_PT_camera_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_SAFE.upper()}_PT_camera_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_rig = layout.box()
box_rig.label(text="専用カメラ (同期球面軌道):", icon='MESH_UVSPHERE')
row_rig = box_rig.row(align=True)
row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="球面リグ作成", icon='ADD')
row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
cam = context.scene.camera
if cam and OBJ_NAME_CAMERA in cam.name:
box_cam = layout.box()
box_cam.label(text="軌道の操作 (経度・緯度):", icon='PLAY')
col_pos = box_cam.column(align=True)
col_pos.enabled = not props.mute_target_tracking
col_pos.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
col_pos.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
col_pos.separator()
col_pos.prop(props, "cam_sphere_center", text="球の中心")
col_pos.prop(props, "cam_sphere_radius", text="球の半径")
col_pos.prop(props, "cam_sphere_rotation", text="球の傾き")
box_cam.separator()
box_cam.label(text="アニメーション自動設定:", icon='REC')
col_anim = box_cam.column(align=True)
col_anim.enabled = not props.mute_target_tracking
row_f = col_anim.row(align=True)
row_f.prop(props, "anim_start_frame", text="開始F")
row_f.prop(props, "anim_end_frame", text="終了F")
row_lon = col_anim.row(align=True)
row_lon.prop(props, "anim_start_lon", text="開始 経度")
row_lon.prop(props, "anim_end_lon", text="終了 経度")
row_lat = col_anim.row(align=True)
row_lat.prop(props, "anim_start_lat", text="開始 緯度")
row_lat.prop(props, "anim_end_lat", text="終了 緯度")
col_anim.operator(f"object.{PREFIX_SAFE}_auto_animate", text="キーフレームを打つ", icon='REC')
box_cam.separator()
box_cam.label(text="注視点の設定:", icon='EMPTY_DATA')
col_tgt_main = box_cam.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="カメラ枠外を暗くする")
if cam.data.show_passepartout:
col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", 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, "aperture_fstop", text="F値 (小さいとボケる)")
# 手動操作 (Location, Pitch, Roll, Yaw) と情報表示
box_sight = box_cam.box()
box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
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)")
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_cam.separator()
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="包み")
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.prop(cprops, "cam_shield_radius", text="半径")
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)
layout.separator(factor=1.5)
# 軌道カメラの個別情報
box_info = layout.box()
row_info_head = box_info.row()
row_info_head.label(text="カメラ位置・座標・範囲情報:", icon='CAMERA_DATA')
row_info_head.operator(f"view3d.{PREFIX_SAFE}_update_info", text="", icon='FILE_REFRESH')
depsgraph = context.evaluated_depsgraph_get()
eval_cam = cam.evaluated_get(depsgraph)
cam_loc = eval_cam.matrix_world.to_translation()
cam_rot = eval_cam.matrix_world.to_quaternion()
view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = res_y / res_x if res_x > 0 else 1.0
fov_h = cam.data.angle
w = 2.0 * dist * math.tan(fov_h / 2.0)
h = w * aspect
col_c = box_info.column(align=True)
col_c.label(text=f"[ 軌道カメラ ({cam.name}) ]", icon='CAMERA_DATA')
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:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
if tgt:
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:.1f}")
col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
else:
col_c.label(text="注視点: なし")
col_c.label(text=f"視座からの距離(Focus): {dist:.1f}")
col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
col_c.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括コピー", icon='COPYDOWN')
else:
layout.label(text="※専用カメラがアクティブではありません", icon='INFO')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
CamRigObjectProperties, SphereCamRigProperties,
SPHERECAM_OT_reset_view, SPHERECAM_OT_rotate_selected, SPHERECAM_OT_rotate_view, SPHERECAM_OT_set_grid_scale,
SPHERECAM_OT_update_info, SPHERECAM_OT_copy_view_info, SPHERECAM_OT_copy_cam_info,
SPHERECAM_OT_auto_animate, SPHERECAM_OT_create_rig,
SPHERECAM_OT_open_url, SPHERECAM_OT_remove_addon,
SPHERECAM_PT_view_panel, SPHERECAM_PT_camera_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=SphereCamRigProperties))
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()