ad23d70d07
- Version off shader compile parsing so gooengine uses BLENDER_REQUIRE still - Localize the common libs that goo engine uses - Fix library submodules - Restore datafiles
442 lines
12 KiB
Python
442 lines
12 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
Light group key management for Goo Engine (EEVEE)
|
|
"""
|
|
|
|
import bpy
|
|
from bpy.types import Panel, Material, Light, PropertyGroup, UIList, Operator, ShaderNodeShaderInfo, ShaderNodeTree
|
|
from bpy.props import StringProperty, CollectionProperty, IntProperty, PointerProperty, EnumProperty, BoolProperty
|
|
from bpy.utils import register_classes_factory
|
|
from bpy.app.handlers import persistent
|
|
from itertools import chain
|
|
from ctypes import c_int32
|
|
# from time import perf_counter
|
|
|
|
SIZEOF_INT = 32
|
|
MAX_LIGHT_GROUP_BIT = 127
|
|
|
|
|
|
def set_bit(vec, bit):
|
|
index = bit // SIZEOF_INT
|
|
mask = 1 << (bit % SIZEOF_INT)
|
|
if index > 3:
|
|
return
|
|
vec[index] = int(c_int32(vec[index] | mask).value)
|
|
|
|
|
|
def map_bits(data, mapping):
|
|
data.light_group_bits = (0, 0, 0, 0)
|
|
is_mat = isinstance(data, (Material, ShaderNodeShaderInfo))
|
|
if is_mat:
|
|
data.light_group_shadow_bits = data.light_group_bits
|
|
|
|
for grp in data.light_groups.groups:
|
|
index = mapping.get(grp.name)
|
|
if index is not None:
|
|
set_bit(data.light_group_bits, index)
|
|
if is_mat and not grp.ignore_shadow:
|
|
set_bit(data.light_group_shadow_bits, index)
|
|
|
|
if data.light_groups.use_default:
|
|
set_bit(data.light_group_bits, MAX_LIGHT_GROUP_BIT)
|
|
if is_mat and not data.light_groups.ignore_default_shadow:
|
|
set_bit(data.light_group_shadow_bits, MAX_LIGHT_GROUP_BIT)
|
|
|
|
|
|
def iter_light_group_owners():
|
|
for mat in bpy.data.materials:
|
|
yield mat
|
|
|
|
# Assign bits to shader info nodes
|
|
for node_tree in chain(bpy.data.node_groups, map(lambda m: m.node_tree, bpy.data.materials)):
|
|
if not node_tree:
|
|
continue
|
|
for node in node_tree.nodes:
|
|
if isinstance(node, ShaderNodeShaderInfo) and node.use_own_light_groups:
|
|
yield node
|
|
|
|
for light in bpy.data.lights:
|
|
yield light
|
|
|
|
def sync_light_groups():
|
|
# Populate light bit set with groups from Light objects, merging duplicates
|
|
light_names = set()
|
|
|
|
for light in bpy.data.lights:
|
|
for grp in light.light_groups.groups:
|
|
light_names.add(grp.name)
|
|
|
|
if len(light_names) >= MAX_LIGHT_GROUP_BIT:
|
|
print("WARNING: Max number of light groups (127) reached. Some Light Groups will not be included.")
|
|
|
|
# Assign bit numbers - default group is reserved at 127
|
|
bit_mapping = {name: index for index, name in enumerate(light_names)}
|
|
|
|
# Assign bits to each material/light/node through lookup
|
|
for data in iter_light_group_owners():
|
|
map_bits(data, bit_mapping)
|
|
|
|
|
|
def update_handler(_s, _c):
|
|
sync_light_groups()
|
|
|
|
@persistent
|
|
def sync_handler(*_):
|
|
sync_light_groups()
|
|
|
|
@persistent
|
|
def sync_dg_handler(scn, dg):
|
|
if dg.mode == 'RENDER':
|
|
return
|
|
|
|
for update in dg.updates:
|
|
uid = update.id
|
|
|
|
if not uid:
|
|
continue
|
|
|
|
if isinstance(uid, bpy.types.Material) or isinstance(uid, bpy.types.Light):
|
|
sync_light_groups()
|
|
return
|
|
|
|
|
|
def rename_group(data, src, tgt):
|
|
if getattr(data, 'library', False):
|
|
return
|
|
for grp in data.light_groups.groups:
|
|
if grp.name == src:
|
|
grp.name = tgt
|
|
|
|
|
|
def get_name(self):
|
|
return self.name
|
|
|
|
|
|
# Update all matching names in the entire file
|
|
def set_name(self, value):
|
|
orig_name = self.name
|
|
for data in iter_light_group_owners():
|
|
rename_group(data, orig_name, value)
|
|
|
|
|
|
class LightGroup(PropertyGroup):
|
|
name: StringProperty()
|
|
viz_name: StringProperty(name="Name", get=get_name, set=set_name)
|
|
ignore_shadow: BoolProperty(name="Ignore Shadows", description="Ignore shadows cast from this light group", default=False, options=set(), update=update_handler)
|
|
|
|
|
|
class LightGroups(PropertyGroup):
|
|
groups: CollectionProperty(type=LightGroup)
|
|
group_index: IntProperty(name="Active Light Group", update=update_handler)
|
|
use_default: BoolProperty(name="Use default Light Group", default=True, description="Use builtin default light group", update=update_handler)
|
|
ignore_default_shadow: BoolProperty(name="Ignore default Light Group shadows", default=False, description="Ignore default light group shadows", update=update_handler)
|
|
|
|
|
|
def get_name_set():
|
|
names = set()
|
|
|
|
for data in iter_light_group_owners():
|
|
for grp in data.light_groups.groups:
|
|
names.add(grp.name)
|
|
|
|
return names
|
|
|
|
|
|
def unique_group_name():
|
|
names = get_name_set()
|
|
i = 1
|
|
name = "LightGroup"
|
|
while name in names:
|
|
name = f"LightGroup.{i:03}"
|
|
i += 1
|
|
return name
|
|
|
|
|
|
class MAT_UL_LightGroupList(UIList):
|
|
def draw_item(self,
|
|
context: 'Context',
|
|
layout: 'UILayout',
|
|
data: 'AnyType',
|
|
item: 'AnyType',
|
|
icon: int,
|
|
active_data: 'AnyType',
|
|
active_property: str,
|
|
index: int = 0,
|
|
flt_flag: int = 0):
|
|
row = layout.row(align=True)
|
|
row.prop(item, "viz_name", emboss=False, text="")
|
|
if isinstance(data.id_data, (Material, ShaderNodeTree)):
|
|
row.prop(item, "ignore_shadow", text="", icon="REC" if item.ignore_shadow else "OVERLAY", emboss=False)
|
|
|
|
def filter_items(self, context: 'Context', data: 'AnyType', property: str):
|
|
keys = getattr(data, property)
|
|
flt_flags = []
|
|
flt_order = []
|
|
|
|
helper_funcs = bpy.types.UI_UL_list
|
|
|
|
if self.filter_name:
|
|
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, keys, "name")
|
|
|
|
if self.use_filter_sort_alpha:
|
|
flt_order = helper_funcs.sort_items_by_name(keys, "name")
|
|
|
|
return flt_flags, flt_order
|
|
|
|
|
|
def get_groups(obj):
|
|
if obj.type == 'LIGHT':
|
|
return obj.data.light_groups
|
|
mat = obj.active_material
|
|
if not mat:
|
|
mat = obj.material_slots[0].material
|
|
return mat.light_groups
|
|
|
|
|
|
class ALightGroupPanel(Panel):
|
|
bl_label = "Light Groups"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
|
|
def get_groups(self, ctx):
|
|
return get_groups_ctx(ctx)
|
|
|
|
def draw_header(self, context):
|
|
...
|
|
|
|
def draw(self, ctx: 'bpy.types.Context'):
|
|
layout = self.layout
|
|
groups = self.get_groups(ctx)
|
|
row = layout.row()
|
|
row.template_list("MAT_UL_LightGroupList", "",
|
|
groups, "groups", groups, "group_index",
|
|
rows=5, type='DEFAULT')
|
|
# Right hand column with operators
|
|
col = row.column(align=True)
|
|
col.operator('light_groups.link', icon='LINKED', text="")
|
|
col.operator('light_groups.unlink', icon='UNLINKED', text="")
|
|
col.separator()
|
|
col.operator('light_groups.new', icon='ADD', text="")
|
|
col.operator('light_groups.remove', icon='X', text="")
|
|
col.separator()
|
|
col.operator('light_groups.resync', icon='FILE_REFRESH', text="")
|
|
|
|
# Below Operators
|
|
row = layout.row()
|
|
row.prop(groups, 'use_default', text="Use Default Group")
|
|
if self.bl_context != "data" or self.bl_space_type == 'NODE_EDITOR':
|
|
row = row.row()
|
|
row.enabled = groups.use_default
|
|
row.prop(groups, 'ignore_default_shadow', text="Ignore Default Shadows")
|
|
|
|
|
|
class OBJ_PT_MLightGroupPanel(ALightGroupPanel):
|
|
bl_context = 'material'
|
|
|
|
def get_groups(self, ctx):
|
|
mat = ctx.material
|
|
ob = ctx.object
|
|
|
|
if ob:
|
|
data = ob.active_material
|
|
elif mat:
|
|
data = mat
|
|
return data.light_groups
|
|
|
|
@classmethod
|
|
def poll(cls, ctx):
|
|
return ctx.material
|
|
|
|
class OBJ_PT_LLightGroupPanel(ALightGroupPanel):
|
|
bl_context = 'data'
|
|
|
|
def get_groups(self, ctx):
|
|
ob = ctx.object
|
|
light = ctx.light
|
|
space = ctx.space_data
|
|
|
|
if ob:
|
|
data = ob.data
|
|
elif light:
|
|
data = space.pin_id
|
|
|
|
return data.light_groups
|
|
|
|
@classmethod
|
|
def poll(cls, ctx):
|
|
return ctx.light
|
|
|
|
|
|
class NOD_PT_LightGroupPanel(ALightGroupPanel):
|
|
bl_space_type = 'NODE_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Node'
|
|
|
|
@classmethod
|
|
def poll(cls, ctx):
|
|
return isinstance(ctx.active_node, ShaderNodeShaderInfo)
|
|
|
|
def get_groups(self, ctx):
|
|
node = ctx.active_node
|
|
if isinstance(node, ShaderNodeShaderInfo):
|
|
return node.light_groups
|
|
|
|
|
|
class LightGroupOp(Operator):
|
|
bl_options = {'UNDO'}
|
|
|
|
@staticmethod
|
|
def get_groups(ctx):
|
|
return get_groups_ctx(ctx)
|
|
|
|
@classmethod
|
|
def poll(cls, ctx):
|
|
try:
|
|
cls.get_groups(ctx)
|
|
return True
|
|
except Exception as e:
|
|
return False
|
|
|
|
|
|
class LightGroupSelectionOp(Operator):
|
|
bl_options = {'UNDO'}
|
|
|
|
@staticmethod
|
|
def get_groups(ctx):
|
|
return get_groups_ctx(ctx)
|
|
|
|
@classmethod
|
|
def poll(cls, ctx):
|
|
try:
|
|
grp = cls.get_groups(ctx)
|
|
return 0 <= grp.group_index < len(grp.groups)
|
|
except Exception as e:
|
|
return False
|
|
|
|
|
|
class MAT_OT_NewLightGroup(LightGroupOp):
|
|
""" Create a new unique light group """
|
|
bl_label = "New Light Group"
|
|
bl_idname = 'light_groups.new'
|
|
|
|
def execute(self, ctx):
|
|
lgs = self.get_groups(ctx)
|
|
name = unique_group_name()
|
|
new = lgs.groups.add()
|
|
new.name = name
|
|
|
|
lgs.group_index = lgs.groups.find(new.name)
|
|
|
|
sync_light_groups()
|
|
return {'FINISHED'}
|
|
|
|
|
|
def get_groups_ctx(ctx):
|
|
if hasattr(ctx, 'active_node') and ctx.active_node and isinstance(ctx.active_node, ShaderNodeShaderInfo):
|
|
return ctx.active_node.light_groups
|
|
return get_groups(ctx.object)
|
|
|
|
|
|
class MAT_OT_LinkLightGroup(LightGroupOp):
|
|
""" Link an existing light group to this data-block """
|
|
bl_label = "Link Existing Light Group"
|
|
bl_idname = 'light_groups.link'
|
|
bl_property = "name"
|
|
|
|
name: EnumProperty(items=lambda scn, ctx: sorted([(x, x, x) for x in get_name_set() if x not in get_groups_ctx(ctx).groups]))
|
|
|
|
def invoke(self, context: 'Context', event: 'Event'):
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {'FINISHED'}
|
|
|
|
def execute(self, ctx):
|
|
lgs = self.get_groups(ctx)
|
|
new = lgs.groups.add()
|
|
new.name = self.name
|
|
|
|
lgs.group_index = lgs.groups.find(new.name)
|
|
|
|
sync_light_groups()
|
|
return {'FINISHED'}
|
|
|
|
|
|
class MAT_OT_DeleteLightGroup(LightGroupSelectionOp):
|
|
""" Remove this light group from all data-blocks """
|
|
bl_label = "Delete Light Group"
|
|
bl_idname = 'light_groups.remove'
|
|
|
|
def invoke(self, ctx, evt):
|
|
return ctx.window_manager.invoke_confirm(self, evt)
|
|
|
|
def execute(self, ctx):
|
|
lgs = self.get_groups(ctx)
|
|
grp_name = lgs.groups[lgs.group_index].name
|
|
|
|
for data in iter_light_group_owners():
|
|
if grp_name not in data.light_groups.groups:
|
|
continue
|
|
data.light_groups.groups.remove(data.light_groups.groups.find(grp_name))
|
|
|
|
lgs.group_index -= 1
|
|
|
|
sync_light_groups()
|
|
return {'FINISHED'}
|
|
|
|
|
|
class MAT_OT_UnlinkLightGroup(LightGroupSelectionOp):
|
|
""" Remove this light group from this data-block """
|
|
bl_label = "Unlink Light Group"
|
|
bl_idname = 'light_groups.unlink'
|
|
|
|
def execute(self, ctx):
|
|
lgs = self.get_groups(ctx)
|
|
lgs.groups.remove(lgs.group_index)
|
|
|
|
lgs.group_index -= 1
|
|
|
|
sync_light_groups()
|
|
return {'FINISHED'}
|
|
|
|
|
|
class MAT_OT_ResyncLightGroups(LightGroupOp):
|
|
""" Ensure light group layers are up to date """
|
|
bl_label = "Resync Light Groups"
|
|
bl_idname = 'light_groups.resync'
|
|
|
|
def execute(self, ctx):
|
|
sync_light_groups()
|
|
return {'FINISHED'}
|
|
|
|
|
|
_classes = (
|
|
LightGroup,
|
|
LightGroups,
|
|
MAT_UL_LightGroupList,
|
|
OBJ_PT_MLightGroupPanel,
|
|
OBJ_PT_LLightGroupPanel,
|
|
NOD_PT_LightGroupPanel,
|
|
MAT_OT_NewLightGroup,
|
|
MAT_OT_LinkLightGroup,
|
|
MAT_OT_DeleteLightGroup,
|
|
MAT_OT_UnlinkLightGroup,
|
|
MAT_OT_ResyncLightGroups
|
|
)
|
|
|
|
_register, _unregister = register_classes_factory(classes=_classes)
|
|
|
|
|
|
def register():
|
|
_register()
|
|
Material.light_groups = PointerProperty(type=LightGroups)
|
|
Light.light_groups = PointerProperty(type=LightGroups)
|
|
ShaderNodeShaderInfo.light_groups = PointerProperty(type=LightGroups)
|
|
|
|
bpy.app.handlers.render_init.append(sync_handler)
|
|
bpy.app.handlers.load_post.append(sync_handler)
|
|
bpy.app.handlers.depsgraph_update_post.append(sync_dg_handler)
|
|
|
|
|
|
def unregister():
|
|
_unregister()
|