Skip to content

Commit

Permalink
feat: add Flare (Lighting) operator
Browse files Browse the repository at this point in the history
  • Loading branch information
tristan-hm authored May 10, 2022
1 parent 22e7e54 commit 83e9be0
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 3 deletions.
6 changes: 5 additions & 1 deletion interface/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@
("nd.cycle", 'LONGDISPLAY', None, None, False),
("nd.smooth", 'MOD_SMOOTH', None, None, False),
("nd.seams", 'UV_DATA', None, None, False),
("nd.hydrate", 'SHADING_RENDERED', None, None, False),
("nd.clear_vgs", 'GROUP_VERTEX', None, None, False),
("nd.triangulate", 'MOD_TRIANGULATE', None, None, False),
]

misc_ops = [
("nd.hydrate", 'SHADING_RENDERED', None, None, False),
("nd.flare", 'LIGHT_AREA', "Flare (Lighting)", None, False),
]

toggle_ops = [
("nd.toggle_wireframes", 'MOD_WIREFRAME', None, None, False),
("nd.toggle_face_orientation", "ORIENTATION_NORMAL", None, None, False),
Expand Down
4 changes: 3 additions & 1 deletion interface/utils_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ def draw(self, context):
layout.separator()
layout.operator("nd.smooth", icon='MOD_SMOOTH')
layout.operator("nd.seams", icon='UV_DATA')
layout.operator("nd.hydrate", icon='SHADING_RENDERED')
layout.operator("nd.clear_vgs", icon='GROUP_VERTEX')
layout.operator("nd.triangulate", icon='MOD_TRIANGULATE')
layout.separator()
layout.operator("nd.hydrate", icon='SHADING_RENDERED')
layout.operator("nd.flare", text="Flare (Lighting)", icon='LIGHT_AREA')


def draw_item(self, context):
Expand Down
5 changes: 4 additions & 1 deletion interface/utils_ui_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import bpy
from .. import bl_info
from . ops import object_names_ops, object_transform_ops, object_properties_ops
from . ops import object_names_ops, object_transform_ops, object_properties_ops, misc_ops
from . common import create_box, render_ops


Expand All @@ -33,6 +33,9 @@ def draw(self, context):
box = create_box("Object Properties", 'MESH_DATA', layout)
render_ops(object_properties_ops, box)

box = create_box("Miscellaneous", 'ASSET_MANAGER', layout)
render_ops(misc_ops, box)


def register():
bpy.utils.register_class(ND_PT_utils_ui_panel)
Expand Down
2 changes: 2 additions & 0 deletions utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from . import hydrate
from . import clear_vgs
from . import cycle
from . import flare
from . import triangulate


Expand All @@ -28,6 +29,7 @@
hydrate,
clear_vgs,
cycle,
flare,
triangulate
)

Expand Down
321 changes: 321 additions & 0 deletions utils/flare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
# “Commons Clause” License Condition v1.0
#
# See LICENSE for license details. If you did not receive a copy of the license,
# it may be obtained at https://github.com/hugemenace/nd/blob/main/LICENSE.
#
# Software: ND Blender Addon
# License: MIT
# Licensor: T.S. & I.J. (HugeMenace)

import bpy
from math import radians, degrees, copysign
from random import random, uniform, randrange
from .. lib.overlay import update_overlay, init_overlay, toggle_pin_overlay, toggle_operator_passthrough, register_draw_handler, unregister_draw_handler, draw_header, draw_property, draw_hint
from .. lib.events import capture_modifier_keys, pressed
from .. lib.preferences import get_preferences


class ND_OT_flare(bpy.types.Operator):
bl_idname = "nd.flare"
bl_label = "Flare"
bl_description = "Adds a new lighting rig targeting selected objects position"
bl_options = {'UNDO'}


def modal(self, context, event):
capture_modifier_keys(self, event)

rotation_factor = 1 if self.key_shift else 15
height_factor = 0.1 if self.key_shift else 1
scale_factor = 0.01 if self.key_shift else 0.1
energy_factor = 1000 if self.key_shift else 10000

if self.key_toggle_operator_passthrough:
toggle_operator_passthrough(self)

