diff --git a/entry_lightmapper.py b/entry_lightmapper.py index f83248c..1615b7d 100644 --- a/entry_lightmapper.py +++ b/entry_lightmapper.py @@ -18,3 +18,4 @@ importlib.reload(lightmapper.lightmapper_panel) lightmapper.__init__.register() + diff --git a/lightmapper/lightmapper_operators.py b/lightmapper/lightmapper_operators.py index a24d6b4..b82f629 100644 --- a/lightmapper/lightmapper_operators.py +++ b/lightmapper/lightmapper_operators.py @@ -42,9 +42,31 @@ class LIGHTMAPPER_OT_bake_lightmap(bpy.types.Operator): bl_description = "Bake lightmap for selected objects" bl_options = {'REGISTER', 'UNDO'} - def check_context(self, context): + _timer = None + progress = 0 + instance = None + + + + def get_bake_name(self, bake_target): + active_object = bpy.context.view_layer.objects.active + + if bake_target == 'ACTIVE_OBJECT': + return active_object.name if active_object else "Unnamed" + elif bake_target == 'COLLECTION': + if active_object and active_object.users_collection: + return active_object.users_collection[0].name + else: + return "Unnamed" + else: + return "Unnamed" + + def check_mesh_objects(self, context, mesh_objects): """ Ensure the object is in a state that supports baking. """ - mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] + if not mesh_objects: + self.report({'ERROR'}, "No mesh objects selected.") + return False + # Ensure we're in object mode. if context.mode != 'OBJECT': self.report({'ERROR'}, "Lightmap baking requires object mode.") @@ -69,9 +91,8 @@ def check_context(self, context): return True - def correct_context(self, context): + def correct_mesh_objects(self, context, mesh_objects): """ Change the state of the selected objects to work for baking.""" - mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] self.report({'INFO'}, "Correcting UV Selections.") # Ensure that the first UVMap is set to render, and that "Lightmap" UV is selected. @@ -81,41 +102,175 @@ def correct_context(self, context): # Ensure the lightmap is selected, as that's the UV we're baking to. obj.data.uv_layers["Lightmap"].active = True - def execute(self, context): - if not self.check_context(context): - return {'CANCELLED'} + def combine_objects(self, context): + """ Combine selected objects into one for baking. """ - self.correct_context(context) + print("Combining objects...") + + bpy.ops.object.duplicate() + # set the active object to the first selected object + bpy.context.view_layer.objects.active = context.selected_objects[0] + # Join duplicated objects into one + bpy.ops.object.join() + + # Store the combined object for later deletion + combined_object = context.active_object + # rename to temp_bake_name + combined_object.name = "temp_bake_object" + return combined_object + + def create_lightmap_image(self, width=2048, height=2048): + """ Create a new lightmap image for baking. """ + new_image = bpy.data.images.new("LightmapBake", width=width, height=height, float_buffer=True) + # give the image a name like temp_diffuse_bake_noise + new_image.name = "temp_lightmap_bake_image" + return new_image + + def assign_lightmap_image(self, context, combined_object, lightmap_image): + # Ensure the object has an active material + if not combined_object.data.materials: + material = bpy.data.materials.new(name="TempBakeMaterial") + combined_object.data.materials.append(material) + + # Get the active material + material = combined_object.active_material + + # Ensure the material uses nodes + if not material.use_nodes: + material.use_nodes = True + + # Access the material's node tree + nodes = material.node_tree.nodes + texture_node = nodes.get('Image Texture') + + # If there's no existing Image Texture node, create one + if texture_node is None: + texture_node = nodes.new(type='ShaderNodeTexImage') + + # Assign the lightmap image to the texture node + texture_node.image = lightmap_image + + # Set the texture node as active for baking + material.node_tree.nodes.active = texture_node + + def setup_bake_settings(self): + """ Set up bake settings for diffuse lightmap baking. """ + bpy.context.scene.render.engine = 'CYCLES' + bpy.context.scene.cycles.samples = 128 + bpy.context.scene.cycles.use_denoising = False + + bpy.context.scene.cycles.bake_type = 'DIFFUSE' + bpy.context.scene.render.bake.use_pass_direct = True + bpy.context.scene.render.bake.use_pass_indirect = True + bpy.context.scene.render.bake.use_pass_color = False + + bpy.context.scene.render.bake.use_selected_to_active = False + + def perform_bake(self): + """ Perform the actual lightmap bake. """ + bpy.ops.object.bake(type='DIFFUSE') + + def setup_compositor(self): + """ Set up compositor nodes to denoise and save as HDR. """ + bpy.context.scene.use_nodes = True + tree = bpy.context.scene.node_tree + tree.nodes.clear() + + render_layer_node = tree.nodes.new('CompositorNodeRLayers') + denoise_node = tree.nodes.new('CompositorNodeDenoise') + file_output_node = tree.nodes.new('CompositorNodeOutputFile') + + file_output_node.format.file_format = 'HDR' + file_output_node.base_path = "//" + file_output_node.file_slots[0].path = "LightmapResult" + + tree.links.new(render_layer_node.outputs['Image'], denoise_node.inputs['Image']) + tree.links.new(denoise_node.outputs['Image'], file_output_node.inputs['Image']) + + def render_and_save(self): + """ Render and save the denoised lightmap as an HDR. """ + bpy.ops.render.render(use_viewport=True, write_still=True) + + def modal(self, context, event): + if event.type in {'RIGHTMOUSE', 'ESC'}: + self.cancel(context) + return {'CANCELLED'} + + if event.type == 'TIMER': + result = next(self.instance) + if result == -1: + self.cancel(context) + return {'CANCELLED'} + if result == 0: + self.finish(context) + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + def bake(self, context): + yield 1 + yield 0 + + return {""} + + scene = context.scene + lightmapper_props = scene.lightmapper_properties + width = lightmapper_props.lightmap_width + height = lightmapper_props.lightmap_height + path = lightmapper_props.export_path + bake_target = lightmapper_props.bake_target + + # 1. Set up and validate the bake. + bake_name = self.get_bake_name(bake_target) + bake_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] + self.check_mesh_objects(context, bake_objects) + self.correct_mesh_objects(context, bake_objects) + + # 2. Combine all selected meshes into 1 object for faster baking. + combined_object = self.combine_objects(context) + + # 3. Create a new lightmap image for baking. + lightmap_image = self.create_lightmap_image(width, height) + self.assign_lightmap_image(context, combined_object, lightmap_image) + + # 4. Start the initial bake. + self.setup_bake_settings() + + # Start the timer + wm = context.window_manager + self._timer = wm.event_timer_add(0.1, window=context.window) + wm.modal_handler_add(self) + + self.perform_bake() + + return {'RUNNING_MODAL'} + + def execute(self, context): + self.instance = self.bake(context) + wm = context.window_manager + self._timer = wm.event_timer_add(0.5, window=context.window) + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def cancel(self, context): + # Stop timer on cancel + wm = context.window_manager + wm.event_timer_remove(self._timer) + return {'CANCELLED'} + + def finish(self, context): + if self.instance.gi_running: + self.instance.close() + wm = context.window_manager + if self._timer: + wm.event_timer_remove(self._timer) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label(text=f"Baking Progress: {self.progress}%") - self.report({'INFO'}, "Lightmap baking started.") - return {'FINISHED'} - for obj in context.selected_objects: - if obj.type == 'MESH': - # Set up lightmap UV if not present - if len(obj.data.uv_layers) < 2: - obj.data.uv_layers.new(name="Lightmap") - - # Set up image for baking - image = bpy.data.images.new(name=f"{obj.name}_Lightmap", width=1024, height=1024) - - # Set up material for baking - material = obj.active_material - if not material: - material = bpy.data.materials.new(name=f"{obj.name}_Lightmap_Material") - obj.data.materials.append(material) - - # Set up node for baking - material.use_nodes = True - node_tree = material.node_tree - texture_node = node_tree.nodes.new('ShaderNodeTexImage') - texture_node.image = image - - # Perform bake - bpy.ops.object.bake(type='COMBINED') - - self.report({'INFO'}, "Lightmap baking completed") - return {'FINISHED'} from bpy.utils import register_class, unregister_class from .lightmapper_properties import LIGHTMAPPER_PT_properties diff --git a/lightmapper/lightmapper_panel.py b/lightmapper/lightmapper_panel.py index 3ab9ee1..6d9b784 100644 --- a/lightmapper/lightmapper_panel.py +++ b/lightmapper/lightmapper_panel.py @@ -23,8 +23,11 @@ def draw(self, context): layout.operator("lightmapper.create_lightmap_uv") - layout.prop(scene.lightmapper_properties, "lightmap_resolution") + layout.prop(scene.lightmapper_properties, "lightmap_width") + layout.prop(scene.lightmapper_properties, "lightmap_height") layout.prop(scene.lightmapper_properties, "export_path") + layout.prop(scene.lightmapper_properties, "bake_target", expand=True) + layout.operator("lightmapper.bake_lightmap") def register(): diff --git a/lightmapper/lightmapper_properties.py b/lightmapper/lightmapper_properties.py index 491ab7c..21f36ed 100644 --- a/lightmapper/lightmapper_properties.py +++ b/lightmapper/lightmapper_properties.py @@ -2,17 +2,20 @@ from bpy.props import EnumProperty, StringProperty # type: ignore class LIGHTMAPPER_PT_properties(bpy.types.PropertyGroup): - lightmap_resolution: EnumProperty( - name="Lightmap Resolution", - description="Choose the resolution for the lightmap", - items=[ - ('512', "512", "512x512 pixels"), - ('1024', "1k", "1024x1024 pixels"), - ('2048', "2k", "2048x2048 pixels"), - ('4096', "4k", "4096x4096 pixels"), - ('8192', "8k", "8192x8192 pixels"), - ], - default='2048' + lightmap_width: bpy.props.IntProperty( + name="Lightmap Width", + description="Width of the lightmap in pixels", + default=1024, + min=4, + max=8192 + ) # type: ignore + + lightmap_height: bpy.props.IntProperty( + name="Lightmap Height", + description="Height of the lightmap in pixels", + default=1024, + min=4, + max=8192 ) # type: ignore export_path: StringProperty( @@ -22,6 +25,18 @@ class LIGHTMAPPER_PT_properties(bpy.types.PropertyGroup): maxlen=1024, subtype='DIR_PATH' ) # type: ignore + + bake_target: EnumProperty( + name="Bake Target", + description="Choose the bake target", + items=[ + ('ACTIVE_OBJECT', "Active Object", "Bake the active object"), + ('COLLECTION', "Collection", "Bake the collection"), + ], + default='ACTIVE_OBJECT' + ) # type: ignore + + def register(): print("Registering lightmapper_properties")