Live patch - Light properties, object visibility and world

This commit is contained in:
2026-07-21 20:57:36 -07:00
parent 48141f23c5
commit 57cf4955a1

View File

@ -1,5 +1,6 @@
import os
import shutil
import math
from typing import Any, Type
import bpy
@ -52,6 +53,24 @@ def start():
for light_type in (bpy.types.AreaLight, bpy.types.PointLight, bpy.types.SpotLight, bpy.types.SunLight):
listen(light_type, "color", "light_color")
listen(light_type, "energy", "light_energy")
listen(light_type, "use_shadow", "light_shadow")
listen(light_type, "lnx_clip_start", "light_near")
listen(light_type, "lnx_clip_end", "light_far")
listen(light_type, "lnx_fov", "light_fov")
listen(light_type, "lnx_shadows_bias", "light_bias")
listen(bpy.types.SpotLight, "spot_size", "light_spot_size")
listen(bpy.types.SpotLight, "spot_blend", "light_spot_blend")
listen(bpy.types.PointLight, "shadow_soft_size", "light_size")
listen(bpy.types.SpotLight, "shadow_soft_size", "light_size")
listen(bpy.types.AreaLight, "size", "light_area_size")
listen(bpy.types.AreaLight, "size_y", "light_area_size")
listen(bpy.types.Camera, "lens", "cam_props")
listen(bpy.types.Camera, "sensor_width", "cam_props")
listen(bpy.types.Camera, "clip_start", "cam_props")
listen(bpy.types.Camera, "clip_end", "cam_props")
listen(bpy.types.Camera, "lnx_frustum_culling", "cam_props")
global __running
__running = True
@ -78,9 +97,59 @@ def get_obj_transforms(obj):
def get_light_props(obj):
light = obj.data
color = (round(light.color[0], 6), round(light.color[1], 6), round(light.color[2], 6))
energy = round(light.energy, 6)
return color, energy
props = {
'light_color': (round(light.color[0], 6), round(light.color[1], 6), round(light.color[2], 6)),
'light_energy': round(light.energy, 6),
'light_shadow': bool(light.use_shadow),
'light_near': round(light.lnx_clip_start, 6),
'light_far': round(light.lnx_clip_end, 6),
'light_fov': round(light.lnx_fov, 6),
'light_bias': round(light.lnx_shadows_bias * 0.0001, 6),
}
if light.type == 'SPOT':
half_angle = light.spot_size * 0.5
outer_cos = math.cos(half_angle)
blend = max(0.0, min(1.0, light.spot_blend))
inner_angle = math.atan(math.tan(half_angle) * (1.0 - blend))
props['light_spot_size'] = round(outer_cos, 6)
props['light_spot_blend'] = round(max(0.0001, math.cos(inner_angle) - outer_cos), 6)
if light.type in ('POINT', 'SPOT'):
props['light_size'] = round(light.shadow_soft_size * 10, 6)
if light.type == 'AREA':
props['light_area_size'] = (round(light.size, 6), round(light.size_y, 6))
return props
def get_camera_props(obj):
cam = obj.data
return {
'cam_props': (
round(cam.lens, 6),
round(cam.sensor_width, 6),
round(cam.clip_start, 6),
round(cam.clip_end, 6),
bool(cam.lnx_frustum_culling),
),
}
def get_world_clear_color():
scene = bpy.context.scene
if scene.world is None:
return (0.051, 0.051, 0.051, 1.0), 1.0
if scene.world.node_tree is None:
c = scene.world.color
return (round(c[0], 6), round(c[1], 6), round(c[2], 6), 1.0), 1.0
if 'Background' in scene.world.node_tree.nodes:
bg = scene.world.node_tree.nodes['Background']
col = bg.inputs[0].default_value
strength = bg.inputs[1].default_value
r = max(min(col[0], 1.0), 0.0)
g = max(min(col[1], 1.0), 0.0)
b = max(min(col[2], 1.0), 0.0)
a = max(min(col[3], 1.0), 0.0)
return (round(r, 6), round(g, 6), round(b, 6), round(a, 6)), round(strength, 6)
return (0.051, 0.051, 0.051, 1.0), 1.0
DEPSGRAPH_HANDLERS = {
'is_updated_transform': [
@ -107,20 +176,54 @@ def on_depsgraph_update(updates):
for event_id, extractor in DEPSGRAPH_HANDLERS['is_updated_transform']:
current = extractor(uid)
cache_key = (event_id, name)
if current != scene_cache.get(cache_key):
if cache_key not in scene_cache:
scene_cache[cache_key] = current
send_event(event_id)
elif current != scene_cache[cache_key]:
scene_cache[cache_key] = current
send_event(event_id, name)
changed = True
if update.is_updated_shading and isinstance(uid.data, bpy.types.Light):
color, energy = get_light_props(uid)
for event_id, current in (('light_color', color), ('light_energy', energy)):
for event_id, current in get_light_props(uid).items():
cache_key = (event_id, uid.data.name)
if current != scene_cache.get(cache_key):
if cache_key not in scene_cache:
scene_cache[cache_key] = current
send_event(event_id)
elif current != scene_cache[cache_key]:
scene_cache[cache_key] = current
send_event(event_id, name)
changed = True
if update.is_updated_shading and isinstance(uid.data, bpy.types.Camera):
for event_id, current in get_camera_props(uid).items():
cache_key = (event_id, uid.data.name)
if cache_key not in scene_cache:
scene_cache[cache_key] = current
elif current != scene_cache[cache_key]:
scene_cache[cache_key] = current
send_event(event_id, name)
changed = True
elif isinstance(uid, bpy.types.Scene):
for obj in bpy.data.objects:
visibility_key = ('obj_visibility', obj.name)
current = not obj.hide_get()
if visibility_key not in scene_cache:
scene_cache[visibility_key] = current
elif current != scene_cache[visibility_key]:
scene_cache[visibility_key] = current
send_event('obj_visibility', obj.name)
changed = True
elif isinstance(uid, bpy.types.World):
clear_color_key = ('world_clear_color', uid.name)
current = get_world_clear_color()
if clear_color_key not in scene_cache:
scene_cache[clear_color_key] = current
elif current != scene_cache[clear_color_key]:
scene_cache[clear_color_key] = current
send_event('world_clear_color')
changed = True
return changed
@ -189,47 +292,134 @@ def send_event(event_id: str, opt_data: Any = None):
if not __running:
return
if hasattr(bpy.context, 'object') and bpy.context.object is not None:
obj = bpy.context.object.name
obj_name = opt_data if isinstance(opt_data, str) else None
if obj_name is not None:
obj_data = bpy.data.objects.get(obj_name)
elif hasattr(bpy.context, 'object') and bpy.context.object is not None:
obj_data = bpy.context.object
obj_name = obj_data.name
else:
obj_data = None
if bpy.context.object.mode == "OBJECT":
if event_id == "obj_location":
vec = bpy.context.object.location
js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.loc.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;'
write_patch(js)
if obj_data is not None and obj_data.mode == "OBJECT":
if event_id == "obj_location":
vec = obj_data.location
js = f'var o = iron.Scene.active.getChild("{obj_name}"); if (o) {{ o.transform.loc.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true; }}'
write_patch(js)
elif event_id == 'obj_scale':
vec = bpy.context.object.scale
js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.scale.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;'
write_patch(js)
elif event_id == 'obj_scale':
vec = obj_data.scale
js = f'var o = iron.Scene.active.getChild("{obj_name}"); if (o) {{ o.transform.scale.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true; }}'
write_patch(js)
elif event_id == 'obj_rotation':
vec = bpy.context.object.rotation_euler.to_quaternion()
js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.rot.set({vec[1]}, {vec[2]}, {vec[3]}, {vec[0]}); o.transform.dirty = true;'
write_patch(js)
elif event_id == 'obj_rotation':
vec = obj_data.rotation_euler.to_quaternion()
js = f'var o = iron.Scene.active.getChild("{obj_name}"); if (o) {{ o.transform.rot.set({vec[1]}, {vec[2]}, {vec[3]}, {vec[0]}); o.transform.dirty = true; }}'
write_patch(js)
elif event_id == 'light_color':
light: bpy.types.Light = bpy.context.object.data
vec = light.color
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.color[0]={vec[0]}; lRaw.color[1]={vec[1]}; lRaw.color[2]={vec[2]};'
write_patch(js)
elif event_id == 'light_color':
light: bpy.types.Light = obj_data.data
vec = light.color
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.color[0]={vec[0]}; lRaw.color[1]={vec[1]}; lRaw.color[2]={vec[2]};'
write_patch(js)
elif event_id == 'light_energy':
light: bpy.types.Light = bpy.context.object.data
elif event_id == 'light_energy':
light: bpy.types.Light = obj_data.data
# Align strength to Leenkx, see exporter.export_light()
# TODO: Use exporter.export_light() and simply reload all raw light data in Iron?
strength_fac = 1.0
if light.type == 'SUN':
strength_fac = 0.325
elif light.type in ('POINT', 'SPOT', 'AREA'):
strength_fac = 0.01
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.strength={light.energy * strength_fac};'
write_patch(js)
# Align strength to Leenkx, see exporter.export_light()
# TODO: Use exporter.export_light() and simply reload all raw light data in Iron?
strength_fac = 1.0
if light.type == 'SUN':
strength_fac = 0.325
elif light.type in ('POINT', 'SPOT', 'AREA'):
strength_fac = 0.01
elif event_id == 'light_shadow':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.cast_shadow={"true" if light.use_shadow else "false"};'
write_patch(js)
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.strength={light.energy * strength_fac};'
write_patch(js)
elif event_id == 'light_near':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.near_plane={light.lnx_clip_start};'
write_patch(js)
else:
patch_export()
elif event_id == 'light_far':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.far_plane={light.lnx_clip_end};'
write_patch(js)
elif event_id == 'light_fov':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.fov={light.lnx_fov};'
write_patch(js)
elif event_id == 'light_bias':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.shadows_bias={light.lnx_shadows_bias * 0.0001};'
write_patch(js)
elif event_id == 'light_spot_size':
light: bpy.types.Light = obj_data.data
half_angle = light.spot_size * 0.5
outer_cos = math.cos(half_angle)
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.spot_size={outer_cos};'
write_patch(js)
elif event_id == 'light_spot_blend':
light: bpy.types.Light = obj_data.data
half_angle = light.spot_size * 0.5
outer_cos = math.cos(half_angle)
blend = max(0.0, min(1.0, light.spot_blend))
inner_angle = math.atan(math.tan(half_angle) * (1.0 - blend))
spot_blend = max(0.0001, math.cos(inner_angle) - outer_cos)
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.spot_blend={spot_blend};'
write_patch(js)
elif event_id == 'light_size':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.light_size={light.shadow_soft_size * 10};'
write_patch(js)
elif event_id == 'light_area_size':
light: bpy.types.Light = obj_data.data
js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.size={light.size}; lRaw.size_y={light.size_y};'
write_patch(js)
elif event_id == 'cam_props':
cam: bpy.types.Camera = obj_data.data
render = bpy.context.scene.render
projection = obj_data.calc_matrix_camera(
bpy.context.evaluated_depsgraph_get(),
x=render.resolution_x,
y=render.resolution_y,
scale_x=render.pixel_aspect_x,
scale_y=render.pixel_aspect_y)
focal_y = projection[1][1]
depth_z = projection[2][2]
depth_w = projection[2][3]
far_near_ratio = (depth_z - 1.0) / (depth_z + 1.0)
fov = 2.0 * math.atan(1.0 / focal_y)
near = (depth_w * (1.0 - far_near_ratio)) / (2.0 * far_near_ratio)
far = far_near_ratio * near
cam_obj = f'iron.Scene.active.getCamera("{cam.name}")'
js = f'var c = {cam_obj}; var cRaw = c.data.raw; cRaw.fov={fov}; cRaw.near_plane={near}; cRaw.far_plane={far}; cRaw.frustum_culling={"true" if cam.lnx_frustum_culling else "false"}; c.buildProjection();'
write_patch(js)
elif event_id == 'obj_visibility':
visible = not obj_data.hide_get()
js = f'var o = iron.Scene.active.getChild("{obj_name}"); if (o) o.visible = {"true" if visible else "false"};'
write_patch(js)
elif obj_data is not None:
patch_export()
if event_id == 'world_clear_color':
color, strength = get_world_clear_color()
bg_int = (int(color[3] * 255) << 24) + (int(color[0] * 255) << 16) + (int(color[1] * 255) << 8) + int(color[2] * 255)
js = f'var w = iron.Scene.active.world; if (w) {{ w.raw.background_color={bg_int}; if (w.probe) w.probe.raw.strength={strength}; }}'
write_patch(js)
if event_id == 'ln_insert_link':
node: LnxLogicTreeNode
@ -414,6 +604,12 @@ def on_operator(operator_id: str):
send_event('obj_rotation')
elif operator_id == 'TRANSFORM_OT_resize':
send_event('obj_scale')
elif operator_id in ('OBJECT_OT_hide_view_set', 'OBJECT_OT_hide_view_clear', 'OBJECT_OT_visible_all', 'OUTLINER_OT_hide'):
visible = operator_id != 'OBJECT_OT_hide_view_set'
if hasattr(bpy.context, 'object') and bpy.context.object is not None:
obj_name = bpy.context.object.name
js = f'var o = iron.Scene.active.getChild("{obj_name}"); if (o) o.visible = {"true" if visible else "false"};'
write_patch(js)
# Rebuild
else: