145 lines
4.6 KiB
Python
145 lines
4.6 KiB
Python
bl_info = {
|
|
"name": "View-Aligned HDRI Lighting",
|
|
"author": "ChatGPT",
|
|
"version": (1, 4, 0),
|
|
"blender": (3, 3, 0),
|
|
"location": "3D Viewport > Sidebar (N) > View tab",
|
|
"description": "Keeps studio HDRI lighting aligned relative to the camera/view. Includes UI and offset control. Starts OFF by default.",
|
|
"category": "3D View",
|
|
}
|
|
|
|
import bpy
|
|
import math
|
|
from bpy.app.handlers import persistent
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Properties
|
|
# -----------------------------------------------------------------------------
|
|
|
|
class VAH_Properties(bpy.types.PropertyGroup):
|
|
enabled: bpy.props.BoolProperty(
|
|
name="View-Aligned HDRI",
|
|
description="Rotate viewport HDRI relative to camera orientation",
|
|
default=False,
|
|
update=lambda self, ctx: ensure_timer_running()
|
|
)
|
|
|
|
offset_degrees: bpy.props.FloatProperty(
|
|
name="Light Angle Offset",
|
|
description="Offset angle relative to the camera (degrees)",
|
|
default=35.0,
|
|
min=-180.0,
|
|
max=180.0,
|
|
soft_min=-180.0,
|
|
soft_max=180.0,
|
|
step=10,
|
|
precision=2
|
|
)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Core logic
|
|
# -----------------------------------------------------------------------------
|
|
|
|
_timer_running = False
|
|
|
|
def get_view_yaw(rv3d):
|
|
eul = rv3d.view_rotation.to_euler('XYZ')
|
|
return eul.z
|
|
|
|
def apply_to_view(space, props):
|
|
if space.shading.type not in {'SOLID', 'MATERIAL'}:
|
|
return
|
|
rv3d = space.region_3d
|
|
deg = ((props.offset_degrees + 180.0) % 360.0) - 180.0
|
|
offset = math.radians(deg)
|
|
yaw = get_view_yaw(rv3d)
|
|
space.shading.studiolight_rotate_z = -yaw + offset
|
|
|
|
def sync_light():
|
|
global _timer_running
|
|
wm = bpy.context.window_manager
|
|
if not hasattr(wm, "vah_props"):
|
|
return None
|
|
props = wm.vah_props
|
|
if not props.enabled:
|
|
_timer_running = False
|
|
return None
|
|
for window in wm.windows:
|
|
for area in window.screen.areas:
|
|
if area.type != 'VIEW_3D':
|
|
continue
|
|
for space in area.spaces:
|
|
if space.type == 'VIEW_3D':
|
|
try:
|
|
apply_to_view(space, props)
|
|
except Exception:
|
|
pass
|
|
return 0.05
|
|
|
|
def ensure_timer_running():
|
|
global _timer_running
|
|
wm = bpy.context.window_manager
|
|
if not hasattr(wm, "vah_props"):
|
|
return
|
|
if wm.vah_props.enabled and not _timer_running:
|
|
bpy.app.timers.register(sync_light, first_interval=0.1)
|
|
_timer_running = True
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# UI Panel
|
|
# -----------------------------------------------------------------------------
|
|
|
|
class VIEW3D_PT_view_aligned_hdri(bpy.types.Panel):
|
|
bl_label = "View-Aligned HDRI"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'View'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
wm = context.window_manager
|
|
props = wm.vah_props
|
|
col = layout.column(align=True)
|
|
col.prop(props, "enabled", toggle=True)
|
|
sub = col.column(align=True)
|
|
sub.enabled = props.enabled
|
|
sub.prop(props, "offset_degrees", slider=True)
|
|
layout.label(text="Works in Solid & Material Preview modes", icon='INFO')
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Startup behavior
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@persistent
|
|
def load_post_handler(dummy):
|
|
# Do nothing automatically; user will turn it on manually
|
|
pass
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Registration
|
|
# -----------------------------------------------------------------------------
|
|
|
|
classes = (
|
|
VAH_Properties,
|
|
VIEW3D_PT_view_aligned_hdri,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.WindowManager.vah_props = bpy.props.PointerProperty(type=VAH_Properties)
|
|
if load_post_handler not in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.append(load_post_handler)
|
|
|
|
def unregister():
|
|
global _timer_running
|
|
if load_post_handler in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.remove(load_post_handler)
|
|
if hasattr(bpy.types.WindowManager, "vah_props"):
|
|
del bpy.types.WindowManager.vah_props
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
_timer_running = False
|
|
|
|
if __name__ == "__main__":
|
|
register()
|