diff --git a/Makefile b/Makefile index 275e023..fe9a851 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ install: cp -r quickbake/* $(INSTALL_DIR)/ run: install - blender + blender "example/Metal Box.blend" checks: lint test diff --git a/pyproject.toml b/pyproject.toml index 05538f2..132a349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,9 @@ [project] name = "quickbake" -version = "0.1.0" -description = "Fast baking for blender" -readme = "README.md" -authors = [{ name = "Thomas Harrison", email = "theharrisoncrafter@gmail.com" }] +version = "0.9.0" +description = "Python environment for testing and linting plugin code" requires-python = ">=3.12" -dependencies = [] - -[dependency-groups] -dev = [ +dependencies = [ "coverage>=7.13.2", "fake-bpy-module>=20260128", "pytest>=9.0.2", @@ -18,10 +13,11 @@ dev = [ [tool.ruff.lint] extend-select = ["I"] -ignore = ["F401"] +ignore = ["F401", "I001"] [tool.ruff.format] -quote-style = "single" +# bpy specifies this https://docs.blender.org/api/current/info_best_practice.html#style-conventions +quote-style = "double" [tool.pytest.ini_options] pythonpath = ["src"] diff --git a/quickbake/__init__.py b/quickbake/__init__.py index a30dcbe..b976cf5 100644 --- a/quickbake/__init__.py +++ b/quickbake/__init__.py @@ -1,25 +1,25 @@ import bpy -from .op import QuickBake_OT_bake -from .panel import QuickBake_PT_main +from .op import RENDER_OT_bake +from .panel import RENDER_PT_main from .properties import QuickBakeToolPropertyGroup def register(): - bpy.utils.register_class(QuickBake_OT_bake) - bpy.utils.register_class(QuickBake_PT_main) + bpy.utils.register_class(RENDER_OT_bake) + bpy.utils.register_class(RENDER_PT_main) bpy.utils.register_class(QuickBakeToolPropertyGroup) - bpy.types.Scene.QuickBakeToolPropertyGroup = bpy.props.PointerProperty( + bpy.types.Scene.QuickBakeToolPropertyGroup = bpy.props.PointerProperty( # type: ignore type=QuickBakeToolPropertyGroup ) def unregister(): - bpy.utils.unregister_class(QuickBake_OT_bake) - bpy.utils.unregister_class(QuickBake_PT_main) + bpy.utils.unregister_class(RENDER_OT_bake) + bpy.utils.unregister_class(RENDER_PT_main) bpy.utils.unregister_class(QuickBakeToolPropertyGroup) - del bpy.types.Scene.QuickBakeToolPropertyGroup + del bpy.types.Scene.QuickBakeToolPropertyGroup # type: ignore -if __name__ == '__main__': +if __name__ == "__main__": register() diff --git a/quickbake/bake.py b/quickbake/bake.py deleted file mode 100644 index fb769be..0000000 --- a/quickbake/bake.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Baking helper functions.""" - -import logging - -import bpy - -_l = logging.getLogger(__name__) - - -def setup_bake_nodes(obj): - """Create material nodes required for baking.""" - _l.info('Creating bake nodes for object %s', obj.name) - - bake_nodes = [] - for mat in obj.data.materials: - _l.debug('Creating nodes for material %s', mat.name) - - mat.use_nodes = True - nodes = mat.node_tree.nodes - texture_node = nodes.new('ShaderNodeTexImage') - texture_node.name = 'Bake_node' - texture_node.select = True - nodes.active = texture_node - bake_nodes.append(texture_node) - - return bake_nodes - - -def cleanup_bake_nodes(obj): - """Remove material nodes created for baking by setup_bake_nodes.""" - _l.info('Cleaning up bake nodes for object %s', obj.name) - - for mat in obj.data.materials: - _l.debug('Clean up nodes for material %s', mat.name) - - for n in mat.node_tree.nodes: - if n.name == 'Bake_node': - _l.debug('Remove bake node %s', n.name) - mat.node_tree.nodes.remove(n) - - -def setup_bake_uv(obj, name): - """Create a uv layer to unwrap obj for baking.""" - _l.info('Creating uv layer %s for baking', name) - - def unwrap_uv(obj, uv): - _l.info('Unwrapping object %s to layer %s', obj.name, uv.name) - - active_layer = None - for layer in obj.data.uv_layers: - if layer.active: - _l.debug('Found active layer %s', layer.name) - active_layer = layer - break - - uv.active = True - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.smart_project(island_margin=0.001) - bpy.ops.object.mode_set(mode='OBJECT') - uv.active = False - - if active_layer is not None: - _l.debug('Restoring active layer %s', active_layer.name) - active_layer.active = True # type: ignore - - bake_uv = obj.data.uv_layers.get(name) - if bake_uv is None: - bake_uv = obj.data.uv_layers.new(name=name) - unwrap_uv(obj, bake_uv) - - else: - _l.debug('Using existing uv layer') - - return bake_uv - - -def setup_bake_image( - obj, bake_nodes, bake_name, bake_size, pass_name, reuse_tex, is_data=False -): - _l.info('Creating image for baking object %s', obj.name) - - image_name = obj.name + '_' + bake_name + '_' + pass_name - _l.debug('Image name %s', image_name) - - img = bpy.data.images.get(image_name) - if img is None or not reuse_tex: - img = bpy.data.images.new(image_name, bake_size, bake_size, is_data=is_data) - - else: - _l.debug('Using existing image') - - for node in bake_nodes: - node.image = img - - return img diff --git a/quickbake/material.py b/quickbake/material.py deleted file mode 100644 index b37971c..0000000 --- a/quickbake/material.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Material helper functions.""" - -import logging - -import bpy -from bpy_extras.node_shader_utils import PrincipledBSDFWrapper - -_l = logging.getLogger(__name__) - - -def setup_bake_material( - obj, name, bake_uv_name, diffuse=None, roughness=None, normal=None -): - _l.info('Creating material %s for object %s', name, obj.name) - - mat = bpy.data.materials.get(name) - if mat is not None: - _l.debug('Found existing material, skipping') - return mat - - mat = bpy.data.materials.new(name=name) - mat.use_nodes = True - obj.data.materials.append(mat) - - principled_mat = PrincipledBSDFWrapper(mat, is_readonly=False) - principled_mat.roughness = 1.0 - - principled_node = principled_mat.node_principled_bsdf - - nodes = mat.node_tree.nodes - links = mat.node_tree.links - - uv_node = nodes.new(type='ShaderNodeUVMap') - uv_node.uv_map = bake_uv_name - uv_node.location.x -= 1000 - # uv_node.location.y += 300 - - mapping_node = nodes.new(type='ShaderNodeMapping') - mapping_node.location.x -= 800 - # mapping_node.location.y += 300 - links.new(uv_node.outputs['UV'], mapping_node.inputs['Vector']) - - def make_tex_node(img, y): - tex_node = nodes.new(type='ShaderNodeTexImage') - tex_node.image = img - tex_node.location.x -= 500 - tex_node.location.y += y - - links.new(mapping_node.outputs['Vector'], tex_node.inputs['Vector']) - - # TODO: color space if not set by default - # tex_node.image.colorspace_settings.name = '...' - - return tex_node - - if diffuse is not None: - diff_node = make_tex_node(diffuse, 400) - links.new(diff_node.outputs['Color'], principled_node.inputs['Base Color']) - - if roughness is not None: - rough_node = make_tex_node(roughness, 100) - links.new(rough_node.outputs['Color'], principled_node.inputs['Roughness']) - - if normal is not None: - norm_node = make_tex_node(normal, -200) - norm_map_node = nodes.new(type='ShaderNodeNormalMap') - norm_map_node.location.x -= 200 - norm_map_node.location.y -= 200 - links.new(norm_node.outputs['Color'], norm_map_node.inputs['Color']) - links.new(norm_map_node.outputs['Normal'], principled_node.inputs['Normal']) - - return mat diff --git a/quickbake/op.py b/quickbake/op.py index b89dbda..cf5d68e 100644 --- a/quickbake/op.py +++ b/quickbake/op.py @@ -1,153 +1,296 @@ # import os -import logging +import typing import bpy +from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -from .bake import cleanup_bake_nodes, setup_bake_image, setup_bake_nodes, setup_bake_uv -from .material import setup_bake_material +if typing.TYPE_CHECKING: + from .properties import QuickBakeToolPropertyGroup -_l = logging.getLogger(__name__) - -class QuickBake_OT_bake(bpy.types.Operator): +class RENDER_OT_bake(bpy.types.Operator): """Do the bake.""" - bl_idname = 'render.quickbake_bake' - bl_label = 'Bake' - bl_options = {'REGISTER', 'UNDO'} - - # material: + # Blender fields + + bl_idname = "render.quickbake_bake" + bl_label = "Bake" + bl_options = {"REGISTER", "UNDO"} + + input_order = [ + "DIFFUSE", + "ROUGHNESS", + "NORMAL", + "GLOSSY", + "TRANSMISSION", + "EMIT", + "AO", + "SHADOW", + "ENVIRONMENT", + "POSITION", + "UV", + ] + + layer_input_map = { + "DIFFUSE": "Base Color", + "ROUGHNESS": "Roughness", + "NORMAL": "Normal", + # "GLOSSY": "", + "TRANSMISSION": "Transmission Weight", + "EMIT": "Emission Color", + # "AO": "", + # "SHADOW": "", + # "ENVIRONMENT": "", + # "POSITION": "", + # "UV": "", + } @classmethod def poll(cls, context): - obj: bpy.types.Object = context.active_object # type: ignore - return obj is not None and obj.type == 'MESH' + """Disable baking until a mesh object is selected.""" + obj = context.active_object + return obj is not None and obj.type == "MESH" - def create_material( - self, obj, name, uv_name, diffuse=None, roughness=None, normal=None - ): - _l.info('Creating bake material %s for object %s', name, obj.name) + def execute(self, context: bpy.types.Context): + # Keeping type hints happy, should not be possible + scene = context.scene + assert scene is not None, "Context must have a scene, got None" - mat = bpy.data.materials.get(name) - if mat is not None: - _l.debug('Material already exists, skipping') - self.report({'INFO'}, 'Material already exists, skipping') - return mat + # Make sure cycles is the current render engine + if scene.render.engine != "CYCLES": + scene.render.engine = "CYCLES" # type: ignore + self.report({"WARNING"}, "Changed render engine to Cycles") - mat = setup_bake_material(obj, name, uv_name, diffuse, roughness, normal) - return mat + scene.render.use_lock_interface = True - def execute(self, context): + # Get the object to bake obj = context.active_object + # This should be enforces by cls.poll() but is here to be sure if obj is None: - self.report({'ERROR'}, 'No active object') - return {'CANCELLED'} + self.report({"ERROR"}, "No active object") + return {"CANCELLED"} # canceled because nothing was altered / needs undo - if obj.type != 'MESH': - self.report({'ERROR'}, 'Active object must be a mesh') - return {'CANCELLED'} + # This should be enforces by cls.poll() but is here to be sure + if obj.type != "MESH": + self.report({"ERROR"}, "Active object must be a mesh") + return {"CANCELLED"} # canceled because nothing was altered / needs undo - props = context.scene.QuickBakeToolPropertyGroup - - bake_nodes = setup_bake_nodes(obj) - # bake_uv = setup_bake_uv(obj, props.bake_uv) - setup_bake_uv(obj, props.bake_uv) + # Setup passes for each enabled layer + props: QuickBakeToolPropertyGroup + props = scene.QuickBakeToolPropertyGroup # type: ignore + # layer name : is data passes = [] if props.diffuse_enabled: - passes.append('DIFFUSE') - if props.normal_enabled: - passes.append('NORMAL') + passes.append(("DIFFUSE", False)) if props.roughness_enabled: - passes.append('ROUGHNESS') + passes.append(("ROUGHNESS", False)) + if props.normal_enabled: + passes.append(("NORMAL", True)) + if props.glossy_enabled: + passes.append(("GLOSSY", False)) + if props.transmission_enabled: + passes.append(("TRANSMISSION", False)) + if props.emit_enabled: + passes.append(("EMIT", False)) if props.ao_enabled: - passes.append('AO') + passes.append(("AO", False)) if props.shadow_enabled: - passes.append('SHADOW') + passes.append(("SHADOW", False)) + if props.environment_enabled: + passes.append(("ENVIRONMENT", False)) if props.position_enabled: - passes.append('POSITION') + passes.append(("POSITION", True)) if props.uv_enabled: - passes.append('UV') - if props.emit_enabled: - passes.append('EMIT') - if props.environment_enabled: - passes.append('ENVIRONMENT') - if props.glossy_enabled: - passes.append('GLOSSY') - if props.transmission_enabled: - passes.append('TRANSMISSION') - - _l.debug('Enabled bake passes: %s', repr(passes)) - - img_cache = {} - - for pass_type in passes: - _l.info('Baking pass %s', pass_type) - - img = setup_bake_image( - obj, - bake_nodes, - props.bake_name, - props.bake_size, - pass_type.lower(), - props.reuse_tex, - pass_type == 'NORMAL', - ) - - img_cache[pass_type] = img - - self.report({'INFO'}, 'Baking pass %s' % pass_type) - - _l.debug('Making object %s active', obj.name) - bpy.context.view_layer.objects.active = obj - - save_mode = 'INTERNAL' - # filepath = '' - - # if props.save_img: - # _l.debug('Saving image externally') - - # img_base_path = bpy.path.abspath(props.image_path) - - # save_mode = 'EXTERNAL' - # filepath = os.path.join(img_base_path, img.name + '.png') - # _l.debug('Filepath %s', filepath) - - self.report({'INFO'}, 'Save mode %s' % save_mode) + passes.append(("UV", True)) + + # Keeping type hints happy + assert isinstance(obj.data, bpy.types.Mesh), "Object is not a mesh" + mesh = obj.data + + uv_layer = self.unwrap_object(mesh) + bake_nodes = self.create_image_nodes(mesh) + images = {} + + for layer, is_data in passes: + self.report({"INFO"}, f"Starting layer {layer}") + + image_name = f"{props.bake_name}_{layer.lower()}" + + # Create image or use existing + img = bpy.data.images.get(image_name) + if img is None: + img = bpy.data.images.new( + image_name, props.bake_size, props.bake_size, is_data=is_data + ) + images[layer] = img + + # Assign image to bake node in all materials + for mat, texture_node in bake_nodes: + # TODO type ignore if it works + texture_node.image = img # type: ignore + texture_node.select = True + mat.node_tree.nodes.active = texture_node # type: ignore + # nodes.active = texture_node # TODO per material + + filepath = "" + save_mode = "INTERNAL" + if props.save_img: + filepath = f"{props.save_path}/{props.bake_name}_{layer}" + save_mode = "EXTERNAL" bpy.ops.object.bake( - type=pass_type, - pass_filter={'COLOR'}, - uv_layer='bake_uv', + type=layer, # type: ignore + pass_filter={"COLOR"}, # TODO change this for other textures + uv_layer=uv_layer.name, use_clear=True, - # save_mode='INTERNAL', - # save_mode=save_mode, - # filepath=filepath, - ) - - # if props.save_img: - # _l.debug('Saving image externally %s', img.name) - - # # filepath = os.path.join(props.image_path, img.name + '.png') - # img.filepath = filepath - # img.file_format = 'PNG' - # # img.save_render(filepath=filepath) - # img.save() - - self.report({'INFO'}, 'Baking complete') - - if props.clean_up and not props.create_mat: - cleanup_bake_nodes(obj) - - if props.create_mat: - self.create_material( - obj, - props.mat_name, - props.bake_uv, - img_cache.get('DIFFUSE'), - img_cache.get('ROUGHNESS'), - img_cache.get('NORMAL'), + save_mode=save_mode, + filepath=filepath, ) - return {'FINISHED'} + self.cleanup_image_nodes(mesh) + + # Create Material + mat = bpy.data.materials.get(props.bake_name) + if mat is None: + mat = bpy.data.materials.new(props.bake_name) + mat.use_nodes = True + + # Get shader node (create if not exist) + principled_mat = PrincipledBSDFWrapper(mat, is_readonly=False) # pyright: ignore[reportCallIssue] + principled_node = principled_mat.node_principled_bsdf + + # Keeping type hints happy + assert mat.node_tree is not None + + shader_nodes = mat.node_tree.nodes + links = mat.node_tree.links + + # Texture coordinate node for uv map + uv_node = shader_nodes.get("Texture Coordinate") + if uv_node is None: + uv_node = shader_nodes.new(type="ShaderNodeUVMap") + uv_node.location.x = -1100 + uv_node.uv_map = uv_layer.name # type: ignore + + # Mapping node for position, scale, rotation + mapping_node = shader_nodes.get("Texture Coordinate") + if mapping_node is None: + mapping_node = shader_nodes.new(type="ShaderNodeMapping") + mapping_node.location.x = -900 + + # Link uv coordinates to mapping node + links.new(uv_node.outputs["UV"], mapping_node.inputs["Vector"]) + + for layer, _ in passes: + y = 0 + if layer in self.input_order: + y = (self.input_order.index(layer) - 1) * -300 + + tex_node = mat.node_tree.get(layer) + if tex_node is None: + tex_node = shader_nodes.new(type="ShaderNodeTexImage") + tex_node.location.x = -700 + tex_node.location.y = y + + tex_node.image = images[layer] # type: ignore + links.new(mapping_node.outputs["Vector"], tex_node.inputs["Vector"]) + + shader_input = self.layer_input_map.get(layer, "") + if shader_input: + if layer == "NORMAL": + normal_map_node = shader_nodes.get("Normal Map") + if normal_map_node is None: + normal_map_node = shader_nodes.new(type="ShaderNodeNormalMap") + normal_map_node.location.x = -400 + normal_map_node.location.y = y + + links.new( + tex_node.outputs["Color"], normal_map_node.inputs["Color"] + ) + links.new( + normal_map_node.outputs["Normal"], + principled_node.inputs[shader_input], + ) + + else: + links.new( + tex_node.outputs["Color"], principled_node.inputs[shader_input] + ) + + # Assign material to object + if props.use_mat: + obj.active_material = mat + + return {"FINISHED"} + + def unwrap_object(self, mesh: bpy.types.Mesh) -> bpy.types.MeshUVLoopLayer: + uv_name = "bake_uv" + + # Use existing or create new uv layer for baking + bake_uv = mesh.uv_layers.get(uv_name) + if bake_uv is None: + bake_uv = mesh.uv_layers.new(name=uv_name) + + # Store currently active layer + active_layer = None + for layer in mesh.uv_layers: + if layer.active: + active_layer = layer + break + + # Unwrap the object + bake_uv.active = True + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.uv.smart_project(island_margin=0.001) + bpy.ops.object.mode_set(mode="OBJECT") + bake_uv.active = False + + # Restore active layer + if active_layer is not None: + active_layer.active = True + + return bake_uv + + # TODO node is being created multiple times + def create_image_nodes( + self, mesh: bpy.types.Mesh + ) -> list[tuple[bpy.types.Material, bpy.types.Node]]: + node_name = "bake_image" + + null_count = 0 + image_nodes = [] + + for mat in mesh.materials: + if mat is None or mat.node_tree is None: + null_count += 1 + continue + + # Enable nodes if not already + mat.use_nodes = True + + texture_node = mat.node_tree.get(node_name) + if texture_node is None: + texture_node = mat.node_tree.nodes.new("ShaderNodeTexImage") + texture_node.name = node_name + + image_nodes.append((mat, texture_node)) + + # Notify user if any materials were unusable + if null_count > 0: + self.report({"WARNING"}, f"Mesh {mesh.name} has {null_count} null material") + + return image_nodes + + def cleanup_image_nodes(self, mesh: bpy.types.Mesh): + node_name = "bake_image" + + for mat in mesh.materials: + if mat is None or mat.node_tree is None: + continue + + node = mat.node_tree.get(node_name) + if node is not None: + mat.node_tree.nodes.remove(node) diff --git a/quickbake/panel.py b/quickbake/panel.py index 7aecc4d..f66705a 100644 --- a/quickbake/panel.py +++ b/quickbake/panel.py @@ -1,101 +1,53 @@ """QuickBake n Menu.""" import bpy +from .op import RENDER_OT_bake -from .op import QuickBake_OT_bake - -class QuickBake_PT_main(bpy.types.Panel): +class RENDER_PT_main(bpy.types.Panel): """Creates a Sub-Panel in the Property Area of the 3D View.""" - bl_label = 'Quick Bake' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = 'Tool' - bl_context = 'objectmode' + bl_label = "Quick Bake" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + # bl_category = 'Tool' + bl_category = "Item" # TODO revert this after testing + bl_context = "objectmode" def draw(self, context): """Override Panel draw method.""" layout = self.layout - assert layout is not None, 'Missing layout' - - assert context.scene is not None, 'Missing scene from context' + scene = context.scene - row = layout.row() - row.operator(QuickBake_OT_bake.bl_idname) - layout.separator() + # Make types happy + assert layout is not None, "Missing layout from parent Panel" + assert scene is not None, "Missing scene from context" - props = context.scene.QuickBakeToolPropertyGroup + props = scene.QuickBakeToolPropertyGroup # type: ignore - layout.label(text='Texture') - row = layout.row() - row.prop(props, 'bake_name') + # This is the bake button + layout.operator(RENDER_OT_bake.bl_idname) - row = layout.row() - row.prop(props, 'bake_uv') - - row = layout.row() - row.prop(props, 'bake_size') - - layout.separator() - layout.label(text='Material') - row = layout.row() - row.prop(props, 'create_mat') + layout.prop(props, "bake_name") + layout.prop(props, "bake_size") + layout.prop(props, "use_mat") + layout.prop(props, "save_img") row = layout.row() - row.prop(props, 'mat_name') + row.enabled = props.save_img + row.prop(props, "save_path") layout.separator() - layout.label(text='Options') - row = layout.row() - row.enabled = not props.create_mat - row.prop(props, 'reuse_tex') - - row = layout.row() - row.enabled = not props.create_mat - row.prop(props, 'clean_up') - - # layout.separator() - # layout.label(text='Output') - # row = layout.row() - # row.prop(props, "save_img") - - # row = layout.row() - # row.prop(props, "image_path") - - layout.separator() - layout.label(text='Layers') - row = layout.row() - row.prop(props, 'diffuse_enabled') - - row = layout.row() - row.prop(props, 'normal_enabled') - - row = layout.row() - row.prop(props, 'roughness_enabled') - - row = layout.row() - row.prop(props, 'ao_enabled') - - row = layout.row() - row.prop(props, 'shadow_enabled') - - row = layout.row() - row.prop(props, 'position_enabled') - - row = layout.row() - row.prop(props, 'uv_enabled') - - row = layout.row() - row.prop(props, 'emit_enabled') - - row = layout.row() - row.prop(props, 'environment_enabled') - - row = layout.row() - row.prop(props, 'glossy_enabled') - - row = layout.row() - row.prop(props, 'transmission_enabled') - - row = layout.row() + layout.label(text="Layers") + + layout.prop(props, "diffuse_enabled") + layout.prop(props, "roughness_enabled") + layout.prop(props, "normal_enabled") + layout.prop(props, "glossy_enabled") + layout.prop(props, "transmission_enabled") + layout.prop(props, "emit_enabled") + layout.prop(props, "ao_enabled") + layout.prop(props, "shadow_enabled") + layout.prop(props, "environment_enabled") + layout.prop(props, "position_enabled") + layout.prop(props, "uv_enabled") diff --git a/quickbake/properties.py b/quickbake/properties.py index 7585100..5b98007 100644 --- a/quickbake/properties.py +++ b/quickbake/properties.py @@ -1,101 +1,89 @@ # pyright: reportInvalidTypeForm=false import bpy +from enum import StrEnum + class QuickBakeToolPropertyGroup(bpy.types.PropertyGroup): - reuse_tex: bpy.props.BoolProperty( - name='Re-use Texture', - description='Use the texture from previous bakes', - default=True, - ) + # Bake - clean_up: bpy.props.BoolProperty( - name='Clean Up', description='Remove generated nodes after baking', default=True + bake_name: bpy.props.StringProperty( + name="Name", + description="Name used for the baked texture images", + default="Bake", ) - create_mat: bpy.props.BoolProperty( - name='Create Material', - description='Create a material after baking and assign it.', - default=True, + bake_size: bpy.props.IntProperty( + name="Size", + description="Resolution for the bake texture", + default=1024, + soft_min=1024, + step=1024, # not yet implemented ) - mat_name: bpy.props.StringProperty( - name='Name', - description='Name used to create a new material after baking', - default='BakeMaterial', + use_mat: bpy.props.BoolProperty( + name="Assign Material", + description="Assign new material with baked textures to the selected object", + default=True, ) save_img: bpy.props.BoolProperty( - name='Save Images', - description='Write images to file after baking', + name="Save Images", + description="Save images to a folder", default=False, ) - image_path: bpy.props.StringProperty( - name='Texture Path', - description='Directory for baking output', - default='', - subtype='DIR_PATH', + save_path: bpy.props.StringProperty( + name="Output Directory", + description="Directory for baking output", + default="", + subtype="DIR_PATH", ) - bake_name: bpy.props.StringProperty( - name='Name', - description='Name used fot the baked texture images', - default='BakeTexture', - ) - - bake_uv: bpy.props.StringProperty( - name='UV', description='Name used fot the uv bake layer', default='bake_uv' - ) - - bake_size: bpy.props.IntProperty( - name='Size', - description='Resolution for the bake texture', - default=1024, - soft_min=1024, - step=1024, - ) + # Layers diffuse_enabled: bpy.props.BoolProperty( - name='Diffuse', description='Bake the diffuse map', default=True + name="Diffuse", description="Bake the Diffuse map", default=True ) - normal_enabled: bpy.props.BoolProperty( - name='Normal', description='Bake the normal map', default=True + roughness_enabled: bpy.props.BoolProperty( + name="Roughness", description="Bake the Roughness map", default=True ) - roughness_enabled: bpy.props.BoolProperty( - name='Roughness', description='Bake the roughness map', default=True + normal_enabled: bpy.props.BoolProperty( + name="Normal", description="Bake the Normal map", default=True ) - ao_enabled: bpy.props.BoolProperty( - name='Ao', description='Bake the Ao map', default=False + glossy_enabled: bpy.props.BoolProperty( + name="Glossy", description="Bake the Glossy map", default=False ) - shadow_enabled: bpy.props.BoolProperty( - name='Shadow', description='Bake the Shadow map', default=False + transmission_enabled: bpy.props.BoolProperty( + name="Transmission", description="Bake the Transmission map", default=False ) - position_enabled: bpy.props.BoolProperty( - name='Position', description='Bake the Position map', default=False + emit_enabled: bpy.props.BoolProperty( + name="Emission", description="Bake the Emission map", default=False ) - uv_enabled: bpy.props.BoolProperty( - name='Uv', description='Bake the Uv map', default=False + ao_enabled: bpy.props.BoolProperty( + name="Ambient Occlusion", + description="Bake the Ambient Occlusion map", + default=False, ) - emit_enabled: bpy.props.BoolProperty( - name='Emit', description='Bake the Emit map', default=False + shadow_enabled: bpy.props.BoolProperty( + name="Shadow", description="Bake the Shadow map", default=False ) environment_enabled: bpy.props.BoolProperty( - name='Environment', description='Bake the Environment map', default=False + name="Environment", description="Bake the Environment map", default=False ) - glossy_enabled: bpy.props.BoolProperty( - name='Glossy', description='Bake the Glossy map', default=False + position_enabled: bpy.props.BoolProperty( + name="Position", description="Bake the Position map", default=False ) - transmission_enabled: bpy.props.BoolProperty( - name='Transmission', description='Bake the Transmission map', default=False + uv_enabled: bpy.props.BoolProperty( + name="UV", description="Bake the UV map", default=False ) diff --git a/quickbake/py.typed b/quickbake/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/quickbake/tmp.py b/quickbake/tmp.py deleted file mode 100644 index 178ed32..0000000 --- a/quickbake/tmp.py +++ /dev/null @@ -1,36 +0,0 @@ -import bpy - -obj = bpy.context.active_object -# You can choose your texture size (This will be the de bake - -assert obj is not None -assert obj.type == 'MESH' - -image_name = obj.name + '_BakedTexture' -img = bpy.data.images.get(image_name) -if img is None: - print('Creating new image') - img = bpy.data.images.new(image_name, 1024, 1024) -else: - print('Using existing image') - -# Due to the presence of any multiple materials, it seems necessary to iterate on all the materials, and assign them a node + the image to bake. -for mat in obj.data.materials: - mat.use_nodes = True # Here it is assumed that the materials have been created with nodes, otherwise it would not be possible to assign a node for the Bake, so this step is a bit useless - nodes = mat.node_tree.nodes - texture_node = nodes.new('ShaderNodeTexImage') - texture_node.name = 'Bake_node' - texture_node.select = True - nodes.active = texture_node - texture_node.image = img # Assign the image to the node - -bpy.context.view_layer.objects.active = obj -bpy.ops.object.bake(type='DIFFUSE', save_mode='EXTERNAL') - -img.save_render(filepath='C:\\TEMP\\baked.png') - -# In the last step, we are going to delete the nodes we created earlier -for mat in obj.data.materials: - for n in mat.node_tree.nodes: - if n.name == 'Bake_node': - mat.node_tree.nodes.remove(n) diff --git a/src/quickbake/bake.py b/src/quickbake/bake.py deleted file mode 100644 index fb769be..0000000 --- a/src/quickbake/bake.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Baking helper functions.""" - -import logging - -import bpy - -_l = logging.getLogger(__name__) - - -def setup_bake_nodes(obj): - """Create material nodes required for baking.""" - _l.info('Creating bake nodes for object %s', obj.name) - - bake_nodes = [] - for mat in obj.data.materials: - _l.debug('Creating nodes for material %s', mat.name) - - mat.use_nodes = True - nodes = mat.node_tree.nodes - texture_node = nodes.new('ShaderNodeTexImage') - texture_node.name = 'Bake_node' - texture_node.select = True - nodes.active = texture_node - bake_nodes.append(texture_node) - - return bake_nodes - - -def cleanup_bake_nodes(obj): - """Remove material nodes created for baking by setup_bake_nodes.""" - _l.info('Cleaning up bake nodes for object %s', obj.name) - - for mat in obj.data.materials: - _l.debug('Clean up nodes for material %s', mat.name) - - for n in mat.node_tree.nodes: - if n.name == 'Bake_node': - _l.debug('Remove bake node %s', n.name) - mat.node_tree.nodes.remove(n) - - -def setup_bake_uv(obj, name): - """Create a uv layer to unwrap obj for baking.""" - _l.info('Creating uv layer %s for baking', name) - - def unwrap_uv(obj, uv): - _l.info('Unwrapping object %s to layer %s', obj.name, uv.name) - - active_layer = None - for layer in obj.data.uv_layers: - if layer.active: - _l.debug('Found active layer %s', layer.name) - active_layer = layer - break - - uv.active = True - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.smart_project(island_margin=0.001) - bpy.ops.object.mode_set(mode='OBJECT') - uv.active = False - - if active_layer is not None: - _l.debug('Restoring active layer %s', active_layer.name) - active_layer.active = True # type: ignore - - bake_uv = obj.data.uv_layers.get(name) - if bake_uv is None: - bake_uv = obj.data.uv_layers.new(name=name) - unwrap_uv(obj, bake_uv) - - else: - _l.debug('Using existing uv layer') - - return bake_uv - - -def setup_bake_image( - obj, bake_nodes, bake_name, bake_size, pass_name, reuse_tex, is_data=False -): - _l.info('Creating image for baking object %s', obj.name) - - image_name = obj.name + '_' + bake_name + '_' + pass_name - _l.debug('Image name %s', image_name) - - img = bpy.data.images.get(image_name) - if img is None or not reuse_tex: - img = bpy.data.images.new(image_name, bake_size, bake_size, is_data=is_data) - - else: - _l.debug('Using existing image') - - for node in bake_nodes: - node.image = img - - return img diff --git a/src/quickbake/blender_manifest.toml b/src/quickbake/blender_manifest.toml deleted file mode 100644 index b89e851..0000000 --- a/src/quickbake/blender_manifest.toml +++ /dev/null @@ -1,68 +0,0 @@ -schema_version = "1.0.0" - -# Example of manifest file for a Blender extension -# Change the values according to your extension -id = "quickbake" -version = "0.0.0" -name = "Quick Bake" -tagline = "Fast baking for blender" -maintainer = "Thomas Harrison " -type = "add-on" - -# TODO replace with gh pages link -# Optional: link to documentation, support, source files, etc -website = "https://github.com/automas-dev/quickbake/" - -# Optional: tag list defined by Blender and server, see: -# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html -tags = ["3D View", "Bake"] - -blender_version_min = "2.28.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-3.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] -files = "Export baked texture images to disk" -clipboard = "Copy and paste bone transforms" - -# Optional: advanced build settings. -# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build -[build] -# These are the default build excluded patterns. -# You only need to edit them if you want different options. -paths_exclude_pattern = ["__pycache__/", "/.git/", "/*.zip"] diff --git a/src/quickbake/material.py b/src/quickbake/material.py deleted file mode 100644 index b37971c..0000000 --- a/src/quickbake/material.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Material helper functions.""" - -import logging - -import bpy -from bpy_extras.node_shader_utils import PrincipledBSDFWrapper - -_l = logging.getLogger(__name__) - - -def setup_bake_material( - obj, name, bake_uv_name, diffuse=None, roughness=None, normal=None -): - _l.info('Creating material %s for object %s', name, obj.name) - - mat = bpy.data.materials.get(name) - if mat is not None: - _l.debug('Found existing material, skipping') - return mat - - mat = bpy.data.materials.new(name=name) - mat.use_nodes = True - obj.data.materials.append(mat) - - principled_mat = PrincipledBSDFWrapper(mat, is_readonly=False) - principled_mat.roughness = 1.0 - - principled_node = principled_mat.node_principled_bsdf - - nodes = mat.node_tree.nodes - links = mat.node_tree.links - - uv_node = nodes.new(type='ShaderNodeUVMap') - uv_node.uv_map = bake_uv_name - uv_node.location.x -= 1000 - # uv_node.location.y += 300 - - mapping_node = nodes.new(type='ShaderNodeMapping') - mapping_node.location.x -= 800 - # mapping_node.location.y += 300 - links.new(uv_node.outputs['UV'], mapping_node.inputs['Vector']) - - def make_tex_node(img, y): - tex_node = nodes.new(type='ShaderNodeTexImage') - tex_node.image = img - tex_node.location.x -= 500 - tex_node.location.y += y - - links.new(mapping_node.outputs['Vector'], tex_node.inputs['Vector']) - - # TODO: color space if not set by default - # tex_node.image.colorspace_settings.name = '...' - - return tex_node - - if diffuse is not None: - diff_node = make_tex_node(diffuse, 400) - links.new(diff_node.outputs['Color'], principled_node.inputs['Base Color']) - - if roughness is not None: - rough_node = make_tex_node(roughness, 100) - links.new(rough_node.outputs['Color'], principled_node.inputs['Roughness']) - - if normal is not None: - norm_node = make_tex_node(normal, -200) - norm_map_node = nodes.new(type='ShaderNodeNormalMap') - norm_map_node.location.x -= 200 - norm_map_node.location.y -= 200 - links.new(norm_node.outputs['Color'], norm_map_node.inputs['Color']) - links.new(norm_map_node.outputs['Normal'], principled_node.inputs['Normal']) - - return mat diff --git a/src/quickbake/panel.py b/src/quickbake/panel.py deleted file mode 100644 index 7aecc4d..0000000 --- a/src/quickbake/panel.py +++ /dev/null @@ -1,101 +0,0 @@ -"""QuickBake n Menu.""" - -import bpy - -from .op import QuickBake_OT_bake - - -class QuickBake_PT_main(bpy.types.Panel): - """Creates a Sub-Panel in the Property Area of the 3D View.""" - - bl_label = 'Quick Bake' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = 'Tool' - bl_context = 'objectmode' - - def draw(self, context): - """Override Panel draw method.""" - layout = self.layout - assert layout is not None, 'Missing layout' - - assert context.scene is not None, 'Missing scene from context' - - row = layout.row() - row.operator(QuickBake_OT_bake.bl_idname) - layout.separator() - - props = context.scene.QuickBakeToolPropertyGroup - - layout.label(text='Texture') - row = layout.row() - row.prop(props, 'bake_name') - - row = layout.row() - row.prop(props, 'bake_uv') - - row = layout.row() - row.prop(props, 'bake_size') - - layout.separator() - layout.label(text='Material') - row = layout.row() - row.prop(props, 'create_mat') - - row = layout.row() - row.prop(props, 'mat_name') - - layout.separator() - layout.label(text='Options') - row = layout.row() - row.enabled = not props.create_mat - row.prop(props, 'reuse_tex') - - row = layout.row() - row.enabled = not props.create_mat - row.prop(props, 'clean_up') - - # layout.separator() - # layout.label(text='Output') - # row = layout.row() - # row.prop(props, "save_img") - - # row = layout.row() - # row.prop(props, "image_path") - - layout.separator() - layout.label(text='Layers') - row = layout.row() - row.prop(props, 'diffuse_enabled') - - row = layout.row() - row.prop(props, 'normal_enabled') - - row = layout.row() - row.prop(props, 'roughness_enabled') - - row = layout.row() - row.prop(props, 'ao_enabled') - - row = layout.row() - row.prop(props, 'shadow_enabled') - - row = layout.row() - row.prop(props, 'position_enabled') - - row = layout.row() - row.prop(props, 'uv_enabled') - - row = layout.row() - row.prop(props, 'emit_enabled') - - row = layout.row() - row.prop(props, 'environment_enabled') - - row = layout.row() - row.prop(props, 'glossy_enabled') - - row = layout.row() - row.prop(props, 'transmission_enabled') - - row = layout.row() diff --git a/src/quickbake/properties.py b/src/quickbake/properties.py deleted file mode 100644 index 7585100..0000000 --- a/src/quickbake/properties.py +++ /dev/null @@ -1,101 +0,0 @@ -# pyright: reportInvalidTypeForm=false -import bpy - - -class QuickBakeToolPropertyGroup(bpy.types.PropertyGroup): - reuse_tex: bpy.props.BoolProperty( - name='Re-use Texture', - description='Use the texture from previous bakes', - default=True, - ) - - clean_up: bpy.props.BoolProperty( - name='Clean Up', description='Remove generated nodes after baking', default=True - ) - - create_mat: bpy.props.BoolProperty( - name='Create Material', - description='Create a material after baking and assign it.', - default=True, - ) - - mat_name: bpy.props.StringProperty( - name='Name', - description='Name used to create a new material after baking', - default='BakeMaterial', - ) - - save_img: bpy.props.BoolProperty( - name='Save Images', - description='Write images to file after baking', - default=False, - ) - - image_path: bpy.props.StringProperty( - name='Texture Path', - description='Directory for baking output', - default='', - subtype='DIR_PATH', - ) - - bake_name: bpy.props.StringProperty( - name='Name', - description='Name used fot the baked texture images', - default='BakeTexture', - ) - - bake_uv: bpy.props.StringProperty( - name='UV', description='Name used fot the uv bake layer', default='bake_uv' - ) - - bake_size: bpy.props.IntProperty( - name='Size', - description='Resolution for the bake texture', - default=1024, - soft_min=1024, - step=1024, - ) - - diffuse_enabled: bpy.props.BoolProperty( - name='Diffuse', description='Bake the diffuse map', default=True - ) - - normal_enabled: bpy.props.BoolProperty( - name='Normal', description='Bake the normal map', default=True - ) - - roughness_enabled: bpy.props.BoolProperty( - name='Roughness', description='Bake the roughness map', default=True - ) - - ao_enabled: bpy.props.BoolProperty( - name='Ao', description='Bake the Ao map', default=False - ) - - shadow_enabled: bpy.props.BoolProperty( - name='Shadow', description='Bake the Shadow map', default=False - ) - - position_enabled: bpy.props.BoolProperty( - name='Position', description='Bake the Position map', default=False - ) - - uv_enabled: bpy.props.BoolProperty( - name='Uv', description='Bake the Uv map', default=False - ) - - emit_enabled: bpy.props.BoolProperty( - name='Emit', description='Bake the Emit map', default=False - ) - - environment_enabled: bpy.props.BoolProperty( - name='Environment', description='Bake the Environment map', default=False - ) - - glossy_enabled: bpy.props.BoolProperty( - name='Glossy', description='Bake the Glossy map', default=False - ) - - transmission_enabled: bpy.props.BoolProperty( - name='Transmission', description='Bake the Transmission map', default=False - ) diff --git a/src/quickbake/tmp.py b/src/quickbake/tmp.py deleted file mode 100644 index 178ed32..0000000 --- a/src/quickbake/tmp.py +++ /dev/null @@ -1,36 +0,0 @@ -import bpy - -obj = bpy.context.active_object -# You can choose your texture size (This will be the de bake - -assert obj is not None -assert obj.type == 'MESH' - -image_name = obj.name + '_BakedTexture' -img = bpy.data.images.get(image_name) -if img is None: - print('Creating new image') - img = bpy.data.images.new(image_name, 1024, 1024) -else: - print('Using existing image') - -# Due to the presence of any multiple materials, it seems necessary to iterate on all the materials, and assign them a node + the image to bake. -for mat in obj.data.materials: - mat.use_nodes = True # Here it is assumed that the materials have been created with nodes, otherwise it would not be possible to assign a node for the Bake, so this step is a bit useless - nodes = mat.node_tree.nodes - texture_node = nodes.new('ShaderNodeTexImage') - texture_node.name = 'Bake_node' - texture_node.select = True - nodes.active = texture_node - texture_node.image = img # Assign the image to the node - -bpy.context.view_layer.objects.active = obj -bpy.ops.object.bake(type='DIFFUSE', save_mode='EXTERNAL') - -img.save_render(filepath='C:\\TEMP\\baked.png') - -# In the last step, we are going to delete the nodes we created earlier -for mat in obj.data.materials: - for n in mat.node_tree.nodes: - if n.name == 'Bake_node': - mat.node_tree.nodes.remove(n) diff --git a/uv.lock b/uv.lock index 1334076..bafd353 100644 --- a/uv.lock +++ b/uv.lock @@ -162,11 +162,9 @@ wheels = [ [[package]] name = "quickbake" -version = "0.1.0" +version = "0.9.0" source = { virtual = "." } - -[package.dev-dependencies] -dev = [ +dependencies = [ { name = "coverage" }, { name = "fake-bpy-module" }, { name = "pytest" }, @@ -175,9 +173,7 @@ dev = [ ] [package.metadata] - -[package.metadata.requires-dev] -dev = [ +requires-dist = [ { name = "coverage", specifier = ">=7.13.2" }, { name = "fake-bpy-module", specifier = ">=20260128" }, { name = "pytest", specifier = ">=9.0.2" },