カメラ線分軌道特化 20260711
合体
bl_info = {
"name": "Viewport & Sync Camera Master",
"author": "Your Name",
"version": (3, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "画面中央化、ビュー詳細情報取得、グリッド・背景色設定、線分軌道カメラ(相対オフセット対応)",
"category": "3D View",
}
# 【作成日】 2026年7月11日 12:44
# ===================================
# 【初期設定・定数定義】 (1行ずつ定義)
# ===================================
PREFIX = "sync_cam"
TAB_NAME = "Sync Cam Master"
GRID_DEFAULT_1 = 10.0
GRID_DEFAULT_2 = 100.0
LINK_NAME_1 = "基本アドオン設定 20260711b"
LINK_URL_1 = "<https://app.notion.com/p/20260711-39af5dacaf438027811dcd877db7c630>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_CAM = "カメラ線分軌道特化 20260711"
LINK_URL_CAM = "<https://app.notion.com/p/20260711-399f5dacaf4380f3bd2bcea570b2b686>"
# --- カメラリグ用 定数 ---
COLLECTION_NAME = "CamRig_Line_Collection"
OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA_ROOT = "TrackingCamera_Root_Line" # 追加: 制約を持つ親
OBJ_NAME_CAMERA = "TrackingCamera_Line" # 子: 実体(オフセット用)
OBJ_NAME_TARGET = "CameraTarget_Line"
SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"
INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.0)
import bpy
import bmesh
import math
import mathutils
import webbrowser
# ===================================
# 【関数群: ビュー情報取得】
# ===================================
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 get_view_info_data(context):
rv3d = None
area = None
space = None
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
rv3d = a.spaces.active.region_3d
area = a
space = a.spaces.active
break
if not rv3d or not area or not space: return None
mode = rv3d.view_perspective
target_pos = rv3d.view_location
view_dist = rv3d.view_distance
cam_pos = rv3d.view_matrix.inverted().translation
view_dir = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
if mode == 'PERSP':
proj_text = "透視投影"
is_persp = True
elif mode == 'ORTHO':
proj_text = "平行投影"
is_persp = False
elif mode == 'CAMERA':
proj_text = "カメラビュー"
is_persp = True
else:
proj_text = "不明"
is_persp = False
if is_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
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
return {
'proj_text': proj_text,
'cam_pos': cam_pos,
'view_dir': view_dir,
'target_pos': target_pos,
'dist': view_dist,
'w': w,
'h': h,
'is_persp': is_persp
}
# ===================================
# 【関数群: カメラリグ制御用】
# ===================================
def update_line_positions(self, context):
cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
if cam_curve and cam_curve.data.splines:
pts = cam_curve.data.splines[0].points
if len(pts) >= 2:
pts[0].co = (*self.cam_line_start, 1.0)
pts[1].co = (*self.cam_line_end, 1.0)
tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
if tgt_curve and tgt_curve.data.splines:
pts = tgt_curve.data.splines[0].points
if len(pts) >= 2:
pts[0].co = (*self.tgt_line_start, 1.0)
pts[1].co = (*self.tgt_line_end, 1.0)
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)
fp = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None)
mode = self.cam_target_mode
if mode == 'LINE_TRACK':
if fp: fp.mute = False
else:
if fp: fp.mute = True
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_root = bpy.data.objects.get(OBJ_NAME_CAMERA_ROOT)
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not cam_root: return
cam_track = next((c for c in cam_root.constraints if c.type == 'TRACK_TO'), None)
cam_path = next((c for c in cam_root.constraints if c.type == 'FOLLOW_PATH'), None)
tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else None
if self.mute_target_tracking:
depsgraph = context.evaluated_depsgraph_get()
eval_root = cam_root.evaluated_get(depsgraph)
cam_root.location = eval_root.matrix_world.to_translation()
cam_root.rotation_euler = eval_root.matrix_world.to_euler(cam_root.rotation_mode)
if tgt:
eval_tgt = tgt.evaluated_get(depsgraph)
tgt.location = eval_tgt.matrix_world.to_translation()
if cam_track: cam_track.mute = True
if cam_path: cam_path.mute = True
if tgt_path: tgt_path.mute = True
else:
if cam_track: cam_track.mute = False
if cam_path: cam_path.mute = False
if tgt_path and self.cam_target_mode == 'LINE_TRACK':
tgt_path.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_target_empty_visibility(self, context):
for obj in bpy.data.objects:
if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, OBJ_NAME_CAMERA_ROOT]:
obj.hide_viewport = not self.show_target_empty
# ===================================
# 【関数群: 包み (シールド) 制御用】
# ===================================
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)
# ===================================
# 【プロパティ定義】
# ===================================
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=1.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 SYNC_CAM_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=50.0, min=0.001)
# --- カメラリグ用 プロパティ ---
anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0)
anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点", default=INIT_CAM_START, update=update_line_positions)
cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点", default=INIT_CAM_END, update=update_line_positions)
tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点", default=INIT_TGT_START, update=update_line_positions)
tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点", default=INIT_TGT_END, update=update_line_positions)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法",
items=[
('LINE_TRACK', "同期線分軌道", ""),
('OBJECT', "単一オブジェクト", ""),
('POINT', "指定座標 (手動)", ""),
('MIDPOINT', "2オブジェクトの中間", "")
],
default='LINE_TRACK', 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)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
# ===================================
# 【オペレーター群: 基本】
# ===================================
class SYNC_CAM_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class SYNC_CAM_OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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 SYNC_CAM_OT_copy_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_copy_info"
bl_label = "情報をコピー"
def execute(self, context):
info = get_view_info_data(context)
if not info: return {'CANCELLED'}
lines = []
lines.append(f"投影モード: {info['proj_text']}")
if info['is_persp']:
lines.append(f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
lines.append(f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
lines.append(f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
lines.append(f"画面水平の大きさ: {info['w']:.1f}")
lines.append(f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
lines.append(f"視座からの距離: {info['dist']:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class SYNC_CAM_OT_set_grid_scale(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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}_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
return {'FINISHED'}
class SYNC_CAM_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class SYNC_CAM_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# ===================================
# 【オペレーター群: カメラリグ】
# ===================================
class SYNC_CAM_OT_create_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_rig"
bl_label = "線分リグ作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
names = [OBJ_NAME_CAMERA, OBJ_NAME_CAMERA_ROOT, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, 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)
def create_line_curve(name, p1, p2):
crv_data = bpy.data.curves.new(name, type='CURVE')
crv_data.dimensions = '3D'
spline = crv_data.splines.new('POLY')
spline.points.add(1)
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
crv_obj = bpy.data.objects.new(name, crv_data)
main_col.objects.link(crv_obj)
return crv_obj
tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_empty.empty_display_type = 'PLAIN_AXES'
main_col.objects.link(tgt_empty)
tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
tgt_path.target = tgt_curve
tgt_path.use_curve_follow = False
tgt_path.use_fixed_location = True
# --- ルートエンプティ (制約を持つ親) の作成 ---
cam_root = bpy.data.objects.new(OBJ_NAME_CAMERA_ROOT, None)
cam_root.empty_display_type = 'ARROWS'
main_col.objects.link(cam_root)
# --- カメラ本体 (オフセット用の子) の作成 ---
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'
cam_obj.location = (0, 0, 0)
cam_obj.rotation_euler = (0, 0, 0)
main_col.objects.link(cam_obj)
# 親子付け (相対オフセット実現の要)
cam_obj.parent = cam_root
# 制約はルートに付与
cam_path = cam_root.constraints.new(type='FOLLOW_PATH')
cam_path.target = cam_curve
cam_path.use_curve_follow = False
cam_path.use_fixed_location = True
cam_track = cam_root.constraints.new(type='TRACK_TO')
cam_track.target = tgt_empty
cam_track.track_axis = 'TRACK_NEGATIVE_Z'
cam_track.up_axis = 'UP_Y'
dp = f"{PREFIX}_props.anim_factor"
def add_offset_driver(constraint):
d = constraint.driver_add("offset_factor").driver
d.type = 'AVERAGE'
v = d.variables.new()
v.name = "var"
v.type = 'SINGLE_PROP'
v.targets[0].id_type = 'SCENE'
v.targets[0].id = context.scene
v.targets[0].data_path = dp
add_offset_driver(tgt_path)
add_offset_driver(cam_path)
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_Line")
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 = 1.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 1.0
cprops.cam_shield_color = (0.0, 1.0, 1.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 = 'LINE_TRACK'
update_line_positions(props, context)
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 SYNC_CAM_OT_auto_animate(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_auto_animate"
bl_label = "進行度アニメーションを自動設定"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
prefs = context.preferences
prev_interp = prefs.edit.keyframe_new_interpolation_type
try:
prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
props.anim_factor = 0.0
props.keyframe_insert(data_path="anim_factor", frame=props.anim_start_frame)
props.anim_factor = 1.0
props.keyframe_insert(data_path="anim_factor", 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 SYNC_CAM_OT_copy_cam_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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'}
# ===================================
# 【UIパネル1: 基本ビュー & 環境】
# ===================================
class SYNC_CAM_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_main_panel"
bl_label = "Viewport & Sync Camera"
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}_props")
info = get_view_info_data(context)
layout.operator(f"view3d.{PREFIX}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_info = layout.box()
box_info.label(text=f"情報: [{info['proj_text'] if info else '不明'}]", icon='INFO')
box_info.operator(f"view3d.{PREFIX}_update_info", text="情報を更新", icon='FILE_REFRESH')
if info:
col_info = box_info.column(align=True)
col_info.label(text="画面中央位置:")
col_info.label(text=f" X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
col_info.separator()
col_info.label(text="画面中央への視線方向:")
col_info.label(text=f" X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
col_info.separator()
col_info.label(text=f"画面水平の大きさ: {info['w']:.1f}")
col_info.label(text=f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
col_info.separator()
col_info.label(text=f"視座からの距離: {info['dist']:.1f}")
box_info.separator()
box_info.operator(f"view3d.{PREFIX}_copy_info", text="情報をコピー (1行ずつペースト)", icon='COPYDOWN')
else:
box_info.label(text="3Dビューが見つかりません")
layout.separator(factor=1.5)
box_grid = layout.box()
box_grid.label(text="Grid / 背景 設定", icon='GRID')
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_1}").scale_val = GRID_DEFAULT_1
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_2}").scale_val = GRID_DEFAULT_2
box_grid.prop(props, "custom_grid_scale")
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text="指定値にする").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_sys = layout.box()
box_sys.label(text="システム / リンク", icon='PREFERENCES')
op1 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL')
op1.url = LINK_URL_1
op2 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL')
op2.url = LINK_URL_2
op3 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL')
op3.url = LINK_URL_3
box_sys.separator()
box_sys.operator(f"wm.{PREFIX}_remove_addon", text="アドオンの登録を解除", icon='CANCEL')
# ===================================
# 【UIパネル2: カメラ軌道 (線分特化)】
# ===================================
class SYNC_CAM_PT_camera_panel(bpy.types.Panel):
bl_idname = f"{PREFIX.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}_props")
# --- 専用カメラ作成 ---
box_rig = layout.box()
box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
box_rig.operator(f"object.{PREFIX}_create_rig", text="線分リグ作成", icon='ADD')
box_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_anim = layout.box()
box_anim.label(text="アニメーション & 軌道の操作:", icon='PLAY')
col_anim = box_anim.column(align=True)
col_anim.enabled = not props.mute_target_tracking
col_anim.prop(props, "anim_factor", slider=True)
col_auto = box_anim.column(align=True)
col_auto.enabled = not props.mute_target_tracking
col_auto.prop(props, "anim_start_frame", text="開始F")
col_auto.prop(props, "anim_end_frame", text="終了F")
col_auto.operator(f"object.{PREFIX}_auto_animate", text="アニメ自動設定", icon='REC')
box_anim.separator()
col_pos = box_anim.column(align=True)
col_pos.enabled = not props.mute_target_tracking
col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
# --- カメラ情報表示 (注視点設定の上に移動) ---
box_info_cam = layout.box()
box_info_cam.label(text="カメラ位置・座標・範囲情報:", icon='CAMERA_DATA')
box_info_cam.operator(f"view3d.{PREFIX}_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_cam.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}_copy_cam_info", text="情報を一括コピー", icon='COPYDOWN')
# --- 注視点の設定 ---
box_tgt = layout.box()
box_tgt.label(text="注視点の設定:", icon='EMPTY_DATA')
col_tgt_main = box_tgt.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", text="モード")
col_tgt = box_tgt.column(align=True)
col_tgt.enabled = not props.mute_target_tracking
if props.cam_target_mode == 'LINE_TRACK':
col_tgt.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
col_tgt.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
elif 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_manual = layout.box()
box_manual.label(text="補助的修正 (相対オフセット):", icon='EDITMODE_HLT')
col_man = box_manual.column(align=True)
# ローカル軸 (TrackTo: -Z, Up: Y) に合わせたわかりやすいラベル
col_man.prop(cam, "location", index=0, text="シフト X (左右)")
col_man.prop(cam, "location", index=2, text="シフト Z (前後・ズーム)")
col_man.prop(cam, "location", index=1, text="シフト Y (上下)")
col_man.separator()
col_man.prop(cam, "rotation_euler", index=0, text="視線 X (Pitch・上下)")
col_man.prop(cam, "rotation_euler", index=1, text="視線 Y (Yaw・左右)")
col_man.prop(cam, "rotation_euler", index=2, text="視線 Z (Roll・傾き)")
# --- レンズ設定 ---
box_lens = layout.box()
box_lens.label(text="レンズ設定:", icon='VIEW_CAMERA')
col_lens = box_lens.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="クリップ開始")
col_lens.prop(cam.data, "clip_end", text="クリップ終了")
col_lens.separator()
col_lens.prop(cam.data, "show_passepartout", text="枠外を暗くする")
if cam.data.show_passepartout:
col_lens.prop(cam.data, "passepartout_alpha", text="枠外の暗さ", slider=True)
col_lens.separator()
col_lens.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)")
if cam.data.dof.use_dof:
col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
# --- 包み設定 ---
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = layout.box()
box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
col_disp = box_shield.column(align=True)
col_disp.prop(cprops, "show_cam_obj", text="カメラ本体を表示")
col_disp.prop(cprops, "show_cam_shield", text="包みを表示")
col_disp.prop(props, "show_target_empty", 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.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (角度)")
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)
else:
layout.label(text="※専用カメラがアクティブではありません", icon='INFO')
# --- 専用リンク ---
layout.separator(factor=1.5)
box_link = layout.box()
box_link.label(text="システム / リンク:", icon='URL')
op_cam = box_link.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_CAM, icon='URL')
op_cam.url = LINK_URL_CAM
# ===================================
# 【登録処理】
# ===================================
classes = [
CamRigObjectProperties,
SYNC_CAM_Properties,
SYNC_CAM_OT_reset_view,
SYNC_CAM_OT_update_info,
SYNC_CAM_OT_copy_info,
SYNC_CAM_OT_set_grid_scale,
SYNC_CAM_OT_open_url,
SYNC_CAM_OT_remove_addon,
SYNC_CAM_OT_create_rig,
SYNC_CAM_OT_auto_animate,
SYNC_CAM_OT_copy_cam_info,
SYNC_CAM_PT_main_panel,
SYNC_CAM_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}_props", bpy.props.PointerProperty(type=SYNC_CAM_Properties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
主な修正内容
タイトルとPrefixの最適化
アドオン名を Viewport & Sync Camera Master に、内部のPrefixを sync_cam に変更し、カメラ同期とビュー操作に特化した名前に刷新しました。
「補助的修正」の相対オフセット化 (カメラリグの階層化)
カメラを直接制約(Follow Path や Track To)で動かすのではなく、「目に見えないルートオブジェクト(親)」を軌道に乗せ、カメラ本体をその「子」として追従させる階層構造(リグ)に作り直しました。
これにより、手動での XYZ移動 や 視線方向(Pitch等) の修正が、設定軌道を壊すことなく純粋な「相対オフセット値(修正値)」として機能するようになります。ミュート状態に関係なく、いつでも補正が可能です。
ローカル軸に合わせたUIラベルの調整
カメラ視線(-Z方向)に合わせたローカル軸に基づき、Xが左右、Yが上下、Zが前後となるようにラベルを親切な表記に変更しました。
補助 オフセット反映
bl_info = {
"name": "Viewport & Automation Master",
"author": "Your Name",
"version": (2, 9),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "画面中央化、ビュー詳細情報取得、グリッド・背景色設定、線分軌道カメラ",
"category": "3D View",
}
# 【作成日】 2026年7月11日 12:44
# ===================================
# 【初期設定・定数定義】 (1行ずつ定義)
# ===================================
PREFIX = "vgm_tools"
TAB_NAME = "VGM Tools"
GRID_DEFAULT_1 = 10.0
GRID_DEFAULT_2 = 100.0
LINK_NAME_1 = "基本アドオン設定 20260711b"
LINK_URL_1 = "<https://app.notion.com/p/20260711-39af5dacaf438027811dcd877db7c630>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_CAM = "カメラ線分軌道特化 20260711"
LINK_URL_CAM = "<https://app.notion.com/p/20260711-399f5dacaf4380f3bd2bcea570b2b686>"
# --- カメラリグ用 定数 ---
COLLECTION_NAME = "CamRig_Line_Collection"
OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA = "TrackingCamera_Line"
OBJ_NAME_TARGET = "CameraTarget_Line"
SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"
INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.0)
import bpy
import bmesh
import math
import mathutils
import webbrowser
# ===================================
# 【関数群: ビュー情報取得】
# ===================================
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 get_view_info_data(context):
rv3d = None
area = None
space = None
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
rv3d = a.spaces.active.region_3d
area = a
space = a.spaces.active
break
if not rv3d or not area or not space: return None
mode = rv3d.view_perspective
target_pos = rv3d.view_location
view_dist = rv3d.view_distance
cam_pos = rv3d.view_matrix.inverted().translation
view_dir = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
if mode == 'PERSP':
proj_text = "透視投影"
is_persp = True
elif mode == 'ORTHO':
proj_text = "平行投影"
is_persp = False
elif mode == 'CAMERA':
proj_text = "カメラビュー"
is_persp = True
else:
proj_text = "不明"
is_persp = False
if is_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
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
return {
'proj_text': proj_text,
'cam_pos': cam_pos,
'view_dir': view_dir,
'target_pos': target_pos,
'dist': view_dist,
'w': w,
'h': h,
'is_persp': is_persp
}
# ===================================
# 【関数群: カメラリグ制御用】
# ===================================
def update_line_positions(self, context):
cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
if cam_curve and cam_curve.data.splines:
pts = cam_curve.data.splines[0].points
if len(pts) >= 2:
pts[0].co = (*self.cam_line_start, 1.0)
pts[1].co = (*self.cam_line_end, 1.0)
tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
if tgt_curve and tgt_curve.data.splines:
pts = tgt_curve.data.splines[0].points
if len(pts) >= 2:
pts[0].co = (*self.tgt_line_start, 1.0)
pts[1].co = (*self.tgt_line_end, 1.0)
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)
fp = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None)
mode = self.cam_target_mode
if mode == 'LINE_TRACK':
if fp: fp.mute = False
else:
if fp: fp.mute = True
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)
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not cam: return
cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
cam_path = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else 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 tgt:
eval_tgt = tgt.evaluated_get(depsgraph)
tgt.location = eval_tgt.matrix_world.to_translation()
if cam_track: cam_track.mute = True
if cam_path: cam_path.mute = True
if tgt_path: tgt_path.mute = True
else:
if cam_track: cam_track.mute = False
if cam_path: cam_path.mute = False
if tgt_path and self.cam_target_mode == 'LINE_TRACK':
tgt_path.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_target_empty_visibility(self, context):
for obj in bpy.data.objects:
if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK]:
obj.hide_viewport = not self.show_target_empty
# ===================================
# 【関数群: 包み (シールド) 制御用】
# ===================================
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)
# ===================================
# 【プロパティ定義】
# ===================================
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=1.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 VGM_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=50.0, min=0.001)
# --- カメラリグ用 プロパティ ---
anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0)
anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点", default=INIT_CAM_START, update=update_line_positions)
cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点", default=INIT_CAM_END, update=update_line_positions)
tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点", default=INIT_TGT_START, update=update_line_positions)
tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点", default=INIT_TGT_END, update=update_line_positions)
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法",
items=[
('LINE_TRACK', "同期線分軌道", ""),
('OBJECT', "単一オブジェクト", ""),
('POINT', "指定座標 (手動)", ""),
('MIDPOINT', "2オブジェクトの中間", "")
],
default='LINE_TRACK', 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)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
# ===================================
# 【オペレーター群: 基本】
# ===================================
class VGM_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class VGM_OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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 VGM_OT_copy_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_copy_info"
bl_label = "情報をコピー"
def execute(self, context):
info = get_view_info_data(context)
if not info: return {'CANCELLED'}
lines = []
lines.append(f"投影モード: {info['proj_text']}")
if info['is_persp']:
lines.append(f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
lines.append(f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
lines.append(f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
lines.append(f"画面水平の大きさ: {info['w']:.1f}")
lines.append(f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
lines.append(f"視座からの距離: {info['dist']:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class VGM_OT_set_grid_scale(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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}_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
return {'FINISHED'}
class VGM_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class VGM_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# ===================================
# 【オペレーター群: カメラリグ】
# ===================================
class VGM_OT_create_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_rig"
bl_label = "線分リグ作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, 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)
def create_line_curve(name, p1, p2):
crv_data = bpy.data.curves.new(name, type='CURVE')
crv_data.dimensions = '3D'
spline = crv_data.splines.new('POLY')
spline.points.add(1)
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
crv_obj = bpy.data.objects.new(name, crv_data)
main_col.objects.link(crv_obj)
return crv_obj
tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_empty.empty_display_type = 'PLAIN_AXES'
main_col.objects.link(tgt_empty)
tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
tgt_path.target = tgt_curve
tgt_path.use_curve_follow = False
tgt_path.use_fixed_location = True
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_path = cam_obj.constraints.new(type='FOLLOW_PATH')
cam_path.target = cam_curve
cam_path.use_curve_follow = False
cam_path.use_fixed_location = True
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'
dp = f"{PREFIX}_props.anim_factor"
def add_offset_driver(constraint):
d = constraint.driver_add("offset_factor").driver
d.type = 'AVERAGE'
v = d.variables.new()
v.name = "var"
v.type = 'SINGLE_PROP'
v.targets[0].id_type = 'SCENE'
v.targets[0].id = context.scene
v.targets[0].data_path = dp
add_offset_driver(tgt_path)
add_offset_driver(cam_path)
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_Line")
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 = 1.0
cprops.cam_shield_hole_angle = 179.0
cprops.cam_shield_hole_radius_back = 1.0
cprops.cam_shield_color = (0.0, 1.0, 1.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 = 'LINE_TRACK'
update_line_positions(props, context)
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 VGM_OT_auto_animate(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_auto_animate"
bl_label = "進行度アニメーションを自動設定"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
prefs = context.preferences
prev_interp = prefs.edit.keyframe_new_interpolation_type
try:
prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
props.anim_factor = 0.0
props.keyframe_insert(data_path="anim_factor", frame=props.anim_start_frame)
props.anim_factor = 1.0
props.keyframe_insert(data_path="anim_factor", 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 VGM_OT_copy_cam_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_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'}
# ===================================
# 【UIパネル1: 基本ビュー & 環境】
# ===================================
class VGM_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_main_panel"
bl_label = "Viewport & Grid Master"
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}_props")
info = get_view_info_data(context)
layout.operator(f"view3d.{PREFIX}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
layout.separator(factor=1.5)
box_info = layout.box()
box_info.label(text=f"情報: [{info['proj_text'] if info else '不明'}]", icon='INFO')
box_info.operator(f"view3d.{PREFIX}_update_info", text="情報を更新", icon='FILE_REFRESH')
if info:
col_info = box_info.column(align=True)
col_info.label(text="画面中央位置:")
col_info.label(text=f" X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
col_info.separator()
col_info.label(text="画面中央への視線方向:")
col_info.label(text=f" X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
col_info.separator()
col_info.label(text=f"画面水平の大きさ: {info['w']:.1f}")
col_info.label(text=f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
col_info.separator()
col_info.label(text=f"視座からの距離: {info['dist']:.1f}")
box_info.separator()
box_info.operator(f"view3d.{PREFIX}_copy_info", text="情報をコピー (1行ずつペースト)", icon='COPYDOWN')
else:
box_info.label(text="3Dビューが見つかりません")
layout.separator(factor=1.5)
box_grid = layout.box()
box_grid.label(text="Grid / 背景 設定", icon='GRID')
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_1}").scale_val = GRID_DEFAULT_1
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_2}").scale_val = GRID_DEFAULT_2
box_grid.prop(props, "custom_grid_scale")
box_grid.operator(f"view3d.{PREFIX}_set_grid_scale", text="指定値にする").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_sys = layout.box()
box_sys.label(text="システム / リンク", icon='PREFERENCES')
op1 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL')
op1.url = LINK_URL_1
op2 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL')
op2.url = LINK_URL_2
op3 = box_sys.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL')
op3.url = LINK_URL_3
box_sys.separator()
box_sys.operator(f"wm.{PREFIX}_remove_addon", text="アドオンの登録を解除", icon='CANCEL')
# ===================================
# 【UIパネル2: カメラ軌道 (線分特化)】
# ===================================
class VGM_PT_camera_panel(bpy.types.Panel):
bl_idname = f"{PREFIX.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}_props")
# --- 専用カメラ作成 ---
box_rig = layout.box()
box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
box_rig.operator(f"object.{PREFIX}_create_rig", text="線分リグ作成", icon='ADD')
box_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_anim = layout.box()
box_anim.label(text="アニメーション & 軌道の操作:", icon='PLAY')
col_anim = box_anim.column(align=True)
col_anim.enabled = not props.mute_target_tracking
col_anim.prop(props, "anim_factor", slider=True)
col_anim.prop(props, "anim_start_frame", text="開始F")
col_anim.prop(props, "anim_end_frame", text="終了F")
col_anim.operator(f"object.{PREFIX}_auto_animate", text="アニメ自動設定", icon='REC')
box_anim.separator()
col_pos = box_anim.column(align=True)
col_pos.enabled = not props.mute_target_tracking
col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
# --- カメラ情報表示 (注視点設定の上に移動) ---
box_info_cam = layout.box()
box_info_cam.label(text="カメラ位置・座標・範囲情報:", icon='CAMERA_DATA')
box_info_cam.operator(f"view3d.{PREFIX}_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_cam.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}_copy_cam_info", text="情報を一括コピー", icon='COPYDOWN')
# --- 注視点の設定 ---
box_tgt = layout.box()
box_tgt.label(text="注視点の設定:", icon='EMPTY_DATA')
col_tgt_main = box_tgt.column(align=True)
col_tgt_main.enabled = not props.mute_target_tracking
col_tgt_main.prop(props, "cam_target_mode", text="モード")
col_tgt = box_tgt.column(align=True)
col_tgt.enabled = not props.mute_target_tracking
if props.cam_target_mode == 'LINE_TRACK':
col_tgt.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
col_tgt.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
elif 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_manual = layout.box()
box_manual.label(text="補助的修正 (手動):", icon='EDITMODE_HLT')
col_man = box_manual.column(align=True)
col_man.prop(cam, "location", index=0, text="カメラ位置 X")
col_man.prop(cam, "location", index=1, text="カメラ位置 Y")
col_man.prop(cam, "location", index=2, text="カメラ位置 Z")
col_man.separator()
col_man.prop(cam, "rotation_euler", index=0, text="カメラ視線 Pitch(X)")
col_man.prop(cam, "rotation_euler", index=1, text="カメラ視線 Roll(Y)")
col_man.prop(cam, "rotation_euler", index=2, text="カメラ視線 Yaw(Z)")
# --- レンズ設定 ---
box_lens = layout.box()
box_lens.label(text="レンズ設定:", icon='VIEW_CAMERA')
col_lens = box_lens.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="クリップ開始")
col_lens.prop(cam.data, "clip_end", text="クリップ終了")
col_lens.separator()
col_lens.prop(cam.data, "show_passepartout", text="枠外を暗くする")
if cam.data.show_passepartout:
col_lens.prop(cam.data, "passepartout_alpha", text="枠外の暗さ", slider=True)
col_lens.separator()
col_lens.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)")
if cam.data.dof.use_dof:
col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
# --- 包み設定 ---
if hasattr(cam, "cam_rig_props"):
cprops = cam.cam_rig_props
box_shield = layout.box()
box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
col_disp = box_shield.column(align=True)
col_disp.prop(cprops, "show_cam_obj", text="カメラ本体を表示")
col_disp.prop(cprops, "show_cam_shield", text="包みを表示")
col_disp.prop(props, "show_target_empty", 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.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (角度)")
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)
else:
layout.label(text="※専用カメラがアクティブではありません", icon='INFO')
# --- 専用リンク ---
layout.separator(factor=1.5)
box_link = layout.box()
box_link.label(text="システム / リンク:", icon='URL')
op_cam = box_link.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_CAM, icon='URL')
op_cam.url = LINK_URL_CAM
# ===================================
# 【登録処理】
# ===================================
classes = [
CamRigObjectProperties,
VGM_Properties,
VGM_OT_reset_view,
VGM_OT_update_info,
VGM_OT_copy_info,
VGM_OT_set_grid_scale,
VGM_OT_open_url,
VGM_OT_remove_addon,
VGM_OT_create_rig,
VGM_OT_auto_animate,
VGM_OT_copy_cam_info,
VGM_PT_main_panel,
VGM_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}_props", bpy.props.PointerProperty(type=VGM_Properties))
def unregister():
if hasattr(bpy.types.Object, "cam_rig_props"): delattr(bpy.types.Object, "cam_rig_props")
if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()