https://note.com/sane_agapan7804/n/nf8e2bbff2c98
https://note.com/sane_agapan7804/n/n9a8e7b28f200
アウター天球の「骨格メッシュ化」
表示形式だけのワイヤーフレームではなく、4番目の球体と同じく実際に太さのある骨格メッシュ(WIREFRAMEモディファイア)に変更し、太さ調整ができるようにしました。
矢印(Ray)の追加とUV座標による起点設定
4番目の球体の表面上の点を「経度(Longitude)と緯度(Latitude)」を使ってUV座標的に指定し、そこから地表(天球の中心P)へ向かう矢印を描画します。矢印の太さと色も調整可能です。
情報パネルとコピーボタン
パネルの最下部に「Measurements & Info」セクションを追加し、
矢印の長さ
先端と後端のXYZ座標
地球表面から先端・後端への距離
地球中心(O)から先端・後端への距離
をリアルタイムで表示します。また、ボタン一つでこの情報をクリップボードにコピーできます。
bl_info = {
"name": "Observer Sky Model with Concentric Sphere",
"author": "AI Assistant",
"version": (1, 10, 1),
"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
import inspect
import sys
import os
import re
# ======================================================================
# 初期設定値
# ======================================================================
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_ExportCode(bpy.types.Operator):
bl_idname = "obs_sky.export_code"
bl_label = "設定をコードに書き出し"
def execute(self, context):
props = context.scene.obs_sky_props
source_code = ""
if context.space_data and context.space_data.type == 'TEXT_EDITOR':
source_code = context.space_data.text.as_string()
else:
try:
module_path = inspect.getfile(sys.modules[__name__])
if os.path.exists(module_path):
with open(module_path, 'r', encoding='utf-8') as f:
source_code = f.read()
except Exception as e:
self.report({'ERROR'}, f"Failed to get source code: {e}")
return {'CANCELLED'}
if not source_code:
self.report({'ERROR'}, "Source code is empty.")
return {'CANCELLED'}
replacements = {
"INIT_EARTH_RADIUS": f"{props.earth_radius:.2f}",
"INIT_EARTH_SHOW": str(props.earth_show),
"INIT_EARTH_COLOR": f"({props.earth_color[0]:.3f}, {props.earth_color[1]:.3f}, {props.earth_color[2]:.3f}, {props.earth_color[3]:.3f})",
"INIT_EARTH_EQUATOR": str(props.earth_equator),
"INIT_OUTER_RADIUS": f"{props.outer_sphere_radius:.2f}",
"INIT_OUTER_SHOW": str(props.outer_show),
"INIT_OUTER_COLOR": f"({props.outer_color[0]:.3f}, {props.outer_color[1]:.3f}, {props.outer_color[2]:.3f}, {props.outer_color[3]:.3f})",
"INIT_OUTER_WIRE": str(props.outer_wire),
"INIT_OUTER_WIRE_THICKNESS": f"{props.outer_wire_thickness:.3f}",
"INIT_SHOW_O_EARTH": str(props.show_o_earth),
"INIT_SHOW_O_OUTER": str(props.show_o_outer),
"INIT_OBS_THETA": f"{props.obs_longitude:.2f}",
"INIT_OBS_SHOW": str(props.obs_show),
"INIT_MARKER_O_SHOW": str(props.marker_o_show),
"INIT_SKY_RADIUS": f"{props.sky_radius:.2f}",
"INIT_SKY_SHOW": str(props.sky_show),
"INIT_SKY_COLOR": f"({props.sky_color[0]:.3f}, {props.sky_color[1]:.3f}, {props.sky_color[2]:.3f}, {props.sky_color[3]:.3f})",
"INIT_TANGENT_RADIUS": f"{props.tangent_radius:.2f}",
"INIT_TANGENT_SHOW": str(props.tangent_show),
"INIT_TANGENT_COLOR": f"({props.tangent_color[0]:.3f}, {props.tangent_color[1]:.3f}, {props.tangent_color[2]:.3f}, {props.tangent_color[3]:.3f})",
"INIT_TANGENT_SEGMENTS": str(props.tangent_segments),
"INIT_TANGENT_RINGS": str(props.tangent_rings),
"INIT_TANGENT_WIRE_THICKNESS": f"{props.tangent_wire_thickness:.3f}",
"INIT_ARROW_SHOW": str(props.arrow_show),
"INIT_ARROW_LON": f"{props.arrow_start_lon:.2f}",
"INIT_ARROW_LAT": f"{props.arrow_start_lat:.2f}",
"INIT_ARROW_THICKNESS": f"{props.arrow_thickness:.3f}",
"INIT_ARROW_COLOR": f"({props.arrow_color[0]:.3f}, {props.arrow_color[1]:.3f}, {props.arrow_color[2]:.3f}, {props.arrow_color[3]:.3f})",
"INIT_STAR_COUNT": str(props.star_count),
"INIT_STAR_SIZE": f"{props.star_size:.3f}",
"INIT_STAR_COLOR": f"({props.star_color[0]:.3f}, {props.star_color[1]:.3f}, {props.star_color[2]:.3f}, {props.star_color[3]:.3f})",
"INIT_STAR_EMISSION": f"{props.star_emission:.2f}",
"INIT_STAR_SEED": str(props.star_seed),
"INIT_SHOW_OP_LINE": str(props.show_op_line),
"INIT_SHOW_P_STAR_LINE": str(props.show_p_star_line),
"INIT_SHOW_DISTANCE": str(props.show_distance),
"INIT_SHOW_AXES": str(props.show_axes),
}
for key, val in replacements.items():
pattern = rf"^{key}\s*=\s*.*$"
replacement = f"{key} = {val}"
source_code = re.sub(pattern, replacement, source_code, flags=re.MULTILINE)
try:
compile(source_code, '<string>', 'exec')
except SyntaxError as e:
self.report({'ERROR'}, f"Syntax Error in generated code: {e}")
return {'CANCELLED'}
new_text = bpy.data.texts.new(name="ObserverSky_Exported.py")
new_text.write(source_code)
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = new_text
break
self.report({'INFO'}, f"Exported code to {new_text.name}")
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_ExportCode.bl_idname, icon='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_ExportCode,
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, 7, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Observer Sky",
"description": "Visualizes geometric relations of Earth, Outer Sphere, Celestial Sphere, and Tangent point.",
"category": "3D View",
}
import bpy
import bmesh
import math
import random
import mathutils
import inspect
import sys
import os
import re
# ======================================================================
# 初期設定値 (Export機能で置換されるマーカーとしての役割も果たします)
# ======================================================================
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_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 (Wireframe) Settings
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
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:
mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
principled.inputs['Roughness'].default_value = 0.1
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_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 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)
theta = math.radians(props.obs_longitude)
P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
R_sky = props.sky_radius
# 観測者Pから天頂方向へR_sky離れた点 (天球とアウター天球が内接する接点 T)
dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
T = P + dir_OP * R_sky
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_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. O中心の球体群
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)
if props.outer_show:
outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
if props.outer_wire:
outer_obj.display_type = 'WIRE'
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)
# 2. 観測者 P と天球 (P中心)
# O マーカー(赤)
if props.marker_o_show:
create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
# P マーカー(緑)
if props.obs_show:
create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)
# O-P Line
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", R_sky, P, col, mat_sky)
create_circle(PREFIX+"Sky_Equator", R_sky, P, col, mat_line)
# 3. 接点 T を中心とする球体 (ワイヤーフレーム化)
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
# 4. 星の生成
star_positions = []
if props.star_count > 0:
stars_obj, star_positions = create_stars(
PREFIX+"Stars", P, R_sky,
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| = {R_sky:.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)
# 5. カメラ群
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((R_sky*2, 0, R_sky)), 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 (Top of UI)
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)
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 Settings
obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)
# Celestial 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="Show Tangent Wireframe", 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 Lines", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
tangent_rings: bpy.props.IntProperty(name="Latitude Lines", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
tangent_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, 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"
bl_description = "Generates or completely rebuilds the geometric 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"
bl_description = "Changes the random seed to randomize star positions"
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"
bl_description = "Switch the active 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]
else:
self.report({'WARNING'}, f"Camera {cam_name} not found. Generate model first.")
return {'FINISHED'}
class OBS_SKY_OT_ExportCode(bpy.types.Operator):
bl_idname = "obs_sky.export_code"
bl_label = "Export Current Settings to Code"
bl_description = "Generates a complete python script with current settings as initial values"
def execute(self, context):
props = context.scene.obs_sky_props
source_code = ""
if context.space_data and context.space_data.type == 'TEXT_EDITOR':
source_code = context.space_data.text.as_string()
else:
try:
module_path = inspect.getfile(sys.modules[__name__])
if os.path.exists(module_path):
with open(module_path, 'r', encoding='utf-8') as f:
source_code = f.read()
except Exception as e:
self.report({'ERROR'}, f"Failed to get source code: {e}")
return {'CANCELLED'}
if not source_code:
self.report({'ERROR'}, "Source code is empty.")
return {'CANCELLED'}
replacements = {
"INIT_EARTH_RADIUS": f"{props.earth_radius:.2f}",
"INIT_EARTH_SHOW": str(props.earth_show),
"INIT_EARTH_COLOR": f"({props.earth_color[0]:.3f}, {props.earth_color[1]:.3f}, {props.earth_color[2]:.3f}, {props.earth_color[3]:.3f})",
"INIT_EARTH_EQUATOR": str(props.earth_equator),
"INIT_OUTER_RADIUS": f"{props.outer_sphere_radius:.2f}",
"INIT_OUTER_SHOW": str(props.outer_show),
"INIT_OUTER_COLOR": f"({props.outer_color[0]:.3f}, {props.outer_color[1]:.3f}, {props.outer_color[2]:.3f}, {props.outer_color[3]:.3f})",
"INIT_OUTER_WIRE": str(props.outer_wire),
"INIT_SHOW_O_EARTH": str(props.show_o_earth),
"INIT_SHOW_O_OUTER": str(props.show_o_outer),
"INIT_OBS_THETA": f"{props.obs_longitude:.2f}",
"INIT_OBS_SHOW": str(props.obs_show),
"INIT_MARKER_O_SHOW": str(props.marker_o_show),
"INIT_SKY_RADIUS": f"{props.sky_radius:.2f}",
"INIT_SKY_SHOW": str(props.sky_show),
"INIT_SKY_COLOR": f"({props.sky_color[0]:.3f}, {props.sky_color[1]:.3f}, {props.sky_color[2]:.3f}, {props.sky_color[3]:.3f})",
"INIT_TANGENT_RADIUS": f"{props.tangent_radius:.2f}",
"INIT_TANGENT_SHOW": str(props.tangent_show),
"INIT_TANGENT_COLOR": f"({props.tangent_color[0]:.3f}, {props.tangent_color[1]:.3f}, {props.tangent_color[2]:.3f}, {props.tangent_color[3]:.3f})",
"INIT_TANGENT_SEGMENTS": str(props.tangent_segments),
"INIT_TANGENT_RINGS": str(props.tangent_rings),
"INIT_TANGENT_WIRE_THICKNESS": f"{props.tangent_wire_thickness:.3f}",
"INIT_STAR_COUNT": str(props.star_count),
"INIT_STAR_SIZE": f"{props.star_size:.3f}",
"INIT_STAR_COLOR": f"({props.star_color[0]:.3f}, {props.star_color[1]:.3f}, {props.star_color[2]:.3f}, {props.star_color[3]:.3f})",
"INIT_STAR_EMISSION": f"{props.star_emission:.2f}",
"INIT_STAR_SEED": str(props.star_seed),
"INIT_SHOW_OP_LINE": str(props.show_op_line),
"INIT_SHOW_P_STAR_LINE": str(props.show_p_star_line),
"INIT_SHOW_DISTANCE": str(props.show_distance),
"INIT_SHOW_AXES": str(props.show_axes),
}
for key, val in replacements.items():
pattern = rf"^{key}\s*=\s*.*$"
replacement = f"{key} = {val}"
source_code = re.sub(pattern, replacement, source_code, flags=re.MULTILINE)
try:
compile(source_code, '<string>', 'exec')
except SyntaxError as e:
self.report({'ERROR'}, f"Syntax Error in generated code: {e}")
return {'CANCELLED'}
new_text = bpy.data.texts.new(name="ObserverSky_Exported.py")
new_text.write(source_code)
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = new_text
break
self.report({'INFO'}, f"Exported code to {new_text.name}")
return {'FINISHED'}
class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "obs_sky.remove_addon"
bl_label = "アドオン解除"
bl_description = "Unregisters this add-on from the sidebar"
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. トップ配置の表示切替ボタン
# ==========================
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')
# ==========================
# Earth & Concentric
# ==========================
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")
box.prop(props, "outer_color")
box.separator()
box.prop(props, "show_o_earth")
box.prop(props, "show_o_outer")
# ==========================
# Observer Settings
# ==========================
box = layout.box()
box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
box.prop(props, "obs_longitude")
R = props.earth_radius
theta = math.radians(props.obs_longitude)
P_x = R * math.cos(theta)
P_y = R * math.sin(theta)
box.label(text=f"P = ({P_x:.2f}, {P_y:.2f}, 0.00)")
# ==========================
# Celestial 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")
# ==========================
# Tangent Wire Sphere
# ==========================
box = layout.box()
box.label(text="Tangent Sphere (Contact Point)", icon='WIREFRAME_HLT')
row = box.row()
row.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", text="Longitude")
row_div.prop(props, "tangent_rings", text="Latitude")
box.prop(props, "tangent_color")
# ==========================
# 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')
# ==========================
# Visualization
# ==========================
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")
# ==========================
# Camera
# ==========================
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 (P -> Sky)").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 (O -> P)").cam_type = "Cam_EarthCenter"
col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
# ==========================
# Export
# ==========================
layout.separator()
layout.operator(OBS_SKY_OT_ExportCode.bl_idname, icon='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():
"""
アドオン有効化時にモデルを自動生成し、Nパネルを開き、マテリアルプレビューに変更する。
Blender起動直後でウィンドウが準備できていない場合はタイマーでリトライする。
"""
global setup_retry_count
if not bpy.context.window_manager.windows:
setup_retry_count += 1
if setup_retry_count > 50: # 安全装置 (5秒以上かかったら諦める)
return None
return 0.1 # 0.1秒後に再試行
# 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}")
# 2. ビューポートの設定(Nパネル展開&マテリアルプレビュー)
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':
# サイドバー(Nパネル)を開く
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_ExportCode,
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()