Cam 固定 blender アドオン
bl_info = {
"name": "Fixed Camera & Automation Master",
"author": "Your Name",
"version": (4, 23),
"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_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"
BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"
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'
ACTIVE_CAMERA_DEFAULT = 'MAIN'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = True
SHOW_CAM_SHIELD_DEFAULT = True
SHOW_BACK_CAM_SHIELD_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
CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
CAM_BACK_DIST_DEFAULT = 10.0000
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
ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000
COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)
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, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
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)
# Fcurveの線形補間適用
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_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if not cam: return
shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.cam_shield_radius,) * 3
shield.hide_viewport = not self.show_cam_shield
z_scale = self.cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
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.cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
def update_back_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if not cam: return
shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.back_cam_shield_radius,) * 3
shield.hide_viewport = not self.show_back_cam_shield
z_scale = self.back_cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
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.back_cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha
def update_cam_back_dist(self, context):
cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if cam_back:
cam_back.location.z = self.cam_back_dist
def update_active_camera(self, context):
cam = None
if self.active_camera == 'MAIN':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
elif self.active_camera == 'BACK':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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'
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)
cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
active_camera: bpy.props.EnumProperty(
name="表示カメラ切り替え",
items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
default=ACTIVE_CAMERA_DEFAULT, update=update_active_camera
)
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
)
show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=SHOW_CAM_SHIELD_DEFAULT, update=update_cam_shield)
cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=SHOW_BACK_CAM_SHIELD_DEFAULT, update=update_back_cam_shield)
back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
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_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
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
cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
cam_back_data.show_passepartout = True
cam_back_data.passepartout_alpha = 0.8
cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
cam_back_obj.parent = cam_obj
cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
cam_back_obj.rotation_mode = 'XYZ'
cam_back_obj.rotation_euler = (0.0, 0.0, 0.0)
sub_col.objects.link(cam_back_obj)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
m = bpy.data.meshes.new(SHIELD_NAME)
bm.to_mesh(m)
bm.free()
s_obj = bpy.data.objects.new(SHIELD_NAME, m)
s_obj.parent = cam_obj
sub_col.objects.link(s_obj)
bm_c = bmesh.new()
bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_c.verts: v.co.z -= 0.5
m_c = bpy.data.meshes.new(CUTTER_NAME)
bm_c.to_mesh(m_c)
bm_c.free()
c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
c_obj.parent = cam_obj
c_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj)
bm_b = bmesh.new()
bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_b.verts: v.co.z += 0.5
m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
bm_b.to_mesh(m_b)
bm_b.free()
cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
cb_obj.parent = cam_obj
cb_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj)
mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front.object = c_obj
mod_front.solver = 'FLOAT'
mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back.object = cb_obj
mod_back.solver = 'FLOAT'
mat = bpy.data.materials.new(MAT_NAME_SHIELD)
mat.use_nodes = True
mat.blend_method = 'BLEND'
s_obj.data.materials.append(mat)
s_obj.show_transparent = True
bm_bs = bmesh.new()
bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
bm_bs.to_mesh(m_bs)
bm_bs.free()
s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
s_obj_b.parent = cam_back_obj
sub_col.objects.link(s_obj_b)
bm_bc = bmesh.new()
bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bc.verts: v.co.z -= 0.5
m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
bm_bc.to_mesh(m_bc)
bm_bc.free()
c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
c_obj_b.parent = cam_back_obj
c_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj_b)
bm_bb = bmesh.new()
bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bb.verts: v.co.z += 0.5
m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
bm_bb.to_mesh(m_bb)
bm_bb.free()
cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
cb_obj_b.parent = cam_back_obj
cb_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj_b)
mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front_b.object = c_obj_b
mod_front_b.solver = 'FLOAT'
mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back_b.object = cb_obj_b
mod_back_b.solver = 'FLOAT'
mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
mat_b.use_nodes = True
mat_b.blend_method = 'BLEND'
s_obj_b.data.materials.append(mat_b)
s_obj_b.show_transparent = True
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_shield(props, context)
update_back_cam_shield(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_switch_cam_main(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_main"
bl_label = "包み内 カメラ視点"
bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
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_switch_cam_back(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_back"
bl_label = "背後 観察視点"
bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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_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 = "メインカメラ"
elif cam.name == OBJ_NAME_CAMERA_BACK:
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("ACTIVE_CAMERA_DEFAULT = "): lines[i] = f"ACTIVE_CAMERA_DEFAULT = '{props.active_camera}'"
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("SHOW_CAM_SHIELD_DEFAULT = "): lines[i] = f"SHOW_CAM_SHIELD_DEFAULT = {props.show_cam_shield}"
elif line.startswith("SHOW_BACK_CAM_SHIELD_DEFAULT = "): lines[i] = f"SHOW_BACK_CAM_SHIELD_DEFAULT = {props.show_back_cam_shield}"
# 各種設定値
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("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"
elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"
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()
# --- 視点切り替えボタン & 背後カメラの距離 ---
col_switch = box_rot.column(align=True)
row_switch = col_switch.row(align=True)
row_switch.prop(props, "active_camera", expand=True)
col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
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")
row_disp.prop(props, "show_cam_shield", text="包み表示")
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="")
col_vis.separator()
if props.show_cam_shield:
col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
col_vis.prop(props, "cam_shield_radius")
col_vis.prop(props, "cam_shield_hole_ratio_front")
col_vis.prop(props, "cam_shield_hole_ratio_back")
col_vis.separator()
col_vis.prop(props, "cam_shield_color")
col_vis.prop(props, "cam_shield_alpha", slider=True)
# --- 5. 背後霊カメラ 包み設定 ---
box_back = layout.box()
box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
row_back_disp = box_back.row(align=True)
row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
col_back = box_back.column(align=True)
if props.show_back_cam_shield:
col_back.separator()
col_back.prop(props, "back_cam_shield_radius")
col_back.prop(props, "back_cam_shield_hole_ratio_front")
col_back.prop(props, "back_cam_shield_hole_ratio_back")
col_back.separator()
col_back.prop(props, "back_cam_shield_color")
col_back.prop(props, "back_cam_shield_alpha", slider=True)
# --- 6. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", 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)")
# --- 7. レンズ・カメラ設定 (統合) ---
box_lens = layout.box()
box_lens.label(text="7. レンズ・カメラ設定", 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値 (小さいとボケる)")
box_exp = layout.box()
box_exp.label(text="8. 設定値の保存 (自己書き出し)", 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_switch_cam_main,
OBJECT_OT_switch_cam_back,
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()
記憶
bl_info = {
"name": "Fixed Camera & Automation Master",
"author": "Your Name",
"version": (4, 22),
"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_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"
BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"
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)),
]
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
CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
CAM_BACK_DIST_DEFAULT = 10.0000
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
ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000
COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)
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, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
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)
# Fcurveの線形補間適用
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_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if not cam: return
shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.cam_shield_radius,) * 3
shield.hide_viewport = not self.show_cam_shield
z_scale = self.cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
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.cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
def update_back_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if not cam: return
shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.back_cam_shield_radius,) * 3
shield.hide_viewport = not self.show_back_cam_shield
z_scale = self.back_cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
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.back_cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha
def update_cam_back_dist(self, context):
cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if cam_back:
cam_back.location.z = self.cam_back_dist
def update_active_camera(self, context):
cam = None
if self.active_camera == 'MAIN':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
elif self.active_camera == 'BACK':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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'
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='FIXED', 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='NONE', 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=True, update=update_visibility)
show_target_empty: bpy.props.BoolProperty(name="Empty", default=True, 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)
cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
active_camera: bpy.props.EnumProperty(
name="表示カメラ切り替え",
items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
default='MAIN', update=update_active_camera
)
camera_preset: bpy.props.EnumProperty(
name="カメラ色プリセット",
items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS],
default='Cam',
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
)
show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=True, update=update_cam_shield)
cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=True, update=update_back_cam_shield)
back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
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_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
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
cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
cam_back_data.show_passepartout = True
cam_back_data.passepartout_alpha = 0.8
cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
cam_back_obj.parent = cam_obj
cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
cam_back_obj.rotation_mode = 'XYZ'
cam_back_obj.rotation_euler = (0.0, 0.0, 0.0)
sub_col.objects.link(cam_back_obj)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
m = bpy.data.meshes.new(SHIELD_NAME)
bm.to_mesh(m)
bm.free()
s_obj = bpy.data.objects.new(SHIELD_NAME, m)
s_obj.parent = cam_obj
sub_col.objects.link(s_obj)
bm_c = bmesh.new()
bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_c.verts: v.co.z -= 0.5
m_c = bpy.data.meshes.new(CUTTER_NAME)
bm_c.to_mesh(m_c)
bm_c.free()
c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
c_obj.parent = cam_obj
c_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj)
bm_b = bmesh.new()
bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_b.verts: v.co.z += 0.5
m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
bm_b.to_mesh(m_b)
bm_b.free()
cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
cb_obj.parent = cam_obj
cb_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj)
mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front.object = c_obj
mod_front.solver = 'FLOAT'
mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back.object = cb_obj
mod_back.solver = 'FLOAT'
mat = bpy.data.materials.new(MAT_NAME_SHIELD)
mat.use_nodes = True
mat.blend_method = 'BLEND'
s_obj.data.materials.append(mat)
s_obj.show_transparent = True
bm_bs = bmesh.new()
bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
bm_bs.to_mesh(m_bs)
bm_bs.free()
s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
s_obj_b.parent = cam_back_obj
sub_col.objects.link(s_obj_b)
bm_bc = bmesh.new()
bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bc.verts: v.co.z -= 0.5
m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
bm_bc.to_mesh(m_bc)
bm_bc.free()
c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
c_obj_b.parent = cam_back_obj
c_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj_b)
bm_bb = bmesh.new()
bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bb.verts: v.co.z += 0.5
m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
bm_bb.to_mesh(m_bb)
bm_bb.free()
cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
cb_obj_b.parent = cam_back_obj
cb_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj_b)
mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front_b.object = c_obj_b
mod_front_b.solver = 'FLOAT'
mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back_b.object = cb_obj_b
mod_back_b.solver = 'FLOAT'
mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
mat_b.use_nodes = True
mat_b.blend_method = 'BLEND'
s_obj_b.data.materials.append(mat_b)
s_obj_b.show_transparent = True
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_shield(props, context)
update_back_cam_shield(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_switch_cam_main(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_main"
bl_label = "包み内 カメラ視点"
bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
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_switch_cam_back(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_back"
bl_label = "背後 観察視点"
bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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_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 = "メインカメラ"
elif cam.name == OBJ_NAME_CAMERA_BACK:
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_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("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"
elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"
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()
# --- 視点切り替えボタン & 背後カメラの距離 ---
col_switch = box_rot.column(align=True)
row_switch = col_switch.row(align=True)
row_switch.prop(props, "active_camera", expand=True)
col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
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")
row_disp.prop(props, "show_cam_shield", text="包み表示")
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="")
col_vis.separator()
if props.show_cam_shield:
col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
col_vis.prop(props, "cam_shield_radius")
col_vis.prop(props, "cam_shield_hole_ratio_front")
col_vis.prop(props, "cam_shield_hole_ratio_back")
col_vis.separator()
col_vis.prop(props, "cam_shield_color")
col_vis.prop(props, "cam_shield_alpha", slider=True)
# --- 5. 背後霊カメラ 包み設定 ---
box_back = layout.box()
box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
row_back_disp = box_back.row(align=True)
row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
col_back = box_back.column(align=True)
if props.show_back_cam_shield:
col_back.separator()
col_back.prop(props, "back_cam_shield_radius")
col_back.prop(props, "back_cam_shield_hole_ratio_front")
col_back.prop(props, "back_cam_shield_hole_ratio_back")
col_back.separator()
col_back.prop(props, "back_cam_shield_color")
col_back.prop(props, "back_cam_shield_alpha", slider=True)
# --- 6. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", 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)")
# --- 7. レンズ・カメラ設定 (統合) ---
box_lens = layout.box()
box_lens.label(text="7. レンズ・カメラ設定", 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値 (小さいとボケる)")
box_exp = layout.box()
box_exp.label(text="8. 設定値の保存 (自己書き出し)", 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_switch_cam_main,
OBJECT_OT_switch_cam_back,
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()
bl_info = {
"name": "Fixed Camera & Automation Master",
"author": "Your Name",
"version": (4, 23),
"blender": (4, 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_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"
OBJ_NAME_CAM_PATH = "FixedCamera_CamPath"
OBJ_NAME_TGT_PATH = "FixedCamera_TargetPath"
SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"
BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"
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)),
]
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
# 軌跡用初期設定
SHOW_CAM_PATH_DEFAULT = True
CAM_PATH_THICKNESS_DEFAULT = 0.05
CAM_PATH_COLOR_DEFAULT = (1.0, 0.2, 0.2, 1.0)
SHOW_TARGET_PATH_DEFAULT = True
TARGET_PATH_THICKNESS_DEFAULT = 0.05
TARGET_PATH_COLOR_DEFAULT = (0.2, 0.8, 1.0, 1.0)
CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000
CAM_BACK_DIST_DEFAULT = 10.0000
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
ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000
COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)
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, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
obj = bpy.data.objects.get(name)
if obj:
obj.hide_viewport = not self.show_target_empty
def create_or_update_curve(obj_name, points, thickness, color, show):
col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM)
if not col: return
obj = bpy.data.objects.get(obj_name)
if not obj:
curve_data = bpy.data.curves.new(f"{obj_name}_Curve", type='CURVE')
curve_data.dimensions = '3D'
curve_data.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve_data)
col.objects.link(obj)
else:
curve_data = obj.data
obj.hide_viewport = not show
obj.hide_render = not show
curve_data.bevel_depth = thickness
if not show or not points:
curve_data.splines.clear()
return
mat_name = f"{obj_name}_Mat"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf and 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if len(curve_data.materials) == 0:
curve_data.materials.append(mat)
else:
curve_data.materials[0] = mat
spline = curve_data.splines[0] if curve_data.splines else curve_data.splines.new('POLY')
if len(spline.points) != len(points):
curve_data.splines.clear()
spline = curve_data.splines.new('POLY')
spline.points.add(len(points) - 1)
for i, p in enumerate(points):
spline.points[i].co = (p.x, p.y, p.z, 1.0)
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
pts = []
if self.cam_target_mode == 'POINT':
tgt.location = self.cam_target_loc
pts.append(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)
pts.append(s_loc)
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
loc = s_loc.lerp(e_loc, ratio)
tgt.location = loc
tgt.keyframe_insert(data_path="location", frame=f)
pts.append(loc)
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))
loc = center + rot_mat @ local_pos
tgt.location = loc
tgt.keyframe_insert(data_path="location", frame=start_f)
pts.append(loc)
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))
loc = center + rot_mat @ local_pos
tgt.location = loc
tgt.keyframe_insert(data_path="location", frame=f)
pts.append(loc)
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'
# ターゲット軌跡の描画
create_or_update_curve(OBJ_NAME_TGT_PATH, pts, self.target_path_thickness, self.target_path_color, self.show_target_path)
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
pts = []
# 1. カメラ(Root)の位置ベイク
if self.cam_move_mode == 'FIXED':
loc = mathutils.Vector(self.cam_fixed_loc)
root.location = loc
root.keyframe_insert(data_path="location", frame=start_f)
pts.append(loc)
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)
pts.append(s_loc)
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
loc = s_loc.lerp(e_loc, ratio)
root.location = loc
root.keyframe_insert(data_path="location", frame=f)
pts.append(loc)
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))
loc = center + rot_mat @ local_pos
root.location = loc
root.keyframe_insert(data_path="location", frame=start_f)
pts.append(loc)
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))
loc = center + rot_mat @ local_pos
root.location = loc
root.keyframe_insert(data_path="location", frame=f)
pts.append(loc)
# カメラ軌跡の描画
create_or_update_curve(OBJ_NAME_CAM_PATH, pts, self.cam_path_thickness, self.cam_path_color, self.show_cam_path)
# 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)
# Fcurveの線形補間適用
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_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
if not cam: return
shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.cam_shield_radius,) * 3
shield.hide_viewport = not self.show_cam_shield
z_scale = self.cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
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.cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
def update_back_cam_shield(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if not cam: return
shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
if shield:
shield.scale = (self.back_cam_shield_radius,) * 3
shield.hide_viewport = not self.show_back_cam_shield
z_scale = self.back_cam_shield_radius * 3.0
if cutter:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
cutter.scale = (r, r, z_scale)
cutter.display_type = 'WIRE'
cutter.hide_render = True
if cutter_back:
r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
cutter_back.scale = (r, r, z_scale)
cutter_back.display_type = 'WIRE'
cutter_back.hide_render = True
if shield and shield.data.materials:
mat = shield.data.materials[0]
mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
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.back_cam_shield_color[:3], 1.0)
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha
def update_cam_back_dist(self, context):
cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
if cam_back:
cam_back.location.z = self.cam_back_dist
def update_active_camera(self, context):
cam = None
if self.active_camera == 'MAIN':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
elif self.active_camera == 'BACK':
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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'
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='FIXED', 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)
show_cam_path: bpy.props.BoolProperty(name="軌跡を描画する", default=SHOW_CAM_PATH_DEFAULT, update=update_cam_anim)
cam_path_thickness: bpy.props.FloatProperty(name="太さ", default=CAM_PATH_THICKNESS_DEFAULT, min=0.001, update=update_cam_anim)
cam_path_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=CAM_PATH_COLOR_DEFAULT, update=update_cam_anim)
cam_target_mode: bpy.props.EnumProperty(
name="注視点設定",
items=[
('NONE', "オフ (速度ベース回転)", ""),
('POINT', "指定座標 (固定)", ""),
('LINE', "線分移動 (アニメ)", ""),
('CIRCLE', "円周移動 (アニメ)", ""),
('OBJECT', "別オブジェクト", "")
],
default='NONE', 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)
show_target_path: bpy.props.BoolProperty(name="軌跡を描画する", default=SHOW_TARGET_PATH_DEFAULT, update=update_target_anim)
target_path_thickness: bpy.props.FloatProperty(name="太さ", default=TARGET_PATH_THICKNESS_DEFAULT, min=0.001, update=update_target_anim)
target_path_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=TARGET_PATH_COLOR_DEFAULT, 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=True, update=update_visibility)
show_target_empty: bpy.props.BoolProperty(name="注視点/カッター表示", default=True, 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)
cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
active_camera: bpy.props.EnumProperty(
name="表示カメラ切り替え",
items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
default='MAIN', update=update_active_camera
)
camera_preset: bpy.props.EnumProperty(
name="カメラ色プリセット",
items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS],
default='Cam',
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
)
show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=True, update=update_cam_shield)
cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=True, update=update_back_cam_shield)
back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
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_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME, OBJ_NAME_CAM_PATH, OBJ_NAME_TGT_PATH]:
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
cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
cam_back_data.show_passepartout = True
cam_back_data.passepartout_alpha = 0.8
cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
cam_back_obj.parent = cam_obj
cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
cam_back_obj.rotation_mode = 'XYZ'
cam_back_obj.rotation_euler = (0.0, 0.0, 0.0)
sub_col.objects.link(cam_back_obj)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
m = bpy.data.meshes.new(SHIELD_NAME)
bm.to_mesh(m)
bm.free()
s_obj = bpy.data.objects.new(SHIELD_NAME, m)
s_obj.parent = cam_obj
sub_col.objects.link(s_obj)
bm_c = bmesh.new()
bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_c.verts: v.co.z -= 0.5
m_c = bpy.data.meshes.new(CUTTER_NAME)
bm_c.to_mesh(m_c)
bm_c.free()
c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
c_obj.parent = cam_obj
c_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj)
bm_b = bmesh.new()
bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_b.verts: v.co.z += 0.5
m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
bm_b.to_mesh(m_b)
bm_b.free()
cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
cb_obj.parent = cam_obj
cb_obj.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj)
mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front.object = c_obj
mod_front.solver = 'FLOAT'
mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back.object = cb_obj
mod_back.solver = 'FLOAT'
mat = bpy.data.materials.new(MAT_NAME_SHIELD)
mat.use_nodes = True
mat.blend_method = 'BLEND'
s_obj.data.materials.append(mat)
s_obj.show_transparent = True
bm_bs = bmesh.new()
bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
bm_bs.to_mesh(m_bs)
bm_bs.free()
s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
s_obj_b.parent = cam_back_obj
sub_col.objects.link(s_obj_b)
bm_bc = bmesh.new()
bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bc.verts: v.co.z -= 0.5
m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
bm_bc.to_mesh(m_bc)
bm_bc.free()
c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
c_obj_b.parent = cam_back_obj
c_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(c_obj_b)
bm_bb = bmesh.new()
bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm_bb.verts: v.co.z += 0.5
m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
bm_bb.to_mesh(m_bb)
bm_bb.free()
cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
cb_obj_b.parent = cam_back_obj
cb_obj_b.hide_viewport = not props.show_target_empty
sub_col.objects.link(cb_obj_b)
mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
mod_front_b.object = c_obj_b
mod_front_b.solver = 'FLOAT'
mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
mod_back_b.object = cb_obj_b
mod_back_b.solver = 'FLOAT'
mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
mat_b.use_nodes = True
mat_b.blend_method = 'BLEND'
s_obj_b.data.materials.append(mat_b)
s_obj_b.show_transparent = True
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_shield(props, context)
update_back_cam_shield(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_switch_cam_main(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_main"
bl_label = "包み内 カメラ視点"
bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
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_switch_cam_back(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_switch_cam_back"
bl_label = "背後 観察視点"
bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
def execute(self, context):
cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
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_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 = "メインカメラ"
elif cam.name == OBJ_NAME_CAMERA_BACK:
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_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("SHOW_CAM_PATH_DEFAULT = "): lines[i] = f"SHOW_CAM_PATH_DEFAULT = {props.show_cam_path}"
elif line.startswith("CAM_PATH_THICKNESS_DEFAULT = "): lines[i] = f"CAM_PATH_THICKNESS_DEFAULT = {props.cam_path_thickness:.4f}"
elif line.startswith("CAM_PATH_COLOR_DEFAULT = "): lines[i] = f"CAM_PATH_COLOR_DEFAULT = ({props.cam_path_color[0]:.4f}, {props.cam_path_color[1]:.4f}, {props.cam_path_color[2]:.4f}, 1.0)"
elif line.startswith("SHOW_TARGET_PATH_DEFAULT = "): lines[i] = f"SHOW_TARGET_PATH_DEFAULT = {props.show_target_path}"
elif line.startswith("TARGET_PATH_THICKNESS_DEFAULT = "): lines[i] = f"TARGET_PATH_THICKNESS_DEFAULT = {props.target_path_thickness:.4f}"
elif line.startswith("TARGET_PATH_COLOR_DEFAULT = "): lines[i] = f"TARGET_PATH_COLOR_DEFAULT = ({props.target_path_color[0]:.4f}, {props.target_path_color[1]:.4f}, {props.target_path_color[2]:.4f}, 1.0)"
elif line.startswith("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"
elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"
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)
if props.cam_move_mode in ['LINE', 'CIRCLE']:
col_loc.separator()
row_path = col_loc.row(align=True)
row_path.prop(props, "show_cam_path")
if props.show_cam_path:
col_loc.prop(props, "cam_path_thickness")
col_loc.prop(props, "cam_path_color")
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)
col_tgt.separator()
row_path_tgt = col_tgt.row(align=True)
row_path_tgt.prop(props, "show_target_path")
if props.show_target_path:
col_tgt.prop(props, "target_path_thickness")
col_tgt.prop(props, "target_path_color")
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()
# --- 視点切り替えボタン & 背後カメラの距離 ---
col_switch = box_rot.column(align=True)
row_switch = col_switch.row(align=True)
row_switch.prop(props, "active_camera", expand=True)
col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
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="注視点/カッター")
row_disp.prop(props, "show_cam_shield", text="包み表示")
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="")
col_vis.separator()
if props.show_cam_shield:
col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
col_vis.prop(props, "cam_shield_radius")
col_vis.prop(props, "cam_shield_hole_ratio_front")
col_vis.prop(props, "cam_shield_hole_ratio_back")
col_vis.separator()
col_vis.prop(props, "cam_shield_color")
col_vis.prop(props, "cam_shield_alpha", slider=True)
# --- 5. 背後霊カメラ 包み設定 ---
box_back = layout.box()
box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
row_back_disp = box_back.row(align=True)
row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
col_back = box_back.column(align=True)
if props.show_back_cam_shield:
col_back.separator()
col_back.prop(props, "back_cam_shield_radius")
col_back.prop(props, "back_cam_shield_hole_ratio_front")
col_back.prop(props, "back_cam_shield_hole_ratio_back")
col_back.separator()
col_back.prop(props, "back_cam_shield_color")
col_back.prop(props, "back_cam_shield_alpha", slider=True)
# --- 6. 瞳孔窓面 設定 ---
box_pupil = layout.box()
box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", 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)")
# --- 7. レンズ・カメラ設定 (統合) ---
box_lens = layout.box()
box_lens.label(text="7. レンズ・カメラ設定", 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値 (小さいとボケる)")
box_exp = layout.box()
box_exp.label(text="8. 設定値の保存 (自己書き出し)", 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_switch_cam_main,
OBJECT_OT_switch_cam_back,
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()
カメラと
立方体 それぞれの 初期値20秒間の移動軌跡を描く兄身
色 別々で 太さ調整あり
それぞれ 表示非表示ボタン 移動軌跡