331 Camera simple
bl_info = {
"name": "Fixed Camera & Automation Master (単一カメラ版)",
"author": "Your Name",
"version": (4, 27),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き)",
"category": "3D View",
}
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed "
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()
# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = ""
LINK_URL_4 = ""
LINK_NAME_5 = ""
LINK_URL_5 = ""
# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
MAT_NAME_PUPIL = "Pupil_Window_Mat"
CAMERA_PRESETS = [
("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
# --- 選択・表示状態の初期値 ---
CAM_MOVE_MODE_DEFAULT = 'FIXED'
CAM_TARGET_MODE_DEFAULT = 'NONE'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = True
CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)
CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000
# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0
CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000
# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0
PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854
ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500
CUSTOM_GRID_SCALE_DEFAULT = 50.0
CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000
CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000
CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000
LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"
# ===================================
# 7. 共通クラス
# ===================================
class CP_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 CP_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# 8. アドオン固有機能
# ===================================
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_addon_source():
source = ""
try:
if '__file__' in globals() and os.path.exists(__file__):
with open(__file__, 'r', encoding='utf-8') as f:
source = f.read()
except Exception: pass
if not source:
for t in bpy.data.texts:
if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
source = t.as_string()
break
return source
def cam_fov_get(self):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
def update_visibility(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
cam.hide_viewport = not self.show_cam_obj
for name in [OBJ_NAME_ROOT, OBJ_NAME_TARGET]:
obj = bpy.data.objects.get(name)
if obj:
obj.hide_viewport = not self.show_target_empty
def update_target_anim(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not tgt: return
if tgt.animation_data:
tgt.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
speed = self.target_anim_speed_mult
if self.cam_target_mode == 'POINT':
tgt.location = self.cam_target_loc
elif self.cam_target_mode == 'LINE':
s_loc = mathutils.Vector(self.target_start_loc)
e_loc = mathutils.Vector(self.target_end_loc)
if delta_f <= 0:
tgt.location = s_loc
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
tgt.location = s_loc.lerp(e_loc, ratio)
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
elif self.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.target_circle_center)
radius = self.target_circle_radius
start_rad = math.radians(self.target_circle_start_angle)
end_rad = math.radians(self.target_circle_end_angle)
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_anim(self, context):
update_target_anim(self, context)
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if not root: return
if root.animation_data:
root.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
# 1. カメラ(Root)の位置ベイク
if self.cam_move_mode == 'FIXED':
root.location = self.cam_fixed_loc
root.keyframe_insert(data_path="location", frame=start_f)
elif self.cam_move_mode == 'LINE':
s_loc = mathutils.Vector(self.cam_start_loc)
e_loc = mathutils.Vector(self.cam_end_loc)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
root.location = s_loc
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
root.location = s_loc.lerp(e_loc, ratio)
root.keyframe_insert(data_path="location", frame=f)
elif self.cam_move_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.cam_circle_center)
radius = self.cam_circle_radius
start_rad = math.radians(self.cam_circle_start_angle)
end_rad = math.radians(self.cam_circle_end_angle)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=f)
# 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
if self.cam_target_mode == 'NONE':
root.rotation_mode = 'XYZ'
if delta_f <= 0:
root.rotation_euler = self.cam_rot_start
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
else:
speed_u = math.radians(self.cam_rot_speed[0])
speed_v = math.radians(self.cam_rot_speed[1])
speed_w = math.radians(self.cam_rot_speed[2])
start_rot = self.cam_rot_start
end_rot = (
start_rot[0] + speed_u * delta_f,
start_rot[1] + speed_v * delta_f,
start_rot[2] + speed_w * delta_f
)
root.rotation_euler = start_rot
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
root.rotation_euler = end_rot
root.keyframe_insert(data_path="rotation_euler", frame=end_f)
if root.animation_data and root.animation_data.action:
act = root.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_constraint(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
if not root: return
tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
if self.cam_target_mode == 'NONE':
if tt: root.constraints.remove(tt)
update_cam_anim(self, context)
else:
if not tt: tt = root.constraints.new(type='TRACK_TO')
tt.track_axis = 'TRACK_NEGATIVE_Z'
tt.up_axis = 'UP_Y'
if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
if tgt_obj:
tt.target = tgt_obj
update_target_anim(self, context)
elif self.cam_target_mode == 'OBJECT':
tt.target = self.cam_target_obj
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_loc(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if root: root.location = self.cam_fixed_loc
def update_cam_local_offset_rot(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam: cam.rotation_euler = self.cam_local_offset_rot
def update_camera_color(self, context):
try:
bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
except Exception:
pass
def update_camera_preset_func(self, context):
for p in CAMERA_PRESETS:
if p[0] == self.camera_preset:
self.camera_color = p[3]
break
def update_pupil_window(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if not obj: return
obj.hide_viewport = not self.pupil_window_show
obj.hide_render = not self.pupil_window_show
if not self.pupil_window_show:
return
obj.location = (0.0, 0.0, -self.pupil_window_dist)
mesh = obj.data
mesh.clear_geometry()
maj_s = self.pupil_window_segments
min_s = 16
val = self.pupil_window_radius
if maj_s == 4:
R = val / math.sqrt(2.0)
else:
R = val
r = self.pupil_window_thickness
verts = []
faces = []
for i in range(maj_s):
u = i * 2.0 * math.pi / maj_s
for j in range(min_s):
v = j * 2.0 * math.pi / min_s
x = (R + r * math.cos(v)) * math.cos(u)
y = (R + r * math.cos(v)) * math.sin(u)
z = r * math.sin(v)
verts.append((x, y, z))
for i in range(maj_s):
for j in range(min_s):
next_i = (i + 1) % maj_s
next_j = (j + 1) % min_s
v0 = i * min_s + j
v1 = i * min_s + next_j
v2 = next_i * min_s + next_j
v3 = next_i * min_s + j
faces.append((v0, v1, v2, v3))
mesh.from_pydata(verts, [], faces)
mesh.update()
def update_pupil_segments(self, context):
if self.pupil_window_segments == 4:
self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
else:
self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
update_pupil_window(self, context)
def update_pupil_window_rot(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj:
obj.rotation_euler = self.pupil_window_rot
def update_pupil_window_mat(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj and obj.data.materials:
mat = obj.data.materials[0]
mat.diffuse_color = self.pupil_window_color
if mat.use_nodes:
bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = self.pupil_window_color
# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
# カメラの移動設定
cam_move_mode: bpy.props.EnumProperty(
name="カメラ(Root)の移動",
items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
default=CAM_MOVE_MODE_DEFAULT, update=update_cam_anim
)
cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)
cam_target_mode: bpy.props.EnumProperty(
name="注視点設定",
items=[
('NONE', "オフ (速度ベース回転)", ""),
('POINT', "指定座標 (固定)", ""),
('LINE', "線分移動 (アニメ)", ""),
('CIRCLE', "円周移動 (アニメ)", ""),
('OBJECT', "別オブジェクト", "")
],
default=CAM_TARGET_MODE_DEFAULT, update=update_cam_root_constraint
)
cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)
target_anim_speed_mult: bpy.props.FloatProperty(name="注視点移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)
cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=SHOW_CAM_OBJ_DEFAULT, update=update_visibility)
show_target_empty: bpy.props.BoolProperty(name="Empty", default=SHOW_TARGET_EMPTY_DEFAULT, update=update_visibility)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
camera_preset: bpy.props.EnumProperty(
name="カメラ色プリセット",
items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS],
default=CAMERA_PRESET_DEFAULT,
update=update_camera_preset_func
)
camera_color: bpy.props.FloatVectorProperty(
name="カメラテーマ色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=CAMERA_COLOR_DEFAULT,
update=update_camera_color
)
pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)
info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")
# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_reset_view"
bl_label = "選択物を中央にする"
def execute(self, context):
rv3d = get_rv3d(context)
if rv3d and context.active_object:
rv3d.view_location = context.active_object.location
return {'FINISHED'}
class OBJECT_OT_create_cam_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_cam_rig"
bl_label = "カメラリグ作成"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_PUPIL]:
o = bpy.data.objects.get(n)
if o: bpy.data.objects.remove(o, do_unlink=True)
main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
if sub_col.name not in main_col.children: main_col.children.link(sub_col)
tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_obj.empty_display_type = 'PLAIN_AXES'
sub_col.objects.link(tgt_obj)
root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
root_obj.empty_display_type = 'PLAIN_AXES'
root_obj.location = props.cam_fixed_loc
root_obj.rotation_mode = 'XYZ'
root_obj.rotation_euler = props.cam_rot_start
sub_col.objects.link(root_obj)
cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
cam_obj.parent = root_obj
cam_obj.location = (0, 0, 0)
cam_obj.rotation_euler = props.cam_local_offset_rot
sub_col.objects.link(cam_obj)
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = False
cam_obj.data.dof.focus_object = tgt_obj
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
o_pupil.parent = cam_obj
o_pupil.rotation_mode = 'XYZ'
sub_col.objects.link(o_pupil)
mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
mat_pupil.use_nodes = True
mat_pupil.blend_method = 'BLEND'
o_pupil.data.materials.append(mat_pupil)
update_pupil_window(props, context)
update_pupil_window_rot(props, context)
update_pupil_window_mat(props, context)
update_cam_root_constraint(props, context)
update_visibility(props, context)
update_cam_anim(props, context)
update_camera_color(props, context)
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
space.region_3d.view_perspective = 'CAMERA'
space.lock_camera = False
self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
return {'FINISHED'}
class OBJECT_OT_sync_from_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_from_scene"
bl_label = "シーンから取得"
bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.anim_start_frame = context.scene.frame_start
props.anim_end_frame = context.scene.frame_end
self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_sync_to_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_to_scene"
bl_label = "シーンへ適用"
bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
context.scene.frame_start = props.anim_start_frame
context.scene.frame_end = props.anim_end_frame
self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_apply_cam_anim"
bl_label = "アニメーションを適用(更新)"
bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
update_cam_anim(props, context)
self.report({'INFO'}, "アニメーションキーフレームを更新しました")
return {'FINISHED'}
class OBJECT_OT_set_active_camera(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_set_active_camera"
bl_label = "専用カメラ視点にする"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, "専用カメラ視点に切り替えました")
else:
self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
return {'FINISHED'}
class OBJECT_OT_update_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_update_camera_info"
bl_label = "情報更新"
bl_description = "現在のビューから画面の情報を取得・更新します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
rv3d = get_rv3d(context)
if not rv3d:
self.report({'WARNING'}, "3Dビューが見つかりません")
return {'CANCELLED'}
if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
else: props.info_proj_mode = rv3d.view_perspective
dist = rv3d.view_distance
props.info_view_dist = dist
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = (res_x / res_y) if res_y != 0 else 1.0
if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
cam = context.scene.camera
if cam.name == OBJ_NAME_CAMERA:
props.info_active_camera_name = "メインカメラ"
else:
props.info_active_camera_name = f"その他 ({cam.name})"
view_loc = cam.matrix_world.translation
view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
center_loc = view_loc + view_dir * dist
fov = cam.data.angle
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
else:
props.info_active_camera_name = "なし (ビューポート)"
center_loc = rv3d.view_location
view_rot = rv3d.view_rotation
view_dir = view_rot @ mathutils.Vector((0, 0, -1))
view_loc = center_loc - view_dir * dist
space = None
for area in context.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
break
lens = space.lens if space and hasattr(space, "lens") else 50.0
fov = 2.0 * math.atan(36.0 / (2.0 * lens))
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
props.info_center_loc = center_loc
props.info_view_loc = view_loc
props.info_view_dir = view_dir
props.info_view_width = width
props.info_view_height = height
props.info_target_path = "ターゲット軌道: なし"
if props.cam_target_mode == 'LINE':
s = props.target_start_loc
e = props.target_end_loc
props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
elif props.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
normal = rot_mat @ mathutils.Vector((0, 0, 1))
c = props.target_circle_center
d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
if props.cam_move_mode == 'LINE':
cs = props.cam_start_loc
ce = props.cam_end_loc
props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
elif props.cam_move_mode == 'CIRCLE':
rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
cc = props.cam_circle_center
dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"
self.report({'INFO'}, "画面情報を更新しました")
return {'FINISHED'}
class OBJECT_OT_copy_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_camera_info"
bl_label = "情報をコピー"
bl_description = "更新された画面情報をクリップボードにコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
info = (
f"投影モード: {props.info_proj_mode}\n"
f"表示カメラ: {props.info_active_camera_name}\n"
f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
f"画面水平の大きさ: {props.info_view_width:.1f}\n"
f"画面上下の大きさ: {props.info_view_height:.1f}\n"
f"視座からの距離: {props.info_view_dist:.1f}\n"
f"{props.info_target_path}"
)
context.window_manager.clipboard = info
self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBJECT_OT_reset_cam_offset_rot(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_reset_cam_offset_rot"
bl_label = "角度ズレをリセット (0, 0, 0)"
bl_description = "カメラの角度ズレ(オフセット)をすべて0にします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.cam_local_offset_rot = (0.0, 0.0, 0.0)
return {'FINISHED'}
class OBJECT_OT_reset_cam_root_rot(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_reset_cam_root_rot"
bl_label = "初期回転をリセット"
bl_description = "Rootの初期回転を元のデフォルト状態に戻します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.cam_rot_start = (CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START)
return {'FINISHED'}
class OBJECT_OT_export_addon(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_export_addon"
bl_label = "現在の設定でアドオンを書き出す"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
source = get_addon_source()
if not source:
self.report({'ERROR'}, "ソースコードを取得できませんでした")
return {'CANCELLED'}
lines = source.split('\n')
for i, line in enumerate(lines):
# モードや表示の選択状態
if line.startswith("CAM_MOVE_MODE_DEFAULT = "): lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{props.cam_move_mode}'"
elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{props.cam_target_mode}'"
elif line.startswith("CAMERA_PRESET_DEFAULT = "): lines[i] = f"CAMERA_PRESET_DEFAULT = '{props.camera_preset}'"
elif line.startswith("SHOW_CAM_OBJ_DEFAULT = "): lines[i] = f"SHOW_CAM_OBJ_DEFAULT = {props.show_cam_obj}"
elif line.startswith("SHOW_TARGET_EMPTY_DEFAULT = "): lines[i] = f"SHOW_TARGET_EMPTY_DEFAULT = {props.show_target_empty}"
# 各種設定値
elif line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"
elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"
elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"
elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"
new_text = '\n'.join(lines)
import time
t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
text_data = bpy.data.texts.new(name=t_name)
text_data.write(new_text)
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
break
self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
return {'FINISHED'}
# ===================================
# 9. パネル構成
# ===================================
class VIEW3D_PT_main(bpy.types.Panel):
bl_label = "Viewport & Grid"
bl_idname = f"{PREFIX.upper()}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')
class VIEW3D_PT_camera(bpy.types.Panel):
bl_label = "Fixed Camera Rig"
bl_idname = f"{PREFIX.upper()}_PT_camera"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
box = layout.box()
box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
box.prop(props, "cam_move_mode", expand=True)
col_loc = box.column(align=True)
if props.cam_move_mode == 'FIXED':
col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
elif props.cam_move_mode == 'LINE':
col_loc.label(text="出発点:")
col_loc.prop(props, "cam_start_loc", index=0, text="X")
col_loc.prop(props, "cam_start_loc", index=1, text="Y")
col_loc.prop(props, "cam_start_loc", index=2, text="Z")
col_loc.separator()
col_loc.label(text="到達点:")
col_loc.prop(props, "cam_end_loc", index=0, text="X")
col_loc.prop(props, "cam_end_loc", index=1, text="Y")
col_loc.prop(props, "cam_end_loc", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
elif props.cam_move_mode == 'CIRCLE':
col_loc.label(text="中心位置:")
col_loc.prop(props, "cam_circle_center", index=0, text="X")
col_loc.prop(props, "cam_circle_center", index=1, text="Y")
col_loc.prop(props, "cam_circle_center", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_circle_radius")
col_loc.separator()
col_loc.label(text="平面の傾き (XYZ):")
col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Yaw)")
col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Roll)")
col_loc.separator()
col_loc.prop(props, "cam_circle_start_angle")
col_loc.prop(props, "cam_circle_end_angle")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
box.separator()
row_cam = box.row(align=True)
row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
root = bpy.data.objects.get(OBJ_NAME_ROOT)
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if root and cam:
box_tgt = layout.box()
box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
box_tgt.prop(props, "cam_target_mode", expand=False)
if props.cam_target_mode == 'POINT':
col_tgt = box_tgt.column(align=True)
col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
elif props.cam_target_mode == 'LINE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="出発点:")
col_tgt.prop(props, "target_start_loc", index=0, text="X")
col_tgt.prop(props, "target_start_loc", index=1, text="Y")
col_tgt.prop(props, "target_start_loc", index=2, text="Z")
col_tgt.separator()
col_tgt.label(text="到達点:")
col_tgt.prop(props, "target_end_loc", index=0, text="X")
col_tgt.prop(props, "target_end_loc", index=1, text="Y")
col_tgt.prop(props, "target_end_loc", index=2, text="Z")
elif props.cam_target_mode == 'CIRCLE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="中心位置:")
col_tgt.prop(props, "target_circle_center", index=0, text="X")
col_tgt.prop(props, "target_circle_center", index=1, text="Y")
col_tgt.prop(props, "target_circle_center", index=2, text="Z")
col_tgt.separator()
col_tgt.prop(props, "target_circle_radius")
col_tgt.separator()
col_tgt.label(text="平面の傾き (XYZ):")
col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Yaw)")
col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Roll)")
col_tgt.separator()
col_tgt.prop(props, "target_circle_start_angle")
col_tgt.prop(props, "target_circle_end_angle")
elif props.cam_target_mode == 'OBJECT':
box_tgt.prop(props, "cam_target_obj")
if props.cam_target_mode in ['LINE', 'CIRCLE']:
col_tgt = box_tgt.column(align=True)
col_tgt.separator()
col_tgt.prop(props, "target_anim_speed_mult", slider=True)
box_offset = layout.box()
box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
col_off = box_offset.column(align=True)
col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Yaw)")
col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Roll)")
col_off.operator(f"object.{PREFIX}_reset_cam_offset_rot", icon='FILE_REFRESH')
box_rot = layout.box()
box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
if props.cam_target_mode != 'NONE':
box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
if props.cam_target_mode == 'NONE':
col_rot = box_rot.column(align=True)
col_rot.label(text="Rootの速度ベース回転:")
col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Yaw)")
col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Roll)")
col_rot.operator(f"object.{PREFIX}_reset_cam_root_rot", icon='FILE_REFRESH')
col_rot.separator()
col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
row_hint = box_rot.row()
row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
col_hint = box_rot.column(align=True)
col_hint.scale_y = 0.8
col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
col_hint.label(text=f"{LABEL_V}+: 左向 / {LABEL_V}-: 右向")
col_hint.label(text=f"{LABEL_W}+: 左傾 / {LABEL_W}-: 右傾")
box_rot.separator()
col_f = box_rot.column(align=True)
row_f = col_f.row(align=True)
row_f.prop(props, "anim_start_frame")
row_f.prop(props, "anim_end_frame")
row_sync = col_f.row(align=True)
row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
box_rot.separator()
col_btn = box_rot.column(align=True)
col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
if not context.screen.is_animation_playing:
col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
else:
col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
box_rot.separator()
# --- 画面情報表示と更新・コピー機能 ---
box_info = box_rot.box()
row_info_head = box_info.row(align=True)
row_info_head.label(text="画面情報", icon='INFO')
row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
box_info.label(text=f"投影モード: {props.info_proj_mode}")
box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
box_info.separator()
for line in props.info_target_path.split('\n'):
box_info.label(text=line, icon='EDITMODE_HLT')
box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')
# --- 4. カメラ色・表示 ---
box_vis = layout.box()
box_vis.label(text="4. カメラ色 (共通テーマ) & 表示", icon='RESTRICT_VIEW_OFF')
row_disp = box_vis.row(align=True)
row_disp.prop(props, "show_cam_obj", text="カメラ")
row_disp.prop(props, "show_target_empty", text="Empty")
col_vis = box_vis.column(align=True)
row_cam_col = col_vis.row(align=True)
row_cam_col.prop(props, "camera_preset", text="")
row_cam_col.prop(props, "camera_color", text="")
# --- 5. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="5. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
box_pupil.prop(props, "pupil_window_show", text="表示する")
if props.pupil_window_show:
col_p = box_pupil.column(align=True)
col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
if props.pupil_window_segments == 4:
col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
else:
col_p.prop(props, "pupil_window_radius", text="半径")
col_p.prop(props, "pupil_window_thickness")
col_p.prop(props, "pupil_window_color")
col_p.separator()
col_p.label(text="ローカル回転 (XYZ):")
col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
col_p.prop(props, "pupil_window_rot", index=1, text="Y (Yaw)")
col_p.prop(props, "pupil_window_rot", index=2, text="Z (Roll)")
# --- 6. レンズ・カメラ設定 ---
box_lens = layout.box()
box_lens.label(text="6. レンズ・カメラ設定", 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:
if cam.data.dof.focus_object:
col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
# --- 7. 設定値の保存 ---
box_exp = layout.box()
box_exp.label(text="7. 設定値の保存 (自己書き出し)", icon='TEXT')
box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')
class VIEW3D_PT_links(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"{PREFIX.upper()}_PT_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class VIEW3D_PT_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = f"{PREFIX.upper()}_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 10. register()
# ===================================
classes = [
CP_OT_open_url,
CP_OT_remove_addon,
ADDON_Properties,
VIEW3D_OT_reset_view,
OBJECT_OT_create_cam_rig,
OBJECT_OT_sync_from_scene,
OBJECT_OT_sync_to_scene,
OBJECT_OT_apply_cam_anim,
OBJECT_OT_set_active_camera,
OBJECT_OT_update_camera_info,
OBJECT_OT_copy_camera_info,
OBJECT_OT_reset_cam_offset_rot,
OBJECT_OT_reset_cam_root_rot,
OBJECT_OT_export_addon,
VIEW3D_PT_main,
VIEW3D_PT_camera,
VIEW3D_PT_links,
VIEW3D_PT_remove
]
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
def apply_initial_settings():
if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
props = getattr(bpy.context.scene, f"{PREFIX}_props")
try:
bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
except Exception:
pass
if bpy.context.window_manager:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
bpy.app.timers.register(apply_initial_settings, first_interval=0.1)
# ===================================
# 11. unregister()
# ===================================
def unregister():
for c in reversed(classes):
try:
bpy.utils.unregister_class(c)
except:
pass
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
try: unregister()
except: pass
register()
bl_info = {
"name": "Fixed Camera & Automation Master (単一カメラ版)",
"author": "Your Name",
"version": (4, 27),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き)",
"category": "3D View",
}
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed "
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()
# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = ""
LINK_URL_4 = ""
LINK_NAME_5 = ""
LINK_URL_5 = ""
# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
MAT_NAME_PUPIL = "Pupil_Window_Mat"
CAMERA_PRESETS = [
("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
# --- 選択・表示状態の初期値 ---
CAM_MOVE_MODE_DEFAULT = 'FIXED'
CAM_TARGET_MODE_DEFAULT = 'NONE'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = True
CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)
CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000
# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0
CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000
# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0
PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854
ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500
CUSTOM_GRID_SCALE_DEFAULT = 50.0
CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000
CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000
CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000
LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"
# ===================================
# 7. 共通クラス
# ===================================
class CP_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 CP_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# 8. アドオン固有機能
# ===================================
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_addon_source():
source = ""
try:
if '__file__' in globals() and os.path.exists(__file__):
with open(__file__, 'r', encoding='utf-8') as f:
source = f.read()
except Exception: pass
if not source:
for t in bpy.data.texts:
if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
source = t.as_string()
break
return source
def cam_fov_get(self):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
def update_visibility(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
cam.hide_viewport = not self.show_cam_obj
for name in [OBJ_NAME_ROOT, OBJ_NAME_TARGET]:
obj = bpy.data.objects.get(name)
if obj:
obj.hide_viewport = not self.show_target_empty
def update_target_anim(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not tgt: return
if tgt.animation_data:
tgt.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
speed = self.target_anim_speed_mult
if self.cam_target_mode == 'POINT':
tgt.location = self.cam_target_loc
elif self.cam_target_mode == 'LINE':
s_loc = mathutils.Vector(self.target_start_loc)
e_loc = mathutils.Vector(self.target_end_loc)
if delta_f <= 0:
tgt.location = s_loc
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
tgt.location = s_loc.lerp(e_loc, ratio)
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
elif self.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.target_circle_center)
radius = self.target_circle_radius
start_rad = math.radians(self.target_circle_start_angle)
end_rad = math.radians(self.target_circle_end_angle)
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_anim(self, context):
update_target_anim(self, context)
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if not root: return
if root.animation_data:
root.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
# 1. カメラ(Root)の位置ベイク
if self.cam_move_mode == 'FIXED':
root.location = self.cam_fixed_loc
root.keyframe_insert(data_path="location", frame=start_f)
elif self.cam_move_mode == 'LINE':
s_loc = mathutils.Vector(self.cam_start_loc)
e_loc = mathutils.Vector(self.cam_end_loc)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
root.location = s_loc
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
root.location = s_loc.lerp(e_loc, ratio)
root.keyframe_insert(data_path="location", frame=f)
elif self.cam_move_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.cam_circle_center)
radius = self.cam_circle_radius
start_rad = math.radians(self.cam_circle_start_angle)
end_rad = math.radians(self.cam_circle_end_angle)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=f)
# 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
if self.cam_target_mode == 'NONE':
root.rotation_mode = 'XYZ'
if delta_f <= 0:
root.rotation_euler = self.cam_rot_start
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
else:
speed_u = math.radians(self.cam_rot_speed[0])
speed_v = math.radians(self.cam_rot_speed[1])
speed_w = math.radians(self.cam_rot_speed[2])
start_rot = self.cam_rot_start
end_rot = (
start_rot[0] + speed_u * delta_f,
start_rot[1] + speed_v * delta_f,
start_rot[2] + speed_w * delta_f
)
root.rotation_euler = start_rot
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
root.rotation_euler = end_rot
root.keyframe_insert(data_path="rotation_euler", frame=end_f)
if root.animation_data and root.animation_data.action:
act = root.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_constraint(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
if not root: return
tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
if self.cam_target_mode == 'NONE':
if tt: root.constraints.remove(tt)
update_cam_anim(self, context)
else:
if not tt: tt = root.constraints.new(type='TRACK_TO')
tt.track_axis = 'TRACK_NEGATIVE_Z'
tt.up_axis = 'UP_Y'
if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
if tgt_obj:
tt.target = tgt_obj
update_target_anim(self, context)
elif self.cam_target_mode == 'OBJECT':
tt.target = self.cam_target_obj
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_loc(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if root: root.location = self.cam_fixed_loc
def update_cam_local_offset_rot(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam: cam.rotation_euler = self.cam_local_offset_rot
def update_camera_color(self, context):
try:
bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
except Exception:
pass
def update_camera_preset_func(self, context):
for p in CAMERA_PRESETS:
if p[0] == self.camera_preset:
self.camera_color = p[3]
break
def update_pupil_window(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if not obj: return
obj.hide_viewport = not self.pupil_window_show
obj.hide_render = not self.pupil_window_show
if not self.pupil_window_show:
return
obj.location = (0.0, 0.0, -self.pupil_window_dist)
mesh = obj.data
mesh.clear_geometry()
maj_s = self.pupil_window_segments
min_s = 16
val = self.pupil_window_radius
if maj_s == 4:
R = val / math.sqrt(2.0)
else:
R = val
r = self.pupil_window_thickness
verts = []
faces = []
for i in range(maj_s):
u = i * 2.0 * math.pi / maj_s
for j in range(min_s):
v = j * 2.0 * math.pi / min_s
x = (R + r * math.cos(v)) * math.cos(u)
y = (R + r * math.cos(v)) * math.sin(u)
z = r * math.sin(v)
verts.append((x, y, z))
for i in range(maj_s):
for j in range(min_s):
next_i = (i + 1) % maj_s
next_j = (j + 1) % min_s
v0 = i * min_s + j
v1 = i * min_s + next_j
v2 = next_i * min_s + next_j
v3 = next_i * min_s + j
faces.append((v0, v1, v2, v3))
mesh.from_pydata(verts, [], faces)
mesh.update()
def update_pupil_segments(self, context):
if self.pupil_window_segments == 4:
self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
else:
self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
update_pupil_window(self, context)
def update_pupil_window_rot(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj:
obj.rotation_euler = self.pupil_window_rot
def update_pupil_window_mat(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj and obj.data.materials:
mat = obj.data.materials[0]
mat.diffuse_color = self.pupil_window_color
if mat.use_nodes:
bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = self.pupil_window_color
# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
# カメラの移動設定
cam_move_mode: bpy.props.EnumProperty(
name="カメラ(Root)の移動",
items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
default=CAM_MOVE_MODE_DEFAULT, update=update_cam_anim
)
cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)
cam_target_mode: bpy.props.EnumProperty(
name="注視点設定",
items=[
('NONE', "オフ (速度ベース回転)", ""),
('POINT', "指定座標 (固定)", ""),
('LINE', "線分移動 (アニメ)", ""),
('CIRCLE', "円周移動 (アニメ)", ""),
('OBJECT', "別オブジェクト", "")
],
default=CAM_TARGET_MODE_DEFAULT, update=update_cam_root_constraint
)
cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)
target_anim_speed_mult: bpy.props.FloatProperty(name="注視点移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)
cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=SHOW_CAM_OBJ_DEFAULT, update=update_visibility)
show_target_empty: bpy.props.BoolProperty(name="Empty", default=SHOW_TARGET_EMPTY_DEFAULT, update=update_visibility)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
camera_preset: bpy.props.EnumProperty(
name="カメラ色プリセット",
items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS],
default=CAMERA_PRESET_DEFAULT,
update=update_camera_preset_func
)
camera_color: bpy.props.FloatVectorProperty(
name="カメラテーマ色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=CAMERA_COLOR_DEFAULT,
update=update_camera_color
)
pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)
info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")
# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_reset_view"
bl_label = "選択物を中央にする"
def execute(self, context):
rv3d = get_rv3d(context)
if rv3d and context.active_object:
rv3d.view_location = context.active_object.location
return {'FINISHED'}
class OBJECT_OT_create_cam_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_cam_rig"
bl_label = "カメラリグ作成"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_PUPIL]:
o = bpy.data.objects.get(n)
if o: bpy.data.objects.remove(o, do_unlink=True)
main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
if sub_col.name not in main_col.children: main_col.children.link(sub_col)
tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, None)
tgt_obj.empty_display_type = 'PLAIN_AXES'
sub_col.objects.link(tgt_obj)
root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
root_obj.empty_display_type = 'PLAIN_AXES'
root_obj.location = props.cam_fixed_loc
root_obj.rotation_mode = 'XYZ'
root_obj.rotation_euler = props.cam_rot_start
sub_col.objects.link(root_obj)
cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
cam_obj.parent = root_obj
cam_obj.location = (0, 0, 0)
cam_obj.rotation_euler = props.cam_local_offset_rot
sub_col.objects.link(cam_obj)
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = False
cam_obj.data.dof.focus_object = tgt_obj
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
o_pupil.parent = cam_obj
o_pupil.rotation_mode = 'XYZ'
sub_col.objects.link(o_pupil)
mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
mat_pupil.use_nodes = True
mat_pupil.blend_method = 'BLEND'
o_pupil.data.materials.append(mat_pupil)
update_pupil_window(props, context)
update_pupil_window_rot(props, context)
update_pupil_window_mat(props, context)
update_cam_root_constraint(props, context)
update_visibility(props, context)
update_cam_anim(props, context)
update_camera_color(props, context)
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
space.region_3d.view_perspective = 'CAMERA'
space.lock_camera = False
self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
return {'FINISHED'}
class OBJECT_OT_sync_from_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_from_scene"
bl_label = "シーンから取得"
bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.anim_start_frame = context.scene.frame_start
props.anim_end_frame = context.scene.frame_end
self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_sync_to_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_to_scene"
bl_label = "シーンへ適用"
bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
context.scene.frame_start = props.anim_start_frame
context.scene.frame_end = props.anim_end_frame
self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_apply_cam_anim"
bl_label = "アニメーションを適用(更新)"
bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
update_cam_anim(props, context)
self.report({'INFO'}, "アニメーションキーフレームを更新しました")
return {'FINISHED'}
class OBJECT_OT_set_active_camera(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_set_active_camera"
bl_label = "専用カメラ視点にする"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, "専用カメラ視点に切り替えました")
else:
self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
return {'FINISHED'}
class OBJECT_OT_update_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_update_camera_info"
bl_label = "情報更新"
bl_description = "現在のビューから画面の情報を取得・更新します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
rv3d = get_rv3d(context)
if not rv3d:
self.report({'WARNING'}, "3Dビューが見つかりません")
return {'CANCELLED'}
if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
else: props.info_proj_mode = rv3d.view_perspective
dist = rv3d.view_distance
props.info_view_dist = dist
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = (res_x / res_y) if res_y != 0 else 1.0
if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
cam = context.scene.camera
if cam.name == OBJ_NAME_CAMERA:
props.info_active_camera_name = "メインカメラ"
else:
props.info_active_camera_name = f"その他 ({cam.name})"
view_loc = cam.matrix_world.translation
view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
center_loc = view_loc + view_dir * dist
fov = cam.data.angle
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
else:
props.info_active_camera_name = "なし (ビューポート)"
center_loc = rv3d.view_location
view_rot = rv3d.view_rotation
view_dir = view_rot @ mathutils.Vector((0, 0, -1))
view_loc = center_loc - view_dir * dist
space = None
for area in context.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
break
lens = space.lens if space and hasattr(space, "lens") else 50.0
fov = 2.0 * math.atan(36.0 / (2.0 * lens))
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
props.info_center_loc = center_loc
props.info_view_loc = view_loc
props.info_view_dir = view_dir
props.info_view_width = width
props.info_view_height = height
props.info_target_path = "ターゲット軌道: なし"
if props.cam_target_mode == 'LINE':
s = props.target_start_loc
e = props.target_end_loc
props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
elif props.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
normal = rot_mat @ mathutils.Vector((0, 0, 1))
c = props.target_circle_center
d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
if props.cam_move_mode == 'LINE':
cs = props.cam_start_loc
ce = props.cam_end_loc
props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
elif props.cam_move_mode == 'CIRCLE':
rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
cc = props.cam_circle_center
dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"
self.report({'INFO'}, "画面情報を更新しました")
return {'FINISHED'}
class OBJECT_OT_copy_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_camera_info"
bl_label = "情報をコピー"
bl_description = "更新された画面情報をクリップボードにコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
info = (
f"投影モード: {props.info_proj_mode}\n"
f"表示カメラ: {props.info_active_camera_name}\n"
f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
f"画面水平の大きさ: {props.info_view_width:.1f}\n"
f"画面上下の大きさ: {props.info_view_height:.1f}\n"
f"視座からの距離: {props.info_view_dist:.1f}\n"
f"{props.info_target_path}"
)
context.window_manager.clipboard = info
self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBJECT_OT_reset_cam_offset_rot(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_reset_cam_offset_rot"
bl_label = "角度ズレをリセット (0, 0, 0)"
bl_description = "カメラの角度ズレ(オフセット)をすべて0にします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.cam_local_offset_rot = (0.0, 0.0, 0.0)
return {'FINISHED'}
class OBJECT_OT_reset_cam_root_rot(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_reset_cam_root_rot"
bl_label = "初期回転をリセット"
bl_description = "Rootの初期回転を元のデフォルト状態に戻します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.cam_rot_start = (CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START)
return {'FINISHED'}
class OBJECT_OT_export_addon(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_export_addon"
bl_label = "現在の設定でアドオンを書き出す"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
source = get_addon_source()
if not source:
self.report({'ERROR'}, "ソースコードを取得できませんでした")
return {'CANCELLED'}
lines = source.split('\n')
for i, line in enumerate(lines):
# モードや表示の選択状態
if line.startswith("CAM_MOVE_MODE_DEFAULT = "): lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{props.cam_move_mode}'"
elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{props.cam_target_mode}'"
elif line.startswith("CAMERA_PRESET_DEFAULT = "): lines[i] = f"CAMERA_PRESET_DEFAULT = '{props.camera_preset}'"
elif line.startswith("SHOW_CAM_OBJ_DEFAULT = "): lines[i] = f"SHOW_CAM_OBJ_DEFAULT = {props.show_cam_obj}"
elif line.startswith("SHOW_TARGET_EMPTY_DEFAULT = "): lines[i] = f"SHOW_TARGET_EMPTY_DEFAULT = {props.show_target_empty}"
# 各種設定値
elif line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"
elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"
elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"
elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"
new_text = '\n'.join(lines)
import time
t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
text_data = bpy.data.texts.new(name=t_name)
text_data.write(new_text)
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
break
self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
return {'FINISHED'}
# ===================================
# 9. パネル構成
# ===================================
class VIEW3D_PT_main(bpy.types.Panel):
bl_label = "Viewport & Grid"
bl_idname = f"{PREFIX.upper()}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')
class VIEW3D_PT_camera(bpy.types.Panel):
bl_label = "Fixed Camera Rig"
bl_idname = f"{PREFIX.upper()}_PT_camera"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
box = layout.box()
box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
box.prop(props, "cam_move_mode", expand=True)
col_loc = box.column(align=True)
if props.cam_move_mode == 'FIXED':
col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
elif props.cam_move_mode == 'LINE':
col_loc.label(text="出発点:")
col_loc.prop(props, "cam_start_loc", index=0, text="X")
col_loc.prop(props, "cam_start_loc", index=1, text="Y")
col_loc.prop(props, "cam_start_loc", index=2, text="Z")
col_loc.separator()
col_loc.label(text="到達点:")
col_loc.prop(props, "cam_end_loc", index=0, text="X")
col_loc.prop(props, "cam_end_loc", index=1, text="Y")
col_loc.prop(props, "cam_end_loc", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
elif props.cam_move_mode == 'CIRCLE':
col_loc.label(text="中心位置:")
col_loc.prop(props, "cam_circle_center", index=0, text="X")
col_loc.prop(props, "cam_circle_center", index=1, text="Y")
col_loc.prop(props, "cam_circle_center", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_circle_radius")
col_loc.separator()
col_loc.label(text="平面の傾き (XYZ):")
col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Yaw)")
col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Roll)")
col_loc.separator()
col_loc.prop(props, "cam_circle_start_angle")
col_loc.prop(props, "cam_circle_end_angle")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
box.separator()
row_cam = box.row(align=True)
row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
root = bpy.data.objects.get(OBJ_NAME_ROOT)
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if root and cam:
box_tgt = layout.box()
box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
box_tgt.prop(props, "cam_target_mode", expand=False)
if props.cam_target_mode == 'POINT':
col_tgt = box_tgt.column(align=True)
col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
elif props.cam_target_mode == 'LINE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="出発点:")
col_tgt.prop(props, "target_start_loc", index=0, text="X")
col_tgt.prop(props, "target_start_loc", index=1, text="Y")
col_tgt.prop(props, "target_start_loc", index=2, text="Z")
col_tgt.separator()
col_tgt.label(text="到達点:")
col_tgt.prop(props, "target_end_loc", index=0, text="X")
col_tgt.prop(props, "target_end_loc", index=1, text="Y")
col_tgt.prop(props, "target_end_loc", index=2, text="Z")
elif props.cam_target_mode == 'CIRCLE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="中心位置:")
col_tgt.prop(props, "target_circle_center", index=0, text="X")
col_tgt.prop(props, "target_circle_center", index=1, text="Y")
col_tgt.prop(props, "target_circle_center", index=2, text="Z")
col_tgt.separator()
col_tgt.prop(props, "target_circle_radius")
col_tgt.separator()
col_tgt.label(text="平面の傾き (XYZ):")
col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Yaw)")
col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Roll)")
col_tgt.separator()
col_tgt.prop(props, "target_circle_start_angle")
col_tgt.prop(props, "target_circle_end_angle")
elif props.cam_target_mode == 'OBJECT':
box_tgt.prop(props, "cam_target_obj")
if props.cam_target_mode in ['LINE', 'CIRCLE']:
col_tgt = box_tgt.column(align=True)
col_tgt.separator()
col_tgt.prop(props, "target_anim_speed_mult", slider=True)
box_offset = layout.box()
box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
col_off = box_offset.column(align=True)
col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Yaw)")
col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Roll)")
col_off.operator(f"object.{PREFIX}_reset_cam_offset_rot", icon='FILE_REFRESH')
box_rot = layout.box()
box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
if props.cam_target_mode != 'NONE':
box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
if props.cam_target_mode == 'NONE':
col_rot = box_rot.column(align=True)
col_rot.label(text="Rootの速度ベース回転:")
col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Yaw)")
col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Roll)")
col_rot.operator(f"object.{PREFIX}_reset_cam_root_rot", icon='FILE_REFRESH')
col_rot.separator()
col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
row_hint = box_rot.row()
row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
col_hint = box_rot.column(align=True)
col_hint.scale_y = 0.8
col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
col_hint.label(text=f"{LABEL_V}+: 左向 / {LABEL_V}-: 右向")
col_hint.label(text=f"{LABEL_W}+: 左傾 / {LABEL_W}-: 右傾")
box_rot.separator()
col_f = box_rot.column(align=True)
row_f = col_f.row(align=True)
row_f.prop(props, "anim_start_frame")
row_f.prop(props, "anim_end_frame")
row_sync = col_f.row(align=True)
row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
box_rot.separator()
col_btn = box_rot.column(align=True)
col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
if not context.screen.is_animation_playing:
col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
else:
col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
box_rot.separator()
# --- 画面情報表示と更新・コピー機能 ---
box_info = box_rot.box()
row_info_head = box_info.row(align=True)
row_info_head.label(text="画面情報", icon='INFO')
row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
box_info.label(text=f"投影モード: {props.info_proj_mode}")
box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
box_info.separator()
for line in props.info_target_path.split('\n'):
box_info.label(text=line, icon='EDITMODE_HLT')
box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')
# --- 4. カメラ色・表示 ---
box_vis = layout.box()
box_vis.label(text="4. カメラ色 (共通テーマ) & 表示", icon='RESTRICT_VIEW_OFF')
row_disp = box_vis.row(align=True)
row_disp.prop(props, "show_cam_obj", text="カメラ")
row_disp.prop(props, "show_target_empty", text="Empty")
col_vis = box_vis.column(align=True)
row_cam_col = col_vis.row(align=True)
row_cam_col.prop(props, "camera_preset", text="")
row_cam_col.prop(props, "camera_color", text="")
# --- 5. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="5. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
box_pupil.prop(props, "pupil_window_show", text="表示する")
if props.pupil_window_show:
col_p = box_pupil.column(align=True)
col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
if props.pupil_window_segments == 4:
col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
else:
col_p.prop(props, "pupil_window_radius", text="半径")
col_p.prop(props, "pupil_window_thickness")
col_p.prop(props, "pupil_window_color")
col_p.separator()
col_p.label(text="ローカル回転 (XYZ):")
col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
col_p.prop(props, "pupil_window_rot", index=1, text="Y (Yaw)")
col_p.prop(props, "pupil_window_rot", index=2, text="Z (Roll)")
# --- 6. レンズ・カメラ設定 ---
box_lens = layout.box()
box_lens.label(text="6. レンズ・カメラ設定", 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:
if cam.data.dof.focus_object:
col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
# --- 7. 設定値の保存 ---
box_exp = layout.box()
box_exp.label(text="7. 設定値の保存 (自己書き出し)", icon='TEXT')
box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')
class VIEW3D_PT_links(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"{PREFIX.upper()}_PT_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class VIEW3D_PT_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = f"{PREFIX.upper()}_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 10. register()
# ===================================
classes = [
CP_OT_open_url,
CP_OT_remove_addon,
ADDON_Properties,
VIEW3D_OT_reset_view,
OBJECT_OT_create_cam_rig,
OBJECT_OT_sync_from_scene,
OBJECT_OT_sync_to_scene,
OBJECT_OT_apply_cam_anim,
OBJECT_OT_set_active_camera,
OBJECT_OT_update_camera_info,
OBJECT_OT_copy_camera_info,
OBJECT_OT_reset_cam_offset_rot,
OBJECT_OT_reset_cam_root_rot,
OBJECT_OT_export_addon,
VIEW3D_PT_main,
VIEW3D_PT_camera,
VIEW3D_PT_links,
VIEW3D_PT_remove
]
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
def apply_initial_settings():
if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
props = getattr(bpy.context.scene, f"{PREFIX}_props")
try:
bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
except Exception:
pass
if bpy.context.window_manager:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
bpy.app.timers.register(apply_initial_settings, first_interval=0.1)
# ===================================
# 11. unregister()
# ===================================
def unregister():
for c in reversed(classes):
try:
bpy.utils.unregister_class(c)
except:
pass
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
try: unregister()
except: pass
register()
bl_info = {
"name": "Fixed Camera & Automation Master (単一カメラ版)",
"author": "Your Name",
"version": (4, 24),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き)",
"category": "3D View",
}
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed & View Master"
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()
# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = ""
LINK_URL_4 = ""
LINK_NAME_5 = ""
LINK_URL_5 = ""
# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
MAT_NAME_PUPIL = "Pupil_Window_Mat"
MAT_NAME_TARGET = "FixedCam_Target_Mat"
CAMERA_PRESETS = [
("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
# --- 選択・表示状態の初期値 ---
CAM_MOVE_MODE_DEFAULT = 'FIXED'
CAM_TARGET_MODE_DEFAULT = 'NONE'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = True
CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)
CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000
# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0
CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000
# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0
TARGET_SIZE_DEFAULT = 1.0
TARGET_COLOR_DEFAULT = (0.0000, 0.8000, 0.2000, 1.0)
TARGET_RANDOM_COLOR_DEFAULT = False
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0
PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854
ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500
CUSTOM_GRID_SCALE_DEFAULT = 50.0
CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000
CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000
CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000
LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"
# ===================================
# 7. 共通クラス
# ===================================
class CP_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 CP_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ===================================
# 8. アドオン固有機能
# ===================================
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_addon_source():
source = ""
try:
if '__file__' in globals() and os.path.exists(__file__):
with open(__file__, 'r', encoding='utf-8') as f:
source = f.read()
except Exception: pass
if not source:
for t in bpy.data.texts:
if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
source = t.as_string()
break
return source
def cam_fov_get(self):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
cam.data.angle = math.radians(value)
def update_visibility(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
cam.hide_viewport = not self.show_cam_obj
for name in [OBJ_NAME_ROOT]:
obj = bpy.data.objects.get(name)
if obj:
obj.hide_viewport = not self.show_target_empty
def update_target_visuals(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if tgt:
tgt.scale = (self.target_size, self.target_size, self.target_size)
mesh = tgt.data
if len(mesh.materials) < 6:
mesh.materials.clear()
for i in range(6):
mat = bpy.data.materials.new(name=f"{MAT_NAME_TARGET}_{i}")
mat.use_nodes = True
mesh.materials.append(mat)
colors = [
(1.0, 0.0, 0.0, 1.0), # 赤
(0.0, 1.0, 0.0, 1.0), # 緑
(0.0, 0.5, 1.0, 1.0), # 青
(1.0, 1.0, 0.0, 1.0), # 黄
(0.0, 1.0, 1.0, 1.0), # シアン
(1.0, 0.0, 1.0, 1.0), # マゼンタ
]
for i in range(6):
mat = mesh.materials[i]
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf and 'Base Color' in bsdf.inputs:
if self.target_random_color:
bsdf.inputs['Base Color'].default_value = colors[i]
else:
bsdf.inputs['Base Color'].default_value = self.target_color
for i, poly in enumerate(mesh.polygons):
poly.material_index = i % 6 if self.target_random_color else 0
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_target_anim(self, context):
tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
if not tgt: return
if tgt.animation_data:
tgt.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
speed = self.target_anim_speed_mult
if self.cam_target_mode == 'POINT':
tgt.location = self.cam_target_loc
elif self.cam_target_mode == 'LINE':
s_loc = mathutils.Vector(self.target_start_loc)
e_loc = mathutils.Vector(self.target_end_loc)
if delta_f <= 0:
tgt.location = s_loc
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
tgt.location = s_loc.lerp(e_loc, ratio)
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
elif self.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.target_circle_center)
radius = self.target_circle_radius
start_rad = math.radians(self.target_circle_start_angle)
end_rad = math.radians(self.target_circle_end_angle)
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
tgt.location = center + rot_mat @ local_pos
tgt.keyframe_insert(data_path="location", frame=f)
if tgt.animation_data and tgt.animation_data.action:
act = tgt.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
if fc.data_path == 'location':
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_anim(self, context):
update_target_anim(self, context)
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if not root: return
if root.animation_data:
root.animation_data.action = None
start_f = self.anim_start_frame
end_f = self.anim_end_frame
delta_f = end_f - start_f
# 1. カメラ(Root)の位置ベイク
if self.cam_move_mode == 'FIXED':
root.location = self.cam_fixed_loc
root.keyframe_insert(data_path="location", frame=start_f)
elif self.cam_move_mode == 'LINE':
s_loc = mathutils.Vector(self.cam_start_loc)
e_loc = mathutils.Vector(self.cam_end_loc)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
root.location = s_loc
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
pp = progress % 2.0
if pp < 0: pp += 2.0
ratio = pp if pp <= 1.0 else 2.0 - pp
root.location = s_loc.lerp(e_loc, ratio)
root.keyframe_insert(data_path="location", frame=f)
elif self.cam_move_mode == 'CIRCLE':
rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
center = mathutils.Vector(self.cam_circle_center)
radius = self.cam_circle_radius
start_rad = math.radians(self.cam_circle_start_angle)
end_rad = math.radians(self.cam_circle_end_angle)
speed = self.cam_anim_speed_mult
if delta_f <= 0:
local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=start_f)
else:
for f in range(start_f, end_f + 1):
progress = ((f - start_f) / delta_f) * speed
t = start_rad + (end_rad - start_rad) * progress
local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
root.location = center + rot_mat @ local_pos
root.keyframe_insert(data_path="location", frame=f)
# 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
if self.cam_target_mode == 'NONE':
root.rotation_mode = 'XYZ'
if delta_f <= 0:
root.rotation_euler = self.cam_rot_start
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
else:
speed_u = math.radians(self.cam_rot_speed[0])
speed_v = math.radians(self.cam_rot_speed[1])
speed_w = math.radians(self.cam_rot_speed[2])
start_rot = self.cam_rot_start
end_rot = (
start_rot[0] + speed_u * delta_f,
start_rot[1] + speed_v * delta_f,
start_rot[2] + speed_w * delta_f
)
root.rotation_euler = start_rot
root.keyframe_insert(data_path="rotation_euler", frame=start_f)
root.rotation_euler = end_rot
root.keyframe_insert(data_path="rotation_euler", frame=end_f)
if root.animation_data and root.animation_data.action:
act = root.animation_data.action
fcs = getattr(act, "fcurves", None)
if fcs:
for fc in fcs:
for kp in fc.keyframe_points:
kp.interpolation = 'LINEAR'
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_constraint(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
if not root: return
tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
if self.cam_target_mode == 'NONE':
if tt: root.constraints.remove(tt)
update_cam_anim(self, context)
else:
if not tt: tt = root.constraints.new(type='TRACK_TO')
tt.track_axis = 'TRACK_NEGATIVE_Z'
tt.up_axis = 'UP_Y'
if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
if tgt_obj:
tt.target = tgt_obj
update_target_anim(self, context)
elif self.cam_target_mode == 'OBJECT':
tt.target = self.cam_target_obj
if bpy.context.scene:
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
def update_cam_root_loc(self, context):
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if root: root.location = self.cam_fixed_loc
def update_cam_local_offset_rot(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam: cam.rotation_euler = self.cam_local_offset_rot
def update_camera_color(self, context):
try:
bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
except Exception:
pass
def update_camera_preset_func(self, context):
for p in CAMERA_PRESETS:
if p[0] == self.camera_preset:
self.camera_color = p[3]
break
def update_pupil_window(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if not obj: return
obj.hide_viewport = not self.pupil_window_show
obj.hide_render = not self.pupil_window_show
if not self.pupil_window_show:
return
obj.location = (0.0, 0.0, -self.pupil_window_dist)
mesh = obj.data
mesh.clear_geometry()
maj_s = self.pupil_window_segments
min_s = 16
val = self.pupil_window_radius
if maj_s == 4:
R = val / math.sqrt(2.0)
else:
R = val
r = self.pupil_window_thickness
verts = []
faces = []
for i in range(maj_s):
u = i * 2.0 * math.pi / maj_s
for j in range(min_s):
v = j * 2.0 * math.pi / min_s
x = (R + r * math.cos(v)) * math.cos(u)
y = (R + r * math.cos(v)) * math.sin(u)
z = r * math.sin(v)
verts.append((x, y, z))
for i in range(maj_s):
for j in range(min_s):
next_i = (i + 1) % maj_s
next_j = (j + 1) % min_s
v0 = i * min_s + j
v1 = i * min_s + next_j
v2 = next_i * min_s + next_j
v3 = next_i * min_s + j
faces.append((v0, v1, v2, v3))
mesh.from_pydata(verts, [], faces)
mesh.update()
def update_pupil_segments(self, context):
if self.pupil_window_segments == 4:
self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
else:
self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
update_pupil_window(self, context)
def update_pupil_window_rot(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj:
obj.rotation_euler = self.pupil_window_rot
def update_pupil_window_mat(self, context):
obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
if obj and obj.data.materials:
mat = obj.data.materials[0]
mat.diffuse_color = self.pupil_window_color
if mat.use_nodes:
bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = self.pupil_window_color
# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
# カメラの移動設定
cam_move_mode: bpy.props.EnumProperty(
name="カメラ(Root)の移動",
items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
default=CAM_MOVE_MODE_DEFAULT, update=update_cam_anim
)
cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)
cam_target_mode: bpy.props.EnumProperty(
name="注視点設定",
items=[
('NONE', "オフ (速度ベース回転)", ""),
('POINT', "指定座標 (固定)", ""),
('LINE', "線分移動 (アニメ)", ""),
('CIRCLE', "円周移動 (アニメ)", ""),
('OBJECT', "別オブジェクト", "")
],
default=CAM_TARGET_MODE_DEFAULT, update=update_cam_root_constraint
)
cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)
target_size: bpy.props.FloatProperty(name="立方体のサイズ", default=TARGET_SIZE_DEFAULT, min=0.01, update=update_target_visuals)
target_color: bpy.props.FloatVectorProperty(name="立方体の色", subtype='COLOR', size=4, default=TARGET_COLOR_DEFAULT, min=0.0, max=1.0, update=update_target_visuals)
target_random_color: bpy.props.BoolProperty(name="6面をランダムな色にする", default=TARGET_RANDOM_COLOR_DEFAULT, update=update_target_visuals)
target_anim_speed_mult: bpy.props.FloatProperty(name="立方体移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)
cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=SHOW_CAM_OBJ_DEFAULT, update=update_visibility)
show_target_empty: bpy.props.BoolProperty(name="Empty", default=SHOW_TARGET_EMPTY_DEFAULT, update=update_visibility)
cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
camera_preset: bpy.props.EnumProperty(
name="カメラ色プリセット",
items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS],
default=CAMERA_PRESET_DEFAULT,
update=update_camera_preset_func
)
camera_color: bpy.props.FloatVectorProperty(
name="カメラテーマ色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=CAMERA_COLOR_DEFAULT,
update=update_camera_color
)
pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)
info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")
# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_reset_view"
bl_label = "選択物を中央にする"
def execute(self, context):
rv3d = get_rv3d(context)
if rv3d and context.active_object:
rv3d.view_location = context.active_object.location
return {'FINISHED'}
class OBJECT_OT_create_cam_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_cam_rig"
bl_label = "カメラリグ作成"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_PUPIL]:
o = bpy.data.objects.get(n)
if o: bpy.data.objects.remove(o, do_unlink=True)
main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
if sub_col.name not in main_col.children: main_col.children.link(sub_col)
m_tgt = bpy.data.meshes.new(f"{OBJ_NAME_TARGET}_Mesh")
bm_t = bmesh.new()
bmesh.ops.create_cube(bm_t, size=1.0)
bm_t.to_mesh(m_tgt)
bm_t.free()
tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, m_tgt)
sub_col.objects.link(tgt_obj)
update_target_visuals(props, context)
root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
root_obj.empty_display_type = 'PLAIN_AXES'
root_obj.location = props.cam_fixed_loc
root_obj.rotation_mode = 'XYZ'
root_obj.rotation_euler = props.cam_rot_start
sub_col.objects.link(root_obj)
cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
cam_obj.parent = root_obj
cam_obj.location = (0, 0, 0)
cam_obj.rotation_euler = props.cam_local_offset_rot
sub_col.objects.link(cam_obj)
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = tgt_obj
cam_obj.data.dof.aperture_fstop = 1.8
cam_obj.data.show_passepartout = True
cam_obj.data.passepartout_alpha = 0.8
m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
o_pupil.parent = cam_obj
o_pupil.rotation_mode = 'XYZ'
sub_col.objects.link(o_pupil)
mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
mat_pupil.use_nodes = True
mat_pupil.blend_method = 'BLEND'
o_pupil.data.materials.append(mat_pupil)
update_pupil_window(props, context)
update_pupil_window_rot(props, context)
update_pupil_window_mat(props, context)
update_cam_root_constraint(props, context)
update_visibility(props, context)
update_cam_anim(props, context)
update_camera_color(props, context)
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
space.region_3d.view_perspective = 'CAMERA'
space.lock_camera = False
self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
return {'FINISHED'}
class OBJECT_OT_sync_from_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_from_scene"
bl_label = "シーンから取得"
bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.anim_start_frame = context.scene.frame_start
props.anim_end_frame = context.scene.frame_end
self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_sync_to_scene(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_sync_to_scene"
bl_label = "シーンへ適用"
bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
context.scene.frame_start = props.anim_start_frame
context.scene.frame_end = props.anim_end_frame
self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
return {'FINISHED'}
class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_apply_cam_anim"
bl_label = "アニメーションを適用(更新)"
bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
update_cam_anim(props, context)
self.report({'INFO'}, "アニメーションキーフレームを更新しました")
return {'FINISHED'}
class OBJECT_OT_set_active_camera(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_set_active_camera"
bl_label = "専用カメラ視点にする"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if cam:
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, "専用カメラ視点に切り替えました")
else:
self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
return {'FINISHED'}
class OBJECT_OT_update_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_update_camera_info"
bl_label = "情報更新"
bl_description = "現在のビューから画面の情報を取得・更新します"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
rv3d = get_rv3d(context)
if not rv3d:
self.report({'WARNING'}, "3Dビューが見つかりません")
return {'CANCELLED'}
if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
else: props.info_proj_mode = rv3d.view_perspective
dist = rv3d.view_distance
props.info_view_dist = dist
res_x = context.scene.render.resolution_x
res_y = context.scene.render.resolution_y
aspect = (res_x / res_y) if res_y != 0 else 1.0
if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
cam = context.scene.camera
if cam.name == OBJ_NAME_CAMERA:
props.info_active_camera_name = "メインカメラ"
else:
props.info_active_camera_name = f"その他 ({cam.name})"
view_loc = cam.matrix_world.translation
view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
center_loc = view_loc + view_dir * dist
fov = cam.data.angle
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
else:
props.info_active_camera_name = "なし (ビューポート)"
center_loc = rv3d.view_location
view_rot = rv3d.view_rotation
view_dir = view_rot @ mathutils.Vector((0, 0, -1))
view_loc = center_loc - view_dir * dist
space = None
for area in context.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
break
lens = space.lens if space and hasattr(space, "lens") else 50.0
fov = 2.0 * math.atan(36.0 / (2.0 * lens))
width = 2.0 * dist * math.tan(fov / 2.0)
height = width / aspect
props.info_center_loc = center_loc
props.info_view_loc = view_loc
props.info_view_dir = view_dir
props.info_view_width = width
props.info_view_height = height
props.info_target_path = "ターゲット軌道: なし"
if props.cam_target_mode == 'LINE':
s = props.target_start_loc
e = props.target_end_loc
props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
elif props.cam_target_mode == 'CIRCLE':
rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
normal = rot_mat @ mathutils.Vector((0, 0, 1))
c = props.target_circle_center
d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
if props.cam_move_mode == 'LINE':
cs = props.cam_start_loc
ce = props.cam_end_loc
props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
elif props.cam_move_mode == 'CIRCLE':
rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
cc = props.cam_circle_center
dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"
self.report({'INFO'}, "画面情報を更新しました")
return {'FINISHED'}
class OBJECT_OT_copy_camera_info(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_camera_info"
bl_label = "情報をコピー"
bl_description = "更新された画面情報をクリップボードにコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
info = (
f"投影モード: {props.info_proj_mode}\n"
f"表示カメラ: {props.info_active_camera_name}\n"
f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
f"画面水平の大きさ: {props.info_view_width:.1f}\n"
f"画面上下の大きさ: {props.info_view_height:.1f}\n"
f"視座からの距離: {props.info_view_dist:.1f}\n"
f"{props.info_target_path}"
)
context.window_manager.clipboard = info
self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBJECT_OT_export_addon(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_export_addon"
bl_label = "現在の設定でアドオンを書き出す"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
source = get_addon_source()
if not source:
self.report({'ERROR'}, "ソースコードを取得できませんでした")
return {'CANCELLED'}
lines = source.split('\n')
for i, line in enumerate(lines):
# モードや表示の選択状態
if line.startswith("CAM_MOVE_MODE_DEFAULT = "): lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{props.cam_move_mode}'"
elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{props.cam_target_mode}'"
elif line.startswith("CAMERA_PRESET_DEFAULT = "): lines[i] = f"CAMERA_PRESET_DEFAULT = '{props.camera_preset}'"
elif line.startswith("SHOW_CAM_OBJ_DEFAULT = "): lines[i] = f"SHOW_CAM_OBJ_DEFAULT = {props.show_cam_obj}"
elif line.startswith("SHOW_TARGET_EMPTY_DEFAULT = "): lines[i] = f"SHOW_TARGET_EMPTY_DEFAULT = {props.show_target_empty}"
# 各種設定値
elif line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"
elif line.startswith("TARGET_SIZE_DEFAULT = "): lines[i] = f"TARGET_SIZE_DEFAULT = {props.target_size:.4f}"
elif line.startswith("TARGET_COLOR_DEFAULT = "): lines[i] = f"TARGET_COLOR_DEFAULT = ({props.target_color[0]:.4f}, {props.target_color[1]:.4f}, {props.target_color[2]:.4f}, 1.0)"
elif line.startswith("TARGET_RANDOM_COLOR_DEFAULT = "): lines[i] = f"TARGET_RANDOM_COLOR_DEFAULT = {props.target_random_color}"
elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"
elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"
elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"
new_text = '\n'.join(lines)
import time
t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
text_data = bpy.data.texts.new(name=t_name)
text_data.write(new_text)
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
break
self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
return {'FINISHED'}
# ===================================
# 9. パネル構成
# ===================================
class VIEW3D_PT_main(bpy.types.Panel):
bl_label = "Viewport & Grid"
bl_idname = f"{PREFIX.upper()}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')
class VIEW3D_PT_camera(bpy.types.Panel):
bl_label = "Fixed Camera Rig"
bl_idname = f"{PREFIX.upper()}_PT_camera"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
box = layout.box()
box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
box.prop(props, "cam_move_mode", expand=True)
col_loc = box.column(align=True)
if props.cam_move_mode == 'FIXED':
col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
elif props.cam_move_mode == 'LINE':
col_loc.label(text="出発点:")
col_loc.prop(props, "cam_start_loc", index=0, text="X")
col_loc.prop(props, "cam_start_loc", index=1, text="Y")
col_loc.prop(props, "cam_start_loc", index=2, text="Z")
col_loc.separator()
col_loc.label(text="到達点:")
col_loc.prop(props, "cam_end_loc", index=0, text="X")
col_loc.prop(props, "cam_end_loc", index=1, text="Y")
col_loc.prop(props, "cam_end_loc", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
elif props.cam_move_mode == 'CIRCLE':
col_loc.label(text="中心位置:")
col_loc.prop(props, "cam_circle_center", index=0, text="X")
col_loc.prop(props, "cam_circle_center", index=1, text="Y")
col_loc.prop(props, "cam_circle_center", index=2, text="Z")
col_loc.separator()
col_loc.prop(props, "cam_circle_radius")
col_loc.separator()
col_loc.label(text="平面の傾き (XYZ):")
col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Roll)")
col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Yaw)")
col_loc.separator()
col_loc.prop(props, "cam_circle_start_angle")
col_loc.prop(props, "cam_circle_end_angle")
col_loc.separator()
col_loc.prop(props, "cam_anim_speed_mult", slider=True)
box.separator()
row_cam = box.row(align=True)
row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
root = bpy.data.objects.get(OBJ_NAME_ROOT)
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if root and cam:
box_tgt = layout.box()
box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
box_tgt.prop(props, "cam_target_mode", expand=False)
if props.cam_target_mode == 'POINT':
col_tgt = box_tgt.column(align=True)
col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
elif props.cam_target_mode == 'LINE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="出発点:")
col_tgt.prop(props, "target_start_loc", index=0, text="X")
col_tgt.prop(props, "target_start_loc", index=1, text="Y")
col_tgt.prop(props, "target_start_loc", index=2, text="Z")
col_tgt.separator()
col_tgt.label(text="到達点:")
col_tgt.prop(props, "target_end_loc", index=0, text="X")
col_tgt.prop(props, "target_end_loc", index=1, text="Y")
col_tgt.prop(props, "target_end_loc", index=2, text="Z")
elif props.cam_target_mode == 'CIRCLE':
col_tgt = box_tgt.column(align=True)
col_tgt.label(text="中心位置:")
col_tgt.prop(props, "target_circle_center", index=0, text="X")
col_tgt.prop(props, "target_circle_center", index=1, text="Y")
col_tgt.prop(props, "target_circle_center", index=2, text="Z")
col_tgt.separator()
col_tgt.prop(props, "target_circle_radius")
col_tgt.separator()
col_tgt.label(text="平面の傾き (XYZ):")
col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Roll)")
col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Yaw)")
col_tgt.separator()
col_tgt.prop(props, "target_circle_start_angle")
col_tgt.prop(props, "target_circle_end_angle")
elif props.cam_target_mode == 'OBJECT':
box_tgt.prop(props, "cam_target_obj")
if props.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
col_tgt = box_tgt.column(align=True)
col_tgt.separator()
col_tgt.prop(props, "target_size")
col_tgt.prop(props, "target_random_color")
if not props.target_random_color:
col_tgt.prop(props, "target_color")
if props.cam_target_mode in ['LINE', 'CIRCLE']:
col_tgt = box_tgt.column(align=True)
col_tgt.separator()
col_tgt.prop(props, "target_anim_speed_mult", slider=True)
box_offset = layout.box()
box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
col_off = box_offset.column(align=True)
col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Roll)")
col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Yaw)")
box_rot = layout.box()
box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
if props.cam_target_mode != 'NONE':
box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
if props.cam_target_mode == 'NONE':
col_rot = box_rot.column(align=True)
col_rot.label(text="Rootの速度ベース回転:")
col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Roll)")
col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Yaw)")
col_rot.separator()
col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
row_hint = box_rot.row()
row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
col_hint = box_rot.column(align=True)
col_hint.scale_y = 0.8
col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
col_hint.label(text=f"{LABEL_V}+: 左傾 / {LABEL_V}-: 右傾")
col_hint.label(text=f"{LABEL_W}+: 左向 / {LABEL_W}-: 右向")
box_rot.separator()
col_f = box_rot.column(align=True)
row_f = col_f.row(align=True)
row_f.prop(props, "anim_start_frame")
row_f.prop(props, "anim_end_frame")
row_sync = col_f.row(align=True)
row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
box_rot.separator()
col_btn = box_rot.column(align=True)
col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
if not context.screen.is_animation_playing:
col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
else:
col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
box_rot.separator()
# --- 画面情報表示と更新・コピー機能 ---
box_info = box_rot.box()
row_info_head = box_info.row(align=True)
row_info_head.label(text="画面情報", icon='INFO')
row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
box_info.label(text=f"投影モード: {props.info_proj_mode}")
box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
box_info.separator()
for line in props.info_target_path.split('\n'):
box_info.label(text=line, icon='EDITMODE_HLT')
box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')
# --- 4. カメラ色・表示 ---
box_vis = layout.box()
box_vis.label(text="4. カメラ色 (共通テーマ) & 表示", icon='RESTRICT_VIEW_OFF')
row_disp = box_vis.row(align=True)
row_disp.prop(props, "show_cam_obj", text="カメラ")
row_disp.prop(props, "show_target_empty", text="Empty")
col_vis = box_vis.column(align=True)
row_cam_col = col_vis.row(align=True)
row_cam_col.prop(props, "camera_preset", text="")
row_cam_col.prop(props, "camera_color", text="")
# --- 5. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="5. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
box_pupil.prop(props, "pupil_window_show", text="表示する")
if props.pupil_window_show:
col_p = box_pupil.column(align=True)
col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
if props.pupil_window_segments == 4:
col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
else:
col_p.prop(props, "pupil_window_radius", text="半径")
col_p.prop(props, "pupil_window_thickness")
col_p.prop(props, "pupil_window_color")
col_p.separator()
col_p.label(text="ローカル回転 (XYZ):")
col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
col_p.prop(props, "pupil_window_rot", index=1, text="Y (Roll)")
col_p.prop(props, "pupil_window_rot", index=2, text="Z (Yaw)")
# --- 6. レンズ・カメラ設定 ---
box_lens = layout.box()
box_lens.label(text="6. レンズ・カメラ設定", 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:
if cam.data.dof.focus_object:
col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
# --- 7. 設定値の保存 ---
box_exp = layout.box()
box_exp.label(text="7. 設定値の保存 (自己書き出し)", icon='TEXT')
box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')
class VIEW3D_PT_links(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"{PREFIX.upper()}_PT_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class VIEW3D_PT_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = f"{PREFIX.upper()}_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 10. register()
# ===================================
classes = [
CP_OT_open_url,
CP_OT_remove_addon,
ADDON_Properties,
VIEW3D_OT_reset_view,
OBJECT_OT_create_cam_rig,
OBJECT_OT_sync_from_scene,
OBJECT_OT_sync_to_scene,
OBJECT_OT_apply_cam_anim,
OBJECT_OT_set_active_camera,
OBJECT_OT_update_camera_info,
OBJECT_OT_copy_camera_info,
OBJECT_OT_export_addon,
VIEW3D_PT_main,
VIEW3D_PT_camera,
VIEW3D_PT_links,
VIEW3D_PT_remove
]
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
def apply_initial_settings():
if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
props = getattr(bpy.context.scene, f"{PREFIX}_props")
try:
bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
except Exception:
pass
if bpy.context.window_manager:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
bpy.app.timers.register(apply_initial_settings, first_interval=0.1)
# ===================================
# 11. unregister()
# ===================================
def unregister():
for c in reversed(classes):
try:
bpy.utils.unregister_class(c)
except:
pass
if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
try: unregister()
except: pass
register()