From 6ab188a1ec9c870a1c32553275d680c8f797e511 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 7 Feb 2026 15:46:43 -0500 Subject: [PATCH] Add option to duplicate object before assigning material --- quickbake/op.py | 167 +++++++++++++++++++++++----------------- quickbake/panel.py | 2 +- quickbake/properties.py | 37 ++++++++- 3 files changed, 129 insertions(+), 77 deletions(-) diff --git a/quickbake/op.py b/quickbake/op.py index cf5d68e..8489fc9 100644 --- a/quickbake/op.py +++ b/quickbake/op.py @@ -1,11 +1,7 @@ -# import os -import typing - import bpy from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -if typing.TYPE_CHECKING: - from .properties import QuickBakeToolPropertyGroup +from .properties import MaterialMode, QuickBakeToolPropertyGroup class RENDER_OT_bake(bpy.types.Operator): @@ -81,7 +77,7 @@ def execute(self, context: bpy.types.Context): props = scene.QuickBakeToolPropertyGroup # type: ignore # layer name : is data - passes = [] + passes: list[tuple[str, bool]] = [] if props.diffuse_enabled: passes.append(("DIFFUSE", False)) if props.roughness_enabled: @@ -151,76 +147,26 @@ def execute(self, context: bpy.types.Context): 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 + # Only create images + if props.mat_mode == MaterialMode.IMAGES: + return {"FINISHED"} - # 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 + mat = self.create_material(props, uv_layer, passes, images) - # Link uv coordinates to mapping node - links.new(uv_node.outputs["UV"], mapping_node.inputs["Vector"]) + # Duplicate object and assign material to new + if props.mat_mode == MaterialMode.COPY: + bpy.ops.object.duplicate() + obj.hide_set(True) + # Get new object + obj = context.active_object - for layer, _ in passes: - y = 0 - if layer in self.input_order: - y = (self.input_order.index(layer) - 1) * -300 + # Keeping type hints happy + assert obj is not None, "Object is None" + assert isinstance(obj.data, bpy.types.Mesh), "Object is not a mesh" - 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: + # Assign or Copy + if props.mat_mode != MaterialMode.CREATE: + obj.data.materials.clear() obj.active_material = mat return {"FINISHED"} @@ -294,3 +240,80 @@ def cleanup_image_nodes(self, mesh: bpy.types.Mesh): node = mat.node_tree.get(node_name) if node is not None: mat.node_tree.nodes.remove(node) + + def create_material( + self, + props: QuickBakeToolPropertyGroup, + uv_layer: bpy.types.MeshUVLoopLayer, + passes: list[tuple[str, bool]], + images: dict, + ): + # 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] + ) + + return mat diff --git a/quickbake/panel.py b/quickbake/panel.py index d143d2e..aef69ca 100644 --- a/quickbake/panel.py +++ b/quickbake/panel.py @@ -29,7 +29,7 @@ def draw(self, context): layout.prop(props, "bake_name") layout.prop(props, "bake_size") - layout.prop(props, "use_mat") + layout.prop(props, "mat_mode") layout.prop(props, "save_img") row = layout.row() diff --git a/quickbake/properties.py b/quickbake/properties.py index 5b98007..2abe364 100644 --- a/quickbake/properties.py +++ b/quickbake/properties.py @@ -4,6 +4,13 @@ from enum import StrEnum +class MaterialMode(StrEnum): + IMAGES = "IMAGES" + CREATE = "CREATE" + ASSIGN = "ASSIGN" + COPY = "COPY" + + class QuickBakeToolPropertyGroup(bpy.types.PropertyGroup): # Bake @@ -21,10 +28,32 @@ class QuickBakeToolPropertyGroup(bpy.types.PropertyGroup): step=1024, # not yet implemented ) - use_mat: bpy.props.BoolProperty( - name="Assign Material", - description="Assign new material with baked textures to the selected object", - default=True, + mat_mode: bpy.props.EnumProperty( + name="Mode", + description="What to do with images after baking", + items=[ + ( + MaterialMode.IMAGES, + "Image Only", + "Only generate images", + ), + ( + MaterialMode.CREATE, + "Create Material", + "Create a new material with the baked images", + ), + ( + MaterialMode.ASSIGN, + "Assign material", + "Assign the material to active object", + ), + ( + MaterialMode.COPY, + "Copy Object", + "Make a copy of the object with baked material assigned", + ), + ], + default="ASSIGN", ) save_img: bpy.props.BoolProperty(