blender Million 2026
# 2026-03-06 Session Template V5
# Blender 4.0+ / 4.2+ (EEVEE-Next compatible)
UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V5"
SCRIPT_VERSION = 5
bl_info = {
"name": "Relativity Visualizer: Dual Light Cones",
"author": "zionadchat Gemini",
"version": (6, 5),
"blender": (4, 0, 0),
"location": "View3D > Sidebar",
"description": "Maxwell絶対静止系での未来光円錐(先進)と過去光円錐(遅延)の可視化",
"category": "Physics",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"velocity": 0.6000,
"radius": 10.0000,
"obs_time": 0.0000,
"resolution": 72,
"ring_thick": 0.1000,
"ray_thick": 0.0500,
"obs_size": 0.3000,
"show_future": True,
"color_future_phys": (0.0000, 0.8000, 0.8000, 0.8000),
"color_future_base": (0.0000, 0.0000, 1.0000, 0.8000),
"color_future_ray": (1.0000, 0.0000, 1.0000, 0.4000),
"show_past": True,
"color_past_phys": (0.0000, 1.0000, 0.0000, 0.8000),
"color_past_base": (1.0000, 0.0000, 0.0000, 0.8000),
"color_past_ray": (1.0000, 1.0000, 0.0000, 0.4000),
}
# <END_DICT>
SYSTEM_DEFAULTS = CURRENT_DEFAULTS.copy()
TAB_NAME = "Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"
ADDON_LINKS = (
{"label": "干渉計 スカートの裾 20260306aa", "url": "<https://www.notion.so/20260306aa-31bf5dacaf4380fc86eedcda55812c81>"},
{"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
{"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
{"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)
# ------------------------------------------------------------------------
# Material & Object Utilities
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if not bsdf:
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0])
if bsdf:
if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
if "Emission Color" in bsdf.inputs: bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.5
mat.diffuse_color = color
return mat
def create_sphere(col, name, location, radius, mat_name, color):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj[OBJECT_TAG] = True
col.objects.link(obj)
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
bm.to_mesh(mesh)
finally:
bm.free()
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
def create_light_cone_object(col, name, phys_points, base_points, ray_paths, thickness, ray_thickness, mats):
"""基準円周、底面円周、射線をすべて1つのカーブオブジェクトとして生成する"""
curve = bpy.data.curves.new(name, 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = thickness
curve.bevel_resolution = 4
obj = bpy.data.objects.new(name, curve)
obj[OBJECT_TAG] = True
col.objects.link(obj)
for mat in mats:
obj.data.materials.append(mat)
ray_radius_factor = ray_thickness / thickness if thickness > 0 else 1.0
# 0: 底面円周 (Base Ring)
if base_points:
spline_base = curve.splines.new('POLY')
spline_base.use_cyclic_u = True
spline_base.points.add(len(base_points) - 1)
for i, p in enumerate(base_points):
spline_base.points[i].co = (p.x, p.y, p.z, 1)
spline_base.material_index = 0
# 1: 射線 (Ray Paths)
if ray_paths:
for path in ray_paths:
spline_ray = curve.splines.new('POLY')
spline_ray.use_cyclic_u = False
spline_ray.points.add(len(path) - 1)
for i, p in enumerate(path):
spline_ray.points[i].co = (p.x, p.y, p.z, 1)
spline_ray.points[i].radius = ray_radius_factor
spline_ray.material_index = 1
# 2: 基準円周 (Phys Ring)
if phys_points:
spline_phys = curve.splines.new('POLY')
spline_phys.use_cyclic_u = True
spline_phys.points.add(len(phys_points) - 1)
for i, p in enumerate(phys_points):
spline_phys.points[i].co = (p.x, p.y, p.z, 1)
spline_phys.material_index = 2
return obj
# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
if not context or not context.scene: return
p = context.scene.rel_visual
v = p.velocity; R = p.radius; t_obs = p.obs_time; res = p.resolution
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: col = bpy.data.collections.new(COLLECTION_NAME)
if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)
# 古いオブジェクトの削除
for obj in[o for o in col.objects if o.get(OBJECT_TAG)]:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
if isinstance(m, bpy.types.Mesh): bpy.data.meshes.remove(m)
elif isinstance(m, bpy.types.Curve): bpy.data.curves.remove(m)
# 観測者(現在位置)
center_pos_now = Vector((v * t_obs, 0, t_obs))
create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs_Center", (1.0, 1.0, 1.0, 1.0))
# 現在時刻の実体円周(共通基準)
phys_points =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
phys_points.append(Vector((v * t_obs + R * math.cos(phi), R * math.sin(phi), t_obs)))
denom = 1.0 - v**2
if abs(denom) < 1e-9: denom = 1e-9
# 1. Future Light Cone (未来光円錐:先進)
if p.show_future:
base_points =[]; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
# 先進時間 (+ v*cos)
dt_adv = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) + v*math.cos(phi))) / denom
t_adv = t_obs + dt_adv
p_adv = Vector((v * t_adv + R * math.cos(phi), R * math.sin(phi), t_adv))
base_points.append(p_adv)
# 未来の底面から現在の中心へ
ray_paths.append([p_adv, center_pos_now])
mats =[
get_fixed_material("Mat_Future_Base", p.color_future_base),
get_fixed_material("Mat_Future_Ray", p.color_future_ray),
get_fixed_material("Mat_Future_Phys", p.color_future_phys)
]
create_light_cone_object(col, "Future_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# 2. Past Light Cone (過去光円錐:遅延)
if p.show_past:
base_points =[]; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
dt_ret = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) - v*math.cos(phi))) / denom
t_emit = t_obs - dt_ret
p_emit = Vector((v * t_emit + R * math.cos(phi), R * math.sin(phi), t_emit))
base_points.append(p_emit)
ray_paths.append([p_emit, center_pos_now])
mats =[
get_fixed_material("Mat_Past_Base", p.color_past_base),
get_fixed_material("Mat_Past_Ray", p.color_past_ray),
get_fixed_material("Mat_Past_Phys", p.color_past_phys)
]
create_light_cone_object(col, "Past_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# ------------------------------------------------------------------------
# Throttling & Properties
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
global _update_timer
_update_timer = None
if bpy.context and bpy.context.scene: draw_rel_visual_core(bpy.context)
return None
def update_view(self, context):
global _update_timer
if _update_timer:
try: bpy.app.timers.unregister(_update_timer)
except: pass
_update_timer = bpy.app.timers.register(delayed_update_func, first_interval=0.05)
class PG_RelativityVisual(bpy.types.PropertyGroup):
velocity: bpy.props.FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view)
radius: bpy.props.FloatProperty(name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view)
obs_time: bpy.props.FloatProperty(name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view)
resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
obs_size: bpy.props.FloatProperty(name="Center Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)
# Future
show_future: bpy.props.BoolProperty(name="Show Future Cone", default=CURRENT_DEFAULTS["show_future"], update=update_view)
color_future_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_phys"], update=update_view)
color_future_base: bpy.props.FloatVectorProperty(name="Base Ring (Top)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_base"], update=update_view)
color_future_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_ray"], update=update_view)
# Past
show_past: bpy.props.BoolProperty(name="Show Past Cone", default=CURRENT_DEFAULTS["show_past"], update=update_view)
color_past_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_phys"], update=update_view)
color_past_base: bpy.props.FloatVectorProperty(name="Base Ring (Bottom)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_base"], update=update_view)
color_past_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_ray"], update=update_view)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
bl_idname = "object.draw_rel_visual"; bl_label = "Force Refresh"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): draw_rel_visual_core(context); return {'FINISHED'}
class OBJECT_OT_DetachVisual(bpy.types.Operator):
bl_idname = "object.detach_visual"; bl_label = "Detach & Keep"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: return {'CANCELLED'}
targets =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
if not targets: return {'CANCELLED'}
scene_col = context.scene.collection
timestamp = datetime.now().strftime('%H%M%S')
for obj in targets:
del obj[OBJECT_TAG]
obj.name = f"{obj.name}_Baked_{timestamp}"
# マルチマテリアル対応の独立化
if obj.data.materials:
new_mats =[m.copy() for m in obj.data.materials if m]
for m in new_mats:
m.name = f"{m.name}_Baked_{timestamp}"
obj.data.materials.clear()
for m in new_mats:
obj.data.materials.append(m)
if obj.name not in scene_col.objects: scene_col.objects.link(obj)
col.objects.unlink(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, f"Detached {len(targets)} object(s).")
return {'FINISHED'}
class WM_OT_CopyFullScript(bpy.types.Operator):
bl_idname = "wm.copy_full_script"; bl_label = "Copy Script with Current Values"
def execute(self, context):
p = context.scene.rel_visual
M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
if not texts: return {'CANCELLED'}
d_str = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(p, k)
if hasattr(val, "__len__") and not isinstance(val, str):
v_str = ", ".join([f"{v:.4f}" for v in val])
d_str += f' "{k}": ({v_str}),\n'
elif isinstance(val, float): d_str += f' "{k}": {val:.4f},\n'
else: d_str += f' "{k}": {val},\n'
d_str += "}\n"
code = texts[0].as_string()
new_code = code.split(M_START)[0] + M_START + "\n" + d_str + M_END + code.split(M_END)[1]
context.window_manager.clipboard = new_code
self.report({'INFO'}, "Copied to clipboard!")
return {'FINISHED'}
class WM_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "wm.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def cleanup_logic():
if __name__ == "__main__":
unregister()
print(f"[{bl_info['name']}] Unregistered from Script Mode.")
else:
import addon_utils
module_name = __package__ if __package__ else __name__
addon_utils.disable(module_name, default_set=True)
print(f"[{bl_info['name']}] Disabled from Addon Mode.")
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
area.tag_redraw()
bpy.app.timers.register(cleanup_logic, first_interval=0.1)
self.report({'INFO'}, "Removing Addon UI...")
return {'FINISHED'}
class WM_OT_OpenUrl(bpy.types.Operator):
bl_idname = "wm.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualMain(bpy.types.Panel):
bl_label = "Relativity Time & Physics"
bl_idname = "VIEW3D_PT_rel_visual_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
# トップアクションボタン3種
layout.operator("wm.copy_full_script", icon='COPY_ID', text="Copy Script with Values")
row = layout.row()
row.scale_y = 1.5
row.operator("object.detach_visual", text="Detach & Keep Scene", icon='PINNED')
layout.operator("object.draw_rel_visual", icon='FILE_REFRESH', text="Force Refresh")
layout.separator()
# パラメータ群
box = layout.box()
box.label(text="Time & Physics", icon='TIME')
box.prop(p, "obs_time")
box.prop(p, "velocity")
box.prop(p, "radius")
box = layout.box()
box.label(text="Geometry details", icon='MESH_GRID')
box.prop(p, "resolution")
box.prop(p, "obs_size")
box.prop(p, "ring_thick")
box.prop(p, "ray_thick")
class VIEW3D_PT_RelVisualFuture(bpy.types.Panel):
bl_label = "Future Light Cone (Advanced)"
bl_idname = "VIEW3D_PT_rel_visual_future"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_future")
if p.show_future:
box = layout.box()
box.prop(p, "color_future_phys")
box.prop(p, "color_future_base")
box.prop(p, "color_future_ray")
class VIEW3D_PT_RelVisualPast(bpy.types.Panel):
bl_label = "Past Light Cone (Retarded)"
bl_idname = "VIEW3D_PT_rel_visual_past"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_past")
if p.show_past:
box = layout.box()
box.prop(p, "color_past_phys")
box.prop(p, "color_past_base")
box.prop(p, "color_past_ray")
class VIEW3D_PT_RelVisualLinks(bpy.types.Panel):
bl_label = "Theory Links"
bl_idname = "VIEW3D_PT_rel_visual_links"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS:
op = self.layout.operator("wm.open_url", text=l["label"])
op.url = l["url"]
class VIEW3D_PT_RelVisualSystem(bpy.types.Panel):
bl_label = "System"
bl_idname = "VIEW3D_PT_rel_visual_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.operator("wm.remove_addon", icon='CANCEL', text="Remove Addon")
# ------------------------------------------------------------------------
# Register
# ------------------------------------------------------------------------
classes = (
PG_RelativityVisual,
OBJECT_OT_DrawRelVisual,
OBJECT_OT_DetachVisual,
WM_OT_CopyFullScript,
WM_OT_RemoveAddon,
WM_OT_OpenUrl,
VIEW3D_PT_RelVisualMain,
VIEW3D_PT_RelVisualFuture,
VIEW3D_PT_RelVisualPast,
VIEW3D_PT_RelVisualLinks,
VIEW3D_PT_RelVisualSystem
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.rel_visual = bpy.props.PointerProperty(type=PG_RelativityVisual)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.rel_visual
if __name__ == "__main__": register()
# 2026-03-06 Session Template V4
# Blender 4.0+ / 4.2+ (EEVEE-Next compatible)
UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V4"
SCRIPT_VERSION = 4
bl_info = {
"name": "Relativity Visualizer: Dual Light Cones",
"author": "zionadchat Gemini",
"version": (6, 4),
"blender": (4, 0, 0),
"location": "View3D > Sidebar",
"description": "Maxwell絶対静止系での未来光円錐(先進)と過去光円錐(遅延)の可視化",
"category": "Physics",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"velocity": 0.6000,
"radius": 10.0000,
"obs_time": 10.0000,
"resolution": 72,
"ring_thick": 0.1000,
"ray_thick": 0.0500,
"obs_size": 0.3000,
"show_future": True,
"color_future_phys": (0.0000, 0.8000, 0.8000, 0.8000),
"color_future_base": (0.0000, 0.0000, 1.0000, 0.8000),
"color_future_ray": (1.0000, 0.0000, 1.0000, 0.4000),
"show_past": True,
"color_past_phys": (0.0000, 1.0000, 0.0000, 0.8000),
"color_past_base": (1.0000, 0.0000, 0.0000, 0.8000),
"color_past_ray": (1.0000, 1.0000, 0.0000, 0.4000),
}
# <END_DICT>
SYSTEM_DEFAULTS = CURRENT_DEFAULTS.copy()
TAB_NAME = "Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"
ADDON_LINKS = (
{"label": "干渉計 スカートの裾 20260306aa", "url": "<https://www.notion.so/20260306aa-31bf5dacaf4380fc86eedcda55812c81>"},
{"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
{"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
{"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)
# ------------------------------------------------------------------------
# Material & Object Utilities
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
# Blender 4.2+ EEVEE-Next対策: shadow_methodの廃止に対応
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if not bsdf:
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0])
if bsdf:
if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
if "Emission Color" in bsdf.inputs: bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.5
mat.diffuse_color = color
return mat
def create_sphere(col, name, location, radius, mat_name, color):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj[OBJECT_TAG] = True
col.objects.link(obj)
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
bm.to_mesh(mesh)
finally:
bm.free()
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
def create_light_cone_object(col, name, phys_points, base_points, ray_paths, thickness, ray_thickness, mats):
"""基準円周、底面円周、射線をすべて1つのカーブオブジェクトとして生成する"""
curve = bpy.data.curves.new(name, 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = thickness
curve.bevel_resolution = 4
obj = bpy.data.objects.new(name, curve)
obj[OBJECT_TAG] = True
col.objects.link(obj)
for mat in mats:
obj.data.materials.append(mat)
ray_radius_factor = ray_thickness / thickness if thickness > 0 else 1.0
# 0: 底面円周 (Base Ring)
if base_points:
spline_base = curve.splines.new('POLY')
spline_base.use_cyclic_u = True
spline_base.points.add(len(base_points) - 1)
for i, p in enumerate(base_points):
spline_base.points[i].co = (p.x, p.y, p.z, 1)
spline_base.material_index = 0
# 1: 射線 (Ray Paths)
if ray_paths:
for path in ray_paths:
spline_ray = curve.splines.new('POLY')
spline_ray.use_cyclic_u = False
spline_ray.points.add(len(path) - 1)
for i, p in enumerate(path):
spline_ray.points[i].co = (p.x, p.y, p.z, 1)
spline_ray.points[i].radius = ray_radius_factor
spline_ray.material_index = 1
# 2: 基準円周 (Phys Ring)
if phys_points:
spline_phys = curve.splines.new('POLY')
spline_phys.use_cyclic_u = True
spline_phys.points.add(len(phys_points) - 1)
for i, p in enumerate(phys_points):
spline_phys.points[i].co = (p.x, p.y, p.z, 1)
spline_phys.material_index = 2
return obj
# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
if not context or not context.scene: return
p = context.scene.rel_visual
v = p.velocity; R = p.radius; t_obs = p.obs_time; res = p.resolution
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: col = bpy.data.collections.new(COLLECTION_NAME)
if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)
# 古いオブジェクトの削除
for obj in[o for o in col.objects if o.get(OBJECT_TAG)]:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
if isinstance(m, bpy.types.Mesh): bpy.data.meshes.remove(m)
elif isinstance(m, bpy.types.Curve): bpy.data.curves.remove(m)
# 観測者(現在位置)
center_pos_now = Vector((v * t_obs, 0, t_obs))
create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs_Center", (1.0, 1.0, 1.0, 1.0))
# 現在時刻の実体円周(共通基準)
phys_points =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
phys_points.append(Vector((v * t_obs + R * math.cos(phi), R * math.sin(phi), t_obs)))
denom = 1.0 - v**2
if abs(denom) < 1e-9: denom = 1e-9
# 1. Future Light Cone (未来光円錐:先進)
if p.show_future:
base_points = []; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
# 先進時間 (+ v*cos)
dt_adv = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) + v*math.cos(phi))) / denom
t_adv = t_obs + dt_adv
p_adv = Vector((v * t_adv + R * math.cos(phi), R * math.sin(phi), t_adv))
base_points.append(p_adv)
# 未来の底面から現在の中心へ
ray_paths.append([p_adv, center_pos_now])
mats =[
get_fixed_material("Mat_Future_Base", p.color_future_base),
get_fixed_material("Mat_Future_Ray", p.color_future_ray),
get_fixed_material("Mat_Future_Phys", p.color_future_phys)
]
create_light_cone_object(col, "Future_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# 2. Past Light Cone (過去光円錐:遅延)
if p.show_past:
base_points = []; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
dt_ret = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) - v*math.cos(phi))) / denom
t_emit = t_obs - dt_ret
p_emit = Vector((v * t_emit + R * math.cos(phi), R * math.sin(phi), t_emit))
base_points.append(p_emit)
ray_paths.append([p_emit, center_pos_now])
mats =[
get_fixed_material("Mat_Past_Base", p.color_past_base),
get_fixed_material("Mat_Past_Ray", p.color_past_ray),
get_fixed_material("Mat_Past_Phys", p.color_past_phys)
]
create_light_cone_object(col, "Past_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# ------------------------------------------------------------------------
# Throttling & Properties
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
global _update_timer
_update_timer = None
if bpy.context and bpy.context.scene: draw_rel_visual_core(bpy.context)
return None
def update_view(self, context):
global _update_timer
if _update_timer:
try: bpy.app.timers.unregister(_update_timer)
except: pass
_update_timer = bpy.app.timers.register(delayed_update_func, first_interval=0.05)
class PG_RelativityVisual(bpy.types.PropertyGroup):
velocity: bpy.props.FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view)
radius: bpy.props.FloatProperty(name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view)
obs_time: bpy.props.FloatProperty(name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view)
resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
obs_size: bpy.props.FloatProperty(name="Center Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)
# Future
show_future: bpy.props.BoolProperty(name="Show Future Cone", default=CURRENT_DEFAULTS["show_future"], update=update_view)
color_future_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_phys"], update=update_view)
color_future_base: bpy.props.FloatVectorProperty(name="Base Ring (Top)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_base"], update=update_view)
color_future_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_ray"], update=update_view)
# Past
show_past: bpy.props.BoolProperty(name="Show Past Cone", default=CURRENT_DEFAULTS["show_past"], update=update_view)
color_past_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_phys"], update=update_view)
color_past_base: bpy.props.FloatVectorProperty(name="Base Ring (Bottom)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_base"], update=update_view)
color_past_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_ray"], update=update_view)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
bl_idname = "object.draw_rel_visual"; bl_label = "Force Refresh"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): draw_rel_visual_core(context); return {'FINISHED'}
class OBJECT_OT_DetachVisual(bpy.types.Operator):
bl_idname = "object.detach_visual"; bl_label = "Detach & Keep"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: return {'CANCELLED'}
targets =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
if not targets: return {'CANCELLED'}
scene_col = context.scene.collection
timestamp = datetime.now().strftime('%H%M%S')
for obj in targets:
del obj[OBJECT_TAG]
obj.name = f"{obj.name}_Baked_{timestamp}"
# マルチマテリアル対応の独立化
if obj.data.materials:
new_mats =[m.copy() for m in obj.data.materials if m]
for m in new_mats:
m.name = f"{m.name}_Baked_{timestamp}"
obj.data.materials.clear()
for m in new_mats:
obj.data.materials.append(m)
if obj.name not in scene_col.objects: scene_col.objects.link(obj)
col.objects.unlink(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, f"Detached {len(targets)} object(s).")
return {'FINISHED'}
class WM_OT_CopyFullScript(bpy.types.Operator):
bl_idname = "wm.copy_full_script"; bl_label = "Copy Script with Current Values"
def execute(self, context):
p = context.scene.rel_visual
M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
if not texts: return {'CANCELLED'}
d_str = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(p, k)
if hasattr(val, "__len__") and not isinstance(val, str):
v_str = ", ".join([f"{v:.4f}" for v in val])
d_str += f' "{k}": ({v_str}),\n'
elif isinstance(val, float): d_str += f' "{k}": {val:.4f},\n'
else: d_str += f' "{k}": {val},\n'
d_str += "}\n"
code = texts[0].as_string()
new_code = code.split(M_START)[0] + M_START + "\n" + d_str + M_END + code.split(M_END)[1]
context.window_manager.clipboard = new_code
self.report({'INFO'}, "Copied to clipboard!")
return {'FINISHED'}
class WM_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "wm.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def cleanup_logic():
if __name__ == "__main__":
unregister()
print(f"[{bl_info['name']}] Unregistered from Script Mode.")
else:
import addon_utils
module_name = __package__ if __package__ else __name__
addon_utils.disable(module_name, default_set=True)
print(f"[{bl_info['name']}] Disabled from Addon Mode.")
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
area.tag_redraw()
bpy.app.timers.register(cleanup_logic, first_interval=0.1)
self.report({'INFO'}, "Removing Addon UI...")
return {'FINISHED'}
class WM_OT_OpenUrl(bpy.types.Operator):
bl_idname = "wm.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualMain(bpy.types.Panel):
bl_label = "Relativity Time & Physics"
bl_idname = "VIEW3D_PT_rel_visual_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
box = layout.box()
box.label(text="Time & Physics", icon='TIME')
box.prop(p, "obs_time")
box.prop(p, "velocity")
box.prop(p, "radius")
box = layout.box()
box.label(text="Geometry details", icon='MESH_GRID')
box.prop(p, "resolution")
box.prop(p, "obs_size")
box.prop(p, "ring_thick")
box.prop(p, "ray_thick")
class VIEW3D_PT_RelVisualFuture(bpy.types.Panel):
bl_label = "Future Light Cone (Advanced)"
bl_idname = "VIEW3D_PT_rel_visual_future"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_future")
if p.show_future:
box = layout.box()
box.prop(p, "color_future_phys")
box.prop(p, "color_future_base")
box.prop(p, "color_future_ray")
class VIEW3D_PT_RelVisualPast(bpy.types.Panel):
bl_label = "Past Light Cone (Retarded)"
bl_idname = "VIEW3D_PT_rel_visual_past"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_past")
if p.show_past:
box = layout.box()
box.prop(p, "color_past_phys")
box.prop(p, "color_past_base")
box.prop(p, "color_past_ray")
class VIEW3D_PT_RelVisualLinks(bpy.types.Panel):
bl_label = "Theory Links"
bl_idname = "VIEW3D_PT_rel_visual_links"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS:
op = self.layout.operator("wm.open_url", text=l["label"])
op.url = l["url"]
class VIEW3D_PT_RelVisualSystem(bpy.types.Panel):
bl_label = "System & Export"
bl_idname = "VIEW3D_PT_rel_visual_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
row = layout.row()
row.scale_y = 1.5
row.operator("object.detach_visual", text="Detach & Keep Scene", icon='PINNED')
layout.separator()
layout.operator("wm.copy_full_script", icon='COPY_ID', text="Copy Script with Values")
layout.operator("object.draw_rel_visual", icon='FILE_REFRESH', text="Force Refresh")
layout.separator()
layout.operator("wm.remove_addon", icon='CANCEL', text="Remove Addon")
# ------------------------------------------------------------------------
# Register
# ------------------------------------------------------------------------
classes = (
PG_RelativityVisual,
OBJECT_OT_DrawRelVisual,
OBJECT_OT_DetachVisual,
WM_OT_CopyFullScript,
WM_OT_RemoveAddon,
WM_OT_OpenUrl,
VIEW3D_PT_RelVisualMain,
VIEW3D_PT_RelVisualFuture,
VIEW3D_PT_RelVisualPast,
VIEW3D_PT_RelVisualLinks,
VIEW3D_PT_RelVisualSystem
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.rel_visual = bpy.props.PointerProperty(type=PG_RelativityVisual)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.rel_visual
if __name__ == "__main__": register()
# 2026-03-06 Session Template V3
# Blender 4.0+ / 4.2+ (EEVEE-Next compatible)
UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V3"
SCRIPT_VERSION = 3
bl_info = {
"name": "Relativity Visualizer: Dual Light Cones",
"author": "zionadchat Gemini",
"version": (6, 3),
"blender": (4, 0, 0),
"location": "View3D > Sidebar",
"description": "Maxwell絶対静止系での未来光円錐(先進)と過去光円錐(遅延)の可視化",
"category": "Physics",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"velocity": 0.6000,
"radius": 5.0000,
"obs_time": 10.0000,
"resolution": 72,
"ring_thick": 0.1000,
"ray_thick": 0.0500,
"obs_size": 0.3000,
"show_future": True,
"color_future_phys": (0.0000, 0.8000, 0.8000, 0.8000),
"color_future_base": (0.0000, 0.0000, 1.0000, 0.8000),
"color_future_ray": (1.0000, 0.0000, 1.0000, 0.4000),
"show_past": True,
"color_past_phys": (0.0000, 1.0000, 0.0000, 0.8000),
"color_past_base": (1.0000, 0.0000, 0.0000, 0.8000),
"color_past_ray": (1.0000, 1.0000, 0.0000, 0.4000),
}
# <END_DICT>
SYSTEM_DEFAULTS = CURRENT_DEFAULTS.copy()
TAB_NAME = "Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"
# ------------------------------------------------------------------------
# Material & Object Utilities
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
# Blender 4.2+ EEVEE-Next対策: shadow_methodの廃止に対応するため削除
# mat.shadow_method = 'NONE'
nodes = mat.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if not bsdf:
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0])
if bsdf:
if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
if "Emission Color" in bsdf.inputs: bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.5
mat.diffuse_color = color
return mat
def create_sphere(col, name, location, radius, mat_name, color):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj[OBJECT_TAG] = True
col.objects.link(obj)
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
bm.to_mesh(mesh)
finally:
bm.free()
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
def create_light_cone_object(col, name, phys_points, base_points, ray_paths, thickness, ray_thickness, mats):
"""基準円周、底面円周、射線をすべて1つのカーブオブジェクトとして生成する"""
curve = bpy.data.curves.new(name, 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = thickness
curve.bevel_resolution = 4
obj = bpy.data.objects.new(name, curve)
obj[OBJECT_TAG] = True
col.objects.link(obj)
for mat in mats:
obj.data.materials.append(mat)
ray_radius_factor = ray_thickness / thickness if thickness > 0 else 1.0
# 0: 底面円周 (Base Ring)
if base_points:
spline_base = curve.splines.new('POLY')
spline_base.use_cyclic_u = True
spline_base.points.add(len(base_points) - 1)
for i, p in enumerate(base_points):
spline_base.points[i].co = (p.x, p.y, p.z, 1)
spline_base.material_index = 0
# 1: 射線 (Ray Paths)
if ray_paths:
for path in ray_paths:
spline_ray = curve.splines.new('POLY')
spline_ray.use_cyclic_u = False
spline_ray.points.add(len(path) - 1)
for i, p in enumerate(path):
spline_ray.points[i].co = (p.x, p.y, p.z, 1)
spline_ray.points[i].radius = ray_radius_factor
spline_ray.material_index = 1
# 2: 基準円周 (Phys Ring)
if phys_points:
spline_phys = curve.splines.new('POLY')
spline_phys.use_cyclic_u = True
spline_phys.points.add(len(phys_points) - 1)
for i, p in enumerate(phys_points):
spline_phys.points[i].co = (p.x, p.y, p.z, 1)
spline_phys.material_index = 2
return obj
# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
if not context or not context.scene: return
p = context.scene.rel_visual
v = p.velocity; R = p.radius; t_obs = p.obs_time; res = p.resolution
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: col = bpy.data.collections.new(COLLECTION_NAME)
if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)
# 古いオブジェクトの削除
for obj in [o for o in col.objects if o.get(OBJECT_TAG)]:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
if isinstance(m, bpy.types.Mesh): bpy.data.meshes.remove(m)
elif isinstance(m, bpy.types.Curve): bpy.data.curves.remove(m)
# 観測者(現在位置)
center_pos_now = Vector((v * t_obs, 0, t_obs))
create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs_Center", (1.0, 1.0, 1.0, 1.0))
# 現在時刻の実体円周(共通基準)
phys_points =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
phys_points.append(Vector((v * t_obs + R * math.cos(phi), R * math.sin(phi), t_obs)))
denom = 1.0 - v**2
if abs(denom) < 1e-9: denom = 1e-9
# 1. Future Light Cone (未来光円錐:先進)
if p.show_future:
base_points = []; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
# 先進時間 (+ v*cos)
dt_adv = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) + v*math.cos(phi))) / denom
t_adv = t_obs + dt_adv
p_adv = Vector((v * t_adv + R * math.cos(phi), R * math.sin(phi), t_adv))
base_points.append(p_adv)
# 未来の底面から現在の中心へ
ray_paths.append([p_adv, center_pos_now])
mats =[
get_fixed_material("Mat_Future_Base", p.color_future_base),
get_fixed_material("Mat_Future_Ray", p.color_future_ray),
get_fixed_material("Mat_Future_Phys", p.color_future_phys)
]
create_light_cone_object(col, "Future_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# 2. Past Light Cone (過去光円錐:遅延)
if p.show_past:
base_points = []; ray_paths =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
dt_ret = (R * (math.sqrt(max(0.0, 1.0 - (v*math.sin(phi))**2)) - v*math.cos(phi))) / denom
t_emit = t_obs - dt_ret
p_emit = Vector((v * t_emit + R * math.cos(phi), R * math.sin(phi), t_emit))
base_points.append(p_emit)
ray_paths.append([p_emit, center_pos_now])
mats =[
get_fixed_material("Mat_Past_Base", p.color_past_base),
get_fixed_material("Mat_Past_Ray", p.color_past_ray),
get_fixed_material("Mat_Past_Phys", p.color_past_phys)
]
create_light_cone_object(col, "Past_Light_Cone", phys_points, base_points, ray_paths, p.ring_thick, p.ray_thick, mats)
# ------------------------------------------------------------------------
# Throttling & Properties
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
global _update_timer
_update_timer = None
if bpy.context and bpy.context.scene: draw_rel_visual_core(bpy.context)
return None
def update_view(self, context):
global _update_timer
if _update_timer:
try: bpy.app.timers.unregister(_update_timer)
except: pass
_update_timer = bpy.app.timers.register(delayed_update_func, first_interval=0.05)
class PG_RelativityVisual(bpy.types.PropertyGroup):
velocity: bpy.props.FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view)
radius: bpy.props.FloatProperty(name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view)
obs_time: bpy.props.FloatProperty(name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view)
resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
obs_size: bpy.props.FloatProperty(name="Center Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)
# Future
show_future: bpy.props.BoolProperty(name="Show Future Cone", default=CURRENT_DEFAULTS["show_future"], update=update_view)
color_future_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_phys"], update=update_view)
color_future_base: bpy.props.FloatVectorProperty(name="Base Ring (Top)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_base"], update=update_view)
color_future_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_future_ray"], update=update_view)
# Past
show_past: bpy.props.BoolProperty(name="Show Past Cone", default=CURRENT_DEFAULTS["show_past"], update=update_view)
color_past_phys: bpy.props.FloatVectorProperty(name="Phys Ring (Center)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_phys"], update=update_view)
color_past_base: bpy.props.FloatVectorProperty(name="Base Ring (Bottom)", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_base"], update=update_view)
color_past_ray: bpy.props.FloatVectorProperty(name="Rays", subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_past_ray"], update=update_view)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
bl_idname = "object.draw_rel_visual"; bl_label = "Force Refresh"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): draw_rel_visual_core(context); return {'FINISHED'}
class OBJECT_OT_DetachVisual(bpy.types.Operator):
bl_idname = "object.detach_visual"; bl_label = "Detach & Keep"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: return {'CANCELLED'}
targets =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
if not targets: return {'CANCELLED'}
scene_col = context.scene.collection
timestamp = datetime.now().strftime('%H%M%S')
for obj in targets:
del obj[OBJECT_TAG]
obj.name = f"{obj.name}_Baked_{timestamp}"
# マルチマテリアル対応の独立化
if obj.data.materials:
new_mats =[m.copy() for m in obj.data.materials if m]
for m in new_mats:
m.name = f"{m.name}_Baked_{timestamp}"
obj.data.materials.clear()
for m in new_mats:
obj.data.materials.append(m)
if obj.name not in scene_col.objects: scene_col.objects.link(obj)
col.objects.unlink(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, f"Detached {len(targets)} object(s).")
return {'FINISHED'}
class WM_OT_CopyFullScript(bpy.types.Operator):
bl_idname = "wm.copy_full_script"; bl_label = "Copy Script with Current Values"
def execute(self, context):
p = context.scene.rel_visual
M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
if not texts: return {'CANCELLED'}
d_str = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(p, k)
if hasattr(val, "__len__") and not isinstance(val, str):
v_str = ", ".join([f"{v:.4f}" for v in val])
d_str += f' "{k}": ({v_str}),\n'
elif isinstance(val, float): d_str += f' "{k}": {val:.4f},\n'
else: d_str += f' "{k}": {val},\n'
d_str += "}\n"
code = texts[0].as_string()
new_code = code.split(M_START)[0] + M_START + "\n" + d_str + M_END + code.split(M_END)[1]
context.window_manager.clipboard = new_code
self.report({'INFO'}, "Copied to clipboard!")
return {'FINISHED'}
# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualMain(bpy.types.Panel):
bl_label = "Relativity Time & Physics"
bl_idname = "VIEW3D_PT_rel_visual_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
box = layout.box()
box.label(text="Time & Physics", icon='TIME')
box.prop(p, "obs_time")
box.prop(p, "velocity")
box.prop(p, "radius")
box = layout.box()
box.label(text="Geometry details", icon='MESH_GRID')
box.prop(p, "resolution")
box.prop(p, "obs_size")
box.prop(p, "ring_thick")
box.prop(p, "ray_thick")
class VIEW3D_PT_RelVisualFuture(bpy.types.Panel):
bl_label = "Future Light Cone (Advanced)"
bl_idname = "VIEW3D_PT_rel_visual_future"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_future")
if p.show_future:
box = layout.box()
box.prop(p, "color_future_phys")
box.prop(p, "color_future_base")
box.prop(p, "color_future_ray")
class VIEW3D_PT_RelVisualPast(bpy.types.Panel):
bl_label = "Past Light Cone (Retarded)"
bl_idname = "VIEW3D_PT_rel_visual_past"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout; p = context.scene.rel_visual
layout.prop(p, "show_past")
if p.show_past:
box = layout.box()
box.prop(p, "color_past_phys")
box.prop(p, "color_past_base")
box.prop(p, "color_past_ray")
class VIEW3D_PT_RelVisualSystem(bpy.types.Panel):
bl_label = "System & Export"
bl_idname = "VIEW3D_PT_rel_visual_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
row = layout.row()
row.scale_y = 1.5
row.operator("object.detach_visual", text="Detach & Keep Scene", icon='PINNED')
layout.separator()
layout.operator("wm.copy_full_script", icon='COPY_ID')
layout.operator("object.draw_rel_visual", icon='FILE_REFRESH')
# ------------------------------------------------------------------------
# Register
# ------------------------------------------------------------------------
classes = (
PG_RelativityVisual, OBJECT_OT_DrawRelVisual, OBJECT_OT_DetachVisual, WM_OT_CopyFullScript,
VIEW3D_PT_RelVisualMain, VIEW3D_PT_RelVisualFuture, VIEW3D_PT_RelVisualPast, VIEW3D_PT_RelVisualSystem
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.rel_visual = bpy.props.PointerProperty(type=PG_RelativityVisual)
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
del bpy.types.Scene.rel_visual
if __name__ == "__main__": register()