elif self.key_toggle_pin_overlay:
toggle_pin_overlay(self, event)

elif self.operator_passthrough:
update_overlay(self, context, event)

return {'PASS_THROUGH'}

elif self.key_cancel:
self.revert(context)

return {'CANCELLED'}

elif pressed(event, {'R'}):
self.generate_rig(context)

elif pressed(event, {'C'}):
self.randomise_colors(context)

elif pressed(event, {'E'}):
self.randomise_energy(context)

elif self.key_step_up:
if self.key_no_modifiers:
self.rotation = (self.rotation + rotation_factor) % 360
elif self.key_alt:
self.height_offset += height_factor
elif self.key_ctrl:
self.scale += scale_factor
elif self.key_ctrl_alt:
self.energy_offset += energy_factor

self.dirty = True

elif self.key_step_down:
if self.key_no_modifiers:
self.rotation = (self.rotation - rotation_factor) % 360
elif self.key_alt:
self.height_offset -= height_factor
elif self.key_ctrl:
self.scale = max(0, self.scale - scale_factor)
elif self.key_ctrl_alt:
self.energy_offset = max(0, self.energy_offset - energy_factor)

self.dirty = True

elif self.key_confirm:
self.finish(context)

return {'FINISHED'}

elif self.key_movement_passthrough:
return {'PASS_THROUGH'}

if get_preferences().enable_mouse_values:
if self.key_alt:
self.height_offset += self.mouse_value
elif self.key_ctrl:
self.scale = max(0, self.scale + self.mouse_value)
elif self.key_ctrl_alt:
self.energy_offset += self.mouse_value * 2500
elif self.key_no_modifiers:
self.rotation = (self.rotation + self.mouse_value_mag) % 360

self.dirty = True

if self.dirty:
self.operate(context)

update_overlay(self, context, event)

return {'RUNNING_MODAL'}


def reset_values(self, context):
self.rotation = 0
self.height_offset = 0
self.scale = 1
self.energy_offset = 0


def invoke(self, context, event):
self.dirty = False
self.summoned = False
self.regenerated_rig = False

self.reset_values(context)

self.last_rotation = 0
self.last_height_offset = 0
self.last_scale = 1
self.last_energy_offset = 0

if context.object.type == 'EMPTY':
self.summoned = True
self.empty = context.object
self.rotation_prev = self.rotation = degrees(self.empty.rotation_euler[2])
self.scale_prev = self.scale = self.empty.scale[0]

existing_lights = [light for light in self.empty.children if light.type == 'LIGHT' and light.data.type == 'AREA']

if not existing_lights:
self.report({'ERROR'}, "Please select a valid Flare Lighting Rig")
return {'CANCELLED'}

self.lights = [(light, light.location[2], light.data.energy) for light in existing_lights]
self.prev_lights_snapshot = [(light.data.energy, light.data.size, light.data.color.copy(), light.location.copy()) for light in existing_lights]
else:
self.create_empty(context)
self.generate_rig(context)

self.operate(context)

capture_modifier_keys(self, None, event.mouse_x)

init_overlay(self, event)
register_draw_handler(self, draw_text_callback)

context.window_manager.modal_handler_add(self)

return {'RUNNING_MODAL'}


@classmethod
def poll(cls, context):
if context.mode == 'OBJECT':
return len(context.selected_objects) == 1 and context.object.type in {'MESH', 'EMPTY'}


def create_empty(self, context):
empty = bpy.data.objects.new("empty", None)
bpy.context.scene.collection.objects.link(empty)
empty.name = "ND — Flare Rig"
empty.empty_display_size = 1
empty.empty_display_type = 'SPHERE'
empty.location = context.object.location.copy()
empty.rotation_euler = (0, 0, radians(self.rotation))
empty.scale = (1, 1, 1)

self.empty = empty


def remove_lights(self, context):
for light, height, energy in self.lights:
bpy.data.lights.remove(light.data, do_unlink=True)

self.lights = []


def generate_rig(self, context):
self.regenerated_rig = True
self.lights = getattr(self, 'lights', [])

