├── Bake ├── __init__.py ├── bake_manager.py └── bake_utilities.py ├── Panels ├── __init__.py ├── panel_ui_list.py └── panel.py ├── Functions ├── __init__.py ├── basic_functions.py ├── constants.py ├── object_functions.py ├── material_functions.py ├── gui_functions.py ├── image_functions.py ├── visibility_functions.py └── node_functions.py ├── Keycodes ├── __init__.py └── key_codes.py ├── Operators ├── __init__.py ├── modal_wrap.py ├── bake_operator.py └── operators.py ├── Properties ├── __init__.py └── properties.py ├── CNAME ├── _config.yml ├── .gitignore ├── .vscode ├── .ropeproject │ ├── objectdb │ └── config.py └── settings.json ├── debug.log ├── README.md ├── todo.txt ├── Utilities └── clean_dupli_textures.py ├── LICENSE ├── __init__.py ├── .github └── workflows │ └── auto_deploy.yml └── auto_load.py /Bake/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Panels/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Keycodes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Properties/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | lorenz.wieseke.glbtexturetools -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | __pycache__/ 4 | glbtexturetools_updater -------------------------------------------------------------------------------- /.vscode/.ropeproject/objectdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LorenzWieseke/GLBTextureTools/HEAD/.vscode/.ropeproject/objectdb -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [1016/121205.994:ERROR:directory_reader_win.cc(43)] FindFirstFile: Das System kann den angegebenen Pfad nicht finden. (0x3) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Documentation can be found here and a help 2 | 3 | https://docs.google.com/document/d/1w1h3ySyZG4taG01RbbDsPODsUP06cRCggxQKCt32QTk/edit 4 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | use uv-packmaster if installed 2 | change camera size back to what is was 3 | reconstruct node tree in compositor 4 | pbr texture bake -> add same uv to bake plane to be able to bake procedural textures or ! add uv automaticly 5 | 6 | 7 | -------------------------------------------------------------------------------- /Functions/basic_functions.py: -------------------------------------------------------------------------------- 1 | def Diff(li1, li2): 2 | return (list(set(li1) - set(li2))) 3 | 4 | def Intersection(li1, li2): 5 | set1 = set(li1) 6 | set2 = set(li2) 7 | return list(set.intersection(set1,set2)) 8 | 9 | def flatten(t): 10 | return [item for sublist in t for item in sublist] 11 | 12 | def remove_duplicate(l): 13 | return list(dict.fromkeys(l)) -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.autoComplete.addBrackets": true, 3 | "blender.addon.reloadOnSave": true, 4 | "python.linting.pylintEnabled": true, 5 | "python.linting.enabled": true, 6 | "python.autoComplete.extraPaths": [ 7 | "c:/3D Animation/blender_autocomplete/2.82" 8 | ], 9 | "python.analysis.extraPaths": [ 10 | "c:/3D Animation/blender_autocomplete/2.82" 11 | ], 12 | "python.analysis.completeFunctionParens": true 13 | 14 | 15 | } -------------------------------------------------------------------------------- /Keycodes/key_codes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | addon_keymaps = [] 4 | 5 | 6 | # keymap 7 | def register(): 8 | 9 | kcfg = bpy.context.window_manager.keyconfigs.addon 10 | if kcfg: 11 | km = kcfg.keymaps.new(name='3D View', space_type='VIEW_3D') 12 | 13 | kmi = km.keymap_items.new("scene.node_to_texture_operator", 'B', 'PRESS', shift=True, ctrl=True) 14 | 15 | addon_keymaps.append((km, kmi)) 16 | def unregister(): 17 | 18 | for km, kmi in addon_keymaps: 19 | km.keymap_items.remove(kmi) 20 | addon_keymaps.clear() -------------------------------------------------------------------------------- /Operators/modal_wrap.py: -------------------------------------------------------------------------------- 1 | 2 | from bpy.ops import OBJECT_OT_bake as op 3 | 4 | def callback(ret): 5 | print('Callback triggered: {} !!'.format(ret)) 6 | 7 | 8 | def modal_wrap(modal_func, callback): 9 | def wrap(self, context, event): 10 | ret, = retset = modal_func(self, context, event) 11 | if ret in {'CANCELLED'}: # my plugin emits the CANCELED event on finish - yours might do FINISH or FINISHED, you might have to look it up in the source code, __init__.py , there look at the modal() function for things like return {'FINISHED'} or function calls that return things alike. 12 | print(f"{self.bl_idname} returned {ret}") 13 | callback(ret) 14 | return retset 15 | return wrap 16 | 17 | # op._modal_org = op.modal 18 | op.modal = modal_wrap(op.modal, callback) 19 | 20 | -------------------------------------------------------------------------------- /Utilities/clean_dupli_textures.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import functions 3 | from .. Functions import material_functions 4 | 5 | 6 | def Diff(li1, li2): 7 | return (list(set(li1) - set(li2))) 8 | 9 | 10 | def compareImages(images): 11 | firstImg = images[0] 12 | firstImgPixels = firstImg.pixels 13 | 14 | for img in images[1:]: 15 | currentImgPixels = img.pixels 16 | diff = Diff(firstImgPixels, currentImgPixels) 17 | if len(diff) == 0: 18 | print(firstImg.name + " and " + img.name + " map !") 19 | 20 | return compareImages(images[1:]) 21 | 22 | 23 | class CleanDupliTexturesOperator(bpy.types.Operator): 24 | """By checking the incoming links in the PBR Shader, new Textures are generated that will include all the node transformations.""" 25 | bl_idname = "object.clean_dupli_textures" 26 | bl_label = "Delete Texture Duplicates" 27 | 28 | def execute(self, context): 29 | 30 | # images = bpy.data.images 31 | # compareImages(images) 32 | material_functions.clean_empty_materials(self) 33 | 34 | return {'FINISHED'} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lorenz Wieseke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Functions/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import bpy 3 | class Node_Types: 4 | image_texture = 'TEX_IMAGE' 5 | pbr_node = 'BSDF_PRINCIPLED' 6 | mapping = 'MAPPING' 7 | normal_map = 'NORMAL_MAP' 8 | bump_map = 'BUMP' 9 | material_output = 'OUTPUT_MATERIAL' 10 | 11 | class Shader_Node_Types: 12 | emission = "ShaderNodeEmission" 13 | image_texture = "ShaderNodeTexImage" 14 | mapping = "ShaderNodeMapping" 15 | normal = "ShaderNodeNormalMap" 16 | ao = "ShaderNodeAmbientOcclusion" 17 | uv = "ShaderNodeUVMap" 18 | comp_image_node = 'CompositorNodeImage' 19 | mix ="ShaderNodeMixRGB" 20 | 21 | 22 | class Bake_Passes: 23 | pbr = ["EMISSION"] 24 | lightmap = ["NOISY", "NRM", "COLOR"] 25 | ao = ["AO","COLOR"] 26 | 27 | class Material_Suffix: 28 | bake_type_mat_suffix = { 29 | "pbr" : "_Bake", 30 | "ao" : "_AO", 31 | "lightmap" : "_AO" 32 | } 33 | class Path_List: 34 | def get_project_dir(): 35 | return os.path.dirname(bpy.data.filepath) 36 | 37 | def get_textures_dir(): 38 | return os.path.join(os.path.dirname(bpy.data.filepath),'textures','GLBTexTool') 39 | 40 | -------------------------------------------------------------------------------- /Functions/object_functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. Functions import node_functions 3 | 4 | def apply_transform_on_linked(): 5 | bpy.ops.object.select_linked(type='OBDATA') 6 | bpy.ops.object.make_single_user(type='SELECTED_OBJECTS', object=True, obdata=True, material=False, animation=False) 7 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 8 | bpy.ops.object.make_links_data(type='OBDATA') 9 | 10 | def select_object(self, obj): 11 | C = bpy.context 12 | O = bpy.ops 13 | try: 14 | O.object.select_all(action='DESELECT') 15 | C.view_layer.objects.active = obj 16 | obj.select_set(True) 17 | except: 18 | self.report({'INFO'}, "Object not in View Layer") 19 | 20 | def select_obj_by_mat(mat,self=None): 21 | D = bpy.data 22 | result = [] 23 | for obj in D.objects: 24 | if obj.type == "MESH": 25 | object_materials = [slot.material for slot in obj.material_slots] 26 | if mat in object_materials: 27 | result.append(obj) 28 | if (self): 29 | select_object(self, obj) 30 | 31 | return result 32 | 33 | # TODO - save objects in array, unlink objects, apply scale and link them back 34 | def apply_scale_on_multiuser(): 35 | O = bpy.ops 36 | O.object.transform_apply(location=False, rotation=False, scale=True) -------------------------------------------------------------------------------- /Functions/material_functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | def get_all_visible_materials(): 3 | objects=[ob for ob in bpy.context.view_layer.objects if ob.visible_get()] 4 | slot_array = [object.material_slots for object in objects] 5 | vis_mat = set() 6 | for slots in slot_array: 7 | for slot in slots: 8 | vis_mat.add(slot.material) 9 | 10 | # to remove None values in list 11 | vis_mat = list(filter(None, vis_mat)) 12 | return vis_mat 13 | 14 | def get_selected_materials(selected_objects): 15 | selected_materials = set() 16 | slots_array = [obj.material_slots for obj in selected_objects] 17 | for slots in slots_array: 18 | for slot in slots: 19 | selected_materials.add(slot.material) 20 | return selected_materials 21 | 22 | def clean_empty_materials(): 23 | for obj in bpy.data.objects: 24 | for slot in obj.material_slots: 25 | mat = slot.material 26 | if mat is None: 27 | print("Removed Empty Materials from " + obj.name) 28 | bpy.ops.object.select_all(action='DESELECT') 29 | obj.select_set(True) 30 | bpy.ops.object.material_slot_remove() 31 | 32 | def clean_no_user_materials(): 33 | for material in bpy.data.materials: 34 | if not material.users: 35 | bpy.data.materials.remove(material) 36 | 37 | def use_nodes(): 38 | for material in bpy.data.materials: 39 | if not material.use_nodes: 40 | material.use_nodes = True -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, but 7 | # WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 | # General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | bl_info = { 15 | "name": "GLBTextureTools", 16 | "author": "Lorenz Wieseke", 17 | "description": "", 18 | "blender": (4, 0, 0), 19 | "version": (0,1,5), 20 | "location": "3DView > Properties (N -KEY) > GLB Texture Tools", 21 | "warning": "", 22 | "wiki_url": "https://govie-editor.de/help/glb-texture-tools/", 23 | "tracker_url": "https://github.com/LorenzWieseke/GLBTextureTools/issues", 24 | "category": "Generic" 25 | } 26 | 27 | import bpy 28 | from .Functions import gui_functions 29 | from .Functions import image_functions 30 | 31 | 32 | from . import auto_load 33 | 34 | auto_load.init() 35 | classes = auto_load.init() 36 | 37 | def register(): 38 | auto_load.register() 39 | bpy.app.handlers.depsgraph_update_post.clear() 40 | bpy.app.handlers.depsgraph_update_post.append(gui_functions.update_on_selection) 41 | bpy.app.handlers.load_post.append(gui_functions.init_values) 42 | bpy.app.handlers.save_pre.append(image_functions.save_images) 43 | 44 | def unregister(): 45 | auto_load.unregister() 46 | bpy.app.handlers.depsgraph_update_post.clear() 47 | bpy.app.handlers.load_post.clear() 48 | bpy.app.handlers.save_pre.clear() 49 | 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/auto_deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | jobs: 7 | build: 8 | name: Upload Release Asset 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Create Release Folder 15 | run: rsync -arv --exclude='.git/' --exclude='.vscode/' --exclude='.github/' --exclude='.gitignore' . ./${{ github.event.repository.name }} 16 | 17 | - name: Switch to Release Folder 18 | run: | 19 | cd ${{ github.event.repository.name }} 20 | ls -la 21 | 22 | - name: Zip Release Folder 23 | run: | 24 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} 25 | 26 | 27 | - name: Create Release 28 | id: create_release 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: Release ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | 38 | - name: Upload Release Asset 39 | id: upload-release-asset 40 | uses: actions/upload-release-asset@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 43 | 44 | with: 45 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 46 | asset_path: ./${{ github.event.repository.name }}.zip 47 | asset_name: ${{ github.event.repository.name }}.zip 48 | asset_content_type: application/zip 49 | -------------------------------------------------------------------------------- /Bake/bake_manager.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import bake_utilities 3 | 4 | from .. Functions import constants 5 | from .. Functions import visibility_functions 6 | 7 | 8 | def bake_texture(self, selected_objects, bake_settings): 9 | parent_operator = self 10 | # ----------------------- CREATE INSTANCE --------------------# 11 | lightmap_utilities = bake_utilities.BakeUtilities(parent_operator, selected_objects, bake_settings) 12 | 13 | if not lightmap_utilities.checkPBR(): 14 | return 15 | 16 | # -----------------------SET LIGHTMAP UV--------------------# 17 | lightmap_utilities.set_active_uv_to_lightmap() 18 | 19 | # -----------------------SETUP UV'S--------------------# 20 | lightmap_utilities.unwrap_selected() 21 | 22 | # -----------------------SETUP ENGINE--------------------# 23 | lightmap_utilities.setup_engine() 24 | 25 | # -----------------------SWITCH BACK TO SHOW ORG MATERIAL --------------------# 26 | visibility_functions.preview_bake_texture(self,context=bpy.context) 27 | 28 | # ----------------------- CREATE NEW MATERIAL FOR BAKING --------------------# 29 | lightmap_utilities.create_bake_material("_AO") 30 | 31 | # -----------------------SETUP NODES--------------------# 32 | lightmap_utilities.add_node_setup() 33 | 34 | # ----------------------- BAKING --------------------# 35 | if bake_settings.lightmap_bake: 36 | lightmap_utilities.save_metal_value() 37 | lightmap_utilities.bake(constants.Bake_Passes.lightmap) 38 | lightmap_utilities.load_metal_value() 39 | lightmap_utilities.add_lightmap_flag() 40 | 41 | if bake_settings.ao_bake: 42 | lightmap_utilities.bake(constants.Bake_Passes.ao) 43 | 44 | lightmap_utilities.cleanup() 45 | del lightmap_utilities 46 | return 47 | 48 | 49 | def bake_on_plane(self,selected_objects,bake_settings): 50 | 51 | parent_operator = self 52 | 53 | # ----------------------- CREATE INSTANCE --------------------# 54 | pbr_utilities = bake_utilities.PbrBakeUtilities(parent_operator,selected_objects,bake_settings) 55 | 56 | # -----------------------SETUP ENGINE--------------------# 57 | 58 | pbr_utilities.setup_engine() 59 | 60 | # ----------------------- BAKE --------------------# 61 | 62 | pbr_utilities.bake_materials_on_object() 63 | del pbr_utilities 64 | 65 | return 66 | -------------------------------------------------------------------------------- /Operators/bake_operator.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..Bake import bake_manager 3 | from .. Functions import gui_functions 4 | 5 | 6 | class GTT_BakeOperator(bpy.types.Operator): 7 | """Bake all attached Textures""" 8 | bl_idname = "object.gtt_bake_operator" 9 | bl_label = "Simple Object Operator" 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | if len(context.selected_objects) < 1: 14 | return False 15 | else: 16 | return True 17 | 18 | def deselect_everything_but_mesh(self,selected_objects): 19 | for obj in selected_objects: 20 | if obj.type != 'MESH': 21 | obj.select_set(False) 22 | 23 | 24 | def execute(self,context): 25 | 26 | # ----------------------- VAR --------------------# 27 | selected_objects = context.selected_objects 28 | bake_settings = context.scene.bake_settings 29 | texture_settings = context.scene.texture_settings 30 | self.deselect_everything_but_mesh(selected_objects) 31 | 32 | # ----------------------- CHECK SELECTION --------------------# 33 | for obj in selected_objects: 34 | if obj.type != 'MESH': 35 | obj.select_set(False) 36 | 37 | if len(obj.material_slots) == 0: 38 | self.report({'INFO'}, 'No Material on ' + obj.name) 39 | 40 | context.view_layer.objects.active = context.selected_objects[0] 41 | # ----------------------- SET VISIBLITY TO MATERIAL --------------------# 42 | texture_settings.preview_bake_texture = False 43 | 44 | # ----------------------- LIGHTMAP / AO --------------------# 45 | if bake_settings.lightmap_bake or bake_settings.ao_bake: 46 | bake_manager.bake_texture(self,selected_objects,bake_settings) 47 | 48 | if bake_settings.show_texture_after_bake and bake_settings.denoise: 49 | texture_settings.preview_bake_texture = True 50 | 51 | for obj in selected_objects: 52 | if bake_settings.ao_bake: 53 | obj.ao_map_name = bake_settings.bake_image_name 54 | if bake_settings.lightmap_bake: 55 | obj.lightmap_name = bake_settings.bake_image_name 56 | 57 | gui_functions.update_active_element_in_bake_list() 58 | 59 | # ----------------------- PBR Texture --------------------# 60 | if bake_settings.pbr_bake: 61 | bake_manager.bake_on_plane(self,selected_objects,bake_settings) 62 | 63 | 64 | return {'FINISHED'} 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Functions/gui_functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.app.handlers import persistent 3 | 4 | 5 | def update_pbr_button(self,context): 6 | self["lightmap_bake"] = False 7 | self["ao_bake"] = False 8 | 9 | def update_lightmap_button(self,context): 10 | self["pbr_bake"] = False 11 | self["ao_bake"] = False 12 | update_active_element_in_bake_list() 13 | 14 | def update_ao_button(self,context): 15 | self["lightmap_bake"] = False 16 | self["pbr_bake"] = False 17 | update_active_element_in_bake_list() 18 | 19 | 20 | # ----------------------- UPDATE BAKE IMAGE NAME / ENUM--------------------# 21 | 22 | last_selection = [] 23 | 24 | @persistent 25 | def update_on_selection(scene): 26 | C = bpy.context 27 | global last_selection 28 | object = getattr(C,"object",None) 29 | if object is None: 30 | return 31 | 32 | if C.selected_objects != last_selection: 33 | last_selection = C.selected_objects 34 | update_active_element_in_bake_list() 35 | 36 | def update_bake_list(bake_settings,context): 37 | 38 | bake_textures_set = set() 39 | 40 | for obj in bpy.data.objects: 41 | if bake_settings.lightmap_bake: 42 | if obj.get("lightmap_name"): 43 | bake_textures_set.add((obj.lightmap_name, obj.lightmap_name, "Baked Texture Name")) 44 | if bake_settings.ao_bake: 45 | if obj.get("ao_map_name"): 46 | bake_textures_set.add((obj.ao_map_name, obj.ao_map_name, "Baked Texture Name")) 47 | 48 | if len(bake_textures_set) == 0: 49 | bake_textures_set.add(("-- Baking Groups --","-- Baking Groups --","No Lightmap baked yet")) 50 | 51 | return list(bake_textures_set) 52 | 53 | 54 | def update_active_element_in_bake_list(): 55 | C = bpy.context 56 | active_object = C.active_object 57 | bake_settings = C.scene.bake_settings 58 | new_bake_image_name = "" 59 | 60 | if bake_settings.lightmap_bake: 61 | new_bake_image_name = active_object.get("lightmap_name") 62 | if new_bake_image_name is None: 63 | new_bake_image_name = "Lightmap " + active_object.name 64 | if bake_settings.ao_bake: 65 | new_bake_image_name = active_object.get("ao_map_name") 66 | if new_bake_image_name is None: 67 | new_bake_image_name = "AO " + active_object.name 68 | 69 | enum_items = bake_settings.get_baked_lightmaps() 70 | keys = [key[0] for key in enum_items] 71 | if new_bake_image_name in keys: 72 | bake_settings.bake_image_name = new_bake_image_name 73 | bake_settings.baking_groups = new_bake_image_name 74 | else: 75 | if active_object.type == "MESH": 76 | bake_settings.bake_image_name = new_bake_image_name 77 | 78 | 79 | def headline(layout,*valueList): 80 | box = layout.box() 81 | row = box.row() 82 | 83 | split = row.split() 84 | for pair in valueList: 85 | split = split.split(factor=pair[0]) 86 | split.label(text=pair[1]) 87 | 88 | @persistent 89 | def init_values(self,context): 90 | bpy.context.scene.world.light_settings.distance = 1 91 | 92 | -------------------------------------------------------------------------------- /Functions/image_functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from . import node_functions 4 | from . import constants 5 | from bpy.app.handlers import persistent 6 | 7 | @persistent 8 | def save_images(self,context): 9 | images = bpy.data.images 10 | for img in images: 11 | if img.is_dirty: 12 | print(img.name) 13 | save_image(img) # img.save() 14 | 15 | 16 | def get_all_images_in_ui_list(): 17 | 18 | images_in_scene = bpy.data.images 19 | image_name_list = bpy.types.GTT_TEX_UL_List.image_name_list 20 | images_found = [] 21 | 22 | if len(image_name_list) > 0: 23 | images_found = [img for img in images_in_scene for name_list_entry in image_name_list if img.name == name_list_entry] 24 | 25 | return images_found 26 | 27 | 28 | def save_image(image,save_internally=False): 29 | 30 | if save_internally: 31 | image.pack() 32 | else: 33 | filePath = bpy.data.filepath 34 | path = os.path.dirname(filePath) 35 | 36 | if not os.path.exists(path + "/textures"): 37 | os.mkdir(path + "/textures") 38 | 39 | if not os.path.exists(path + "/textures/GLBTexTool"): 40 | os.mkdir(path + "/textures/GLBTexTool") 41 | 42 | if not os.path.exists(path + "/textures/GLBTexTool/" + str(image.size[0])): 43 | os.mkdir(path + "/textures/GLBTexTool/" + str(image.size[0])) 44 | 45 | # file format 46 | image.file_format = bpy.context.scene.img_file_format 47 | 48 | # change path 49 | savepath = path + "\\textures\\GLBTexTool\\" + str(image.size[0]) + "\\" + image.name + "." + image.file_format 50 | # image.use_fake_user = True 51 | image.filepath_raw = savepath 52 | image.save() 53 | 54 | 55 | 56 | def create_image(image_name, image_size): 57 | D = bpy.data 58 | # find image 59 | image = D.images.get(image_name) 60 | 61 | if image: 62 | old_size = list(image.size) 63 | new_size = list(image_size) 64 | 65 | if old_size != new_size: 66 | D.images.remove(image) 67 | image = None 68 | 69 | # image = D.images.get(image_name) 70 | 71 | if image is None: 72 | image = D.images.new( 73 | image_name, width=image_size[0], height=image_size[1]) 74 | image.name = image_name 75 | 76 | return image 77 | 78 | def get_file_size(filepath): 79 | size = "Unpack Files" 80 | try: 81 | path = bpy.path.abspath(filepath) 82 | size = os.path.getsize(path) 83 | size /= 1024 84 | except: 85 | return ("Unpack") 86 | # print("error getting file path for " + filepath) 87 | 88 | return (size) 89 | 90 | def scale_image(image, new_size): 91 | if (image.org_filepath != ''): 92 | image.filepath = image.org_filepath 93 | 94 | image.org_filepath = image.filepath 95 | 96 | if new_size[0] > image.size[0] or new_size[1] > image.size[1]: 97 | new_size[0] = image.size[0] 98 | new_size[1] = image.size[1] 99 | 100 | # set image back to original if size is 0, else scale it 101 | if new_size[0] == 0: 102 | image.filepath_raw = image.org_filepath 103 | else: 104 | image.scale(new_size[0], new_size[1]) 105 | save_image(image) 106 | 107 | 108 | -------------------------------------------------------------------------------- /Panels/panel_ui_list.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. Functions import node_functions 3 | from .. Functions import image_functions 4 | from .. Functions import constants 5 | 6 | class GTT_TEX_UL_List(bpy.types.UIList): 7 | image_name_list = set() 8 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag): 9 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 10 | row = layout.row() 11 | 12 | split = row.split(factor=0.6) 13 | split.label(text=str(item.users) + " " +item.name) 14 | 15 | split = split.split(factor=0.5) 16 | split.label(text=str(item.size[0])) 17 | 18 | split = split.split(factor=1) 19 | 20 | filepath = item.filepath 21 | if filepath != '': 22 | filesize = image_functions.get_file_size(filepath) 23 | split.label(text=str(filesize).split('.')[0]) 24 | else: 25 | split.label(text="file not saved") 26 | 27 | def filter_items(self, context, data, propname): 28 | texture_settings = context.scene.texture_settings 29 | selected_objects = context.selected_objects 30 | 31 | all_materials = set() 32 | all_image_names = [image.name for image in data.images] 33 | 34 | slots_array = [obj.material_slots for obj in selected_objects] 35 | for slots in slots_array: 36 | for slot in slots: 37 | all_materials.add(slot.material) 38 | 39 | # Default return values. 40 | flt_flags = [] 41 | flt_neworder = [] 42 | images = [] 43 | 44 | if texture_settings.show_all_textures: 45 | images = [image.name for image in data.images if image.name not in ('Viewer Node','Render Result')] 46 | if texture_settings.show_per_material: 47 | mat = context.active_object.active_material 48 | nodes = mat.node_tree.nodes 49 | tex_nodes = node_functions.get_nodes_by_type(nodes,constants.Node_Types.image_texture) 50 | [images.append(node.image.name) for node in tex_nodes] 51 | else: 52 | for mat in all_materials: 53 | if mat is None: 54 | continue 55 | nodes = mat.node_tree.nodes 56 | tex_nodes = node_functions.get_nodes_by_type(nodes,constants.Node_Types.image_texture) 57 | [images.append(node.image.name) for node in tex_nodes] 58 | 59 | # save filtered list to texture settings / ui_list_itmes 60 | self.image_name_list.clear() 61 | for img in images: 62 | self.image_name_list.add(img) 63 | flt_flags = [self.bitflag_filter_item if name in images else 0 for name in all_image_names] 64 | 65 | return flt_flags, flt_neworder 66 | 67 | 68 | 69 | class GTT_BAKE_IMAGE_UL_List(bpy.types.UIList): 70 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag): 71 | 72 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 73 | row = layout.row() 74 | row.label(text=item.name) 75 | 76 | def filter_items(self, context, data, propname): 77 | bake_settings = context.scene.bake_settings 78 | objects = data.objects 79 | 80 | # Default return values. 81 | flt_flags = [] 82 | flt_neworder = [] 83 | 84 | flt_flags = [self.bitflag_filter_item if obj.lightmap_name == bake_settings.baking_groups and obj.hasLightmap else 0 for obj in objects] 85 | 86 | return flt_flags, flt_neworder 87 | 88 | 89 | -------------------------------------------------------------------------------- /.vscode/.ropeproject/config.py: -------------------------------------------------------------------------------- 1 | # The default ``config.py`` 2 | # flake8: noqa 3 | 4 | 5 | def set_prefs(prefs): 6 | """This function is called before opening the project""" 7 | 8 | # Specify which files and folders to ignore in the project. 9 | # Changes to ignored resources are not added to the history and 10 | # VCSs. Also they are not returned in `Project.get_files()`. 11 | # Note that ``?`` and ``*`` match all characters but slashes. 12 | # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' 13 | # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' 14 | # '.svn': matches 'pkg/.svn' and all of its children 15 | # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' 16 | # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' 17 | prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', 18 | '.hg', '.svn', '_svn', '.git', '.tox'] 19 | 20 | # Specifies which files should be considered python files. It is 21 | # useful when you have scripts inside your project. Only files 22 | # ending with ``.py`` are considered to be python files by 23 | # default. 24 | # prefs['python_files'] = ['*.py'] 25 | 26 | # Custom source folders: By default rope searches the project 27 | # for finding source folders (folders that should be searched 28 | # for finding modules). You can add paths to that list. Note 29 | # that rope guesses project source folders correctly most of the 30 | # time; use this if you have any problems. 31 | # The folders should be relative to project root and use '/' for 32 | # separating folders regardless of the platform rope is running on. 33 | # 'src/my_source_folder' for instance. 34 | # prefs.add('source_folders', 'src') 35 | 36 | # You can extend python path for looking up modules 37 | # prefs.add('python_path', '~/python/') 38 | 39 | # Should rope save object information or not. 40 | prefs['save_objectdb'] = True 41 | prefs['compress_objectdb'] = False 42 | 43 | # If `True`, rope analyzes each module when it is being saved. 44 | prefs['automatic_soa'] = True 45 | # The depth of calls to follow in static object analysis 46 | prefs['soa_followed_calls'] = 0 47 | 48 | # If `False` when running modules or unit tests "dynamic object 49 | # analysis" is turned off. This makes them much faster. 50 | prefs['perform_doa'] = True 51 | 52 | # Rope can check the validity of its object DB when running. 53 | prefs['validate_objectdb'] = True 54 | 55 | # How many undos to hold? 56 | prefs['max_history_items'] = 32 57 | 58 | # Shows whether to save history across sessions. 59 | prefs['save_history'] = True 60 | prefs['compress_history'] = False 61 | 62 | # Set the number spaces used for indenting. According to 63 | # :PEP:`8`, it is best to use 4 spaces. Since most of rope's 64 | # unit-tests use 4 spaces it is more reliable, too. 65 | prefs['indent_size'] = 4 66 | 67 | # Builtin and c-extension modules that are allowed to be imported 68 | # and inspected by rope. 69 | prefs['extension_modules'] = [] 70 | 71 | # Add all standard c-extensions to extension_modules list. 72 | prefs['import_dynload_stdmods'] = True 73 | 74 | # If `True` modules with syntax errors are considered to be empty. 75 | # The default value is `False`; When `False` syntax errors raise 76 | # `rope.base.exceptions.ModuleSyntaxError` exception. 77 | prefs['ignore_syntax_errors'] = False 78 | 79 | # If `True`, rope ignores unresolvable imports. Otherwise, they 80 | # appear in the importing namespace. 81 | prefs['ignore_bad_imports'] = False 82 | 83 | # If `True`, rope will insert new module imports as 84 | # `from import ` by default. 85 | prefs['prefer_module_from_imports'] = False 86 | 87 | # If `True`, rope will transform a comma list of imports into 88 | # multiple separate import statements when organizing 89 | # imports. 90 | prefs['split_imports'] = False 91 | 92 | # If `True`, rope will remove all top-level import statements and 93 | # reinsert them at the top of the module when making changes. 94 | prefs['pull_imports_to_top'] = True 95 | 96 | # If `True`, rope will sort imports alphabetically by module name instead 97 | # of alphabetically by import statement, with from imports after normal 98 | # imports. 99 | prefs['sort_imports_alphabetically'] = False 100 | 101 | # Location of implementation of 102 | # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general 103 | # case, you don't have to change this value, unless you're an rope expert. 104 | # Change this value to inject you own implementations of interfaces 105 | # listed in module rope.base.oi.type_hinting.providers.interfaces 106 | # For example, you can add you own providers for Django Models, or disable 107 | # the search type-hinting in a class hierarchy, etc. 108 | prefs['type_hinting_factory'] = ( 109 | 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') 110 | 111 | 112 | def project_opened(project): 113 | """This function is called after opening the project""" 114 | # Do whatever you like here! 115 | -------------------------------------------------------------------------------- /auto_load.py: -------------------------------------------------------------------------------- 1 | import os 2 | import bpy 3 | import sys 4 | import typing 5 | import inspect 6 | import pkgutil 7 | import importlib 8 | from pathlib import Path 9 | 10 | __all__ = ( 11 | "init", 12 | "register", 13 | "unregister", 14 | ) 15 | 16 | blender_version = bpy.app.version 17 | 18 | modules = None 19 | ordered_classes = None 20 | 21 | def init(): 22 | global modules 23 | global ordered_classes 24 | 25 | modules = get_all_submodules(Path(__file__).parent) 26 | ordered_classes = get_ordered_classes_to_register(modules) 27 | 28 | def register(): 29 | for cls in ordered_classes: 30 | bpy.utils.register_class(cls) 31 | 32 | for module in modules: 33 | if module.__name__ == __name__: 34 | continue 35 | if hasattr(module, "register"): 36 | module.register() 37 | 38 | def unregister(): 39 | for cls in reversed(ordered_classes): 40 | bpy.utils.unregister_class(cls) 41 | 42 | for module in modules: 43 | if module.__name__ == __name__: 44 | continue 45 | if hasattr(module, "unregister"): 46 | module.unregister() 47 | 48 | 49 | # Import modules 50 | ################################################# 51 | 52 | def get_all_submodules(directory): 53 | return list(iter_submodules(directory, directory.name)) 54 | 55 | def iter_submodules(path, package_name): 56 | for name in sorted(iter_submodule_names(path)): 57 | yield importlib.import_module("." + name, package_name) 58 | 59 | def iter_submodule_names(path, root=""): 60 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]): 61 | if is_package: 62 | sub_path = path / module_name 63 | sub_root = root + module_name + "." 64 | yield from iter_submodule_names(sub_path, sub_root) 65 | else: 66 | yield root + module_name 67 | 68 | 69 | # Find classes to register 70 | ################################################# 71 | 72 | def get_ordered_classes_to_register(modules): 73 | return toposort(get_register_deps_dict(modules)) 74 | 75 | def get_register_deps_dict(modules): 76 | my_classes = set(iter_my_classes(modules)) 77 | my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")} 78 | 79 | deps_dict = {} 80 | for cls in my_classes: 81 | deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) 82 | return deps_dict 83 | 84 | def iter_my_register_deps(cls, my_classes, my_classes_by_idname): 85 | yield from iter_my_deps_from_annotations(cls, my_classes) 86 | yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) 87 | 88 | def iter_my_deps_from_annotations(cls, my_classes): 89 | for value in typing.get_type_hints(cls, {}, {}).values(): 90 | dependency = get_dependency_from_annotation(value) 91 | if dependency is not None: 92 | if dependency in my_classes: 93 | yield dependency 94 | 95 | def get_dependency_from_annotation(value): 96 | if blender_version >= (2, 93): 97 | if isinstance(value, bpy.props._PropertyDeferred): 98 | return value.keywords.get("type") 99 | else: 100 | if isinstance(value, tuple) and len(value) == 2: 101 | if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): 102 | return value[1]["type"] 103 | return None 104 | 105 | def iter_my_deps_from_parent_id(cls, my_classes_by_idname): 106 | if bpy.types.Panel in cls.__bases__: 107 | parent_idname = getattr(cls, "bl_parent_id", None) 108 | if parent_idname is not None: 109 | parent_cls = my_classes_by_idname.get(parent_idname) 110 | if parent_cls is not None: 111 | yield parent_cls 112 | 113 | def iter_my_classes(modules): 114 | base_types = get_register_base_types() 115 | for cls in get_classes_in_modules(modules): 116 | if any(base in base_types for base in cls.__bases__): 117 | if not getattr(cls, "is_registered", False): 118 | yield cls 119 | 120 | def get_classes_in_modules(modules): 121 | classes = set() 122 | for module in modules: 123 | for cls in iter_classes_in_module(module): 124 | classes.add(cls) 125 | return classes 126 | 127 | def iter_classes_in_module(module): 128 | for value in module.__dict__.values(): 129 | if inspect.isclass(value): 130 | yield value 131 | 132 | def get_register_base_types(): 133 | return set(getattr(bpy.types, name) for name in [ 134 | "Panel", "Operator", "PropertyGroup", 135 | "AddonPreferences", "Header", "Menu", 136 | "Node", "NodeSocket", "NodeTree", 137 | "UIList", "RenderEngine", 138 | "Gizmo", "GizmoGroup", 139 | ]) 140 | 141 | 142 | # Find order to register to solve dependencies 143 | ################################################# 144 | 145 | def toposort(deps_dict): 146 | sorted_list = [] 147 | sorted_values = set() 148 | while len(deps_dict) > 0: 149 | unsorted = [] 150 | for value, deps in deps_dict.items(): 151 | if len(deps) == 0: 152 | sorted_list.append(value) 153 | sorted_values.add(value) 154 | else: 155 | unsorted.append(value) 156 | deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} 157 | return sorted_list -------------------------------------------------------------------------------- /Properties/properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy import context 3 | from bpy.props import * 4 | from .. Functions import gui_functions 5 | from .. Functions import visibility_functions 6 | 7 | 8 | bpy.types.Scene.img_bake_size = EnumProperty( 9 | name='Image Size', 10 | description='Set resolution for baking and scaling images. This effects PBR, AO and Lightmap baking as well as scaling imges. As of scaling images, choose ORIGINAL to switch back to your image before scaling.', 11 | default='1024', 12 | items=[ 13 | ('128', '128', 'Set image size to 128'), 14 | ('256', '256', 'Set image size to 256'), 15 | ('512', '512', 'Set image size to 512'), 16 | ('1024', '1024', 'Set image size to 1024'), 17 | ('2048', '2048', 'Set image size to 2048'), 18 | ('4096', '4096', 'Set image size to 4096'), 19 | ('8184', '8184', 'Set image size to 8184'), 20 | ('0', 'Original', 'Set image back to original file'), 21 | ]) 22 | 23 | bpy.types.Scene.img_file_format = EnumProperty( 24 | name='File Format', 25 | description='Set file format for output image', 26 | default='JPEG', 27 | items=[ 28 | ('JPEG', 'JPEG', 'JPG is a lossy format with no additional alpha channel, use for color maps'), 29 | ('PNG', 'PNG', 'PNG is lossless and has option for alpha channel, use for normal maps'), 30 | ('HDR', 'HDR', 'HDR is a 32 bit format, use if you need more details or see color banding'), 31 | ]) 32 | 33 | bpy.types.Scene.affect = EnumProperty( 34 | name='Affect', 35 | description='Define if operator should run on active material, all materials on selected object, all materials on visible objects or all materials in scene.', 36 | default='linked', 37 | items=[ 38 | ('active', 'ACTIVE', 'Change only active materials'), 39 | ('selected', 'SELECTED', 'Change all selected materials'), 40 | ('linked', 'LINKED MATERIALS', 'Change all materials on selected objects and all objects that share these materials'), 41 | ('visible', 'VISIBLE', 'Change all visible materials'), 42 | ('scene', 'SCENE', 'Change all materials in scene'), 43 | ]) 44 | 45 | 46 | 47 | class GTT_UV_Settings(bpy.types.PropertyGroup): 48 | uv_slot: IntProperty(default=1) 49 | uv_name: StringProperty(default="AO") 50 | 51 | bpy.utils.register_class(GTT_UV_Settings) 52 | bpy.types.Scene.uv_settings = PointerProperty(type=GTT_UV_Settings) 53 | 54 | class GTT_Cleanup_Settings(bpy.types.PropertyGroup): 55 | clean_texture: BoolProperty(default=True) 56 | clean_material: BoolProperty(default=True) 57 | clean_bake: BoolProperty(default=False) 58 | clean_node_tree: BoolProperty(default=False) 59 | 60 | bpy.utils.register_class(GTT_Cleanup_Settings) 61 | bpy.types.Scene.cleanup_settings = PointerProperty(type=GTT_Cleanup_Settings) 62 | 63 | 64 | class GTT_Bake_Settings(bpy.types.PropertyGroup): 65 | open_bake_settings_menu: BoolProperty(default = False) 66 | open_object_bake_list_menu: BoolProperty(default = False) 67 | 68 | # Type of bake 69 | pbr_bake: BoolProperty(default = True,update=gui_functions.update_pbr_button) 70 | pbr_samples: IntProperty(name = "Samples for PBR bake", default = 1) 71 | 72 | ao_bake: BoolProperty(default = False,update=gui_functions.update_ao_button) 73 | ao_samples: IntProperty(name = "Samples for AO bake", default = 2) 74 | 75 | lightmap_bake: BoolProperty(default = False,update=gui_functions.update_lightmap_button) 76 | lightmap_samples: IntProperty(name = "Samples for Lightmap bake", default = 10) 77 | 78 | baking_groups: EnumProperty( 79 | name='Baked Textures', 80 | description='Groups of objects that share the same baking maps. Click on cursor on the right to select all objectes in that group.', 81 | items=gui_functions.update_bake_list 82 | ) 83 | 84 | def get_current_bake_type(self): 85 | if self.pbr_bake: 86 | current_bake_type = "pbr" 87 | if self.ao_bake: 88 | current_bake_type = "ao" 89 | if self.lightmap_bake: 90 | current_bake_type = "lightmap" 91 | 92 | return current_bake_type 93 | 94 | def get_baked_lightmaps(context): 95 | return gui_functions.update_bake_list(context,context) 96 | 97 | # render_pass : EnumProperty(name='Render Pass',description='Define Render Pass',items=[("Combined","Combined","Bake all passes in this singel Combined Pass"),("Lightmap","Lightmap","Lightmap")]) 98 | 99 | # Checkbox settings 100 | bake_image_name: StringProperty(default="Lightmap") 101 | bake_image_clear: BoolProperty(default= True) 102 | mute_texture_nodes: BoolProperty(default = True) 103 | bake_margin:IntProperty(default=2) 104 | unwrap_margin:FloatProperty(default=0.002) 105 | unwrap: BoolProperty(default= True) 106 | denoise: BoolProperty(default=False) 107 | show_texture_after_bake: BoolProperty(default=True) 108 | bake_object_index:IntProperty(name = "Index for baked Objects", default = 0) 109 | 110 | uv_name="Lightmap" 111 | texture_node_lightmap="Lightmap" 112 | texture_node_ao="AO" 113 | cleanup_textures=False 114 | 115 | bpy.utils.register_class(GTT_Bake_Settings) 116 | bpy.types.Scene.bake_settings = PointerProperty(type=GTT_Bake_Settings) 117 | class GTT_Texture_Settings(bpy.types.PropertyGroup): 118 | open_texture_settings_menu:BoolProperty(default=False) 119 | open_sel_mat_menu:BoolProperty(default=False) 120 | show_all_textures:BoolProperty(default=False) 121 | show_per_material:BoolProperty(default=False) 122 | operate_on_all_textures:BoolProperty(default=False) 123 | 124 | preview_bake_texture:BoolProperty(default=False,update=visibility_functions.preview_bake_texture) 125 | preview_lightmap:BoolProperty(default=False,update=visibility_functions.preview_lightmap) 126 | texture_index:IntProperty(name = "Index for Texture List", default=0, update=visibility_functions.update_selected_image) 127 | 128 | bpy.utils.register_class(GTT_Texture_Settings) 129 | bpy.types.Scene.texture_settings = PointerProperty(type=GTT_Texture_Settings) 130 | 131 | # HELP PANEL PROPERTIES 132 | def run_help_operator(self,context): 133 | bpy.ops.scene.help('INVOKE_DEFAULT') 134 | 135 | bpy.types.Scene.help_tex_tools = BoolProperty(default=False,update=run_help_operator) 136 | 137 | # MATERIAL PROPERTIES 138 | bpy.types.Material.bake_material_name = StringProperty() 139 | 140 | 141 | # IMAGE PROPERTIES 142 | bpy.types.Image.org_filepath = StringProperty() 143 | bpy.types.Image.org_image_name = StringProperty() 144 | 145 | # OBJECT PROPERTIES 146 | bpy.types.Object.hasLightmap = BoolProperty() 147 | bpy.types.Object.lightmap_name = StringProperty() 148 | bpy.types.Object.ao_map_name = StringProperty() 149 | bpy.types.Object.bake_version = StringProperty() 150 | 151 | -------------------------------------------------------------------------------- /Functions/visibility_functions.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy import context 3 | from . import node_functions 4 | from . import material_functions 5 | from . import object_functions 6 | from . import constants 7 | from . import basic_functions 8 | import mathutils 9 | 10 | 11 | def update_selected_image(self, context): 12 | sel_texture = bpy.data.images[self.texture_index] 13 | show_image_in_image_editor(sel_texture) 14 | 15 | 16 | def show_image_in_image_editor(image): 17 | for area in bpy.context.screen.areas: 18 | if area.type == 'IMAGE_EDITOR': 19 | area.spaces.active.image = image 20 | 21 | 22 | def switch_baked_material(show_bake_material,affect): 23 | 24 | current_bake_type = bpy.context.scene.bake_settings.get_current_bake_type() 25 | material_name_suffix = constants.Material_Suffix.bake_type_mat_suffix[current_bake_type] 26 | 27 | # on what object to work 28 | if affect == 'active': 29 | objects = [bpy.context.active_object] 30 | elif affect == 'selected': 31 | objects = bpy.context.selected_editable_objects 32 | elif affect == 'visible': 33 | objects = [ob for ob in bpy.context.view_layer.objects if ob.visible_get()] 34 | elif affect == 'scene': 35 | objects = bpy.context.scene.objects 36 | elif affect == 'linked': 37 | objects = [] 38 | selected_objects = bpy.context.selected_editable_objects 39 | selected_materials = material_functions.get_selected_materials(selected_objects) 40 | 41 | for material in selected_materials: 42 | objs = object_functions.select_obj_by_mat(material) 43 | objects.append(objs) 44 | objects = basic_functions.flatten(objects) 45 | objects = basic_functions.remove_duplicate(objects) 46 | 47 | 48 | all_mats = bpy.data.materials 49 | baked_mats = [mat for mat in all_mats if material_name_suffix in mat.name] 50 | 51 | 52 | for obj in objects: 53 | # if current_bake_type != "pbr": 54 | # baked_ao_flag = getattr(obj,"ao_map_name") != '' or getattr(obj,"lightmap_name") != '' 55 | # if not baked_ao_flag: 56 | # continue 57 | 58 | for slot in obj.material_slots: 59 | if show_bake_material: 60 | for baked_mat in baked_mats: 61 | try: 62 | if baked_mat.name == slot.material.name + material_name_suffix + obj.bake_version: 63 | slot.material = baked_mat 64 | except: 65 | pass 66 | 67 | else: 68 | if (material_name_suffix in slot.material.name): 69 | bake_material = slot.material 70 | index = bake_material.name.find(material_name_suffix) 71 | org_mat = all_mats.get(bake_material.name[0:index]) 72 | if org_mat is not None: 73 | slot.material = org_mat 74 | 75 | def preview_bake_texture(self,context): 76 | context = bpy.context 77 | bake_settings = context.scene.bake_settings 78 | preview_bake_texture = context.scene.texture_settings.preview_bake_texture 79 | vis_mats = material_functions.get_all_visible_materials() 80 | for mat in vis_mats: 81 | if not mat.node_tree: 82 | continue 83 | 84 | nodes = mat.node_tree.nodes 85 | bake_texture_node = None 86 | if bake_settings.lightmap_bake: 87 | bake_texture_node = nodes.get(bake_settings.texture_node_lightmap) 88 | 89 | elif bake_settings.ao_bake: 90 | bake_texture_node = nodes.get(bake_settings.texture_node_ao) 91 | 92 | 93 | if bake_texture_node is not None: 94 | if preview_bake_texture: 95 | node_functions.emission_setup(mat, bake_texture_node.outputs["Color"]) 96 | else: 97 | pbr_node = node_functions.get_nodes_by_type(nodes, constants.Node_Types.pbr_node) 98 | if len(pbr_node) == 0: 99 | return 100 | 101 | pbr_node = pbr_node[0] 102 | node_functions.remove_node(mat, "Emission Bake") 103 | node_functions.reconnect_PBR(mat, pbr_node) 104 | 105 | 106 | def preview_lightmap(self, context): 107 | preview_lightmap = context.scene.texture_settings.preview_lightmap 108 | vis_mats = material_functions.get_all_visible_materials() 109 | for material in vis_mats: 110 | if not material.node_tree: 111 | continue 112 | 113 | nodes = material.node_tree.nodes 114 | 115 | lightmap_node = nodes.get("Lightmap") 116 | if lightmap_node is None: 117 | continue 118 | 119 | pbr_node = node_functions.get_pbr_node(material) 120 | if pbr_node is None: 121 | print("\n " + material.name + " has no PBR Node \n") 122 | continue 123 | base_color_input = node_functions.get_pbr_inputs(pbr_node)["base_color_input"] 124 | emission_input = node_functions.get_pbr_inputs(pbr_node)["emission_input"] 125 | 126 | lightmap_output = lightmap_node.outputs["Color"] 127 | 128 | if preview_lightmap: 129 | 130 | # add mix node 131 | mix_node_name = "Mulitply Lightmap" 132 | mix_node = node_functions.add_node(material,constants.Shader_Node_Types.mix, mix_node_name) 133 | mix_node.blend_type = 'MULTIPLY' 134 | mix_node.inputs[0].default_value = 1 # set factor to 1 135 | pos_offset = mathutils.Vector((-200, 200)) 136 | mix_node.location = pbr_node.location + pos_offset 137 | 138 | mix_node_input1 = mix_node.inputs["Color1"] 139 | mix_node_input2 = mix_node.inputs["Color2"] 140 | mix_node_output = mix_node.outputs["Color"] 141 | 142 | # image texture in base color 143 | if base_color_input.is_linked: 144 | node_before_base_color = base_color_input.links[0].from_node 145 | if not node_before_base_color.name == mix_node_name: 146 | node_functions.make_link(material, node_before_base_color.outputs["Color"], mix_node_input1) 147 | node_functions.make_link(material, lightmap_output, mix_node_input2) 148 | node_functions.make_link(material, mix_node_output, base_color_input) 149 | else : 150 | mix_node_input1.default_value = base_color_input.default_value 151 | node_functions.make_link(material, lightmap_output, mix_node_input2) 152 | node_functions.make_link(material, mix_node_output, base_color_input) 153 | 154 | node_functions.remove_link(material,lightmap_output,emission_input) 155 | 156 | if not preview_lightmap: 157 | 158 | # remove mix and reconnect base color 159 | 160 | mix_node = nodes.get("Mulitply Lightmap") 161 | 162 | if mix_node is not None: 163 | color_input_connections = len(mix_node.inputs["Color1"].links) 164 | 165 | if (color_input_connections == 0): 166 | node_functions.remove_node(material,mix_node.name) 167 | else: 168 | node_functions.remove_reconnect_node(material,mix_node.name) 169 | 170 | node_functions.link_pbr_to_output(material,pbr_node) 171 | 172 | 173 | 174 | 175 | 176 | def lightmap_to_emission(self, context, connect): 177 | 178 | vis_mats = material_functions.get_all_visible_materials() 179 | for material in vis_mats: 180 | if not material.node_tree: 181 | continue 182 | 183 | nodes = material.node_tree.nodes 184 | 185 | pbr_node = node_functions.get_pbr_node(material) 186 | lightmap_node = nodes.get("Lightmap") 187 | 188 | if lightmap_node is None: 189 | continue 190 | 191 | emission_input = node_functions.get_pbr_inputs(pbr_node)["emission_input"] 192 | lightmap_output = lightmap_node.outputs["Color"] 193 | 194 | if connect: 195 | node_functions.make_link(material, lightmap_output, emission_input) 196 | else: 197 | node_functions.remove_link(material,lightmap_output,emission_input) 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /Panels/panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. Functions import gui_functions 3 | # from .. Update import addon_updater_ops 4 | 5 | 6 | class GTT_ResolutionPanel(bpy.types.Panel): 7 | bl_idname = "GLBTEXTOOLS_PT_resolution_panel" 8 | bl_label = "Global Settings" 9 | bl_space_type = "VIEW_3D" 10 | bl_region_type = "UI" 11 | bl_category = 'GLB Texture Tools' 12 | # bl_parent_id = "TEXTURETOOLS_PT_parent_panel" 13 | bl_order = 0 14 | 15 | def draw(self, context): 16 | layout = self.layout 17 | scene = context.scene 18 | 19 | box = layout.box() 20 | column = box.column() 21 | column.prop(scene, "img_bake_size") 22 | column.prop(scene,"img_file_format") 23 | column.prop(scene,"affect") 24 | column.prop(scene.cycles,"device") 25 | 26 | class GTT_BakeTexturePanel(bpy.types.Panel): 27 | bl_idname = "GLBTEXTOOLS_PT_bake_panel" 28 | bl_label = "Baking" 29 | bl_space_type = "VIEW_3D" 30 | bl_region_type = "UI" 31 | bl_category = 'GLB Texture Tools' 32 | bl_order = 1 33 | 34 | def draw(self, context): 35 | layout = self.layout 36 | scene = context.scene 37 | data = bpy.data 38 | bake_settings = bpy.context.scene.bake_settings 39 | 40 | row = layout.row() 41 | row.prop(bake_settings, 'open_bake_settings_menu', text="Bake Settings", icon = 'TRIA_DOWN' if bake_settings.open_bake_settings_menu else 'TRIA_RIGHT' ) 42 | 43 | if bake_settings.open_bake_settings_menu: 44 | 45 | box = layout.box() 46 | 47 | col = box.column(align = True) 48 | row = col.row(align = True) 49 | row.prop(scene.bake_settings, 'pbr_bake', text="PBR",toggle = True) 50 | row.prop(scene.bake_settings, 'pbr_samples', text="Samples",toggle = True) 51 | 52 | row = col.row(align = True) 53 | row.prop(scene.bake_settings, 'ao_bake', text="AO",toggle = True) 54 | row.prop(scene.bake_settings, 'ao_samples', text="Samples") 55 | 56 | row = col.row(align = True) 57 | row.prop(scene.bake_settings, 'lightmap_bake', text="Lightmap",toggle = True) 58 | row.prop(scene.bake_settings, 'lightmap_samples', text="Samples") 59 | 60 | 61 | if bake_settings.pbr_bake: 62 | row = box.row() 63 | # col = row.collumn() 64 | row.prop(scene.bake_settings, 'mute_texture_nodes', text="Mute Texture Mapping") 65 | # row.prop(scene.bake_settings, 'bake_image_clear', text="Clear Bake Image") 66 | 67 | 68 | if bake_settings.lightmap_bake or bake_settings.ao_bake: 69 | 70 | row = box.row() 71 | row.prop(scene.bake_settings, 'bake_image_name', text="") 72 | row = box.row() 73 | row.prop(scene.bake_settings, 'baking_groups',text="") 74 | row.operator("object.select_lightmap_objects",text="",icon="RESTRICT_SELECT_OFF") 75 | 76 | if bake_settings.lightmap_bake: 77 | try: 78 | box.prop(scene.world.node_tree.nodes["Background"].inputs[1],'default_value',text="World Influence") 79 | except: 80 | pass 81 | 82 | if bake_settings.ao_bake: 83 | box.prop(scene.world.light_settings,"distance",text="AO Distance") 84 | 85 | box.prop(scene.bake_settings, 'unwrap_margin', text="UV Margin") 86 | box.prop(scene.bake_settings, 'bake_margin', text="Bake Margin") 87 | 88 | split = box.split() 89 | col = split.column(align=True) 90 | col.prop(scene.bake_settings, 'unwrap', text="Unwrap") 91 | col.prop(scene.bake_settings, 'bake_image_clear', text="Clear Bake Image") 92 | 93 | col = split.column(align=True) 94 | col.prop(scene.bake_settings, 'denoise', text="Denoise") 95 | if bake_settings.denoise: 96 | col.prop(scene.bake_settings, 'show_texture_after_bake', text="Show Texture after Bake") 97 | 98 | # LIGHTMAPPED OBJECT LIST 99 | # row = layout.row() 100 | # row.prop(scene.bake_settings, 'open_object_bake_list_menu', text="Lightmapped Objects", icon = 'TRIA_DOWN' if bake_settings.open_object_bake_list_menu else 'TRIA_RIGHT' ) 101 | 102 | # BAKE LIST 103 | if bake_settings.open_object_bake_list_menu: 104 | layout.template_list("GTT_BAKE_IMAGE_UL_List", "", data, "objects", scene.bake_settings, "bake_object_index") 105 | 106 | 107 | row = layout.row(align=True) 108 | row.scale_y = 2.0 109 | row.operator("object.gtt_bake_operator",text="Bake Textures") 110 | row.operator("scene.open_textures_folder",icon='FILEBROWSER') 111 | 112 | class GTT_VisibilityPanel(bpy.types.Panel): 113 | bl_idname = "GLBTEXTOOLS_PT_visibility_panel" 114 | bl_label = "Visibility" 115 | bl_space_type = "VIEW_3D" 116 | bl_region_type = "UI" 117 | bl_category = 'GLB Texture Tools' 118 | bl_order = 2 119 | 120 | def draw(self, context): 121 | layout = self.layout 122 | scene = context.scene 123 | 124 | col = layout.column() 125 | row = col.row() 126 | row.operator("object.switch_org_mat_operator",icon = 'NODE_MATERIAL', text="Show Original Material") 127 | row.operator("object.switch_bake_mat_operator",icon = 'MATERIAL', text="Show Baked Material") 128 | 129 | # row = col.row(align=True) 130 | layout.prop(scene.texture_settings,"preview_lightmap",text="Show without Lightmap" if scene.texture_settings.preview_lightmap else "Preview Lightmap on Material", icon="SHADING_RENDERED" if scene.texture_settings.preview_bake_texture else "NODE_MATERIAL") 131 | layout.prop(scene.texture_settings,"preview_bake_texture", text="Show Material" if scene.texture_settings.preview_bake_texture else "Preview Baked Texture", icon="SHADING_RENDERED" if scene.texture_settings.preview_bake_texture else "NODE_MATERIAL") 132 | 133 | 134 | class GTT_CleanupPanel(bpy.types.Panel): 135 | bl_idname = "GLBTEXTOOLS_PT_cleanup_panel" 136 | bl_label = "Cleanup" 137 | bl_space_type = "VIEW_3D" 138 | bl_region_type = "UI" 139 | bl_category = 'GLB Texture Tools' 140 | bl_order = 3 141 | 142 | def draw(self, context): 143 | layout = self.layout 144 | 145 | row = layout.row() 146 | row.operator("image.clean_textures",text="Clean Textures",icon = 'OUTLINER_OB_IMAGE') 147 | row.operator("material.clean_materials",text="Clean Materials",icon = 'NODE_MATERIAL') 148 | 149 | row = layout.row() 150 | row.operator("material.clean_lightmap",text="Clean Lightmap",icon = 'MOD_UVPROJECT') 151 | row.operator("material.clean_ao_map",text="Clean AO Map",icon = 'TRASH') 152 | 153 | row = layout.row() 154 | # row.operator("scene.clean_unused_images",text="Clean Unused Images",icon = 'TRASH') 155 | 156 | 157 | class GTT_TextureSelectionPanel(bpy.types.Panel): 158 | bl_idname = "GLBTEXTOOLS_PT_tex_selection_panel" 159 | bl_label = "Texture" 160 | bl_space_type = "VIEW_3D" 161 | bl_region_type = "UI" 162 | bl_category = 'GLB Texture Tools' 163 | # bl_parent_id = "TEXTURETOOLS_PT_parent_panel" 164 | bl_order = 4 165 | 166 | def draw(self, context): 167 | layout = self.layout 168 | scene = context.scene 169 | data = bpy.data 170 | 171 | texture_settings = bpy.context.scene.texture_settings 172 | 173 | # UI LIST 174 | gui_functions.headline(layout,(0.6,"IMAGE NAME"),(0.5,"SIZE"),(1,"KB")) 175 | layout.template_list("GTT_TEX_UL_List", "", data, "images", scene.texture_settings, "texture_index") 176 | 177 | row = layout.row() 178 | row.prop(texture_settings, 'open_texture_settings_menu', text="Texture Settings", icon = 'TRIA_DOWN' if texture_settings.open_texture_settings_menu else 'TRIA_RIGHT' ) 179 | 180 | if texture_settings.open_texture_settings_menu: 181 | 182 | box = layout.box() 183 | 184 | box.prop(scene.texture_settings, 'show_all_textures', text="Show all Textures") 185 | box.prop(scene.texture_settings, 'show_per_material', text="Show Textures per Material") 186 | box.prop(scene.texture_settings, 'operate_on_all_textures', text="Operate on all Textures in List") 187 | box.label(text="If scaled images don't get saved/exported, try unpacking before scaling !") 188 | row = box.row() 189 | row.operator("file.unpack_all",text="Unpack") 190 | row.operator("file.pack_all",text="Pack") 191 | 192 | # Select Material by Texture 193 | row = layout.row() 194 | row.prop(scene.texture_settings,"open_sel_mat_menu",text="Select Material by Texture", icon = 'TRIA_DOWN' if scene.texture_settings.open_sel_mat_menu else 'TRIA_RIGHT' ) 195 | 196 | if scene.texture_settings.open_sel_mat_menu: 197 | box = layout.box() 198 | col = box.column(align = True) 199 | col.operator("scene.select_mat_by_tex",text="Select Material",icon='RESTRICT_SELECT_OFF') 200 | col = box.column(align = True) 201 | if len(scene.materials_found) > 0: 202 | col.label(text="Texture found in Material :") 203 | for mat in scene.materials_found: 204 | col.label(text=mat) 205 | else: 206 | col.label(text="Texture not used") 207 | 208 | # Scale and Clean 209 | row = layout.row() 210 | row.scale_y = 2.0 211 | row.operator("image.scale_image",text="Scale Image",icon= 'FULLSCREEN_EXIT') 212 | 213 | class GTT_UVPanel(bpy.types.Panel): 214 | bl_idname = "GLBTEXTOOLS_PT_UV_panel" 215 | bl_label = "UV" 216 | bl_space_type = "VIEW_3D" 217 | bl_region_type = "UI" 218 | bl_category = "GLB Texture Tools" 219 | bl_options = {'DEFAULT_CLOSED'} 220 | bl_order = 5 221 | 222 | def draw(self, context): 223 | layout = self.layout 224 | 225 | uv_settings = context.scene.uv_settings 226 | box = layout.box() 227 | 228 | row = box.row() 229 | row.prop(uv_settings,"uv_name", text="UV Name") 230 | row.prop(uv_settings,"uv_slot", text="UV Slot") 231 | row = box.row() 232 | row.operator("object.add_uv",text="Add UV",icon = 'ADD').uv_name = uv_settings.uv_name 233 | row.operator("object.remove_uv",text="Remove UV",icon = 'REMOVE').uv_slot = uv_settings.uv_slot 234 | row.operator("object.set_active_uv",text="Set Active",icon = 'RESTRICT_SELECT_OFF').uv_slot = uv_settings.uv_slot 235 | 236 | 237 | class GTT_HelpPanel(bpy.types.Panel): 238 | bl_idname = "GLBTEXTOOLS_PT_help_panel" 239 | bl_label = "Help" 240 | bl_space_type = "VIEW_3D" 241 | bl_region_type = "UI" 242 | bl_category = "GLB Texture Tools" 243 | bl_order = 6 244 | 245 | def draw(self,context): 246 | 247 | layout = self.layout 248 | lang = bpy.app.translations.locale 249 | if lang == 'en_US': 250 | layout.label(text="Find out how to use this addon") 251 | layout.operator("scene.open_link",text="Add-on Documentation",icon='HELP').url = "https://govie.de/en/tutorials-blender/?utm_source=blender-add-on&utm_medium=button#glb_texture_tools" 252 | if lang == 'de_DE': 253 | layout.label(text="Hilfe zur Bedienung des Add-ons") 254 | layout.operator("scene.open_link",text="Add-on Documentation",icon='HELP').url = "https://govie.de/tutorials-blender/?utm_source=blender-add-on&utm_medium=button#glb_texture_tools" 255 | 256 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /Functions/node_functions.py: -------------------------------------------------------------------------------- 1 | import bpy.ops as O 2 | import bpy 3 | import os 4 | from .. Functions import constants 5 | import mathutils 6 | 7 | 8 | # -----------------------COMPOSITING--------------------# 9 | def blur_bake_image(noisy_image,color_image): 10 | 11 | # switch on nodes and get reference 12 | if not bpy.context.scene.use_nodes: 13 | bpy.context.scene.use_nodes = True 14 | 15 | tree = bpy.context.scene.node_tree 16 | 17 | # add cam if not in scene 18 | cam = bpy.context.scene.camera 19 | if not cam: 20 | bpy.ops.object.camera_add() 21 | 22 | # bake image 23 | image_node = tree.nodes.new(type='CompositorNodeImage') 24 | image_node.image = noisy_image 25 | image_node.location = 0, 0 26 | 27 | # color image 28 | color_image_node = tree.nodes.new(type='CompositorNodeImage') 29 | color_image_node.image = color_image 30 | color_image_node.location = 0, 300 31 | 32 | # create blur node 33 | blur_node = tree.nodes.new(type='CompositorNodeBilateralblur') 34 | blur_node.location = 300, 0 35 | 36 | # create output node 37 | comp_node = tree.nodes.new('CompositorNodeComposite') 38 | comp_node.location = 600, 0 39 | 40 | # link nodes 41 | links = tree.links 42 | links.new(image_node.outputs[0], blur_node.inputs[0]) 43 | links.new(color_image_node.outputs[0], blur_node.inputs[1]) 44 | links.new(blur_node.outputs[0], comp_node.inputs[0]) 45 | 46 | # set output resolution to image res 47 | bpy.context.scene.render.resolution_x = noisy_image.size[0] 48 | bpy.context.scene.render.resolution_y = noisy_image.size[1] 49 | 50 | 51 | # set output path 52 | scene = bpy.context.scene 53 | outputImagePath = constants.Path_List.get_textures_dir() 54 | 55 | # set image format and quality 56 | scene.render.image_settings.file_format = bpy.context.scene.img_file_format 57 | scene.render.image_settings.quality = 100 58 | 59 | scene.render.filepath = os.path.join(outputImagePath,noisy_image.name + "_Denoise_AO") 60 | bpy.ops.render.render(write_still=True) 61 | 62 | if bpy.context.scene.img_file_format == 'JPEG': 63 | file_extention = '.jpg' 64 | elif bpy.context.scene.img_file_format == 'PNG': 65 | file_extention = '.png' 66 | elif bpy.context.scene.img_file_format == 'HDR': 67 | file_extention = '.hdr' 68 | 69 | # cleanup 70 | comp_nodes = [image_node,color_image_node,blur_node,comp_node] 71 | for node in comp_nodes: 72 | tree.nodes.remove(node) 73 | 74 | return scene.render.filepath + file_extention 75 | 76 | def comp_ai_denoise(noisy_image, nrm_image, color_image): 77 | 78 | # switch on nodes and get reference 79 | if not bpy.context.scene.use_nodes: 80 | bpy.context.scene.use_nodes = True 81 | 82 | tree = bpy.context.scene.node_tree 83 | 84 | # add cam if not in scene 85 | cam = bpy.context.scene.camera 86 | if not cam: 87 | bpy.ops.object.camera_add() 88 | 89 | # bake image 90 | image_node = tree.nodes.new(type='CompositorNodeImage') 91 | image_node.image = noisy_image 92 | image_node.location = 0, 0 93 | 94 | # nrm image 95 | nrm_image_node = tree.nodes.new(type='CompositorNodeImage') 96 | nrm_image_node.image = nrm_image 97 | nrm_image_node.location = 0, 300 98 | 99 | # color image 100 | color_image_node = tree.nodes.new(type='CompositorNodeImage') 101 | color_image_node.image = color_image 102 | color_image_node.location = 0, 600 103 | 104 | # create denoise node 105 | denoise_node = tree.nodes.new(type='CompositorNodeDenoise') 106 | denoise_node.location = 300, 0 107 | 108 | # create output node 109 | comp_node = tree.nodes.new('CompositorNodeComposite') 110 | comp_node.location = 600, 0 111 | 112 | # link nodes 113 | links = tree.links 114 | links.new(image_node.outputs[0], denoise_node.inputs[0]) 115 | links.new(nrm_image_node.outputs[0], denoise_node.inputs[1]) 116 | links.new(color_image_node.outputs[0], denoise_node.inputs[2]) 117 | links.new(denoise_node.outputs[0], comp_node.inputs[0]) 118 | 119 | # set output resolution to image res 120 | bpy.context.scene.render.resolution_x = noisy_image.size[0] 121 | bpy.context.scene.render.resolution_y = noisy_image.size[1] 122 | 123 | # set output path 124 | scene = bpy.context.scene 125 | outputImagePath = constants.Path_List.get_textures_dir() 126 | 127 | # set image format and quality 128 | scene.render.image_settings.file_format = bpy.context.scene.img_file_format 129 | scene.render.image_settings.quality = 100 130 | 131 | scene.render.filepath = os.path.join(outputImagePath,noisy_image.name + "_Denoise_LM") 132 | print("Starting Denoise") 133 | bpy.ops.render.render(write_still=True) 134 | 135 | if bpy.context.scene.img_file_format == 'JPEG': 136 | file_extention = '.jpg' 137 | elif bpy.context.scene.img_file_format == 'PNG': 138 | file_extention = '.png' 139 | elif bpy.context.scene.img_file_format == 'HDR': 140 | file_extention = '.hdr' 141 | 142 | # cleanup 143 | comp_nodes = [image_node, nrm_image_node,color_image_node, denoise_node, comp_node] 144 | for node in comp_nodes: 145 | tree.nodes.remove(node) 146 | 147 | return scene.render.filepath + file_extention 148 | 149 | 150 | # -----------------------CHECKING --------------------# 151 | 152 | 153 | def check_pbr(self, material): 154 | check_ok = True 155 | 156 | if material is None: 157 | return False 158 | 159 | if material.node_tree is None: 160 | return False 161 | 162 | if material.node_tree.nodes is None: 163 | return False 164 | 165 | # get pbr shader 166 | nodes = material.node_tree.nodes 167 | pbr_node_type = constants.Node_Types.pbr_node 168 | pbr_nodes = get_nodes_by_type(nodes, pbr_node_type) 169 | 170 | # check only one pbr node 171 | if len(pbr_nodes) == 0: 172 | self.report({'INFO'}, 'No PBR Shader Found') 173 | check_ok = False 174 | 175 | if len(pbr_nodes) > 1: 176 | self.report( 177 | {'INFO'}, 'More than one PBR Node found ! Clean before Baking.') 178 | check_ok = False 179 | 180 | return check_ok 181 | 182 | 183 | def check_is_org_material(self, material): 184 | check_ok = True 185 | if "_Bake" in material.name: 186 | self.report({'INFO'}, 'Change back to org. Material') 187 | check_ok = False 188 | 189 | return check_ok 190 | 191 | 192 | # -----------------------NODES --------------------# 193 | 194 | 195 | def get_pbr_inputs(pbr_node): 196 | 197 | base_color_input = pbr_node.inputs["Base Color"] 198 | metallic_input = pbr_node.inputs["Metallic"] 199 | specular_input = pbr_node.inputs["Specular Tint"] 200 | roughness_input = pbr_node.inputs["Roughness"] 201 | normal_input = pbr_node.inputs["Normal"] 202 | emission_input = pbr_node.inputs["Emission Color"] 203 | alpha_input = pbr_node.inputs["Alpha"] 204 | 205 | pbr_inputs = {"base_color_input": base_color_input, "metallic_input": metallic_input, 206 | "specular_input": specular_input, "roughness_input": roughness_input, 207 | "normal_input": normal_input, "emission_input": emission_input,"alpha_input":alpha_input} 208 | return pbr_inputs 209 | 210 | 211 | def get_nodes_by_type(nodes, node_type): 212 | nodes_found = [n for n in nodes if n.type == node_type] 213 | return nodes_found 214 | 215 | 216 | def get_node_by_type_recusivly(material, note_to_start, node_type, del_nodes_inbetween=False): 217 | nodes = material.node_tree.nodes 218 | if note_to_start.type == node_type: 219 | return note_to_start 220 | 221 | for input in note_to_start.inputs: 222 | for link in input.links: 223 | current_node = link.from_node 224 | if (del_nodes_inbetween and note_to_start.type != constants.Node_Types.normal_map and note_to_start.type != constants.Node_Types.bump_map): 225 | nodes.remove(note_to_start) 226 | return get_node_by_type_recusivly(material, current_node, node_type, del_nodes_inbetween) 227 | 228 | 229 | def get_node_by_name_recusivly(node, idname): 230 | if node.bl_idname == idname: 231 | return node 232 | 233 | for input in node.inputs: 234 | for link in input.links: 235 | current_node = link.from_node 236 | return get_node_by_name_recusivly(current_node, idname) 237 | 238 | 239 | def get_pbr_node(material): 240 | nodes = material.node_tree.nodes 241 | pbr_node = get_nodes_by_type(nodes, constants.Node_Types.pbr_node) 242 | if len(pbr_node) > 0: 243 | return pbr_node[0] 244 | 245 | 246 | def make_link(material, socket1, socket2): 247 | links = material.node_tree.links 248 | links.new(socket1, socket2) 249 | 250 | 251 | def remove_link(material, socket1, socket2): 252 | 253 | node_tree = material.node_tree 254 | links = node_tree.links 255 | 256 | for l in socket1.links: 257 | if l.to_socket == socket2: 258 | links.remove(l) 259 | 260 | 261 | def add_in_gamme_node(material, pbrInput): 262 | nodeToPrincipledOutput = pbrInput.links[0].from_socket 263 | 264 | gammaNode = material.node_tree.nodes.new("ShaderNodeGamma") 265 | gammaNode.inputs[1].default_value = 2.2 266 | gammaNode.name = "Gamma Bake" 267 | 268 | # link in gamma 269 | make_link(material, nodeToPrincipledOutput, gammaNode.inputs["Color"]) 270 | make_link(material, gammaNode.outputs["Color"], pbrInput) 271 | 272 | 273 | def remove_gamma_node(material, pbrInput): 274 | nodes = material.node_tree.nodes 275 | gammaNode = nodes.get("Gamma Bake") 276 | nodeToPrincipledOutput = gammaNode.inputs[0].links[0].from_socket 277 | 278 | make_link(material, nodeToPrincipledOutput, pbrInput) 279 | material.node_tree.nodes.remove(gammaNode) 280 | 281 | 282 | def emission_setup(material, node_output): 283 | nodes = material.node_tree.nodes 284 | emission_node = add_node( 285 | material, constants.Shader_Node_Types.emission, "Emission Bake") 286 | 287 | # link emission to whatever goes into current pbrInput 288 | emission_input = emission_node.inputs[0] 289 | make_link(material, node_output, emission_input) 290 | 291 | # link emission to materialOutput 292 | surface_input = get_nodes_by_type(nodes,constants.Node_Types.material_output)[0].inputs[0] 293 | emission_output = emission_node.outputs[0] 294 | make_link(material, emission_output, surface_input) 295 | 296 | 297 | def link_pbr_to_output(material, pbr_node): 298 | nodes = material.node_tree.nodes 299 | surface_input = get_nodes_by_type(nodes,constants.Node_Types.material_output)[0].inputs[0] 300 | make_link(material, pbr_node.outputs[0], surface_input) 301 | 302 | def reconnect_PBR(material, pbrNode): 303 | nodes = material.node_tree.nodes 304 | pbr_output = pbrNode.outputs[0] 305 | surface_input = get_nodes_by_type( 306 | nodes, constants.Node_Types.material_output)[0].inputs[0] 307 | make_link(material, pbr_output, surface_input) 308 | 309 | 310 | def mute_all_texture_mappings(material, do_mute): 311 | nodes = material.node_tree.nodes 312 | for node in nodes: 313 | if node.bl_idname == "ShaderNodeMapping": 314 | node.mute = do_mute 315 | 316 | 317 | def add_node(material, shader_node_type, node_name): 318 | nodes = material.node_tree.nodes 319 | new_node = nodes.get(node_name) 320 | if new_node is None: 321 | new_node = nodes.new(shader_node_type) 322 | new_node.name = node_name 323 | new_node.label = node_name 324 | return new_node 325 | 326 | 327 | def remove_node(material, node_name): 328 | nodes = material.node_tree.nodes 329 | node = nodes.get(node_name) 330 | if node is not None: 331 | nodes.remove(node) 332 | 333 | def remove_reconnect_node(material, node_name): 334 | nodes = material.node_tree.nodes 335 | node = nodes.get(node_name) 336 | input_node = node.inputs["Color1"].links[0].from_node 337 | output_node = node.outputs["Color"].links[0].to_node 338 | 339 | if node is not None: 340 | make_link(material,input_node.outputs["Color"],output_node.inputs["Base Color"]) 341 | nodes.remove(node) 342 | 343 | def remove_unused_nodes(material): 344 | nodes = material.node_tree.nodes 345 | all_nodes = set(nodes) 346 | connected_nodes = set() 347 | material_output = nodes.get("Material Output") 348 | 349 | get_all_connected_nodes(material_output, connected_nodes) 350 | 351 | unconnected_nodes = all_nodes - connected_nodes 352 | 353 | for node in unconnected_nodes: 354 | nodes.remove(node) 355 | 356 | 357 | def remove_double_linking(material,texture_node): 358 | color_output = texture_node.outputs["Color"] 359 | links_count = len(color_output.links) 360 | org_vector_input = texture_node.inputs["Vector"] 361 | position_y = texture_node.location.y 362 | 363 | if links_count > 1: 364 | for link in color_output.links: 365 | 366 | new_texture_node = add_node(material,constants.Shader_Node_Types.image_texture,texture_node.name + "_Copy" + str(link)) 367 | new_texture_node.image = texture_node.image 368 | new_texture_node.location = texture_node.location 369 | position_y -= 250 370 | new_texture_node.location.y = position_y 371 | 372 | # relink tex node output 373 | make_link(material,new_texture_node.outputs["Color"],link.to_socket) 374 | 375 | # remap texture mapping 376 | if len(org_vector_input.links) != 0: 377 | new_vector_input = new_texture_node.inputs["Vector"] 378 | tex_transform_socket = org_vector_input.links[0].from_socket 379 | make_link(material,tex_transform_socket,new_vector_input) 380 | 381 | 382 | 383 | def get_all_connected_nodes(node, connected_nodes): 384 | 385 | connected_nodes.add(node) 386 | 387 | for input in node.inputs: 388 | for link in input.links: 389 | current_node = link.from_node 390 | get_all_connected_nodes(current_node, connected_nodes) 391 | -------------------------------------------------------------------------------- /Operators/operators.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import bpy 4 | 5 | 6 | from ..Functions import (basic_functions, constants, 7 | image_functions, material_functions, node_functions, 8 | visibility_functions,object_functions) 9 | 10 | 11 | class GTT_VerifyMaterialsOperator(bpy.types.Operator): 12 | """Check for each visbile material if it has a PBR Shader so the GLTF Export works fine""" 13 | bl_idname = "object.verify_materials" 14 | bl_label = "Verify Materials" 15 | 16 | def execute(self, context): 17 | 18 | vis_mats = material_functions.get_all_visible_materials() 19 | 20 | for mat in vis_mats: 21 | check_ok = node_functions.check_pbr(self,mat) 22 | if not check_ok: 23 | self.report({'INFO'}, "No PBR Shader in " + mat.name) 24 | 25 | objects_in_scene = bpy.data.objects 26 | for obj in objects_in_scene: 27 | for slot in obj.material_slots: 28 | if slot.material is None: 29 | self.report({'INFO'}, "Empty Material Slot on " + obj.name) 30 | return {'FINISHED'} 31 | 32 | 33 | 34 | # ----------------------- LIGHTAP OPERATORS--------------------# 35 | class GTT_SelectLightmapObjectsOperator(bpy.types.Operator): 36 | """Select all Objects in the list that have the according lightmap attached to them. Makes it easy to rebake multiple Objects""" 37 | bl_idname = "object.select_lightmap_objects" 38 | bl_label = "Select Lightmap Objects" 39 | 40 | @classmethod 41 | def poll(cls, context): 42 | if context.scene.bake_settings.baking_groups == '-- Baking Groups --': 43 | return False 44 | return True 45 | 46 | def execute(self, context): 47 | 48 | C = context 49 | O = bpy.ops 50 | 51 | bake_settings = C.scene.bake_settings 52 | active_lightmap = bake_settings.baking_groups 53 | objects = [ob for ob in bpy.context.view_layer.objects if ob.visible_get()] 54 | 55 | O.object.select_all(action='DESELECT') 56 | for obj in objects: 57 | if obj.lightmap_name == active_lightmap or obj.ao_map_name == active_lightmap: 58 | C.view_layer.objects.active = obj 59 | obj.select_set(True) 60 | 61 | return {'FINISHED'} 62 | 63 | # ----------------------- TEXTURE OPERATORS--------------------# 64 | 65 | class GTT_GetMaterialByTextureOperator(bpy.types.Operator): 66 | bl_idname = "scene.select_mat_by_tex" 67 | bl_label = "Select Material By Texture" 68 | bl_description = "Selecting all materials in scene that use the selected texture" 69 | bl_options = {"REGISTER"} 70 | 71 | bpy.types.Scene.materials_found = [] 72 | 73 | @classmethod 74 | def poll(cls, context): 75 | D = bpy.data 76 | images = D.images 77 | 78 | display = True 79 | 80 | # image to index not found 81 | try: 82 | sel_image_texture = images[context.scene.texture_settings.texture_index] 83 | if sel_image_texture.name in ('Viewer Node', 'Render Result'): 84 | display = False 85 | except: 86 | display = False 87 | return display 88 | 89 | 90 | def execute(self, context): 91 | D = bpy.data 92 | 93 | images = D.images 94 | sel_image_texture = images[context.scene.texture_settings.texture_index] 95 | materials = D.materials 96 | 97 | # to print materials with current image texture 98 | materials_found = context.scene.materials_found 99 | materials_found.clear() 100 | 101 | for mat in materials: 102 | if hasattr(mat,"node_tree"): 103 | if hasattr(mat.node_tree,"nodes"): 104 | nodes = mat.node_tree.nodes 105 | tex_node_type = constants.Node_Types.image_texture 106 | tex_nodes = node_functions.get_nodes_by_type(nodes,tex_node_type) 107 | 108 | # if texture node in current node tree 109 | if len(tex_nodes) > 0: 110 | images = [node.image for node in tex_nodes] 111 | if sel_image_texture in images: 112 | materials_found.append(mat.name) 113 | object_functions.select_obj_by_mat(mat,self) 114 | 115 | 116 | return {"FINISHED"} 117 | 118 | class GTT_ScaleImageOperator(bpy.types.Operator): 119 | """Scale all Images on selected Material to specific resolution""" 120 | bl_idname = "image.scale_image" 121 | bl_label = "Scale Images" 122 | 123 | @classmethod 124 | def poll(cls, context): 125 | D = bpy.data 126 | images = D.images 127 | 128 | display = True 129 | 130 | # if operate on all textures is checked we don't need to get the index 131 | if context.scene.texture_settings.operate_on_all_textures: 132 | return True 133 | 134 | # image to index not found 135 | try: 136 | sel_image_texture = images[context.scene.texture_settings.texture_index] 137 | if sel_image_texture.name in ('Viewer Node','Render Result'): 138 | display = False 139 | except: 140 | display = False 141 | return display 142 | 143 | def invoke(self,context,event): 144 | D = bpy.data 145 | 146 | texture_settings = context.scene.texture_settings 147 | image_size = [int(context.scene.img_bake_size),int(context.scene.img_bake_size)] 148 | 149 | images_in_scene = D.images 150 | all_images = image_functions.get_all_images_in_ui_list() 151 | 152 | if texture_settings.operate_on_all_textures: 153 | for img in all_images: 154 | image_functions.scale_image(img,image_size) 155 | else: 156 | sel_image_texture = images_in_scene[texture_settings.texture_index] 157 | image_functions.scale_image(sel_image_texture,image_size) 158 | 159 | return {'FINISHED'} 160 | 161 | def modal(self, context, event): 162 | if event.type in {'RIGHTMOUSE', 'ESC'}: 163 | return {'CANCELLED'} 164 | 165 | return {'PASS_THROUGH'} 166 | 167 | # ----------------------- VIEW OPERATORS--------------------# 168 | 169 | class GTT_SwitchBakeMaterialOperator(bpy.types.Operator): 170 | """Switch to baked material""" 171 | bl_idname = "object.switch_bake_mat_operator" 172 | bl_label = "Baked Material" 173 | 174 | def execute(self, context): 175 | 176 | show_bake_material = True 177 | visibility_functions.switch_baked_material(show_bake_material,context.scene.affect) 178 | 179 | return {'FINISHED'} 180 | 181 | class GTT_SwitchOrgMaterialOperator(bpy.types.Operator): 182 | """Switch from baked to original material""" 183 | bl_idname = "object.switch_org_mat_operator" 184 | bl_label = "Org. Material" 185 | 186 | def execute(self, context): 187 | 188 | show_bake_material = False 189 | visibility_functions.switch_baked_material(show_bake_material,context.scene.affect) 190 | 191 | return {'FINISHED'} 192 | 193 | class GTT_PreviewBakeTextureOperator(bpy.types.Operator): 194 | """Connect baked texture to emission to see result""" 195 | bl_idname = "object.preview_bake_texture" 196 | bl_label = "Preview Bake Texture" 197 | 198 | connect : bpy.props.BoolProperty() 199 | def execute(self, context): 200 | context.scene.texture_settings.preview_bake_texture = self.connect 201 | visibility_functions.preview_bake_texture(self,context) 202 | 203 | return {'FINISHED'} 204 | 205 | 206 | 207 | class GTT_LightmapEmissionOperator(bpy.types.Operator): 208 | """Connect baked Lightmap to Emission input of Principled Shader""" 209 | bl_idname = "object.lightmap_to_emission" 210 | bl_label = "Lightmap to Emission" 211 | 212 | connect : bpy.props.BoolProperty() 213 | def execute(self, context): 214 | 215 | visibility_functions.lightmap_to_emission(self,context,self.connect) 216 | 217 | 218 | return {'FINISHED'} 219 | 220 | class GTT_PreviewLightmap(bpy.types.Operator): 221 | """Connect baked Lightmap to Base Color input of Principled Shader""" 222 | bl_idname = "object.preview_lightmap" 223 | bl_label = "Lightmap to Base Color" 224 | 225 | connect : bpy.props.BoolProperty() 226 | def execute(self, context): 227 | context.scene.texture_settings.preview_lightmap = self.connect 228 | visibility_functions.preview_lightmap(self,context) 229 | 230 | return {'FINISHED'} 231 | 232 | 233 | # ----------------------- UV OPERATORS--------------------# 234 | 235 | class GTT_AddUVOperator(bpy.types.Operator): 236 | """Add uv layer with layer name entered above""" 237 | bl_idname = "object.add_uv" 238 | bl_label = "Add UV to all selected objects" 239 | 240 | uv_name:bpy.props.StringProperty() 241 | 242 | def execute(self, context): 243 | sel_objects = context.selected_objects 244 | 245 | for obj in sel_objects: 246 | if obj.type != "MESH": 247 | continue 248 | uv_layers = obj.data.uv_layers 249 | if self.uv_name in uv_layers: 250 | print("UV Name already take, choose another one") 251 | continue 252 | uv_layers.new(name=self.uv_name) 253 | uv_layers.get(self.uv_name).active = True 254 | 255 | return {'FINISHED'} 256 | 257 | class GTT_RemoveUVOperator(bpy.types.Operator): 258 | """Delete all uv layers found in uv_slot entered above""" 259 | bl_idname = "object.remove_uv" 260 | bl_label = "Remove UV" 261 | 262 | uv_slot: bpy.props.IntProperty() 263 | 264 | 265 | def execute(self, context): 266 | sel_objects = context.selected_objects 267 | self.uv_slot -= 1 268 | 269 | for obj in sel_objects: 270 | if obj.type != "MESH": 271 | continue 272 | uv_layers = obj.data.uv_layers 273 | 274 | if len(uv_layers) > self.uv_slot: 275 | uv_layers.remove(uv_layers[self.uv_slot]) 276 | 277 | return {'FINISHED'} 278 | 279 | class GTT_SetActiveUVOperator(bpy.types.Operator): 280 | """Set the acive uv to the slot entered above""" 281 | bl_idname = "object.set_active_uv" 282 | bl_label = "Set Active UV" 283 | 284 | uv_slot:bpy.props.IntProperty() 285 | 286 | def execute(self, context): 287 | sel_objects = context.selected_objects 288 | self.uv_slot -= 1 289 | 290 | for obj in sel_objects: 291 | if obj.type != "MESH": 292 | continue 293 | uv_layers = obj.data.uv_layers 294 | 295 | if len(uv_layers) > self.uv_slot: 296 | uv_layers.active_index = self.uv_slot 297 | 298 | return {'FINISHED'} 299 | # ----------------------- CLEAN OPERATORS--------------------# 300 | 301 | # class CleanBakesOperator(bpy.types.Operator): 302 | class GTT_RemoveLightmapOperator(bpy.types.Operator): 303 | """Remove Lightmap and UV Node""" 304 | bl_idname = "material.clean_lightmap" 305 | bl_label = "Clean Lightmap" 306 | 307 | def execute(self, context): 308 | 309 | selected_objects = context.selected_objects 310 | bake_settings = context.scene.bake_settings 311 | all_materials = set() 312 | slots_array = [obj.material_slots for obj in selected_objects] 313 | for slots in slots_array: 314 | for slot in slots: 315 | all_materials.add(slot.material) 316 | 317 | for mat in all_materials: 318 | if mat is None: 319 | continue 320 | node_functions.remove_node(mat,bake_settings.texture_node_lightmap) 321 | node_functions.remove_node(mat,"Mulitply Lightmap") 322 | node_functions.remove_node(mat,"Second_UV") 323 | 324 | #remove lightmap flag 325 | for obj in selected_objects: 326 | obj.hasLightmap = False 327 | if obj.get('lightmap_name') is not None : 328 | del obj["lightmap_name"] 329 | 330 | return {'FINISHED'} 331 | 332 | class GTT_RemoveAOOperator(bpy.types.Operator): 333 | """Remove AO Node and clear baking flags on object""" 334 | bl_idname = "material.clean_ao_map" 335 | bl_label = "Clean AO map" 336 | 337 | def execute(self, context): 338 | # visibility_functions.switch_baked_material(False,"scene") 339 | bpy.ops.material.clean_materials() 340 | 341 | bake_settings = context.scene.bake_settings 342 | selected_objects = context.selected_objects 343 | all_materials = set() 344 | slots_array = [obj.material_slots for obj in selected_objects] 345 | for slots in slots_array: 346 | for slot in slots: 347 | all_materials.add(slot.material) 348 | 349 | for mat in all_materials: 350 | if mat is None: 351 | continue 352 | 353 | node_functions.remove_node(mat,bake_settings.texture_node_ao) 354 | node_functions.remove_node(mat,"glTF Settings") 355 | 356 | #remove flag 357 | for obj in selected_objects: 358 | if obj.get('ao_map_name') is not None : 359 | del obj["ao_map_name"] 360 | if obj.get('bake_version') is not None : 361 | del obj["bake_version"] 362 | 363 | return {'FINISHED'} 364 | 365 | class GTT_CleanTexturesOperator(bpy.types.Operator): 366 | """Remove unreferenced images""" 367 | bl_idname = "image.clean_textures" 368 | bl_label = "Clean Textures" 369 | 370 | def execute(self, context): 371 | for image in bpy.data.images: 372 | if not image.users or list(image.size) == [0,0]: 373 | bpy.data.images.remove(image) 374 | return {'FINISHED'} 375 | 376 | class GTT_CleanMaterialsOperator(bpy.types.Operator): 377 | """Clean materials with no users and remove empty material slots""" 378 | bl_idname = "material.clean_materials" 379 | bl_label = "Clean Materials" 380 | 381 | def execute(self, context): 382 | material_functions.clean_empty_materials() 383 | material_functions.clean_no_user_materials() 384 | material_functions.use_nodes() 385 | 386 | return {'FINISHED'} 387 | 388 | class GTT_CleanUnusedImagesOperator(bpy.types.Operator): 389 | """Clean all images from Hard Disk that are not used in scene""" 390 | bl_idname = "scene.clean_unused_images" 391 | bl_label = "Clean Images" 392 | bl_options = {"REGISTER"} 393 | 394 | @classmethod 395 | def poll(cls, context): 396 | return True 397 | 398 | def execute(self, context): 399 | images_in_folder = [] 400 | images_in_blender = bpy.data.images 401 | image_paths_in_blender = [] 402 | 403 | filePath = bpy.data.filepath 404 | path = os.path.dirname(filePath) + "\\textures" 405 | 406 | # find images on hard drive 407 | if os.path.exists(path): 408 | for path, subdirs, files in os.walk(path): 409 | for name in files: 410 | images_in_folder.append(path+"\\"+name) 411 | 412 | for img in images_in_blender: 413 | image_paths_in_blender.append(img.filepath) 414 | 415 | images_intersection = basic_functions.Intersection(image_paths_in_blender,images_in_folder) 416 | images_to_clean = basic_functions.Diff(images_in_folder,images_intersection) 417 | 418 | print("Deleting files :") 419 | for img in images_to_clean: 420 | os.remove(img) 421 | print(img) 422 | return {'FINISHED'} 423 | 424 | 425 | # ----------------------- FILE OPERATORS--------------------# 426 | 427 | class GTT_OpenTexturesFolderOperator(bpy.types.Operator): 428 | """Open Texture folder if it exists, bake or scale texture to create texture folder""" 429 | bl_idname = "scene.open_textures_folder" 430 | bl_label = "Open Folder" 431 | 432 | texture_path="\\textures\\" 433 | 434 | @classmethod 435 | def poll(self, context): 436 | filePath = bpy.data.filepath 437 | path = os.path.dirname(filePath) 438 | if os.path.exists(path + self.texture_path): 439 | return True 440 | return False 441 | 442 | def execute(self, context): 443 | filepath = bpy.data.filepath 444 | directory = os.path.dirname(filepath) + self.texture_path 445 | 446 | if filepath != "": 447 | subprocess.call("explorer " + directory, shell=True) 448 | else: 449 | self.report({'INFO'}, 'You need to save Blend file first !') 450 | 451 | return {"FINISHED"} 452 | 453 | class GOVIE_Open_Link_Operator(bpy.types.Operator): 454 | bl_idname = "scene.open_link" 455 | bl_label = "Open Website" 456 | bl_description = "Go to GOVIE Website" 457 | 458 | url : bpy.props.StringProperty(name="url") 459 | 460 | @classmethod 461 | def poll(cls, context): 462 | return True 463 | def execute(self, context): 464 | bpy.ops.wm.url_open(url=self.url) 465 | return {"FINISHED"} 466 | -------------------------------------------------------------------------------- /Bake/bake_utilities.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import ObjectShaderFx 3 | import mathutils 4 | from .. Functions import node_functions 5 | from .. Functions import image_functions 6 | from .. Functions import constants 7 | from .. Functions import visibility_functions 8 | from .. Functions import material_functions 9 | 10 | blender_version = bpy.app.version 11 | 12 | class BakeUtilities(): 13 | C = bpy.context 14 | D = bpy.data 15 | O = bpy.ops 16 | 17 | all_materials = None 18 | image_texture_nodes = None 19 | bake_settings = None 20 | bake_image = None 21 | render_engine = None 22 | selected_objects = None 23 | image_size = None 24 | parent_operator = None 25 | tex_node_name = None 26 | 27 | def __init__(self,parent_operator,selected_objects, bake_settings): 28 | self.C = bpy.context 29 | self.D = bpy.data 30 | self.parent_operator = parent_operator 31 | self.render_engine = self.C.scene.render.engine 32 | self.selected_objects = selected_objects 33 | self.all_materials = self.D.materials 34 | self.selected_materials = material_functions.get_selected_materials(self.selected_objects) 35 | self.bake_settings = bake_settings 36 | self.baked_images = [] 37 | self.image_texture_nodes = set() 38 | self.image_size = [int(self.C.scene.img_bake_size), 39 | int(self.C.scene.img_bake_size)] 40 | image_name = bake_settings.bake_image_name 41 | 42 | self.bake_image = image_functions.create_image(image_name, self.image_size) 43 | 44 | def setup_engine(self): 45 | # setup engine 46 | if self.render_engine == 'BLENDER_EEVEE': 47 | self.C.scene.render.engine = 'CYCLES' 48 | 49 | # setup device type 50 | self.cycles_device_type = self.C.preferences.addons['cycles'].preferences.compute_device_type 51 | if self.cycles_device_type == 'OPTIX': 52 | self.C.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA' 53 | 54 | # setup samples 55 | if self.bake_settings.pbr_bake: 56 | self.C.scene.cycles.samples = self.bake_settings.pbr_samples 57 | if self.bake_settings.lightmap_bake: 58 | self.C.scene.cycles.samples = self.bake_settings.lightmap_samples 59 | if self.bake_settings.ao_bake: 60 | self.C.scene.cycles.samples = self.bake_settings.ao_samples 61 | 62 | self.C.scene.render.resolution_percentage = 100 63 | 64 | def set_active_uv_to_lightmap(self): 65 | bpy.ops.object.set_active_uv(uv_slot=2) 66 | 67 | def checkPBR(self): 68 | for material in self.selected_materials: 69 | self.active_material = material 70 | # check if pbr node exists 71 | check_ok = node_functions.check_pbr(self.parent_operator,material) 72 | if not check_ok : 73 | self.parent_operator.report({'INFO'}, "Material " + material.name + " has no PBR Node !") 74 | return check_ok 75 | 76 | def unwrap_selected(self): 77 | if self.bake_settings.unwrap: 78 | self.O.object.add_uv(uv_name=self.bake_settings.uv_name) 79 | 80 | # apply scale on linked 81 | sel_objects = self.C.selected_objects 82 | scene_objects = self.D.objects 83 | linked_objects = set() 84 | 85 | for sel_obj in sel_objects: 86 | for scene_obj in scene_objects: 87 | if sel_obj.data.original is scene_obj.data and sel_obj is not scene_obj: 88 | linked_objects.add(sel_obj) 89 | 90 | # do not apply transform if linked objects in selection 91 | if not len(linked_objects)>0: 92 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 93 | 94 | 95 | self.O.object.mode_set(mode='EDIT') 96 | self.O.mesh.reveal() 97 | self.O.mesh.select_all(action='SELECT') 98 | self.O.uv.smart_project(island_margin=self.bake_settings.unwrap_margin) 99 | self.O.object.mode_set(mode='OBJECT') 100 | 101 | def create_bake_material(self,material_name_suffix): 102 | 103 | bake_materials = [] 104 | selected_materials = [] 105 | for obj in self.selected_objects: 106 | for slot in obj.material_slots: 107 | selected_materials.append(slot.material) 108 | 109 | # switch to ao material if we are on org and ao was already baked 110 | visibility_functions.switch_baked_material(True,"scene") 111 | 112 | for obj in self.selected_objects: 113 | for slot in obj.material_slots: 114 | material = slot.material 115 | bake_material_name = material.name + material_name_suffix 116 | 117 | # check if material was already baked and continue 118 | if material_name_suffix in material.name: 119 | bake_materials.append(material) 120 | continue 121 | 122 | # if not, copy material or take one out of the previewsly filled bake list 123 | else : 124 | bake_material = list(filter(lambda material: material.name == bake_material_name, bake_materials)) 125 | 126 | if len(bake_material) == 0: 127 | bake_material = material.copy() 128 | bake_material.name = bake_material_name 129 | bake_materials.append(bake_material) 130 | slot.material = bake_material 131 | 132 | else: 133 | bake_material = bake_material[0] 134 | slot.material = bake_material 135 | 136 | index = bake_material.name.find(".") 137 | if index == -1: 138 | obj.bake_version = "" 139 | else: 140 | obj.bake_version = bake_material.name[index:] 141 | 142 | material.use_fake_user = True 143 | 144 | 145 | # remove duplicate entries 146 | self.selected_materials = list(set(bake_materials)) 147 | 148 | def add_gltf_material_output_node(self, material): 149 | nodes = material.node_tree.nodes 150 | 151 | name = "glTF Material Output" 152 | gltf_node_group = bpy.data.node_groups.new(name, 'ShaderNodeTree') 153 | gltf_node_group.inputs.new("NodeSocketFloat", "Occlusion") 154 | thicknessFactor = gltf_node_group.inputs.new("NodeSocketFloat", "Thickness") 155 | thicknessFactor.default_value = 0.0 156 | gltf_node_group.nodes.new('NodeGroupOutput') 157 | gltf_node_group_input = gltf_node_group.nodes.new('NodeGroupInput') 158 | specular = gltf_node_group.inputs.new("NodeSocketFloat", "Specular") 159 | specular.default_value = 1.0 160 | specularColor = gltf_node_group.inputs.new("NodeSocketColor", "Specular Color") 161 | specularColor.default_value = [1.0,1.0,1.0,1.0] 162 | gltf_node_group_input.location = -200, 0 163 | 164 | 165 | gltf_settings_node = nodes.get(name) 166 | if gltf_settings_node is None: 167 | gltf_settings_node = nodes.new('ShaderNodeGroup') 168 | gltf_settings_node.name = name 169 | gltf_settings_node.node_tree = bpy.data.node_groups[name] 170 | 171 | return gltf_settings_node 172 | 173 | 174 | 175 | 176 | def add_gltf_settings_node(self, material): 177 | nodes = material.node_tree.nodes 178 | # create group data 179 | gltf_settings = bpy.data.node_groups.get('glTF Settings') 180 | if gltf_settings is None: 181 | bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree') 182 | 183 | # add group to node tree 184 | gltf_settings_node = nodes.get('glTF Settings') 185 | if gltf_settings_node is None: 186 | gltf_settings_node = nodes.new('ShaderNodeGroup') 187 | gltf_settings_node.name = 'glTF Settings' 188 | gltf_settings_node.node_tree = bpy.data.node_groups['glTF Settings'] 189 | 190 | # create group inputs 191 | if gltf_settings_node.inputs.get('Occlusion') is None: 192 | gltf_settings_node.inputs.new('NodeSocketFloat','Occlusion') 193 | 194 | return gltf_settings_node 195 | 196 | def add_image_texture_node(self, material): 197 | nodes = material.node_tree.nodes 198 | 199 | # add image texture 200 | if self.bake_settings.lightmap_bake: 201 | self.tex_node_name = self.bake_settings.texture_node_lightmap 202 | 203 | if self.bake_settings.ao_bake: 204 | self.tex_node_name = self.bake_settings.texture_node_ao 205 | 206 | image_texture_node = node_functions.add_node(material, constants.Shader_Node_Types.image_texture, self.tex_node_name) 207 | image_texture_node.image = self.bake_image 208 | self.bake_image.colorspace_settings.name = "Linear FilmLight E-Gamut" 209 | nodes.active = image_texture_node 210 | 211 | # save texture nodes and pbr nodes for later 212 | self.image_texture_nodes.add(image_texture_node) 213 | 214 | return image_texture_node 215 | 216 | def save_metal_value(self): 217 | for material in self.selected_materials: 218 | pbr_node = node_functions.get_pbr_node(material) 219 | 220 | # save metal value 221 | metallic_value = pbr_node.inputs["Metallic"].default_value 222 | pbr_node["original_metallic"] = metallic_value 223 | pbr_node.inputs["Metallic"].default_value = 0 224 | 225 | # save metal image 226 | if pbr_node.inputs["Metallic"].is_linked: 227 | 228 | # get metal image node, save it in pbr node and remove connection 229 | metal_image_node_socket = pbr_node.inputs["Metallic"].links[0].from_socket 230 | self.metal_image_node_output = metal_image_node_socket 231 | node_functions.remove_link(material,metal_image_node_socket,pbr_node.inputs["Metallic"]) 232 | 233 | def load_metal_value(self): 234 | for material in self.selected_materials: 235 | pbr_node = node_functions.get_pbr_node(material) 236 | pbr_node.inputs["Metallic"].default_value = pbr_node["original_metallic"] 237 | 238 | # reconnect metal image 239 | if hasattr(self,"metal_image_node_output"): 240 | node_functions.make_link(material,self.metal_image_node_output,pbr_node.inputs["Metallic"]) 241 | 242 | def add_uv_node(self,material): 243 | 244 | uv_node = node_functions.add_node(material, constants.Shader_Node_Types.uv, "Second_UV") 245 | uv_node.uv_map = self.bake_settings.uv_name 246 | return uv_node 247 | 248 | def position_gltf_setup_nodes(self,material,uv_node,image_texture_node,gltf_settings_node): 249 | nodes = material.node_tree.nodes 250 | # uv node 251 | pbr_node = node_functions.get_pbr_node(material) 252 | pos_offset = mathutils.Vector((-900, 400)) 253 | loc = pbr_node.location + pos_offset 254 | uv_node.location = loc 255 | 256 | # image texture 257 | loc = loc + mathutils.Vector((300, 0)) 258 | image_texture_node.location = loc 259 | 260 | # ao node 261 | loc = loc + mathutils.Vector((300, 0)) 262 | gltf_settings_node.location = loc 263 | 264 | nodes.active = image_texture_node 265 | 266 | def add_node_setup(self): 267 | for material in self.selected_materials: 268 | # AO 269 | if self.bake_settings.ao_bake: 270 | uv_node = self.add_uv_node(material) 271 | image_texture_node = self.add_image_texture_node(material) 272 | 273 | if blender_version <= (3, 3): 274 | gltf_settings_node = self.add_gltf_settings_node(material) 275 | else: 276 | gltf_settings_node = self.add_gltf_material_output_node(material) 277 | 278 | 279 | # position 280 | self.position_gltf_setup_nodes(material,uv_node,image_texture_node,gltf_settings_node) 281 | 282 | # linking 283 | node_functions.make_link(material, uv_node.outputs["UV"],image_texture_node.inputs['Vector']) 284 | node_functions.make_link(material, image_texture_node.outputs['Color'], gltf_settings_node.inputs['Occlusion']) 285 | 286 | # LIGHTMAP 287 | if self.bake_settings.lightmap_bake: 288 | image_texture_node = self.add_image_texture_node(material) 289 | uv_node = self.add_uv_node(material) 290 | # position 291 | image_texture_node.location = mathutils.Vector((-500, 200)) 292 | uv_node.location = mathutils.Vector((-700, 200)) 293 | 294 | # linking 295 | node_functions.make_link(material, uv_node.outputs["UV"],image_texture_node.inputs['Vector']) 296 | 297 | def bake(self,bake_type): 298 | channels_to_bake = bake_type 299 | self.baked_images = [] 300 | denoise = self.bake_settings.denoise 301 | 302 | # no denoise 303 | if not denoise: 304 | channel = bake_type[0] 305 | image = self.bake_images(self.bake_image,channel,denoise) 306 | image_functions.save_image(image,False) 307 | return 308 | 309 | # bake channels for denoise 310 | for channel in channels_to_bake: 311 | image_name = self.bake_image.name + "_" + channel 312 | image = image_functions.create_image(image_name,self.bake_image.size) 313 | self.change_image_in_nodes(image) 314 | baked_channel_image = self.bake_images(image,channel,denoise) 315 | image_functions.save_image(image,True) 316 | self.baked_images.append(baked_channel_image) 317 | 318 | self.denoise() 319 | 320 | def change_image_in_nodes(self,image): 321 | for image_texture_node in self.image_texture_nodes: 322 | if image_texture_node.name == self.tex_node_name: 323 | image_texture_node.image = image 324 | 325 | def bake_images(self, image, channel,denoise): 326 | if channel == "NRM": 327 | print("Baking Normal Pass") 328 | self.C.scene.cycles.samples = 1 329 | self.O.object.bake(type="NORMAL", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 330 | 331 | if channel == "COLOR": 332 | print("Baking Color Pass") 333 | self.C.scene.cycles.samples = 1 334 | self.O.object.bake(type="DIFFUSE", pass_filter={'COLOR'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 335 | 336 | if channel == "AO": 337 | if not denoise: 338 | self.O.object.bake('INVOKE_DEFAULT',type="AO", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 339 | else: 340 | self.O.object.bake(type="AO", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 341 | 342 | if channel == "NOISY": 343 | print("Baking Diffuse Pass") 344 | if not denoise: 345 | self.O.object.bake('INVOKE_DEFAULT',type="DIFFUSE", pass_filter={'DIRECT', 'INDIRECT'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 346 | else: 347 | self.O.object.bake(type="DIFFUSE", pass_filter={'DIRECT', 'INDIRECT'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin) 348 | 349 | return image 350 | 351 | def denoise(self): 352 | # denoise 353 | if self.bake_settings.lightmap_bake: 354 | denoised_image_path = node_functions.comp_ai_denoise(self.baked_images[0],self.baked_images[1],self.baked_images[2]) 355 | 356 | self.bake_image.filepath = denoised_image_path 357 | self.bake_image.source = "FILE" 358 | 359 | self.change_image_in_nodes(self.bake_image) 360 | 361 | # blur 362 | if self.bake_settings.ao_bake and self.bake_settings.denoise: 363 | blur_image_path = node_functions.blur_bake_image(self.baked_images[0],self.baked_images[1]) 364 | self.bake_image.filepath = blur_image_path 365 | self.bake_image.source = "FILE" 366 | self.change_image_in_nodes(self.bake_image) 367 | 368 | def add_lightmap_flag(self): 369 | for obj in self.selected_objects: 370 | obj.hasLightmap = True 371 | 372 | def cleanup(self): 373 | # set back engine 374 | # self.C.scene.render.engine = self.render_engine 375 | self.C.preferences.addons['cycles'].preferences.compute_device_type = self.cycles_device_type 376 | 377 | # cleanup images 378 | if self.bake_settings.cleanup_textures: 379 | for img in self.D.images: 380 | if self.bake_image.name in img.name and ("_COLOR" in img.name or "_NRM" in img.name or "_NOISY" in img.name) : 381 | self.D.images.remove(img) 382 | 383 | # show image 384 | visibility_functions.show_image_in_image_editor(self.bake_image) 385 | 386 | 387 | class PbrBakeUtilities(BakeUtilities): 388 | active_material = None 389 | parent_operator = None 390 | 391 | def __init__(self,parent_operator,selected_objects, bake_settings): 392 | super().__init__(parent_operator,selected_objects,bake_settings) 393 | self.selected_materials = material_functions.get_selected_materials(selected_objects) 394 | self.parent_operator = parent_operator 395 | 396 | def ready_for_bake(self,material): 397 | 398 | # check if not baked material 399 | if "_Bake" in material.name: 400 | print("Skipping cause already baked : " + material.name) 401 | return False 402 | 403 | print("\n Checking " + material.name + "\n") 404 | 405 | # check if renderer not set to optix 406 | self.setup_engine() 407 | 408 | # check if selected to active is on 409 | bpy.context.scene.render.bake.use_selected_to_active = False 410 | 411 | # check if pbr node exists 412 | check_ok = node_functions.check_pbr(self.parent_operator,material) and node_functions.check_is_org_material(self.parent_operator,material) 413 | if not check_ok : 414 | self.parent_operator.report({'INFO'}, "Material " + material.name + " has no PBR Node !") 415 | return False 416 | 417 | # copy texture nodes if they are linked multiple times 418 | nodes = material.node_tree.nodes 419 | image_textrure_nodes = node_functions.get_nodes_by_type(nodes,constants.Node_Types.image_texture) 420 | for image_texture_node in image_textrure_nodes: 421 | node_functions.remove_double_linking(material,image_texture_node) 422 | return True 423 | 424 | 425 | 426 | def bake_materials_on_object(self): 427 | for material in self.selected_materials: 428 | self.active_material = material 429 | if not (self.ready_for_bake(material)): 430 | continue 431 | self.add_bake_plane() 432 | self.bake_pbr() 433 | self.create_pbr_bake_material("_Bake") 434 | self.create_nodes_after_pbr_bake() 435 | self.cleanup_nodes() 436 | 437 | visibility_functions.switch_baked_material(True,"visible") 438 | 439 | def add_bake_plane(self): 440 | material = self.active_material 441 | bake_plane = self.D.objects.get(material.name + "_Bake") 442 | 443 | if bake_plane is not None: 444 | self.parent_operator.report({'INFO'}, 'Delete Bake Plane') 445 | return 446 | 447 | self.O.mesh.primitive_plane_add(size=2, location=(2, 0, 0)) 448 | bake_plane = self.C.object 449 | bake_plane.name = material.name + "_Bake" 450 | bake_plane.data.materials.append(material) 451 | 452 | 453 | def bake_pbr(self): 454 | material = self.active_material 455 | material.use_fake_user = True 456 | 457 | nodes = material.node_tree.nodes 458 | pbr_node = node_functions.get_pbr_node(material) 459 | pbr_inputs = node_functions.get_pbr_inputs(pbr_node) 460 | image_texture_node = None 461 | 462 | # mute texture mapping 463 | if self.bake_settings.mute_texture_nodes: 464 | node_functions.mute_all_texture_mappings(material, True) 465 | 466 | for pbr_input in pbr_inputs.values(): 467 | 468 | # -----------------------TESTING--------------------# 469 | # skip if input has no connection 470 | if not pbr_input.is_linked: 471 | continue 472 | 473 | # -----------------------IMAGE --------------------# 474 | 475 | image_name = material.name + "_" + pbr_input.name 476 | 477 | # find image 478 | bake_image = self.D.images.get(image_name) 479 | 480 | # remove image 481 | if bake_image is not None: 482 | self.D.images.remove(bake_image) 483 | 484 | bake_image = self.D.images.new(image_name, width=self.image_size[0], height=self.image_size[1]) 485 | bake_image.name = image_name 486 | 487 | image_texture_node = node_functions.add_node(material,constants.Shader_Node_Types.image_texture,"PBR Bake") 488 | 489 | image_texture_node.image = bake_image 490 | nodes.active = image_texture_node 491 | 492 | # -----------------------SET COLOR SPACE--------------------# 493 | if pbr_input is not pbr_inputs["base_color_input"]: 494 | bake_image.colorspace_settings.name = "Non-Color" 495 | 496 | # -----------------------BAKING--------------------# 497 | if pbr_input is pbr_inputs["normal_input"]: 498 | node_functions.link_pbr_to_output(material, pbr_node) 499 | self.O.object.bake(type="NORMAL", use_clear=True) 500 | else: 501 | node_functions.emission_setup(material, pbr_input.links[0].from_socket) 502 | self.O.object.bake(type="EMIT", use_clear=True) 503 | 504 | # unmute texture mappings 505 | node_functions.mute_all_texture_mappings(material, False) 506 | 507 | # delete plane 508 | self.O.object.delete() 509 | 510 | # cleanup nodes 511 | node_functions.remove_node(material,"Emission Bake") 512 | node_functions.remove_node(material,"PBR Bake") 513 | node_functions.reconnect_PBR(material, pbr_node) 514 | 515 | def create_pbr_bake_material(self,material_name_suffix): 516 | 517 | # -----------------------CREATE MATERIAL--------------------# 518 | org_material = self.active_material 519 | bake_material_name = org_material.name + material_name_suffix 520 | bake_material = bpy.data.materials.get(bake_material_name) 521 | 522 | if bake_material is not None: 523 | bpy.data.materials.remove(bake_material) 524 | 525 | # and create new from org. material 526 | bake_material = org_material.copy() 527 | bake_material.name = bake_material_name 528 | self.bake_material = bake_material 529 | 530 | def create_nodes_after_pbr_bake(self): 531 | # -----------------------SETUP VARS--------------------# 532 | org_material = self.active_material 533 | bake_material = self.bake_material 534 | nodes = bake_material.node_tree.nodes 535 | pbr_node = node_functions.get_pbr_node(bake_material) 536 | pbr_inputs = node_functions.get_pbr_inputs(pbr_node) 537 | 538 | for pbr_input in pbr_inputs.values(): 539 | 540 | if not pbr_input.is_linked: 541 | continue 542 | 543 | # -----------------------REPLACE IMAGE TEXTURES--------------------# 544 | first_node_after_input = pbr_input.links[0].from_node 545 | tex_node = node_functions.get_node_by_type_recusivly(bake_material,first_node_after_input,constants.Node_Types.image_texture,True) 546 | 547 | bake_image_name = org_material.name + "_" + pbr_input.name 548 | bake_image = self.D.images.get(bake_image_name) 549 | 550 | # if no texture node found (baking procedural textures) add new one 551 | if tex_node is None: 552 | tex_node = node_functions.add_node(bake_material,constants.Shader_Node_Types.image_texture,bake_image_name) 553 | 554 | # keep org image if nothing changed 555 | if bake_image is None: 556 | org_image = self.D.images.get(tex_node.image.org_image_name) 557 | tex_node.image = org_image 558 | else: 559 | image_functions.save_image(bake_image) 560 | tex_node.image = bake_image 561 | 562 | # -----------------------LINKING--------------------# 563 | if pbr_input is pbr_inputs["normal_input"]: 564 | normal_node = first_node_after_input 565 | normal_node.inputs["Strength"].default_value = 1 566 | 567 | 568 | if normal_node.type == constants.Node_Types.bump_map: 569 | bump_node = normal_node 570 | normal_node = node_functions.add_node(bake_material,constants.Shader_Node_Types.normal,"Normal from Bump") 571 | normal_node.location = bump_node.location 572 | nodes.remove(bump_node) 573 | 574 | node_functions.make_link(bake_material, tex_node.outputs[0], normal_node.inputs["Color"]) 575 | node_functions.make_link(bake_material, normal_node.outputs["Normal"], pbr_input) 576 | else: 577 | node_functions.make_link(bake_material,tex_node.outputs[0], pbr_input) 578 | 579 | # -----------------------SET COLOR SPACE--------------------# 580 | if pbr_input is not pbr_inputs["base_color_input"]: 581 | tex_node.image.colorspace_settings.name = "Non-Color" 582 | 583 | self.active_material = bake_material 584 | return bake_material 585 | 586 | def cleanup_nodes(self): 587 | bake_material = self.active_material 588 | node_functions.remove_unused_nodes(bake_material) --------------------------------------------------------------------------------