bl_info = {
"name": "Observation Planes Extended",
"author": "zionad2022",
"version": (1, 9, 1),
"blender": (5, 0, 0),
"location": "View3D > Sidebar(N) > Observation Plane",
"description": "2人の観測者による時間の流れ(過去方向)の不一致を可視化するアドオン",
"category": "3D View",
}
# Code Created : 2026_0716_0730
import bpy
import bmesh
import math
import mathutils
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Observation Planes"
PREFIX_VAL = "ObsPlane"
ADDON_CATEGORY = ADDON_TITLE
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "アドオン専用ページ"
LINK_URL_1 = "URL"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = "blender コンテナ 20260713"
LINK_URL_4 = "<https://note.com/zionadmillion/n/n9a47d88fe7b4>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 初期設定 (Copied from UI)
# =========================================================
GREEN_X_MIN_DEFAULT = -10.000
GREEN_X_MAX_DEFAULT = 10.000
TRIANGLE_EXTENSION_DEFAULT = 2.000
BLUE_EYE_X_DEFAULT = 0.000
BLUE_EYE_Y_DEFAULT = -10.000
BLUE_EYE_Z_DEFAULT = -10.000
RED_EYE_X_DEFAULT = 0.000
RED_EYE_Y_DEFAULT = 10.000
RED_EYE_Z_DEFAULT = -10.000
PAST_ARROW_LENGTH_DEFAULT = 5.000
COLOR_GREEN_EDGE = (0.000, 1.000, 0.500, 1.000)
COLOR_TEXT_DEFAULT = (1.000, 1.000, 1.000, 1.000)
COLOR_BLUE_FRONT_DEFAULT = (0.016, 0.083, 0.448, 0.327)
COLOR_BLUE_BACK_DEFAULT = (0.129, 0.467, 0.546, 1.000)
COLOR_RED_FRONT_DEFAULT = (1.000, 0.200, 0.200, 0.500)
COLOR_RED_BACK_DEFAULT = (0.921, 0.045, 1.000, 0.152)
USE_EMISSION_DEFAULT = False
EMISSION_STRENGTH_FRONT_DEFAULT = 10.000
EMISSION_STRENGTH_BACK_DEFAULT = 1.000
ENABLE_BLOOM_DEFAULT = False
BLOOM_THRESHOLD_DEFAULT = 0.000
BLOOM_MIX_DEFAULT = 0.000
WORLD_BRIGHTNESS_DEFAULT = 0.100
TEXT_SIZE_DEFAULT = 1.000
TEXT_OFFSET_X_DEFAULT = 0.000
TEXT_OFFSET_Y_DEFAULT = 0.000
TEXT_OFFSET_Z_DEFAULT = 0.000
TEXT_ROT_X_DEFAULT = 90.000
TEXT_ROT_Y_DEFAULT = 0.000
TEXT_ROT_Z_DEFAULT = 0.000
# =========================================================
# 7. 共通クラス (Utility Operators)
# =========================================================
class OBSPLANE_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
bpy.ops.wm.url_open(url=self.url)
return {'FINISHED'}
class OBSPLANE_OT_Uninstall(bpy.types.Operator):
bl_idname = f"{PREFIX}.uninstall"
bl_label = "アドオンを完全削除 (Unregister)"
def execute(self, context):
import sys
mod_name = __name__
if mod_name in sys.modules:
unregister()
self.report({'INFO'}, f"【{ADDON_TITLE}】をUnregisterしました。")
return {'FINISHED'}
class OBSPLANE_OT_SwitchRenderEngine(bpy.types.Operator):
bl_idname = f"{PREFIX}.switch_render_engine"
bl_label = "レンダーエンジン切り替え"
engine: bpy.props.StringProperty()
def execute(self, context):
try:
context.scene.render.engine = self.engine
except TypeError:
if self.engine == 'BLENDER_EEVEE_NEXT':
context.scene.render.engine = 'BLENDER_EEVEE'
return {'FINISHED'}
class OBSPLANE_OT_CopyDefaults(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_defaults"
bl_label = "設定値をコードとしてコピー"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
def c_str(c): return f"({c[0]:.3f}, {c[1]:.3f}, {c[2]:.3f}, {c[3]:.3f})"
code = f"""# =========================================================
# 6. 初期設定 (Copied from UI)
# =========================================================
GREEN_X_MIN_DEFAULT = {props.green_x_min:.3f}
GREEN_X_MAX_DEFAULT = {props.green_x_max:.3f}
TRIANGLE_EXTENSION_DEFAULT = {props.triangle_extension:.3f}
BLUE_EYE_X_DEFAULT = {props.blue_eye_x:.3f}
BLUE_EYE_Y_DEFAULT = {props.blue_eye_y:.3f}
BLUE_EYE_Z_DEFAULT = {props.blue_eye_z:.3f}
RED_EYE_X_DEFAULT = {props.red_eye_x:.3f}
RED_EYE_Y_DEFAULT = {props.red_eye_y:.3f}
RED_EYE_Z_DEFAULT = {props.red_eye_z:.3f}
PAST_ARROW_LENGTH_DEFAULT = {props.past_arrow_length:.3f}
COLOR_GREEN_EDGE = {c_str(props.color_green)}
COLOR_TEXT_DEFAULT = {c_str(props.color_text)}
COLOR_BLUE_FRONT_DEFAULT = {c_str(props.color_blue_front)}
COLOR_BLUE_BACK_DEFAULT = {c_str(props.color_blue_back)}
COLOR_RED_FRONT_DEFAULT = {c_str(props.color_red_front)}
COLOR_RED_BACK_DEFAULT = {c_str(props.color_red_back)}
USE_EMISSION_DEFAULT = {props.use_emission}
EMISSION_STRENGTH_FRONT_DEFAULT = {props.emission_strength_front:.3f}
EMISSION_STRENGTH_BACK_DEFAULT = {props.emission_strength_back:.3f}
ENABLE_BLOOM_DEFAULT = {props.enable_bloom}
BLOOM_THRESHOLD_DEFAULT = {props.bloom_threshold:.3f}
BLOOM_MIX_DEFAULT = {props.bloom_mix:.3f}
WORLD_BRIGHTNESS_DEFAULT = {props.world_brightness:.3f}
TEXT_SIZE_DEFAULT = {props.text_size:.3f}
TEXT_OFFSET_X_DEFAULT = {props.text_offset_x:.3f}
TEXT_OFFSET_Y_DEFAULT = {props.text_offset_y:.3f}
TEXT_OFFSET_Z_DEFAULT = {props.text_offset_z:.3f}
TEXT_ROT_X_DEFAULT = {props.text_rot_x:.3f}
TEXT_ROT_Y_DEFAULT = {props.text_rot_y:.3f}
TEXT_ROT_Z_DEFAULT = {props.text_rot_z:.3f}
"""
context.window_manager.clipboard = code
self.report({'INFO'}, "クリップボードに初期設定コードをコピーしました")
return {'FINISHED'}
class OBSPLANE_OT_ResetDefaults(bpy.types.Operator):
bl_idname = f"{PREFIX}.reset_defaults"
bl_label = "初期値にリセット"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.green_x_min = GREEN_X_MIN_DEFAULT
props.green_x_max = GREEN_X_MAX_DEFAULT
props.triangle_extension = TRIANGLE_EXTENSION_DEFAULT
props.blue_eye_x = BLUE_EYE_X_DEFAULT
props.blue_eye_y = BLUE_EYE_Y_DEFAULT
props.blue_eye_z = BLUE_EYE_Z_DEFAULT
props.red_eye_x = RED_EYE_X_DEFAULT
props.red_eye_y = RED_EYE_Y_DEFAULT
props.red_eye_z = RED_EYE_Z_DEFAULT
props.past_arrow_length = PAST_ARROW_LENGTH_DEFAULT
props.color_green = COLOR_GREEN_EDGE
props.color_text = COLOR_TEXT_DEFAULT
props.color_blue_front = COLOR_BLUE_FRONT_DEFAULT
props.color_blue_back = COLOR_BLUE_BACK_DEFAULT
props.color_red_front = COLOR_RED_FRONT_DEFAULT
props.color_red_back = COLOR_RED_BACK_DEFAULT
props.use_emission = USE_EMISSION_DEFAULT
props.emission_strength_front = EMISSION_STRENGTH_FRONT_DEFAULT
props.emission_strength_back = EMISSION_STRENGTH_BACK_DEFAULT
props.enable_bloom = ENABLE_BLOOM_DEFAULT
props.bloom_threshold = BLOOM_THRESHOLD_DEFAULT
props.bloom_mix = BLOOM_MIX_DEFAULT
props.world_brightness = WORLD_BRIGHTNESS_DEFAULT
props.text_size = TEXT_SIZE_DEFAULT
props.text_offset_x = TEXT_OFFSET_X_DEFAULT
props.text_offset_y = TEXT_OFFSET_Y_DEFAULT
props.text_offset_z = TEXT_OFFSET_Z_DEFAULT
props.text_rot_x = TEXT_ROT_X_DEFAULT
props.text_rot_y = TEXT_ROT_Y_DEFAULT
props.text_rot_z = TEXT_ROT_Z_DEFAULT
self.report({'INFO'}, "設定を初期値にリセットしました")
return {'FINISHED'}
# =========================================================
# 8. アドオン固有機能 (コンポジター連携コールバック等)
# =========================================================
def setup_compositor_bloom(scene):
scene.use_nodes = True
tree = scene.node_tree
if not tree: return
rl_node = next((n for n in tree.nodes if n.type == 'R_LAYERS'), None)
comp_node = next((n for n in tree.nodes if n.type == 'COMPOSITE'), None)
glare_node = next((n for n in tree.nodes if n.type == 'GLARE' and getattr(n, 'glare_type', '') == 'BLOOM'), None)
if not rl_node: rl_node = tree.nodes.new('CompositorNodeRLayers'); rl_node.location = (-300, 0)
if not comp_node: comp_node = tree.nodes.new('CompositorNodeComposite'); comp_node.location = (300, 0)
if not glare_node:
glare_node = tree.nodes.new('CompositorNodeGlare')
glare_node.location = (0, 0)
glare_node.glare_type = 'BLOOM'
for link in list(tree.links):
if link.from_node == rl_node and link.to_node == comp_node:
tree.links.remove(link)
if rl_node.outputs and glare_node.inputs:
if not any(l.from_node == rl_node and l.to_node == glare_node for l in tree.links):
tree.links.new(rl_node.outputs[0], glare_node.inputs[0])
if glare_node.outputs and comp_node.inputs:
if not any(l.from_node == glare_node and l.to_node == comp_node for l in tree.links):
tree.links.new(glare_node.outputs[0], comp_node.inputs[0])
def update_world_brightness(self, context):
if getattr(context, "scene", None) is None:
return
world = context.scene.world
if not world: return
if not world.use_nodes: world.use_nodes = True
tree = world.node_tree
if tree:
for node in tree.nodes:
if node.type == 'BACKGROUND':
try: node.inputs['Strength'].default_value = self.world_brightness
except KeyError: node.inputs[1].default_value = self.world_brightness
def update_bloom_settings(self, context):
if getattr(context, "scene", None) is None:
return
scene = context.scene
try:
if self.enable_bloom:
scene.use_nodes = True
tree = getattr(scene, 'node_tree', None)
if tree:
glare_node = next((n for n in tree.nodes if n.type == 'GLARE' and getattr(n, 'glare_type', '') == 'BLOOM'), None)
if not glare_node:
setup_compositor_bloom(scene)
glare_node = next((n for n in tree.nodes if n.type == 'GLARE' and getattr(n, 'glare_type', '') == 'BLOOM'), None)
if glare_node:
glare_node.mute = False
glare_node.mix = self.bloom_mix
glare_node.threshold = self.bloom_threshold
update_world_brightness(self, context)
if hasattr(context, "screen") and context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'RENDERED'
space.shading.use_compositor = 'ALWAYS'
else:
tree = getattr(scene, 'node_tree', None)
if tree:
for node in tree.nodes:
if node.type == 'GLARE' and getattr(node, 'glare_type', '') == 'BLOOM':
node.mute = True
except Exception as e:
print(f"Bloom更新エラー: {e}")
# =========================================================
# 8. アドオン固有機能 (ジオメトリとマテリアル)
# =========================================================
def get_or_create_collection():
coll_name = f"{PREFIX_VAL}_Collection"
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
bpy.context.scene.collection.children.link(coll)
return coll
def get_or_create_material_simple(name, color):
mat_name = f"{PREFIX_VAL}_{name}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
try:
mat.surface_render_method = 'BLENDED'
mat.use_transparency_overlap = True
except AttributeError:
mat.blend_method = 'BLEND'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
return mat
def get_or_create_material_double_sided(name, c_front, c_back, use_em, str_front, str_back):
mat_name = f"{PREFIX_VAL}_{name}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.show_transparent_back = False
try:
mat.surface_render_method = 'BLENDED'
mat.use_transparency_overlap = True
except AttributeError:
mat.blend_method = 'BLEND'
mat.node_tree.nodes.clear()
nodes = mat.node_tree.nodes
links = mat.node_tree.links
n_out = nodes.new('ShaderNodeOutputMaterial')
n_mix = nodes.new('ShaderNodeMixShader')
n_geo = nodes.new('ShaderNodeNewGeometry')
n_front = nodes.new('ShaderNodeBsdfPrincipled')
n_front.inputs['Base Color'].default_value = c_front
n_front.inputs['Alpha'].default_value = c_front[3]
if use_em:
n_front.inputs['Emission Color'].default_value = c_front
n_front.inputs['Emission Strength'].default_value = str_front
else:
n_front.inputs['Emission Strength'].default_value = 0.0
n_back = nodes.new('ShaderNodeBsdfPrincipled')
n_back.inputs['Base Color'].default_value = c_back
n_back.inputs['Alpha'].default_value = c_back[3]
if use_em:
n_back.inputs['Emission Color'].default_value = c_back
n_back.inputs['Emission Strength'].default_value = str_back
else:
n_back.inputs['Emission Strength'].default_value = 0.0
links.new(n_geo.outputs['Backfacing'], n_mix.inputs[0])
links.new(n_front.outputs[0], n_mix.inputs[1])
links.new(n_back.outputs[0], n_mix.inputs[2])
links.new(n_mix.outputs[0], n_out.inputs['Surface'])
return mat
def create_or_update_mesh(obj_name, vertices, faces, mat):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}_{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
mesh = bpy.data.meshes.new(name=full_name)
obj = bpy.data.objects.new(full_name, mesh)
coll.objects.link(obj)
else:
mesh = obj.data
mesh.clear_geometry()
bm = bmesh.new()
for v in vertices: bm.verts.new(v)
bm.verts.ensure_lookup_table()
if faces:
for f in faces: bm.faces.new([bm.verts[i] for i in f])
elif len(vertices) == 2:
bm.edges.new((bm.verts[0], bm.verts[1]))
bm.to_mesh(mesh)
bm.free()
if len(obj.data.materials) == 0: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
return obj
def create_or_update_text(obj_name, text_body, loc, rot, mat, size):
coll = get_or_create_collection()
full_name = f"{PREFIX_VAL}_Text_{obj_name}"
obj = bpy.data.objects.get(full_name)
if not obj:
curve = bpy.data.curves.new(type="FONT", name=full_name)
obj = bpy.data.objects.new(full_name, curve)
coll.objects.link(obj)
obj.data.body = text_body
obj.location = loc
obj.data.size = size
obj.data.align_x = 'CENTER'
obj.data.align_y = 'CENTER'
if len(obj.data.materials) == 0: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
obj.rotation_euler = rot
return obj
def update_geometry(self, context):
if getattr(context, "scene", None) is None:
return
props = getattr(context.scene, f"{PREFIX}_props")
if not props.is_active: return
x_min = props.green_x_min
x_max = props.green_x_max
ext = props.triangle_extension
v_green_a = mathutils.Vector((x_min, 0, 0))
v_green_b = mathutils.Vector((x_max, 0, 0))
v_blue_eye = mathutils.Vector((props.blue_eye_x, props.blue_eye_y, props.blue_eye_z))
v_red_eye = mathutils.Vector((props.red_eye_x, props.red_eye_y, props.red_eye_z))
v_blue_ext_a = v_blue_eye + (v_green_a - v_blue_eye) * ext
v_blue_ext_b = v_blue_eye + (v_green_b - v_blue_eye) * ext
v_red_ext_a = v_red_eye + (v_green_a - v_red_eye) * ext
v_red_ext_b = v_red_eye + (v_green_b - v_red_eye) * ext
mat_green = get_or_create_material_simple("Mat_Green", props.color_green)
mat_text = get_or_create_material_simple("Mat_Text", props.color_text)
mat_blue = get_or_create_material_double_sided(
"Mat_Blue", props.color_blue_front, props.color_blue_back,
props.use_emission, props.emission_strength_front, props.emission_strength_back
)
mat_red = get_or_create_material_double_sided(
"Mat_Red", props.color_red_front, props.color_red_back,
props.use_emission, props.emission_strength_front, props.emission_strength_back
)
create_or_update_mesh("GreenEdge", [v_green_a, v_green_b], [], mat_green)
obj_blue = create_or_update_mesh("BlueTriangle", [v_blue_eye, v_blue_ext_a, v_blue_ext_b], [[0, 1, 2]], mat_blue)
obj_red = create_or_update_mesh("RedTriangle", [v_red_eye, v_red_ext_a, v_red_ext_b], [[0, 1, 2]], mat_red)
if props.show_past_arrow:
x_axis = mathutils.Vector((1, 0, 0))
blue_normal = mathutils.geometry.normal([v_blue_eye, v_blue_ext_b, v_blue_ext_a])
blue_past_dir = x_axis.cross(blue_normal)
if blue_past_dir.y < 0: blue_past_dir = -blue_past_dir
red_normal = mathutils.geometry.normal([v_red_eye, v_red_ext_b, v_red_ext_a])
red_past_dir = x_axis.cross(red_normal)
if red_past_dir.y > 0: red_past_dir = -red_past_dir
create_or_update_mesh("BluePastArrow", [(0,0,0), blue_past_dir * props.past_arrow_length], [], mat_blue)
create_or_update_mesh("RedPastArrow", [(0,0,0), red_past_dir * props.past_arrow_length], [], mat_red)
else:
create_or_update_mesh("BluePastArrow", [], [], mat_blue)
create_or_update_mesh("RedPastArrow", [], [], mat_red)
offset = mathutils.Vector((props.text_offset_x, props.text_offset_y, props.text_offset_z))
rot = (math.radians(props.text_rot_x), math.radians(props.text_rot_y), math.radians(props.text_rot_z))
if props.show_labels:
loc_blue = v_blue_eye + mathutils.Vector((0, -2, 0)) + offset
create_or_update_text("Label_Blue", "「俺」の観測平面 (青)\nY > 0 が過去", loc_blue, rot, mat_text, props.text_size)
loc_red = v_red_eye + mathutils.Vector((0, 2, 0)) + offset
create_or_update_text("Label_Red", "「貴殿」の観測平面 (赤)\nY < 0 が過去", loc_red, rot, mat_text, props.text_size)
loc_green = v_green_b + mathutils.Vector((1.5, 0, 0)) + offset
create_or_update_text("Label_Green", "共通事象 (現在)", loc_green, rot, mat_text, props.text_size)
else:
create_or_update_text("Label_Blue", "", (0,0,0), (0,0,0), mat_text, props.text_size)
create_or_update_text("Label_Red", "", (0,0,0), (0,0,0), mat_text, props.text_size)
create_or_update_text("Label_Green", "", (0,0,0), (0,0,0), mat_text, props.text_size)
# =========================================================
# 8. アドオン固有機能 (Properties & Operator)
# =========================================================
class OBSPLANE_Properties(bpy.types.PropertyGroup):
is_active: bpy.props.BoolProperty(default=False)
green_x_min: bpy.props.FloatProperty(name="Min X", default=GREEN_X_MIN_DEFAULT, update=update_geometry)
green_x_max: bpy.props.FloatProperty(name="Max X", default=GREEN_X_MAX_DEFAULT, update=update_geometry)
triangle_extension: bpy.props.FloatProperty(name="延長率", default=TRIANGLE_EXTENSION_DEFAULT, min=1.0, max=10.0, update=update_geometry)
blue_eye_x: bpy.props.FloatProperty(name="Blue X", default=BLUE_EYE_X_DEFAULT, update=update_geometry)
blue_eye_y: bpy.props.FloatProperty(name="Blue Y", default=BLUE_EYE_Y_DEFAULT, update=update_geometry)
blue_eye_z: bpy.props.FloatProperty(name="Blue Z", default=BLUE_EYE_Z_DEFAULT, update=update_geometry)
red_eye_x: bpy.props.FloatProperty(name="Red X", default=RED_EYE_X_DEFAULT, update=update_geometry)
red_eye_y: bpy.props.FloatProperty(name="Red Y", default=RED_EYE_Y_DEFAULT, update=update_geometry)
red_eye_z: bpy.props.FloatProperty(name="Red Z", default=RED_EYE_Z_DEFAULT, update=update_geometry)
show_past_arrow: bpy.props.BoolProperty(name="Show Past Arrow", default=True, update=update_geometry)
past_arrow_length: bpy.props.FloatProperty(name="Past Arrow Length", default=PAST_ARROW_LENGTH_DEFAULT, update=update_geometry)
show_labels: bpy.props.BoolProperty(name="Show Labels", default=True, update=update_geometry)
# カラー(min=0.0, max=1.0 を追加し、範囲を制限)
color_green: bpy.props.FloatVectorProperty(name="共通事象", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_GREEN_EDGE, update=update_geometry)
color_text: bpy.props.FloatVectorProperty(name="テキスト", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_TEXT_DEFAULT, update=update_geometry)
color_blue_front: bpy.props.FloatVectorProperty(name="青平面 (表面)", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_BLUE_FRONT_DEFAULT, update=update_geometry)
color_blue_back: bpy.props.FloatVectorProperty(name="青平面 (裏面)", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_BLUE_BACK_DEFAULT, update=update_geometry)
color_red_front: bpy.props.FloatVectorProperty(name="赤平面 (表面)", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_RED_FRONT_DEFAULT, update=update_geometry)
color_red_back: bpy.props.FloatVectorProperty(name="赤平面 (裏面)", subtype='COLOR', size=4, min=0.0, max=1.0, default=COLOR_RED_BACK_DEFAULT, update=update_geometry)
# 発光・Bloom
use_emission: bpy.props.BoolProperty(name="発光を有効にする", default=USE_EMISSION_DEFAULT, update=update_geometry)
emission_strength_front: bpy.props.FloatProperty(name="表面 発光強度", min=0.0, max=1000.0, default=EMISSION_STRENGTH_FRONT_DEFAULT, update=update_geometry)
emission_strength_back: bpy.props.FloatProperty(name="裏面 発光強度", min=0.0, max=1000.0, default=EMISSION_STRENGTH_BACK_DEFAULT, update=update_geometry)
enable_bloom: bpy.props.BoolProperty(name="Bloom を有効にする", default=ENABLE_BLOOM_DEFAULT, update=update_bloom_settings)
bloom_threshold: bpy.props.FloatProperty(name="発光しきい値", min=0.0, max=10.0, default=BLOOM_THRESHOLD_DEFAULT, update=update_bloom_settings)
bloom_mix: bpy.props.FloatProperty(name="広がりの強さ(Mix)", min=-1.0, max=1.0, default=BLOOM_MIX_DEFAULT, subtype='FACTOR', update=update_bloom_settings)
world_brightness: bpy.props.FloatProperty(name="背景の明るさ", min=0.0, max=10.0, default=WORLD_BRIGHTNESS_DEFAULT, subtype='FACTOR', update=update_world_brightness)
# テキスト
text_size: bpy.props.FloatProperty(name="文字サイズ", default=TEXT_SIZE_DEFAULT, min=0.1, max=10.0, update=update_geometry)
text_offset_x: bpy.props.FloatProperty(name="Offset X", default=TEXT_OFFSET_X_DEFAULT, update=update_geometry)
text_offset_y: bpy.props.FloatProperty(name="Offset Y", default=TEXT_OFFSET_Y_DEFAULT, update=update_geometry)
text_offset_z: bpy.props.FloatProperty(name="Offset Z", default=TEXT_OFFSET_Z_DEFAULT, update=update_geometry)
text_rot_x: bpy.props.FloatProperty(name="Rotation X (度)", default=TEXT_ROT_X_DEFAULT, update=update_geometry)
text_rot_y: bpy.props.FloatProperty(name="Rotation Y (度)", default=TEXT_ROT_Y_DEFAULT, update=update_geometry)
text_rot_z: bpy.props.FloatProperty(name="Rotation Z (度)", default=TEXT_ROT_Z_DEFAULT, update=update_geometry)
class OBSPLANE_OT_Generate(bpy.types.Operator):
bl_idname = f"{PREFIX}.generate"
bl_label = "観測平面を生成"
def execute(self, context):
context.scene.obsplane_props.is_active = True
update_geometry(self, context)
return {'FINISHED'}
class OBSPLANE_OT_Detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach"
bl_label = "アドオンから切り離し"
def execute(self, context):
context.scene.obsplane_props.is_active = False
coll = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
if coll:
coll.name = f"{PREFIX_VAL}_Detached"
for obj in coll.objects: obj.name = obj.name.replace(f"{PREFIX_VAL}_", "Detached_")
return {'FINISHED'}
# =========================================================
# 9. パネル構成
# =========================================================
class OBSPLANE_PT_MainPanel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX.upper()}_PT_MainPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
row = layout.row(align=True)
is_eevee = context.scene.render.engine in {'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'}
row.operator(f"{PREFIX}.switch_render_engine", text="EEVEE", depress=is_eevee).engine = 'BLENDER_EEVEE_NEXT'
row.operator(f"{PREFIX}.switch_render_engine", text="Cycles", depress=not is_eevee).engine = 'CYCLES'
layout.separator()
row = layout.row(align=True)
row.operator(f"{PREFIX}.copy_defaults", icon='COPYDOWN')
row.operator(f"{PREFIX}.reset_defaults", icon='FILE_REFRESH')
layout.operator(f"{PREFIX}.generate", icon='PLAY')
box = layout.box()
box.label(text="【共通事象 (X軸)】")
row = box.row(align=True)
row.prop(props, "green_x_min")
row.prop(props, "green_x_max")
col = box.column(align=True)
col.prop(props, "color_green")
col.prop(props, "color_green", index=3, text="透明度", slider=True)
box = layout.box()
box.label(text="【青平面 (俺)】", icon='COLOR')
col = box.column(align=True)
col.prop(props, "color_blue_front")
col.prop(props, "color_blue_front", index=3, text="透明度 (表面)", slider=True)
col.separator()
col.prop(props, "color_blue_back")
col.prop(props, "color_blue_back", index=3, text="透明度 (裏面)", slider=True)
box = layout.box()
box.label(text="【赤平面 (貴殿)】", icon='COLOR')
col = box.column(align=True)
col.prop(props, "color_red_front")
col.prop(props, "color_red_front", index=3, text="透明度 (表面)", slider=True)
col.separator()
col.prop(props, "color_red_back")
col.prop(props, "color_red_back", index=3, text="透明度 (裏面)", slider=True)
box2 = layout.box()
box2.prop(props, "use_emission", toggle=True, icon='LIGHT')
if props.use_emission:
col2 = box2.column(align=True)
col2.prop(props, "emission_strength_front")
col2.prop(props, "emission_strength_back")
box_bloom = layout.box()
box_bloom.label(text="【 Bloom設定 】", icon='SOLO_ON')
box_bloom.prop(props, "enable_bloom", toggle=True, icon='NODE_COMPOSITING')
if props.enable_bloom:
col_b = box_bloom.column(align=True)
col_b.prop(props, "bloom_mix", slider=True)
col_b.prop(props, "bloom_threshold", slider=True)
col_b.prop(props, "world_brightness", slider=True)
box = layout.box()
box.label(text="【テキスト設定】")
col = box.column(align=True)
col.prop(props, "color_text")
col.prop(props, "color_text", index=3, text="透明度", slider=True)
box.prop(props, "text_size")
row = box.row(align=True)
row.prop(props, "text_offset_x", text="X")
row.prop(props, "text_offset_y", text="Y")
row.prop(props, "text_offset_z", text="Z")
row = box.row(align=True)
row.prop(props, "text_rot_x", text="RotX")
row.prop(props, "text_rot_y", text="RotY")
row.prop(props, "text_rot_z", text="RotZ")
box = layout.box()
box.label(text="【全体調整・表示切替】")
box.prop(props, "triangle_extension")
box.prop(props, "show_past_arrow")
box.prop(props, "show_labels")
box = layout.box()
box.label(text="【眼の座標】")
col = box.column(align=True)
col.prop(props, "blue_eye_y")
col.prop(props, "blue_eye_z")
col.separator()
col.prop(props, "red_eye_y")
col.prop(props, "red_eye_z")
layout.separator()
layout.operator(f"{PREFIX}.detach", icon='UNLINKED')
class OBSPLANE_PT_LinksPanel(bpy.types.Panel):
bl_label = "Links"
bl_idname = f"{PREFIX.upper()}_PT_LinksPanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = f"{PREFIX.upper()}_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for name, url in [(LINK_NAME_1, LINK_URL_1), (LINK_NAME_2, LINK_URL_2), (LINK_NAME_3, LINK_URL_3), (LINK_NAME_4, LINK_URL_4), (LINK_NAME_5, LINK_URL_5)]:
if name and url: self.layout.operator(f"{PREFIX}.open_url", text=name, icon='URL').url = url
class OBSPLANE_PT_RemovePanel(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = f"{PREFIX.upper()}_PT_RemovePanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_parent_id = f"{PREFIX.upper()}_PT_MainPanel"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.label(text="Unregisterを呼び出して解除します", icon='ERROR')
self.layout.operator(f"{PREFIX}.uninstall", icon='TRASH')
# =========================================================
# 10. register() / 11. unregister()
# =========================================================
classes = (
OBSPLANE_OT_OpenUrl, OBSPLANE_OT_Uninstall, OBSPLANE_OT_SwitchRenderEngine,
OBSPLANE_OT_CopyDefaults, OBSPLANE_OT_ResetDefaults, OBSPLANE_Properties,
OBSPLANE_OT_Generate, OBSPLANE_OT_Detach, OBSPLANE_PT_MainPanel,
OBSPLANE_PT_LinksPanel, OBSPLANE_PT_RemovePanel
)
def register():
for cls in classes: bpy.utils.register_class(cls)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=OBSPLANE_Properties))
def unregister():
for cls in reversed(classes):
if hasattr(bpy.types, cls.__name__): bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
if __name__ == "__main__":
register()