self.remove_lights(context)

for i in range(0, randrange(3, 5)):
self.lights.append(self.add_light(context))

self.reset_values(context)
self.operate(context)


def randomise_colors(self, context):
for light, height, energy in self.lights:
light.data.color = (random(), random(), random())


def randomise_energy(self, context):
for light, height, energy in self.lights:
light.data.energy = uniform(1000, 25000)


def add_light(self, context, energy=None, size=None, color=None, location=None):
light_data = bpy.data.lights.new(name="ND — Flare Light.001", type='AREA')
light_data.energy = uniform(1000, 25000) if energy is None else energy
light_data.size = uniform(5, 30) if size is None else size
light_data.color = (random(), random(), random()) if color is None else color

light_object = bpy.data.objects.new(name="ND — Flare Light.001", object_data=light_data)
light_object.parent = self.empty

bpy.context.scene.collection.objects.link(light_object)

light_object.location = (uniform(-30, 30), uniform(-30, 30), uniform(5, 50)) if location is None else location

damped_track = light_object.constraints.new('DAMPED_TRACK')
damped_track.target = self.empty
damped_track.track_axis = 'TRACK_NEGATIVE_Z'

return (light_object, light_object.location[2], light_data.energy)


def operate(self, context):
if self.rotation != self.last_rotation:
self.empty.rotation_euler = (0, 0, radians(self.rotation))
self.last_rotation = self.rotation

if self.scale != self.last_scale:
self.empty.scale = (self.scale, self.scale, self.scale)
self.last_scale = self.scale

if self.height_offset != self.last_height_offset:
for light, height, energy in self.lights:
light.location[2] = height + self.height_offset
self.last_height_offset = self.height_offset

if self.energy_offset != self.last_energy_offset:
for light, height, energy in self.lights:
light.data.energy = max(0, energy + self.energy_offset)
self.last_energy_offset = self.energy_offset

self.dirty = False


def finish(self, context):
unregister_draw_handler()


def revert(self, context):
if self.summoned:
self.empty.rotation_euler = (0, 0, radians(self.rotation_prev))
self.empty.scale = (self.scale_prev, self.scale_prev, self.scale_prev)

self.remove_lights(context)
for energy, size, color, location in self.prev_lights_snapshot:
self.lights.append(self.add_light(context, energy, size, color, location))
else:
self.remove_lights(context)
bpy.data.objects.remove(self.empty, do_unlink=True)

unregister_draw_handler()


def draw_text_callback(self):
draw_header(self)

draw_property(
self,
"Rotation: {0:.1f}".format(self.rotation),
"(±15) | Shift (±1)",
active=self.key_no_modifiers,
alt_mode=self.key_shift_no_modifiers,
mouse_value=True)

draw_property(
self,
"Height Offset: {0:.1f}".format(self.height_offset),
"Alt (±1) | Shift + Alt (±0.1)",
active=self.key_alt,
alt_mode=self.key_shift_alt,
mouse_value=True)

draw_property(
self,
"Scale: {0:.2f}".format(self.scale),
"Ctrl (±0.1) | Shift + Ctrl (±0.01)",
active=self.key_ctrl,
alt_mode=self.key_shift_ctrl,
mouse_value=True)

draw_property(
self,
"Energy Offset: {0:.1e}".format(self.energy_offset),
"Ctrl + Alt (±10k) | Shift + Ctrl + Alt (±1k)",
active=self.key_ctrl_alt,
alt_mode=self.key_shift_ctrl_alt,
mouse_value=True)

draw_hint(self, "New Rig [R]", "Generate a new randomised rig and reset light options")

draw_hint(self, "New Colors [C]", "Generate new randomised colors for the lights")

draw_hint(self, "New Energy [E]", "Generate new randomised energy for the lights")


def menu_func(self, context):
self.layout.operator(ND_OT_flare.bl_idname, text=ND_OT_flare.bl_label)


def register():
bpy.utils.register_class(ND_OT_flare)


def unregister():
bpy.utils.unregister_class(ND_OT_flare)
unregister_draw_handler()

0 comments on commit 83e9be0

Please sign in to comment.