blender まとめ20260821
bl_info = {
"name": "Observer Sky Model with UV Camera",
"author": "AI Assistant",
"version": (2, 2, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations with UV-controlled & Pan-Tilt Camera System.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.00
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.100, 0.400, 0.800, 0.300)
INIT_EARTH_EQUATOR = True
INIT_OUTER_RADIUS = 10.00
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.500, 0.800, 0.800, 0.100)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.020
INIT_OBS_THETA = 0.00
INIT_OBS_SHOW = False
INIT_MARKER_O_SHOW = False
INIT_SKY_RADIUS = 5.00
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.300, 0.100, 0.500, 0.150)
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.00
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.082, 0.063, 0.081, 0.800)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.050
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.00
INIT_ARROW_LAT = 30.00
INIT_ARROW_THICKNESS = 0.500
INIT_ARROW_COLOR = (0.105, 1.000, 0.000, 1.000)
INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.050
INIT_STAR_COLOR = (1.000, 1.000, 0.800, 1.000)
INIT_STAR_EMISSION = 5.00
INIT_STAR_SEED = 42
INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True
INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True
# === メインカメラ用の初期値 ===
INIT_CAM_TARGET = 'T'
INIT_CAM_MODE = 'ORBIT'
# 周回(ORBIT)用
INIT_CAM_RADIUS = 20.00
INIT_CAM_LON = 45.00
INIT_CAM_LAT = 30.00
# 首振り(PAN_TILT)用 (Blenderカメラは初期状態で下を向くため、X90で正面を向くように設定)
INIT_CAM_PITCH = 90.00
INIT_CAM_YAW = 0.00
INIT_CAM_ROLL = 0.00
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
# ======================================================================
# ユーティリティ関数群
# ======================================================================
def get_or_create_collection(name):
if name in bpy.data.collections:
return bpy.data.collections[name]
new_col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(new_col)
return new_col
def safe_remove_collection_contents(col_name):
if col_name not in bpy.data.collections:
return
col = bpy.data.collections[col_name]
for obj in list(col.objects):
# カメラは削除せず再利用することで、スライダー操作中の視点外れを防止
if obj.type == 'CAMERA':
continue
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data:
if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
bpy.data.curves.remove(mesh_data)
def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
principled.inputs['Base Color'].default_value = color
principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
principled.inputs['Emission Strength'].default_value = emission
principled.inputs['Alpha'].default_value = alpha
if is_transparent and alpha < 1.0:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
else:
mat.blend_method = 'OPAQUE'
mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
return mat
def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
bm.to_mesh(mesh)
bm.free()
obj.location = location
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_arrow(name, p1, p2, thickness, mat, collection):
direction = p2 - p1
length = direction.length
if length < 1e-4:
return None
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
head_len = min(thickness * 4.0, length * 0.2)
shaft_len = length - head_len
# Shaft
ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
# Head
ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
z_axis = mathutils.Vector((0, 0, 1))
rot_quat = z_axis.rotation_difference(direction)
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons: poly.use_smooth = True
return obj
def create_stars(name, P, radius, count, size, seed, color, emission, collection):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
obj.data.materials.append(mat)
bm = bmesh.new()
random.seed(seed)
star_positions = []
for _ in range(count):
z = random.uniform(-1.0, 1.0)
phi = random.uniform(0.0, 2.0 * math.pi)
x = math.sqrt(1.0 - z*z) * math.cos(phi)
y = math.sqrt(1.0 - z*z) * math.sin(phi)
pos = P + mathutils.Vector((x, y, z)) * radius
star_positions.append(pos)
geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
bm.to_mesh(mesh)
bm.free()
return obj, star_positions
def create_line(name, p1, p2, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bm.edges.new((bm.verts.new(p1), bm.verts.new(p2)))
bm.to_mesh(mesh)
bm.free()
return obj
def create_circle(name, radius, location, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
bm.to_mesh(mesh)
bm.free()
obj.location = location
return obj
def create_text(name, text, location, collection, scale=1.0, mat=None):
curve = bpy.data.curves.new(type="FONT", name=name)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
obj = bpy.data.objects.new(name, curve)
collection.objects.link(obj)
obj.location = location
obj.scale = (scale, scale, scale)
if mat: obj.data.materials.append(mat)
constraint = obj.constraints.new(type='TRACK_TO')
constraint.track_axis = 'TRACK_Z'
constraint.up_axis = 'UP_Y'
return obj, constraint
def setup_camera(name, location, target_loc, collection, rot_euler=None):
""" カメラが存在すれば再利用し、指定された位置と回転(またはターゲット方向)にセットする """
cam_obj = bpy.data.objects.get(name)
if not cam_obj:
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
collection.objects.link(cam_obj)
cam_obj.location = location
if rot_euler is not None:
cam_obj.rotation_euler = rot_euler
elif target_loc is not None:
direction = target_loc - mathutils.Vector(location)
if direction.length > 1e-4:
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
else:
cam_obj.rotation_euler = (0, 0, 0)
return cam_obj
# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
R = props.earth_radius
theta = math.radians(props.obs_longitude)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
R_sky = props.sky_radius
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
lon = math.radians(props.arrow_start_lon)
lat = math.radians(props.arrow_start_lat)
R_T = props.tangent_radius
Start = T + mathutils.Vector((
R_T * math.cos(lat) * math.cos(lon),
R_T * math.cos(lat) * math.sin(lon),
R_T * math.sin(lat)
))
End = P
return {
"start": Start,
"end": End,
"arrow_len": (End - Start).length,
"dist_end_surf": abs(End.length - R),
"dist_start_surf": abs(Start.length - R),
"dist_end_center": End.length,
"dist_start_center": Start.length,
"O": mathutils.Vector((0, 0, 0)),
"P": P,
"T": T
}
# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
safe_remove_collection_contents(COLLECTION_NAME)
col = get_or_create_collection(COLLECTION_NAME)
measurements = calculate_measurements(props)
O = measurements["O"]
P = measurements["P"]
T = measurements["T"]
Start = measurements["start"]
R = props.earth_radius
R_outer = max(props.outer_sphere_radius, R + 0.01)
# Materials
mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
# 1. Earth
if props.earth_show:
create_sphere("Earth", R, O, col, mat_earth)
if props.earth_equator:
create_circle(PREFIX+"Equator", R, O, col, mat_line)
create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)
# 2. Outer Sphere
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.outer_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
if props.show_o_earth:
dir_e = mathutils.Vector((-0.707, 0.707, 0))
P_e = O + dir_e * R
create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
if props.show_o_outer and props.outer_show:
dir_o = mathutils.Vector((-0.707, -0.707, 0))
P_o = O + dir_o * R_outer
create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
# 3. P & Sky
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
if props.show_op_line:
create_line(PREFIX+"Line_OP", O, P, col, mat_line)
if props.show_distance:
create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", (O + P)/2.0 + mathutils.Vector((0,0,R*0.05)), col, scale=R_outer*0.025, mat=mat_line)
if props.sky_show:
create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
# 4. Tangent Sphere
if props.tangent_show:
tangent_obj = create_sphere("TangentWireSphere", props.tangent_radius, T, col, mat_tangent, segments=props.tangent_segments, rings=props.tangent_rings)
wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.tangent_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
# 5. Arrow
if props.arrow_show:
create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
# 6. Stars
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(PREFIX+"Stars", P, props.sky_radius, props.star_count, props.star_size, props.star_seed, tuple(props.star_color), props.star_emission, col)
if props.show_p_star_line and len(star_positions) > 0:
random.seed(props.star_seed + 1)
for i, s_pos in enumerate(random.sample(star_positions, min(3, len(star_positions)))):
create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
if props.show_distance:
create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", (P + s_pos)/2.0, col, scale=R_outer*0.025, mat=mat_line_p_star)
if props.show_axes:
create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)
# 7. カメラ生成と位置・回転計算
if props.cam_target == 'O': target_loc = O
elif props.cam_target == 'P': target_loc = P
elif props.cam_target == 'T': target_loc = T
else: target_loc = O
# 周回モードの時のみ、ターゲット位置に目印のマーカーを表示する
if props.cam_mode == 'ORBIT':
mat_cam_tgt = create_material(PREFIX+"Mat_CamTargetMarker", color=(1.0, 0.2, 0.8, 1.0), emission=2.0)
create_sphere(PREFIX+"Cam_Target_Marker", max(0.2, R_outer*0.015), target_loc, col, mat_cam_tgt, segments=16, rings=8)
# メインカメラの計算
if props.cam_mode == 'ORBIT':
u = math.radians(props.cam_lon)
v = math.radians(props.cam_lat)
r = props.cam_radius
cam_loc = target_loc + mathutils.Vector((
r * math.cos(v) * math.cos(u),
r * math.cos(v) * math.sin(u),
r * math.sin(v)
))
cam_rot = None # target_loc を見るように自動計算
else:
cam_loc = target_loc
# オイラー角で回転指定 (Yaw と Roll の適用軸を修正)
# Blender カメラ初期軸: X軸回転=Pitch, Y軸回転=Roll, Z軸回転=Yaw になるよう設定
cam_rot = (math.radians(props.cam_pitch), math.radians(props.cam_roll), math.radians(props.cam_yaw))
# 各カメラ生成
cam_main = setup_camera(PREFIX+"Cam_Main", cam_loc, target_loc, col, rot_euler=cam_rot)
setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
# アクティブカメラが未設定の場合はメインカメラを登録
if not context.scene.camera:
context.scene.camera = cam_main
# テキストオブジェクトが常にカメラを向くように設定
for obj in col.objects:
if obj.type == 'FONT':
for cons in obj.constraints:
if cons.type == 'TRACK_TO':
cons.target = context.scene.camera
def update_callback(self, context):
if self.outer_sphere_radius <= self.earth_radius:
self.outer_sphere_radius = self.earth_radius + 0.1
try:
generate_sky_system(self, context)
except Exception as e:
print(f"Observer Sky Update Error: {e}")
# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
# Main Camera (UV & Pan/Tilt)
cam_target: bpy.props.EnumProperty(
name="基準球体 (Target)",
items=[
('O', "1. 地球 (O)", "地球の中心"),
('P', "2. 天球 (P)", "地表の観測者"),
('T', "3. Tangent Sphere (T)", "4番目の球体の中心")
],
default=INIT_CAM_TARGET,
update=update_callback
)
cam_mode: bpy.props.EnumProperty(
name="Camera Mode",
items=[
('ORBIT', "周回モード (球面移動)", "ターゲットの周囲を回る"),
('PAN_TILT', "首振りモード (位置固定)", "ターゲット位置に固定して首を振る")
],
default=INIT_CAM_MODE,
update=update_callback
)
# ORBIT 用プロパティ
cam_radius: bpy.props.FloatProperty(name="Camera Radius", default=INIT_CAM_RADIUS, min=0.1, max=1000.0, update=update_callback)
cam_lon: bpy.props.FloatProperty(name="Camera Longitude", default=INIT_CAM_LON, min=-360.0, max=360.0, update=update_callback)
cam_lat: bpy.props.FloatProperty(name="Camera Latitude", default=INIT_CAM_LAT, min=-90.0, max=90.0, update=update_callback)
# PAN/TILT 用プロパティ
cam_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=INIT_CAM_PITCH, min=-360.0, max=360.0, update=update_callback)
cam_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=INIT_CAM_YAW, min=-360.0, max=360.0, update=update_callback)
cam_roll: bpy.props.FloatProperty(name="Roll (回転)", default=INIT_CAM_ROLL, min=-360.0, max=360.0, update=update_callback)
# Marker
obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)
# Earth
earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)
# Outer Sphere
outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)
# Observer
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Sky Sphere
sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)
# Tangent Wire Sphere
tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
# Arrow (Ray)
arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)
# Stars
star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)
# Visualizations
show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)
# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
bl_idname = "obs_sky.generate"
bl_label = "Generate / Update Model"
def execute(self, context):
props = context.scene.obs_sky_props
generate_sky_system(props, context)
return {'FINISHED'}
class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
bl_idname = "obs_sky.regenerate_stars"
bl_label = "Randomize Stars"
def execute(self, context):
props = context.scene.obs_sky_props
props.star_seed = random.randint(0, 10000)
return {'FINISHED'}
class OBS_SKY_OT_SetCamera(bpy.types.Operator):
bl_idname = "obs_sky.set_camera"
bl_label = "Set Camera"
cam_type: bpy.props.StringProperty()
def execute(self, context):
cam_name = PREFIX + self.cam_type
if cam_name in bpy.data.objects:
cam = bpy.data.objects[cam_name]
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
# ビューポートを自動的にカメラ視点に切り替える
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"{cam_name} に切り替えました")
return {'FINISHED'}
class OBS_SKY_OT_ResetCamOrbit(bpy.types.Operator):
bl_idname = "obs_sky.reset_cam_orbit"
bl_label = "経度・緯度・半径をリセット"
bl_description = "周回モードの設定を初期値に戻します"
def execute(self, context):
props = context.scene.obs_sky_props
props.cam_radius = INIT_CAM_RADIUS
props.cam_lon = INIT_CAM_LON
props.cam_lat = INIT_CAM_LAT
return {'FINISHED'}
class OBS_SKY_OT_ResetCamPanTilt(bpy.types.Operator):
bl_idname = "obs_sky.reset_cam_pan_tilt"
bl_label = "首振り角度をリセット"
bl_description = "首振りモードのPitch/Yaw/Rollを初期値に戻します"
def execute(self, context):
props = context.scene.obs_sky_props
props.cam_pitch = INIT_CAM_PITCH
props.cam_yaw = INIT_CAM_YAW
props.cam_roll = INIT_CAM_ROLL
return {'FINISHED'}
class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
bl_idname = "obs_sky.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene.obs_sky_props
meas = calculate_measurements(props)
info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
"""
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_CopyInitValues(bpy.types.Operator):
bl_idname = "obs_sky.copy_init_values"
bl_label = "初期設定値コードをコピー"
def execute(self, context):
props = context.scene.obs_sky_props
copy_text = f"""INIT_EARTH_RADIUS = {props.earth_radius:.2f}
INIT_CAM_TARGET = '{props.cam_target}'
INIT_CAM_MODE = '{props.cam_mode}'
INIT_CAM_RADIUS = {props.cam_radius:.2f}
INIT_CAM_LON = {props.cam_lon:.2f}
INIT_CAM_LAT = {props.cam_lat:.2f}
INIT_CAM_PITCH = {props.cam_pitch:.2f}
INIT_CAM_YAW = {props.cam_yaw:.2f}
INIT_CAM_ROLL = {props.cam_roll:.2f}
"""
context.window_manager.clipboard = copy_text
self.report({'INFO'}, "現在のパラメーターを初期設定値としてクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
bl_label = "Observer Sky Model"
bl_idname = "OBS_SKY_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
def draw(self, context):
layout = self.layout
props = context.scene.obs_sky_props
box = layout.box()
box.prop(props, "obs_show", icon='USER')
box.prop(props, "marker_o_show", icon='WORLD')
layout.separator()
layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')
# ======================================================
# Main Camera Control (Target, Mode, Coordinates)
# ======================================================
box = layout.box()
box.label(text="メインカメラ制御", icon='CAMERA_DATA')
box.prop(props, "cam_target")
box.separator()
# モード選択 (周回 / 首振り)
box.prop(props, "cam_mode", expand=True)
# モードに応じたパラメータ表示とリセットボタン
if props.cam_mode == 'ORBIT':
box.prop(props, "cam_radius", text="半径 (中心からの距離)")
box.separator()
col = box.column(align=True)
col.prop(props, "cam_lon", text="経度 (U)")
col.prop(props, "cam_lat", text="緯度 (V)")
box.separator()
box.operator(OBS_SKY_OT_ResetCamOrbit.bl_idname, icon='FILE_REFRESH')
else: # PAN_TILT
col = box.column(align=True)
col.prop(props, "cam_pitch", text="Pitch (上下)")
col.prop(props, "cam_yaw", text="Yaw (左右)")
col.prop(props, "cam_roll", text="Roll (回転)")
box.separator()
box.operator(OBS_SKY_OT_ResetCamPanTilt.bl_idname, icon='FILE_REFRESH')
box.separator()
box.operator(OBS_SKY_OT_SetCamera.bl_idname, text="メインカメラの視点に入る", icon='HIDE_OFF').cam_type = "Cam_Main"
# ======================================================
box = layout.box()
box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
box.prop(props, "tangent_show")
if props.tangent_show:
box.prop(props, "tangent_radius")
box.prop(props, "tangent_wire_thickness")
row_div = box.row()
row_div.prop(props, "tangent_segments")
row_div.prop(props, "tangent_rings")
box.prop(props, "tangent_color")
box = layout.box()
box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
box.prop(props, "arrow_show")
if props.arrow_show:
box.prop(props, "arrow_start_lon")
box.prop(props, "arrow_start_lat")
box.prop(props, "arrow_thickness")
box.prop(props, "arrow_color")
box = layout.box()
box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
box.prop(props, "earth_radius")
row = box.row()
row.prop(props, "earth_show")
row.prop(props, "earth_equator")
box.prop(props, "earth_color")
box.separator()
box.label(text="Outer Sphere [000]:")
box.prop(props, "outer_sphere_radius")
row = box.row()
row.prop(props, "outer_show")
row.prop(props, "outer_wire")
if props.outer_wire:
box.prop(props, "outer_wire_thickness")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
box = layout.box()
box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
box.prop(props, "sky_radius")
box.prop(props, "sky_show")
box.prop(props, "sky_color")
box = layout.box()
box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
box.prop(props, "star_count")
box.prop(props, "star_size")
box.prop(props, "star_color")
box.prop(props, "star_emission")
box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
box = layout.box()
box.label(text="Visualization / Vectors", icon='GIZMO')
col = box.column(align=True)
col.prop(props, "show_op_line")
col.prop(props, "show_p_star_line")
col.prop(props, "show_distance")
col.prop(props, "show_axes")
box = layout.box()
box.label(text="その他の固定カメラ (Other Cameras)", icon='VIEW_CAMERA')
col = box.column(align=True)
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
box = layout.box()
box.label(text="Measurements & Info (情報パネル)", icon='INFO')
box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
layout.separator()
layout.operator(OBS_SKY_OT_CopyInitValues.bl_idname, icon='COPYDOWN', text="初期設定値コードをコピー")
class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = "OBS_SKY_PT_AdminPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')
# ======================================================================
# Registration
# ======================================================================
setup_retry_count = 0
def init_addon_setup():
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50: return None
return 0.1
try:
props = bpy.context.scene.obs_sky_props
generate_sky_system(props, bpy.context)
except Exception as e:
print(f"Observer Sky Auto-generate Error: {e}")
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
if hasattr(space, 'shading'):
space.shading.type = 'MATERIAL'
return None
classes = (
ObserverSkyProperties,
OBS_SKY_OT_Generate,
OBS_SKY_OT_RegenerateStars,
OBS_SKY_OT_SetCamera,
OBS_SKY_OT_ResetCamOrbit,
OBS_SKY_OT_ResetCamPanTilt,
OBS_SKY_OT_CopyInfo,
OBS_SKY_OT_CopyInitValues,
OBS_SKY_OT_RemoveAddon,
OBS_SKY_PT_MainPanel,
OBS_SKY_PT_AdminPanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
if not bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.register(init_addon_setup, first_interval=0.1)
def unregister():
if bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.unregister(init_addon_setup)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.obs_sky_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Observer Sky Model with UV Camera",
"author": "AI Assistant",
"version": (2, 2, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations with UV-controlled & Pan-Tilt Camera System.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.00
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.100, 0.400, 0.800, 0.300)
INIT_EARTH_EQUATOR = True
INIT_OUTER_RADIUS = 10.00
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.500, 0.800, 0.800, 0.100)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.020
INIT_OBS_THETA = 0.00
INIT_OBS_SHOW = False
INIT_MARKER_O_SHOW = False
INIT_SKY_RADIUS = 5.00
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.300, 0.100, 0.500, 0.150)
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.00
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.082, 0.063, 0.081, 0.800)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.050
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.00
INIT_ARROW_LAT = 30.00
INIT_ARROW_THICKNESS = 0.500
INIT_ARROW_COLOR = (0.105, 1.000, 0.000, 1.000)
INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.050
INIT_STAR_COLOR = (1.000, 1.000, 0.800, 1.000)
INIT_STAR_EMISSION = 5.00
INIT_STAR_SEED = 42
INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True
INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True
# === メインカメラ用の初期値 ===
INIT_CAM_TARGET = 'T'
INIT_CAM_MODE = 'ORBIT'
# 周回(ORBIT)用
INIT_CAM_RADIUS = 20.00
INIT_CAM_LON = 45.00
INIT_CAM_LAT = 30.00
# 首振り(PAN_TILT)用 (Blenderカメラは初期状態で下を向くため、X90で正面を向くように設定)
INIT_CAM_PITCH = 90.00
INIT_CAM_YAW = 0.00
INIT_CAM_ROLL = 0.00
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
# ======================================================================
# ユーティリティ関数群
# ======================================================================
def get_or_create_collection(name):
if name in bpy.data.collections:
return bpy.data.collections[name]
new_col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(new_col)
return new_col
def safe_remove_collection_contents(col_name):
if col_name not in bpy.data.collections:
return
col = bpy.data.collections[col_name]
for obj in list(col.objects):
# カメラは削除せず再利用することで、スライダー操作中の視点外れを防止
if obj.type == 'CAMERA':
continue
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data:
if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
bpy.data.curves.remove(mesh_data)
def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
principled.inputs['Base Color'].default_value = color
principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
principled.inputs['Emission Strength'].default_value = emission
principled.inputs['Alpha'].default_value = alpha
if is_transparent and alpha < 1.0:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
else:
mat.blend_method = 'OPAQUE'
mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
return mat
def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
bm.to_mesh(mesh)
bm.free()
obj.location = location
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_arrow(name, p1, p2, thickness, mat, collection):
direction = p2 - p1
length = direction.length
if length < 1e-4:
return None
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
head_len = min(thickness * 4.0, length * 0.2)
shaft_len = length - head_len
# Shaft
ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
# Head
ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
z_axis = mathutils.Vector((0, 0, 1))
rot_quat = z_axis.rotation_difference(direction)
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons: poly.use_smooth = True
return obj
def create_stars(name, P, radius, count, size, seed, color, emission, collection):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
obj.data.materials.append(mat)
bm = bmesh.new()
random.seed(seed)
star_positions = []
for _ in range(count):
z = random.uniform(-1.0, 1.0)
phi = random.uniform(0.0, 2.0 * math.pi)
x = math.sqrt(1.0 - z*z) * math.cos(phi)
y = math.sqrt(1.0 - z*z) * math.sin(phi)
pos = P + mathutils.Vector((x, y, z)) * radius
star_positions.append(pos)
geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
bm.to_mesh(mesh)
bm.free()
return obj, star_positions
def create_line(name, p1, p2, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bm.edges.new((bm.verts.new(p1), bm.verts.new(p2)))
bm.to_mesh(mesh)
bm.free()
return obj
def create_circle(name, radius, location, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
bm.to_mesh(mesh)
bm.free()
obj.location = location
return obj
def create_text(name, text, location, collection, scale=1.0, mat=None):
curve = bpy.data.curves.new(type="FONT", name=name)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
obj = bpy.data.objects.new(name, curve)
collection.objects.link(obj)
obj.location = location
obj.scale = (scale, scale, scale)
if mat: obj.data.materials.append(mat)
constraint = obj.constraints.new(type='TRACK_TO')
constraint.track_axis = 'TRACK_Z'
constraint.up_axis = 'UP_Y'
return obj, constraint
def setup_camera(name, location, target_loc, collection, rot_euler=None):
""" カメラが存在すれば再利用し、指定された位置と回転(またはターゲット方向)にセットする """
cam_obj = bpy.data.objects.get(name)
if not cam_obj:
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
collection.objects.link(cam_obj)
cam_obj.location = location
if rot_euler is not None:
cam_obj.rotation_euler = rot_euler
elif target_loc is not None:
direction = target_loc - mathutils.Vector(location)
if direction.length > 1e-4:
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
else:
cam_obj.rotation_euler = (0, 0, 0)
return cam_obj
# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
R = props.earth_radius
theta = math.radians(props.obs_longitude)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
R_sky = props.sky_radius
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
lon = math.radians(props.arrow_start_lon)
lat = math.radians(props.arrow_start_lat)
R_T = props.tangent_radius
Start = T + mathutils.Vector((
R_T * math.cos(lat) * math.cos(lon),
R_T * math.cos(lat) * math.sin(lon),
R_T * math.sin(lat)
))
End = P
return {
"start": Start,
"end": End,
"arrow_len": (End - Start).length,
"dist_end_surf": abs(End.length - R),
"dist_start_surf": abs(Start.length - R),
"dist_end_center": End.length,
"dist_start_center": Start.length,
"O": mathutils.Vector((0, 0, 0)),
"P": P,
"T": T
}
# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
safe_remove_collection_contents(COLLECTION_NAME)
col = get_or_create_collection(COLLECTION_NAME)
measurements = calculate_measurements(props)
O = measurements["O"]
P = measurements["P"]
T = measurements["T"]
Start = measurements["start"]
R = props.earth_radius
R_outer = max(props.outer_sphere_radius, R + 0.01)
# Materials
mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
# 1. Earth
if props.earth_show:
create_sphere("Earth", R, O, col, mat_earth)
if props.earth_equator:
create_circle(PREFIX+"Equator", R, O, col, mat_line)
create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)
# 2. Outer Sphere
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.outer_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
if props.show_o_earth:
dir_e = mathutils.Vector((-0.707, 0.707, 0))
P_e = O + dir_e * R
create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
if props.show_o_outer and props.outer_show:
dir_o = mathutils.Vector((-0.707, -0.707, 0))
P_o = O + dir_o * R_outer
create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
# 3. P & Sky
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
if props.show_op_line:
create_line(PREFIX+"Line_OP", O, P, col, mat_line)
if props.show_distance:
create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", (O + P)/2.0 + mathutils.Vector((0,0,R*0.05)), col, scale=R_outer*0.025, mat=mat_line)
if props.sky_show:
create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
# 4. Tangent Sphere
if props.tangent_show:
tangent_obj = create_sphere("TangentWireSphere", props.tangent_radius, T, col, mat_tangent, segments=props.tangent_segments, rings=props.tangent_rings)
wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.tangent_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
# 5. Arrow
if props.arrow_show:
create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
# 6. Stars
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(PREFIX+"Stars", P, props.sky_radius, props.star_count, props.star_size, props.star_seed, tuple(props.star_color), props.star_emission, col)
if props.show_p_star_line and len(star_positions) > 0:
random.seed(props.star_seed + 1)
for i, s_pos in enumerate(random.sample(star_positions, min(3, len(star_positions)))):
create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
if props.show_distance:
create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", (P + s_pos)/2.0, col, scale=R_outer*0.025, mat=mat_line_p_star)
if props.show_axes:
create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)
# 7. カメラ生成と位置・回転計算
if props.cam_target == 'O': target_loc = O
elif props.cam_target == 'P': target_loc = P
elif props.cam_target == 'T': target_loc = T
else: target_loc = O
# 周回モードの時のみ、ターゲット位置に目印のマーカーを表示する
if props.cam_mode == 'ORBIT':
mat_cam_tgt = create_material(PREFIX+"Mat_CamTargetMarker", color=(1.0, 0.2, 0.8, 1.0), emission=2.0)
create_sphere(PREFIX+"Cam_Target_Marker", max(0.2, R_outer*0.015), target_loc, col, mat_cam_tgt, segments=16, rings=8)
# メインカメラの計算
if props.cam_mode == 'ORBIT':
u = math.radians(props.cam_lon)
v = math.radians(props.cam_lat)
r = props.cam_radius
cam_loc = target_loc + mathutils.Vector((
r * math.cos(v) * math.cos(u),
r * math.cos(v) * math.sin(u),
r * math.sin(v)
))
cam_rot = None # target_loc を見るように自動計算
else:
cam_loc = target_loc
# オイラー角で回転指定 (Yaw と Roll の適用軸を修正)
# Blender カメラ初期軸: X軸回転=Pitch, Y軸回転=Roll, Z軸回転=Yaw になるよう設定
cam_rot = (math.radians(props.cam_pitch), math.radians(props.cam_roll), math.radians(props.cam_yaw))
# 各カメラ生成
cam_main = setup_camera(PREFIX+"Cam_Main", cam_loc, target_loc, col, rot_euler=cam_rot)
setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
# アクティブカメラが未設定の場合はメインカメラを登録
if not context.scene.camera:
context.scene.camera = cam_main
# テキストオブジェクトが常にカメラを向くように設定
for obj in col.objects:
if obj.type == 'FONT':
for cons in obj.constraints:
if cons.type == 'TRACK_TO':
cons.target = context.scene.camera
def update_callback(self, context):
if self.outer_sphere_radius <= self.earth_radius:
self.outer_sphere_radius = self.earth_radius + 0.1
try:
generate_sky_system(self, context)
except Exception as e:
print(f"Observer Sky Update Error: {e}")
# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
# Main Camera (UV & Pan/Tilt)
cam_target: bpy.props.EnumProperty(
name="基準球体 (Target)",
items=[
('O', "1. 地球 (O)", "地球の中心"),
('P', "2. 天球 (P)", "地表の観測者"),
('T', "3. Tangent Sphere (T)", "4番目の球体の中心")
],
default=INIT_CAM_TARGET,
update=update_callback
)
cam_mode: bpy.props.EnumProperty(
name="Camera Mode",
items=[
('ORBIT', "周回モード (球面移動)", "ターゲットの周囲を回る"),
('PAN_TILT', "首振りモード (位置固定)", "ターゲット位置に固定して首を振る")
],
default=INIT_CAM_MODE,
update=update_callback
)
# ORBIT 用プロパティ
cam_radius: bpy.props.FloatProperty(name="Camera Radius", default=INIT_CAM_RADIUS, min=0.1, max=1000.0, update=update_callback)
cam_lon: bpy.props.FloatProperty(name="Camera Longitude", default=INIT_CAM_LON, min=-360.0, max=360.0, update=update_callback)
cam_lat: bpy.props.FloatProperty(name="Camera Latitude", default=INIT_CAM_LAT, min=-90.0, max=90.0, update=update_callback)
# PAN/TILT 用プロパティ
cam_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=INIT_CAM_PITCH, min=-360.0, max=360.0, update=update_callback)
cam_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=INIT_CAM_YAW, min=-360.0, max=360.0, update=update_callback)
cam_roll: bpy.props.FloatProperty(name="Roll (回転)", default=INIT_CAM_ROLL, min=-360.0, max=360.0, update=update_callback)
# Marker
obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)
# Earth
earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)
# Outer Sphere
outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)
# Observer
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Sky Sphere
sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)
# Tangent Wire Sphere
tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
# Arrow (Ray)
arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)
# Stars
star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)
# Visualizations
show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)
# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
bl_idname = "obs_sky.generate"
bl_label = "Generate / Update Model"
def execute(self, context):
props = context.scene.obs_sky_props
generate_sky_system(props, context)
return {'FINISHED'}
class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
bl_idname = "obs_sky.regenerate_stars"
bl_label = "Randomize Stars"
def execute(self, context):
props = context.scene.obs_sky_props
props.star_seed = random.randint(0, 10000)
return {'FINISHED'}
class OBS_SKY_OT_SetCamera(bpy.types.Operator):
bl_idname = "obs_sky.set_camera"
bl_label = "Set Camera"
cam_type: bpy.props.StringProperty()
def execute(self, context):
cam_name = PREFIX + self.cam_type
if cam_name in bpy.data.objects:
cam = bpy.data.objects[cam_name]
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
# ビューポートを自動的にカメラ視点に切り替える
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"{cam_name} に切り替えました")
return {'FINISHED'}
class OBS_SKY_OT_ResetCamOrbit(bpy.types.Operator):
bl_idname = "obs_sky.reset_cam_orbit"
bl_label = "経度・緯度・半径をリセット"
bl_description = "周回モードの設定を初期値に戻します"
def execute(self, context):
props = context.scene.obs_sky_props
props.cam_radius = INIT_CAM_RADIUS
props.cam_lon = INIT_CAM_LON
props.cam_lat = INIT_CAM_LAT
return {'FINISHED'}
class OBS_SKY_OT_ResetCamPanTilt(bpy.types.Operator):
bl_idname = "obs_sky.reset_cam_pan_tilt"
bl_label = "首振り角度をリセット"
bl_description = "首振りモードのPitch/Yaw/Rollを初期値に戻します"
def execute(self, context):
props = context.scene.obs_sky_props
props.cam_pitch = INIT_CAM_PITCH
props.cam_yaw = INIT_CAM_YAW
props.cam_roll = INIT_CAM_ROLL
return {'FINISHED'}
class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
bl_idname = "obs_sky.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene.obs_sky_props
meas = calculate_measurements(props)
info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
"""
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_CopyInitValues(bpy.types.Operator):
bl_idname = "obs_sky.copy_init_values"
bl_label = "初期設定値コードをコピー"
def execute(self, context):
props = context.scene.obs_sky_props
copy_text = f"""INIT_EARTH_RADIUS = {props.earth_radius:.2f}
INIT_CAM_TARGET = '{props.cam_target}'
INIT_CAM_MODE = '{props.cam_mode}'
INIT_CAM_RADIUS = {props.cam_radius:.2f}
INIT_CAM_LON = {props.cam_lon:.2f}
INIT_CAM_LAT = {props.cam_lat:.2f}
INIT_CAM_PITCH = {props.cam_pitch:.2f}
INIT_CAM_YAW = {props.cam_yaw:.2f}
INIT_CAM_ROLL = {props.cam_roll:.2f}
"""
context.window_manager.clipboard = copy_text
self.report({'INFO'}, "現在のパラメーターを初期設定値としてクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
bl_label = "Observer Sky Model"
bl_idname = "OBS_SKY_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
def draw(self, context):
layout = self.layout
props = context.scene.obs_sky_props
box = layout.box()
box.prop(props, "obs_show", icon='USER')
box.prop(props, "marker_o_show", icon='WORLD')
layout.separator()
layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')
# ======================================================
# Main Camera Control (Target, Mode, Coordinates)
# ======================================================
box = layout.box()
box.label(text="メインカメラ制御", icon='CAMERA_DATA')
box.prop(props, "cam_target")
box.separator()
# モード選択 (周回 / 首振り)
box.prop(props, "cam_mode", expand=True)
# モードに応じたパラメータ表示とリセットボタン
if props.cam_mode == 'ORBIT':
box.prop(props, "cam_radius", text="半径 (中心からの距離)")
box.separator()
col = box.column(align=True)
col.prop(props, "cam_lon", text="経度 (U)")
col.prop(props, "cam_lat", text="緯度 (V)")
box.separator()
box.operator(OBS_SKY_OT_ResetCamOrbit.bl_idname, icon='FILE_REFRESH')
else: # PAN_TILT
col = box.column(align=True)
col.prop(props, "cam_pitch", text="Pitch (上下)")
col.prop(props, "cam_yaw", text="Yaw (左右)")
col.prop(props, "cam_roll", text="Roll (回転)")
box.separator()
box.operator(OBS_SKY_OT_ResetCamPanTilt.bl_idname, icon='FILE_REFRESH')
box.separator()
box.operator(OBS_SKY_OT_SetCamera.bl_idname, text="メインカメラの視点に入る", icon='HIDE_OFF').cam_type = "Cam_Main"
# ======================================================
box = layout.box()
box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
box.prop(props, "tangent_show")
if props.tangent_show:
box.prop(props, "tangent_radius")
box.prop(props, "tangent_wire_thickness")
row_div = box.row()
row_div.prop(props, "tangent_segments")
row_div.prop(props, "tangent_rings")
box.prop(props, "tangent_color")
box = layout.box()
box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
box.prop(props, "arrow_show")
if props.arrow_show:
box.prop(props, "arrow_start_lon")
box.prop(props, "arrow_start_lat")
box.prop(props, "arrow_thickness")
box.prop(props, "arrow_color")
box = layout.box()
box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
box.prop(props, "earth_radius")
row = box.row()
row.prop(props, "earth_show")
row.prop(props, "earth_equator")
box.prop(props, "earth_color")
box.separator()
box.label(text="Outer Sphere [000]:")
box.prop(props, "outer_sphere_radius")
row = box.row()
row.prop(props, "outer_show")
row.prop(props, "outer_wire")
if props.outer_wire:
box.prop(props, "outer_wire_thickness")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
box = layout.box()
box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
box.prop(props, "sky_radius")
box.prop(props, "sky_show")
box.prop(props, "sky_color")
box = layout.box()
box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
box.prop(props, "star_count")
box.prop(props, "star_size")
box.prop(props, "star_color")
box.prop(props, "star_emission")
box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
box = layout.box()
box.label(text="Visualization / Vectors", icon='GIZMO')
col = box.column(align=True)
col.prop(props, "show_op_line")
col.prop(props, "show_p_star_line")
col.prop(props, "show_distance")
col.prop(props, "show_axes")
box = layout.box()
box.label(text="その他の固定カメラ (Other Cameras)", icon='VIEW_CAMERA')
col = box.column(align=True)
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
box = layout.box()
box.label(text="Measurements & Info (情報パネル)", icon='INFO')
box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
layout.separator()
layout.operator(OBS_SKY_OT_CopyInitValues.bl_idname, icon='COPYDOWN', text="初期設定値コードをコピー")
class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = "OBS_SKY_PT_AdminPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')
# ======================================================================
# Registration
# ======================================================================
setup_retry_count = 0
def init_addon_setup():
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50: return None
return 0.1
try:
props = bpy.context.scene.obs_sky_props
generate_sky_system(props, bpy.context)
except Exception as e:
print(f"Observer Sky Auto-generate Error: {e}")
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
if hasattr(space, 'shading'):
space.shading.type = 'MATERIAL'
return None
classes = (
ObserverSkyProperties,
OBS_SKY_OT_Generate,
OBS_SKY_OT_RegenerateStars,
OBS_SKY_OT_SetCamera,
OBS_SKY_OT_ResetCamOrbit,
OBS_SKY_OT_ResetCamPanTilt,
OBS_SKY_OT_CopyInfo,
OBS_SKY_OT_CopyInitValues,
OBS_SKY_OT_RemoveAddon,
OBS_SKY_PT_MainPanel,
OBS_SKY_PT_AdminPanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
if not bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.register(init_addon_setup, first_interval=0.1)
def unregister():
if bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.unregister(init_addon_setup)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.obs_sky_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Observer Sky Model with UV Camera",
"author": "AI Assistant",
"version": (2, 0, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations with UV-controlled Camera System.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.00
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.100, 0.400, 0.800, 0.300)
INIT_EARTH_EQUATOR = True
INIT_OUTER_RADIUS = 10.00
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.500, 0.800, 0.800, 0.100)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.020
INIT_OBS_THETA = 0.00
INIT_OBS_SHOW = False
INIT_MARKER_O_SHOW = False
INIT_SKY_RADIUS = 5.00
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.300, 0.100, 0.500, 0.150)
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.00
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.082, 0.063, 0.081, 0.800)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.050
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.00
INIT_ARROW_LAT = 30.00
INIT_ARROW_THICKNESS = 0.500
INIT_ARROW_COLOR = (0.105, 1.000, 0.000, 1.000)
INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.050
INIT_STAR_COLOR = (1.000, 1.000, 0.800, 1.000)
INIT_STAR_EMISSION = 5.00
INIT_STAR_SEED = 42
INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True
INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True
# === 追加: メインカメラ用の初期値 ===
INIT_CAM_TARGET = 'T'
INIT_CAM_RADIUS = 20.00
INIT_CAM_LON = 45.00
INIT_CAM_LAT = 30.00
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
# ======================================================================
# ユーティリティ関数群
# ======================================================================
def get_or_create_collection(name):
if name in bpy.data.collections:
return bpy.data.collections[name]
new_col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(new_col)
return new_col
def safe_remove_collection_contents(col_name):
if col_name not in bpy.data.collections:
return
col = bpy.data.collections[col_name]
for obj in list(col.objects):
# カメラは削除せず再利用することで、スライダー操作中の視点外れを防止
if obj.type == 'CAMERA':
continue
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data:
if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
bpy.data.curves.remove(mesh_data)
def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
principled.inputs['Base Color'].default_value = color
principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
principled.inputs['Emission Strength'].default_value = emission
principled.inputs['Alpha'].default_value = alpha
if is_transparent and alpha < 1.0:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
else:
mat.blend_method = 'OPAQUE'
mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
return mat
def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
bm.to_mesh(mesh)
bm.free()
obj.location = location
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_arrow(name, p1, p2, thickness, mat, collection):
direction = p2 - p1
length = direction.length
if length < 1e-4:
return None
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
head_len = min(thickness * 4.0, length * 0.2)
shaft_len = length - head_len
# Shaft
ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
# Head
ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
z_axis = mathutils.Vector((0, 0, 1))
rot_quat = z_axis.rotation_difference(direction)
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons: poly.use_smooth = True
return obj
def create_stars(name, P, radius, count, size, seed, color, emission, collection):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
obj.data.materials.append(mat)
bm = bmesh.new()
random.seed(seed)
star_positions = []
for _ in range(count):
z = random.uniform(-1.0, 1.0)
phi = random.uniform(0.0, 2.0 * math.pi)
x = math.sqrt(1.0 - z*z) * math.cos(phi)
y = math.sqrt(1.0 - z*z) * math.sin(phi)
pos = P + mathutils.Vector((x, y, z)) * radius
star_positions.append(pos)
geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
bm.to_mesh(mesh)
bm.free()
return obj, star_positions
def create_line(name, p1, p2, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bm.edges.new((bm.verts.new(p1), bm.verts.new(p2)))
bm.to_mesh(mesh)
bm.free()
return obj
def create_circle(name, radius, location, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat: obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
bm.to_mesh(mesh)
bm.free()
obj.location = location
return obj
def create_text(name, text, location, collection, scale=1.0, mat=None):
curve = bpy.data.curves.new(type="FONT", name=name)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
obj = bpy.data.objects.new(name, curve)
collection.objects.link(obj)
obj.location = location
obj.scale = (scale, scale, scale)
if mat: obj.data.materials.append(mat)
constraint = obj.constraints.new(type='TRACK_TO')
constraint.track_axis = 'TRACK_Z'
constraint.up_axis = 'UP_Y'
return obj, constraint
def setup_camera(name, location, target_loc, collection):
# 既にカメラが存在する場合は再利用し、視点外れを防ぐ
cam_obj = bpy.data.objects.get(name)
if not cam_obj:
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
collection.objects.link(cam_obj)
cam_obj.location = location
direction = target_loc - mathutils.Vector(location)
if direction.length > 1e-4:
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
else:
cam_obj.rotation_euler = (0, 0, 0)
return cam_obj
# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
R = props.earth_radius
theta = math.radians(props.obs_longitude)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
R_sky = props.sky_radius
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
lon = math.radians(props.arrow_start_lon)
lat = math.radians(props.arrow_start_lat)
R_T = props.tangent_radius
Start = T + mathutils.Vector((
R_T * math.cos(lat) * math.cos(lon),
R_T * math.cos(lat) * math.sin(lon),
R_T * math.sin(lat)
))
End = P
return {
"start": Start,
"end": End,
"arrow_len": (End - Start).length,
"dist_end_surf": abs(End.length - R),
"dist_start_surf": abs(Start.length - R),
"dist_end_center": End.length,
"dist_start_center": Start.length,
"O": mathutils.Vector((0, 0, 0)),
"P": P,
"T": T
}
# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
safe_remove_collection_contents(COLLECTION_NAME)
col = get_or_create_collection(COLLECTION_NAME)
measurements = calculate_measurements(props)
O = measurements["O"]
P = measurements["P"]
T = measurements["T"]
Start = measurements["start"]
R = props.earth_radius
R_outer = max(props.outer_sphere_radius, R + 0.01)
# Materials
mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
# 1. Earth
if props.earth_show:
create_sphere("Earth", R, O, col, mat_earth)
if props.earth_equator:
create_circle(PREFIX+"Equator", R, O, col, mat_line)
create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)
# 2. Outer Sphere
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.outer_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
if props.show_o_earth:
dir_e = mathutils.Vector((-0.707, 0.707, 0))
P_e = O + dir_e * R
create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
if props.show_o_outer and props.outer_show:
dir_o = mathutils.Vector((-0.707, -0.707, 0))
P_o = O + dir_o * R_outer
create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
# 3. P & Sky
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
if props.show_op_line:
create_line(PREFIX+"Line_OP", O, P, col, mat_line)
if props.show_distance:
create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", (O + P)/2.0 + mathutils.Vector((0,0,R*0.05)), col, scale=R_outer*0.025, mat=mat_line)
if props.sky_show:
create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
# 4. Tangent Sphere
if props.tangent_show:
tangent_obj = create_sphere("TangentWireSphere", props.tangent_radius, T, col, mat_tangent, segments=props.tangent_segments, rings=props.tangent_rings)
wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.tangent_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
# 5. Arrow
if props.arrow_show:
create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
# 6. Stars
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(PREFIX+"Stars", P, props.sky_radius, props.star_count, props.star_size, props.star_seed, tuple(props.star_color), props.star_emission, col)
if props.show_p_star_line and len(star_positions) > 0:
random.seed(props.star_seed + 1)
for i, s_pos in enumerate(random.sample(star_positions, min(3, len(star_positions)))):
create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
if props.show_distance:
create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", (P + s_pos)/2.0, col, scale=R_outer*0.025, mat=mat_line_p_star)
if props.show_axes:
create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)
# 7. Cameras & Target Logic
# ターゲット位置の取得
if props.cam_target == 'O': target_loc = O
elif props.cam_target == 'P': target_loc = P
elif props.cam_target == 'T': target_loc = T
else: target_loc = O
# 目印用のターゲットマーカー生成
mat_cam_tgt = create_material(PREFIX+"Mat_CamTargetMarker", color=(1.0, 0.2, 0.8, 1.0), emission=2.0)
create_sphere(PREFIX+"Cam_Target_Marker", max(0.2, R_outer*0.015), target_loc, col, mat_cam_tgt, segments=16, rings=8)
# 球面(UV)移動に基づくメインカメラ位置の計算
u = math.radians(props.cam_lon)
v = math.radians(props.cam_lat)
r = props.cam_radius
cam_loc = target_loc + mathutils.Vector((
r * math.cos(v) * math.cos(u),
r * math.cos(v) * math.sin(u),
r * math.sin(v)
))
# 各種カメラの生成・更新
cam_main = setup_camera(PREFIX+"Cam_Main", cam_loc, target_loc, col)
setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
# アクティブカメラが存在しない場合はメインを登録
if not context.scene.camera:
context.scene.camera = cam_main
# テキストオブジェクトの制約をカメラに向ける
for obj in col.objects:
if obj.type == 'FONT':
for cons in obj.constraints:
if cons.type == 'TRACK_TO':
cons.target = context.scene.camera
def update_callback(self, context):
if self.outer_sphere_radius <= self.earth_radius:
self.outer_sphere_radius = self.earth_radius + 0.1
try:
generate_sky_system(self, context)
except Exception as e:
print(f"Observer Sky Update Error: {e}")
# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
# Main Camera (UV)
cam_target: bpy.props.EnumProperty(
name="Camera Target",
items=[
('O', "O (Earth Center)", "地球の中心"),
('P', "P (Observer)", "地表の観測者"),
('T', "T (Tangent Sphere)", "4番目の球体の中心")
],
default=INIT_CAM_TARGET,
update=update_callback
)
cam_radius: bpy.props.FloatProperty(name="Camera Radius", default=INIT_CAM_RADIUS, min=0.1, max=1000.0, update=update_callback)
cam_lon: bpy.props.FloatProperty(name="Camera Longitude", default=INIT_CAM_LON, min=-360.0, max=360.0, update=update_callback)
cam_lat: bpy.props.FloatProperty(name="Camera Latitude", default=INIT_CAM_LAT, min=-90.0, max=90.0, update=update_callback)
# Marker
obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)
# Earth
earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)
# Outer Sphere
outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)
# Observer
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Sky Sphere
sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)
# Tangent Wire Sphere
tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
# Arrow (Ray)
arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)
# Stars
star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)
# Visualizations
show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)
# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
bl_idname = "obs_sky.generate"
bl_label = "Generate / Update Model"
def execute(self, context):
props = context.scene.obs_sky_props
generate_sky_system(props, context)
return {'FINISHED'}
class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
bl_idname = "obs_sky.regenerate_stars"
bl_label = "Randomize Stars"
def execute(self, context):
props = context.scene.obs_sky_props
props.star_seed = random.randint(0, 10000)
return {'FINISHED'}
class OBS_SKY_OT_SetCamera(bpy.types.Operator):
bl_idname = "obs_sky.set_camera"
bl_label = "Set Camera"
cam_type: bpy.props.StringProperty()
def execute(self, context):
cam_name = PREFIX + self.cam_type
if cam_name in bpy.data.objects:
cam = bpy.data.objects[cam_name]
context.scene.camera = cam
cam.data.show_passepartout = True
cam.data.passepartout_alpha = 0.8
# ビューポートを自動的にカメラ視点に切り替える
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.region_3d.view_perspective = 'CAMERA'
self.report({'INFO'}, f"{cam_name} に切り替えました")
return {'FINISHED'}
class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
bl_idname = "obs_sky.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene.obs_sky_props
meas = calculate_measurements(props)
info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
"""
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_CopyInitValues(bpy.types.Operator):
bl_idname = "obs_sky.copy_init_values"
bl_label = "初期設定値コードをコピー"
def execute(self, context):
props = context.scene.obs_sky_props
copy_text = f"""INIT_EARTH_RADIUS = {props.earth_radius:.2f}
INIT_CAM_TARGET = '{props.cam_target}'
INIT_CAM_RADIUS = {props.cam_radius:.2f}
INIT_CAM_LON = {props.cam_lon:.2f}
INIT_CAM_LAT = {props.cam_lat:.2f}
"""
context.window_manager.clipboard = copy_text
self.report({'INFO'}, "現在のパラメーターを初期設定値としてクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
bl_label = "Observer Sky Model"
bl_idname = "OBS_SKY_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
def draw(self, context):
layout = self.layout
props = context.scene.obs_sky_props
box = layout.box()
box.prop(props, "obs_show", icon='USER')
box.prop(props, "marker_o_show", icon='WORLD')
layout.separator()
layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')
# ======================================================
# 追加: Main Camera (UV Control)
# ======================================================
box = layout.box()
box.label(text="メインカメラ制御 (UV球面移動)", icon='CAMERA_DATA')
box.prop(props, "cam_target", text="見つめる位置 (Target)")
box.prop(props, "cam_radius", text="半径 (Targetからの距離)")
row = box.row(align=True)
row.prop(props, "cam_lon", text="経度 (U)")
row.prop(props, "cam_lat", text="緯度 (V)")
box.separator()
box.operator(OBS_SKY_OT_SetCamera.bl_idname, text="メインカメラの視点に入る", icon='HIDE_OFF').cam_type = "Cam_Main"
# ======================================================
box = layout.box()
box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
box.prop(props, "tangent_show")
if props.tangent_show:
box.prop(props, "tangent_radius")
box.prop(props, "tangent_wire_thickness")
row_div = box.row()
row_div.prop(props, "tangent_segments")
row_div.prop(props, "tangent_rings")
box.prop(props, "tangent_color")
box = layout.box()
box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
box.prop(props, "arrow_show")
if props.arrow_show:
box.prop(props, "arrow_start_lon")
box.prop(props, "arrow_start_lat")
box.prop(props, "arrow_thickness")
box.prop(props, "arrow_color")
box = layout.box()
box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
box.prop(props, "earth_radius")
row = box.row()
row.prop(props, "earth_show")
row.prop(props, "earth_equator")
box.prop(props, "earth_color")
box.separator()
box.label(text="Outer Sphere [000]:")
box.prop(props, "outer_sphere_radius")
row = box.row()
row.prop(props, "outer_show")
row.prop(props, "outer_wire")
if props.outer_wire:
box.prop(props, "outer_wire_thickness")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
box = layout.box()
box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
box.prop(props, "sky_radius")
box.prop(props, "sky_show")
box.prop(props, "sky_color")
box = layout.box()
box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
box.prop(props, "star_count")
box.prop(props, "star_size")
box.prop(props, "star_color")
box.prop(props, "star_emission")
box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
box = layout.box()
box.label(text="Visualization / Vectors", icon='GIZMO')
col = box.column(align=True)
col.prop(props, "show_op_line")
col.prop(props, "show_p_star_line")
col.prop(props, "show_distance")
col.prop(props, "show_axes")
box = layout.box()
box.label(text="その他の固定カメラ (Other Cameras)", icon='VIEW_CAMERA')
col = box.column(align=True)
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
box = layout.box()
box.label(text="Measurements & Info (情報パネル)", icon='INFO')
box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
layout.separator()
layout.operator(OBS_SKY_OT_CopyInitValues.bl_idname, icon='COPYDOWN', text="初期設定値コードをコピー")
class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = "OBS_SKY_PT_AdminPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')
# ======================================================================
# Registration
# ======================================================================
setup_retry_count = 0
def init_addon_setup():
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50: return None
return 0.1
try:
props = bpy.context.scene.obs_sky_props
generate_sky_system(props, bpy.context)
except Exception as e:
print(f"Observer Sky Auto-generate Error: {e}")
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
if hasattr(space, 'shading'):
space.shading.type = 'MATERIAL'
return None
classes = (
ObserverSkyProperties,
OBS_SKY_OT_Generate,
OBS_SKY_OT_RegenerateStars,
OBS_SKY_OT_SetCamera,
OBS_SKY_OT_CopyInfo,
OBS_SKY_OT_CopyInitValues,
OBS_SKY_OT_RemoveAddon,
OBS_SKY_PT_MainPanel,
OBS_SKY_PT_AdminPanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
if not bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.register(init_addon_setup, first_interval=0.1)
def unregister():
if bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.unregister(init_addon_setup)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.obs_sky_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Observer Sky Model with Concentric Sphere",
"author": "AI Assistant",
"version": (1, 10, 3),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations of Earth, Outer Sphere, Celestial Sphere, and Ray Arrow.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.00
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.100, 0.400, 0.800, 0.300)
INIT_EARTH_EQUATOR = True
INIT_OUTER_RADIUS = 10.00
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.500, 0.800, 0.800, 0.100)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.020
INIT_OBS_THETA = 0.00
INIT_OBS_SHOW = False
INIT_MARKER_O_SHOW = False
INIT_SKY_RADIUS = 5.00
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.300, 0.100, 0.500, 0.150)
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.00
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.082, 0.063, 0.081, 0.800)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.050
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.00
INIT_ARROW_LAT = 30.00
INIT_ARROW_THICKNESS = 0.500
INIT_ARROW_COLOR = (0.105, 1.000, 0.000, 1.000)
INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.050
INIT_STAR_COLOR = (1.000, 1.000, 0.800, 1.000)
INIT_STAR_EMISSION = 5.00
INIT_STAR_SEED = 42
INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True
INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
# ======================================================================
# ユーティリティ関数群
# ======================================================================
def get_or_create_collection(name):
if name in bpy.data.collections:
return bpy.data.collections[name]
new_col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(new_col)
return new_col
def safe_remove_collection_contents(col_name):
if col_name not in bpy.data.collections:
return
col = bpy.data.collections[col_name]
for obj in list(col.objects):
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data:
if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
bpy.data.curves.remove(mesh_data)
def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
principled.inputs['Base Color'].default_value = color
principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
principled.inputs['Emission Strength'].default_value = emission
principled.inputs['Alpha'].default_value = alpha
if is_transparent and alpha < 1.0:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
else:
mat.blend_method = 'OPAQUE'
mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
return mat
def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
bm.to_mesh(mesh)
bm.free()
obj.location = location
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_arrow(name, p1, p2, thickness, mat, collection):
direction = p2 - p1
length = direction.length
if length < 1e-4:
return None
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
head_len = min(thickness * 4.0, length * 0.2)
shaft_len = length - head_len
# 軸 (Shaft)
ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
# 先端 (Head)
ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
# 回転と移動 (p1 から p2 を向くように)
z_axis = mathutils.Vector((0, 0, 1))
rot_quat = z_axis.rotation_difference(direction)
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_stars(name, P, radius, count, size, seed, color, emission, collection):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
obj.data.materials.append(mat)
bm = bmesh.new()
random.seed(seed)
star_positions = []
for _ in range(count):
z = random.uniform(-1.0, 1.0)
phi = random.uniform(0.0, 2.0 * math.pi)
x = math.sqrt(1.0 - z*z) * math.cos(phi)
y = math.sqrt(1.0 - z*z) * math.sin(phi)
pos = P + mathutils.Vector((x, y, z)) * radius
star_positions.append(pos)
geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
bm.to_mesh(mesh)
bm.free()
return obj, star_positions
def create_line(name, p1, p2, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
v1 = bm.verts.new(p1)
v2 = bm.verts.new(p2)
bm.edges.new((v1, v2))
bm.to_mesh(mesh)
bm.free()
return obj
def create_circle(name, radius, location, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
bm.to_mesh(mesh)
bm.free()
obj.location = location
return obj
def create_text(name, text, location, collection, scale=1.0, mat=None):
curve = bpy.data.curves.new(type="FONT", name=name)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
obj = bpy.data.objects.new(name, curve)
collection.objects.link(obj)
obj.location = location
obj.scale = (scale, scale, scale)
if mat:
obj.data.materials.append(mat)
constraint = obj.constraints.new(type='TRACK_TO')
constraint.track_axis = 'TRACK_Z'
constraint.up_axis = 'UP_Y'
return obj, constraint
def setup_camera(name, location, target_loc, collection):
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
collection.objects.link(cam_obj)
cam_obj.location = location
direction = target_loc - mathutils.Vector(location)
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
return cam_obj
# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
R = props.earth_radius
theta = math.radians(props.obs_longitude)
# P: 地表(観測者=天球の中心)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
# T: 天球とアウター球の接点(4番目の球体の中心)
R_sky = props.sky_radius
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
# 矢印の出発地(4番目の球体上のUV座標から計算)
lon = math.radians(props.arrow_start_lon)
lat = math.radians(props.arrow_start_lat)
R_T = props.tangent_radius
Start = T + mathutils.Vector((
R_T * math.cos(lat) * math.cos(lon),
R_T * math.cos(lat) * math.sin(lon),
R_T * math.sin(lat)
))
# 矢印の到達地(地表1点=天球の中心P)
End = P
arrow_len = (End - Start).length
dist_end_surf = abs(End.length - R)
dist_start_surf = abs(Start.length - R)
return {
"start": Start,
"end": End,
"arrow_len": arrow_len,
"dist_end_surf": dist_end_surf,
"dist_start_surf": dist_start_surf,
"dist_end_center": End.length,
"dist_start_center": Start.length,
}
# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
safe_remove_collection_contents(COLLECTION_NAME)
col = get_or_create_collection(COLLECTION_NAME)
O = mathutils.Vector((0, 0, 0))
R = props.earth_radius
R_outer = max(props.outer_sphere_radius, R + 0.01)
# 座標情報の計算
measurements = calculate_measurements(props)
P = measurements["end"]
T = P + mathutils.Vector((math.cos(math.radians(props.obs_longitude)), math.sin(math.radians(props.obs_longitude)), 0)) * props.sky_radius
Start = measurements["start"]
# マテリアル設定
mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
# 1. Earth
if props.earth_show:
create_sphere("Earth", R, O, col, mat_earth)
if props.earth_equator:
create_circle(PREFIX+"Equator", R, O, col, mat_line)
create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)
# 2. Outer Sphere
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.outer_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
if props.show_o_earth:
dir_e = mathutils.Vector((-0.707, 0.707, 0))
P_e = O + dir_e * R
create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
if props.show_o_outer and props.outer_show:
dir_o = mathutils.Vector((-0.707, -0.707, 0))
P_o = O + dir_o * R_outer
create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
# 3. P (Observer) & Sky
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
if props.show_op_line:
create_line(PREFIX+"Line_OP", O, P, col, mat_line)
if props.show_distance:
mid_point = (O + P) / 2.0
mid_point.z += R * 0.05
create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", mid_point, col, scale=R_outer*0.025, mat=mat_line)
if props.sky_show:
create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
# 4. Tangent Wire Sphere (4番目の骨格メッシュ球体)
if props.tangent_show:
tangent_obj = create_sphere(
"TangentWireSphere",
props.tangent_radius,
T, col,
mat_tangent,
segments=props.tangent_segments,
rings=props.tangent_rings
)
wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.tangent_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
# 5. Arrow (矢印) の生成
if props.arrow_show:
create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
# 6. Stars
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(
PREFIX+"Stars", P, props.sky_radius,
props.star_count, props.star_size, props.star_seed,
tuple(props.star_color), props.star_emission, col
)
if props.show_p_star_line and len(star_positions) > 0:
random.seed(props.star_seed + 1)
sample_stars = random.sample(star_positions, min(3, len(star_positions)))
for i, s_pos in enumerate(sample_stars):
create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
if props.show_distance:
mid_s = (P + s_pos) / 2.0
create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", mid_s, col, scale=R_outer*0.025, mat=mat_line_p_star)
if props.show_axes:
create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)
# 7. Cameras
setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
for obj in col.objects:
if obj.type == 'FONT':
for cons in obj.constraints:
if cons.type == 'TRACK_TO':
cons.target = context.scene.camera
def update_callback(self, context):
if self.outer_sphere_radius <= self.earth_radius:
self.outer_sphere_radius = self.earth_radius + 0.1
try:
generate_sky_system(self, context)
except Exception as e:
print(f"Observer Sky Update Error: {e}")
# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
# Marker
obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)
# Earth
earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)
# Outer Sphere
outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)
# Observer
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Sky Sphere
sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)
# Tangent Wire Sphere
tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
# Arrow (Ray)
arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)
# Stars
star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)
# Visualizations
show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)
# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
bl_idname = "obs_sky.generate"
bl_label = "Generate / Update Model"
def execute(self, context):
props = context.scene.obs_sky_props
generate_sky_system(props, context)
return {'FINISHED'}
class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
bl_idname = "obs_sky.regenerate_stars"
bl_label = "Randomize Stars"
def execute(self, context):
props = context.scene.obs_sky_props
props.star_seed = random.randint(0, 10000)
return {'FINISHED'}
class OBS_SKY_OT_SetCamera(bpy.types.Operator):
bl_idname = "obs_sky.set_camera"
bl_label = "Set Camera"
cam_type: bpy.props.StringProperty()
def execute(self, context):
cam_name = PREFIX + self.cam_type
if cam_name in bpy.data.objects:
context.scene.camera = bpy.data.objects[cam_name]
return {'FINISHED'}
class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
bl_idname = "obs_sky.copy_info"
bl_label = "情報をクリップボードにコピー"
bl_description = "Copy measurement info to clipboard"
def execute(self, context):
props = context.scene.obs_sky_props
meas = calculate_measurements(props)
info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
[地球表面から先端への距離]: {meas['dist_end_surf']:.3f}
[地球表面から後端への距離]: {meas['dist_start_surf']:.3f}
[地球中心(O)から先端への距離]: {meas['dist_end_center']:.3f}
[地球中心(O)から後端への距離]: {meas['dist_start_center']:.3f}
"""
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_CopyInitValues(bpy.types.Operator):
bl_idname = "obs_sky.copy_init_values"
bl_label = "初期設定値コードをコピー"
bl_description = "現在のパラメーターをPythonコードの初期設定値ブロックとしてコピーします"
def execute(self, context):
props = context.scene.obs_sky_props
copy_text = f"""# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = {props.earth_radius:.2f}
INIT_EARTH_SHOW = {props.earth_show}
INIT_EARTH_COLOR = ({props.earth_color[0]:.3f}, {props.earth_color[1]:.3f}, {props.earth_color[2]:.3f}, {props.earth_color[3]:.3f})
INIT_EARTH_EQUATOR = {props.earth_equator}
INIT_OUTER_RADIUS = {props.outer_sphere_radius:.2f}
INIT_OUTER_SHOW = {props.outer_show}
INIT_OUTER_COLOR = ({props.outer_color[0]:.3f}, {props.outer_color[1]:.3f}, {props.outer_color[2]:.3f}, {props.outer_color[3]:.3f})
INIT_OUTER_WIRE = {props.outer_wire}
INIT_OUTER_WIRE_THICKNESS = {props.outer_wire_thickness:.3f}
INIT_OBS_THETA = {props.obs_longitude:.2f}
INIT_OBS_SHOW = {props.obs_show}
INIT_MARKER_O_SHOW = {props.marker_o_show}
INIT_SKY_RADIUS = {props.sky_radius:.2f}
INIT_SKY_SHOW = {props.sky_show}
INIT_SKY_COLOR = ({props.sky_color[0]:.3f}, {props.sky_color[1]:.3f}, {props.sky_color[2]:.3f}, {props.sky_color[3]:.3f})
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = {props.tangent_radius:.2f}
INIT_TANGENT_SHOW = {props.tangent_show}
INIT_TANGENT_COLOR = ({props.tangent_color[0]:.3f}, {props.tangent_color[1]:.3f}, {props.tangent_color[2]:.3f}, {props.tangent_color[3]:.3f})
INIT_TANGENT_SEGMENTS = {props.tangent_segments}
INIT_TANGENT_RINGS = {props.tangent_rings}
INIT_TANGENT_WIRE_THICKNESS = {props.tangent_wire_thickness:.3f}
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = {props.arrow_show}
INIT_ARROW_LON = {props.arrow_start_lon:.2f}
INIT_ARROW_LAT = {props.arrow_start_lat:.2f}
INIT_ARROW_THICKNESS = {props.arrow_thickness:.3f}
INIT_ARROW_COLOR = ({props.arrow_color[0]:.3f}, {props.arrow_color[1]:.3f}, {props.arrow_color[2]:.3f}, {props.arrow_color[3]:.3f})
INIT_STAR_COUNT = {props.star_count}
INIT_STAR_SIZE = {props.star_size:.3f}
INIT_STAR_COLOR = ({props.star_color[0]:.3f}, {props.star_color[1]:.3f}, {props.star_color[2]:.3f}, {props.star_color[3]:.3f})
INIT_STAR_EMISSION = {props.star_emission:.2f}
INIT_STAR_SEED = {props.star_seed}
INIT_SHOW_OP_LINE = {props.show_op_line}
INIT_SHOW_P_STAR_LINE = {props.show_p_star_line}
INIT_SHOW_DISTANCE = {props.show_distance}
INIT_SHOW_AXES = {props.show_axes}
INIT_SHOW_O_EARTH = {props.show_o_earth}
INIT_SHOW_O_OUTER = {props.show_o_outer}
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
"""
context.window_manager.clipboard = copy_text
self.report({'INFO'}, "現在のパラメーターを初期設定値としてクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
bl_label = "Observer Sky Model"
bl_idname = "OBS_SKY_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
def draw(self, context):
layout = self.layout
props = context.scene.obs_sky_props
# 1. Top
box = layout.box()
box.prop(props, "obs_show", icon='USER')
box.prop(props, "marker_o_show", icon='WORLD')
layout.separator()
layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')
# ======================================================
# 4番目の球体と矢印の設定を一番上に配置
# ======================================================
# 4番目の球体 (Tangent Sphere)
box = layout.box()
box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
box.prop(props, "tangent_show")
if props.tangent_show:
box.prop(props, "tangent_radius")
box.prop(props, "tangent_wire_thickness")
row_div = box.row()
row_div.prop(props, "tangent_segments")
row_div.prop(props, "tangent_rings")
box.prop(props, "tangent_color")
# 矢印 (Arrow / Ray) の独立パネル
box = layout.box()
box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
box.prop(props, "arrow_show")
if props.arrow_show:
box.prop(props, "arrow_start_lon")
box.prop(props, "arrow_start_lat")
box.prop(props, "arrow_thickness")
box.prop(props, "arrow_color")
# ======================================================
# 2. Earth
box = layout.box()
box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
box.prop(props, "earth_radius")
row = box.row()
row.prop(props, "earth_show")
row.prop(props, "earth_equator")
box.prop(props, "earth_color")
# 3. Outer Sphere
box.separator()
box.label(text="Outer Sphere [000]:")
box.prop(props, "outer_sphere_radius")
row = box.row()
row.prop(props, "outer_show")
row.prop(props, "outer_wire")
if props.outer_wire:
box.prop(props, "outer_wire_thickness")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
# 4. Observer
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
# 5. Sky Sphere
box = layout.box()
box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
box.prop(props, "sky_radius")
box.prop(props, "sky_show")
box.prop(props, "sky_color")
# 6. Stars
box = layout.box()
box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
box.prop(props, "star_count")
box.prop(props, "star_size")
box.prop(props, "star_color")
box.prop(props, "star_emission")
box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
# 7. Vis & Cam
box = layout.box()
box.label(text="Visualization / Vectors", icon='GIZMO')
col = box.column(align=True)
col.prop(props, "show_op_line")
col.prop(props, "show_p_star_line")
col.prop(props, "show_distance")
col.prop(props, "show_axes")
box = layout.box()
box.label(text="Camera View", icon='CAMERA_DATA')
col = box.column(align=True)
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
# 8. Measurements (Info)
meas = calculate_measurements(props)
box = layout.box()
box.label(text="Measurements & Info (情報パネル)", icon='INFO')
col = box.column(align=True)
col.label(text=f"矢印の長さ: {meas['arrow_len']:.2f}")
col.label(text=f"先端位置(P): ({meas['end'].x:.2f}, {meas['end'].y:.2f}, {meas['end'].z:.2f})")
col.label(text=f"後端位置(UV): ({meas['start'].x:.2f}, {meas['start'].y:.2f}, {meas['start'].z:.2f})")
col.separator()
col.label(text=f"地表から先端への距離: {meas['dist_end_surf']:.2f}")
col.label(text=f"地表から後端への距離: {meas['dist_start_surf']:.2f}")
col.separator()
col.label(text=f"中心(O)から先端への距離: {meas['dist_end_center']:.2f}")
col.label(text=f"中心(O)から後端への距離: {meas['dist_start_center']:.2f}")
box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
# === 追加したコピー用ボタン ===
layout.separator()
layout.operator(OBS_SKY_OT_CopyInitValues.bl_idname, icon='COPYDOWN', text="初期設定値コードをコピー")
class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = "OBS_SKY_PT_AdminPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')
# ======================================================================
# Auto-setup on Registration
# ======================================================================
setup_retry_count = 0
def init_addon_setup():
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50:
return None
return 0.1
try:
props = bpy.context.scene.obs_sky_props
generate_sky_system(props, bpy.context)
except Exception as e:
print(f"Observer Sky Auto-generate Error: {e}")
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
if hasattr(space, 'shading'):
space.shading.type = 'MATERIAL'
return None
# ======================================================================
# Registration
# ======================================================================
classes = (
ObserverSkyProperties,
OBS_SKY_OT_Generate,
OBS_SKY_OT_RegenerateStars,
OBS_SKY_OT_SetCamera,
OBS_SKY_OT_CopyInfo,
OBS_SKY_OT_CopyInitValues,
OBS_SKY_OT_RemoveAddon,
OBS_SKY_PT_MainPanel,
OBS_SKY_PT_AdminPanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
if not bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.register(init_addon_setup, first_interval=0.1)
def unregister():
if bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.unregister(init_addon_setup)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.obs_sky_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Observer Sky Model with Concentric Sphere",
"author": "AI Assistant",
"version": (1, 10, 2),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations of Earth, Outer Sphere, Celestial Sphere, and Ray Arrow.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.0
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.1, 0.4, 0.8, 0.3)
INIT_EARTH_EQUATOR = True
INIT_OUTER_RADIUS = 10.0
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.5, 0.8, 0.8, 0.1)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.02
INIT_OBS_THETA = 0.0
INIT_OBS_SHOW = True
INIT_MARKER_O_SHOW = True
INIT_SKY_RADIUS = 5.0
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.3, 0.1, 0.5, 0.15)
# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.0
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.9, 0.5, 0.2, 0.8)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.05
# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.0
INIT_ARROW_LAT = 90.0
INIT_ARROW_THICKNESS = 0.1
INIT_ARROW_COLOR = (1.0, 0.2, 0.2, 1.0)
INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.05
INIT_STAR_COLOR = (1.0, 1.0, 0.8, 1.0)
INIT_STAR_EMISSION = 5.0
INIT_STAR_SEED = 42
INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True
INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True
PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"
# ======================================================================
# ユーティリティ関数群
# ======================================================================
def get_or_create_collection(name):
if name in bpy.data.collections:
return bpy.data.collections[name]
new_col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(new_col)
return new_col
def safe_remove_collection_contents(col_name):
if col_name not in bpy.data.collections:
return
col = bpy.data.collections[col_name]
for obj in list(col.objects):
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data:
if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
bpy.data.curves.remove(mesh_data)
def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.clear()
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (400, 0)
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
principled.inputs['Base Color'].default_value = color
principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
principled.inputs['Emission Strength'].default_value = emission
principled.inputs['Alpha'].default_value = alpha
if is_transparent and alpha < 1.0:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
else:
mat.blend_method = 'OPAQUE'
mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
return mat
def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
bm.to_mesh(mesh)
bm.free()
obj.location = location
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_arrow(name, p1, p2, thickness, mat, collection):
direction = p2 - p1
length = direction.length
if length < 1e-4:
return None
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
head_len = min(thickness * 4.0, length * 0.2)
shaft_len = length - head_len
# 軸 (Shaft)
ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
# 先端 (Head)
ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
# 回転と移動 (p1 から p2 を向くように)
z_axis = mathutils.Vector((0, 0, 1))
rot_quat = z_axis.rotation_difference(direction)
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons:
poly.use_smooth = True
return obj
def create_stars(name, P, radius, count, size, seed, color, emission, collection):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
obj.data.materials.append(mat)
bm = bmesh.new()
random.seed(seed)
star_positions = []
for _ in range(count):
z = random.uniform(-1.0, 1.0)
phi = random.uniform(0.0, 2.0 * math.pi)
x = math.sqrt(1.0 - z*z) * math.cos(phi)
y = math.sqrt(1.0 - z*z) * math.sin(phi)
pos = P + mathutils.Vector((x, y, z)) * radius
star_positions.append(pos)
geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
bm.to_mesh(mesh)
bm.free()
return obj, star_positions
def create_line(name, p1, p2, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
v1 = bm.verts.new(p1)
v2 = bm.verts.new(p2)
bm.edges.new((v1, v2))
bm.to_mesh(mesh)
bm.free()
return obj
def create_circle(name, radius, location, collection, mat=None):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
if mat:
obj.data.materials.append(mat)
bm = bmesh.new()
bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
bm.to_mesh(mesh)
bm.free()
obj.location = location
return obj
def create_text(name, text, location, collection, scale=1.0, mat=None):
curve = bpy.data.curves.new(type="FONT", name=name)
curve.body = text
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
obj = bpy.data.objects.new(name, curve)
collection.objects.link(obj)
obj.location = location
obj.scale = (scale, scale, scale)
if mat:
obj.data.materials.append(mat)
constraint = obj.constraints.new(type='TRACK_TO')
constraint.track_axis = 'TRACK_Z'
constraint.up_axis = 'UP_Y'
return obj, constraint
def setup_camera(name, location, target_loc, collection):
cam_data = bpy.data.cameras.new(name)
cam_obj = bpy.data.objects.new(name, cam_data)
collection.objects.link(cam_obj)
cam_obj.location = location
direction = target_loc - mathutils.Vector(location)
rot_quat = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_quat.to_euler()
return cam_obj
# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
R = props.earth_radius
theta = math.radians(props.obs_longitude)
# P: 地表(観測者=天球の中心)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
# T: 天球とアウター球の接点(4番目の球体の中心)
R_sky = props.sky_radius
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
# 矢印の出発地(4番目の球体上のUV座標から計算)
lon = math.radians(props.arrow_start_lon)
lat = math.radians(props.arrow_start_lat)
R_T = props.tangent_radius
Start = T + mathutils.Vector((
R_T * math.cos(lat) * math.cos(lon),
R_T * math.cos(lat) * math.sin(lon),
R_T * math.sin(lat)
))
# 矢印の到達地(地表1点=天球の中心P)
End = P
arrow_len = (End - Start).length
dist_end_surf = abs(End.length - R)
dist_start_surf = abs(Start.length - R)
return {
"start": Start,
"end": End,
"arrow_len": arrow_len,
"dist_end_surf": dist_end_surf,
"dist_start_surf": dist_start_surf,
"dist_end_center": End.length,
"dist_start_center": Start.length,
}
# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
safe_remove_collection_contents(COLLECTION_NAME)
col = get_or_create_collection(COLLECTION_NAME)
O = mathutils.Vector((0, 0, 0))
R = props.earth_radius
R_outer = max(props.outer_sphere_radius, R + 0.01)
# 座標情報の計算
measurements = calculate_measurements(props)
P = measurements["end"]
T = P + mathutils.Vector((math.cos(math.radians(props.obs_longitude)), math.sin(math.radians(props.obs_longitude)), 0)) * props.sky_radius
Start = measurements["start"]
# マテリアル設定
mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
# 1. Earth
if props.earth_show:
create_sphere("Earth", R, O, col, mat_earth)
if props.earth_equator:
create_circle(PREFIX+"Equator", R, O, col, mat_line)
create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)
# 2. Outer Sphere
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.outer_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
if props.show_o_earth:
dir_e = mathutils.Vector((-0.707, 0.707, 0))
P_e = O + dir_e * R
create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
if props.show_o_outer and props.outer_show:
dir_o = mathutils.Vector((-0.707, -0.707, 0))
P_o = O + dir_o * R_outer
create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)
# 3. P (Observer) & Sky
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
if props.show_op_line:
create_line(PREFIX+"Line_OP", O, P, col, mat_line)
if props.show_distance:
mid_point = (O + P) / 2.0
mid_point.z += R * 0.05
create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", mid_point, col, scale=R_outer*0.025, mat=mat_line)
if props.sky_show:
create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
# 4. Tangent Wire Sphere (4番目の骨格メッシュ球体)
if props.tangent_show:
tangent_obj = create_sphere(
"TangentWireSphere",
props.tangent_radius,
T, col,
mat_tangent,
segments=props.tangent_segments,
rings=props.tangent_rings
)
wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
wire_mod.thickness = props.tangent_wire_thickness
wire_mod.use_replace = True
wire_mod.use_even_offset = True
# 5. Arrow (矢印) の生成
if props.arrow_show:
create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
# 6. Stars
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(
PREFIX+"Stars", P, props.sky_radius,
props.star_count, props.star_size, props.star_seed,
tuple(props.star_color), props.star_emission, col
)
if props.show_p_star_line and len(star_positions) > 0:
random.seed(props.star_seed + 1)
sample_stars = random.sample(star_positions, min(3, len(star_positions)))
for i, s_pos in enumerate(sample_stars):
create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
if props.show_distance:
mid_s = (P + s_pos) / 2.0
create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", mid_s, col, scale=R_outer*0.025, mat=mat_line_p_star)
if props.show_axes:
create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)
# 7. Cameras
setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
for obj in col.objects:
if obj.type == 'FONT':
for cons in obj.constraints:
if cons.type == 'TRACK_TO':
cons.target = context.scene.camera
def update_callback(self, context):
if self.outer_sphere_radius <= self.earth_radius:
self.outer_sphere_radius = self.earth_radius + 0.1
try:
generate_sky_system(self, context)
except Exception as e:
print(f"Observer Sky Update Error: {e}")
# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
# Marker
obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)
# Earth
earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)
# Outer Sphere
outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)
# Observer
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Sky Sphere
sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)
# Tangent Wire Sphere
tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
# Arrow (Ray)
arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)
# Stars
star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)
# Visualizations
show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)
# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
bl_idname = "obs_sky.generate"
bl_label = "Generate / Update Model"
def execute(self, context):
props = context.scene.obs_sky_props
generate_sky_system(props, context)
return {'FINISHED'}
class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
bl_idname = "obs_sky.regenerate_stars"
bl_label = "Randomize Stars"
def execute(self, context):
props = context.scene.obs_sky_props
props.star_seed = random.randint(0, 10000)
return {'FINISHED'}
class OBS_SKY_OT_SetCamera(bpy.types.Operator):
bl_idname = "obs_sky.set_camera"
bl_label = "Set Camera"
cam_type: bpy.props.StringProperty()
def execute(self, context):
cam_name = PREFIX + self.cam_type
if cam_name in bpy.data.objects:
context.scene.camera = bpy.data.objects[cam_name]
return {'FINISHED'}
class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
bl_idname = "obs_sky.copy_info"
bl_label = "情報をクリップボードにコピー"
bl_description = "Copy measurement info to clipboard"
def execute(self, context):
props = context.scene.obs_sky_props
meas = calculate_measurements(props)
info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
[地球表面から先端への距離]: {meas['dist_end_surf']:.3f}
[地球表面から後端への距離]: {meas['dist_start_surf']:.3f}
[地球中心(O)から先端への距離]: {meas['dist_end_center']:.3f}
[地球中心(O)から後端への距離]: {meas['dist_start_center']:.3f}
"""
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
def execute(self, context):
bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
return {'FINISHED'}
# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
bl_label = "Observer Sky Model"
bl_idname = "OBS_SKY_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
def draw(self, context):
layout = self.layout
props = context.scene.obs_sky_props
# 1. Top
box = layout.box()
box.prop(props, "obs_show", icon='USER')
box.prop(props, "marker_o_show", icon='WORLD')
layout.separator()
layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')
# ======================================================
# 4番目の球体と矢印の設定を一番上に配置
# ======================================================
# 4番目の球体 (Tangent Sphere)
box = layout.box()
box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
box.prop(props, "tangent_show")
if props.tangent_show:
box.prop(props, "tangent_radius")
box.prop(props, "tangent_wire_thickness")
row_div = box.row()
row_div.prop(props, "tangent_segments")
row_div.prop(props, "tangent_rings")
box.prop(props, "tangent_color")
# 矢印 (Arrow / Ray) の独立パネル
box = layout.box()
box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
box.prop(props, "arrow_show")
if props.arrow_show:
box.prop(props, "arrow_start_lon")
box.prop(props, "arrow_start_lat")
box.prop(props, "arrow_thickness")
box.prop(props, "arrow_color")
# ======================================================
# 2. Earth
box = layout.box()
box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
box.prop(props, "earth_radius")
row = box.row()
row.prop(props, "earth_show")
row.prop(props, "earth_equator")
box.prop(props, "earth_color")
# 3. Outer Sphere
box.separator()
box.label(text="Outer Sphere [000]:")
box.prop(props, "outer_sphere_radius")
row = box.row()
row.prop(props, "outer_show")
row.prop(props, "outer_wire")
if props.outer_wire:
box.prop(props, "outer_wire_thickness")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
# 4. Observer
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
# 5. Sky Sphere
box = layout.box()
box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
box.prop(props, "sky_radius")
box.prop(props, "sky_show")
box.prop(props, "sky_color")
# 6. Stars
box = layout.box()
box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
box.prop(props, "star_count")
box.prop(props, "star_size")
box.prop(props, "star_color")
box.prop(props, "star_emission")
box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
# 7. Vis & Cam
box = layout.box()
box.label(text="Visualization / Vectors", icon='GIZMO')
col = box.column(align=True)
col.prop(props, "show_op_line")
col.prop(props, "show_p_star_line")
col.prop(props, "show_distance")
col.prop(props, "show_axes")
box = layout.box()
box.label(text="Camera View", icon='CAMERA_DATA')
col = box.column(align=True)
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
# 8. Measurements (Info)
meas = calculate_measurements(props)
box = layout.box()
box.label(text="Measurements & Info (情報パネル)", icon='INFO')
col = box.column(align=True)
col.label(text=f"矢印の長さ: {meas['arrow_len']:.2f}")
col.label(text=f"先端位置(P): ({meas['end'].x:.2f}, {meas['end'].y:.2f}, {meas['end'].z:.2f})")
col.label(text=f"後端位置(UV): ({meas['start'].x:.2f}, {meas['start'].y:.2f}, {meas['start'].z:.2f})")
col.separator()
col.label(text=f"地表から先端への距離: {meas['dist_end_surf']:.2f}")
col.label(text=f"地表から後端への距離: {meas['dist_start_surf']:.2f}")
col.separator()
col.label(text=f"中心(O)から先端への距離: {meas['dist_end_center']:.2f}")
col.label(text=f"中心(O)から後端への距離: {meas['dist_start_center']:.2f}")
box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
bl_label = "Admin / Remove"
bl_idname = "OBS_SKY_PT_AdminPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Observer Sky"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')
# ======================================================================
# Auto-setup on Registration
# ======================================================================
setup_retry_count = 0
def init_addon_setup():
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50:
return None
return 0.1
try:
props = bpy.context.scene.obs_sky_props
generate_sky_system(props, bpy.context)
except Exception as e:
print(f"Observer Sky Auto-generate Error: {e}")
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
if hasattr(space, 'shading'):
space.shading.type = 'MATERIAL'
return None
# ======================================================================
# Registration
# ======================================================================
classes = (
ObserverSkyProperties,
OBS_SKY_OT_Generate,
OBS_SKY_OT_RegenerateStars,
OBS_SKY_OT_SetCamera,
OBS_SKY_OT_CopyInfo,
OBS_SKY_OT_RemoveAddon,
OBS_SKY_PT_MainPanel,
OBS_SKY_PT_AdminPanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
if not bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.register(init_addon_setup, first_interval=0.1)
def unregister():
if bpy.app.timers.is_registered(init_addon_setup):
bpy.app.timers.unregister(init_addon_setup)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.obs_sky_props
if __name__ == "__main__":
register()