diff --git a/entry_lightmapper.py b/entry_lightmapper.py new file mode 100644 index 0000000..f83248c --- /dev/null +++ b/entry_lightmapper.py @@ -0,0 +1,20 @@ +import importlib +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_dir) +__package__ = os.path.basename(script_dir) + +import lightmapper.__init__ +import lightmapper.lightmapper_properties +import lightmapper.lightmapper_operators +import lightmapper.lightmapper_panel + +# TODO: Make this autoload everything. +importlib.reload(lightmapper.__init__) +importlib.reload(lightmapper.lightmapper_properties) +importlib.reload(lightmapper.lightmapper_operators) +importlib.reload(lightmapper.lightmapper_panel) + +lightmapper.__init__.register() diff --git a/lightmapper/__init__.py b/lightmapper/__init__.py new file mode 100644 index 0000000..0e8dd94 --- /dev/null +++ b/lightmapper/__init__.py @@ -0,0 +1,16 @@ +from . import lightmapper_operators +from . import lightmapper_panel +from . import lightmapper_properties + +print("Running lightmapper/__init__.py") + +def register(): + lightmapper_properties.register() + lightmapper_operators.register() + lightmapper_panel.register() + +def unregister(): + lightmapper_properties.unregister() + lightmapper_operators.unregister() + lightmapper_panel.unregister() + \ No newline at end of file diff --git a/lightmapper/blender_manifest.toml b/lightmapper/blender_manifest.toml new file mode 100644 index 0000000..e252db4 --- /dev/null +++ b/lightmapper/blender_manifest.toml @@ -0,0 +1,74 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "lightmapper" +version = "1.0.0" +name = "Lightmapper" +tagline = "Lightmapper Blender Addon" +maintainer = "Simon Nordon " +# Supported types: "add-on", "theme" +type = "add-on" + +# Optional link to documentation, support, source files, etc +# Optional link to documentation, support, source files, etc +# website = "https://extensions.blender.org/add-ons/my-example-package/" + +# Optional list defined by Blender and server, see: +# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = ["Bake"] + +blender_version_min = "4.2.0" +# # Optional: Blender version that the extension does not support, earlier versions are supported. +# # This can be omitted and defined later on the extensions platform if an issue is found. +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html +license = [ + "SPDX:GPL-2.0-or-later", +] +# Optional: required by some licenses. +# copyright = [ +# "2002-2024 Developer Name", +# "1998 Company Name", +# ] + +# Optional list of supported platforms. If omitted, the extension will be available in all operating systems. +# platforms = ["windows-x64", "macos-arm64", "linux-x64"] +# Other supported platforms: "windows-arm64", "macos-x64" + +# Optional: bundle 3rd party Python modules. +# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html +# wheels = [ +# "./wheels/hexdump-3.3-py3-none-any.whl", +# "./wheels/jsmin-3.0.1-py3-none-any.whl", +# ] + +# # Optional: add-ons can list which resources they will require: +# # * files (for access of any filesystem operations) +# # * network (for internet access) +# # * clipboard (to read and/or write the system clipboard) +# # * camera (to capture photos and videos) +# # * microphone (to capture audio) +# # +# # If using network, remember to also check `bpy.app.online_access` +# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +# # +# # For each permission it is important to also specify the reason why it is required. +# # Keep this a single short sentence without a period (.) at the end. +# # For longer explanations use the documentation or detail page. +# +# [permissions] +# network = "Need to sync motion-capture data to server" +# files = "Import/export FBX from/to disk" +# clipboard = "Copy and paste bone transforms" + +# Optional: build settings. +# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +# [build] +# paths_exclude_pattern = [ +# "__pycache__/", +# "/.git/", +# "/*.zip", +# ] \ No newline at end of file diff --git a/lightmapper/lightmapper_operators.py b/lightmapper/lightmapper_operators.py new file mode 100644 index 0000000..a24d6b4 --- /dev/null +++ b/lightmapper/lightmapper_operators.py @@ -0,0 +1,133 @@ +import bpy + +import bpy.utils + + + +class LIGHTMAPPER_OT_create_lightmap_uv(bpy.types.Operator): + bl_idname = "lightmapper.create_lightmap_uv" + bl_label = "Create Lightmap UV" + bl_description = "Create a second UV channel called 'Lightmap' and select it" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + 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 {'CANCELLED'} + + for obj in mesh_objects: + uv_layers = obj.data.uv_layers + if len(uv_layers) < 2: + uv_layer = uv_layers.new(name="Lightmap") + uv_layers.active = uv_layer + self.report({'INFO'}, f"Lightmap UV created for {obj.name}.") + else: + self.report({'INFO'}, f"{obj.name} already has a Lightmap UV.") + + # Perform lightmap unwrap + print(f"Performing lightmap unwrap for {obj.name}") + bpy.context.view_layer.objects.active = obj + bpy.ops.uv.lightmap_pack() + + return {'FINISHED'} + + + + +class LIGHTMAPPER_OT_bake_lightmap(bpy.types.Operator): + bl_idname = "lightmapper.bake_lightmap" + bl_label = "Bake Lightmap" + bl_description = "Bake lightmap for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def check_context(self, context): + """ Ensure the object is in a state that supports baking. """ + mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] + # Ensure we're in object mode. + if context.mode != 'OBJECT': + self.report({'ERROR'}, "Lightmap baking requires object mode.") + return False + + # Ensure we have at least one object selected. + if not mesh_objects: + self.report({'ERROR'}, "No mesh objects selected.") + return False + + # Ensure there are no disabled for rendering. + for obj in mesh_objects: + if obj.hide_render or obj.hide_viewport: + self.report({'ERROR'}, f"Object {obj.name} is disabled for rendering or hidden in viewport.") + return False + + # Ensure each mesh has 2 uv channels. + for obj in mesh_objects: + if len(obj.data.uv_layers) < 2: + self.report({'ERROR'}, f"Object {obj.name} does not have a Lightmap channel.") + return False + + return True + + def correct_context(self, context): + """ 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. + for obj in mesh_objects: + # If the lightmap is renderable, set it to the first UVMap. Otherwise respect user choice. + obj.data.uv_layers[0].active_render = True + # 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'} + + self.correct_context(context) + + 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 + +def register(): + print("Registering lightmapper_operators") + bpy.utils.register_class(LIGHTMAPPER_OT_create_lightmap_uv) + bpy.utils.register_class(LIGHTMAPPER_OT_bake_lightmap) + +def unregister(): + bpy.utils.unregister_class(LIGHTMAPPER_OT_create_lightmap_uv) + bpy.utils.unregister_class(LIGHTMAPPER_OT_bake_lightmap) + +if __name__ == "__main__": + register() diff --git a/lightmapper/lightmapper_panel.py b/lightmapper/lightmapper_panel.py new file mode 100644 index 0000000..3ab9ee1 --- /dev/null +++ b/lightmapper/lightmapper_panel.py @@ -0,0 +1,36 @@ +import bpy + +class LightmapperPanel(bpy.types.Panel): + bl_idname = "OBJECT_PT_lightmapper_panel" + bl_label = "Lightmapper Panel" + bl_description = "This is a lightmapper panel, for starting a new addon." + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Lightmapper" + bl_order = 0 + + def draw_header(self, context): + layout = self.layout + layout.label(text="", icon="OUTLINER_DATA_LIGHTPROBE") + + def draw(self, context): + layout = self.layout + scene = context.scene + lightmapper_props = scene.lightmapper_properties + + + + layout.operator("lightmapper.create_lightmap_uv") + + + layout.prop(scene.lightmapper_properties, "lightmap_resolution") + layout.prop(scene.lightmapper_properties, "export_path") + layout.operator("lightmapper.bake_lightmap") + +def register(): + bpy.utils.register_class(LightmapperPanel) + +def unregister(): + bpy.utils.unregister_class(LightmapperPanel) + + diff --git a/lightmapper/lightmapper_properties.py b/lightmapper/lightmapper_properties.py new file mode 100644 index 0000000..491ab7c --- /dev/null +++ b/lightmapper/lightmapper_properties.py @@ -0,0 +1,33 @@ +import bpy +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' + ) # type: ignore + + export_path: StringProperty( + name="Export Path", + description="Path to export the lightmap", + default="", + maxlen=1024, + subtype='DIR_PATH' + ) # type: ignore + +def register(): + print("Registering lightmapper_properties") + bpy.utils.register_class(LIGHTMAPPER_PT_properties) + bpy.types.Scene.lightmapper_properties = bpy.props.PointerProperty(type=LIGHTMAPPER_PT_properties) + +def unregister(): + del bpy.types.Scene.lightmapper_properties + bpy.utils.unregister_class(LIGHTMAPPER_PT_properties)