blender Million 2026
カメラ3台 ジグザク 20260328
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================
PREFIX = "unit_circle_cam"
bl_info = {
"name": "zionad 521 [Unit Circle Cam]",
"author": "zionadchat",
"version": (37, 0, 11),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラ、\u53cd\u5c04円柱ジェネレータ、ビューポートカメラ制御(完全安定版)",
"category": "Cam zigzag",
}
# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
HDRI_PATHS =[
r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS =[("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS =[("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]
MASTER_COLLECTION_NAME = "Cam three"
CAMERA_COLLECTION_NAME = "Cam"
SENSOR_WIDTH = 36.0
FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================
NEW_DOC_LINKS = [
{"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
# --- パネル管理 ---
# ======================================================================
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup",
"REFLECTION": f"{PREFIX}_PT_reflection",
"AIMING": f"{PREFIX}_PT_aiming",
"VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
"LENS": f"{PREFIX}_PT_lens",
"CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display",
"WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"GRID": f"{PREFIX}_PT_grid_panel",
"WIRE": f"{PREFIX}_PT_wire_panel",
"LINKS": f"{PREFIX}_PT_links",
"REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
PANEL_IDS["SETUP"]: 0,
PANEL_IDS["REFLECTION"]: 1,
PANEL_IDS["AIMING"]: 2,
PANEL_IDS["VIEWPORT_CAM"]: 3,
PANEL_IDS["LENS"]: 4,
PANEL_IDS["CAMERA_DISPLAY"]: 5,
PANEL_IDS["WORLD_CONTROL"]: 6,
PANEL_IDS["GRID"]: 89,
PANEL_IDS["WIRE"]: 90,
PANEL_IDS["LINKS"]: 190,
PANEL_IDS["REMOVE"]: 200,
}
# ======================================================================
# --- ロック機構 & タイマー管理 ---
# ======================================================================
def set_update_lock(scene, state: bool):
if scene:
scene["_sfc_updating"] = state
def is_updating(scene):
if scene:
return scene.get("_sfc_updating", False)
return False
def schedule_update_lock_reset():
if bpy.context and hasattr(bpy.context, 'scene'):
bpy.context.scene["_sfc_updating"] = False
return None
def trigger_delayed_unlock():
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)
# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================
def get_or_create_collection(context, name, parent_col=None):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
if parent_col:
if col.name not in parent_col.children:
parent_col.children.link(col)
else:
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
return col
def get_master_collection(context):
return get_or_create_collection(context, MASTER_COLLECTION_NAME)
def find_node(nodes, node_type, name):
if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
return nodes.get(name)
def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
node = find_node(nodes, node_type, name)
if node: return node
new_node = nodes.new(type=node_type)
new_node.name = name
new_node.label = name.replace("_", " ")
output_node = find_node(nodes, 'OUTPUT_WORLD', '')
if output_node:
new_node.location = output_node.location + mathutils.Vector(location_offset)
return new_node
def get_world_nodes(context, create=True):
world = context.scene.world
if not world and create:
world = bpy.data.worlds.new("World")
context.scene.world = world
if not world: return None, None, None
if create: world.use_nodes = True
if not world.use_nodes: return world, None, None
return world, world.node_tree.nodes, world.node_tree.links
def load_hdri_from_path(filepath, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return False
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if os.path.exists(filepath):
try:
env_node.image = bpy.data.images.load(filepath, check_existing=True)
return True
except Exception as e:
print(f"[HDRI Load Error] {filepath} -> {e}")
return False
return False
def update_viewport(context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
return
def update_background_mode(self, context):
mode = context.scene.zionad_swt_props.background_mode
world, nodes, links = get_world_nodes(context)
if not nodes: return
output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
if mode == 'SKY':
links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
elif mode == 'HDRI':
if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
links.new(env_node.outputs['Color'], background_node.inputs['Color'])
props = context.scene.zionad_swt_props
if 0 <= props.hdri_list_index < len(HDRI_PATHS):
load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
update_viewport(context)
# ======================================================================
# --- \u53cd\u5c04円柱ジェネレータ関連 (パネル2) ---
# ======================================================================
def get_or_create_color_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = None
if mat.use_nodes:
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.blend_method = 'BLEND'
return mat
def create_cylinder_object(name, collection, loc, rot, radius, length, mat):
me = bpy.data.meshes.new(name)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=radius, radius2=radius, depth=length)
bm.to_mesh(me)
bm.free()
for poly in me.polygons:
poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
obj.location = loc
obj.rotation_euler = rot
if mat: obj.data.materials.append(mat)
collection.objects.link(obj)
return obj
def update_reflection_visibility(self, context):
col = bpy.data.collections.get("ReflectObjects")
if not col: return
hide = not self.show_objects
for obj in col.objects:
if obj.name.startswith("Reflect_"):
obj.hide_viewport = hide
obj.hide_render = hide
class ReflectionPointInfo(PropertyGroup):
index: IntProperty()
pos_x: FloatProperty()
pos_z: FloatProperty()
reflect_time: FloatProperty()
cam2_dist: FloatProperty()
cam2_arrival_time: FloatProperty()
class ReflectionProperties(PropertyGroup):
show_objects: BoolProperty(name="オブジェクトを表示", default=True, update=update_reflection_visibility)
z_floor: FloatProperty(name="床面 Z", default=0.0)
z_ceil: FloatProperty(name="天井 Z", default=10.0)
radius_bound: FloatProperty(name="床/天井 太さ", default=0.2, min=0.01)
color_bound: FloatVectorProperty(name="床/天井 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.2, 0.8, 1.0))
start_x: FloatProperty(name="開始位置 X", default=0.0)
angle: FloatProperty(name="角度", default=45.0, min=1.0, max=89.0)
bounce_count: IntProperty(name="\u53cd\u5c04回数", default=1, min=1)
radius_ray: FloatProperty(name="斜め円柱 太さ", default=0.2, min=0.01)
color_ray: FloatVectorProperty(name="斜め円柱 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.8, 0.1, 1.0))
info_segment_length: StringProperty(name="1区間 長さ", default="0.000")
info_total_length: StringProperty(name="合計 長さ", default="0.000")
dx: FloatProperty(name="X軸差分", default=0.0)
segment_length: FloatProperty(name="1区間長さ", default=0.0)
points: CollectionProperty(type=ReflectionPointInfo)
class SFC_OT_GenerateReflectionCylinders(Operator):
bl_idname = f"{PREFIX}.generate_reflection_cylinders"
bl_label = "斜め円柱(\u53cd\u5c04)を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.reflection_props
sfc_props = context.scene.surface_camera_properties
master_col = get_master_collection(context)
col = get_or_create_collection(context, "ReflectObjects", master_col)
objs_to_remove =[obj for obj in col.objects if obj.name.startswith("Reflect_")]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
mat_bound = get_or_create_color_material("Mat_Reflect_Bound", props.color_bound)
mat_ray = get_or_create_color_material("Mat_Reflect_Ray", props.color_ray)
rot_x = mathutils.Euler((0, math.pi/2, 0), 'XYZ')
create_cylinder_object("Reflect_Floor", col, (0, 0, props.z_floor), rot_x, props.radius_bound, 200.0, mat_bound)
create_cylinder_object("Reflect_Ceil", col, (0, 0, props.z_ceil), rot_x, props.radius_bound, 200.0, mat_bound)
z_f = props.z_floor
z_c = props.z_ceil
if z_f >= z_c:
self.report({'WARNING'}, "天井は床より高く設定してください")
return {'CANCELLED'}
dz = z_c - z_f
angle_rad = math.radians(props.angle)
dx = dz / math.tan(angle_rad)
segment_len = math.sqrt(dx**2 + dz**2)
props.dx = dx
props.segment_length = segment_len
props.info_segment_length = f"{segment_len:.3f}"
props.info_total_length = f"{segment_len * props.bounce_count:.3f}"
# 情報取得用のカメラ2の座標取得
cam2 = bpy.data.objects.get("Fixed_Cam_2")
if cam2 and cam2.type == 'CAMERA':
cam2_loc = cam2.location
else:
cam2_loc = mathutils.Vector(sfc_props.cam2_init_loc)
props.points.clear()
p0 = mathutils.Vector((props.start_x, 0.0, z_f))
# 0番(スタート地点)の情報を記録
pt = props.points.add()
pt.index = 0
pt.pos_x = p0.x
pt.pos_z = p0.z
pt.reflect_time = 0.0
pt.cam2_dist = (cam2_loc - p0).length
pt.cam2_arrival_time = pt.reflect_time + pt.cam2_dist
p_current = p0.copy()
for i in range(1, props.bounce_count + 1):
direction_z = 1 if (i-1) % 2 == 0 else -1
p_next = p_current + mathutils.Vector((dx, 0.0, dz * direction_z))
# 最大10番(配列サイズで11個)まで表示・記録する
if i <= 10:
pt = props.points.add()
pt.index = i
pt.pos_x = p_next.x
pt.pos_z = p_next.z
pt.reflect_time = i * segment_len
pt.cam2_dist = (cam2_loc - p_next).length
pt.cam2_arrival_time = pt.reflect_time + pt.cam2_dist
vec = p_next - p_current
loc = (p_current + p_next) / 2.0
rot_quat = vec.to_track_quat('Z', 'Y')
create_cylinder_object(f"Reflect_Ray_{i:03d}", col, loc, rot_quat.to_euler('XYZ'), props.radius_ray, segment_len, mat_ray)
p_current = p_next
update_reflection_visibility(props, context)
self.report({'INFO'}, "\u53cd\u5c04円柱と情報を生成しました")
return {'FINISHED'}
class SFC_OT_CopyReflectionInfo(Operator):
bl_idname = f"{PREFIX}.copy_reflection_info"
bl_label = "情報をコピー"
def execute(self, context):
props = context.scene.reflection_props
lines =[]
lines.append(f"1区間長さ: {props.segment_length:.3f}")
lines.append(f"X軸差分(dx): {props.dx:.3f}")
lines.append("-" * 30)
for p in props.points:
lines.append(f"【 番号 {p.index} 】")
lines.append(f"\u53cd\u5c04位置 x={p.pos_x:.3f} z={p.pos_z:.3f}")
lines.append(f"\u53cd\u5c04時刻 t={p.reflect_time:.3f}")
lines.append(f"カメラ2までの距離: {p.cam2_dist:.3f}")
lines.append(f"カメラ2 到達時刻 t={p.cam2_arrival_time:.3f}")
lines.append("-" * 30)
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "\u53cd\u5c04円柱の全情報をコピーしました")
return {'FINISHED'}
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context):
if self.camera_obj:
context.preferences.themes[0].view_3d.camera = self.camera_color
def update_grid_color_cb(self, context):
context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color_cb(self, context):
context.preferences.themes[0].view_3d.wire = self.wire_color
context.preferences.themes[0].view_3d.object_active = self.wire_color
class ThemeGridProperties(PropertyGroup):
grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))
class ThemeWireProperties(PropertyGroup):
wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))
class TargetProperty(PropertyGroup): name: StringProperty()
# ----------------------------------------------------------------------
# Property update の過密呼び出しを防ぐ Debounce(遅延)処理
# ----------------------------------------------------------------------
def _do_update_viewport_cam():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
vp_loc = mathutils.Vector(props.viewport_location)
vp_tgt = mathutils.Vector(props.viewport_target)
direction = vp_tgt - vp_loc
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
set_update_lock(scene, True)
try:
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
finally:
trigger_delayed_unlock()
break
return None
def safe_update_viewport_cam(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
bpy.app.timers.register(_do_update_viewport_cam, first_interval=0.01)
def _do_update_surface_camera():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
camera_obj = props.camera_obj
set_update_lock(scene, True)
try:
if props.is_updating_settings or not camera_obj:
update_info_panel_text(props, scene)
return None
cam_data = camera_obj.data
if cam_data:
cam_data.sensor_fit = 'HORIZONTAL'
cam_data.lens_unit = 'MILLIMETERS'
cam_data.lens = props.lens_focal_length
cam_data.clip_start = props.clip_start
cam_data.clip_end = props.clip_end
update_object_transform(camera_obj, props)
update_info_panel_text(props, scene)
finally:
trigger_delayed_unlock()
return None
def safe_update_surface_camera(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
bpy.app.timers.register(_do_update_surface_camera, first_interval=0.01)
# ----------------------------------------------------------------------
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=safe_update_surface_camera)
show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 1.0), subtype='XYZ')
cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 20.0), subtype='XYZ')
cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=safe_update_surface_camera)
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=safe_update_surface_camera)
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=safe_update_viewport_cam)
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=safe_update_viewport_cam)
is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=safe_update_surface_camera)
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=safe_update_surface_camera)
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=safe_update_surface_camera)
info_horizontal_fov: StringProperty(name="水平視野角")
camera_color: FloatVectorProperty(
name="カメラ枠線 色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=(0.0, 1.0, 1.0),
update=lambda self, context: update_cam_color(self, context)
)
class ZIONAD_SWT_Properties(PropertyGroup):
background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)
def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
except: return 0.0
def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
except: return 50.0
def get_target_location(props):
return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
location = obj.location
target_location = get_target_location(props)
direction = target_location - location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
final_quat = base_track_quat @ offset_euler.to_quaternion()
obj.rotation_euler = final_quat.to_euler('XYZ')
def update_info_panel_text(props, scene):
if not props: return
camera_obj = props.camera_obj
if not camera_obj: return
current_fov = calculate_horizontal_fov(props.lens_focal_length)
props.info_horizontal_fov = f"{current_fov:.1f} °"
def sync_ui_from_manual_transform(props, obj, scene):
if is_updating(scene): return
set_update_lock(scene, True)
try:
target_location = get_target_location(props)
direction = target_location - obj.location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
final_quat = obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat
offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch = offset_euler.x
props.offset_yaw = offset_euler.y
props.offset_roll = offset_euler.z
finally:
trigger_delayed_unlock()
update_info_panel_text(props, scene)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if is_updating(scene): return
sfc_props = scene.surface_camera_properties
cam_obj = sfc_props.camera_obj
if not cam_obj: return
for update in depsgraph.updates:
if not update.is_updated_transform: continue
if update.id.original == cam_obj:
sync_ui_from_manual_transform(sfc_props, cam_obj, scene)
return
# ======================================================================
# --- オペレーター ---
# ======================================================================
def set_initial_camera_transform(obj, loc, tgt):
loc_vec = mathutils.Vector(loc)
tgt_vec = mathutils.Vector(tgt)
direction = tgt_vec - loc_vec
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
obj.location = loc_vec
obj.rotation_euler = rot_quat.to_euler('XYZ')
class SFC_OT_CreateThreeCameras(Operator):
bl_idname = f"{PREFIX}.create_three_cameras"
bl_label = "3つのカメラを生成・初期化"
def execute(self, context):
master_col = get_master_collection(context)
col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
name = f"Fixed_Cam_{idx}"
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
cam_data = bpy.data.cameras.new(name=name)
cam_obj = bpy.data.objects.new(name, cam_data)
col.objects.link(cam_obj)
if context.scene.collection.objects.get(cam_obj.name):
context.scene.collection.objects.unlink(cam_obj)
set_initial_camera_transform(cam_obj, loc, tgt)
op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
op_func(cam_index="1")
self.report({'INFO'}, "3つのカメラを生成しました")
return {'FINISHED'}
class SFC_OT_ResetThreeCameras(Operator):
bl_idname = f"{PREFIX}.reset_three_cameras"
bl_label = "カメラを初期値に一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
if cam_obj and cam_obj.type == 'CAMERA':
set_initial_camera_transform(cam_obj, loc, tgt)
if props.camera_obj == cam_obj:
props.is_updating_settings = True
props.target_location = tgt
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
self.report({'INFO'}, "カメラを初期値にリセットしました")
return {'FINISHED'}
class SFC_OT_CopyThreeCamerasInitInfo(Operator):
bl_idname = f"{PREFIX}.copy_three_cameras_init_info"
bl_label = "初期値情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
lines =[]
fmt = ".2f"
for idx, loc, tgt in configs:
loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
lines.append(f"Cam {idx}: 位置 {loc_str} / 注視 {tgt_str}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ3つの初期値をコピーしました")
return {'FINISHED'}
class SFC_OT_ResetViewportCam(Operator):
bl_idname = f"{PREFIX}.reset_viewport_cam"
bl_label = "架空カメラを一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
props.viewport_location = (0.0, -10.0, 5.0)
props.viewport_target = (0.0, 0.0, 0.0)
self.report({'INFO'}, "架空カメラをリセットしました")
return {'FINISHED'}
class SFC_OT_CopyViewportInfo(Operator):
bl_idname = f"{PREFIX}.copy_viewport_info"
bl_label = "視座・注視点情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties
loc = props.viewport_location
tgt = props.viewport_target
fmt = ".2f"
loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
context.window_manager.clipboard = text_to_copy
self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
return {'FINISHED'}
class SFC_OT_SwitchCamera(Operator):
bl_idname = f"{PREFIX}.switch_camera"
bl_label = "カメラを切り替え"
cam_index: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
name = f"Fixed_Cam_{self.cam_index}"
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
return {'CANCELLED'}
props.is_updating_settings = True
props.camera_obj = cam_obj
context.scene.camera = cam_obj
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_perspective = 'CAMERA'
context.preferences.themes[0].view_3d.camera = props.camera_color
cam_data = cam_obj.data
props.lens_focal_length = cam_data.lens
props.clip_start = cam_data.clip_start
props.clip_end = cam_data.clip_end
forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
forward_vec.rotate(cam_obj.rotation_euler)
props.target_location = cam_obj.location + forward_vec
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
sync_ui_from_manual_transform(props, cam_obj, context.scene)
return {'FINISHED'}
class SFC_OT_GridApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_grid_properties
props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()
class SFC_OT_GridCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}
class SFC_OT_ResetProperty(Operator):
bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip":["clip_start", "clip_end", "lens_focal_length"],}
target_names, props_to_reset = {t.name for t in self.targets}, set()
if "all" in target_names:
for g in prop_groups.values(): props_to_reset.update(g)
else:
for name in target_names: props_to_reset.update(prop_groups.get(name,[]))
props.is_updating_settings = True
for p in props_to_reset:
if hasattr(props, p): props.property_unset(p)
props.is_updating_settings = False
safe_update_surface_camera(props, context)
return {'FINISHED'}
class SFC_OT_SetFOV(Operator):
bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}
class SFC_OT_OpenURL(Operator):
bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SFC_OT_RemoveAddon(Operator):
bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}
class SFC_OT_WireApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_wire_properties
props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()
class SFC_OT_WireCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}
class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
def execute(self, context):
props = context.scene.zionad_swt_props
if 0 <= self.hdri_index < len(HDRI_PATHS):
props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
return {'FINISHED'}
class ZIONAD_SWT_OT_ResetTransform(Operator):
bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
def execute(self, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return {'CANCELLED'}
mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
if not mapping_node: return {'CANCELLED'}
if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
return {'FINISHED'}
# ======================================================================
# --- UIパネル ---
# ======================================================================
class SFC_PT_CameraSetupPanel(Panel):
bl_label = "1. カメラ作成・切り替え"
bl_idname = PANEL_IDS["SETUP"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
box_init = layout.box()
box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
if props.show_init_settings:
col_init = box_init.column(align=True)
col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text=" 注視")
box_init.separator()
row_init_ops = box_init.row(align=True)
row_init_ops.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
row_init_ops.operator(SFC_OT_CopyThreeCamerasInitInfo.bl_idname, icon='COPYDOWN', text="情報をコピー")
layout.separator()
box = layout.box()
box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
row = box.row(align=True)
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
if props.camera_obj:
box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
else:
box.label(text="操作カメラ未選択", icon='ERROR')
box.separator()
box_color = box.box()
box_color.prop(props, "camera_color")
class SFC_PT_ReflectionPanel(Panel):
bl_label = "2. 斜め円柱 (\u53cd\u5c04) ジェネレータ"
bl_idname = PANEL_IDS["REFLECTION"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["REFLECTION"]]
def draw(self, context):
layout = self.layout
props = context.scene.reflection_props
layout.prop(props, "show_objects", toggle=True, icon='HIDE_OFF' if props.show_objects else 'HIDE_ON')
box1 = layout.box()
box1.label(text="床と天井 (長さ200固定)", icon='MESH_CYLINDER')
col1 = box1.column(align=True)
col1.prop(props, "z_floor")
col1.prop(props, "z_ceil")
row1 = col1.row(align=True)
row1.prop(props, "radius_bound", text="太さ")
row1.prop(props, "color_bound", text="")
box2 = layout.box()
box2.label(text="斜め円柱 (\u53cd\u5c04)", icon='LIGHT')
col2 = box2.column(align=True)
col2.prop(props, "start_x")
col2.prop(props, "angle")
col2.prop(props, "bounce_count")
row2 = col2.row(align=True)
row2.prop(props, "radius_ray", text="太さ")
row2.prop(props, "color_ray", text="")
layout.separator()
layout.operator(SFC_OT_GenerateReflectionCylinders.bl_idname, icon='FILE_REFRESH')
if len(props.points) > 0:
box3 = layout.box()
box3.label(text="生成情報", icon='INFO')
col3 = box3.column(align=True)
row3 = col3.row()
row3.label(text=f"1区間 長さ: {props.segment_length:.2f}")
row3.label(text=f"X軸差分: {props.dx:.2f}")
for p in props.points:
b = col3.box()
b.label(text=f"◆ {p.index} 番 (\u53cd\u5c04点)")
r1 = b.row()
r1.label(text=f"x={p.pos_x:.2f}, z={p.pos_z:.2f}")
r1.label(text=f"\u53cd\u5c04時刻 t={p.reflect_time:.2f}")
r2 = b.row()
r2.label(text=f"Cam2距離: {p.cam2_dist:.2f}")
r2.label(text=f"到達時刻 t={p.cam2_arrival_time:.2f}")
box3.operator(SFC_OT_CopyReflectionInfo.bl_idname, icon='COPYDOWN')
class SFC_PT_CameraAimingPanel(Panel):
bl_label = "3. 専用カメラ視線制御 (位置固定)"
bl_idname = PANEL_IDS["AIMING"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box_manual = layout.box()
box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
if props.camera_obj:
box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
col_aim = box_manual.column(align=True)
row_aim = col_aim.row(align=True)
row_aim.label(text="注視点")
op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_aim.targets.add().name = "aim"
op_aim.prop_group_name = "camera"
col_aim.prop(props, "target_location", text="")
box_manual.separator()
col_offset = box_manual.column(align=True)
row_offset = col_offset.row(align=True)
row_offset.label(text="視線オフセット (YPR)")
op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_offset.targets.add().name = "ypr"
op_offset.prop_group_name = "camera"
col_offset.prop(props, "offset_yaw")
col_offset.prop(props, "offset_pitch")
col_offset.prop(props, "offset_roll")
class SFC_PT_ViewportCamPanel(Panel):
bl_label = "4. ビューポート視座コントロール (架空カメラ)"
bl_idname = PANEL_IDS["VIEWPORT_CAM"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box = layout.box()
box.label(text="透視投影ビューの操作", icon='VIEW3D')
col = box.column(align=True)
col.prop(props, "viewport_location")
col.prop(props, "viewport_target")
box.separator()
box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")
class SFC_PT_LensPanel(Panel):
bl_label = "5. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
if props.camera_obj and props.camera_obj.data:
cam_data = props.camera_obj.data
box_type = layout.box()
box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="レンズとクリップ")
op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op.targets.add().name = "clip"
op.prop_group_name = "camera"
col.prop(props, "lens_focal_length")
row = col.row(align=True)
row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
col.label(text="FOVプリセット:")
row = col.row(align=True)
col1, col2 = row.column(align=True), row.column(align=True)
for i, fov in enumerate(FOV_PRESETS):
op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
op.fov = fov
col.separator()
row = col.row(align=True)
row.prop(props, "clip_start")
row.prop(props, "clip_end")
class SFC_PT_CameraDisplayPanel(Panel):
bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
def draw(self, context):
layout, scene, cam = self.layout, context.scene, context.scene.camera
box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
if not overlay: return
box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")
class ZIONAD_SWT_PT_WorldControlPanel(Panel):
bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
if not world or not world.use_nodes or not nodes: return
box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
if props.background_mode == 'HDRI':
box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
elif props.background_mode == 'SKY':
box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")
class SFC_PT_GridPanel(Panel):
bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")
class SFC_PT_WirePanel(Panel):
bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")
class SFC_PT_LinksPanel(Panel):
bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box1 = layout.box()
box1.label(text="ドキュメント", icon='HELP')
for link in NEW_DOC_LINKS:
op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
box2 = layout.box()
box2.label(text="ソーシャル", icon='WORLD_DATA')
for link in SOCIAL_LINKS:
op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_RemovePanel(Panel):
bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')
# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================
def initial_setup():
context = bpy.context
if not context.window_manager:
return 0.1
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.show_region_ui = True
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
if context.scene.world and context.scene.world.use_nodes:
props = context.scene.zionad_swt_props
nodes = context.scene.world.node_tree.nodes
background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
if background_node and background_node.inputs['Color'].is_linked:
source_node = background_node.inputs['Color'].links[0].from_node
if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
else: props.background_mode = 'HDRI'
update_background_mode(props, context)
return None
# ======================================================================
# --- 登録/解除 ---
# ======================================================================
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty,
ReflectionPointInfo, ReflectionProperties, SurfaceCameraProperties, ZIONAD_SWT_Properties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_CopyThreeCamerasInitInfo, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyViewportInfo, SFC_OT_GenerateReflectionCylinders, SFC_OT_CopyReflectionInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_ReflectionPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
SFC_PT_RemovePanel,
)
_registered_classes =[]
def register():
global _registered_classes
_registered_classes.clear()
for cls in classes:
try:
bpy.utils.register_class(cls)
_registered_classes.append(cls)
except Exception as e:
print(f"[REGISTER ERROR] {cls.__name__}: {e}")
bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
bpy.types.Scene.reflection_props = PointerProperty(type=ReflectionProperties)
if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
if not bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.register(initial_setup, first_interval=0.1)
def unregister():
global _registered_classes
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
if bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.unregister(initial_setup)
for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props', 'reflection_props']:
if prop_name in bpy.types.Scene.__dict__:
try: delattr(bpy.types.Scene, prop_name)
except Exception as e: print(f"[UNREGISTER ERROR] delattr {prop_name}: {e}")
for cls in reversed(_registered_classes):
try:
bpy.utils.unregister_class(cls)
except Exception as e:
print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except: pass
register()
反射 の フォント コード
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================
PREFIX = "unit_circle_cam"
bl_info = {
"name": "zionad 521 [Unit Circle Cam]",
"author": "zionadchat",
"version": (37, 0, 11),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラ、反射円柱ジェネレータ、ビューポートカメラ制御(完全安定版)",
"category": "Cam zigzag",
}
# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
HDRI_PATHS =[
r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS =[("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS =[("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]
MASTER_COLLECTION_NAME = "Cam three"
CAMERA_COLLECTION_NAME = "Cam"
SENSOR_WIDTH = 36.0
FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================
NEW_DOC_LINKS = [
{"label": "THIS_ADDON [ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
# --- パネル管理 ---
# ======================================================================
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup",
"REFLECTION": f"{PREFIX}_PT_reflection",
"AIMING": f"{PREFIX}_PT_aiming",
"VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
"LENS": f"{PREFIX}_PT_lens",
"CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display",
"WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"GRID": f"{PREFIX}_PT_grid_panel",
"WIRE": f"{PREFIX}_PT_wire_panel",
"LINKS": f"{PREFIX}_PT_links",
"REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
PANEL_IDS["SETUP"]: 0,
PANEL_IDS["REFLECTION"]: 1,
PANEL_IDS["AIMING"]: 2,
PANEL_IDS["VIEWPORT_CAM"]: 3,
PANEL_IDS["LENS"]: 4,
PANEL_IDS["CAMERA_DISPLAY"]: 5,
PANEL_IDS["WORLD_CONTROL"]: 6,
PANEL_IDS["GRID"]: 89,
PANEL_IDS["WIRE"]: 90,
PANEL_IDS["LINKS"]: 190,
PANEL_IDS["REMOVE"]: 200,
}
# ======================================================================
# --- ロック機構 & タイマー管理 ---
# ======================================================================
def set_update_lock(scene, state: bool):
if scene:
scene["_sfc_updating"] = state
def is_updating(scene):
if scene:
return scene.get("_sfc_updating", False)
return False
def schedule_update_lock_reset():
if bpy.context and hasattr(bpy.context, 'scene'):
bpy.context.scene["_sfc_updating"] = False
return None
def trigger_delayed_unlock():
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)
# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================
def get_or_create_collection(context, name, parent_col=None):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
if parent_col:
if col.name not in parent_col.children:
parent_col.children.link(col)
else:
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
return col
def get_master_collection(context):
return get_or_create_collection(context, MASTER_COLLECTION_NAME)
def find_node(nodes, node_type, name):
if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
return nodes.get(name)
def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
node = find_node(nodes, node_type, name)
if node: return node
new_node = nodes.new(type=node_type)
new_node.name = name
new_node.label = name.replace("_", " ")
output_node = find_node(nodes, 'OUTPUT_WORLD', '')
if output_node:
new_node.location = output_node.location + mathutils.Vector(location_offset)
return new_node
def get_world_nodes(context, create=True):
world = context.scene.world
if not world and create:
world = bpy.data.worlds.new("World")
context.scene.world = world
if not world: return None, None, None
if create: world.use_nodes = True
if not world.use_nodes: return world, None, None
return world, world.node_tree.nodes, world.node_tree.links
def load_hdri_from_path(filepath, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return False
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if os.path.exists(filepath):
try:
env_node.image = bpy.data.images.load(filepath, check_existing=True)
return True
except Exception as e:
print(f"[HDRI Load Error] {filepath} -> {e}")
return False
return False
def update_viewport(context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
return
def update_background_mode(self, context):
mode = context.scene.zionad_swt_props.background_mode
world, nodes, links = get_world_nodes(context)
if not nodes: return
output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
if mode == 'SKY':
links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
elif mode == 'HDRI':
if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
links.new(env_node.outputs['Color'], background_node.inputs['Color'])
props = context.scene.zionad_swt_props
if 0 <= props.hdri_list_index < len(HDRI_PATHS):
load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
update_viewport(context)
# ======================================================================
# --- 反射円柱ジェネレータ関連 (パネル2) ---
# ======================================================================
def get_or_create_color_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = None
if mat.use_nodes:
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.blend_method = 'BLEND'
return mat
def create_cylinder_object(name, collection, loc, rot, radius, length, mat):
me = bpy.data.meshes.new(name)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=radius, radius2=radius, depth=length)
bm.to_mesh(me)
bm.free()
for poly in me.polygons:
poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
obj.location = loc
obj.rotation_euler = rot
if mat: obj.data.materials.append(mat)
collection.objects.link(obj)
return obj
def update_reflection_visibility(self, context):
col = bpy.data.collections.get("ReflectObjects")
if not col: return
hide = not self.show_objects
for obj in col.objects:
if obj.name.startswith("Reflect_"):
obj.hide_viewport = hide
obj.hide_render = hide
class ReflectionPointInfo(PropertyGroup):
index: IntProperty()
pos_x: FloatProperty()
pos_z: FloatProperty()
reflect_time: FloatProperty()
cam2_dist: FloatProperty()
cam2_arrival_time: FloatProperty()
class ReflectionProperties(PropertyGroup):
show_objects: BoolProperty(name="オブジェクトを表示", default=True, update=update_reflection_visibility)
z_floor: FloatProperty(name="床面 Z", default=0.0)
z_ceil: FloatProperty(name="天井 Z", default=10.0)
radius_bound: FloatProperty(name="床/天井 太さ", default=0.2, min=0.01)
color_bound: FloatVectorProperty(name="床/天井 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.2, 0.8, 1.0))
start_x: FloatProperty(name="開始位置 X", default=0.0)
angle: FloatProperty(name="角度", default=45.0, min=1.0, max=89.0)
bounce_count: IntProperty(name="反射回数", default=1, min=1)
radius_ray: FloatProperty(name="斜め円柱 太さ", default=0.2, min=0.01)
color_ray: FloatVectorProperty(name="斜め円柱 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.8, 0.1, 1.0))
info_segment_length: StringProperty(name="1区間 長さ", default="0.000")
info_total_length: StringProperty(name="合計 長さ", default="0.000")
dx: FloatProperty(name="X軸差分", default=0.0)
segment_length: FloatProperty(name="1区間長さ", default=0.0)
points: CollectionProperty(type=ReflectionPointInfo)
class SFC_OT_GenerateReflectionCylinders(Operator):
bl_idname = f"{PREFIX}.generate_reflection_cylinders"
bl_label = "斜め円柱(反射)を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.reflection_props
sfc_props = context.scene.surface_camera_properties
master_col = get_master_collection(context)
col = get_or_create_collection(context, "ReflectObjects", master_col)
objs_to_remove =[obj for obj in col.objects if obj.name.startswith("Reflect_")]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
mat_bound = get_or_create_color_material("Mat_Reflect_Bound", props.color_bound)
mat_ray = get_or_create_color_material("Mat_Reflect_Ray", props.color_ray)
rot_x = mathutils.Euler((0, math.pi/2, 0), 'XYZ')
create_cylinder_object("Reflect_Floor", col, (0, 0, props.z_floor), rot_x, props.radius_bound, 200.0, mat_bound)
create_cylinder_object("Reflect_Ceil", col, (0, 0, props.z_ceil), rot_x, props.radius_bound, 200.0, mat_bound)
z_f = props.z_floor
z_c = props.z_ceil
if z_f >= z_c:
self.report({'WARNING'}, "天井は床より高く設定してください")
return {'CANCELLED'}
dz = z_c - z_f
angle_rad = math.radians(props.angle)
dx = dz / math.tan(angle_rad)
segment_len = math.sqrt(dx**2 + dz**2)
props.dx = dx
props.segment_length = segment_len
props.info_segment_length = f"{segment_len:.3f}"
props.info_total_length = f"{segment_len * props.bounce_count:.3f}"
# 情報取得用のカメラ2の座標取得
cam2 = bpy.data.objects.get("Fixed_Cam_2")
if cam2 and cam2.type == 'CAMERA':
cam2_loc = cam2.location
else:
cam2_loc = mathutils.Vector(sfc_props.cam2_init_loc)
props.points.clear()
p0 = mathutils.Vector((props.start_x, 0.0, z_f))
# 0番(スタート地点)の情報を記録
pt = props.points.add()
pt.index = 0
pt.pos_x = p0.x
pt.pos_z = p0.z
pt.reflect_time = 0.0
pt.cam2_dist = (cam2_loc - p0).length
pt.cam2_arrival_time = pt.reflect_time + pt.cam2_dist
p_current = p0.copy()
for i in range(1, props.bounce_count + 1):
direction_z = 1 if (i-1) % 2 == 0 else -1
p_next = p_current + mathutils.Vector((dx, 0.0, dz * direction_z))
# 最大10番(配列サイズで11個)まで表示・記録する
if i <= 10:
pt = props.points.add()
pt.index = i
pt.pos_x = p_next.x
pt.pos_z = p_next.z
pt.reflect_time = i * segment_len
pt.cam2_dist = (cam2_loc - p_next).length
pt.cam2_arrival_time = pt.reflect_time + pt.cam2_dist
vec = p_next - p_current
loc = (p_current + p_next) / 2.0
rot_quat = vec.to_track_quat('Z', 'Y')
create_cylinder_object(f"Reflect_Ray_{i:03d}", col, loc, rot_quat.to_euler('XYZ'), props.radius_ray, segment_len, mat_ray)
p_current = p_next
update_reflection_visibility(props, context)
self.report({'INFO'}, "反射円柱と情報を生成しました")
return {'FINISHED'}
class SFC_OT_CopyReflectionInfo(Operator):
bl_idname = f"{PREFIX}.copy_reflection_info"
bl_label = "情報をコピー"
def execute(self, context):
props = context.scene.reflection_props
lines =[]
lines.append(f"1区間長さ: {props.segment_length:.3f}")
lines.append(f"X軸差分(dx): {props.dx:.3f}")
lines.append("-" * 30)
for p in props.points:
lines.append(f"【 番号 {p.index} 】")
lines.append(f"反射位置 x={p.pos_x:.3f} z={p.pos_z:.3f}")
lines.append(f"反射時刻 t={p.reflect_time:.3f}")
lines.append(f"カメラ2までの距離: {p.cam2_dist:.3f}")
lines.append(f"カメラ2 到達時刻 t={p.cam2_arrival_time:.3f}")
lines.append("-" * 30)
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "反射円柱の全情報をコピーしました")
return {'FINISHED'}
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context):
if self.camera_obj:
context.preferences.themes[0].view_3d.camera = self.camera_color
def update_grid_color_cb(self, context):
context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color_cb(self, context):
context.preferences.themes[0].view_3d.wire = self.wire_color
context.preferences.themes[0].view_3d.object_active = self.wire_color
class ThemeGridProperties(PropertyGroup):
grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))
class ThemeWireProperties(PropertyGroup):
wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))
class TargetProperty(PropertyGroup): name: StringProperty()
# ----------------------------------------------------------------------
# Property update の過密呼び出しを防ぐ Debounce(遅延)処理
# ----------------------------------------------------------------------
def _do_update_viewport_cam():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
vp_loc = mathutils.Vector(props.viewport_location)
vp_tgt = mathutils.Vector(props.viewport_target)
direction = vp_tgt - vp_loc
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
set_update_lock(scene, True)
try:
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
finally:
trigger_delayed_unlock()
break
return None
def safe_update_viewport_cam(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
bpy.app.timers.register(_do_update_viewport_cam, first_interval=0.01)
def _do_update_surface_camera():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
camera_obj = props.camera_obj
set_update_lock(scene, True)
try:
if props.is_updating_settings or not camera_obj:
update_info_panel_text(props, scene)
return None
cam_data = camera_obj.data
if cam_data:
cam_data.sensor_fit = 'HORIZONTAL'
cam_data.lens_unit = 'MILLIMETERS'
cam_data.lens = props.lens_focal_length
cam_data.clip_start = props.clip_start
cam_data.clip_end = props.clip_end
update_object_transform(camera_obj, props)
update_info_panel_text(props, scene)
finally:
trigger_delayed_unlock()
return None
def safe_update_surface_camera(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
bpy.app.timers.register(_do_update_surface_camera, first_interval=0.01)
# ----------------------------------------------------------------------
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=safe_update_surface_camera)
show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 1.0), subtype='XYZ')
cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 20.0), subtype='XYZ')
cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=safe_update_surface_camera)
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=safe_update_surface_camera)
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=safe_update_viewport_cam)
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=safe_update_viewport_cam)
is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=safe_update_surface_camera)
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=safe_update_surface_camera)
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=safe_update_surface_camera)
info_horizontal_fov: StringProperty(name="水平視野角")
camera_color: FloatVectorProperty(
name="カメラ枠線 色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=(0.0, 1.0, 1.0),
update=lambda self, context: update_cam_color(self, context)
)
class ZIONAD_SWT_Properties(PropertyGroup):
background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)
def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
except: return 0.0
def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
except: return 50.0
def get_target_location(props):
return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
location = obj.location
target_location = get_target_location(props)
direction = target_location - location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
final_quat = base_track_quat @ offset_euler.to_quaternion()
obj.rotation_euler = final_quat.to_euler('XYZ')
def update_info_panel_text(props, scene):
if not props: return
camera_obj = props.camera_obj
if not camera_obj: return
current_fov = calculate_horizontal_fov(props.lens_focal_length)
props.info_horizontal_fov = f"{current_fov:.1f} °"
def sync_ui_from_manual_transform(props, obj, scene):
if is_updating(scene): return
set_update_lock(scene, True)
try:
target_location = get_target_location(props)
direction = target_location - obj.location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
final_quat = obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat
offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch = offset_euler.x
props.offset_yaw = offset_euler.y
props.offset_roll = offset_euler.z
finally:
trigger_delayed_unlock()
update_info_panel_text(props, scene)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if is_updating(scene): return
sfc_props = scene.surface_camera_properties
cam_obj = sfc_props.camera_obj
if not cam_obj: return
for update in depsgraph.updates:
if not update.is_updated_transform: continue
if update.id.original == cam_obj:
sync_ui_from_manual_transform(sfc_props, cam_obj, scene)
return
# ======================================================================
# --- オペレーター ---
# ======================================================================
def set_initial_camera_transform(obj, loc, tgt):
loc_vec = mathutils.Vector(loc)
tgt_vec = mathutils.Vector(tgt)
direction = tgt_vec - loc_vec
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
obj.location = loc_vec
obj.rotation_euler = rot_quat.to_euler('XYZ')
class SFC_OT_CreateThreeCameras(Operator):
bl_idname = f"{PREFIX}.create_three_cameras"
bl_label = "3つのカメラを生成・初期化"
def execute(self, context):
master_col = get_master_collection(context)
col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
name = f"Fixed_Cam_{idx}"
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
cam_data = bpy.data.cameras.new(name=name)
cam_obj = bpy.data.objects.new(name, cam_data)
col.objects.link(cam_obj)
if context.scene.collection.objects.get(cam_obj.name):
context.scene.collection.objects.unlink(cam_obj)
set_initial_camera_transform(cam_obj, loc, tgt)
op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
op_func(cam_index="1")
self.report({'INFO'}, "3つのカメラを生成しました")
return {'FINISHED'}
class SFC_OT_ResetThreeCameras(Operator):
bl_idname = f"{PREFIX}.reset_three_cameras"
bl_label = "カメラを初期値に一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
if cam_obj and cam_obj.type == 'CAMERA':
set_initial_camera_transform(cam_obj, loc, tgt)
if props.camera_obj == cam_obj:
props.is_updating_settings = True
props.target_location = tgt
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
self.report({'INFO'}, "カメラを初期値にリセットしました")
return {'FINISHED'}
class SFC_OT_CopyThreeCamerasInitInfo(Operator):
bl_idname = f"{PREFIX}.copy_three_cameras_init_info"
bl_label = "初期値情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties
configs =[
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
lines =[]
fmt = ".2f"
for idx, loc, tgt in configs:
loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
lines.append(f"Cam {idx}: 位置 {loc_str} / 注視 {tgt_str}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "カメラ3つの初期値をコピーしました")
return {'FINISHED'}
class SFC_OT_ResetViewportCam(Operator):
bl_idname = f"{PREFIX}.reset_viewport_cam"
bl_label = "架空カメラを一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
props.viewport_location = (0.0, -10.0, 5.0)
props.viewport_target = (0.0, 0.0, 0.0)
self.report({'INFO'}, "架空カメラをリセットしました")
return {'FINISHED'}
class SFC_OT_CopyViewportInfo(Operator):
bl_idname = f"{PREFIX}.copy_viewport_info"
bl_label = "視座・注視点情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties
loc = props.viewport_location
tgt = props.viewport_target
fmt = ".2f"
loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
context.window_manager.clipboard = text_to_copy
self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
return {'FINISHED'}
class SFC_OT_SwitchCamera(Operator):
bl_idname = f"{PREFIX}.switch_camera"
bl_label = "カメラを切り替え"
cam_index: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
name = f"Fixed_Cam_{self.cam_index}"
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
return {'CANCELLED'}
props.is_updating_settings = True
props.camera_obj = cam_obj
context.scene.camera = cam_obj
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_perspective = 'CAMERA'
context.preferences.themes[0].view_3d.camera = props.camera_color
cam_data = cam_obj.data
props.lens_focal_length = cam_data.lens
props.clip_start = cam_data.clip_start
props.clip_end = cam_data.clip_end
forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
forward_vec.rotate(cam_obj.rotation_euler)
props.target_location = cam_obj.location + forward_vec
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
sync_ui_from_manual_transform(props, cam_obj, context.scene)
return {'FINISHED'}
class SFC_OT_GridApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_grid_properties
props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()
class SFC_OT_GridCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}
class SFC_OT_ResetProperty(Operator):
bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip":["clip_start", "clip_end", "lens_focal_length"],}
target_names, props_to_reset = {t.name for t in self.targets}, set()
if "all" in target_names:
for g in prop_groups.values(): props_to_reset.update(g)
else:
for name in target_names: props_to_reset.update(prop_groups.get(name,[]))
props.is_updating_settings = True
for p in props_to_reset:
if hasattr(props, p): props.property_unset(p)
props.is_updating_settings = False
safe_update_surface_camera(props, context)
return {'FINISHED'}
class SFC_OT_SetFOV(Operator):
bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}
class SFC_OT_OpenURL(Operator):
bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SFC_OT_RemoveAddon(Operator):
bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}
class SFC_OT_WireApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_wire_properties
props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()
class SFC_OT_WireCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}
class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
def execute(self, context):
props = context.scene.zionad_swt_props
if 0 <= self.hdri_index < len(HDRI_PATHS):
props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
return {'FINISHED'}
class ZIONAD_SWT_OT_ResetTransform(Operator):
bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
def execute(self, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return {'CANCELLED'}
mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
if not mapping_node: return {'CANCELLED'}
if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
return {'FINISHED'}
# ======================================================================
# --- UIパネル ---
# ======================================================================
class SFC_PT_CameraSetupPanel(Panel):
bl_label = "1. カメラ作成・切り替え"
bl_idname = PANEL_IDS["SETUP"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
box_init = layout.box()
box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
if props.show_init_settings:
col_init = box_init.column(align=True)
col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text=" 注視")
box_init.separator()
row_init_ops = box_init.row(align=True)
row_init_ops.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
row_init_ops.operator(SFC_OT_CopyThreeCamerasInitInfo.bl_idname, icon='COPYDOWN', text="情報をコピー")
layout.separator()
box = layout.box()
box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
row = box.row(align=True)
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
if props.camera_obj:
box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
else:
box.label(text="操作カメラ未選択", icon='ERROR')
box.separator()
box_color = box.box()
box_color.prop(props, "camera_color")
class SFC_PT_ReflectionPanel(Panel):
bl_label = "2. 斜め円柱 (反射) ジェネレータ"
bl_idname = PANEL_IDS["REFLECTION"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["REFLECTION"]]
def draw(self, context):
layout = self.layout
props = context.scene.reflection_props
layout.prop(props, "show_objects", toggle=True, icon='HIDE_OFF' if props.show_objects else 'HIDE_ON')
box1 = layout.box()
box1.label(text="床と天井 (長さ200固定)", icon='MESH_CYLINDER')
col1 = box1.column(align=True)
col1.prop(props, "z_floor")
col1.prop(props, "z_ceil")
row1 = col1.row(align=True)
row1.prop(props, "radius_bound", text="太さ")
row1.prop(props, "color_bound", text="")
box2 = layout.box()
box2.label(text="斜め円柱 (反射)", icon='LIGHT')
col2 = box2.column(align=True)
col2.prop(props, "start_x")
col2.prop(props, "angle")
col2.prop(props, "bounce_count")
row2 = col2.row(align=True)
row2.prop(props, "radius_ray", text="太さ")
row2.prop(props, "color_ray", text="")
layout.separator()
layout.operator(SFC_OT_GenerateReflectionCylinders.bl_idname, icon='FILE_REFRESH')
if len(props.points) > 0:
box3 = layout.box()
box3.label(text="生成情報", icon='INFO')
col3 = box3.column(align=True)
row3 = col3.row()
row3.label(text=f"1区間 長さ: {props.segment_length:.2f}")
row3.label(text=f"X軸差分: {props.dx:.2f}")
for p in props.points:
b = col3.box()
b.label(text=f"◆ {p.index} 番 (反射点)")
r1 = b.row()
r1.label(text=f"x={p.pos_x:.2f}, z={p.pos_z:.2f}")
r1.label(text=f"反射時刻 t={p.reflect_time:.2f}")
r2 = b.row()
r2.label(text=f"Cam2距離: {p.cam2_dist:.2f}")
r2.label(text=f"到達時刻 t={p.cam2_arrival_time:.2f}")
box3.operator(SFC_OT_CopyReflectionInfo.bl_idname, icon='COPYDOWN')
class SFC_PT_CameraAimingPanel(Panel):
bl_label = "3. 専用カメラ視線制御 (位置固定)"
bl_idname = PANEL_IDS["AIMING"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box_manual = layout.box()
box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
if props.camera_obj:
box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
col_aim = box_manual.column(align=True)
row_aim = col_aim.row(align=True)
row_aim.label(text="注視点")
op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_aim.targets.add().name = "aim"
op_aim.prop_group_name = "camera"
col_aim.prop(props, "target_location", text="")
box_manual.separator()
col_offset = box_manual.column(align=True)
row_offset = col_offset.row(align=True)
row_offset.label(text="視線オフセット (YPR)")
op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_offset.targets.add().name = "ypr"
op_offset.prop_group_name = "camera"
col_offset.prop(props, "offset_yaw")
col_offset.prop(props, "offset_pitch")
col_offset.prop(props, "offset_roll")
class SFC_PT_ViewportCamPanel(Panel):
bl_label = "4. ビューポート視座コントロール (架空カメラ)"
bl_idname = PANEL_IDS["VIEWPORT_CAM"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box = layout.box()
box.label(text="透視投影ビューの操作", icon='VIEW3D')
col = box.column(align=True)
col.prop(props, "viewport_location")
col.prop(props, "viewport_target")
box.separator()
box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")
class SFC_PT_LensPanel(Panel):
bl_label = "5. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
if props.camera_obj and props.camera_obj.data:
cam_data = props.camera_obj.data
box_type = layout.box()
box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="レンズとクリップ")
op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op.targets.add().name = "clip"
op.prop_group_name = "camera"
col.prop(props, "lens_focal_length")
row = col.row(align=True)
row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
col.label(text="FOVプリセット:")
row = col.row(align=True)
col1, col2 = row.column(align=True), row.column(align=True)
for i, fov in enumerate(FOV_PRESETS):
op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
op.fov = fov
col.separator()
row = col.row(align=True)
row.prop(props, "clip_start")
row.prop(props, "clip_end")
class SFC_PT_CameraDisplayPanel(Panel):
bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
def draw(self, context):
layout, scene, cam = self.layout, context.scene, context.scene.camera
box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
if not overlay: return
box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")
class ZIONAD_SWT_PT_WorldControlPanel(Panel):
bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
if not world or not world.use_nodes or not nodes: return
box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
if props.background_mode == 'HDRI':
box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
elif props.background_mode == 'SKY':
box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")
class SFC_PT_GridPanel(Panel):
bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")
class SFC_PT_WirePanel(Panel):
bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")
class SFC_PT_LinksPanel(Panel):
bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box1 = layout.box()
box1.label(text="ドキュメント", icon='HELP')
for link in NEW_DOC_LINKS:
op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
box2 = layout.box()
box2.label(text="ソーシャル", icon='WORLD_DATA')
for link in SOCIAL_LINKS:
op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_RemovePanel(Panel):
bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')
# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================
def initial_setup():
context = bpy.context
if not context.window_manager:
return 0.1
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.show_region_ui = True
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
if context.scene.world and context.scene.world.use_nodes:
props = context.scene.zionad_swt_props
nodes = context.scene.world.node_tree.nodes
background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
if background_node and background_node.inputs['Color'].is_linked:
source_node = background_node.inputs['Color'].links[0].from_node
if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
else: props.background_mode = 'HDRI'
update_background_mode(props, context)
return None
# ======================================================================
# --- 登録/解除 ---
# ======================================================================
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty,
ReflectionPointInfo, ReflectionProperties, SurfaceCameraProperties, ZIONAD_SWT_Properties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_CopyThreeCamerasInitInfo, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyViewportInfo, SFC_OT_GenerateReflectionCylinders, SFC_OT_CopyReflectionInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_ReflectionPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
SFC_PT_RemovePanel,
)
_registered_classes =[]
def register():
global _registered_classes
_registered_classes.clear()
for cls in classes:
try:
bpy.utils.register_class(cls)
_registered_classes.append(cls)
except Exception as e:
print(f"[REGISTER ERROR] {cls.__name__}: {e}")
bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
bpy.types.Scene.reflection_props = PointerProperty(type=ReflectionProperties)
if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
if not bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.register(initial_setup, first_interval=0.1)
def unregister():
global _registered_classes
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
if bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.unregister(initial_setup)
for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props', 'reflection_props']:
if prop_name in bpy.types.Scene.__dict__:
try: delattr(bpy.types.Scene, prop_name)
except Exception as e: print(f"[UNREGISTER ERROR] delattr {prop_name}: {e}")
for cls in reversed(_registered_classes):
try:
bpy.utils.unregister_class(cls)
except Exception as e:
print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except: pass
register()