import bpy from bpy.types import Menu, Panel, UIList from bpy.props import * from lnx.lightmapper import operators, properties, utility import lnx.assets import lnx.utils if lnx.is_reload(__name__): lnx.assets = lnx.reload_module(lnx.assets) lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) class LnxBakeListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") res_x: IntProperty(name="X", description="Texture resolution", default=1024) res_y: IntProperty(name="Y", description="Texture resolution", default=1024) class LNX_UL_BakeList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # We could write some code to decide which icon to use here... custom_icon = 'OBJECT_DATAMODE' # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: row = layout.row() row.prop(item, "obj", text="", emboss=False, icon=custom_icon) col = row.column() col.alignment = 'RIGHT' col.label(text=str(item.res_x) + 'x' + str(item.res_y)) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon) class LnxBakeListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "lnx_bakelist.new_item" bl_label = "Add a new item" def execute(self, context): scn = context.scene scn.lnx_bakelist.add() scn.lnx_bakelist_index = len(scn.lnx_bakelist) - 1 return{'FINISHED'} class LnxBakeListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "lnx_bakelist.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): """ Enable if there's something in the list """ scn = context.scene return len(scn.lnx_bakelist) > 0 def execute(self, context): scn = context.scene list = scn.lnx_bakelist index = scn.lnx_bakelist_index list.remove(index) if index > 0: index = index - 1 scn.lnx_bakelist_index = index return{'FINISHED'} class LnxBakeListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "lnx_bakelist.move_item" bl_label = "Move an item in the list" direction: EnumProperty( items=( ('UP', 'Up', ""), ('DOWN', 'Down', ""),)) def move_index(self): # Move index of an item render queue while clamping it obj = bpy.context.scene index = obj.lnx_bakelist_index list_length = len(obj.lnx_bakelist) - 1 new_index = 0 if self.direction == 'UP': new_index = index - 1 elif self.direction == 'DOWN': new_index = index + 1 new_index = max(0, min(new_index, list_length)) obj.lnx_bakelist.move(index, new_index) obj.lnx_bakelist_index = new_index def execute(self, context): obj = bpy.context.scene list = obj.lnx_bakelist index = obj.lnx_bakelist_index if self.direction == 'DOWN': neighbor = index + 1 self.move_index() elif self.direction == 'UP': neighbor = index - 1 self.move_index() else: return{'CANCELLED'} return{'FINISHED'} class LnxBakeButton(bpy.types.Operator): '''Bake textures for listed objects''' bl_idname = 'lnx.bake_textures' bl_label = 'Bake' def execute(self, context): scn = context.scene if len(scn.lnx_bakelist) == 0: return{'FINISHED'} self.report({'INFO'}, "Once baked, hit 'Leenkx Bake - Apply' to pack lightmaps") # At least one material required for now.. for o in scn.lnx_bakelist: ob = o.obj if len(ob.material_slots) == 0: if not 'MaterialDefault' in bpy.data.materials: mat = bpy.data.materials.new(name='MaterialDefault') mat.use_nodes = True else: mat = bpy.data.materials['MaterialDefault'] ob.data.materials.append(mat) # Single user materials for o in scn.lnx_bakelist: ob = o.obj for slot in ob.material_slots: # Temp material already exists if slot.material.name.endswith('_temp'): continue n = slot.material.name + '_' + ob.name + '_temp' if not n in bpy.data.materials: slot.material = slot.material.copy() slot.material.name = n # Images for baking for o in scn.lnx_bakelist: ob = o.obj img_name = ob.name + '_baked' sc = scn.lnx_bakelist_scale / 100 rx = o.res_x * sc ry = o.res_y * sc # Get image if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != rx or bpy.data.images[img_name].size[1] != ry: img = bpy.data.images.new(img_name, int(rx), int(ry)) img.name = img_name # Force img_name (in case Blender picked img_name.001) else: img = bpy.data.images[img_name] for slot in ob.material_slots: # Add image nodes mat = slot.material mat.use_nodes = True nodes = mat.node_tree.nodes if 'Baked Image' in nodes: img_node = nodes['Baked Image'] else: img_node = nodes.new('ShaderNodeTexImage') img_node.name = 'Baked Image' img_node.location = (100, 100) img_node.image = img img_node.select = True nodes.active = img_node obs = bpy.context.view_layer.objects # Unwrap active = obs.active for o in scn.lnx_bakelist: ob = o.obj uv_layers = ob.data.uv_layers if not 'UVMap_baked' in uv_layers: uvmap = uv_layers.new(name='UVMap_baked') uv_layers.active_index = len(uv_layers) - 1 obs.active = ob if scn.lnx_bakelist_unwrap == 'Lightmap Pack': bpy.context.view_layer.objects.active = ob ob.select_set(True) bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES') else: bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = ob ob.select_set(True) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project() bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') else: for i in range(0, len(uv_layers)): if uv_layers[i].name == 'UVMap_baked': uv_layers.active_index = i break obs.active = active # Materials for runtime # TODO: use single mat per object for o in scn.lnx_bakelist: ob = o.obj img_name = ob.name + '_baked' for slot in ob.material_slots: n = slot.material.name[:-5] + '_baked' if not n in bpy.data.materials: mat = bpy.data.materials.new(name=n) mat.use_nodes = True mat.use_fake_user = True nodes = mat.node_tree.nodes img_node = nodes.new('ShaderNodeTexImage') img_node.name = 'Baked Image' img_node.location = (100, 100) img_node.image = bpy.data.images[img_name] mat.node_tree.links.new(img_node.outputs[0], nodes['Principled BSDF'].inputs[0]) else: mat = bpy.data.materials[n] nodes = mat.node_tree.nodes nodes['Baked Image'].image = bpy.data.images[img_name] # Bake bpy.ops.object.select_all(action='DESELECT') for o in scn.lnx_bakelist: o.obj.select_set(True) obs.active = scn.lnx_bakelist[0].obj bpy.ops.object.bake('INVOKE_DEFAULT', type='COMBINED') bpy.ops.object.select_all(action='DESELECT') return{'FINISHED'} class LnxBakeApplyButton(bpy.types.Operator): '''Pack baked textures and restore materials''' bl_idname = 'lnx.bake_apply' bl_label = 'Apply' def execute(self, context): scn = context.scene if len(scn.lnx_bakelist) == 0: return{'FINISHED'} for material in bpy.data.materials: if not material.users: bpy.data.materials.remove(material) # Remove leftover _baked materials for removed objects for mat in bpy.data.materials: if mat.name.endswith('_baked'): has_user = False for ob in bpy.data.objects: if ob.type == 'MESH' and mat.name.endswith('_' + ob.name + '_baked'): has_user = True break if not has_user: bpy.data.materials.remove(mat, do_unlink=True) # Recache lightmaps lnx.assets.invalidate_unpacked_data(None, None) for o in scn.lnx_bakelist: ob = o.obj img_name = ob.name + '_baked' # Save images bpy.data.images[img_name].pack() #bpy.data.images[img_name].save() for slot in ob.material_slots: mat = slot.material # Remove temp material if mat.name.endswith('_temp'): old = slot.material slot.material = bpy.data.materials[old.name.split('_' + ob.name)[0] + "_" + ob.name + "_baked"] bpy.data.materials.remove(old, do_unlink=True) # Restore uv slots for o in scn.lnx_bakelist: ob = o.obj uv_layers = ob.data.uv_layers uv_layers.active_index = 0 uv_layers["UVMap_baked"].active_render = True return{'FINISHED'} class LnxBakeSpecialsMenu(bpy.types.Menu): bl_label = "Bake" bl_idname = "LNX_MT_BakeListSpecials" def draw(self, context): layout = self.layout layout.operator("lnx.bake_add_all") layout.operator("lnx.bake_add_selected") layout.operator("lnx.bake_clear_all") layout.operator("lnx.bake_remove_baked_materials") class LnxBakeAddAllButton(bpy.types.Operator): '''Fill the list with scene objects''' bl_idname = 'lnx.bake_add_all' bl_label = 'Add All' def execute(self, context): scn = context.scene scn.lnx_bakelist.clear() for ob in scn.objects: if ob.type == 'MESH': scn.lnx_bakelist.add().obj = ob return{'FINISHED'} class LnxBakeAddSelectedButton(bpy.types.Operator): '''Add selected objects to the list''' bl_idname = 'lnx.bake_add_selected' bl_label = 'Add Selected' def contains(self, scn, ob): for o in scn.lnx_bakelist: if o == ob: return True return False def execute(self, context): scn = context.scene for ob in context.selected_objects: if ob.type == 'MESH' and not self.contains(scn, ob): scn.lnx_bakelist.add().obj = ob return{'FINISHED'} class LnxBakeClearAllButton(bpy.types.Operator): '''Clear the list''' bl_idname = 'lnx.bake_clear_all' bl_label = 'Clear' def execute(self, context): scn = context.scene scn.lnx_bakelist.clear() return{'FINISHED'} class LnxBakeRemoveBakedMaterialsButton(bpy.types.Operator): '''Clear the list''' bl_idname = 'lnx.bake_remove_baked_materials' bl_label = 'Remove Baked Materials' def execute(self, context): for mat in bpy.data.materials: if mat.name.endswith('_baked'): bpy.data.materials.remove(mat, do_unlink=True) return{'FINISHED'} __REG_CLASSES = ( LnxBakeListItem, LNX_UL_BakeList, LnxBakeListNewItem, LnxBakeListDeleteItem, LnxBakeListMoveItem, LnxBakeButton, LnxBakeApplyButton, LnxBakeSpecialsMenu, LnxBakeAddAllButton, LnxBakeAddSelectedButton, LnxBakeClearAllButton, LnxBakeRemoveBakedMaterialsButton ) __reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): __reg_classes() bpy.types.Scene.lnx_bakelist_scale = FloatProperty( name="Resolution", description="Resolution scale", subtype='PERCENTAGE', default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0 ) bpy.types.Scene.lnx_bakelist = CollectionProperty(type=LnxBakeListItem) bpy.types.Scene.lnx_bakelist_index = IntProperty(name="Index for my_list", default=0) bpy.types.Scene.lnx_bakelist_unwrap = EnumProperty( name="UV Unwrap", default='Smart UV Project', items=[ ('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), ('Smart UV Project', 'Smart UV Project', 'Smart UV Project') ] ) # Register lightmapper bpy.types.Scene.lnx_bakemode = EnumProperty( name="Bake mode", default='Static Map', items=[ ('Static Map', 'Static Map', 'Static Map'), ('Lightmap', 'Lightmap', 'Lightmap') ] ) operators.register() properties.register() def unregister(): __unreg_classes() # Unregister lightmapper operators.unregister() properties.unregister()