From 850443b9f5f13ca1ec8a67d296856e350c663f19 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sun, 1 Feb 2026 18:22:14 -0500 Subject: [PATCH 1/3] Move quickbake out of src --- .vscode/settings.json | 4 +- {src/quickbake => quickbake}/__init__.py | 0 quickbake/bake.py | 96 +++++++++++++++++++++ quickbake/blender_manifest.toml | 68 +++++++++++++++ quickbake/material.py | 72 ++++++++++++++++ {src/quickbake => quickbake}/op.py | 0 quickbake/panel.py | 101 +++++++++++++++++++++++ quickbake/properties.py | 101 +++++++++++++++++++++++ {src/quickbake => quickbake}/py.typed | 0 quickbake/tmp.py | 36 ++++++++ tests/test_hello_world.py | 12 --- 11 files changed, 476 insertions(+), 14 deletions(-) rename {src/quickbake => quickbake}/__init__.py (100%) create mode 100644 quickbake/bake.py create mode 100644 quickbake/blender_manifest.toml create mode 100644 quickbake/material.py rename {src/quickbake => quickbake}/op.py (100%) create mode 100644 quickbake/panel.py create mode 100644 quickbake/properties.py rename {src/quickbake => quickbake}/py.typed (100%) create mode 100644 quickbake/tmp.py diff --git a/.vscode/settings.json b/.vscode/settings.json index f59f91a..5ada81e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "python.testing.pytestArgs": [ "tests", - "--cov=src", - "--cov=branch" + "--cov", + "--cov-branch" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, diff --git a/src/quickbake/__init__.py b/quickbake/__init__.py similarity index 100% rename from src/quickbake/__init__.py rename to quickbake/__init__.py diff --git a/quickbake/bake.py b/quickbake/bake.py new file mode 100644 index 0000000..fb769be --- /dev/null +++ b/quickbake/bake.py @@ -0,0 +1,96 @@ +"""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/blender_manifest.toml b/quickbake/blender_manifest.toml new file mode 100644 index 0000000..b89e851 --- /dev/null +++ b/quickbake/blender_manifest.toml @@ -0,0 +1,68 @@ +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/quickbake/material.py b/quickbake/material.py new file mode 100644 index 0000000..b37971c --- /dev/null +++ b/quickbake/material.py @@ -0,0 +1,72 @@ +"""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/op.py b/quickbake/op.py similarity index 100% rename from src/quickbake/op.py rename to quickbake/op.py diff --git a/quickbake/panel.py b/quickbake/panel.py new file mode 100644 index 0000000..7aecc4d --- /dev/null +++ b/quickbake/panel.py @@ -0,0 +1,101 @@ +"""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/quickbake/properties.py b/quickbake/properties.py new file mode 100644 index 0000000..7585100 --- /dev/null +++ b/quickbake/properties.py @@ -0,0 +1,101 @@ +# 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/py.typed b/quickbake/py.typed similarity index 100% rename from src/quickbake/py.typed rename to quickbake/py.typed diff --git a/quickbake/tmp.py b/quickbake/tmp.py new file mode 100644 index 0000000..178ed32 --- /dev/null +++ b/quickbake/tmp.py @@ -0,0 +1,36 @@ +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/tests/test_hello_world.py b/tests/test_hello_world.py index 9145cee..4752ae6 100644 --- a/tests/test_hello_world.py +++ b/tests/test_hello_world.py @@ -4,15 +4,3 @@ def test_import(): import quickbake - - -# @patch('quickbake.bpy') -def test_version(): - import quickbake - - with open('pyproject.toml', 'rb') as f: - t = tomllib.load(f) - - expect_version = tuple(map(int, t['project']['version'].split('.'))) - - assert quickbake.version_tuple == expect_version From 4dcc0131a4ec56440412f5fea5179b40819f3d1c Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sun, 1 Feb 2026 18:26:54 -0500 Subject: [PATCH 2/3] Update ci and makefile --- .github/workflows/ci.yaml | 6 +++--- Makefile | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e59ed99..d8001a8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,19 +60,19 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - sed -i "s/^version = \"[^\"]\+\"$/version=\"${VERSION}\"/g" src/quickbake/blender_manifest.toml + sed -i "s/^version = \"[^\"]\+\"$/version=\"${VERSION}\"/g" quickbake/blender_manifest.toml - name: Create Archive env: VERSION: ${{ steps.version.outputs.version }} - working-directory: ./src/quickbake + working-directory: ./quickbake run: | zip quickbake-${VERSION}.zip * - name: Create Release uses: ncipollo/release-action@v1.14.0 with: - artifacts: src/quickbake/quickbake-${{ steps.version.outputs.version }}.zip + artifacts: quickbake/quickbake-${{ steps.version.outputs.version }}.zip makeLatest: true generateReleaseNotes: true tag: ${{ steps.version.outputs.version }} diff --git a/Makefile b/Makefile index 50d2b83..275e023 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,11 @@ setup: build_dev: @mkdir -p release/ - @cd src/quickbake && zip $(PWD)/release/$(ZIP_ARCHIVE_NAME) * + @cd quickbake/ && zip $(PWD)/release/$(ZIP_ARCHIVE_NAME) * install: rm -rf $(INSTALL_DIR)/* - cp -r src/quickbake/* $(INSTALL_DIR)/ + cp -r quickbake/* $(INSTALL_DIR)/ run: install blender From 4ef7d63f0473c905b491516d880e8839b38523e3 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sun, 1 Feb 2026 18:28:36 -0500 Subject: [PATCH 3/3] Remove package build --- pyproject.toml | 4 ---- uv.lock | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f13c3e9..05538f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,6 @@ authors = [{ name = "Thomas Harrison", email = "theharrisoncrafter@gmail.com" }] requires-python = ">=3.12" dependencies = [] -[build-system] -requires = ["uv_build>=0.9.27,<0.10.0"] -build-backend = "uv_build" - [dependency-groups] dev = [ "coverage>=7.13.2", diff --git a/uv.lock b/uv.lock index 1c2eba4..1334076 100644 --- a/uv.lock +++ b/uv.lock @@ -163,7 +163,7 @@ wheels = [ [[package]] name = "quickbake" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } [package.dev-dependencies] dev = [