blender Million 2026
単位円 カメラアイ 20260326
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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"unit_circle_cam_{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad 521 [Unit Circle Cam]",
"author": "zionadchat",
"version": (37, 0, 0),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能",
"category": " [ Unit Circle Cam ] ",
}
# ======================================================================
# --- ユーザー設定 / 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 = "Unit Circle Cam"
CAMERA_COLLECTION_NAME = "Cam"
BASE_CIRCLE_COLLECTION = "基準円"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
{"label": "単位円 カメラアイ 20260326", "url": "<https://www.notion.so/20260326-32ef5dacaf438072801cf561b3790fed>"},
]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},]
SOCIAL_LINKS = [{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},]
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "BASE_CIRCLE": f"{PREFIX}_PT_base_circle",
"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",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["BASE_CIRCLE"]: 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["INFO"]: 7, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- Collection Helper ---
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)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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 create_torus_bmesh(bm, major_radius, minor_radius, major_segments, minor_segments):
rings = []
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = major_radius * math.cos(t)
y = major_radius * math.sin(t)
p = mathutils.Vector((x, y, 0))
n = mathutils.Vector((math.cos(t), math.sin(t), 0)).normalized()
up = mathutils.Vector((0, 0, 1))
ring = []
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges = []
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def update_base_visibility(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col: return
props = context.scene.base_circle_props
for obj in col.objects:
hide = False
name = obj.name
if name.startswith("Base_Circle_R1"): hide = not props.show_base_circle_1
elif name.startswith("Base_Circle_R2"): hide = not props.show_base_circle_2
elif name.startswith("Torus_Main_R1"): hide = not props.show_main_torus_1
elif name.startswith("Torus_Main_R2"): hide = not props.show_main_torus_2
elif name.startswith("半径1_"): hide = not props.show_spheres_1
elif name.startswith("半径2_"): hide = not props.show_spheres_2
elif name.startswith("SubTorus_R1_"): hide = not props.show_sub_torus_1
elif name.startswith("SubTorus_R2_"): hide = not props.show_sub_torus_2
obj.hide_viewport = hide
obj.hide_render = hide
class BaseCircleProperties(PropertyGroup):
radius_1: FloatProperty(name="半径 1", default=1.0, min=0.01)
radius_2: FloatProperty(name="半径 2", default=2.0, min=0.01)
color_1: FloatVectorProperty(name="色 1", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.2, 0.2, 1.0))
color_2: FloatVectorProperty(name="色 2", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.5, 1.0, 1.0))
show_base_circle_1: BoolProperty(name="基準円1", default=True, update=update_base_visibility)
show_base_circle_2: BoolProperty(name="基準円2", default=True, update=update_base_visibility)
show_main_torus_1: BoolProperty(name="トーラス1", default=True, update=update_base_visibility)
show_main_torus_2: BoolProperty(name="トーラス2", default=True, update=update_base_visibility)
show_spheres_1: BoolProperty(name="球体1", default=True, update=update_base_visibility)
show_spheres_2: BoolProperty(name="球体2", default=True, update=update_base_visibility)
show_sub_torus_1: BoolProperty(name="小トーラス1", default=True, update=update_base_visibility)
show_sub_torus_2: BoolProperty(name="小トーラス2", default=True, update=update_base_visibility)
def 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
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
class SFC_OT_GenerateBaseCircleData(Operator):
bl_idname = f"{PREFIX}.generate_base_circle_data"
bl_label = "基準円・トーラスを生成 / 更新"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
master_col = get_master_collection(context)
col = get_or_create_collection(context, BASE_CIRCLE_COLLECTION, master_col)
prefix_list = ["Base_Circle_R", "Torus_Main_R", "半径1_", "半径2_", "SubTorus_R"]
objs_to_remove = [obj for obj in col.objects if any(obj.name.startswith(p) for p in prefix_list)]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
props = context.scene.base_circle_props
mat_r1 = create_color_material("Mat_Radius1", props.color_1)
mat_r2 = create_color_material("Mat_Radius2", props.color_2)
def make_mesh(name, bm, mat=None):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
if mat: obj.data.materials.append(mat)
col.objects.link(obj)
return obj
for radius, mat, r_name in [(props.radius_1, mat_r1, "1"), (props.radius_2, mat_r2, "2")]:
if radius <= 0: continue
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
make_mesh(f"Base_Circle_R{r_name}", bm, mat)
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=radius, minor_radius=0.04, major_segments=64, minor_segments=16)
make_mesh(f"Torus_Main_R{r_name}", bm, mat)
for i in range(12):
angle_deg = i * 30
angle_rad = math.radians(angle_deg)
name_suffix = f"{angle_deg:03d}度"
x = math.cos(angle_rad) * radius
y = math.sin(angle_rad) * radius
loc = mathutils.Vector((x, y, 0))
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.08)
obj_sph = make_mesh(f"半径{r_name}_{name_suffix}", bm, mat)
obj_sph.location = loc
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=0.15, minor_radius=0.02, major_segments=32, minor_segments=8)
obj_sub = make_mesh(f"SubTorus_R{r_name}_{name_suffix}", bm, mat)
obj_sub.location = loc
if loc.length > 0.001:
rot_quat = (-loc).to_track_quat('Z', 'Y')
obj_sub.rotation_euler = rot_quat.to_euler()
update_base_visibility(context.scene.base_circle_props, context)
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
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))
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))
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()
def update_viewport_cam(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
vp_loc = mathutils.Vector(self.viewport_location)
vp_tgt = mathutils.Vector(self.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 area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
_is_updating_by_addon = True
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
_is_updating_by_addon = False
break
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
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, 0.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, 100.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=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: 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_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
camera_obj = props.camera_obj
if not camera_obj: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location = camera_obj.location
target_location = get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"
current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = 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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); 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 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 cam_obj.name in context.scene.collection.objects:
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:
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_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_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 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)
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; 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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = f"焦点距離: {props.info_focal_length}\nカメラ位置: {props.info_camera_location}\n注視点: {props.info_target_location}"; 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()
box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', 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_BaseCirclePanel(Panel):
bl_label = "2. 基準円・トーラス生成"; bl_idname = PANEL_IDS["BASE_CIRCLE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["BASE_CIRCLE"]]
def draw(self, context):
layout = self.layout
props = context.scene.base_circle_props
box_param = layout.box()
box_param.label(text="パラメータ設定", icon='PREFERENCES')
col_param = box_param.column(align=True)
row_p = col_param.row(); row_p.prop(props, "radius_1"); row_p.prop(props, "radius_2")
row_c = col_param.row(); row_c.prop(props, "color_1", text=""); row_c.prop(props, "color_2", text="")
layout.separator()
layout.operator(SFC_OT_GenerateBaseCircleData.bl_idname, icon='MESH_TORUS', text="基準円・トーラスを生成 / 更新")
box = layout.box()
box.label(text="表示 / 非表示", icon='HIDE_OFF')
col = box.column(align=True)
row = col.row(); row.label(text="[グループ 1]"); row.label(text="[グループ 2]")
row = col.row(); row.prop(props, "show_base_circle_1", text="基準円"); row.prop(props, "show_base_circle_2", text="基準円")
row = col.row(); row.prop(props, "show_main_torus_1", text="大トーラス"); row.prop(props, "show_main_torus_2", text="大トーラス")
row = col.row(); row.prop(props, "show_spheres_1", text="球体(12個)"); row.prop(props, "show_spheres_2", text="球体(12個)")
row = col.row(); row.prop(props, "show_sub_torus_1", text="小トーラス"); row.prop(props, "show_sub_torus_2", text="小トーラス")
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_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_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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): pass
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.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():
if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props; nodes = bpy.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, bpy.context)
return None
# --- 登録/解除 ---
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties, BaseCircleProperties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_GenerateBaseCircleData,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_BaseCirclePanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, 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: pass
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.base_circle_props = PointerProperty(type=BaseCircleProperties)
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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', 'base_circle_props']:
if hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except: pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except: pass
_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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei_torus_{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & Base Circle]",
"author": "zionadchat",
"version": (36, 8, 0),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / 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)),]
CAMERA_COLLECTION_NAME = "Cam"
BASE_CIRCLE_COLLECTION = "基準円"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
{"label": "単位円 カメラアイ 20260326", "url": "<https://www.notion.so/20260326-32ef5dacaf438072801cf561b3790fed>"},
]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},]
SOCIAL_LINKS = [{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},]
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "BASE_CIRCLE": f"{PREFIX}_PT_base_circle",
"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",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["BASE_CIRCLE"]: 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["INFO"]: 7, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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 create_torus_bmesh(bm, major_radius, minor_radius, major_segments, minor_segments):
rings = []
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = major_radius * math.cos(t)
y = major_radius * math.sin(t)
p = mathutils.Vector((x, y, 0))
n = mathutils.Vector((math.cos(t), math.sin(t), 0)).normalized()
up = mathutils.Vector((0, 0, 1))
ring = []
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges = []
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def update_base_visibility(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col: return
props = context.scene.base_circle_props
for obj in col.objects:
hide = False
name = obj.name
if name.startswith("Base_Circle_R1"): hide = not props.show_base_circle_1
elif name.startswith("Base_Circle_R2"): hide = not props.show_base_circle_2
elif name.startswith("Torus_Main_R1"): hide = not props.show_main_torus_1
elif name.startswith("Torus_Main_R2"): hide = not props.show_main_torus_2
elif name.startswith("半径1_"): hide = not props.show_spheres_1
elif name.startswith("半径2_"): hide = not props.show_spheres_2
elif name.startswith("SubTorus_R1_"): hide = not props.show_sub_torus_1
elif name.startswith("SubTorus_R2_"): hide = not props.show_sub_torus_2
obj.hide_viewport = hide
obj.hide_render = hide
class BaseCircleProperties(PropertyGroup):
radius_1: FloatProperty(name="半径 1", default=1.0, min=0.01)
radius_2: FloatProperty(name="半径 2", default=2.0, min=0.01)
color_1: FloatVectorProperty(name="色 1", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.2, 0.2, 1.0))
color_2: FloatVectorProperty(name="色 2", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.5, 1.0, 1.0))
show_base_circle_1: BoolProperty(name="基準円1", default=True, update=update_base_visibility)
show_base_circle_2: BoolProperty(name="基準円2", default=True, update=update_base_visibility)
show_main_torus_1: BoolProperty(name="トーラス1", default=True, update=update_base_visibility)
show_main_torus_2: BoolProperty(name="トーラス2", default=True, update=update_base_visibility)
show_spheres_1: BoolProperty(name="球体1", default=True, update=update_base_visibility)
show_spheres_2: BoolProperty(name="球体2", default=True, update=update_base_visibility)
show_sub_torus_1: BoolProperty(name="小トーラス1", default=True, update=update_base_visibility)
show_sub_torus_2: BoolProperty(name="小トーラス2", default=True, update=update_base_visibility)
def 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
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
class SFC_OT_GenerateBaseCircleData(Operator):
bl_idname = f"{PREFIX}.generate_base_circle_data"
bl_label = "基準円・トーラスを生成 / 更新"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col:
col = bpy.data.collections.new(BASE_CIRCLE_COLLECTION)
context.scene.collection.children.link(col)
prefix_list = ["Base_Circle_R", "Torus_Main_R", "半径1_", "半径2_", "SubTorus_R"]
objs_to_remove = [obj for obj in col.objects if any(obj.name.startswith(p) for p in prefix_list)]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
props = context.scene.base_circle_props
mat_r1 = create_color_material("Mat_Radius1", props.color_1)
mat_r2 = create_color_material("Mat_Radius2", props.color_2)
def make_mesh(name, bm, mat=None):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
if mat: obj.data.materials.append(mat)
col.objects.link(obj)
return obj
for radius, mat, r_name in [(props.radius_1, mat_r1, "1"), (props.radius_2, mat_r2, "2")]:
if radius <= 0: continue
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
make_mesh(f"Base_Circle_R{r_name}", bm, mat)
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=radius, minor_radius=0.04, major_segments=64, minor_segments=16)
make_mesh(f"Torus_Main_R{r_name}", bm, mat)
for i in range(12):
angle_deg = i * 30
angle_rad = math.radians(angle_deg)
name_suffix = f"{angle_deg:03d}度"
x = math.cos(angle_rad) * radius
y = math.sin(angle_rad) * radius
loc = mathutils.Vector((x, y, 0))
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.08)
obj_sph = make_mesh(f"半径{r_name}_{name_suffix}", bm, mat)
obj_sph.location = loc
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=0.15, minor_radius=0.02, major_segments=32, minor_segments=8)
obj_sub = make_mesh(f"SubTorus_R{r_name}_{name_suffix}", bm, mat)
obj_sub.location = loc
if loc.length > 0.001:
rot_quat = (-loc).to_track_quat('Z', 'Y')
obj_sub.rotation_euler = rot_quat.to_euler()
update_base_visibility(context.scene.base_circle_props, context)
self.report({'INFO'}, "基準円とトーラス・球体を生成/更新しました")
return {'FINISHED'}
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context, index):
obj = self.camera_obj
if obj and obj.name == f"Fixed_Cam_{index}":
color = getattr(self, f"cam{index}_color")
context.preferences.themes[0].view_3d.camera = 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))
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))
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()
def update_viewport_cam(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
vp_loc = mathutils.Vector(self.viewport_location)
vp_tgt = mathutils.Vector(self.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 area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
_is_updating_by_addon = True
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
_is_updating_by_addon = False
break
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
# --- カメラ設定パネル (各カメラの初期値) ---
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, 0.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, 100.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=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
cam1_color: FloatVectorProperty(name="Cam 1 色", 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, 1))
cam2_color: FloatVectorProperty(name="Cam 2 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(1.0, 1.0, 0.0), update=lambda self, context: update_cam_color(self, context, 2))
cam3_color: FloatVectorProperty(name="Cam 3 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.5, 0.0, 0.5), update=lambda self, context: update_cam_color(self, context, 3))
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_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
camera_obj = props.camera_obj
if not camera_obj: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location = camera_obj.location
target_location = get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"
current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = 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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); 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):
col = bpy.data.collections.get(CAMERA_COLLECTION_NAME)
if not col:
col = bpy.data.collections.new(CAMERA_COLLECTION_NAME)
context.scene.collection.children.link(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 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 cam_obj.name in context.scene.collection.objects:
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:
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_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_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 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'
color = getattr(props, f"cam{self.cam_index}_color")
context.preferences.themes[0].view_3d.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)
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; 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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = f"焦点距離: {props.info_focal_length}\nカメラ位置: {props.info_camera_location}\n注視点: {props.info_target_location}"; 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
box_init = layout.box()
box_init.label(text="各カメラの初期値設定:", icon='PREFERENCES')
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 = layout.box()
row_ops = box.row()
row_ops.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成")
row_ops.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
box.separator()
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.label(text="カメラ枠線 色設定:", icon='COLOR')
col_c = box_color.column(align=True)
col_c.prop(props, "cam1_color", text="Cam 1")
col_c.prop(props, "cam2_color", text="Cam 2")
col_c.prop(props, "cam3_color", text="Cam 3")
class SFC_PT_BaseCirclePanel(Panel):
bl_label = "2. 基準円・トーラス生成"; bl_idname = PANEL_IDS["BASE_CIRCLE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["BASE_CIRCLE"]]
def draw(self, context):
layout = self.layout
props = context.scene.base_circle_props
box_param = layout.box()
box_param.label(text="パラメータ設定", icon='PREFERENCES')
col_param = box_param.column(align=True)
row_p = col_param.row(); row_p.prop(props, "radius_1"); row_p.prop(props, "radius_2")
row_c = col_param.row(); row_c.prop(props, "color_1", text=""); row_c.prop(props, "color_2", text="")
layout.separator()
layout.operator(SFC_OT_GenerateBaseCircleData.bl_idname, icon='MESH_TORUS', text="基準円・トーラスを生成 / 更新")
box = layout.box()
box.label(text="表示 / 非表示", icon='HIDE_OFF')
col = box.column(align=True)
row = col.row(); row.label(text="[グループ 1]"); row.label(text="[グループ 2]")
row = col.row(); row.prop(props, "show_base_circle_1", text="基準円"); row.prop(props, "show_base_circle_2", text="基準円")
row = col.row(); row.prop(props, "show_main_torus_1", text="大トーラス"); row.prop(props, "show_main_torus_2", text="大トーラス")
row = col.row(); row.prop(props, "show_spheres_1", text="球体(12個)"); row.prop(props, "show_spheres_2", text="球体(12個)")
row = col.row(); row.prop(props, "show_sub_torus_1", text="小トーラス"); row.prop(props, "show_sub_torus_2", text="小トーラス")
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_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_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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): pass
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.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():
if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props; nodes = bpy.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, bpy.context)
return None
# --- 登録/解除 ---
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties, BaseCircleProperties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_GenerateBaseCircleData,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_BaseCirclePanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, 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: pass
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.base_circle_props = PointerProperty(type=BaseCircleProperties)
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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', 'base_circle_props']:
if hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except: pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except: pass
_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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei_torus_{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & Base Circle]",
"author": "zionadchat",
"version": (36, 7, 0),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラの回転(注視点)管理と、ビューポート透視投影の視座位置コントロール",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / 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)),]
CAMERA_COLLECTION_NAME = "Cam"
BASE_CIRCLE_COLLECTION = "基準円"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
{"label": "単位円 カメラアイ 20260326", "url": "<https://www.notion.so/20260326-32ef5dacaf438072801cf561b3790fed>"},
]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},]
SOCIAL_LINKS = [{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},]
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "BASE_CIRCLE": f"{PREFIX}_PT_base_circle",
"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",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["BASE_CIRCLE"]: 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["INFO"]: 7, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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 create_torus_bmesh(bm, major_radius, minor_radius, major_segments, minor_segments):
rings = []
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = major_radius * math.cos(t)
y = major_radius * math.sin(t)
p = mathutils.Vector((x, y, 0))
n = mathutils.Vector((math.cos(t), math.sin(t), 0)).normalized()
up = mathutils.Vector((0, 0, 1))
ring = []
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges = []
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def update_base_visibility(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col: return
props = context.scene.base_circle_props
for obj in col.objects:
hide = False
name = obj.name
if name.startswith("Base_Circle_R1"): hide = not props.show_base_circle_1
elif name.startswith("Base_Circle_R2"): hide = not props.show_base_circle_2
elif name.startswith("Torus_Main_R1"): hide = not props.show_main_torus_1
elif name.startswith("Torus_Main_R2"): hide = not props.show_main_torus_2
elif name.startswith("半径1_"): hide = not props.show_spheres_1
elif name.startswith("半径2_"): hide = not props.show_spheres_2
elif name.startswith("SubTorus_R1_"): hide = not props.show_sub_torus_1
elif name.startswith("SubTorus_R2_"): hide = not props.show_sub_torus_2
obj.hide_viewport = hide
obj.hide_render = hide
class BaseCircleProperties(PropertyGroup):
radius_1: FloatProperty(name="半径 1", default=1.0, min=0.01)
radius_2: FloatProperty(name="半径 2", default=2.0, min=0.01)
color_1: FloatVectorProperty(name="色 1", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.2, 0.2, 1.0))
color_2: FloatVectorProperty(name="色 2", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.5, 1.0, 1.0))
show_base_circle_1: BoolProperty(name="基準円1", default=True, update=update_base_visibility)
show_base_circle_2: BoolProperty(name="基準円2", default=True, update=update_base_visibility)
show_main_torus_1: BoolProperty(name="トーラス1", default=True, update=update_base_visibility)
show_main_torus_2: BoolProperty(name="トーラス2", default=True, update=update_base_visibility)
show_spheres_1: BoolProperty(name="球体1", default=True, update=update_base_visibility)
show_spheres_2: BoolProperty(name="球体2", default=True, update=update_base_visibility)
show_sub_torus_1: BoolProperty(name="小トーラス1", default=True, update=update_base_visibility)
show_sub_torus_2: BoolProperty(name="小トーラス2", default=True, update=update_base_visibility)
def 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
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
class SFC_OT_GenerateBaseCircleData(Operator):
bl_idname = f"{PREFIX}.generate_base_circle_data"
bl_label = "基準円・トーラスを生成 / 更新"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col:
col = bpy.data.collections.new(BASE_CIRCLE_COLLECTION)
context.scene.collection.children.link(col)
prefix_list = ["Base_Circle_R", "Torus_Main_R", "半径1_", "半径2_", "SubTorus_R"]
objs_to_remove = [obj for obj in col.objects if any(obj.name.startswith(p) for p in prefix_list)]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
props = context.scene.base_circle_props
mat_r1 = create_color_material("Mat_Radius1", props.color_1)
mat_r2 = create_color_material("Mat_Radius2", props.color_2)
def make_mesh(name, bm, mat=None):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
if mat: obj.data.materials.append(mat)
col.objects.link(obj)
return obj
for radius, mat, r_name in [(props.radius_1, mat_r1, "1"), (props.radius_2, mat_r2, "2")]:
if radius <= 0: continue
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
make_mesh(f"Base_Circle_R{r_name}", bm, mat)
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=radius, minor_radius=0.04, major_segments=64, minor_segments=16)
make_mesh(f"Torus_Main_R{r_name}", bm, mat)
for i in range(12):
angle_deg = i * 30
angle_rad = math.radians(angle_deg)
name_suffix = f"{angle_deg:03d}度"
x = math.cos(angle_rad) * radius
y = math.sin(angle_rad) * radius
loc = mathutils.Vector((x, y, 0))
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.08)
obj_sph = make_mesh(f"半径{r_name}_{name_suffix}", bm, mat)
obj_sph.location = loc
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=0.15, minor_radius=0.02, major_segments=32, minor_segments=8)
obj_sub = make_mesh(f"SubTorus_R{r_name}_{name_suffix}", bm, mat)
obj_sub.location = loc
if loc.length > 0.001:
rot_quat = (-loc).to_track_quat('Z', 'Y')
obj_sub.rotation_euler = rot_quat.to_euler()
update_base_visibility(context.scene.base_circle_props, context)
self.report({'INFO'}, "基準円とトーラス・球体を生成/更新しました")
return {'FINISHED'}
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context, index):
obj = self.camera_obj
if obj and obj.name == f"Fixed_Cam_{index}":
color = getattr(self, f"cam{index}_color")
context.preferences.themes[0].view_3d.camera = 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))
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))
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()
# ★ ビューポート架空カメラ用 プロパティ追加
def update_viewport_cam(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
vp_loc = mathutils.Vector(self.viewport_location)
vp_tgt = mathutils.Vector(self.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 area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
_is_updating_by_addon = True
# ビューポートカメラから抜ける
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
_is_updating_by_addon = False
break
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
# ★ 実際のカメラの "位置" は削除し、注視点とオフセットのみで回転制御
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
# ★ ビューポートの視座・注視点
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
cam1_color: FloatVectorProperty(name="Cam 1 色", 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, 1))
cam2_color: FloatVectorProperty(name="Cam 2 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(1.0, 1.0, 0.0), update=lambda self, context: update_cam_color(self, context, 2))
cam3_color: FloatVectorProperty(name="Cam 3 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.5, 0.0, 0.5), update=lambda self, context: update_cam_color(self, context, 3))
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_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
camera_obj = props.camera_obj
if not camera_obj: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location = camera_obj.location
target_location = get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"
current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = 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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); 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):
col = bpy.data.collections.get(CAMERA_COLLECTION_NAME)
if not col:
col = bpy.data.collections.new(CAMERA_COLLECTION_NAME)
context.scene.collection.children.link(col)
configs = [
(1, (0.0, 0.0, 0.0), (0.0, 100.0, 0.0)),
(2, (0.0, -10.0, 0.0), (0.0, 0.0, 0.0)),
(3, (0.0, 0.0, 100.0), (0.0, 0.0, 0.0)),
]
for idx, loc, tgt in configs:
name = f"Fixed_Cam_{idx}"
cam_obj = bpy.data.objects.get(name)
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 cam_obj.name in context.scene.collection.objects:
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_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 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
# 3Dビューをアクティブカメラ視点に切り替える
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'
color = getattr(props, f"cam{self.cam_index}_color")
context.preferences.themes[0].view_3d.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
# 注視点の再計算 (Z軸方向の前方100mの位置を初期注視点とする)
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)
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; 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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = f"焦点距離: {props.info_focal_length}\nカメラ位置: {props.info_camera_location}\n注視点: {props.info_target_location}"; 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
box = layout.box()
box.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
box.separator()
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.label(text="カメラ枠線 色設定 (操作時に自動反映):", icon='COLOR')
col_c = box_color.column(align=True)
col_c.prop(props, "cam1_color", text="Cam 1")
col_c.prop(props, "cam2_color", text="Cam 2")
col_c.prop(props, "cam3_color", text="Cam 3")
class SFC_PT_BaseCirclePanel(Panel):
bl_label = "2. 基準円・トーラス生成"; bl_idname = PANEL_IDS["BASE_CIRCLE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["BASE_CIRCLE"]]
def draw(self, context):
layout = self.layout
props = context.scene.base_circle_props
box_param = layout.box()
box_param.label(text="パラメータ設定", icon='PREFERENCES')
col_param = box_param.column(align=True)
row_p = col_param.row(); row_p.prop(props, "radius_1"); row_p.prop(props, "radius_2")
row_c = col_param.row(); row_c.prop(props, "color_1", text=""); row_c.prop(props, "color_2", text="")
layout.separator()
layout.operator(SFC_OT_GenerateBaseCircleData.bl_idname, icon='MESH_TORUS', text="基準円・トーラスを生成 / 更新")
box = layout.box()
box.label(text="表示 / 非表示", icon='HIDE_OFF')
col = box.column(align=True)
row = col.row(); row.label(text="[グループ 1]"); row.label(text="[グループ 2]")
row = col.row(); row.prop(props, "show_base_circle_1", text="基準円"); row.prop(props, "show_base_circle_2", text="基準円")
row = col.row(); row.prop(props, "show_main_torus_1", text="大トーラス"); row.prop(props, "show_main_torus_2", text="大トーラス")
row = col.row(); row.prop(props, "show_spheres_1", text="球体(12個)"); row.prop(props, "show_spheres_2", text="球体(12個)")
row = col.row(); row.prop(props, "show_sub_torus_1", text="小トーラス"); row.prop(props, "show_sub_torus_2", text="小トーラス")
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")
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_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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): pass
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.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():
if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props; nodes = bpy.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, bpy.context)
return None
# --- 登録/解除 ---
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties, BaseCircleProperties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_GenerateBaseCircleData,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_BaseCirclePanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, 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: pass
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.base_circle_props = PointerProperty(type=BaseCircleProperties)
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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', 'base_circle_props']:
if hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except: pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except: pass
_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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei_torus_{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & Base Circle]",
"author": "zionadchat",
"version": (36, 6, 0),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラの独立管理(同期・描画カメラ自動切替)と、基準円・トーラスの生成機能",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / 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)),]
CAMERA_COLLECTION_NAME = "Cam"
BASE_CIRCLE_COLLECTION = "基準円"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
{"label": "単位円 カメラアイ 20260326", "url": "<https://www.notion.so/20260326-32ef5dacaf438072801cf561b3790fed>"},
]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},]
SOCIAL_LINKS = [{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},]
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "BASE_CIRCLE": f"{PREFIX}_PT_base_circle", "POSITION_AIM": f"{PREFIX}_PT_position_aim",
"LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["BASE_CIRCLE"]: 1, PANEL_IDS["POSITION_AIM"]: 2, PANEL_IDS["LENS"]: 3, PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, PANEL_IDS["INFO"]: 6, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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 create_torus_bmesh(bm, major_radius, minor_radius, major_segments, minor_segments):
rings = []
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = major_radius * math.cos(t)
y = major_radius * math.sin(t)
p = mathutils.Vector((x, y, 0))
n = mathutils.Vector((math.cos(t), math.sin(t), 0)).normalized()
up = mathutils.Vector((0, 0, 1))
ring = []
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges = []
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def update_base_visibility(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col: return
props = context.scene.base_circle_props
for obj in col.objects:
hide = False
name = obj.name
if name.startswith("Base_Circle_R1"): hide = not props.show_base_circle_1
elif name.startswith("Base_Circle_R2"): hide = not props.show_base_circle_2
elif name.startswith("Torus_Main_R1"): hide = not props.show_main_torus_1
elif name.startswith("Torus_Main_R2"): hide = not props.show_main_torus_2
elif name.startswith("半径1_"): hide = not props.show_spheres_1
elif name.startswith("半径2_"): hide = not props.show_spheres_2
elif name.startswith("SubTorus_R1_"): hide = not props.show_sub_torus_1
elif name.startswith("SubTorus_R2_"): hide = not props.show_sub_torus_2
obj.hide_viewport = hide
obj.hide_render = hide
class BaseCircleProperties(PropertyGroup):
radius_1: FloatProperty(name="半径 1", default=1.0, min=0.01)
radius_2: FloatProperty(name="半径 2", default=2.0, min=0.01)
color_1: FloatVectorProperty(name="色 1", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.2, 0.2, 1.0))
color_2: FloatVectorProperty(name="色 2", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.5, 1.0, 1.0))
show_base_circle_1: BoolProperty(name="基準円1", default=True, update=update_base_visibility)
show_base_circle_2: BoolProperty(name="基準円2", default=True, update=update_base_visibility)
show_main_torus_1: BoolProperty(name="トーラス1", default=True, update=update_base_visibility)
show_main_torus_2: BoolProperty(name="トーラス2", default=True, update=update_base_visibility)
show_spheres_1: BoolProperty(name="球体1", default=True, update=update_base_visibility)
show_spheres_2: BoolProperty(name="球体2", default=True, update=update_base_visibility)
show_sub_torus_1: BoolProperty(name="小トーラス1", default=True, update=update_base_visibility)
show_sub_torus_2: BoolProperty(name="小トーラス2", default=True, update=update_base_visibility)
def 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
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
class SFC_OT_GenerateBaseCircleData(Operator):
bl_idname = f"{PREFIX}.generate_base_circle_data"
bl_label = "基準円・トーラスを生成 / 更新"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col:
col = bpy.data.collections.new(BASE_CIRCLE_COLLECTION)
context.scene.collection.children.link(col)
prefix_list = ["Base_Circle_R", "Torus_Main_R", "半径1_", "半径2_", "SubTorus_R"]
objs_to_remove = [obj for obj in col.objects if any(obj.name.startswith(p) for p in prefix_list)]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
props = context.scene.base_circle_props
mat_r1 = create_color_material("Mat_Radius1", props.color_1)
mat_r2 = create_color_material("Mat_Radius2", props.color_2)
def make_mesh(name, bm, mat=None):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
if mat: obj.data.materials.append(mat)
col.objects.link(obj)
return obj
for radius, mat, r_name in [(props.radius_1, mat_r1, "1"), (props.radius_2, mat_r2, "2")]:
if radius <= 0: continue
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
make_mesh(f"Base_Circle_R{r_name}", bm, mat)
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=radius, minor_radius=0.04, major_segments=64, minor_segments=16)
make_mesh(f"Torus_Main_R{r_name}", bm, mat)
for i in range(12):
angle_deg = i * 30
angle_rad = math.radians(angle_deg)
name_suffix = f"{angle_deg:03d}度"
x = math.cos(angle_rad) * radius
y = math.sin(angle_rad) * radius
loc = mathutils.Vector((x, y, 0))
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.08)
obj_sph = make_mesh(f"半径{r_name}_{name_suffix}", bm, mat)
obj_sph.location = loc
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=0.15, minor_radius=0.02, major_segments=32, minor_segments=8)
obj_sub = make_mesh(f"SubTorus_R{r_name}_{name_suffix}", bm, mat)
obj_sub.location = loc
if loc.length > 0.001:
rot_quat = (-loc).to_track_quat('Z', 'Y')
obj_sub.rotation_euler = rot_quat.to_euler()
update_base_visibility(context.scene.base_circle_props, context)
self.report({'INFO'}, "基準円とトーラス・球体を生成/更新しました")
return {'FINISHED'}
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context, index):
obj = self.camera_obj
if obj and obj.name == f"Fixed_Cam_{index}":
color = getattr(self, f"cam{index}_color")
context.preferences.themes[0].view_3d.camera = 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))
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))
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()
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
fixed_location: FloatVectorProperty(name="固定位置", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
cam1_color: FloatVectorProperty(name="Cam 1 色", 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, 1))
cam2_color: FloatVectorProperty(name="Cam 2 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(1.0, 1.0, 0.0), update=lambda self, context: update_cam_color(self, context, 2))
cam3_color: FloatVectorProperty(name="Cam 3 色", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.5, 0.0, 0.5), update=lambda self, context: update_cam_color(self, context, 3))
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 = mathutils.Vector(props.fixed_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.location = location; obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location, target_location = mathutils.Vector(props.fixed_location), get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"
current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props.fixed_location = obj.location
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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); 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):
col = bpy.data.collections.get(CAMERA_COLLECTION_NAME)
if not col:
col = bpy.data.collections.new(CAMERA_COLLECTION_NAME)
context.scene.collection.children.link(col)
configs = [
(1, (0.0, 0.0, 0.0), (0.0, 100.0, 0.0)),
(2, (0.0, -10.0, 0.0), (0.0, 0.0, 0.0)),
(3, (0.0, 0.0, 100.0), (0.0, 0.0, 0.0)),
]
for idx, loc, tgt in configs:
name = f"Fixed_Cam_{idx}"
cam_obj = bpy.data.objects.get(name)
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 cam_obj.name in context.scene.collection.objects:
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_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 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
# 3Dビューをカメラ視点(透視投影)に切り替える
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'
color = getattr(props, f"cam{self.cam_index}_color")
context.preferences.themes[0].view_3d.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
props.is_updating_settings = False
sync_ui_from_manual_transform(props, cam_obj, context)
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 = {"location": ["fixed_location"],"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; 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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = f"焦点距離: {props.info_focal_length}\nカメラ位置: {props.info_camera_location}\n注視点: {props.info_target_location}"; 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
box = layout.box()
box.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
box.separator()
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.label(text="カメラ枠線 色設定 (操作時に自動反映):", icon='COLOR')
col_c = box_color.column(align=True)
col_c.prop(props, "cam1_color", text="Cam 1")
col_c.prop(props, "cam2_color", text="Cam 2")
col_c.prop(props, "cam3_color", text="Cam 3")
class SFC_PT_BaseCirclePanel(Panel):
bl_label = "2. 基準円・トーラス生成"; bl_idname = PANEL_IDS["BASE_CIRCLE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["BASE_CIRCLE"]]
def draw(self, context):
layout = self.layout
props = context.scene.base_circle_props
box_param = layout.box()
box_param.label(text="パラメータ設定", icon='PREFERENCES')
col_param = box_param.column(align=True)
row_p = col_param.row(); row_p.prop(props, "radius_1"); row_p.prop(props, "radius_2")
row_c = col_param.row(); row_c.prop(props, "color_1", text=""); row_c.prop(props, "color_2", text="")
layout.separator()
layout.operator(SFC_OT_GenerateBaseCircleData.bl_idname, icon='MESH_TORUS', text="基準円・トーラスを生成 / 更新")
box = layout.box()
box.label(text="表示 / 非表示", icon='HIDE_OFF')
col = box.column(align=True)
row = col.row(); row.label(text="[グループ 1]"); row.label(text="[グループ 2]")
row = col.row(); row.prop(props, "show_base_circle_1", text="基準円"); row.prop(props, "show_base_circle_2", text="基準円")
row = col.row(); row.prop(props, "show_main_torus_1", text="大トーラス"); row.prop(props, "show_main_torus_2", text="大トーラス")
row = col.row(); row.prop(props, "show_spheres_1", text="球体(12個)"); row.prop(props, "show_spheres_2", text="球体(12個)")
row = col.row(); row.prop(props, "show_sub_torus_1", text="小トーラス"); row.prop(props, "show_sub_torus_2", text="小トーラス")
class SFC_PT_CameraPositionAimPanel(Panel):
bl_label = "3. カメラ位置 & 視線制御"
bl_idname = PANEL_IDS["POSITION_AIM"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["POSITION_AIM"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box_manual = layout.box()
box_manual.label(text="手動コントロール", icon='MOUSE_LMB')
col_loc = box_manual.column(align=True)
row_loc = col_loc.row(align=True)
row_loc.label(text="固定位置")
op_loc = row_loc.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_loc.targets.add().name = "location"
op_loc.prop_group_name = "camera"
col_loc.prop(props, "fixed_location", text="")
box_manual.separator()
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_LensPanel(Panel):
bl_label = "4. レンズ設定"; 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_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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): pass
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.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():
if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props; nodes = bpy.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, bpy.context)
return None
# --- 登録/解除 ---
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties, BaseCircleProperties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_GenerateBaseCircleData,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_BaseCirclePanel, SFC_PT_CameraPositionAimPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, 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: pass
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.base_circle_props = PointerProperty(type=BaseCircleProperties)
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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', 'base_circle_props']:
if hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except: pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except: pass
_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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei_torus_{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & Base Circle]",
"author": "zionadchat",
"version": (36, 0, 1),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "カメラの位置固定制御に加え、基準円とトーラス・球体の生成機能を提供します。",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / 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)),]
CAMERA_COLLECTION_NAME = "Cam"
DEDICATED_CAMERA_NAME = "Fixed_Cam"
BASE_CIRCLE_COLLECTION = "基準円"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
CAMERA_COLOR_PRESETS = [("CYAN", "Cyan", "水色", (0.0, 1.0, 1.0)), ("Cam 4.4.0", "Cam 4.4.0", "Blenderデフォルト色", (0.0, 0.0, 0.0)), ("YELLOW", "Yellow", "黄色", (1.0, 1.0, 0.0)), ("PURPLE", "Purple", "紫色", (0.5, 0.0, 0.5)),]
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
{"label": "単位円 カメラアイ 20260326", "url": "<https://www.notion.so/20260326-32ef5dacaf438072801cf561b3790fed>"},
]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},]
SOCIAL_LINKS = [{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},]
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "BASE_CIRCLE": f"{PREFIX}_PT_base_circle", "POSITION": f"{PREFIX}_PT_position", "AIMING": f"{PREFIX}_PT_aiming",
"LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["BASE_CIRCLE"]: 1, PANEL_IDS["POSITION"]: 2, PANEL_IDS["AIMING"]: 3, PANEL_IDS["LENS"]: 4, PANEL_IDS["CAMERA_DISPLAY"]: 5, PANEL_IDS["WORLD_CONTROL"]: 6, PANEL_IDS["INFO"]: 7, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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 create_torus_bmesh(bm, major_radius, minor_radius, major_segments, minor_segments):
""" BMesh内にトーラスの形状を直接生成する関数 """
rings = []
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = major_radius * math.cos(t)
y = major_radius * math.sin(t)
p = mathutils.Vector((x, y, 0))
# 法線方向
n = mathutils.Vector((math.cos(t), math.sin(t), 0)).normalized()
up = mathutils.Vector((0, 0, 1))
ring = []
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges = []
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def update_base_visibility(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col: return
props = context.scene.base_circle_props
for obj in col.objects:
hide = False
name = obj.name
if name.startswith("Base_Circle_R1"): hide = not props.show_base_circle_1
elif name.startswith("Base_Circle_R2"): hide = not props.show_base_circle_2
elif name.startswith("Torus_Main_R1"): hide = not props.show_main_torus_1
elif name.startswith("Torus_Main_R2"): hide = not props.show_main_torus_2
elif name.startswith("半径1_"): hide = not props.show_spheres_1
elif name.startswith("半径2_"): hide = not props.show_spheres_2
elif name.startswith("SubTorus_R1_"): hide = not props.show_sub_torus_1
elif name.startswith("SubTorus_R2_"): hide = not props.show_sub_torus_2
obj.hide_viewport = hide
obj.hide_render = hide
class BaseCircleProperties(PropertyGroup):
show_base_circle_1: BoolProperty(name="基準円1", default=True, update=update_base_visibility)
show_base_circle_2: BoolProperty(name="基準円2", default=True, update=update_base_visibility)
show_main_torus_1: BoolProperty(name="トーラス1", default=True, update=update_base_visibility)
show_main_torus_2: BoolProperty(name="トーラス2", default=True, update=update_base_visibility)
show_spheres_1: BoolProperty(name="球体1", default=True, update=update_base_visibility)
show_spheres_2: BoolProperty(name="球体2", default=True, update=update_base_visibility)
show_sub_torus_1: BoolProperty(name="小トーラス1", default=True, update=update_base_visibility)
show_sub_torus_2: BoolProperty(name="小トーラス2", default=True, update=update_base_visibility)
def 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 = mat.node_tree.nodes.get("Principled BSDF")
if bsdf and "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
return mat
class SFC_OT_GenerateBaseCircleData(Operator):
bl_idname = f"{PREFIX}.generate_base_circle_data"
bl_label = "基準円・トーラス・球体を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(BASE_CIRCLE_COLLECTION)
if not col:
col = bpy.data.collections.new(BASE_CIRCLE_COLLECTION)
context.scene.collection.children.link(col)
# 既存の対象オブジェクトをクリア
prefix_list = ["Base_Circle_R", "Torus_Main_R", "半径1_", "半径2_", "SubTorus_R"]
objs_to_remove = [obj for obj in col.objects if any(obj.name.startswith(p) for p in prefix_list)]
for obj in objs_to_remove:
bpy.data.objects.remove(obj, do_unlink=True)
mat_r1 = create_color_material("Mat_Radius1", (1.0, 0.2, 0.2, 1.0)) # 赤系
mat_r2 = create_color_material("Mat_Radius2", (0.2, 0.5, 1.0, 1.0)) # 青系
def make_mesh(name, bm, mat=None):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
# スムースシェードの適用
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me)
if mat: obj.data.materials.append(mat)
col.objects.link(obj)
return obj
for radius, mat, r_name in [(1.0, mat_r1, "1"), (2.0, mat_r2, "2")]:
# 1. 基準円 (Edgeのみ)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
make_mesh(f"Base_Circle_R{r_name}", bm, mat)
# 2. メイントーラス
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=radius, minor_radius=0.04, major_segments=64, minor_segments=16)
make_mesh(f"Torus_Main_R{r_name}", bm, mat)
# 3. 12個の球体と小型トーラス
for i in range(12):
angle_deg = i * 30
angle_rad = math.radians(angle_deg)
name_suffix = f"{angle_deg:03d}度"
# X=100が0度, Y=100が90度となる極座標
x = math.cos(angle_rad) * radius
y = math.sin(angle_rad) * radius
loc = mathutils.Vector((x, y, 0))
# 球体
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.08)
obj_sph = make_mesh(f"半径{r_name}_{name_suffix}", bm, mat)
obj_sph.location = loc
# 小型トーラス
bm = bmesh.new()
create_torus_bmesh(bm, major_radius=0.15, minor_radius=0.02, major_segments=32, minor_segments=8)
obj_sub = make_mesh(f"SubTorus_R{r_name}_{name_suffix}", bm, mat)
obj_sub.location = loc
# 小型トーラスの法線(Z)を原点(0,0,0)に向ける
if loc.length > 0.001:
rot_quat = (-loc).to_track_quat('Z', 'Y')
obj_sub.rotation_euler = rot_quat.to_euler()
# 現在のトグル状態を反映
update_base_visibility(context.scene.base_circle_props, context)
self.report({'INFO'}, "基準円とトーラス・球体を生成しました")
return {'FINISHED'}
# --- プロパティグループ ---
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))
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))
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()
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
# ★ カメラ位置の初期値を (0, 0, 0)、注視点を (0, 100, 0) に変更
fixed_location: FloatVectorProperty(name="固定位置", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
camera_color: FloatVectorProperty(name="カメラカラー", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.0, 1.0, 1.0))
camera_preset: EnumProperty(name="カメラプリセット", items=[(p[0], p[1], p[2]) for p in CAMERA_COLOR_PRESETS], default="CYAN", update=lambda self, context: SFC_OT_ApplyCameraColor.update_preset(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 = mathutils.Vector(props.fixed_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.location = location; obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location, target_location = mathutils.Vector(props.fixed_location), get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"
current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props.fixed_location = obj.location
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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); return
# --- オペレーター ---
class SFC_OT_ApplyCameraColor(Operator):
bl_idname = f"{PREFIX}.apply_camera_color"; bl_label = "カメラカラー適用"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): context.preferences.themes[0].view_3d.camera = context.scene.surface_camera_properties.camera_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context): props = context.scene.surface_camera_properties; props.camera_color = next((p[3] for p in CAMERA_COLOR_PRESETS if p[0] == props.camera_preset), props.camera_color); getattr(bpy.ops, f"{PREFIX}.apply_camera_color")()
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_CreateDedicatedCamera(Operator):
bl_idname = f"{PREFIX}.create_dedicated_camera"; bl_label = "専用カメラ作成"
def execute(self, context):
if DEDICATED_CAMERA_NAME not in bpy.data.objects:
cam_data = bpy.data.cameras.new(name=DEDICATED_CAMERA_NAME); cam_obj = bpy.data.objects.new(DEDICATED_CAMERA_NAME, cam_data)
cam_collection = bpy.data.collections.get(CAMERA_COLLECTION_NAME) or bpy.data.collections.new(CAMERA_COLLECTION_NAME)
if CAMERA_COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(cam_collection)
cam_collection.objects.link(cam_obj)
if cam_obj.name in context.scene.collection.objects: context.scene.collection.objects.unlink(cam_obj)
else: cam_obj = bpy.data.objects[DEDICATED_CAMERA_NAME]
props = context.scene.surface_camera_properties; props.camera_obj = cam_obj; props.is_updating_settings = True
for key in props.bl_rna.properties.keys():
if key not in ['camera_obj', 'bl_rna', 'is_updating_settings'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
props.is_updating_settings = False; update_surface_camera(props, context); self.report({'INFO'}, f"カメラ '{DEDICATED_CAMERA_NAME}' を初期化しました。"); return {'FINISHED'}
class SFC_OT_SyncWithCamera(Operator):
bl_idname = f"{PREFIX}.sync_with_camera"; bl_label = "UIを同期"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj or cam_obj.type != 'CAMERA': return {'CANCELLED'}
context.scene.camera = cam_obj; cam_data = cam_obj.data; props.is_updating_settings = True
props.lens_focal_length, props.clip_start, props.clip_end = cam_data.lens, cam_data.clip_start, cam_data.clip_end
props.is_updating_settings = False; sync_ui_from_manual_transform(props, cam_obj, context); return {'FINISHED'}
class SFC_OT_UnlinkObject(Operator):
bl_idname = f"{PREFIX}.unlink_object"; bl_label = "解除"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
setattr(props, 'camera_obj', None)
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 = {"location": ["fixed_location"],"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; 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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = f"焦点距離: {props.info_focal_length}\nカメラ位置: {props.info_camera_location}\n注視点: {props.info_target_location}"; 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 SFC_OT_SetFixedLocationFromView(Operator):
bl_idname = f"{PREFIX}.set_fixed_location_from_view"; bl_label = "現在のカメラ位置をセット"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj: return {'CANCELLED'}
props.fixed_location = cam_obj.location; 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; box = layout.box(); col = box.column(); col.prop(props, "camera_obj", text="カメラ")
if props.camera_obj: row = col.row(align=True); row.operator(f"{PREFIX}.sync_with_camera", icon='UV_SYNC_SELECT'); row.operator(f"{PREFIX}.unlink_object", icon='X')
else: col.label(text="カメラを選択してください", icon='ERROR'); col.operator(f"{PREFIX}.create_dedicated_camera", text=f"'{DEDICATED_CAMERA_NAME}' を作成/選択", icon='ADD')
col.separator(); box.prop(props, "camera_preset", text="色プリセット"); box.prop(props, "camera_color", text="カラー"); box.operator(f"{PREFIX}.apply_camera_color", text="ビューポート色を適用")
class SFC_PT_BaseCirclePanel(Panel):
bl_label = "2. 基準円・トーラス生成"; bl_idname = PANEL_IDS["BASE_CIRCLE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["BASE_CIRCLE"]]
def draw(self, context):
layout = self.layout
props = context.scene.base_circle_props
layout.operator(SFC_OT_GenerateBaseCircleData.bl_idname, icon='MESH_TORUS', text="基準円・トーラスを生成")
box = layout.box()
box.label(text="表示 / 非表示", icon='HIDE_OFF')
col = box.column(align=True)
row = col.row(); row.label(text="[半径 1]"); row.label(text="[半径 2]")
row = col.row(); row.prop(props, "show_base_circle_1", text="基準円"); row.prop(props, "show_base_circle_2", text="基準円")
row = col.row(); row.prop(props, "show_main_torus_1", text="大トーラス"); row.prop(props, "show_main_torus_2", text="大トーラス")
row = col.row(); row.prop(props, "show_spheres_1", text="球体(12個)"); row.prop(props, "show_spheres_2", text="球体(12個)")
row = col.row(); row.prop(props, "show_sub_torus_1", text="小トーラス"); row.prop(props, "show_sub_torus_2", text="小トーラス")
class SFC_PT_PositionPanel(Panel):
bl_label = "3. カメラ位置 (固定)"; bl_idname = PANEL_IDS["POSITION"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["POSITION"]]
def draw(self, context):
layout = self.layout; props = context.scene.surface_camera_properties; 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 = "location"; op.prop_group_name = "camera"
col.prop(props, "fixed_location", text=""); col.operator(f"{PREFIX}.set_fixed_location_from_view", icon='OBJECT_ORIGIN')
class SFC_PT_AimingPanel(Panel):
bl_label = "4. カメラ視線制御"; 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"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, props = self.layout, context.scene.surface_camera_properties
box_aim = layout.box(); col_aim = box_aim.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_offset = layout.box(); col_offset = box_offset.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_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, props = self.layout, context.scene.surface_camera_properties; 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_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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): pass
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.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():
if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props; nodes = bpy.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, bpy.context)
return None
# --- 登録/解除 ---
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties, BaseCircleProperties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, SFC_OT_ApplyCameraColor,
SFC_OT_CreateDedicatedCamera, SFC_OT_SyncWithCamera, SFC_OT_UnlinkObject, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_SetFixedLocationFromView, SFC_OT_GenerateBaseCircleData,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_BaseCirclePanel, SFC_PT_PositionPanel, SFC_PT_AimingPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, 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: pass
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.base_circle_props = PointerProperty(type=BaseCircleProperties)
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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', 'base_circle_props']:
if hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except: pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except: pass
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except: pass
register()