disable (currently unused) addons, update

This commit is contained in:
Nathan
2026-07-16 12:41:53 -06:00
parent e3310263f3
commit 0f26db3345
76 changed files with 19896 additions and 6197 deletions
+2 -2
View File
@@ -10,6 +10,8 @@ D:\Work\9 iClone\Amazon\
D:\Amazon\00_external-files\
N:\1. CHARACTERS\remapping\
[Recent]
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\animations\
T:\Amazon\active\1 BlenderAssets\
F:\renders\
C:\Users\Nathan\Downloads\
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Deliverable\Stills\
@@ -18,5 +20,3 @@ A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Deliverable\Scan Aw
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\Scan Aware\
A:\1 Amazon_Active_Projects\1 BlenderAssets\
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\BD2\
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\
+1 -1
View File
@@ -1,3 +1,4 @@
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\animations\Dual-induct_animation 1_horizontal.blend
C:\Users\Nathan\Downloads\test.blend
A:\1 Amazon_Active_Projects\260709_Optimal_ADTA_stow\Blends\animations\OptimalStow_animation 2.blend
F:\jobs\OptimalStow_animation 2-1bxe\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Hailey_v4.4.blend
@@ -197,4 +198,3 @@ G:\Amazon\2024\240402_Amazon_Coaching&Upstream\Upstream\blends\mod 2\animation 1
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M5_6.blend
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M2_5.blend
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M5_13.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\comp_RR\Visual 2B_SPA_comp_RR.blend
Binary file not shown.
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,12 +0,0 @@
schema_version = "1.0.0"
id = "looptools"
name = "LoopTools"
version = "4.7.7"
tagline = "Mesh modelling toolkit. Several tools to aid modelling"
maintainer = "Community"
type = "add-on"
tags = ["Mesh"]
blender_version_min = "4.2.0"
license = ["SPDX:GPL-2.0-or-later"]
website = "https://projects.blender.org/extensions/mesh_looptools"
copyright = ["2024 Bart Crouch", "2024 Vladimir Spivak (cwolf3d)"]
@@ -54,13 +54,13 @@
"id": "rainclouds_bulk_scene_tools",
"name": "Raincloud's Bulk Scene Tools",
"tagline": "Bulk utilities for optimizing scene data",
"version": "0.17.0",
"version": "0.18.0",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"blender_version_min": "4.5.0",
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
"permissions": {
"files": "Read and write external resources referenced by scenes"
@@ -70,9 +70,9 @@
"Workflow",
"Materials"
],
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.17.0/Rainys_Bulk_Scene_Tools.v0.17.0.zip",
"archive_size": 80981,
"archive_hash": "sha256:419433069465b45ea903bd7bb46d89aa28b9c96c541d587d5f3be651a762811f"
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/0.18.0/Rainys_Bulk_Scene_Tools.v0.18.0.zip",
"archive_size": 75599,
"archive_hash": "sha256:6653a01b9ac5822e51113609b5a2970102715ec5f7816a9dccd80ecb4c755ea5"
},
{
"schema_version": "1.0.0",
@@ -92,7 +92,7 @@
"submission",
"utility"
],
"archive_url": "https://github.com/RaincloudTheDragon/sheepit_project_submitter/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_url": "https://github.com/RaincloudTheDragon/based-blendfile-packer/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_size": 47667,
"archive_hash": "sha256:93cd8f18456079130c48c66cfd40235f7fe6414f929f59f90670e7a864821110"
}
@@ -8,7 +8,6 @@ from .panels import bulk_scene_general
from .ops.AutoMatExtractor import RBST_AutoMat_OT_AutoMatExtractor, RBST_AutoMat_OT_summary_dialog
from .ops.Rename_images_by_mat import RBST_RenameImg_OT_Rename_images_by_mat, RBST_RenameImg_OT_summary_dialog
from .ops.FreeGPU import RBST_FreeGPU
from .ops import ghost_buster
from . import rainys_repo_bootstrap
from .utils import compat
@@ -77,7 +76,6 @@ def register():
bulk_viewport_display.register()
bulk_data_remap.register()
bulk_path_management.register()
ghost_buster.register()
# Add keybind for Free GPU (global context)
wm = bpy.context.window_manager
@@ -106,10 +104,6 @@ def unregister():
delattr(bpy.types.Scene, '_bst_keymaps')
# Unregister modules
try:
ghost_buster.unregister()
except Exception:
pass
try:
bulk_path_management.unregister()
except Exception:
@@ -3,12 +3,12 @@ schema_version = "1.0.0"
id = "rainclouds_bulk_scene_tools"
name = "Raincloud's Bulk Scene Tools"
tagline = "Bulk utilities for optimizing scene data"
version = "0.17.0"
version = "0.18.0"
type = "add-on"
maintainer = "RaincloudTheDragon <raincloudthedragon@gmail.com>"
license = ["GPL-3.0-or-later"]
blender_version_min = "4.2.0"
blender_version_min = "4.5.0"
website = "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools"
@@ -1,3 +1,9 @@
# v0.18.0 2026-07-14
- Official support: Blender 4.5 and 5.2 LTS only; minimum version raised to 4.5 (#9)
- Bulk Data Remap: native node group rename/merge for numbered duplicates (#16)
- Ghost Buster removed; use Atomic instead (#17)
- Remapper UI: larger Purge Unused button, fixed post-Ghost-Buster spacing
# v0.17.0 2026-03-13
- Remove Action FU: new operator in Animation Data section—clears fake users from all actions so only used animations are kept
@@ -4,7 +4,7 @@ from ..utils import version
def _collect_keyframe_stats(action):
"""Return (total_keyframes, keyframe_frames_set). Compatible with 4.2/4.5 (action.fcurves) and 5.0 (layers/strips/channelbags)."""
"""Return (total_keyframes, keyframe_frames_set). Compatible with 4.5 (action.fcurves) and 5.0+ (layers/strips/channelbags)."""
keyframe_frames = set()
total_keyframes = 0
if version.is_version_less_than(5, 0, 0):
@@ -1,688 +0,0 @@
import bpy
from ..utils import compat
def safe_wgt_removal():
"""Safely remove only WGT widget objects that are clearly ghosts"""
print("="*80)
print("CONSERVATIVE WGT GHOST REMOVAL")
print("="*80)
# Find all WGT objects
wgt_objects = []
for obj in bpy.data.objects:
if obj.name.startswith('WGT-'):
wgt_objects.append(obj)
print(f"Found {len(wgt_objects)} WGT objects")
# Check which ones are actually being used by armatures
used_wgts = set()
for armature in bpy.data.armatures:
for bone in armature.bones:
if bone.use_deform and hasattr(bone, 'custom_shape') and bone.custom_shape:
used_wgts.add(bone.custom_shape.name)
print(f"Found {len(used_wgts)} WGT objects actually used by armatures")
# Remove unused WGT objects
removed_wgts = 0
for obj in wgt_objects:
if obj.name not in used_wgts:
try:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
print(f" Skipping linked WGT: {obj.name} (from {obj.library.name})")
continue
# Check if it's in the WGTS collection (typical ghost pattern)
in_wgts_collection = False
for collection in bpy.data.collections:
if 'WGTS' in collection.name and obj in collection.objects.values():
in_wgts_collection = True
break
if in_wgts_collection:
print(f" Removing unused WGT: {obj.name}")
bpy.data.objects.remove(obj, do_unlink=True)
removed_wgts += 1
except Exception as e:
print(f" Failed to remove {obj.name}: {e}")
print(f"Removed {removed_wgts} unused WGT objects")
return removed_wgts
def is_collection_in_scene_hierarchy(collection, scene_collection):
"""Recursively check if a collection exists anywhere in the scene collection hierarchy"""
if collection == scene_collection:
return True
for child_collection in scene_collection.children:
if child_collection == collection:
return True
if is_collection_in_scene_hierarchy(collection, child_collection):
return True
return False
def clean_empty_collections():
"""Remove empty collections that are not linked to scenes"""
print("\n" + "="*80)
print("CLEANING EMPTY COLLECTIONS")
print("="*80)
removed_collections = 0
collections_to_remove = []
for collection in bpy.data.collections:
# Check if collection is empty
if len(collection.objects) == 0 and len(collection.children) == 0:
# Skip linked collections (they're legitimate library content)
if hasattr(collection, 'library') and collection.library is not None:
print(f" Skipping linked empty collection: {collection.name}")
continue
# Check if it's anywhere in any scene's collection hierarchy
linked_to_scene = False
for scene in bpy.data.scenes:
if is_collection_in_scene_hierarchy(collection, scene.collection):
linked_to_scene = True
print(f" Preserving empty collection: {collection.name} (in scene '{scene.name}')")
break
if not linked_to_scene:
collections_to_remove.append(collection)
for collection in collections_to_remove:
try:
print(f" Removing empty collection: {collection.name}")
bpy.data.collections.remove(collection)
removed_collections += 1
except Exception as e:
print(f" Failed to remove collection {collection.name}: {e}")
print(f"Removed {removed_collections} empty collections")
return removed_collections
def is_object_used_by_scene_instance_collections(obj):
"""Check if object is in a collection that's being instanced by objects in scenes"""
# Find all collections that contain this object
obj_collections = []
for collection in bpy.data.collections:
if obj in collection.objects.values():
obj_collections.append(collection)
if not obj_collections:
return False
# Check if any of these collections are being instanced by objects in scenes
for collection in obj_collections:
# Find objects that instance this collection
for other_obj in bpy.data.objects:
if (other_obj.instance_type == 'COLLECTION' and
other_obj.instance_collection == collection):
# Check if the instancing object is in any scene
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
return True
return False
def is_object_legitimate_outside_scene(obj):
"""Check if an object has legitimate reasons to exist outside scenes"""
# WGT objects (rig widgets) are legitimate outside scenes
if obj.name.startswith('WGT-'):
return True
# Collection instance objects (linked collection references) are legitimate
if obj.instance_type == 'COLLECTION' and obj.instance_collection is not None:
return True
# Objects that are being used by instance collections in scenes are legitimate
if is_object_used_by_scene_instance_collections(obj):
return True
# Objects used as curve modifiers, constraints targets, etc.
# Check if object is used by modifiers on other objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for modifier in other_obj.modifiers:
if hasattr(modifier, 'object') and modifier.object == obj:
return True
if hasattr(modifier, 'target') and modifier.target == obj:
return True
# Check if object is used by constraints on other objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for constraint in other_obj.constraints:
if hasattr(constraint, 'target') and constraint.target == obj:
return True
if hasattr(constraint, 'subtarget') and constraint.subtarget == obj.name:
return True
# Check if object is used in particle systems on objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for modifier in other_obj.modifiers:
if modifier.type == 'PARTICLE_SYSTEM':
settings = modifier.particle_system.settings
if hasattr(settings, 'object') and settings.object == obj:
return True
if hasattr(settings, 'instance_object') and settings.instance_object == obj:
return True
return False
def clean_object_ghosts(delete_low_priority=False):
"""Remove objects that are not in any scene and have no legitimate purpose (potential ghosts)"""
print("\n" + "="*80)
print("OBJECT GHOST CLEANUP")
print("="*80)
# Get all objects, excluding cameras and lights by default (they're often not in scenes for good reasons)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
if not candidate_objects:
print("No candidate objects found")
return 0
print(f"Found {len(candidate_objects)} candidate objects")
removed_objects = 0
ghosts_to_remove = []
for obj in candidate_objects:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
# If not in any scene, check if it has legitimate reasons to exist
if len(in_scenes) == 0:
if is_object_legitimate_outside_scene(obj):
print(f" Preserving object: {obj.name} (legitimate use outside scene)")
continue
# If not legitimate, it's a ghost - but be conservative with low user count objects
should_remove = False
removal_reason = ""
if obj.users >= 2:
# Higher user count ghosts are definitely safe to remove
should_remove = True
removal_reason = "ghost (users >= 2, no legitimate use found)"
elif obj.users < 2 and delete_low_priority:
# Low user count ghosts only if user enables the option
should_remove = True
removal_reason = "low priority ghost (users < 2, no legitimate use found)"
elif obj.users < 2:
print(f" Skipping low priority object: {obj.name} (users < 2, enable 'Delete Low Priority' to remove)")
if should_remove:
ghosts_to_remove.append(obj)
print(f" Marking ghost for removal: {obj.name} (type: {obj.type}) - {removal_reason}")
# Remove the ghost objects
for obj in ghosts_to_remove:
try:
print(f" Removing object ghost: {obj.name}")
bpy.data.objects.remove(obj, do_unlink=True)
removed_objects += 1
except Exception as e:
print(f" Failed to remove object {obj.name}: {e}")
print(f"Removed {removed_objects} ghost objects")
return removed_objects
def manual_object_analysis():
"""Manual analysis of objects - show info but don't auto-remove"""
print("\n" + "="*80)
print("OBJECT GHOST ANALYSIS (MANUAL REVIEW)")
print("="*80)
# Get all objects, excluding cameras and lights (they're often legitimately not in scenes)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
# Filter to only objects not in scenes for analysis
objects_not_in_scenes = []
for obj in candidate_objects:
# Skip linked objects for analysis
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
if len(in_scenes) == 0:
objects_not_in_scenes.append(obj)
if not objects_not_in_scenes:
print("No local objects found outside scenes")
return
print(f"Found {len(objects_not_in_scenes)} local objects not in any scene:")
for obj in objects_not_in_scenes:
print(f"\n Object: {obj.name} (type: {obj.type})")
print(f" Users: {obj.users}")
print(f" Parent: {obj.parent.name if obj.parent else 'None'}")
# Check collections
in_collections = []
for collection in bpy.data.collections:
if obj in collection.objects.values():
in_collections.append(collection.name)
print(f" In collections: {in_collections}")
# Show recommendation
if is_object_legitimate_outside_scene(obj):
print(f" -> LEGITIMATE: Has valid use outside scenes")
elif obj.users >= 2:
print(f" -> GHOST: No legitimate use found, users >= 2 (will be removed)")
elif obj.users < 2:
print(f" -> LOW PRIORITY: No legitimate use found, users < 2 (needs option enabled)")
else:
print(f" -> UNCLEAR: Manual review needed")
def main(delete_low_priority=False):
"""Main conservative cleanup function"""
print("CONSERVATIVE GHOST DATA CLEANUP")
print("="*80)
print("This script removes:")
print("1. Unused local WGT widget objects")
print("2. Empty unlinked collections")
print("3. Objects not in any scene with no legitimate use")
if delete_low_priority:
print(" - Including low priority ghosts (no legitimate use, users < 2)")
else:
print(" - Excluding low priority ghosts (no legitimate use, users < 2)")
print("="*80)
initial_objects = len(list(bpy.data.objects))
initial_collections = len(list(bpy.data.collections))
# Safe operations only
wgts_removed = safe_wgt_removal()
collections_removed = clean_empty_collections()
object_ghosts_removed = clean_object_ghosts(delete_low_priority)
# Show remaining object analysis
manual_object_analysis()
# Final purge
print("\n" + "="*80)
print("FINAL SAFE PURGE")
print("="*80)
try:
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
print("Safe purge completed")
except:
print("Purge had issues")
final_objects = len(list(bpy.data.objects))
final_collections = len(list(bpy.data.collections))
print(f"\n" + "="*80)
print("CONSERVATIVE CLEANUP SUMMARY")
print("="*80)
print(f"Objects: {initial_objects} -> {final_objects} (removed {initial_objects - final_objects})")
print(f"Collections: {initial_collections} -> {final_collections} (removed {collections_removed})")
print(f"WGT objects removed: {wgts_removed}")
print(f"Object ghosts removed: {object_ghosts_removed}")
print("="*80)
class RBST_Bustin_OT_GhostBuster(bpy.types.Operator):
"""Conservative cleanup of ghost data (unused WGT objects, empty collections)"""
bl_idname = "bst.ghost_buster"
bl_label = "Ghost Buster"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
# Get the delete low priority setting from scene properties
delete_low_priority = getattr(context.scene, "ghost_buster_delete_low_priority", False)
# Call the main ghost buster function
main(delete_low_priority)
self.report({'INFO'}, "Ghost data cleanup completed")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Ghost buster failed: {str(e)}")
return {'CANCELLED'}
class RBST_Bustin_OT_GhostDetector(bpy.types.Operator):
"""Detect and analyze ghost data without removing it"""
bl_idname = "bst.ghost_detector"
bl_label = "Ghost Detector"
bl_options = {'REGISTER', 'INTERNAL'}
# Properties to store analysis data
total_wgt_objects: bpy.props.IntProperty(default=0)
unused_wgt_objects: bpy.props.IntProperty(default=0)
used_wgt_objects: bpy.props.IntProperty(default=0)
empty_collections: bpy.props.IntProperty(default=0)
ghost_objects: bpy.props.IntProperty(default=0)
ghost_potential: bpy.props.IntProperty(default=0)
ghost_legitimate: bpy.props.IntProperty(default=0)
ghost_low_priority: bpy.props.IntProperty(default=0)
wgt_details: bpy.props.StringProperty(default="")
collection_details: bpy.props.StringProperty(default="")
ghost_details: bpy.props.StringProperty(default="")
def analyze_ghost_data(self):
"""Analyze ghost data similar to ghost_buster functions"""
# Analyze WGT objects
wgt_objects = []
for obj in bpy.data.objects:
if obj.name.startswith('WGT-'):
wgt_objects.append(obj)
self.total_wgt_objects = len(wgt_objects)
# Check which WGT objects are used by armatures
used_wgts = set()
for armature in bpy.data.armatures:
for bone in armature.bones:
if bone.use_deform and hasattr(bone, 'custom_shape') and bone.custom_shape:
used_wgts.add(bone.custom_shape.name)
self.used_wgt_objects = len(used_wgts)
# Count unused WGT objects
unused_wgts = []
wgt_details_list = []
for obj in wgt_objects:
if obj.name not in used_wgts:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check if it's in the WGTS collection (typical ghost pattern)
in_wgts_collection = False
for collection in bpy.data.collections:
if 'WGTS' in collection.name and obj in collection.objects.values():
in_wgts_collection = True
break
if in_wgts_collection:
unused_wgts.append(obj)
wgt_details_list.append(f"{obj.name} (in WGTS collection)")
self.unused_wgt_objects = len(unused_wgts)
self.wgt_details = "\n".join(wgt_details_list[:10]) # Limit to first 10
if len(unused_wgts) > 10:
self.wgt_details += f"\n... and {len(unused_wgts) - 10} more"
# Analyze empty collections
empty_collections = []
collection_details_list = []
for collection in bpy.data.collections:
if len(collection.objects) == 0 and len(collection.children) == 0:
# Skip linked collections (they're legitimate library content)
if hasattr(collection, 'library') and collection.library is not None:
continue
# Check if it's anywhere in any scene's collection hierarchy
linked_to_scene = False
for scene in bpy.data.scenes:
if is_collection_in_scene_hierarchy(collection, scene.collection):
linked_to_scene = True
break
if not linked_to_scene:
empty_collections.append(collection)
collection_details_list.append(f"{collection.name}")
self.empty_collections = len(empty_collections)
self.collection_details = "\n".join(collection_details_list[:10]) # Limit to first 10
if len(empty_collections) > 10:
self.collection_details += f"\n... and {len(empty_collections) - 10} more"
# Analyze ghost objects (objects not in scenes)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
potential_ghosts = 0
legitimate = 0
low_priority = 0
ghost_details_list = []
for obj in candidate_objects:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
# Only analyze objects not in scenes
if len(in_scenes) == 0:
# Classify object
status = ""
if is_object_legitimate_outside_scene(obj):
legitimate += 1
status = "LEGITIMATE (has valid use outside scenes)"
elif obj.users >= 2:
potential_ghosts += 1
status = "GHOST (no legitimate use found, users >= 2)"
elif obj.users < 2:
low_priority += 1
status = "LOW PRIORITY (no legitimate use found, users < 2)"
else:
status = "UNCLEAR"
ghost_details_list.append(f"{obj.name} ({obj.type}): {status}")
self.ghost_objects = len([obj for obj in candidate_objects if len([s for s in bpy.data.scenes if obj in s.objects.values()]) == 0 and not (hasattr(obj, 'library') and obj.library is not None)])
self.ghost_potential = potential_ghosts
self.ghost_legitimate = legitimate
self.ghost_low_priority = low_priority
self.ghost_details = "\n".join(ghost_details_list[:10]) # Limit to first 10
if len(ghost_details_list) > 10:
self.ghost_details += f"\n... and {len(ghost_details_list) - 10} more"
def draw(self, context):
layout = self.layout
# Title
layout.label(text="Ghost Data Analysis", icon='GHOST_ENABLED')
layout.separator()
# WGT Objects section
box = layout.box()
box.label(text="WGT Widget Objects", icon='ARMATURE_DATA')
col = box.column(align=True)
col.label(text=f"Total WGT objects: {self.total_wgt_objects}")
col.label(text=f"Used by armatures: {self.used_wgt_objects}", icon='CHECKMARK')
if self.unused_wgt_objects > 0:
col.label(text=f"Unused (potential ghosts): {self.unused_wgt_objects}", icon='ERROR')
if self.wgt_details:
box.separator()
details_col = box.column(align=True)
for line in self.wgt_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No unused WGT objects found", icon='CHECKMARK')
# Empty Collections section
box = layout.box()
box.label(text="Empty Collections", icon='OUTLINER_COLLECTION')
col = box.column(align=True)
if self.empty_collections > 0:
col.label(text=f"Empty unlinked collections: {self.empty_collections}", icon='ERROR')
if self.collection_details:
box.separator()
details_col = box.column(align=True)
for line in self.collection_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No empty unlinked collections found", icon='CHECKMARK')
# Ghost Objects section
box = layout.box()
box.label(text="Ghost Objects Analysis", icon='OBJECT_DATA')
col = box.column(align=True)
col.label(text=f"Objects not in scenes: {self.ghost_objects}")
if self.ghost_objects > 0:
if self.ghost_potential > 0:
col.label(text=f"Ghosts (users >= 2): {self.ghost_potential}", icon='ERROR')
if self.ghost_legitimate > 0:
col.label(text=f"Legitimate objects: {self.ghost_legitimate}", icon='CHECKMARK')
if self.ghost_low_priority > 0:
col.label(text=f"Low priority (users < 2): {self.ghost_low_priority}", icon='QUESTION')
if self.ghost_details:
box.separator()
details_col = box.column(align=True)
for line in self.ghost_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No ghost objects found", icon='CHECKMARK')
# Summary
layout.separator()
summary_box = layout.box()
summary_box.label(text="Summary", icon='INFO')
total_issues = self.unused_wgt_objects + self.empty_collections + self.ghost_potential
if total_issues > 0:
summary_box.label(text=f"Found {total_issues} ghost data issues that will be removed", icon='ERROR')
if self.ghost_low_priority > 0:
summary_box.label(text=f"+ {self.ghost_low_priority} low priority issues (optional)", icon='QUESTION')
summary_box.label(text="Use Ghost Buster to clean up safely")
else:
summary_box.label(text="No ghost data issues detected!", icon='CHECKMARK')
if self.ghost_low_priority > 0:
summary_box.label(text=f"({self.ghost_low_priority} low priority issues available)", icon='INFO')
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
# Analyze the ghost data before showing the dialog
self.analyze_ghost_data()
return context.window_manager.invoke_popup(self, width=500)
class RBST_Bustin_OT_ResyncEnforce(bpy.types.Operator):
"""Resync Enforce: Fix broken library override hierarchies by rebuilding from linked references"""
bl_idname = "bst.resync_enforce"
bl_label = "Resync Enforce"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
# Only available if there are selected objects
return context.selected_objects
def execute(self, context):
# Get selected objects
selected_objects = context.selected_objects.copy()
if not selected_objects:
self.report({'WARNING'}, "No objects selected for resync enforce")
return {'CANCELLED'}
# Count library override objects
override_objects = []
for obj in selected_objects:
if obj.override_library:
override_objects.append(obj)
if not override_objects:
self.report({'WARNING'}, "No library override objects found in selection")
return {'CANCELLED'}
try:
# Store the current selection
original_selection = set(context.selected_objects)
# Select only the override objects
bpy.ops.object.select_all(action='DESELECT')
for obj in override_objects:
obj.select_set(True)
# Call Blender's resync enforce operation
result = bpy.ops.object.library_override_operation(
'INVOKE_DEFAULT',
type='OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE',
selection_set='SELECTED'
)
if result == {'FINISHED'}:
self.report({'INFO'}, f"Resync enforce completed on {len(override_objects)} override objects")
return_code = {'FINISHED'}
else:
self.report({'WARNING'}, "Resync enforce operation was cancelled or failed")
return_code = {'CANCELLED'}
# Restore original selection
bpy.ops.object.select_all(action='DESELECT')
for obj in original_selection:
if obj.name in bpy.data.objects: # Check if object still exists
obj.select_set(True)
return return_code
except Exception as e:
self.report({'ERROR'}, f"Resync enforce failed: {str(e)}")
return {'CANCELLED'}
# Note: main() is called by the operator, not automatically
# List of classes to register
classes = (
RBST_Bustin_OT_GhostBuster,
RBST_Bustin_OT_GhostDetector,
RBST_Bustin_OT_ResyncEnforce,
)
def register():
for cls in classes:
compat.safe_register_class(cls)
def unregister():
for cls in reversed(classes):
compat.safe_unregister_class(cls)
@@ -4,8 +4,6 @@ import os
import sys
import subprocess
# Import ghost buster functionality
from ..ops.ghost_buster import RBST_Bustin_OT_GhostBuster, RBST_Bustin_OT_GhostDetector, RBST_Bustin_OT_ResyncEnforce
from ..utils import compat
# Regular expression to match numbered suffixes like .001, .002, _001, _0001, etc.
@@ -45,6 +43,12 @@ def RBST_DatRem_register_properties():
default=True
)
bpy.types.Scene.dataremap_node_groups = bpy.props.BoolProperty( # type: ignore
name="Node Groups",
description="Find and remap duplicate node groups",
default=True
)
# Add properties for showing duplicate lists
bpy.types.Scene.show_image_duplicates = bpy.props.BoolProperty( # type: ignore
name="Show Image Duplicates",
@@ -70,6 +74,12 @@ def RBST_DatRem_register_properties():
default=False
)
bpy.types.Scene.show_node_group_duplicates = bpy.props.BoolProperty( # type: ignore
name="Show Node Group Duplicates",
description="Show list of duplicate node groups",
default=False
)
# Sort by selected properties for each data type
bpy.types.Scene.dataremap_sort_images = bpy.props.BoolProperty( # type: ignore
name="Sort Images by Selected",
@@ -91,6 +101,11 @@ def RBST_DatRem_register_properties():
description="Show selected world groups at the top of the list",
default=False
)
bpy.types.Scene.dataremap_sort_node_groups = bpy.props.BoolProperty( # type: ignore
name="Sort Node Groups by Selected",
description="Show selected node group groups at the top of the list",
default=False
)
# Search filter properties for each data type
bpy.types.Scene.dataremap_search_images = bpy.props.StringProperty( # type: ignore
@@ -117,6 +132,12 @@ def RBST_DatRem_register_properties():
default=""
)
bpy.types.Scene.dataremap_search_node_groups = bpy.props.StringProperty( # type: ignore
name="Search Node Groups",
description="Filter node groups by name (case-insensitive)",
default=""
)
# Dictionary to store excluded groups
if not hasattr(bpy.types.Scene, "excluded_remap_groups"):
bpy.types.Scene.excluded_remap_groups = {}
@@ -128,23 +149,18 @@ def RBST_DatRem_register_properties():
# Store the last clicked group for shift-click range selection
if not hasattr(bpy.types.Scene, "last_clicked_group"):
bpy.types.Scene.last_clicked_group = {}
# Ghost Buster properties
bpy.types.Scene.ghost_buster_delete_low_priority = bpy.props.BoolProperty( # type: ignore
name="Delete Low Priority Ghosts",
description="Delete objects not in scenes with no legitimate use and users < 2",
default=False
)
def RBST_DatRem_unregister_properties():
del bpy.types.Scene.dataremap_images
del bpy.types.Scene.dataremap_materials
del bpy.types.Scene.dataremap_fonts
del bpy.types.Scene.dataremap_worlds
del bpy.types.Scene.dataremap_node_groups
del bpy.types.Scene.show_image_duplicates
del bpy.types.Scene.show_material_duplicates
del bpy.types.Scene.show_font_duplicates
del bpy.types.Scene.show_world_duplicates
del bpy.types.Scene.show_node_group_duplicates
if hasattr(bpy.types.Scene, "excluded_remap_groups"):
del bpy.types.Scene.excluded_remap_groups
if hasattr(bpy.types.Scene, "expanded_remap_groups"):
@@ -157,10 +173,7 @@ def RBST_DatRem_unregister_properties():
del bpy.types.Scene.dataremap_sort_materials
del bpy.types.Scene.dataremap_sort_fonts
del bpy.types.Scene.dataremap_sort_worlds
# Delete ghost buster properties
if hasattr(bpy.types.Scene, "ghost_buster_delete_low_priority"):
del bpy.types.Scene.ghost_buster_delete_low_priority
del bpy.types.Scene.dataremap_sort_node_groups
def RBST_DatRem_get_base_name(name):
"""Extract the base name without numbered suffix"""
@@ -194,6 +207,56 @@ def RBST_DatRem_find_data_groups(data_collection):
return {name: items for name, items in groups.items()
if len(items) > 1 and any(not (hasattr(item, 'library') and item.library is not None) for item in items)}
def RBST_DatRem_get_node_group_tree_label(node_group):
"""Return a short label for a node group's tree type."""
tree_id = getattr(node_group, 'bl_idname', '') or ''
if tree_id.endswith('NodeTree'):
return tree_id[:-8]
return tree_id or 'Unknown'
def RBST_DatRem_format_node_group_group_key(group_key):
"""Format a node group group key for display."""
if ':' not in group_key:
return group_key
tree_id, base_name = group_key.split(':', 1)
tree_label = tree_id.replace('NodeTree', '') if tree_id.endswith('NodeTree') else tree_id
return f"{base_name} ({tree_label})"
def RBST_DatRem_find_node_group_data_groups():
"""Group node groups by tree type and base name."""
groups = {}
for node_group in bpy.data.node_groups:
if node_group.users == 0:
continue
if hasattr(node_group, 'library') and node_group.library is not None:
continue
base_name = RBST_DatRem_get_base_name(node_group.name)
tree_id = getattr(node_group, 'bl_idname', 'UnknownNodeTree')
group_key = f"{tree_id}:{base_name}"
if group_key not in groups:
groups[group_key] = []
groups[group_key].append(node_group)
return {name: items for name, items in groups.items()
if len(items) > 1 and any(not (hasattr(item, 'library') and item.library is not None) for item in items)}
def RBST_DatRem_get_duplicate_groups(data_type):
"""Return duplicate groups for a remapper data type."""
if data_type == "images":
return RBST_DatRem_find_data_groups(bpy.data.images)
if data_type == "materials":
return RBST_DatRem_find_data_groups(bpy.data.materials)
if data_type == "fonts":
return RBST_DatRem_find_data_groups(bpy.data.fonts)
if data_type == "worlds":
return RBST_DatRem_find_data_groups(bpy.data.worlds)
if data_type == "node_groups":
return RBST_DatRem_find_node_group_data_groups()
return {}
def RBST_DatRem_find_target_data(data_group):
"""Find the target data block to remap to"""
# Filter out linked datablocks
@@ -242,7 +305,7 @@ def RBST_DatRem_clean_data_names(data_collection):
return cleaned_count
def RBST_DatRem_remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap_worlds):
def RBST_DatRem_remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap_worlds, remap_node_groups):
"""Remap redundant data blocks to their base versions like Blender's Remap Users function, and clean up names."""
remapped_count = 0
cleaned_count = 0
@@ -587,6 +650,35 @@ def RBST_DatRem_remap_data_blocks(context, remap_images, remap_materials, remap_
# Then clean up any remaining numbered suffixes
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.worlds)
# Process node groups
if remap_node_groups:
node_group_groups = RBST_DatRem_find_node_group_data_groups()
for group_key, node_groups in node_group_groups.items():
if f"node_groups:{group_key}" in context.scene.excluded_remap_groups:
continue
target_group = RBST_DatRem_find_target_data(node_groups)
if RBST_DatRem_get_base_name(target_group.name) != target_group.name:
try:
target_group.name = RBST_DatRem_get_base_name(target_group.name)
except AttributeError:
print(f"Warning: Cannot rename linked node group {target_group.name}")
continue
try:
for node_group in node_groups:
if node_group != target_group:
try:
node_group.user_remap(target_group)
remapped_count += 1
except AttributeError:
print(f"Warning: Cannot remap linked node group {node_group.name}")
except Exception as e:
print(f"Error using built-in remap for node groups: {e}")
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.node_groups)
# Force an update of the dependency graph to ensure all users are properly updated
if context.view_layer:
context.view_layer.update()
@@ -605,14 +697,20 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
remap_materials = context.scene.dataremap_materials
remap_fonts = context.scene.dataremap_fonts
remap_worlds = context.scene.dataremap_worlds
remap_node_groups = context.scene.dataremap_node_groups
# Count duplicates before remapping (only for local datablocks)
image_groups = RBST_DatRem_find_data_groups(bpy.data.images) if remap_images else {}
material_groups = RBST_DatRem_find_data_groups(bpy.data.materials) if remap_materials else {}
font_groups = RBST_DatRem_find_data_groups(bpy.data.fonts) if remap_fonts else {}
world_groups = RBST_DatRem_find_data_groups(bpy.data.worlds) if remap_worlds else {}
node_group_groups = RBST_DatRem_find_node_group_data_groups() if remap_node_groups else {}
total_duplicates = sum(len(group) - 1 for groups in [image_groups, material_groups, font_groups, world_groups] for group in groups.values())
total_duplicates = sum(
len(group) - 1
for groups in [image_groups, material_groups, font_groups, world_groups, node_group_groups]
for group in groups.values()
)
# Count data blocks with numbered suffixes (only for local datablocks)
total_numbered = 0
@@ -636,6 +734,11 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
if world.users > 0
and not (hasattr(world, 'library') and world.library is not None)
and RBST_DatRem_get_base_name(world.name) != world.name)
if remap_node_groups:
total_numbered += sum(1 for node_group in bpy.data.node_groups
if node_group.users > 0
and not (hasattr(node_group, 'library') and node_group.library is not None)
and RBST_DatRem_get_base_name(node_group.name) != node_group.name)
if total_duplicates == 0 and total_numbered == 0:
self.report({'INFO'}, "No local data blocks to process")
@@ -647,7 +750,8 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
remap_images,
remap_materials,
remap_fonts,
remap_worlds
remap_worlds,
remap_node_groups,
)
# Report results
@@ -808,15 +912,7 @@ class RBST_DatRem_OT_SelectAllGroups(bpy.types.Operator):
context.scene.excluded_remap_groups = {}
# Get the appropriate data groups based on data_type
data_groups = {}
if self.data_type == "images":
data_groups = RBST_DatRem_find_data_groups(bpy.data.images)
elif self.data_type == "materials":
data_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
elif self.data_type == "fonts":
data_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
elif self.data_type == "worlds":
data_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
data_groups = RBST_DatRem_get_duplicate_groups(self.data_type)
# Process only groups with more than one item
for base_name, items in data_groups.items():
@@ -873,15 +969,7 @@ class RBST_DatRem_OT_ToggleGroupSelection(bpy.types.Operator):
last_group = context.scene.last_clicked_group[self.data_type]
# Get all data groups for this data type
data_groups = []
if self.data_type == "images":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.images).keys())
elif self.data_type == "materials":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.materials).keys())
elif self.data_type == "fonts":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.fonts).keys())
elif self.data_type == "worlds":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.worlds).keys())
data_groups = list(RBST_DatRem_get_duplicate_groups(self.data_type).keys())
# Find the indices of the last clicked group and the current group
try:
@@ -966,6 +1054,10 @@ def RBST_DatRem_search_matches_group(group, search_string):
# Check base name
if search_lower in base_name.lower():
return True
if ':' in base_name:
formatted_name = RBST_DatRem_format_node_group_group_key(base_name)
if search_lower in formatted_name.lower():
return True
# Check all item names in group
for item in items:
if search_lower in item.name.lower():
@@ -1075,9 +1167,13 @@ def RBST_DatRem_draw_data_duplicates(layout, context, data_type, data_groups):
row.template_icon(icon_value=target_item.preview.icon_id, scale=1.0)
else:
row.label(text="", icon='WORLD')
elif data_type == "node_groups":
row.label(text="", icon='NODETREE')
display_name = RBST_DatRem_format_node_group_group_key(base_name) if data_type == "node_groups" else base_name
# Add rename operator for the group name
rename_op = row.operator("bst.rename_datablock_remap", text=f"{base_name}: {len(items)} versions", emboss=False)
rename_op = row.operator("bst.rename_datablock_remap", text=f"{display_name}: {len(items)} versions", emboss=False)
rename_op.data_type = data_type
rename_op.old_name = target_item.name
@@ -1112,9 +1208,15 @@ def RBST_DatRem_draw_data_duplicates(layout, context, data_type, data_groups):
sub_row.template_icon(icon_value=item.preview.icon_id, scale=1.0)
else:
sub_row.label(text="", icon='WORLD')
elif data_type == "node_groups":
sub_row.label(text="", icon='NODETREE')
item_label = item.name
if data_type == "node_groups":
item_label = f"{item.name} ({RBST_DatRem_get_node_group_tree_label(item)})"
# Add rename operator for each item
rename_op = sub_row.operator("bst.rename_datablock_remap", text=f"{item.name}", emboss=False)
rename_op = sub_row.operator("bst.rename_datablock_remap", text=item_label, emboss=False)
rename_op.data_type = data_type
rename_op.old_name = item.name
@@ -1160,9 +1262,14 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
linked_types.append("worlds")
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.worlds))
# Display warning about linked datablocks in a separate section if found
if context.scene.dataremap_node_groups and RBST_DatRem_has_linked_datablocks(bpy.data.node_groups):
linked_datablocks_found = True
linked_types.append("node groups")
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.node_groups))
# Display warning about linked datablocks inside the remapper box
if linked_datablocks_found:
warning_box = layout.box()
warning_box = box.box()
warning_box.alert = True
warning_box.label(text="Warning: Linked datablocks detected", icon='ERROR')
warning_box.label(text=f"Types: {', '.join(linked_types)}")
@@ -1192,16 +1299,19 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
material_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
font_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
world_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
node_group_groups = RBST_DatRem_find_node_group_data_groups()
image_duplicates = sum(len(group) - 1 for group in image_groups.values())
material_duplicates = sum(len(group) - 1 for group in material_groups.values())
font_duplicates = sum(len(group) - 1 for group in font_groups.values())
world_duplicates = sum(len(group) - 1 for group in world_groups.values())
node_group_duplicates = sum(len(group) - 1 for group in node_group_groups.values())
image_numbered = sum(1 for img in bpy.data.images if img.users > 0 and RBST_DatRem_get_base_name(img.name) != img.name)
material_numbered = sum(1 for mat in bpy.data.materials if mat.users > 0 and RBST_DatRem_get_base_name(mat.name) != mat.name)
font_numbered = sum(1 for font in bpy.data.fonts if font.users > 0 and RBST_DatRem_get_base_name(font.name) != font.name)
world_numbered = sum(1 for world in bpy.data.worlds if world.users > 0 and RBST_DatRem_get_base_name(world.name) != world.name)
node_group_numbered = sum(1 for node_group in bpy.data.node_groups if node_group.users > 0 and RBST_DatRem_get_base_name(node_group.name) != node_group.name)
# Initialize excluded_remap_groups if it doesn't exist
if not hasattr(context.scene, "excluded_remap_groups"):
@@ -1332,6 +1442,33 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
if context.scene.show_world_duplicates and world_duplicates > 0 and context.scene.dataremap_worlds:
RBST_DatRem_draw_data_duplicates(col, context, "worlds", world_groups)
# Node Groups
row = col.row()
split = row.split(factor=0.6)
sub_row = split.row()
op = sub_row.operator("bst.toggle_data_type", text="", icon='NODETREE', depress=context.scene.dataremap_node_groups)
op.data_type = "node_groups"
if context.scene.dataremap_node_groups:
sub_row.label(text="Node Groups")
else:
sub_row.label(text="Node Groups", icon='RADIOBUT_OFF')
sub_row = split.row()
if node_group_duplicates > 0:
sub_row.prop(context.scene, "show_node_group_duplicates",
text=f"{node_group_duplicates} duplicates",
icon='DISCLOSURE_TRI_DOWN' if context.scene.show_node_group_duplicates else 'DISCLOSURE_TRI_RIGHT',
emboss=False)
elif node_group_numbered > 0:
sub_row.label(text=f"{node_group_numbered} numbered")
else:
sub_row.label(text="0 duplicates")
if context.scene.show_node_group_duplicates and node_group_duplicates > 0 and context.scene.dataremap_node_groups:
RBST_DatRem_draw_data_duplicates(col, context, "node_groups", node_group_groups)
# Add the operator button
row = box.row()
row.scale_y = 1.5
@@ -1355,8 +1492,8 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
dbu_box.label(text="Data-block utilities addon not installed", icon='ERROR')
# Show total counts
total_duplicates = image_duplicates + material_duplicates + font_duplicates + world_duplicates
total_numbered = image_numbered + material_numbered + font_numbered + world_numbered
total_duplicates = image_duplicates + material_duplicates + font_duplicates + world_duplicates + node_group_duplicates
total_numbered = image_numbered + material_numbered + font_numbered + world_numbered + node_group_numbered
if total_duplicates > 0 or total_numbered > 0:
box.separator()
@@ -1367,37 +1504,9 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
else:
box.label(text="No data blocks to process")
# Add a separator and purge button
box.separator()
row = box.row()
row.operator("bst.purge_unused_data", icon='TRASH')
# Ghost Buster section
layout.separator()
ghost_box = layout.box()
ghost_box.label(text="Ghost Buster - Experimental")
col = ghost_box.column()
col.label(text="Ghost data cleanup & library override fixes:")
col.label(text="• Unused local WGT widget objects")
col.label(text="• Empty unlinked collections")
col.label(text="• Objects not in scenes with no legitimate use")
col.label(text="• Fix broken library override hierarchies")
# Two button layout
row = ghost_box.row(align=True)
row = layout.row()
row.scale_y = 1.5
row.operator("bst.ghost_detector", text="Ghost Detector", icon='ZOOM_IN')
row.operator("bst.ghost_buster", text="Ghost Buster", icon='GHOST_ENABLED')
# Ghost Buster option
ghost_box.prop(context.scene, "ghost_buster_delete_low_priority", text="Delete Low Priority Ghosts")
# Resync Enforce button
ghost_box.separator()
row = ghost_box.row()
row.scale_y = 1.2
row.operator("bst.resync_enforce", text="Resync Enforce", icon='FILE_REFRESH')
row.operator("bst.purge_unused_data", icon='TRASH')
# Add a new operator for toggling data types
class RBST_DatRem_OT_ToggleDataType(bpy.types.Operator):
@@ -1421,6 +1530,8 @@ class RBST_DatRem_OT_ToggleDataType(bpy.types.Operator):
context.scene.dataremap_fonts = not context.scene.dataremap_fonts
elif self.data_type == "worlds":
context.scene.dataremap_worlds = not context.scene.dataremap_worlds
elif self.data_type == "node_groups":
context.scene.dataremap_node_groups = not context.scene.dataremap_node_groups
return {'FINISHED'}
@@ -1543,6 +1654,8 @@ class RBST_DatRem_OT_RenameDatablock(bpy.types.Operator):
data_collection = bpy.data.fonts
elif self.data_type == "worlds":
data_collection = bpy.data.worlds
elif self.data_type == "node_groups":
data_collection = bpy.data.node_groups
if not data_collection:
self.report({'ERROR'}, "Invalid data type")
@@ -395,7 +395,7 @@ class RBST_ViewDisp_OT_RefreshMaterialPreviews(bpy.types.Operator):
if hasattr(material, 'preview_ensure'):
try:
preview = material.preview_ensure()
# In Blender 4.2/4.5, accessing icon_id is safe and helps ensure generation
# In Blender 4.5, accessing icon_id is safe and helps ensure generation
# In Blender 5.0+, we skip this to avoid crashes
if not version.is_version_5_0() and preview and hasattr(preview, 'icon_id'):
try:
@@ -7,7 +7,7 @@ A couple Blender tools to help me automate some tedious tasks in scene optimizat
- Bulk Data Remap
- Bulk Viewport Display
Officially supports Blender 4.2, 4.4, 4.5, and 5.0.
Officially supports Blender 4.5 and 5.2.
## Installation
@@ -1,6 +1,6 @@
"""
This module provides API compatibility functions for handling differences
between Blender 4.2, 4.4, 4.5, and 5.0.
between Blender 4.5 and 5.2.
"""
@@ -1,16 +1,14 @@
"""
This module provides version detection and comparison utilities for
multi-version Blender support (4.2, 4.4, 4.5, and 5.0).
multi-version Blender support (4.5 and 5.2).
"""
import bpy
# Version constants
VERSION_4_2 = (4, 2, 0)
VERSION_4_4 = (4, 4, 0)
VERSION_4_5 = (4, 5, 0)
VERSION_5_0 = (5, 0, 0)
VERSION_5_2 = (5, 2, 0)
def get_blender_version():
@@ -25,7 +23,7 @@ def get_blender_version():
def get_version_string():
"""
Returns the current Blender version as a string (e.g., "4.2.0").
Returns the current Blender version as a string (e.g., "4.5.0").
Returns:
str: Version string in format "major.minor.patch"
@@ -76,35 +74,16 @@ def get_version_category():
Returns the version category string for the current Blender version.
Returns:
str: '4.2', '4.4', '4.5', or '5.0' based on the current version
str: '4.5' or '5.2' for officially supported versions; otherwise 'major.minor'
"""
version = get_blender_version()
major, minor = version[0], version[1]
if major == 4:
if minor < 4:
return '4.2'
elif minor < 5:
return '4.4'
else:
return '4.5'
elif major >= 5:
return '5.0'
else:
# Fallback for older versions
return f"{major}.{minor}"
def is_version_4_2():
"""Check if running Blender 4.2 (4.2.x only, not 4.3 or 4.4)."""
version = get_blender_version()
return version[0] == 4 and version[1] == 2
def is_version_4_4():
"""Check if running Blender 4.4 (4.4.x only, not 4.5)."""
version = get_blender_version()
return version[0] == 4 and version[1] == 4
if major == 4 and minor >= 5:
return '4.5'
if major >= 5 and is_version_at_least(5, 2, 0):
return '5.2'
return f"{major}.{minor}"
def is_version_4_5():
@@ -116,3 +95,7 @@ def is_version_5_0():
"""Check if running Blender 5.0 or later."""
return is_version_at_least(5, 0, 0)
def is_version_5_2():
"""Check if running Blender 5.2 LTS or later."""
return is_version_at_least(5, 2, 0)
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
{
"last_check": "2026-07-14 14:42:39.577911",
"last_check": "2026-07-16 12:39:54.557568",
"backup_date": "July-14-2026",
"update_ready": false,
"ignore": false,
+156 -96
View File
@@ -71,7 +71,6 @@ def draw_wgt(scale, bone, shape = 'sphere'):
shape = shape.lower()
mesh.from_pydata(np.array(shapes[shape]['vertices'])*scale , shapes[shape]['edges'], shapes[shape]['faces'])
# print('draw shape scale ', scale)
shape_obj['WGT_TempCtrl'] = True
return shape_obj
@@ -205,27 +204,28 @@ def tempctrl_shape_type(self, context):
ctrl.custom_shape_translation = (0, 0, 0)
ctrl.custom_shape_rotation_euler = (0, 0, 0)
continue
if org_bone.custom_shape_transform is not None and ctrl.custom_shape_transform is None:
if org_bone.custom_shape_transform is not None:# and ctrl.custom_shape_transform is None:
#check if the transform bone shape already exists
transform_bonename = org_bone.custom_shape_transform.name
if transform_bonename in ctrl.id_data.pose.bones:
# If it already exists then find it
ctrl.custom_shape_transform = ctrl.id_data.pose.bones[transform_bonename]
continue
#add an extra transform bone, if it doesnt exist
else:
#add an extra transform bone, if it doesnt exist
bpy.ops.object.mode_set(mode = 'EDIT')
boneshape = add_ctrl_bone(rig, org_bone.custom_shape_transform, rig.data.edit_bones[ctrl.name].parent, '')
transform_bonename = boneshape.name
add_bone_to_collection(rig, boneshape)
bpy.ops.object.mode_set(mode = 'POSE')
rig = ctrl.id_data
bpy.ops.object.mode_set(mode = 'EDIT')
boneshape = add_ctrl_bone(rig, org_bone.custom_shape_transform, rig.data.edit_bones[ctrl.name].parent, '')
bone_name = boneshape.name
add_bone_to_collection(rig, boneshape)
bpy.ops.object.mode_set(mode = 'POSE')
posebone = rig.pose.bones[bone_name]
posebone = rig.pose.bones[transform_bonename]
constraint_add(posebone, org_bone.custom_shape_transform, 'COPY_TRANSFORMS')
ctrl.custom_shape_transform = posebone
else:
# scale = (length_avg / ctrl.length)*0.4 + 0.4
scale = 1
# print('drawing shape in scale of ', scale)
ctrl.custom_shape = draw_wgt(scale, ctrl, shape = self.shape_type)
ctrl.custom_shape_scale_xyz = tuple([self.shape_size]*3)
ctrl.custom_shape_transform = None
@@ -1098,8 +1098,13 @@ class smartfcu:
self.all_frames = self.frames
self.inbetweens = []
#give only linear interpolation to all the keyframes
self.interpolations = {frame : 'LINEAR' for frame in self.frames}
# Use defaults
interpolation = bpy.context.preferences.edit.keyframe_new_interpolation_type
handle_type = bpy.context.preferences.edit.keyframe_new_handle_type
self.interpolations = {frame : interpolation for frame in self.frames}
self.handle_left_types = self.handle_right_types = {frame : handle_type for frame in self.frames}
def copy(self, smartfcurve, copy_fcurves = True):
'''Copying the smartfcurve attribute from another one'''
self.posebone = smartfcurve.posebone
@@ -1385,12 +1390,8 @@ def find_smartfcus_to_copy(smartfcurve, smartfcus):
if smartfcurve.frames:
return
def get_bone_layers_actions(bone, actions):
def get_obj_layered_actions(obj, actions):
'''get the actions from all layers'''
if not bpy.context.scene.btc.bake_layers:
return actions
obj = bone.id_data
if not obj.animation_data:
return actions
if not obj.animation_data.use_nla:
@@ -1406,6 +1407,16 @@ def get_bone_layers_actions(bone, actions):
actions.add(strip.action)
return actions
def get_bone_layers_actions(bone, actions):
'''get the actions from all layers during bake'''
if not bpy.context.scene.btc.bake_layers:
return actions
obj = bone.id_data
actions = get_obj_layered_actions(obj, actions)
return actions
def smartbake_to_ik(context, posebones, chain, sb):
@@ -1466,6 +1477,11 @@ def smartbake_to_ik(context, posebones, chain, sb):
#create the fcurves and add them to smartfcurves
for ctrl in ctrls:
if ctrl == chain.ik_tip and ctrl != chain.ik_ctrl:
continue
# if chain.pole_bone and ctrl == chain.base_bone:
# continue
ctrl_action = ctrl.id_data.animation_data.action
path = ctrl.path_from_id()
rot = Tools.rot_mode_to_channel(ctrl.rotation_mode)
@@ -1523,40 +1539,57 @@ def deselect_all_fcurves(fcurves):
fcurves.foreach_set('hide', array_true)
fcurves.foreach_set('select', array_false)
def get_enabled_keyframes(ctrl_rig, action_frames_remove):
def get_enabled_keyframes(ctrl_rig, smartframes):
'''Get all the frames where Temp Ctrls are turned off'''
'''Need to iterate over all layers of the ctrl rig'''
if not ctrl_rig:
return
action = ctrl_rig.animation_data.action
if action in action_frames_remove:
return action_frames_remove[action]
fcurves = Tools.get_fcurves(ctrl_rig, action)
enable_fcu = fcurves.find('animtoolbox.ctrls_enabled')
# if action in action_frames_remove:
# return action_frames_remove[action]
actions = set()
enable_fcu = None
get_obj_layered_actions(ctrl_rig, actions)
# Check for the enabled fcu in all layers of the ctrl rig
for action in actions:
fcurves = Tools.get_fcurves(ctrl_rig, action)
enable_fcu = fcurves.find('animtoolbox.ctrls_enabled')
if enable_fcu:
break
if not enable_fcu:
return None
action_frame_range = action.frame_range
frames_remove = []
# action_frame_range = action.frame_range
frames_enabled = []
keyframes = np.zeros(len(enable_fcu.keyframe_points)*2)
enable_fcu.keyframe_points.foreach_get('co', keyframes)
frames = keyframes[::2]
values = keyframes[1::2]
if values[0] == 0:
frames_remove += list(range(round(action_frame_range[0]), round(frames[0])))
for i in range(1, len(frames)):
if values[i-1] == 0 and values[i] == 1:
frames_remove += list(range(round(frames[i-1]), round(frames[i])))
if values[-1] == 0:
frames_remove += list(range(round(frames[-1]), round(action_frame_range[1])))
# Getting all the frames where it's turned on and off
frames_on = frames[values == 1]
frames_off = frames[values == 0]
# Getting the ranges from when it's turned on until it's off
for frame in frames_on:
if frame in frames_enabled:
continue
if len(frames_off[frame < frames_off]):
next_off = min(frames_off[frame < frames_off])
if next_off:
frames_enabled += list(range(int(frame), int(next_off)))
# if the last keyframe is turned on then get the frames until the rest of the range
if max(smartframes) > max(frames_on) > max(frames_off):
frames_enabled += list(range(int(max(frames_on)), int(max(smartframes))))
action_frames_remove.update({action : frames_remove})
# action_frames_remove.update({action : frames_remove})
return frames_remove
return frames_enabled
def replace_rot_mode(origin_data_path, posebone):
@@ -1904,7 +1937,7 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
#Store the influence and set them to 0 before calculating the matrix without the constraint (to get the offset)
con_influence = get_con_influence(constrained)
if chain:
if chain and chain.pole_bone is not None:
# Adding the pole bone to the end of the list
bones_matrices.update({chain.pole_bone : None})
bones_matrices[chain.pole_bone] = bones_matrices.pop(chain.pole_bone)
@@ -1924,11 +1957,22 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
else:
# Get the rest of the bones to check if they are children and if we need viewlayer update
bones = set(bones_matrices.keys()).difference(pasted_bones)
# Calculating pole target matrix after other bones were calcluated
if chain and bone == chain.pole_bone:
bpy.context.view_layer.update()
matrix = find_pole_vector(chain.base_bone, chain.ik_tip, chain.pole_bone, chain.step)
base_bone = chain.base_bone
ik_tip = chain.ik_tip
angle = chain.base_bone.vector.angle(chain.ik_tip.vector)
if not round(angle):
# continue
# If the angle is flat then use the position of the pole relative to the rest position base bone
relative_matrix = base_bone.bone.matrix_local.inverted() @ chain.pole_bone.bone.matrix_local
matrix = base_bone.matrix @ relative_matrix
else:
bpy.context.view_layer.update()
matrix = find_pole_vector(base_bone, ik_tip, chain.pole_bone, chain.step)
Tools.paste_bone_matrix(bone, matrix, constrained, bones, x_filter = False)
pasted_bones.add(bone)
@@ -1937,10 +1981,11 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
Tools.compatible_rotation(bone, bones_prevrot)
#Add keyframes to all the fcurves of the bone
for fcu in bone_fcus[bone]:
groupname = fcu.group.name if fcu.group else ""
obj.keyframe_insert(fcu.data_path, index = fcu.array_index , frame = frame, group = groupname)
#Turn the influence of the constraints back on
if con_influence:
for con, influence in con_influence.items():
@@ -1977,6 +2022,7 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
key.interpolation = smartfcurve.interpolations[frame]
if key.interpolation != 'BEZIER':
continue
key.handle_left_type = smartfcurve.handle_left_types[frame]
key.handle_right_type = smartfcurve.handle_right_types[frame]
@@ -1985,6 +2031,7 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
# print('writing keyframes time 0.7.7', time.time() - starttime)
def add_interpolations(smartfcus):
#turn inbetween keyframes values to handles
for smartfcurve in smartfcus.values():
fcu = smartfcurve.fcu
@@ -2187,7 +2234,8 @@ def smartbake(context, org_action, posebones, ids):
frame_current = scene.frame_current
smartfcus = dict()
smartframes = set()
action_frames_remove = dict()
ctrl_rig = None
#Use this to check if an fcurve exist also in the original action (and not just on a layer)
org_action_fcus = set()
transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
@@ -2202,10 +2250,13 @@ def smartbake(context, org_action, posebones, ids):
# find_fcu = False
path = posebone.path_from_id()
# Find the Ctrl Rig
if 'EMPTY' in posebone.btc.setup:
ctrl_rig = find_empty_ctrl(context, posebone)
else:
ctrl_rig = posebone.id_data.animtoolbox.controller
if not ctrl_rig:
ctrl_rig = next((item.controller for item in scene.btc.ctrl_items if item.controlled == posebone.id_data), None)
actions = {org_action}
get_bone_layers_actions(posebone, actions)
@@ -2249,8 +2300,8 @@ def smartbake(context, org_action, posebones, ids):
if ctrl_rig is None:
continue
frames_remove = get_enabled_keyframes(ctrl_rig, action_frames_remove)
# frames_remove = get_enabled_keyframes(ctrl_rig, action_frames_remove)
#Getting the Fcurve container, either action or channelbag
channelbag = Tools.get_channelbag(posebone.id_data, org_action)
if not channelbag:
@@ -2311,6 +2362,7 @@ def smartbake(context, org_action, posebones, ids):
else:
#Add the fcurve if it's in the ctrl but not in the original action
fcu = smartbake_add_fcurve(channelbag, origin_data_path, ctrl_fcu.array_index, posebone.name)
smartfcurve = get_smartfcurve(ctrl_fcu, smartfcus, posebone, origin_data_path, ctrl_fcu.array_index, scene.btc.smartbake)
if smartfcurve : smartfcurve.fcu = fcu
@@ -2325,22 +2377,26 @@ def smartbake(context, org_action, posebones, ids):
for mod in ctrl_fcu.modifiers:
smartfcurve.modifiers.append(mod)
#Remove frames to not include in the bake, in case of custom frame range
if frames_remove and smartfcurve:
smartframes = {frame for frame in smartframes if round(frame) not in frames_remove}
smartfcurve.frames = [frame for frame in smartfcurve.frames if round(frame) not in frames_remove]
smartfcurve.interpolations = {frame : interpolation for frame, interpolation in smartfcurve.interpolations.items() if round(frame) not in frames_remove}
#need to add the fcurves
add_missing_fcurves(org_action, posebone, smartfcus)
# print('frames_remove ', frames_remove)
smartframes = get_smartframes(scene, smartfcus)
frames_enabled = get_enabled_keyframes(ctrl_rig, smartframes)
# smartframes = smartframes - set(frames_remove)
# print('smartframes', smartframes)
if frames_enabled:
# Iterating instead of sets in case of sub-frames
smartframes = {frame for frame in smartframes if round(frame) in frames_enabled}
smartframes = sorted(smartframes)
#write all the keyframes
smartbake_write_keyframes(scene, smartframes, smartfcus, posebones = posebones)
add_interpolations(smartfcus)
btc = scene.btc
if btc.bake_range_type == 'KEYFRAMES' and btc.smartbake:
add_interpolations(smartfcus)
scene.frame_set(frame_current)
@@ -3442,16 +3498,12 @@ def child_prop_edit(self):
child_names = remove_child_ctrls(rig, child_names)
return child_ctrls
# org_obj = rig.animtoolbox.controlled
# print('org obj ', org_obj)
#Add child ctrls
child_names = []
for ctrl_posebone in ctrls:
ctrl = rig.data.edit_bones[ctrl_posebone.name]
child_ctrl = add_ctrl_bone(rig, ctrl, ctrl, 'Child_', 0.5, orient_world = self.orientation)
# child_ctrl.matrix = ctrl.matrix
# child_ctrl.length = ctrl.length * 0.5
# bpy.context.view_layer.update()
child_ctrls.append(child_ctrl)
add_bone_to_collection(rig, child_ctrl, col_name = 'Child Ctrls', visible = True)
child_names.append(child_ctrl.name)
@@ -4134,7 +4186,7 @@ def assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_i
#Assign group, color, matrix and setup ids in pose mode
for ctrl, bone in zip(ctrls, selected):
ctrl.matrix = get_relative_ws_matrix(bone, ctrl)
#assign the controller to a group
if bpy.app.version < (4, 0, 0):
ctrl.bone_group = ctrl_group
@@ -4176,6 +4228,7 @@ def assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_i
#get custom shape and assign the matrix
if bone.custom_shape_transform and self.shape_type == 'ORIGINAL':
obj_mat = bone.id_data.matrix_world
ot_bone = bone.custom_shape_transform.name
#Get the original override transform bone
target = bone.id_data.pose.bones[ot_bone]
@@ -5310,14 +5363,16 @@ def pole_prop_edit(self, context):
else:
add_bone_to_collection(rig, rig.data.edit_bones[chain.base_bone.name], col_name = 'IK Ctrls', visible = True)
#remove the pole bone and switch to base bone
if not hasattr(chain, 'pole_bone'):
if chain.pole_bone is None:
continue
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
#remove from ctrls and append base bone instead
if chain.pole_bone in ctrls:
ctrls.remove(chain.pole_bone)
ctrls.append(chain.base_bone)
del chain.pole_bone, chain.pole_bone_name
chain.pole_bone = chain.pole_bone_name = None
# del chain.pole_bone, chain.pole_bone_name
#Remove child ctrls
if btc.child:
@@ -5330,13 +5385,17 @@ def pole_prop_pose(self, context):
if not btc.pole_target:
chain.base_bone.btc.org = 'CTRL'
if not hasattr(chain, 'pole_bone'):
continue
del chain.pole_bone
if hasattr(chain,'pole_bone_name'):
chain.pole_bone_name
continue
# if not hasattr(chain, 'pole_bone'):
# continue
if not chain.pole_bone:
continue
if not chain.pole_bone_name:
continue
chain.pole_bone = chain.pole_bone_name = None
# del chain.pole_bone
# if hasattr(chain,'pole_bone_name'):
# chain.pole_bone_name
# continue
pole_posebone_update(chain, ctrls)
if chain.base_bone in ctrls:
@@ -5413,7 +5472,9 @@ def ik_ctrl_orientation(self, context):
def pole_posebone_update(chain, ctrls):
'''update pole bone properties in the pole prop and add ik prop'''
if not hasattr(chain, 'pole_bone_name'):
# if not hasattr(chain, 'pole_bone_name'):
# return
if not chain.pole_bone_name:
return
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
@@ -5447,6 +5508,8 @@ def update_axis_prop(self, context):
if not hasattr(chain, 'pole_bone_name'):
continue
if not chain.pole_bone_name:
continue
pole_matrix = find_pole_vector(base_bone, ik_tip, ik_tip, chain.step)
#pole matrix orientation to world by default
@@ -5551,15 +5614,6 @@ def add_pole_bone(self, context, rig, chain):
new_rot = mathutils.Euler((0, 0, 0), 'XYZ')
pole_matrix = Matrix.LocRotScale(loc, new_rot, scale)
#In case of a flat IK skip adding a pole bone and use base bone instead
# if pole_matrix.translation == chain.ik_tip.matrix.translation:
# print('no pole bone')
# self.report({'ERROR'}, "Can't add Pole Ctrl because of a flat IK chain")
# chain.pole_bone = None
# add_bone_to_collection(rig, chain.base_bone, col_name = 'IK Ctrls', visible = True)
# btc['pole_target'] = False
# return
pole_bone_name = chain.org_bones[-2].name + '_Pole' if len(chain.org_bones) > 1 else chain.org_bones[0].name + '_Pole'
chain.pole_bone = rig.data.edit_bones.new(pole_bone_name)
@@ -5582,7 +5636,9 @@ def pole_offset(self, context):
global ik_chains, ctrls, child_names
for chain in ik_chains:
if not hasattr(chain, 'pole_bone_name'):
# if not hasattr(chain, 'pole_bone_name'):
# continue
if not chain.pole_bone_name:
continue
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
@@ -5621,7 +5677,7 @@ def add_ik_prop_edit(self, context):
chain.ik_tip_name = chain.ik_tip.name
#remove the pole bone
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
if btc.pole_target and chain.pole_bone_name:
#removing and adding the pole_target without calling the whole pole prop update
ctrls.remove(chain.pole_bone)
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
@@ -5654,7 +5710,7 @@ def add_ik_prop_pose(self, context):
chain.ik_ctrl.btc.org = 'CTRL'
chain.ik_ctrl.color.palette = context.scene.btc.color_set
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
if btc.pole_target and chain.pole_bone_name: #hasattr(chain, 'pole_bone_name'):
pole_posebone_update(chain, ctrls)
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
@@ -5782,7 +5838,7 @@ class TempIK(bpy.types.Operator):
# for ctrl in ctrls:
# add_bone_to_collection(rig, ctrl, col_name = 'ORG', visible = True)
chain = IK_Chain(hierarchy, ctrls, root_ctrl)
#if only one bone is selected in the chain then add an extra control automatically
if len(hierarchy) == 1:
add_ik_ctrl = True
@@ -5791,7 +5847,7 @@ class TempIK(bpy.types.Operator):
for i, ctrl in enumerate(ctrls[:-1]):
ctrls[i+1].tail = ctrl.head
context.view_layer.update()
# return {'FINISHED'}
switch_layers(rig, chain.ik_tip, [31])
add_bone_to_collection(rig, chain.ik_tip)
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
@@ -5800,7 +5856,7 @@ class TempIK(bpy.types.Operator):
# rig.data.collections['ORG'].is_visible = False
ik_chains.append(chain)
btc['add_ik_ctrl'] = add_ik_ctrl
if add_ik_ctrl:
add_ik_prop_edit(self, context)
@@ -5824,8 +5880,7 @@ class TempIK(bpy.types.Operator):
####################################################################
bpy.ops.object.mode_set(mode = 'POSE')
setup_ids = set()
if btc.root:
@@ -5858,20 +5913,22 @@ class TempIK(bpy.types.Operator):
# #assign the matrix
distance = (bone.id_data.matrix_world @ bone.bone.matrix_local).inverted() @ ctrl.bone.matrix_local.copy()
ctrl.matrix = bone.id_data.matrix_world @ bone.matrix @ distance
# context.view_layer.update()
ctrl.color.palette = btc.color_set
add_bone_setup_id(chain.ik_ctrl, False, setup, 'CTRL', setup_id, org_id = chain.ik_ctrl.btc.org_id)
if add_ik_ctrl:
add_ik_prop_pose(self, context)
context.view_layer.update()
pole_prop_pose(self, context)
# child_prop_pose(context)
if context.scene.btc.child:
child_prop(context.scene.btc, context)
# return {'FINISHED'}
self.ik_chains = ik_chains
tempctrl_shape_type(self, context)
@@ -5885,7 +5942,7 @@ class TempIK(bpy.types.Operator):
if 'ctrls' not in globals():
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
return {'CANCELLED'}
btc = context.scene.btc
controlled_objs = {item.controlled for item in context.scene.btc.ctrl_items if item.controlled}
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
@@ -5952,7 +6009,7 @@ class TempIK(bpy.types.Operator):
rig.data.edit_bones[ctrl.name].parent = rig.data.edit_bones[chain.ctrls[i+1].name]
#updating the polebone position with the offset
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
if btc.pole_target and chain.pole_bone_name: #hasattr(chain, 'pole_bone_name'):
pole_bone = rig.data.edit_bones[chain.pole_bone_name]
base_bone = rig.data.edit_bones[chain.base_bone_name]
ik_tip = rig.data.edit_bones[chain.ik_tip_name]
@@ -6081,7 +6138,8 @@ class TempIK(bpy.types.Operator):
#add the ik constraint to the ctrls
ik_con = constraint_add(chain.ik_tip, chain.ik_ctrl if not btc.child else chain.ik_child_ctrl, 'IK')
chain.pole_angle = get_pole_angle(chain.base_bone, chain.ik_tip, chain.pole_bone.matrix.translation)
if chain.pole_bone:
chain.pole_angle = get_pole_angle(chain.base_bone, chain.ik_tip, chain.pole_bone.matrix.translation)
ik_con.chain_count = chain.length(btc.add_ik_ctrl)
#setup the ik constraint
@@ -6316,6 +6374,8 @@ class IK_Chain:
self.ik_ctrl_name = self.ik_ctrl.name
self.base_bone_name = self.base_bone.name
self.ik_tip_name = self.ik_tip.name
self.pole_bone = None
self.pole_bone_name = None
self.step = get_step(hierarchy[0].id_data)
@@ -6337,7 +6397,7 @@ class IK_Chain:
self.parent = rig.pose.bones[self.parent_name]
if hasattr(self, 'root_name'):
self.root_ctrl = rig.pose.bones[self.root_name]
if hasattr(self, 'pole_bone_name'):
if self.pole_bone_name:
self.pole_bone = rig.pose.bones[self.pole_bone_name]
# if hasattr(self, 'pole_bone_ref_name'):
# self.pole_bone_ref = rig.pose.bones[self.pole_bone_ref_name]
+45 -11
View File
@@ -574,7 +574,12 @@ def cursor_change_post(self, context):
new_matrices = [bone.matrix.copy() for bone in context.selected_pose_bones]
elif context.selected_objects:
new_matrices = [obj.matrix_world.copy() for obj in context.selected_objects]
else:
return
if 'last_matrices' not in globals():
last_matrices = []
#checking if the selection was moving and have different matrix
if last_matrices != new_matrices:
update_cursor_matrix(context)
@@ -606,7 +611,7 @@ def cursor_change_pre(self, context):
return
item = selected_items[0]
if 'relative_cursor' in item:
if 'relative_cursor' in item.animtoolbox:
update_cursor_matrix(context)
else:
assign_relative_cursor_prop(context)
@@ -639,7 +644,11 @@ def update_cursor_matrix(context):
else:
item = context.object
selected_items = context.selected_objects
# No Matrix All zeros
if not any (item.animtoolbox.relative_cursor):
return
# for bone in selected_bones:
if len(selected_items) > 1:
#multiple selection cursor is relative to the center of all the selections
@@ -651,11 +660,12 @@ def update_cursor_matrix(context):
item_matrix = item.matrix_world
if bpy.app.version < (5, 0, 0):
relative_matrix = Matrix(item['relative_cursor'])
matrix_4x4 = item.animtoolbox.relative_cursor
else:
# Since Blender 5 I need to reshape the array into a matrix using numpy
matrix_4x4 = np.array(item['relative_cursor']).reshape(4, 4).T
relative_matrix = Matrix(matrix_4x4)
matrix_4x4 = np.array(item.animtoolbox.relative_cursor).reshape(4, 4).T
relative_matrix = Matrix(matrix_4x4)
cursor.matrix = item_matrix @ relative_matrix.inverted()
cursor_matrix = cursor.matrix
@@ -681,8 +691,11 @@ def assign_relative_matrix(context, items, prop = 'relative_cursor'):
item_matrix = matrix_avg if len(items) > 1 else item_matrix
matrix_dist = cursor_matrix.inverted() @ item_matrix
item[prop] = matrix_dist
# item[prop] = matrix_dist
# Storing the matrix in a non animated float vector property instead of ID property
item.animtoolbox.relative_cursor = np.array(matrix_dist.transposed()).flatten() # [v for row in matrix_dist for v in row] #
return matrix_dist
def assign_relative_cursor_prop(context):
@@ -751,7 +764,7 @@ class RelativeCursor(bpy.types.Operator):
if items:
#if it's a single selection and already has a cursor assigned then don't re-assign
if len(items) != 1 or 'relative_cursor' not in items[0]:
if len(items) != 1 or not hasattr (items[0].animtoolbox, 'relative_cursor' ):
assign_relative_cursor_prop(context)
#appending all the handlers
@@ -1520,10 +1533,20 @@ def compatible_rotation(bone, bones_prevrot):
'''Make sure the bone is not flipping and is compatible with previous frame rotation'''
rotation_channel = rot_mode_to_channel(bone.rotation_mode)
if bone in bones_prevrot:
rot = getattr(bone, rotation_channel)
rot.make_compatible(bones_prevrot[bone])
bones_prevrot[bone] = getattr(bone, rotation_channel).copy()
# def compatible_matrix_rotation(prev_matrix, new_matrix):
# '''Make sure the bone is not flipping and is compatible with previous frame rotation'''
# prev_loc, prev_rot, prev_scale = prev_matrix.decompose()
# loc, rot, scale = new_matrix.decompose()
# rot.make_compatible(prev_rot)
# new_matrix = Matrix.LocRotScale(loc, rot, scale)
# return new_matrix
class CopyMatrix(bpy.types.Operator):
"""Copy the matrix of the selection and store it"""
bl_idname = "anim.copy_matrix"
@@ -1771,7 +1794,7 @@ class CopyRelativeMatrix(bpy.types.Operator):
def paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens, locked_fcus, bones_prevrot):
#Make sure the rotation is compatible with the previous rotation
compatible_rotation(target, bones_prevrot)
# compatible_rotation(target, bones_prevrot)
#if autokey is turned on then add a keyframe
if (scene.tool_settings.use_keyframe_insert_auto or scene.animtoolbox.range_type != 'CURRENT') and 'target' in locals():
@@ -1885,6 +1908,7 @@ def paste_bone_matrix(bone, matrix_copied, constrained, bones = {}, x_filter = T
context = bpy.context
if x_filter : matrix_copied = filter_matrix_properties(context, getattr(bone, matrix_attr), matrix_copied)
# bone.matrix = bone.id_data.matrix_world.inverted() @ matrix_copied
# matrix_copied = compatible_matrix_rotation(getattr(bone, matrix_attr), matrix_copied)
setattr(bone, matrix_attr, matrix_copied) # bone.id_data.matrix_world.inverted() @
children = set(bone.children_recursive).intersection(bones)
@@ -1898,6 +1922,7 @@ def paste_bone_matrix(bone, matrix_copied, constrained, bones = {}, x_filter = T
matrix_copied = reverse_bone_constraints(context, bone, matrix_copied)
if x_filter : matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied)
# bone.matrix = matrix_copied
setattr(bone, matrix_attr, matrix_copied)
#important to have the extra update
context.view_layer.update()
@@ -1907,8 +1932,16 @@ def paste_bones_matrices(bones_matrices, constrained, x_filter = True):
pasted_bones = set()
for bone, matrix_copied in bones_matrices.items():
#Get the rest of the bones to check if they are children of the current bone
old_rot = bone.rotation_quaternion.copy()
bones = set(bones_matrices.keys()).difference(pasted_bones)
paste_bone_matrix(bone, matrix_copied, constrained, bones, x_filter)
# Make sure rotation difference is compatible, no flipping
new_rot = bone.rotation_quaternion.copy()
new_rot.make_compatible(old_rot)
bone.rotation_quaternion = new_rot
pasted_bones.add(bone)
class PasteRelativeMatrix(bpy.types.Operator):
@@ -1986,6 +2019,8 @@ class PasteRelativeMatrix(bpy.types.Operator):
rig_offset = rig_offset @ source_rig.matrix_world
matrix_new = rig_offset @ matrix_source @ bone_matrix_dist
# matrix_new = compatible_matrix_rotation(matrix_source, matrix_new)
#Store the matrices for each bone that will use it
matrix_new = reverse_childof_constraint(bone, matrix_new, constrained)
@@ -2126,7 +2161,7 @@ def get_fcurves_frames(selection, fcurves, all_fcurves, frames, attr_index):
# transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
for fcu in fcurves:
# print('fcu ', fcu)
if not bone_paths and not fcu.data_path.startswith('pose.bones'):
#in case it's just an object or data
all_fcurves.append(fcu)
@@ -2251,7 +2286,6 @@ class ShareKeys(bpy.types.Operator):
if obj.type in actions_data_types:
action_data = self.add_get_action(obj.data)
# print('udating objfcurves animation data')
obj_fcurves.update({obj.data: [fcu for fcu in all_fcurves if fcu.id_data == action_data]})
obj_fcurves[obj.data] = sharekeys_add_missing_fcurves(obj.data, attr_index_obj, obj_fcurves[obj.data])
+11 -4
View File
@@ -20,7 +20,7 @@
bl_info = {
"name": "AnimToolBox",
"author": "Tal Hershkovich",
"version" : (0, 2, 0),
"version" : (0, 2, 6),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "A set of animation tools",
@@ -204,7 +204,12 @@ class AnimToolBoxObjectSettings(bpy.types.PropertyGroup):
child: bpy.props.BoolProperty(name = "Child Empty", description = "Empty is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", default = 0)
relative_cursor: bpy.props.FloatVectorProperty(name="Relative Cursor", size=16, options={'HIDDEN'})
class AnimToolBoxBoneSettings(bpy.types.PropertyGroup):
relative_cursor: bpy.props.FloatVectorProperty(name="Relative Cursor", size=16, options={'HIDDEN'})
class IsolatedRigs(bpy.types.PropertyGroup):
hidden: bpy.props.PointerProperty(name = "Hidden Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
@@ -468,7 +473,7 @@ def loadanimtoolbox_post(self, context):
Tools.selection_order(self, context)
classes = (TempCtrlsItems, TempCtrlsOrgIds, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties,
IsolatedRigs, AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
IsolatedRigs, AnimToolBoxObjectSettings, AnimToolBoxBoneSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
addon_keymaps = []
@@ -493,9 +498,10 @@ def register():
bpy.types.Scene.btc = bpy.props.PointerProperty(type = TempCtrlsSceneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.PoseBone.btc = bpy.props.PointerProperty(type = TempCtrlsBoneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Object.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxObjectSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.PoseBone.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxBoneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Scene.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxGlobalSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.WindowManager.atb_ui = bpy.props.PointerProperty(type = AnimToolBoxUILayout, override = {'LIBRARY_OVERRIDABLE'})
ui.update_panel(None, bpy.context)
ui.add_multikey(None, bpy.context)
ui.add_riggertoolbox(None, bpy.context)
@@ -554,6 +560,7 @@ def unregister():
del bpy.types.Scene.btc
# del bpy.types.Bone.btc
del bpy.types.Object.animtoolbox
del bpy.types.PoseBone.animtoolbox
del bpy.types.Scene.animtoolbox
if hasattr(bpy.types.Object, 'keyframes_offset'):
del bpy.types.Object.keyframes_offset
@@ -1374,7 +1374,8 @@ def register(bl_info):
# **WARNING** Depending on the engine, this token can act like a password!!
# Only provide a token if the project is *non-public*, see readme for
# other considerations and suggestions from a security standpoint
updater.private_token = "glpat-js__ikigVSQ_tKWgjVn1" # "tokenstring"
# updater.private_token = "glpat-js__ikigVSQ_tKWgjVn1" # "tokenstring"
updater.private_token = "glpat-5rHfAdBG2Br3ww6H8U4vI286MQp1OjFpcXEK.01.100el42cj"
# choose your own username, must match website (not needed for GitLab)
updater.user = ""
@@ -1,9 +0,0 @@
{
"last_check": "",
"backup_date": "",
"update_ready": false,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {}
}
+18 -6
View File
@@ -2007,7 +2007,7 @@ def remove_draw_handlers(self):
# Operator with modal and draw handler
class MotionPathOperator(bpy.types.Operator):
"""Creates a custom motion path"""
"""Creates an editable motion path, Ctrl click to refresh"""
bl_idname = "object.motion_path_operator"
bl_label = "Motion Path Operator"
bl_options = {'REGISTER', 'UNDO'}
@@ -2028,10 +2028,15 @@ class MotionPathOperator(bpy.types.Operator):
scene = context.scene
emp = scene.emp
emp.refresh = False
if emp.motion_path:
emp.motion_path = False
if event.ctrl:
# Refresh the motion path
emp.refresh = True
else:
emp.motion_path = False
return {'CANCELLED'}
if context.selected_pose_bones is None and context.selected_objects is None:
emp.motion_path = False
return {'CANCELLED'}
@@ -2237,9 +2242,6 @@ class MotionPathOperator(bpy.types.Operator):
#return negative frame range to what it was
if context.preferences.edit.use_negative_frames != nfr:
context.preferences.edit.use_negative_frames = nfr
#assign the draw handler
context.window_manager.modal_handler_add(self)
#If overlays are turned off then skip drawing
if self.mp_vis:
@@ -2255,6 +2257,9 @@ class MotionPathOperator(bpy.types.Operator):
self.area.tag_redraw()
# Tools.redraw_areas(['VIEW_3D'])
#assign the draw handler
context.window_manager.modal_handler_add(self)
bpy.ops.ed.undo_push(message = 'Initialize Motion Path')
return {'RUNNING_MODAL'}
@@ -2279,6 +2284,11 @@ class MotionPathOperator(bpy.types.Operator):
if not emp.motion_path:
quit_mp(self, context)
return {'FINISHED'}
if emp.refresh:
get_mp_points(self, context)
emp.refresh = False
return {'PASS_THROUGH'}
if not self.mp_vis:
check_mp_vis_on(self, context)
@@ -4864,6 +4874,8 @@ def disbale_non_smooth(self, context):
class EditableMotionPathSettings(bpy.types.PropertyGroup):
'''All the settings for Editable motion path'''
motion_path: bpy.props.BoolProperty(name = "Motion Path", description = "Flag when Motion Path is on", default = False, override = {'LIBRARY_OVERRIDABLE'})
refresh: bpy.props.BoolProperty(name = "Refresh Motion Path", description = "Refresh the motion paths with hotkey ctrl", default = False, override = {'LIBRARY_OVERRIDABLE'})
settings: bpy.props.BoolProperty(name = "Motion Path Settings", description = "Open the settings Menu", default = False, override = {'LIBRARY_OVERRIDABLE'})
# mp_keyframe_scale: bpy.props.FloatProperty(name = "Scale Selecgted Keyframes Bounding Box", description = "Change the scale of the bounding box around the selected keyframes ", default = 0.1, step = 0.1, precision = 3)
color_before: bpy.props.FloatVectorProperty(name="Motion Path Before Color", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame", update=update_prop_callback)
+126
View File
@@ -0,0 +1,126 @@
bl_info = {
"name": "SimpleBake",
"author": "Lewis <www.toohey.co.uk/SimpleBake/support/>",
"version": (2, 8, 4),
"blender": (5, 0, 0),
"location": "Properties Panel -> Render Settings Tab",
"description": "Simple baking of PBR and other textures",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Object",
}
import bpy
from bpy.utils import register_class, unregister_class
from . import object_preperation
from . import image_management
from . import material_management
from . import external_save
from . import property_group
from . import uv_management
from . import copy_and_apply
from . import starting_checks
from . import utils
from . import background_and_progress
from . import channel_packing
from . import cleanup
try:
from . import auto_update
except:
auto_update = None
from . import presets
from . import presets_local
from . import sketchfab_upload
from . import messages
from . import auto_cage
from . import aov
from . import preview_operators
from . import ui
from . import bake_operators
import platform
import sys
if auto_update != None:
from .auto_update import VersionControl
else:
VersionControl = None
classes = ([
])
def register():
global classes
for cls in classes:
register_class(cls)
object_preperation.register()
image_management.register()
material_management.register()
external_save.register()
property_group.register()
ui.register()
uv_management.register()
copy_and_apply.register()
bake_operators.register()
starting_checks.register()
utils.register()
background_and_progress.register()
channel_packing.register()
if auto_update!=None:
auto_update.register()
presets.register()
presets_local.register()
sketchfab_upload.register()
messages.register()
auto_cage.register()
cleanup.register()
aov.register()
preview_operators.register()
prefs = bpy.context.preferences.addons[__package__].preferences
version = bl_info["version"]
if VersionControl != None:
VersionControl.installed_version = version
no_online_check = False
if prefs.no_update_check:
no_online_check = True
VersionControl.check_at_current_version(no_online_check)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
object_preperation.unregister()
image_management.unregister()
material_management.unregister()
external_save.unregister()
property_group.unregister()
ui.unregister()
uv_management.unregister()
copy_and_apply.unregister()
bake_operators.unregister()
starting_checks.unregister()
utils.unregister()
background_and_progress.unregister()
channel_packing.unregister()
if auto_update != None:
auto_update.unregister()
presets.unregister()
presets_local.unregister()
sketchfab_upload.unregister()
messages.unregister()
auto_cage.unregister()
cleanup.unregister()
aov.unregister()
preview_operators.unregister()
+166
View File
@@ -0,0 +1,166 @@
import bpy
from .utils import SBConstants, blender_default_colorspace#, get_cs_list
from .messages import print_message
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty, IntProperty, BoolProperty
def get_aov_name(context, num):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
num = str(num)
num = int(num.replace(f"{SBConstants.PBRAOVS}_", ""))
for aov in aov_collection:
if aov.aov_number == num:
return aov.name
print_message(context, "ERROR: Could not find AOV when looking for AOV name")
return False
def get_aov_number(context, name):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
for aov in aov_collection:
if aov.name == name:
return aov.aov_number
return False
def get_aov_colspace(context, name):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
num = int(name.replace(f"{SBConstants.PBRAOVS}_", ""))
for aov in aov_collection:
if aov.aov_number == num:
return aov.cs
print_message(context, "ERROR: Could not find AOV when looking for Color Space")
return False
def get_aov_type(context, name):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
num = int(name.replace(f"{SBConstants.PBRAOVS}_", ""))
for aov in aov_collection:
if aov.aov_number == num:
return aov.aov_type
print_message(context, "ERROR: Could not find AOV when looking for Type")
return False
def get_list_enabled_aov_names(context):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
names = []
for i in aov_collection:
if i.enabled:
names.append(i.name)
return names
class SimpleBake_OT_RefreshAOVList(bpy.types.Operator):
bl_idname = "simplebake.refresh_aov_list"
bl_label = "Refresh AOV List"
bl_description = "Scan objects in bake objects list for AOV Output nodes and update the AOV list"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
aov_collection = sbp.aov_items
#So we can renable them at the emd
original_state = [[i.name, i.cs, i.enabled] for i in aov_collection]
# Clear existing list
aov_collection.clear()
aov_names = [] #Really only used to check for duplicates
obj_names = [i.obj_point.name for i in sbp.objects_list]
#As always, declas is an annoying excpetion
if sbp.s2a_opmode == 'decals' and sbp.targetobj!=None:
obj_names.append(sbp.targetobj.name)
for obj_name in obj_names:
obj = bpy.data.objects.get(obj_name)
if not obj:
continue
for slot in obj.material_slots:
mat = slot.material
if not mat or not mat.use_nodes:
continue
node_trees = []
node_trees.append(mat.node_tree)
for node_tree in node_trees:
if not hasattr(node_tree, "nodes"):
continue #Avoiding error reported on Discord. Not sure how this happens
for node in node_tree.nodes:
if node.bl_idname == "ShaderNodeGroup":
node_trees.append(node.node_tree)
elif node.bl_idname == "ShaderNodeOutputAOV":
if len(node.inputs['Color'].links)>0 or len(node.inputs['Value'].links)>0:
aov_name = node.aov_name
if aov_name not in aov_names: # avoid duplicates
aov_names.append(aov_name)
#Get the CS names only so we can find the index later
#cs_list = [i[0] for i in get_cs_list(self, context)]
item = aov_collection.add()
item.name = aov_name
item.enabled = False
item.aov_number = (aov_names.index(aov_name)+1)
if len(node.inputs['Color'].links)>0:
item.aov_type = "C"
#item.cs = "s#RGB"
item.cs = blender_default_colorspace(float_buffer=False, col=True)[1]
else:
item.aov_type = "V"
#item.cs = "Non#-Color"
item.cs = blender_default_colorspace(float_buffer=False, col=False)[1]
item.object_name = obj_name
item.mat_name = mat.name
#Renable if originally enabled
for i in aov_collection:
for o in original_state:
name = o[0]
if i.name == name:
i.cs=o[1]
i.enabled = o[2]
return {'FINISHED'}
classes = ([
SimpleBake_OT_RefreshAOVList
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+287
View File
@@ -0,0 +1,287 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty
from .utils import select_only_this
AUTO_CAGE_TARGET_HIDDEN_MESSAGE = (
"Auto-generate cage requires the target object to be visible in the current viewport. "
"Unhide the target object first."
)
def target_is_visible(context, obj):
return obj is not None and obj.name in context.view_layer.objects and obj.visible_get()
def get_auto_cage_target(context, target_name=""):
sbp = context.scene.SimpleBake_Props
if target_name:
return context.scene.objects.get(target_name)
if sbp.global_mode == "PBR":
return sbp.targetobj
if sbp.global_mode == "CyclesBake":
return sbp.targetobj_cycles
return None
def auto_cage_poll_message(cls, message):
if hasattr(cls, "poll_message_set"):
cls.poll_message_set(message)
def set_parent(context, child_name, parent_name):
"""
Sets the parent of the specified child object to the specified parent object.
:param child_name: The name of the child object.
:param parent_name: The name of the parent object.
"""
# Get the child and parent objects
child = context.scene.objects.get(child_name)
parent = context.scene.objects.get(parent_name)
# Check if both objects exist
if child is None:
print(f"Child object '{child_name}' not found.")
return
if parent is None:
print(f"Parent object '{parent_name}' not found.")
return
original_matrix = child.matrix_world.copy()
# Set the parent
child.parent = parent
child.matrix_parent_inverse = parent.matrix_world.inverted()
child.matrix_world = original_matrix
# Function to duplicate an object and apply all modifiers
def duplicate_and_apply_modifiers(context, obj_name):
obj = context.scene.objects.get(obj_name)
if obj is None:
print(f"Object '{obj_name}' not found")
return None
# Duplicate the object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
bpy.ops.object.duplicate()
new_obj = context.view_layer.objects.active
new_obj.name = f"{obj_name}_cage"
new_obj["SB_bake_operation_id"] = "Blah"
#new_obj.location = obj.location.copy()
#new_obj.rotation_euler = obj.rotation_euler.copy()
#new_obj.scale = obj.scale.copy()
# Apply all modifiers
for modifier in new_obj.modifiers:
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except:
new_obj.modifiers.remove(modifier)
new_obj.hide_select = True
return new_obj.name
# Function to remove all materials from an object
def remove_all_materials(obj):
obj.data.materials.clear()
# Function to add a new material to an object and set its viewport color
def add_new_material(obj, mat_name, color, alpha):
# Create a new material
mat = bpy.data.materials.new(name=mat_name)
# Set the diffuse color
mat.diffuse_color = (color[0], color[1], color[2], alpha)
mat.use_nodes = True
if (pnode:= mat.node_tree.nodes.get("Principled BSDF")):
pnode.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha)
pnode.inputs["Alpha"].default_value = 0.5
# Assign the material to the object
obj.data.materials.append(mat)
# Set Viewport Display color
obj.color = (color[0], color[1], color[2], alpha)
obj.show_transparent = True
# Function to add a displacement modifier to an object
def add_displacement_modifier(obj):
displace = obj.modifiers.new(name='Displace', type='DISPLACE')
displace.texture = None # Set the texture to None
displace.mid_level = 1 # Set the midlevel to 1
displace.name = "SB_AUTO_CAGE"
class SimpleBake_OT_Auto_Cage(Operator):
bl_idname = "simplebake.auto_cage"
bl_label = "Auto-Cage"
bl_description = "Automatically create a cage object for the selected target object. The target must be visible and Blender must be in Object Mode"
target_name: StringProperty()
@classmethod
def poll(cls, context):
if context.mode != "OBJECT":
auto_cage_poll_message(cls, "Auto-generate cage is only available in Object Mode")
return False
sbp = context.scene.SimpleBake_Props
if sbp.global_mode == "PBR":
if sbp.targetobj == None:
auto_cage_poll_message(cls, "Choose a target object before generating an auto cage")
return False
if sbp.targetobj.type != "MESH":
auto_cage_poll_message(cls, "Auto-generate cage requires a mesh target object")
return False
if sbp.targetobj.name not in context.view_layer.objects:
auto_cage_poll_message(cls, "The target object is not in the current view layer")
return False
if not target_is_visible(context, sbp.targetobj):
auto_cage_poll_message(cls, AUTO_CAGE_TARGET_HIDDEN_MESSAGE)
return False
#Is there already an auto cage object?
t = [o.name for o in context.scene.objects if "SB_auto_cage" in o and o["SB_auto_cage"] == sbp.targetobj.name]
if len(t) > 0:
auto_cage_poll_message(cls, "An auto-generated cage already exists for this target object")
return False
elif sbp.global_mode == "CyclesBake":
if sbp.targetobj_cycles == None:
auto_cage_poll_message(cls, "Choose a target object before generating an auto cage")
return False
if sbp.targetobj_cycles.type != "MESH":
auto_cage_poll_message(cls, "Auto-generate cage requires a mesh target object")
return False
if sbp.targetobj_cycles.name not in context.view_layer.objects:
auto_cage_poll_message(cls, "The target object is not in the current view layer")
return False
if not target_is_visible(context, sbp.targetobj_cycles):
auto_cage_poll_message(cls, AUTO_CAGE_TARGET_HIDDEN_MESSAGE)
return False
#Is there already an auto cage object?
t = [o.name for o in context.scene.objects if "SB_auto_cage" in o and o["SB_auto_cage"] == sbp.targetobj_cycles.name]
if len(t) > 0:
auto_cage_poll_message(cls, "An auto-generated cage already exists for this target object")
return False
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
target_obj = get_auto_cage_target(context, self.target_name)
if not target_is_visible(context, target_obj):
self.report({'ERROR'}, AUTO_CAGE_TARGET_HIDDEN_MESSAGE)
return {'CANCELLED'}
# Main script
if self.target_name != "":
obj_name = self.target_name
elif sbp.global_mode == "PBR" and sbp.targetobj != None:
obj_name = sbp.targetobj.name
elif sbp.global_mode == "CyclesBake" and sbp.targetobj_cycles != None:
obj_name = sbp.targetobj_cycles.name
else:
#Something has gone very wrong
return {'CANCELLED'}
new_obj_name = duplicate_and_apply_modifiers(context, obj_name)
if new_obj_name:
new_obj = context.scene.objects.get(new_obj_name)
remove_all_materials(new_obj)
add_new_material(new_obj, 'SB_Cage_Mat', (1.0, 0.0, 0.0), 0.5) # Red color with 50% alpha
add_displacement_modifier(new_obj)
set_parent(context, new_obj_name, obj_name)
new_obj["SB_auto_cage"] = obj_name
context.scene.render.bake.cage_object = new_obj
#Return selection to the actual target object
select_only_this(context, context.scene.objects.get(obj_name))
return {'FINISHED'}
class SimpleBake_OT_Remove_Auto_Cage(Operator):
bl_idname = "simplebake.remove_auto_cage"
bl_label = "Remove Auto-Cage Object *for selected Target Object*"
bl_description = "Remove automatically created cage object *for selected Target Object*. Must be in object mode to run"
target_name: StringProperty()
@classmethod
def poll(cls, context):
if context.mode != "OBJECT":
return False
sbp = context.scene.SimpleBake_Props
to_name = ""
if sbp.global_mode == "PBR":
if sbp.targetobj == None: return False
if sbp.targetobj.type != "MESH": return False
if sbp.targetobj.name not in context.view_layer.objects: return False
to_name = sbp.targetobj.name
elif sbp.global_mode == "CyclesBake":
if sbp.targetobj_cycles == None: return False
if sbp.targetobj_cycles.type != "MESH": return False
if sbp.targetobj_cycles.name not in context.view_layer.objects: return False
to_name = sbp.targetobj_cycles.name
#Make sure there is a cage object to remove
t = [o.name for o in context.scene.objects if "SB_auto_cage" in o and o["SB_auto_cage"] == to_name]
if len(t) == 0:
return False
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if self.target_name != "":
obj_name = self.target_name
elif sbp.global_mode == "PBR" and sbp.targetobj != None:
obj_name = sbp.targetobj.name
elif sbp.global_mode == "CyclesBake" and sbp.targetobj_cycles != None:
obj_name = sbp.targetobj_cycles.name
else:
#Something has gone very wrong
return {'CANCELLED'}
to_remove = [o.name for o in context.scene.objects if "SB_auto_cage" in o and o["SB_auto_cage"] == obj_name]
for o_name in to_remove:
o = context.scene.objects.get(o_name)
if o != None:
bpy.data.objects.remove(o)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Auto_Cage,
SimpleBake_OT_Remove_Auto_Cage
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+200
View File
@@ -0,0 +1,200 @@
import bpy
from urllib.request import urlopen
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
import base64
from pathlib import Path
import os
import urllib.request
import shutil
from .utils import show_message_box
from .messages import print_message
def ignore_delete_errors(func, path, exc_info):
# This function is called when shutil.rmtree hits an error.
# Just ignore the error and move on.
print(f"Could not delete {path}. Ignoring. Reason: {exc_info[1]}")
class VersionControl:
#Non-Blender class
was_error = False
at_current = True
installed_version = False #Will be set by init - Tuple
installed_version_str = ""
current_version_str = ""
just_updated = False
@classmethod
def check_at_current_version(cls, no_online_check):
iv = cls.installed_version
iv = str(iv[0]) + str(iv[1]) + str(iv[2])
iv = int(iv)
#For use in the panel
cls.installed_version_str = str(cls.installed_version).replace(",",".").replace(")","").replace("(","")
#If we aren't checking online, wrap it up here
if no_online_check:
return True
#Grab the most recent version from my website
link = "http://www.toohey.co.uk/SimpleBake/currentversion4"
cver = 0
try:
f = urlopen(link, timeout=5)
cver = f.read()
cver = cver.decode("utf-8")
cls.current_version_str = cver
cver = cver.replace(".","")
cver = int(cver)
except Exception as e:
cls.was_error = True
print_message(bpy.context, "Unable to check for latest version of SimpleBake - are you online?", screen=False)
cver = iv
print_message(bpy.context, str(e), screen=False)
if cver > iv:
cls.at_current = False
print_message(bpy.context, "There is a newer version of SimpleBake available", screen=False)
else:
cls.at_current = True
print_message(bpy.context, "Your copy of SimpleBake is up to date", screen=False)
class SimpleBake_OT_Auto_Update(Operator):
"""Download and install updated version of SimpleBake"""
bl_idname = "simplebake.auto_update"
bl_label = "Update"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
try:
#Current ver URL
current_ver_url = base64.b64decode("aHR0cDovL3d3dy50b29oZXkuY28udWsvU2ltcGxlQmFrZS9TaW1wbGVCYWtlX0N1cnJlbnQ0LnppcA==").decode("utf-8")
current_ver_zip_name = "SimpleBake_Curent4.zip"
addon_dir_name = "SimpleBake"
import zipfile #only needed here
#Get the path where the addons are kept
#if bpy.utils.script_path_pref() != None:
#addons_path = Path(bpy.utils.script_path_pref())
#else:
#addons_path = Path(bpy.utils.script_path_user())
#addons_path = addons_path / "addons"
#Get the addons folder (alternative method)
addons_path = Path(os.path.dirname(__file__)).parents[0]
print_message(context, f"Addons folder: {addons_path}")
#Make our current directory the addons directory
os.chdir(str(addons_path))
#Download new SimbleBake_Current.zip to addons folder
print_message(context, "Starting download")
urllib.request.urlretrieve(current_ver_url, current_ver_zip_name)
current_ver_zip_name = "SimpleBake_Curent4.zip"
print_message(context, "Download complete")
#Delete current SimpleBake folder
addon_dir = addons_path / addon_dir_name
shutil.rmtree(str(addon_dir), onerror=ignore_delete_errors)
#Unzip
current_ver_zip = zipfile.ZipFile(current_ver_zip_name, "r")
current_ver_zip.extractall()
current_ver_zip.close()
#Delete the zip file we downlaoded
downloaded_zip = addons_path / current_ver_zip_name
downloaded_zip.unlink()
#Report
message_lines = ["SimpleBake update complete", "Please restart Blender"]
show_message_box(context, message_lines, "Update complete", icon = 'INFO')
print_message(context, "Update complete. Please restart Blender")
VersionControl.just_updated = True
return {'FINISHED'}
except Exception as e:
print_message(context, str(e))
return {'CANCELLED'}
class SimpleBake_OT_Rollback_Update(Operator):
"""Download and install previous version of SimpleBake"""
bl_idname = "simplebake.rollback_update"
bl_label = "Rollback"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
try:
#Current ver URL
current_ver_url = base64.b64decode("aHR0cDovL3d3dy50b29oZXkuY28udWsvU2ltcGxlQmFrZS9TaW1wbGVCYWtlX1ByZXZpb3VzNC56aXA=").decode("utf-8")
current_ver_zip_name = "SimpleBake_Previous4.zip"
addon_dir_name = "SimpleBake"
import zipfile #only needed here
#Get the addons folder (alternative method)
addons_path = Path(os.path.dirname(__file__)).parents[0]
print_message(context, f"Addons folder: {addons_path}")
#Make our current directory the addons directory
os.chdir(str(addons_path))
#Download new SimbleBake_Current.zip to addons folder
print_message(context, "Starting download")
urllib.request.urlretrieve(current_ver_url, current_ver_zip_name)
print_message(context, "Download complete")
#Delete current SimpleBake folder
addon_dir = addons_path / addon_dir_name
shutil.rmtree(str(addon_dir), onerror=ignore_delete_errors)
#Unzip
current_ver_zip = zipfile.ZipFile(current_ver_zip_name, "r")
current_ver_zip.extractall()
current_ver_zip.close()
#Delete the zip file we downlaoded
downloaded_zip = addons_path / current_ver_zip_name
downloaded_zip.unlink()
#Report
message_lines = ["SimpleBake rollback complete", "Please restart Blender"]
show_message_box(context, message_lines, "Rollback complete", icon = 'INFO')
print_message(context, "Rollback complete. Please restart Blender")
VersionControl.just_updated = True
return {'FINISHED'}
except Exception as e:
print_message(context, str(e))
return {'CANCELLED'}
classes = ([
SimpleBake_OT_Auto_Update,
SimpleBake_OT_Rollback_Update
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,514 @@
import bpy
from bpy.props import StringProperty, BoolProperty, IntProperty
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
import os, signal
import uuid
import tempfile
import subprocess
import sys
from pathlib import Path
from .utils import show_message_box, force_to_object_mode, SBConstants, pbr_selections_to_list, specials_selection_to_list
#from .messages import print_message
class BakeInProgress:
is_baking = False
was_error = False
class Sequence:
"""State for an image-sequence bake in progress.
The image_sequence operator is the sole writer of these flags. All
other modules must consume them via ``should_run_setup`` /
``should_run_teardown`` rather than reading the booleans directly,
so that "first-frame" / "last-frame" / "not-a-sequence" semantics
stay in one place.
"""
active = False # True while a sequence bake is running
is_first_frame = False # True during the bake of the first frame
is_last_frame = False # True during the bake of the final frame
total_frames = 0 # Number of frames covered by the sequence
@classmethod
def should_run_setup(cls):
"""Per-frame operators that do one-shot setup (master backup,
material initialise) should only run on the first frame of a
sequence or on every bake if we're not in a sequence at all.
"""
return (not cls.active) or cls.is_first_frame
@classmethod
def should_run_teardown(cls):
"""Per-frame operators that do one-shot teardown (master
restore, copy-and-apply, S2A hide, completion report, mesh
export) should only run on the final frame of a sequence
or on every bake if we're not in a sequence at all.
"""
return (not cls.active) or cls.is_last_frame
@classmethod
def reset(cls):
cls.active = False
cls.is_first_frame = False
cls.is_last_frame = False
cls.total_frames = 0
@classmethod
def reset(cls):
cls.is_baking = False
cls.was_error = False
cls.Sequence.reset()
class BackgroundBakeTasks:
#Non-blender class. BG bake task
active_tasks = []
completed_tasks = []
queued_tasks = []
@classmethod
def get_completed_or_active_bgt_by_pid(cls, pid):
pid = int(pid)
match = [t for t in cls.completed_tasks if t.pid == pid]
if len(match) == 0:
match = [t for t in cls.active_tasks if t.pid == pid]
assert(len(match)>0)
match = match[0]
return match
@classmethod
def get_queued_bgt_by_id(cls, id):
match = [t for t in cls.queued_tasks if t.id == id]
assert(len(match) == 1)
match = match[0]
return match
@classmethod
def get_bgbake_status(cls, active_bgbake):
sbp = bpy.context.scene.SimpleBake_Props
pid = active_bgbake.pid
path = Path(sbp.bgbake_temp_dir) / "progress" / str(pid)
try:
with open(str(path), "r") as progfile:
progress = int(progfile.readline())
except:
#No file yet, as no bake operation has completed yet. Holding message
progress = 0
active_bgbake.progress = progress
if progress == 100:
cls.completed_tasks.append(active_bgbake)
cls.active_tasks.remove(active_bgbake)
cls.next_please()
return progress
@classmethod
def next_please(cls):
if len(cls.active_tasks) == 0 and len(cls.queued_tasks) > 0:
t = cls.queued_tasks[0]
cls.active_tasks.append(t)
cls.queued_tasks.remove(t)
t.spawn_bg_process()
return 2
elif len(cls.active_tasks) == 0 and len(cls.queued_tasks) == 0:
#Nothing happening, kill timer
return None
else:
if len(cls.active_tasks)>0: # Just in case
active_bgt = cls.active_tasks[0]
cls.get_bgbake_status(active_bgt)
return 2
def save_file_copy(self):
sbp = bpy.context.scene.SimpleBake_Props
unique_id = str(uuid.uuid4())[0:7]
tmp_dir_path = Path(sbp.bgbake_temp_dir)
if not os.path.exists(str(tmp_dir_path)):
os.makedirs(str(tmp_dir_path))
save_path = tmp_dir_path / (unique_id + ".blend")
#Pack dirty images or we lose them on save
[i.pack() for i in bpy.data.images if i.is_dirty]
bpy.ops.wm.save_as_mainfile(filepath=str(save_path), check_existing=False, copy=True)
self.initial_save_path = save_path
return True
def spawn_bg_process(self):
sbp = bpy.context.scene.SimpleBake_Props
tmp_dir_path = sbp.bgbake_temp_dir #Already posix path
base_folder_override = Path(bpy.data.filepath).parent.as_posix()
args = [bpy.app.binary_path]
if not sbp.fake_background:
args.append("--background")
#args.append("--factory-startup")
args.append(str(self.initial_save_path))
args.append("--python-expr")
args.append(f"""
import bpy;
import os;
from pathlib import Path;
prefs = bpy.context.preferences;
bpy.ops.preferences.addon_enable(module='SimpleBake');
allowed_addons = ['io_anim_bvh', 'io_curve_svg', 'io_mesh_uv_layout', 'io_scene_fbx', 'io_scene_gltf2', 'cycles', 'pose_library', 'bl_pkg', 'SimpleBake'];
for addon in prefs.addons.keys():
if addon not in allowed_addons:
try:
bpy.ops.preferences.addon_disable(module=addon)
print(f"Disabled " + addon)
except:
print(f"Error when disabling addon " + addon)
orig_file_path = bpy.data.filepath;
savepath=str(Path('{tmp_dir_path}') / (str(os.getpid()) + '.blend'));
bpy.ops.wm.save_as_mainfile(filepath=str(savepath), check_existing=False);
os.remove(orig_file_path);
bpy.context.scene.SimpleBake_Props.bgbake_temp_dir = '{tmp_dir_path}';
bpy.context.scene.SimpleBake_Props.base_folder_override = '{base_folder_override}';
{self.start_command};
""")
process = subprocess.Popen(args, shell=False)
self.pid = process.pid
self.file_path_po = Path(tmp_dir_path) / (str(process.pid) + ".blend")
return True
def __init__(self, name, copy_and_apply, start_command, bg_bake_id):
sbp = bpy.context.scene.SimpleBake_Props
#Check for queued that need promotion
bpy.app.timers.register(BackgroundBakeTasks.next_please, first_interval=2)
# #If this is some kind of PBR bake, run the pre-bake now
# master_switch = "P" if sbp.global_mode == "PBR" else "C"
# need = (True if ("pbr" in start_command) or
# ("automatch" in start_command and master_switch == "P") or
# ("decals" in start_command)
# else False)
# if need:
# bpy.ops.simplebake.pbr_pre_bake()
self.start_command = start_command
self.save_file_copy()
self.progress = 0
if name == "": name = "Untitled"
self.name = name
self.copy_and_apply = copy_and_apply
self.bg_bake_id = bg_bake_id
self.pid = 0
self.id = str(uuid.uuid4())
# #Restore materials in the foreground file
# bpy.ops.simplebake.material_backup(mode="master_restore")
#Assume queued for now
self.__class__.queued_tasks.append(self)
#print_message(context, "BG bake task created")
class SimpleBake_OT_Import_BGBake(Operator):
"""Import baked textures and objects from the background"""
bl_idname = "simplebake.import_bgbake"
bl_label = "Import"
pid: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
bgt = BackgroundBakeTasks.get_completed_or_active_bgt_by_pid(self.pid)
if not force_to_object_mode(context):
return {'CANCELLED'}
b_img_names = [i.name for i in bpy.data.images]
b_obj_names = [o.name for o in context.scene.objects]
bg_blend_path = str(Path(sbp.bgbake_temp_dir) / (self.pid + ".blend"))
if bgt.copy_and_apply:
# Import objects + images together via collection append
savepath = bg_blend_path + "/Collection/"
bpy.ops.wm.append(filename="SimpleBake_Bakes_Background", directory=savepath,
use_recursive=False, active_collection=False)
sbc = bpy.data.collections["SimpleBake_Bakes_Background"]
sbc.name = "SimpleBake_Bakes"
# Relocate, as append leaves it in a funny place
context.scene.collection.children.link(sbc)
del_list = []
for c in bpy.data.collections:
if sbc.name in c.children:
c.children.unlink(sbc)
if len(c.children) == 0:
del_list.append(c.name)
for name in del_list:
c = bpy.data.collections.get(name)
if c is not None:
bpy.data.collections.remove(c)
else:
# Import images directly — no need to append objects just to delete them
replaced_names = set()
with bpy.data.libraries.load(bg_blend_path, link=False) as (data_from, data_to):
intended_names = list(data_from.images)
data_to.images = list(data_from.images)
# data_to.images[i] corresponds to intended_names[i].
# If the loaded image's name differs from its intended name, Blender renamed
# it due to a conflict with an existing foreground image. We compare by
# intended name (not by stripping .001/.002 suffixes) so that materials whose
# names legitimately contain ".001"/".002" are not misidentified as conflicts.
for intended, img in zip(intended_names, data_to.images):
if img is None:
continue
if "SB_bake_operation_id" not in img:
bpy.data.images.remove(img)
continue
if img.name != intended:
# Blender renamed this image — it conflicted with an existing foreground image
fg_img = bpy.data.images.get(intended)
if fg_img is not None and fg_img.get("SB_bake_operation_id") == img.get("SB_bake_operation_id"):
# Stale: identical image already in foreground — discard the import
bpy.data.images.remove(img)
else:
# New image that collided with an existing one (overwrite)
if fg_img is not None and "SB_bake_operation_id" in fg_img:
bpy.data.images.remove(fg_img)
replaced_names.add(intended)
img.name = intended
img.use_fake_user = True
else:
# Name unchanged — no conflict, freshly baked image
img.use_fake_user = True
# Exclude replaced names so count reflects actual new/updated images
b_img_names = [n for n in b_img_names if n not in replaced_names]
a_img_names = [i.name for i in bpy.data.images]
a_obj_names = [o.name for o in context.scene.objects]
message = []
message.append(f"Imported - {len(a_img_names) - len(b_img_names)} images")
message.append(f"Imported - {len(a_obj_names) - len(b_obj_names)} objects")
# Set fake_user on newly arrived images
diflist = [n for n in a_img_names if n not in b_img_names]
for i_name in diflist:
if i := bpy.data.images.get(i_name):
i.use_fake_user = True
# Duplicate handling only needed for copy_and_apply (wm.append path)
if bgt.copy_and_apply:
for name in b_img_names:
if name + ".002" in a_img_names:
bpy.data.images.remove(bpy.data.images[name])
bpy.data.images[name + ".002"].name = name
elif name + ".001" in a_img_names:
bpy.data.images.remove(bpy.data.images[name])
bpy.data.images[name + ".001"].name = name
BackgroundBakeTasks.completed_tasks.remove(bgt)
#Hide source and/or cage object if requested
for obj in context.scene.objects:
if "SB_BG_HIDE" in obj:
l = obj["SB_BG_HIDE"]
if bgt.bg_bake_id in l:
obj.hide_set(True)
show_message_box(context, message, "Import complete", icon = 'INFO')
return{'FINISHED'}
class SimpleBake_OT_Delete_Queued_BGBake(Operator):
"""Delete a queued background bake"""
bl_idname = "simplebake.delete_queued_bgbake"
bl_label = "Delete"
id: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
bgt = BackgroundBakeTasks.get_queued_bgt_by_id(self.id)
BackgroundBakeTasks.queued_tasks.remove(bgt)
return{'FINISHED'}
class SimpleBake_OT_Discard_BGBake(Operator):
"""Discard a completed background bake"""
bl_idname = "simplebake.discard_bgbake"
bl_label = "Discard"
pid: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
bgt = BackgroundBakeTasks.get_completed_or_active_bgt_by_pid(self.pid)
pid = bgt.pid
BackgroundBakeTasks.completed_tasks.remove(bgt)
path = str(bgt.file_path_po)
try:
os.remove(path)
os.remove(path.replace(".blend", ".blend1"))
except:
pass
return{'FINISHED'}
class SimpleBake_OT_Kill_Active_BGBake(Operator):
"""Kill an active background bake"""
bl_idname = "simplebake.kill_active_bgbake"
bl_label = "Kill"
pid: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
bgt = BackgroundBakeTasks.get_completed_or_active_bgt_by_pid(self.pid)
pid = bgt.pid
BackgroundBakeTasks.active_tasks.remove(bgt)
try:
os.kill(pid, signal.SIGTERM)
except OSError:
print(f"Unable to kill process {pid}")
return{'FINISHED'}
class SimpleBake_OT_Update_Progress(Operator):
bl_idname = "simplebake.update_progress"
bl_label = "Update"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
print(f"Progress update - {sbp.current_bake_image_number}")
self.background = True if "--background" in sys.argv or sbp.fake_background else False
sbp.current_bake_image_number+=1
percent_complete = (sbp.current_bake_image_number/sbp.total_bake_images_number) *100
percent_complete = round(percent_complete)
sbp.percent_complete = percent_complete
if self.background:
path = Path(sbp.bgbake_temp_dir) / "progress"
if not os.path.exists(str(path)):
os.makedirs(str(path))
pid = os.getpid()
path = path / str(pid)
with open(str(path), "w") as progfile:
progfile.write(str(percent_complete))
return{'FINISHED'}
#Non-Blender class
class BakeProgress:
def __init__(self, mode, objnum, hl_matches=None):
sbp = bpy.context.scene.SimpleBake_Props
t=0
if mode == SBConstants.PBRS2A_DECALS:
ps = len(pbr_selections_to_list(bpy.context))
ss = len(specials_selection_to_list(bpy.context))
#The S2A element
t = (ps+ss) * 1
#The non-S2A element
t+= ps * 1
sbp.total_bake_images_number = t
if mode == SBConstants.PBR:
ps = len(pbr_selections_to_list(bpy.context))
print(f"Got {ps} pbr bakes")
ss = len(specials_selection_to_list(bpy.context))
print(f"Got {ss} pbr bakes")
t = (ps + ss) * objnum
print(f"Total of {t} based on {ps+ss} x {objnum}")
sbp.total_bake_images_number = t
if mode == SBConstants.PBRS2A:
ps = len(pbr_selections_to_list(bpy.context))
ss = len(specials_selection_to_list(bpy.context))
o = 1 #S2A bake
t = (ps + ss) * o
sbp.total_bake_images_number = t
if mode == SBConstants.CYCLESBAKE:
cb = 1
ss = len(specials_selection_to_list(bpy.context))
t = (cb + ss) * objnum
sbp.total_bake_images_number = t
if mode == SBConstants.CYCLESBAKE_S2A:
cb = 1 #CyclesBake
ss = len(specials_selection_to_list(bpy.context))
objnum = 1 #S2A bake
t = (cb + ss) * objnum
sbp.total_bake_images_number = t
if mode in [SBConstants.PBRS2A_AUTOMATCH, SBConstants.CYCLESBAKES2A_AUTOMATCH]:
#Cycles bake always has only one bake mode
ps = len(pbr_selections_to_list(bpy.context)) if mode == SBConstants.PBRS2A_AUTOMATCH else 1
ss = len(specials_selection_to_list(bpy.context))
t = len(hl_matches) * (ps + ss)
sbp.total_bake_images_number = t
classes = ([
SimpleBake_OT_Update_Progress,
SimpleBake_OT_Import_BGBake,
SimpleBake_OT_Delete_Queued_BGBake,
SimpleBake_OT_Kill_Active_BGBake,
SimpleBake_OT_Discard_BGBake
])
def register():
global classes
BakeInProgress.reset()
for cls in classes:
register_class(cls)
def unregister():
global classes
BakeInProgress.reset()
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,32 @@
from . import pbr_bake_operators
from . import common_bake_support
from . import specials_bake_operators
from . import pbr_bake_support_operators
from . import cycles_bake_operators
from . import cyclesbake_support_operators
from . import auto_match_operators
from . import decal_operators
from . import image_sequence
def register():
pbr_bake_operators.register()
pbr_bake_support_operators.register()
common_bake_support.register()
specials_bake_operators.register()
cycles_bake_operators.register()
cyclesbake_support_operators.register()
auto_match_operators.register()
decal_operators.register()
image_sequence.register()
def unregister():
pbr_bake_operators.unregister()
pbr_bake_support_operators.unregister()
common_bake_support.unregister()
specials_bake_operators.unregister()
cycles_bake_operators.unregister()
cyclesbake_support_operators.unregister()
auto_match_operators.unregister()
decal_operators.unregister()
image_sequence.unregister()
@@ -0,0 +1,460 @@
#TODO - Background?
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, Macro
import uuid
import sys
from .common_bake_support import CommonBakePrepandFinish, match_high_low_objects, clean_up_after_bg_spawn, common_prestart
from ..utils import pbr_selections_to_list, specials_selection_to_list, SBConstants, is_blend_saved, force_to_object_mode, find_3d_viewport
from ..background_and_progress import BakeInProgress, BakeProgress, BackgroundBakeTasks
from ..material_management import SimpleBake_OT_Material_Backup as MatManager
from .pbr_bake_support_operators import sample_nodes_present
from .specials_bake_operators import add_specials_to_macro, add_specials_to_macro_s2a
from .. import __package__ as base_package
from ..messages import print_message
def determine_cage_settings(context, op, lo, MACRO):
sbp = context.scene.SimpleBake_Props
#Cage---------
#If we are auto matching in name mode, try to auto assign a cage
if sbp.auto_match_mode == "name":
parts = lo.split("_")
base_name = '_'.join(parts[:-1]).lower()
for o in context.scene.objects:
if o.name.lower() == f"{base_name}_cage":
op.properties.use_cage = True
op.properties.cage_object = o.name
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Using cage object {o.name}"
return True
#Wasn't name more, or didn't find a cage
if sbp.cage_smooth_hard == "smooth":
op.properties.use_cage = True
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Using auto-cage (smooth)"
elif sbp.cage_smooth_hard == "hard":
op.properties.use_cage = False
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Using auto-cage (hard)"
return True
def set_bake_op_settings(context, op, master_switch, bake_mode):
sbp = context.scene.SimpleBake_Props
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.use_selected_to_active=True
op.properties.margin = context.scene.render.bake.margin
try: op.properties.margin_type = context.scene.render.bake.margin_type
except:pass
#PBR
if master_switch=="P":
op.properties.type = "NORMAL" if bake_mode == SBConstants.PBR_NORMAL else "EMIT"
#CycesBake
else:
op.properties.type = context.scene.cycles.bake_type
op.properties.normal_space = context.scene.render.bake.normal_space
op.properties.normal_r = context.scene.render.bake.normal_r
op.properties.normal_g = context.scene.render.bake.normal_g
op.properties.normal_b = context.scene.render.bake.normal_b
op.properties.max_ray_distance = (sbp.ray_distance * sbp.cage_and_ray_multiplier)
def image_post_processing(context, MACRO, bake_mode, bake_operation_id, master_switch):
sbp = context.scene.SimpleBake_Props
#Scale all baked images if needed (catching this latest one)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
#Pack them
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#Denoise if requested and CB
if master_switch=="C" and sbp.rundenoise:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Running compositor denoiser"
op = MACRO.define("SIMPLEBAKE_OT_compositor_denoise")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_type = context.scene.cycles.bake_type
#Save bakes external
if sbp.save_bakes_external:
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#MUST COME AFTER SAVE EXTERNAL
#Glossiness and DirectX
if master_switch == "P":
#if sbp.rough_glossy_switch == SBConstants.PBR_GLOSSY:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Inverting roughness to create glossy"
op = MACRO.define("SIMPLEBAKE_OT_invert_roughness_to_glossy")
op.properties.bake_operation_id = bake_operation_id
if sbp.normal_format_switch == SBConstants.NORMAL_DIRECTX:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating DirectX normal from OpenGL map"
op = MACRO.define("SIMPLEBAKE_OT_create_directx_normal")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
class SimpleBake_OT_AutoMatch_Operation_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.automatch_operation_background"
bl_description = "Commence background bake for AutoMatch"
bl_label = "Bake (Background)"
hl_matches = {}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
if not common_prestart(context, automatch=True):
return {'CANCELLED'}
#Get the high to low matches dict. Abort if no matches
orig_objects_list = []
match_mode = "NAME" if sbp.auto_match_mode=="name" else "POSITION"
dummy = match_high_low_objects(context, __class__.hl_matches, orig_objects_list, match_mode)
#If only one match, we shouldn't do a merged bake
if len(__class__.hl_matches)==1:
sbp.merged_bake = False
#Start operator call for call back
start_command = "bpy.ops.simplebake.automatch_operation()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
for l in list(__class__.hl_matches.keys()):
l = context.scene.objects[l]
append_to_bg_hide_list(l, bg_bake_id)
for h in list(__class__.hl_matches.values()):
h = context.scene.objects[h]
append_to_bg_hide_list(h, bg_bake_id)
if sbp.hide_cage_object and context.scene.render.bake.cage_object != None:
append_to_bg_hide_list(context.scene.render.bake.cage_object, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_AutoMatch_Macro(Macro):
bl_idname = "simplebake.pbr_automatch_macro"
bl_options = {'BLOCKING', 'INTERNAL'}
bl_label = "Go"
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_AutoMatch_Operation(Operator):
"""Start PBR AutoMatch High Low Bake"""
bl_idname = "simplebake.automatch_operation"
bl_description = "Commence AutoMatch Bake"
bl_label = "Bake"
hl_matches = {}
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
#We need to make sure we do this before pre-start because prestart processes UVs and hl_matches needs to be populated by then
orig_objects_list=[]
match_mode = "NAME" if sbp.auto_match_mode=="name" else "POSITION"
dummy = match_high_low_objects(context, __class__.hl_matches, orig_objects_list, match_mode)
if not self.in_background:
if not common_prestart(context, automatch=True):
return {'CANCELLED'}
#We need to know what overall mode is on the panel
master_switch = "P" if sbp.global_mode == "PBR" else "C"
bake_modes = pbr_selections_to_list(context) if master_switch=="P" else [context.scene.cycles.bake_type]
if not force_to_object_mode(context):
return {'CANCELLED'}
#Set up our macro
MACRO = SimpleBake_OT_AutoMatch_Macro
MACRO.clean()
#Bake op ID
bake_operation_id = str(uuid.uuid4())
#If only one match, we shouldn't do a merged bake
if len(__class__.hl_matches)==1:
sbp.merged_bake = False
#Now we know what we are doing, call bake progress
mode = SBConstants.PBRS2A_AUTOMATCH if master_switch=="P" else SBConstants.CYCLESBAKES2A_AUTOMATCH
BakeProgress(mode, len(orig_objects_list), hl_matches=__class__.hl_matches)
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
if master_switch=="P":
#Pre-bake (node groups and then reroute nodes)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Pre bake starting"
if not bpy.app.version >= (4,1,0):
MACRO.define("SIMPLEBAKE_OT_pbr_pre_bake_old")
MACRO.define("SIMPLEBAKE_OT_remove_reroutes")
if master_switch=="P":
#PBR specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
else:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs (Op will adjust for auto match)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
#Will boost be needed for later on?
if master_switch=="P":
boost_needed = False
for hi in __class__.hl_matches.values():
boost_needed = True if sample_nodes_present(context, hi) else boost_needed
#CyclesBake - just don't use this at all
else:
boost_needed = False
#Need to loop through each pair
run_count=0
for lo in __class__.hl_matches.keys():
run_count += 1
last_run = True if run_count==(len(__class__.hl_matches.keys())) else False
if last_run:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Last run"
hi = __class__.hl_matches[lo]
for bake_mode in bake_modes:
#Create image for this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Bake image creation (low objects)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = lo #!!
op.properties.global_mode = "AutoMatch" #Just used for the image name
#No setup of high objects for Cycles Bake
if master_switch=="P":
#Setup the high
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A source objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = hi
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Prepare high for this bake mode (no image)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material prep (Auto Match Highs)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = hi
op.properties.this_bake = bake_mode
op.properties.no_bake_image_node = True
#Setup the low
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A source objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = lo
op.properties.mode = MatManager.MODE_WORKING_BACKUP
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Object preparation (Auto Match Lows)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = lo #!!!
op.properties.this_bake = bake_mode
op.properties.only_bake_image_node = True
#Select specific high and low objects
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Select objects for S2A auto match setup"
op = MACRO.define("SIMPLEBAKE_OT_select_selected_to_active")
op.properties.mode = SBConstants.PBRS2A_AUTOMATCH #TODO - Refactor. Works for CB but name shouldnt be specific
op.properties.specific_low = lo
op.properties.specific_high = hi
if sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.object_name = lo
op.properties.high_object_name = hi
op.properties.auto_match = True
#Find and pass the cage for this pair so it stays visible during the bake
if sbp.auto_match_mode == "name":
parts = lo.split("_")
base_name = '_'.join(parts[:-1]).lower()
for scene_obj in context.scene.objects:
if scene_obj.name.lower() == f"{base_name}_cage":
op.properties.cage_object_name = scene_obj.name
break
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
def do_bake():
if master_switch=="P": #CyclesBake is just left at scene setting
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = sbp.boosted_sample_count if boost_needed else prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
#Set the core bake options for the bake operation
set_bake_op_settings(context, op, master_switch, bake_mode)
#Work out cage settings
determine_cage_settings(context, op, lo, MACRO)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore the materials WORKING MODE
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#Do scale, glossiness, directx, pack, denoise, export etc.
if not sbp.merged_bake or (sbp.merged_bake and last_run):
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Last run for {bake_mode}, calling post processing"
image_post_processing(context, MACRO, bake_mode, bake_operation_id, master_switch)
#-----END OF BAKE MODE LOOP---------------------
#Specials!
if len(specials_selection_to_list(context))>0:
for lo in __class__.hl_matches.keys():
add_specials_to_macro_s2a(MACRO, context, bake_operation_id,
specific_high=__class__.hl_matches[lo], specific_low=lo, mode=SBConstants.PBRS2A_AUTOMATCH) #TODO - Works for CB but name should be changed
#All images now baked
#Copy and apply
if sbp.copy_and_apply or sbp.save_obj_external or sbp.apply_bakes_to_original or self.in_background:
for lo in __class__.hl_matches.keys():
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = lo
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = SBConstants.PBRS2A if master_switch=="P" else SBConstants.CYCLESBAKE_S2A
#Just PBR or CB would be OK too
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = bake_operation_id
#Save mesh external
if sbp.save_obj_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = bake_operation_id
if master_switch=="P":
#PBR specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Multiply Diffuse by AO?
if sbp.multiply_diffuse_ao == "diffusexao":
op = MACRO.define("SIMPLEBAKE_OT_multiply_ao")
op.properties.bake_operation_id = bake_operation_id
#Channel packing
cp_op = "SIMPLEBAKE_OT_channel_packing"
op = MACRO.define(cp_op)
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = SBConstants.PBRS2A #Only used for the texture name
#CyclesBake
else:
#CyclesBake specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
op.properties.bake_operation_id = bake_operation_id
#----------------------------------------------------------------------------------
#Setup and then start the Macro
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
#Run macro
bpy.ops.simplebake.pbr_automatch_macro('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
classes = ([
SimpleBake_OT_AutoMatch_Operation,
SimpleBake_OT_AutoMatch_Macro,
SimpleBake_OT_AutoMatch_Operation_Background
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,588 @@
import bpy
import uuid
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, Macro
from bpy.props import StringProperty, BoolProperty
import sys
from ..background_and_progress import BackgroundBakeTasks, BakeInProgress, BakeProgress
from ..utils import (SBConstants, select_only_this,
show_message_box, specials_selection_to_list, force_to_object_mode,
find_closest_obj)
from ..material_management import SimpleBake_OT_Material_Backup as MatManager
from .specials_bake_operators import add_specials_to_macro, add_specials_to_macro_s2a
from ..messages import print_message
from .common_bake_support import match_high_low_objects, common_prestart, clean_up_after_bg_spawn, CommonBakePrepandFinish
import re
from .. import __package__ as base_package
class SimpleBake_OT_CyclesBake_Bake_Macro(Macro):
bl_idname = "simplebake.cyclesbake_bake_macro"
bl_label = "Go"
bl_options = {'BLOCKING', 'INTERNAL'}
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_Bake_Operation_CyclesBake_s2a_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_cyclesbake_s2a_background"
bl_description = "Commence background bake for CyclesBake S2A"
bl_label = "Bake (Background)"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
if not common_prestart(context):
return {'CANCELLED'}
#Start operator call for call back
start_command = "bpy.ops.simplebake.bake_operation_cyclesbake_s2a()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
if sbp.hide_source_objects:
for obj in sbp.objects_list:
o = obj.obj_point
append_to_bg_hide_list(o, bg_bake_id)
#Also hide target object
if sbp.targetobj_cycles != None:
append_to_bg_hide_list(sbp.targetobj_cycles, bg_bake_id)
if sbp.hide_cage_object and context.scene.render.bake.cage_object != None:
append_to_bg_hide_list(context.scene.render.bake.cage_object, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_Bake_Operation_CyclesBake_S2A(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_cyclesbake_s2a"
bl_description = "Commence bake for CyclesBake S2A"
bl_label = "Bake"
first_run: BoolProperty(default=True)
last_run: BoolProperty(default=False)
bake_id_override: StringProperty(default="")
_timer = None
hl_matches = {}
orig_objects_list = []
was_error = False
objects_list_restored = False
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
if BakeInProgress.is_baking: return False
else: return True
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
#Always remove the timer if we are not baking
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
if not self.in_background:
if not common_prestart(context):
return {'CANCELLED'}
#Set bake ID, override if needed
if self.bake_id_override != "":
self.bake_operation_id = self.bake_id_override
else:
self.bake_operation_id = str(uuid.uuid4())
#Objects
bake_objects_names = [o.obj_point.name for o in sbp.objects_list]
if sbp.targetobj_cycles != None: #Starting checks will catch this later if it's None
target_object_name = sbp.targetobj_cycles.name
if target_object_name in bake_objects_names:
bake_objects_names.remove(target_object_name)
BakeProgress(SBConstants.CYCLESBAKE_S2A, 1)
#-----------------------------------------------------
#Sidestep any geometary nodes
bpy.ops.simplebake.sidestep_geo_nodes()
if not force_to_object_mode(context):
return {'CANCELLED'}
MACRO = SimpleBake_OT_CyclesBake_Bake_Macro
#Clean macro
MACRO.clean()
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
#CyclesBake specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs (target object only)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A target object)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = target_object_name #!!!
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Create bake image if needed for TARGET OBJECT and this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Bake image creation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.this_bake = context.scene.cycles.bake_type
op.properties.target_object_name = target_object_name #!!
op.properties.global_mode = SBConstants.CYCLESBAKE_S2A
#Set bake image on TARGET OBJECT
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Object preperation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.target_name = target_object_name #!!!
op.properties.this_bake = context.scene.cycles.bake_type
op.properties.only_bake_image_node = True
#Select objects
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Select objects for S2A bake setup"
op = MACRO.define("SIMPLEBAKE_OT_select_selected_to_active")
op.properties.mode = SBConstants.CYCLESBAKE_S2A
if sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.object_name = target_object_name
op.properties.isolate_s2a = True
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
def do_bake():
op = MACRO.define("OBJECT_OT_bake")
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.use_selected_to_active=True
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
op.properties.type = context.scene.cycles.bake_type
op.properties.normal_space = context.scene.render.bake.normal_space
op.properties.normal_r = context.scene.render.bake.normal_r
op.properties.normal_g = context.scene.render.bake.normal_g
op.properties.normal_b = context.scene.render.bake.normal_b
op.properties.max_ray_distance = (sbp.ray_distance * sbp.cage_and_ray_multiplier)
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
#Cage---------
if context.scene.render.bake.cage_object != None:
op.properties.use_cage = True
op.properties.cage_object = context.scene.render.bake.cage_object.name
elif sbp.cage_smooth_hard == "smooth":
op.properties.use_cage = True
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
elif sbp.cage_smooth_hard == "hard":
op.properties.use_cage = False
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore all materials (scene wide) for next bake mode (if any)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#Scale all baked images if needed (catching this latest one)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = self.bake_operation_id
#Pack
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Packing images into blend file"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.bake_mode = context.scene.cycles.bake_type
#Save bakes external?
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving bake externally"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.bake_mode = context.scene.cycles.bake_type
#Denoise if requested MUST COME AFTER SAVE EXTERNAL
if sbp.rundenoise:
op = MACRO.define("SIMPLEBAKE_OT_compositor_denoise")
op.properties.bake_operation_id = self.bake_operation_id
#Cycles specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Add specials to the macro queue
if len(specials_selection_to_list(context))> 0:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Adding specials to macro queue (CB S2A)"
add_specials_to_macro_s2a(MACRO, context, self.bake_operation_id)#!!
#All bakes done, copy and apply, save_obj_external or in background??
if sbp.copy_and_apply or sbp.save_obj_external or self.in_background or sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="All bakes done, creating copy and apply object"
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = target_object_name
op.properties.bake_operation_id = self.bake_operation_id
op.properties.global_mode = SBConstants.CYCLESBAKE_S2A
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = self.bake_operation_id
if sbp.save_obj_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = self.bake_operation_id
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
#op.properties.baked_number = sbp.total_bake_images_number
op.properties.bake_operation_id = self.bake_operation_id
#--------------------------------------------------------------------------------
# if not self.in_background: #Cases a crash in background!
# message_list=([
# "Foreground bake has started",
# "Please wait...."
# ])
# show_message_box(context, message_list, "SIMPLEBAKE - FOREGROUND BAKE STARTED", icon = 'INFO')
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
bpy.ops.simplebake.cyclesbake_bake_macro('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
class SimpleBake_OT_Bake_Operation_CyclesBake_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_cyclesbake_background"
bl_description = "Commence background bake for CyclesBake (non S2A)"
bl_label = "Bake (Background)"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
if not common_prestart(context):
return {'CANCELLED'}
#Start operator call for call back
start_command = "bpy.ops.simplebake.bake_operation_cyclesbake()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
if sbp.hide_source_objects:
for obj in sbp.objects_list:
o = obj.obj_point
append_to_bg_hide_list(o, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_Bake_Operation_CyclesBake(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_cyclesbake"
bl_description = "Commence bake for CyclesBake (non S2A)"
bl_label = "Bake"
_timer = None
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
if BakeInProgress.is_baking: return False
else: return True
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
if not self.in_background:
if not common_prestart(context):
return {'CANCELLED'}
bake_operation_id = str(uuid.uuid4())
bake_objects_names = [o.obj_point.name for o in sbp.objects_list]
#if sbp.everything_16bit: s8or16 = "16"
#else: s8or16 = "8"
#Progress Tracking
BakeProgress(SBConstants.CYCLESBAKE, len(bake_objects_names))
#-----------------------------------------------------
if not force_to_object_mode(context):
return {'CANCELLED'}
MACRO = SimpleBake_OT_CyclesBake_Bake_Macro
#Clean macro
MACRO.clean()
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
#CyclesBake specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake specific bake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
obj_counter = 1
for obj_name in bake_objects_names:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting object: {obj_name}"
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake mode: {context.scene.cycles.bake_type}"
last_obj = True if obj_counter == len(bake_objects_names) else False
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = obj_name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Create bake image if needed for this object and this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating bake image"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = context.scene.cycles.bake_type
op.properties.target_object_name = obj_name
op.properties.global_mode = SBConstants.CYCLESBAKE
#Configure all object materials for this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Material prep for {context.scene.cycles.bake_type}"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = obj_name
op.properties.this_bake = context.scene.cycles.bake_type
op.properties.only_bake_image_node = True
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
op = MACRO.define("SIMPLEBAKE_OT_select_only_this")
op.properties.object_name = obj_name
if sbp.isolate_objects:
op.properties.isolate = True
def do_bake():
op = MACRO.define("OBJECT_OT_bake")
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.type = context.scene.cycles.bake_type
op.properties.normal_space = context.scene.render.bake.normal_space
op.properties.normal_r = context.scene.render.bake.normal_r
op.properties.normal_g = context.scene.render.bake.normal_g
op.properties.normal_b = context.scene.render.bake.normal_b
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore all materials (scene wide) for next bake mode (if any)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#-----------If not a merged bake, done with this image.
#-----------If merged bake, only done if this is last object
if not sbp.merged_bake or (sbp.merged_bake and last_obj):
#LAST OPERATION
#Scale all baked images if needed
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
#Pack
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Packing images into blend file"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = context.scene.cycles.bake_type
#Save bakes external?
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving bake externally"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = context.scene.cycles.bake_type
#Denoise if requested MUST COME AFTER SAVE EXTERNAL
if sbp.rundenoise:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Running compositor denoiser"
op = MACRO.define("SIMPLEBAKE_OT_compositor_denoise")
op.properties.bake_operation_id = bake_operation_id
#AA if requested MUST COME AFTER SAVE EXTERNAL
# if sbp.do_aa:
# MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Running compositor aa"
# op = MACRO.define("SIMPLEBAKE_OT_compositor_aa")
# op.properties.bake_operation_id = bake_operation_id
#---Done with this object---
obj_counter += 1
#---Done with all objects---
#CyclesBake specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="CyclesBake specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_cyclesbake_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Add specials to the macro queue
if len(specials_selection_to_list(context))> 0:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Adding specials to macro queue"
add_specials_to_macro(MACRO, context, bake_operation_id, bake_objects_names)
#All bakes done, copy and apply, save_obj_external or in background??
if sbp.copy_and_apply or sbp.save_obj_external or self.in_background or sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="All bakes done, creating copy and apply"
for obj_name in list(sbp['SB_orig_bake_objects']):
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = obj_name
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = SBConstants.CYCLESBAKE
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = bake_operation_id
if sbp.save_obj_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = bake_operation_id
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
#op.properties.baked_number = sbp.total_bake_images_number
op.properties.bake_operation_id = bake_operation_id
#--------------------------------------------------------------------------------
if not self.in_background: #Cases a crash in background!
message_list=([
"Foreground bake has started",
"Please wait...."
])
show_message_box(context, message_list, "SIMPLEBAKE - FOREGROUND BAKE STARTED", icon = 'INFO')
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
bpy.ops.simplebake.cyclesbake_bake_macro('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
classes = ([
SimpleBake_OT_Bake_Operation_CyclesBake,
SimpleBake_OT_Bake_Operation_CyclesBake_S2A,
SimpleBake_OT_CyclesBake_Bake_Macro,
SimpleBake_OT_Bake_Operation_CyclesBake_Background,
SimpleBake_OT_Bake_Operation_CyclesBake_s2a_Background
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,59 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty
from ..utils import SBConstants
from ..messages import print_message
class SimpleBake_OT_CyclesBake_Specific_Bake_Prep_And_Finish(Operator):
"""Preperation and finishing specific to PBR baking"""
bl_idname = "simplebake.cyclesbake_specific_bake_prep_and_finish"
bl_description = "Preperation or Finish for CyclesBake bake type"
bl_label = "Prepare or Finish"
mode: StringProperty()
orig_sample_count = 0
orig_object_render_vis = {}
def finish(self, context):
print_message(context, "CyclesBake specific bake finishing actions")
#Restore original object visibility
for obj in context.scene.objects:
if obj.name in __class__.orig_object_render_vis:
obj.hide_render = __class__.orig_object_render_vis[obj.name]
def prepare(self, context):
print_message(context, "CyclesBake specific bake prep actions")
#Record original object visibility
for obj in context.scene.objects:
__class__.orig_object_render_vis[obj.name] = obj.hide_render
def execute(self, context):
if self.mode == "prepare":
self.prepare(context)
elif self.mode == "finish":
self.finish(context)
return {'FINISHED'}
classes = ([
SimpleBake_OT_CyclesBake_Specific_Bake_Prep_And_Finish
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,424 @@
#TODO - Background?
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, Macro
import uuid
import sys
from .common_bake_support import CommonBakePrepandFinish, common_prestart, clean_up_after_bg_spawn
from ..utils import (pbr_selections_to_list, specials_selection_to_list, SBConstants, force_to_object_mode)
from ..background_and_progress import BakeInProgress, BakeProgress, BackgroundBakeTasks
from ..material_management import SimpleBake_OT_Material_Backup as MatManager
from .pbr_bake_support_operators import sample_nodes_present
from .specials_bake_operators import add_specials_to_macro_s2a
from ..messages import print_message
from .. import __package__ as base_package
def determine_cage_settings(context, op, lo, MACRO):
sbp = context.scene.SimpleBake_Props
#Cage---------
#If we are auto matching in name mode, try to auto assign a cage
if context.scene.render.bake.cage_object != None:
op.properties.use_cage = True
op.properties.cage_object = context.scene.render.bake.cage_object.name
elif sbp.cage_smooth_hard == "smooth":
op.properties.use_cage = True
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
elif sbp.cage_smooth_hard == "hard":
op.properties.use_cage = False
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
return True
def set_bake_op_settings(context, op, bake_mode, s2a=True):
sbp = context.scene.SimpleBake_Props
if s2a:
op.properties.use_selected_to_active=True
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.margin = context.scene.render.bake.margin
try: op.properties.margin_type = context.scene.render.bake.margin_type
except:pass
op.properties.type = "NORMAL" if bake_mode == SBConstants.PBR_NORMAL else "EMIT"
op.properties.max_ray_distance = (sbp.ray_distance * sbp.cage_and_ray_multiplier)
def image_post_processing(context, MACRO, bake_mode, bake_operation_id):
sbp = context.scene.SimpleBake_Props
#Scale all baked images if needed (catching this latest one)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
#Pack them
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#Save bakes external
if sbp.save_bakes_external:
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#MUST COME AFTER SAVE EXTERNAL
#Glossiness and DirectX
#if sbp.rough_glossy_switch == SBConstants.PBR_GLOSSY:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Inverting roughness to create glossy"
op = MACRO.define("SIMPLEBAKE_OT_invert_roughness_to_glossy")
op.properties.bake_operation_id = bake_operation_id
if sbp.normal_format_switch == SBConstants.NORMAL_DIRECTX:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating DirectX normal from OpenGL map"
op = MACRO.define("SIMPLEBAKE_OT_create_directx_normal")
op.properties.bake_operation_id = bake_operation_id
class SimpleBake_OT_Decals_Operation_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_decals_background"
bl_description = "Commence background bake for Decals"
bl_label = "Bake (Background)"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
if not common_prestart(context, decals=True):
return {'CANCELLED'}
#Start operator call for call back
start_command = "bpy.ops.simplebake.bake_operation_decals()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
if sbp.hide_source_objects:
for obj in sbp.objects_list:
o = obj.obj_point
append_to_bg_hide_list(o, bg_bake_id)
#Also hide target object
if sbp.targetobj != None:
append_to_bg_hide_list(sbp.targetobj, bg_bake_id)
if sbp.hide_cage_object and context.scene.render.bake.cage_object != None:
append_to_bg_hide_list(context.scene.render.bake.cage_object, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_Decals_Macro(Macro):
bl_idname = "simplebake.decals_macro"
bl_options = {'BLOCKING', 'INTERNAL'}
bl_label = "Go"
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_Decals_Operation(Operator):
"""Start PBR Decals Bake"""
bl_idname = "simplebake.bake_operation_decals"
bl_description = "Commence Decals Bake"
bl_label = "Bake"
hl_matches = {}
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
if not self.in_background:
if not common_prestart(context, decals=True):
return {'CANCELLED'}
orig_objects_list = [i.obj_point for i in sbp.objects_list]
if not force_to_object_mode(context):
return {'CANCELLED'}
#Run after starting checks (and disable impossible)
bake_modes = pbr_selections_to_list(context)
#Set up our macro
MACRO = SimpleBake_OT_Decals_Macro
MACRO.clean()
#Bake op ID
bake_operation_id = str(uuid.uuid4())
#Now we know what we are doing, call bake progress
mode = SBConstants.PBRS2A_DECALS
BakeProgress(mode, len(orig_objects_list))
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
#Pre-bake (node groups and then reroute nodes)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Pre bake starting"
if not bpy.app.version >= (4,1,0):
MACRO.define("SIMPLEBAKE_OT_pbr_pre_bake_old")
MACRO.define("SIMPLEBAKE_OT_remove_reroutes")
#PBR specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs (Op will adjust for auto match)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
#Will boost be needed for later on?
boost_needed = False
for o in orig_objects_list:
boost_needed = True if sample_nodes_present(context, o.name) else boost_needed
#Do the S2A element of the bake
#
for bake_mode in bake_modes:
#Create image for this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Bake image creation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = sbp.targetobj.name
op.properties.global_mode = SBConstants.PBRS2A #Just used for the image name
#Backup all materials on target object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A source objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = sbp.targetobj.name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Prepare (create the image node)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Object preparation (Auto Match Lows)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = sbp.targetobj.name
op.properties.this_bake = bake_mode
op.properties.only_bake_image_node = True
#Now prepare the decal objects
for obj in orig_objects_list:
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (Decal Objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = obj.name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Prepare high for this bake mode (no image)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material prep (Decal objects)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = obj.name
op.properties.this_bake = bake_mode
op.properties.no_bake_image_node = True
#Done preparing Decal objects
#Select specific high and low objects
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Select objects for S2A auto match setup"
op = MACRO.define("SIMPLEBAKE_OT_select_selected_to_active")
op.properties.mode = SBConstants.PBRS2A
if sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.object_name = sbp.targetobj.name
op.properties.isolate_s2a = True
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
def do_bake():
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = sbp.boosted_sample_count if boost_needed else prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
#Set the core bake options for the bake operation
set_bake_op_settings(context, op, bake_mode)
#Work out cage settings
determine_cage_settings(context, op, sbp.targetobj.name, MACRO)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Do scale, glossiness, directx, pack, denoise, export etc.
image_post_processing(context, MACRO, bake_mode, bake_operation_id)
#Restore the materials WORKING MODE
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#And now the non-S2A element of the bake
#orig_bake_operation_id = bake_operation_id
bake_operation_id = "DECALSBASE_" + bake_operation_id
for bake_mode in bake_modes:
#Create image for this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Bake image creation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = sbp.targetobj.name
op.properties.global_mode = SBConstants.PBR #Just used for the image name
#Backup all materials on target object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A source objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = sbp.targetobj.name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Prepare
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Object preparation (Auto Match Lows)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = sbp.targetobj.name
op.properties.this_bake = bake_mode
#Select
op = MACRO.define("SIMPLEBAKE_OT_select_only_this")
op.properties.object_name = sbp.targetobj.name
if sbp.isolate_objects:
op.properties.isolate = True
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation (Non-S2A)"
def do_bake():
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = sbp.boosted_sample_count if boost_needed else prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
#Set the core bake options for the bake operation
set_bake_op_settings(context, op, bake_mode, s2a=False)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Do scale, glossiness, directx, pack, denoise, export etc.
image_post_processing(context, MACRO, bake_mode, bake_operation_id)
#Restore the materials WORKING MODE
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#Add specials to the macro queue
if len(specials_selection_to_list(context))> 0:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Adding specials to macro queue"
add_specials_to_macro_s2a(MACRO, context, bake_operation_id)#!!
#Copy and apply
if sbp.copy_and_apply or sbp.save_obj_external or sbp.apply_bakes_to_original or self.in_background:
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = sbp.targetobj.name
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = SBConstants.PBRS2A
op.properties.decals_override = True
#Just PBR or CB would be OK too
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = bake_operation_id
#Save mesh external
if sbp.save_obj_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = bake_operation_id
#PBR specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
op.properties.bake_operation_id = bake_operation_id
# #Multiply Diffuse by AO?
# if sbp.multiply_diffuse_ao == "diffusexao":
# op = MACRO.define("SIMPLEBAKE_OT_multiply_ao")
# op.properties.bake_operation_id = bake_operation_id
#Channel packing x2
cp_op = "SIMPLEBAKE_OT_channel_packing"
op = MACRO.define(cp_op)
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = "BaseObject" #Only used for the texture name
op = MACRO.define(cp_op)
op.properties.bake_operation_id = bake_operation_id.replace("DECALSBASE_","")
op.properties.global_mode = "Decals" #Only used for the texture name
#----------------------------------------------------------------------------------
#Setup and then start the Macro
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
#Run macro
bpy.ops.simplebake.decals_macro('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
classes = ([
SimpleBake_OT_Decals_Operation,
SimpleBake_OT_Decals_Macro,
SimpleBake_OT_Decals_Operation_Background
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,135 @@
"""Image sequence bake operator.
Drives a frame-by-frame repeat of the main PBR / Cycles bake operator.
Architecture:
- This is a modal operator that owns Blender's event loop for the
duration of the sequence. A 1-second timer polls ``Bip.is_baking``
between frames this is coarse but reliable across very different
bake durations.
- Because a modal operator needs the event loop, sequence bakes cannot
run under background mode (which fires off a headless subprocess
and exits). The UI disables background mode whenever the image
sequence toggle is on.
- This operator is the single writer of ``Bip.Sequence`` state. All
other modules must consume that state via
``Bip.Sequence.should_run_setup`` / ``should_run_teardown`` rather
than poking the raw flags.
"""
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty
from ..background_and_progress import BakeInProgress as Bip
from ..messages import print_message
def _resolve_bpy_op(cmd):
"""Resolve ``"simplebake.foo"`` → ``bpy.ops.simplebake.foo``.
Replaces an older ``eval()``-based call. Raises ValueError if the
operator can't be found or isn't callable.
"""
parts = cmd.split(".")
op = bpy.ops
for part in parts:
op = getattr(op, part, None)
if op is None:
raise ValueError(f"Unknown bpy operator: {cmd}")
if not callable(op):
raise ValueError(f"Resolved object is not callable: {cmd}")
return op
class SimpleBake_OT_Image_Sequence(Operator):
"""SimpleBake Image Sequence Operator"""
bl_idname = "simplebake.image_sequence"
bl_label = "SimpleBake Image Squence Bake Operator"
_timer = []
start_frame = 0
end_frame = 0
cmd: StringProperty()
def modal(self, context, event):
if event.type == 'TIMER':
if Bip.was_error:
print_message(context, "Cancelling Image Sequence timer after error")
self.cancel(context)
return {'CANCELLED'}
cur_frame = context.scene.frame_current
if cur_frame < self.end_frame:
if not Bip.is_baking:
#Must be after the first run
Bip.Sequence.is_first_frame = False
#Check if this will be the last run
if cur_frame + 1 == self.end_frame:
#From here on the per-frame teardown operators will
#run (master restore, copy-and-apply, completion
#message). common_bake_support calls
#Bip.Sequence.reset() once the last frame finishes.
Bip.Sequence.is_last_frame = True
#Advance frame and run the bake again
context.scene.frame_current += 1
print_message(context, f"Image sequence calling main operator for frame {context.scene.frame_current}")
_resolve_bpy_op(self.cmd)()
else:
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, f"Image sequence calling main operator: {self.cmd}")
self.start_frame = sbp.image_sequence_start_frame
self.end_frame = sbp.image_sequence_end_frame
# Inclusive range: we bake at frames start, start+1, ..., end.
Bip.Sequence.active = True
Bip.Sequence.is_first_frame = True
Bip.Sequence.is_last_frame = (self.start_frame == self.end_frame)
Bip.Sequence.total_frames = (self.end_frame - self.start_frame) + 1
context.scene.frame_set(self.start_frame)
wm = context.window_manager
self._timer.append(wm.event_timer_add(1.0, window=context.window))
wm.modal_handler_add(self)
#First run of the chosen command
_resolve_bpy_op(self.cmd)()
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
for t in self._timer:
wm.event_timer_remove(t)
classes = ([
SimpleBake_OT_Image_Sequence
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,705 @@
import bpy
import uuid
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, Macro
from bpy.props import StringProperty, BoolProperty
import sys
from ..background_and_progress import BackgroundBakeTasks, BakeInProgress, BakeProgress
from ..utils import (SBConstants, pbr_selections_to_list,
select_only_this, show_message_box, specials_selection_to_list, force_to_object_mode)
from ..material_management import SimpleBake_OT_Material_Backup as MatManager
from .specials_bake_operators import add_specials_to_macro, add_specials_to_macro_s2a
from .pbr_bake_support_operators import sample_nodes_present
from .common_bake_support import CommonBakePrepandFinish, match_high_low_objects, common_prestart, clean_up_after_bg_spawn
from ..messages import print_message
from .. import __package__ as base_package
#import re
class SimpleBake_OT_PBR_Bake_Macro(Macro):
bl_idname = "simplebake.pbr_bake_macro"
bl_label = "Go"
bl_options = {'BLOCKING', 'INTERNAL'}
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_PBR_Bake_Macro_S2A(Macro):
bl_idname = "simplebake.pbr_bake_macro_s2a"
bl_label = "Go"
bl_options = {'BLOCKING', 'INTERNAL'}
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_Bake_Operation_PBRS2A_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_pbrs2a_background"
bl_description = "Commence background bake for PBR S2A"
bl_label = "Bake (Background)"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if not common_prestart(context):
return {'CANCELLED'}
#Start operator call for call back
start_command = "bpy.ops.simplebake.bake_operation_pbrs2a()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
if sbp.hide_source_objects:
for obj in sbp.objects_list:
o = obj.obj_point
append_to_bg_hide_list(o, bg_bake_id)
#Also hide target object
if sbp.targetobj != None:
append_to_bg_hide_list(sbp.targetobj, bg_bake_id)
if sbp.hide_cage_object and context.scene.render.bake.cage_object != None:
append_to_bg_hide_list(context.scene.render.bake.cage_object, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_Bake_Operation_PBRS2A(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_pbrs2a"
bl_description = "Commence bake for PBR S2A"
bl_label = "Bake"
first_run: BoolProperty(default=True)
last_run: BoolProperty(default=False)
bake_id_override: StringProperty(default="")
_timer = None
hl_matches = {}
orig_objects_list = []
was_error = False
objects_list_restored = False
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
if BakeInProgress.is_baking: return False
else: return True
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
#Always remove the timer if we are not baking
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
if not self.in_background:
if not common_prestart(context):
return {'CANCELLED'}
#Set bake ID, override if needed
if self.bake_id_override != "":
self.bake_operation_id = self.bake_id_override
else:
self.bake_operation_id = str(uuid.uuid4())
#Objects
bake_objects_names = [o.obj_point.name for o in sbp.objects_list]
if sbp.targetobj != None: #Starting checks will catch this later if it's None
target_object_name = sbp.targetobj.name
if target_object_name in bake_objects_names:
bake_objects_names.remove(target_object_name)
BakeProgress(SBConstants.PBRS2A, len(bake_objects_names))
#-----------------------------------------------------
if not force_to_object_mode(context):
return {'CANCELLED'}
#After starting checks (and disable impossible)
pbr_bake_modes = pbr_selections_to_list(context)
MACRO = SimpleBake_OT_PBR_Bake_Macro_S2A
#Clean macro
MACRO.clean()
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
#Pre-bake (node groups and then reroute nodes)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Pre bake starting"
if not bpy.app.version >= (4,1,0):
MACRO.define("SIMPLEBAKE_OT_pbr_pre_bake_old")
MACRO.define("SIMPLEBAKE_OT_remove_reroutes")
#PBR specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs (target object only)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
#For each bake mode
for bake_mode in pbr_bake_modes:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake mode: {bake_mode}"
boost_needed = False
#For each bake object
for obj_name in bake_objects_names:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting object: {obj_name}"
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A source objects)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = obj_name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Prepare all materials for this bake mode (no image)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material prep (S2A)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.target_name = obj_name
op.properties.this_bake = bake_mode
op.properties.no_bake_image_node = True
#Done with this bake object
#Note now if we need boost
boost_needed = True if sample_nodes_present(context, obj_name) else boost_needed
#Target object only now
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (S2A target object)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = target_object_name #!!!
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Create bake image if needed for TARGET OBJECT and this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Bake image creation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = target_object_name #!!
op.properties.global_mode = SBConstants.PBRS2A
#Set bake image on TARGET OBJECT
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Object preperation (target object)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.target_name = target_object_name #!!!
op.properties.this_bake = bake_mode
op.properties.only_bake_image_node = True
#Select objects
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Select objects for S2A bake setup"
op = MACRO.define("SIMPLEBAKE_OT_select_selected_to_active")
op.properties.mode = SBConstants.PBRS2A
if sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.object_name = target_object_name
op.properties.isolate_s2a = True
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
def do_bake():
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
#Sample count
op.properties.sample_count = sbp.boosted_sample_count if boost_needed else prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.use_selected_to_active=True
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
if bake_mode == SBConstants.PBR_NORMAL: op.properties.type = "NORMAL"
else: op.properties.type = "EMIT"
op.properties.max_ray_distance = (sbp.ray_distance * sbp.cage_and_ray_multiplier)
#Cage---------
if context.scene.render.bake.cage_object != None:
op.properties.use_cage = True
op.properties.cage_object = context.scene.render.bake.cage_object.name
elif sbp.cage_smooth_hard == "smooth":
op.properties.use_cage = True
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
elif sbp.cage_smooth_hard == "hard":
op.properties.use_cage = False
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore all materials (scene wide) for next bake mode (if any)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#Scale all baked images if needed (catching this latest one)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = self.bake_operation_id
#Pack
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Packing images into blend file"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.bake_mode = bake_mode
#Save bakes external?
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving bake externally"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = self.bake_operation_id
op.properties.bake_mode = bake_mode
#MUST COME AFTER SAVE EXTERNAL
#Should we invert roughness to glossy or normal map to DirecX?
#if bake_mode == SBConstants.PBR_GLOSSY and sbp.rough_glossy_switch == SBConstants.PBR_GLOSSY:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Inverting roughness to create glossy"
op = MACRO.define("SIMPLEBAKE_OT_invert_roughness_to_glossy")
op.properties.bake_operation_id = self.bake_operation_id
if bake_mode == SBConstants.PBR_NORMAL and sbp.normal_format_switch == SBConstants.NORMAL_DIRECTX:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating DirectX normal from OpenGL map"
op = MACRO.define("SIMPLEBAKE_OT_create_directx_normal")
op.properties.bake_operation_id = self.bake_operation_id
#Done with this bake mode
#--Done with all bake modes
#PBR specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Add specials to the macro queue
if len(specials_selection_to_list(context))> 0:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Adding specials to macro queue"
add_specials_to_macro_s2a(MACRO, context, self.bake_operation_id)#!!
#All bakes done, copy and apply, save_obj_external or in background??
if not (sbp.selected_s2a == True and sbp.s2a_opmode == "decals"):
if sbp.copy_and_apply or sbp.save_obj_external or self.in_background or sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="All bakes done, creating copy and apply object"
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = target_object_name
op.properties.bake_operation_id = self.bake_operation_id
op.properties.global_mode = SBConstants.PBRS2A
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = self.bake_operation_id
if sbp.save_obj_external and not (sbp.selected_s2a == True and sbp.s2a_opmode == "decals"): #Cos decals will be saved on the second run of the non-S2A operator
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = self.bake_operation_id
#Multiply Diffuse by AO?
if sbp.multiply_diffuse_ao == "diffusexao":
op = MACRO.define("SIMPLEBAKE_OT_multiply_ao")
op.properties.bake_operation_id = self.bake_operation_id
#Channel packing
cp_op = "SIMPLEBAKE_OT_channel_packing"
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Channel packing - {cp_op}"
op = MACRO.define(cp_op)
op.properties.bake_operation_id = self.bake_operation_id
op.properties.pbrtarget_only = True
op.properties.global_mode = SBConstants.PBRS2A
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
#op.properties.baked_number = sbp.total_bake_images_number
op.properties.bake_operation_id = self.bake_operation_id
if sbp.selected_s2a == True and sbp.s2a_opmode == "decals":
op.properties.decal_first_run=True
#--------------------------------------------------------------------------------
if not self.in_background: #Cases a crash in background!
message_list=([
"Foreground bake has started",
"Please wait...."
])
show_message_box(context, message_list, "SIMPLEBAKE - FOREGROUND BAKE STARTED", icon = 'INFO')
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
bpy.ops.simplebake.pbr_bake_macro_s2a('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
class SimpleBake_OT_Bake_Operation_PBR_Background(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_pbr_background"
bl_description = "Commence background bake for PBR (non S2A)"
bl_label = "Bake (Background)"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if not common_prestart(context):
return {'CANCELLED'}
#Start operator call for call back
start_command = "bpy.ops.simplebake.bake_operation_pbr()"
#Tag objects in case we want to hide them later
bg_bake_id = str(uuid.uuid4())
def append_to_bg_hide_list(obj, bg_bake_id):
if "SB_BG_HIDE" in obj:
existing = obj["SB_BG_HIDE"]
existing.append(bg_bake_id)
obj["SB_BG_HIDE"] = existing
else:
obj["SB_BG_HIDE"] = [bg_bake_id]
if sbp.hide_source_objects:
for obj in sbp.objects_list:
o = obj.obj_point
append_to_bg_hide_list(o, bg_bake_id)
BackgroundBakeTasks(sbp.bgbake_name, sbp.copy_and_apply, start_command, bg_bake_id)
clean_up_after_bg_spawn(context)
return{'FINISHED'}
class SimpleBake_OT_Bake_Operation_PBR(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.bake_operation_pbr"
bl_description = "Commence bake for PBR (non S2A)"
bl_label = "Bake"
bake_id_override: StringProperty(default="")
decals_override: BoolProperty(default=False)
_timer = None
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
if BakeInProgress.is_baking: return False
else: return True
def modal(self, context, event):
sbp = context.scene.SimpleBake_Props
if event.type == 'TIMER':
if not BakeInProgress.is_baking:
context.window_manager.event_timer_remove(self._timer)
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
if not self.in_background:
if not common_prestart(context):
return {'CANCELLED'}
#Bake operation ID
bake_operation_id = self.bake_id_override if self.bake_id_override != "" else str(uuid.uuid4())
bake_objects_names = [o.obj_point.name for o in sbp.objects_list]
BakeProgress(SBConstants.PBR, len(bake_objects_names))
#After starting checks (and disable impossible)
pbr_bake_modes = pbr_selections_to_list(context)
#Macro
MACRO = SimpleBake_OT_PBR_Bake_Macro
MACRO.clean()
#-----------DEF NEEDED ABOVE----------------------------
#---------------------------------------------------------------------
#-----------------------------------------------------
if not force_to_object_mode(context):
return {'CANCELLED'}
#Common bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake prep starting"
MACRO.define("SIMPLEBAKE_OT_common_bake_prep")
#Pre-bake (node groups)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Pre bake starting"
if not bpy.app.version >= (4,1,0):
MACRO.define("SIMPLEBAKE_OT_pbr_pre_bake_old")
MACRO.define("SIMPLEBAKE_OT_remove_reroutes")
#PBR specific bake prep
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake prep starting"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="prepare"
#Process UVs
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Processing UVs"
#op = MACRO.define("SIMPLEBAKE_OT_process_uvs")
#Merged bake proxy if needed
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating merged bake proxy if needed"
#op = MACRO.define("SIMPLEBAKE_OT_merged_bake_proxy_create")
obj_counter = 1
for obj_name in bake_objects_names:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting object: {obj_name}"
last_obj = True if obj_counter == len(bake_objects_names) else False
for bake_mode in pbr_bake_modes:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake mode: {bake_mode}"
#Backup all materials on object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Material backup (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.target_object_name = obj_name
op.properties.mode = MatManager.MODE_WORKING_BACKUP
#Create bake image if needed for this object and this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating bake image"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = obj_name
op.properties.global_mode = SBConstants.PBR
op.properties.bake_mode = bake_mode
#Configure all object materials for this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Material prep for {bake_mode}"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = obj_name
op.properties.this_bake = bake_mode
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Starting bake operation"
op = MACRO.define("SIMPLEBAKE_OT_select_only_this")
op.properties.object_name = obj_name
if sbp.isolate_objects:
op.properties.isolate = True
def do_bake():
#Sample count
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = sbp.boosted_sample_count\
if sample_nodes_present(context, obj_name) else prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
if bake_mode == SBConstants.PBR_NORMAL: op.properties.type = "NORMAL"
else: op.properties.type = "EMIT"
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore all materials (scene wide) for next bake mode (if any)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Materials restore (working mode)"
op = MACRO.define("SIMPLEBAKE_OT_material_backup")
op.properties.mode=MatManager.MODE_WORKING_RESTORE
#-----------If not a merged bake, done with this image.
#-----------If merged bake, only done if this is last object
if not sbp.merged_bake or (sbp.merged_bake and last_obj):
#LAST OPERATION
#Scale all baked images if needed (catching this latest one)
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Scaling images if needed"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
#Pack
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Packing images into blend file"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#Save bakes external?
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving bake externally"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#---Done with this object---
obj_counter += 1
#---Done with all objects---
#MUST COME AFTER SAVE EXTERNAL
#Should we invert roughness to glossy or normal map to DirecX?
#if bake_mode == SBConstants.PBR_GLOSSY and sbp.rough_glossy_switch == SBConstants.PBR_GLOSSY:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Inverting roughness to create glossy (if needed)"
op = MACRO.define("SIMPLEBAKE_OT_invert_roughness_to_glossy")
op.properties.bake_operation_id = bake_operation_id
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Creating DirectX normal from OpenGL map (if needed)"
op = MACRO.define("SIMPLEBAKE_OT_create_directx_normal")
op.properties.bake_operation_id = bake_operation_id
#PBR specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="PBR specific bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_pbr_specific_bake_prep_and_finish")
op.properties.mode="finish"
#Add specials to the macro queue
if len(specials_selection_to_list(context))> 0:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Adding specials to macro queue"
add_specials_to_macro(MACRO, context, bake_operation_id, bake_objects_names)
#All bakes done, copy and apply, save_obj_external or in background??
if sbp.copy_and_apply or sbp.save_obj_external or self.in_background or sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="All bakes done, creating copy and apply"
for obj_name in list(sbp['SB_orig_bake_objects']):
op = MACRO.define("SIMPLEBAKE_OT_copy_and_apply")
op.properties.target_object_name = obj_name
op.properties.bake_operation_id = bake_operation_id
op.properties.global_mode = SBConstants.PBR
if self.decals_override:
op.properties.decals_override = True
#Are we applying bakes to original objects?
if sbp.apply_bakes_to_original:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Applyig bakes to original objects"
op = MACRO.define("SIMPLEBAKE_OT_apply_bakes_to_original")
op.properties.bake_operation_id = bake_operation_id
if sbp.save_obj_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving mesh object externally"
#Export obj? (Will also remove copy and apply object if not needed)
op = MACRO.define("SIMPLEBAKE_OT_save_objects_externally")
op.properties.bake_operation_id = bake_operation_id
#Multiply Diffuse by AO?
if sbp.multiply_diffuse_ao == "diffusexao":
op = MACRO.define("SIMPLEBAKE_OT_multiply_ao")
op.properties.bake_operation_id = bake_operation_id
#Channel packing
cp_op = "SIMPLEBAKE_OT_channel_packing"
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Channel packing - {cp_op}"
op = MACRO.define(cp_op)
op.properties.bake_operation_id = bake_operation_id
op.properties.pbrtarget_only = False
op.properties.global_mode = SBConstants.PBR
#Common bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Common bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_common_bake_finishing")
#op.properties.baked_number = sbp.total_bake_images_number
op.properties.bake_operation_id = bake_operation_id
if self.decals_override:
op.properties.decal_second_run=True
#--------------------------------------------------------------------------------
# if not self.in_background: #Cases a crash in background!
# message_list=([
# "Foreground bake has started",
# "Please wait...."
# ])
# show_message_box(context, message_list, "SIMPLEBAKE - FOREGROUND BAKE STARTED", icon = 'INFO')
BakeInProgress.is_baking = True
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(1, window=context.window)
bpy.ops.simplebake.pbr_bake_macro('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
classes = ([
SimpleBake_OT_Bake_Operation_PBR,
SimpleBake_OT_Bake_Operation_PBRS2A,
SimpleBake_OT_PBR_Bake_Macro,
SimpleBake_OT_PBR_Bake_Macro_S2A,
SimpleBake_OT_Bake_Operation_PBR_Background,
SimpleBake_OT_Bake_Operation_PBRS2A_Background
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes: unregister_class(cls)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,656 @@
import bpy
import os
import random
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, Macro
from bpy.props import StringProperty, BoolProperty, IntProperty
import sys
from ..utils import SBConstants, specials_selection_to_list
from ..messages import print_message
from .. import __package__ as base_package
class SimpleBake_OT_Specials_Bake_Macro(Macro):
bl_idname = "simplebake.specials_bake_macro"
bl_label = "Go"
@classmethod
def clean(cls):
try:
bpy.utils.unregister_class(cls)
except:
pass
bpy.utils.register_class(cls)
class SimpleBake_OT_Specials_Mats_Swapping(Operator):
bl_idname = "simplebake.specials_mats_swapping"
bl_label = "Record or Restore"
mode: StringProperty()
obj_name: StringProperty()
original_mats = {}
def record(self, context):
obj = context.scene.objects[self.obj_name]
obj_name = self.obj_name
orig_mats_dict = self.__class__.original_mats
i=0
for matslot in obj.material_slots:
if matslot.material != None:
if self.obj_name not in orig_mats_dict:
orig_mats_dict[self.obj_name] = {}
orig_mats_dict[self.obj_name][i] = matslot.material.name
print_message(context, f"RECORDED {matslot.material.name} for slot {i} for {obj_name}")
i+=1
self.__class__.original_mats = orig_mats_dict
def restore(self, context):
obj = context.scene.objects[self.obj_name]
obj_name = self.obj_name
orig_mats_dict = self.__class__.original_mats
i=0
for matslot in obj.material_slots:
#Remove the bake image node from our special material (for possilbe re-use)
if matslot.material != None:
nodes = matslot.material.node_tree.nodes
[nodes.remove(n) for n in nodes if "SB_bake_image_node" in n]
#Restore the material originally in this slot
try:
matslot.material = bpy.data.materials[orig_mats_dict[obj_name][i]]
print_message(context, f"RESTORED {orig_mats_dict[obj_name][i]} for slot {i} on {obj_name}")
except:
print_message(context, f"ERROR - was unable to restore material after specials bake")
i+=1
def execute(self, context):
if self.mode == "record":
self.record(context)
if self.mode == "restore":
self.restore(context)
return {'FINISHED'}
class SimpleBake_OT_Specials_Specific_Bake_Prep_And_Finish(Operator):
bl_idname = "simplebake.specials_specific_bake_prep_and_finish"
bl_label = "Prepare or Finish"
mode: StringProperty()
orig_sample_count: IntProperty()
def finish(self, context):
#print_message(context, "Specials specific bake finishing actions")
context.scene.cycles.samples = self.orig_sample_count
def prepare(self, context):
print_message(context, "Specials specific bake prep actions")
SimpleBake_OT_Setup_Col_ID.clear()
#Sample count
prefs = context.preferences.addons[base_package].preferences
context.scene.cycles.samples = prefs.pbr_sample_count2
def execute(self, context):
if self.mode == "prepare":
self.prepare(context)
elif self.mode == "finish":
self.finish(context)
return {'FINISHED'}
class SimpleBake_OT_Import_And_Assign_Specials(Operator):
bl_idname = "simplebake.import_and_assign_specials"
bl_label = "Import"
bake_mode: StringProperty()
obj_name: StringProperty()
def _strip_sb_prefixes(self, name):
if name.startswith("SBW_"):
name = name[4:]
if name.startswith("SBM_"):
name = name[4:]
return name
def execute(self, context):
sbp = context.scene.SimpleBake_Props
import_mat_name = f"SimpleBake_{self.bake_mode}"
if import_mat_name not in bpy.data.materials:
path = os.path.dirname(__file__) + "/../resources/specials.blend/Material/"
bpy.ops.wm.append(filename=import_mat_name, directory=path)
base_mat = bpy.data.materials[import_mat_name]
obj = context.scene.objects[self.obj_name]
orig_mats_dict = SimpleBake_OT_Specials_Mats_Swapping.original_mats
for slot_idx, matslot in enumerate(obj.material_slots):
if matslot.material is None:
continue
if sbp.tex_per_mat:
mat = base_mat.copy()
mat.name = f"SB_special_{self.bake_mode}_{slot_idx}"
orig_name = orig_mats_dict.get(self.obj_name, {}).get(slot_idx, matslot.material.name)
mat["SB_original_mat_name"] = self._strip_sb_prefixes(orig_name)
else:
mat = base_mat
matslot.material = mat
if "SB_bake_config" in mat:
del mat["SB_bake_config"]
for matslot in obj.material_slots:
if matslot.material is None:
continue
ao_nodes = [n for n in matslot.material.node_tree.nodes
if n.bl_idname == "ShaderNodeAmbientOcclusion"]
for ao_node in ao_nodes:
ao_node.samples = sbp.ao_sample_count
return {'FINISHED'}
class SimpleBake_OT_Setup_Vertex_Col(Operator):
bl_idname = "simplebake.setup_vertex_col"
bl_label = "Setup"
obj_name: StringProperty()
def execute(self, context):
obj = context.scene.objects[self.obj_name]
for matslot in obj.material_slots:
if matslot.material is None:
continue
nodes = matslot.material.node_tree.nodes
vc_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeVertexColor"]
if len(vc_nodes) == 1:
vc_nodes[0].layer_name = obj.data.color_attributes[0].name
return {'FINISHED'}
class SimpleBake_OT_Setup_Col_ID(Operator):
bl_idname = "simplebake.setup_col_id"
bl_label = "Setup"
obj_name: StringProperty()
existing_col_id_mats = {}
used_cols = []
@classmethod
def clear(cls):
cls.existing_col_id_mats = {}
cls.used_cols = []
def check_col_distance_from_existing(self,context, r, g, b, min_diff):
used_cols = self.__class__.used_cols
#Free pass
if len(used_cols) < 1:
used_cols.append((r,g,b))
self.__class__.used_cols = used_cols
return True
for uc in used_cols:
if round(abs(r - uc[0]),1) > min_diff or round(abs(g - uc[1]), 1) > min_diff or round(abs(b - uc[2]),1) > min_diff:
used_cols.append([r,g,b])
self.__class__.used_cols = used_cols
return True
else:
#self.__class__.used_cols = used_cols
return False #At least one rgb was too close
def _strip_sb_prefixes(self, name):
if name.startswith("SBW_"):
name = name[4:]
if name.startswith("SBM_"):
name = name[4:]
return name
def execute(self, context):
sbp = context.scene.SimpleBake_Props
obj = context.scene.objects[self.obj_name]
orig_mats_dict = SimpleBake_OT_Specials_Mats_Swapping.original_mats
for slot_idx, slot in enumerate(obj.material_slots):
orig_mat_name = slot.material.name
existing_col_id_mats = self.__class__.existing_col_id_mats
if orig_mat_name in existing_col_id_mats:
col_id_mat = existing_col_id_mats[orig_mat_name]
slot.material = col_id_mat
else:
col_id_mat = bpy.data.materials.new("SB_col_id")
slot.material = col_id_mat
col_id_mat.use_nodes = True
node_tree = col_id_mat.node_tree
nodes = node_tree.nodes
m_output_node = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output]
assert(len(m_output_node)==1)
m_output_node = m_output_node[0]
e_node = nodes.new("ShaderNodeEmission")
#First attempt
randr = random.randint(0,10) / 10
randg = random.randint(0,10) / 10
randb = random.randint(0,10) / 10
min_diff = 0.6
i=0
giveup = False
while self.check_col_distance_from_existing(context, randr,randg,randb, min_diff) == False and not giveup:
randr = random.randint(0,10) / 10
randg = random.randint(0,10) / 10
randb = random.randint(0,10) / 10
i+=1
if i == 100:
#We've tried 100 times and got nothing, reduce required min_diff
i=0
min_diff = min_diff - 0.1
print_message(context, f"Just reduced min_diff to {min_diff}")
#If we are now at 0, give up
if min_diff == 0:
print_message(context, "Giving up. Couldn't find a distanced colour")
giveup = True
e_node.inputs[0].default_value = (randr,randg,randb, 1)
node_tree.links.new(e_node.outputs[0], m_output_node.inputs[0])
self.__class__.existing_col_id_mats[orig_mat_name] = col_id_mat
if sbp.tex_per_mat:
recorded_name = orig_mats_dict.get(self.obj_name, {}).get(slot_idx, orig_mat_name)
col_id_mat["SB_original_mat_name"] = self._strip_sb_prefixes(recorded_name)
return {'FINISHED'}
def add_specials_to_macro_s2a(MACRO, context, bake_operation_id, specific_high="", specific_low="", mode=""):
#Set variables from panel
sbp = context.scene.SimpleBake_Props
if specific_high == "":
if sbp.global_mode == SBConstants.PBR:
mode = SBConstants.PBRS2A
target_object_name = sbp.targetobj.name
else:
mode = SBConstants.CYCLESBAKE_S2A
target_object_name = sbp.targetobj_cycles.name
else: #Auto match with specific high and low
mode = SBConstants.PBRS2A_AUTOMATCH
target_object_name = specific_low
specials_bake_modes = specials_selection_to_list(context)
global_mode = SBConstants.PBR #########?????????????????????????????????????????????
bake_objects_names = [o.obj_point.name for o in sbp.objects_list]
orig_sample_count = context.scene.cycles.samples
lightmap_apply_colman = sbp.lightmap_apply_colman
denoise = sbp.rundenoise
#Specials specific bake prep
op = MACRO.define("SIMPLEBAKE_OT_specials_specific_bake_prep_and_finish")
op.properties.mode = "prepare"
#Record all bake object materials
for obj_name in bake_objects_names:
#Record original mats being used
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "record"
op.properties.obj_name = obj_name
#Also the target object
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "record"
op.properties.obj_name = target_object_name
for bake_mode in specials_bake_modes:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake mode: {bake_mode}"
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Now baking {bake_mode}"
for obj_name in bake_objects_names:
op = MACRO.define("SIMPLEBAKE_OT_import_and_assign_specials")
op.properties.bake_mode = bake_mode
op.properties.obj_name = obj_name
#Create bake image if needed for TARGET OBJECT and this bake mode
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Creating bake image (specials)"
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = target_object_name #!!
op.properties.global_mode = global_mode
#Set bake image on TARGET OBJECT
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Setting bake image on target object"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = target_object_name #!!!
op.properties.this_bake = bake_mode
op.properties.only_bake_image_node = True
#Select all bake objects with target as target
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Selecting objects for S2A baking setup"
op = MACRO.define("SIMPLEBAKE_OT_select_selected_to_active")
op.properties.mode = mode
if mode == SBConstants.PBRS2A_AUTOMATCH:
op.properties.specific_high = specific_high
op.properties.specific_low = specific_low
if sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.auto_match = True
op.properties.object_name = specific_low
op.properties.high_object_name = specific_high
elif sbp.isolate_objects:
op = MACRO.define("SIMPLEBAKE_OT_isolate_for_bake_to_target")
op.properties.object_name = target_object_name
op.properties.isolate_s2a = True
#Bake-------------------------------------------------------
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake operation"
def do_bake():
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
if bake_mode in [SBConstants.AO, SBConstants.THICKNESS, SBConstants.CURVATURE]:
op.properties.sample_count =sbp.boosted_sample_count
else:
prefs = context.preferences.addons[base_package].preferences
op.properties.sample_count = prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.use_selected_to_active=True
op.properties.type = "EMIT"
op.properties.max_ray_distance = (sbp.ray_distance * sbp.cage_and_ray_multiplier)
#Cage---------
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.auto_match_mode == "name" and sbp.s2a_opmode == "automatch":
parts = target_object_name.split("_")
base_name = '_'.join(parts[:-1]).lower()
for o in context.scene.objects:
if o.name.lower() == f"{base_name}_cage":
op.properties.use_cage = True
op.properties.cage_object = o.name
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Using cage object for specials {o.name}"
#Not auto-match
else:
if context.scene.render.bake.cage_object != None:
op.properties.use_cage = True
op.properties.cage_object = context.scene.render.bake.cage_object.name
elif sbp.cage_smooth_hard == "smooth":
op.properties.use_cage = True
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
elif sbp.cage_smooth_hard == "hard":
op.properties.use_cage = False
op.properties.cage_extrusion = (sbp.cage_extrusion * sbp.cage_and_ray_multiplier)
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore material on target object before next bake mode
#Will actually just remove the bake image node, as the material never changed for target
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Restore materials (specials)"
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "restore"
op.properties.obj_name = target_object_name
#Scale all baked images if needed (catching this latest one)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Scale or images if needed (specials)"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
#Pack
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Packing images into blend file (specials)"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#Save external
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving baked images externally (specials)"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
op.properties.lightmap_apply_colman = sbp.lightmap_apply_colman
#Restore materials on all bake objects as we are done
for obj_name in bake_objects_names:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Restoring materials for {obj_name} (specials)"
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "restore"
op.properties.obj_name = obj_name
#Denoise lightmap, AO or thickness MUST COME AFTER SAVE EXTERNAL
if denoise and bake_mode in [SBConstants.LIGHTMAP, SBConstants.AO, SBConstants.THICKNESS]:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Running denoise (specials)"
op = MACRO.define("SIMPLEBAKE_OT_compositor_denoise")
op.properties.bake_operation_id = bake_operation_id
#op.properties.bake_type = bake_mode
#Specials specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Specials bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_specials_specific_bake_prep_and_finish")
op.properties.mode = "finish"
op.properties.orig_sample_count = orig_sample_count
def add_specials_to_macro(MACRO, context, bake_operation_id, bake_objects_names):
#Will always run second, so a lot of the setup (UVs etc.) will be done.
sbp = context.scene.SimpleBake_Props
#Set variables from panel
denoise = sbp.rundenoise
specials_bake_modes = specials_selection_to_list(context)
global_mode = SBConstants.PBR #TODO-------------------------?
lightmap_apply_colman = sbp.lightmap_apply_colman
orig_sample_count = context.scene.cycles.samples
#-----------------------------------------------------
#MACRO = SimpleBake_OT_Specials_Bake_Macro
#Clean macro
#MACRO.clean()
#Specials specific bake prep
op = MACRO.define("SIMPLEBAKE_OT_specials_specific_bake_prep_and_finish")
op.properties.mode = "prepare"
obj_counter = 1
for obj_name in bake_objects_names:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting object: {obj_name}"
if obj_counter == len(bake_objects_names):
last_obj = True
#print_message(context, "Last operation")
else: last_obj = False
for bake_mode in specials_bake_modes:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake mode: {bake_mode}"
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Now baking {bake_mode}"
#Create bake image if needed for this object and this bake mode
op = MACRO.define("SIMPLEBAKE_OT_bake_image")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
op.properties.target_object_name = obj_name
op.properties.global_mode = global_mode
#Record original mats being used
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Backup materials (specials)"
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "record"
op.properties.obj_name = obj_name
if bake_mode == SBConstants.COLOURID:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Setting up for ColID map (specials)"
op = MACRO.define("SIMPLEBAKE_OT_setup_col_id")
op.properties.obj_name = obj_name
elif bake_mode == SBConstants.VERTEXCOL:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Setting up for VertexCol bake (specials)"
#Import needed special into scene, and set to object
op = MACRO.define("SIMPLEBAKE_OT_import_and_assign_specials")
op.properties.bake_mode = bake_mode
op.properties.obj_name = obj_name
op = MACRO.define("SIMPLEBAKE_OT_setup_vertex_col")
op.properties.obj_name = obj_name
elif bake_mode == SBConstants.LIGHTMAP:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Setting up for LightMap bake (specials)"
op = MACRO.define("SIMPLEBAKE_OT_import_and_assign_specials")
op.properties.bake_mode = bake_mode
op.properties.obj_name = obj_name
#AO, Curvature, Thickness
else:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Setting up for AO/Curvate/Thickness bake (specials)"
#Import needed special into scene, and set to object
op = MACRO.define("SIMPLEBAKE_OT_import_and_assign_specials")
op.properties.bake_mode = bake_mode
op.properties.obj_name = obj_name
#Config (mostly just adding the bake image node)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Specials pre-bake config (add image node)"
op = MACRO.define("SIMPLEBAKE_OT_prepare_object_mats_pbr")
op.properties.bake_operation_id = bake_operation_id
op.properties.target_name = obj_name
op.properties.this_bake = bake_mode
#Bake
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Starting bake operation"
op = MACRO.define("SIMPLEBAKE_OT_select_only_this")
op.properties.object_name = obj_name
if sbp.isolate_objects:
op.properties.isolate = True
def do_bake():
if bake_mode == SBConstants.LIGHTMAP:
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = orig_sample_count
op = MACRO.define("OBJECT_OT_bake")
op.properties.type = "COMBINED"
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
#op = MACRO.define("SIMPLEBAKE_OT_update_progress")
#op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
#prefs = context.preferences.addons[base_package].preferences
#op.properties.sample_count = prefs.pbr_sample_count2
else:
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
if bake_mode in [SBConstants.AO, SBConstants.THICKNESS, SBConstants.CURVATURE]:
op.properties.sample_count = sbp.boosted_sample_count
else:
prefs = context.preferences.addons[base_package].preferences
op.properties.sample_count = prefs.pbr_sample_count2
op = MACRO.define("OBJECT_OT_bake")
op.properties.type = "EMIT"
op.properties.use_clear = False
op.properties.target = "IMAGE_TEXTURES"
op.properties.margin = context.scene.render.bake.margin
try: #Magin type added after Blender 3.0
op.properties.margin_type = context.scene.render.bake.margin_type
except:
pass
op = MACRO.define("SIMPLEBAKE_OT_set_sample_count")
op.properties.sample_count = orig_sample_count
op = MACRO.define("SIMPLEBAKE_OT_update_progress")
do_bake()
#Restore all materials (and repair specials material if applicable)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Restore materials (specials)"
op = MACRO.define("SIMPLEBAKE_OT_specials_mats_swapping")
op.properties.mode = "restore"
op.properties.obj_name = obj_name
#If not a merged bake, done with this image. If merged bake, only done if this is last object
if not sbp.merged_bake or (sbp.merged_bake and last_obj):
#Scale all baked images if needed (catching this latest one)
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Scale or images if needed (specials)"
op = MACRO.define("SIMPLEBAKE_OT_scale_images_if_needed")
op.properties.bake_operation_id = bake_operation_id
op.properties.this_bake = bake_mode
#Pack
#MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Packing images into blend file (specials)"
op = MACRO.define("SIMPLEBAKE_OT_pack_baked_images")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
#Save external
if sbp.save_bakes_external:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Saving baked images externally (specials)"
op = MACRO.define("SIMPLEBAKE_OT_save_images_externally")
op.properties.bake_operation_id = bake_operation_id
op.properties.bake_mode = bake_mode
op.properties.lightmap_apply_colman = sbp.lightmap_apply_colman
#Done with this object
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Finished baking {obj_name} (specials)"
obj_counter += 1
#Done with all objects
#Denoise lightmap, AO or thickness MUST COME AFTER SAVE EXTERNAL
if denoise and bake_mode in [SBConstants.LIGHTMAP, SBConstants.AO, SBConstants.THICKNESS]:
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message=f"Running denoise (specials)"
op = MACRO.define("SIMPLEBAKE_OT_compositor_denoise")
op.properties.bake_operation_id = bake_operation_id
#op.properties.bake_type = bake_mode
#Specials specific bake finishing
MACRO.define("SIMPLEBAKE_OT_print_message").properties.message="Specials bake finishing"
op = MACRO.define("SIMPLEBAKE_OT_specials_specific_bake_prep_and_finish")
op.properties.mode = "finish"
op.properties.orig_sample_count = orig_sample_count
classes = ([
SimpleBake_OT_Specials_Specific_Bake_Prep_And_Finish,
SimpleBake_OT_Import_And_Assign_Specials,
SimpleBake_OT_Setup_Col_ID,
SimpleBake_OT_Setup_Vertex_Col,
SimpleBake_OT_Specials_Mats_Swapping,
SimpleBake_OT_Specials_Bake_Macro,
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,455 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
from pathlib import Path
import tempfile
import shutil
import os
import numpy as np
import sys
from .utils import get_bake_objects
from .messages import print_message
from .background_and_progress import BakeInProgress as Bip
from . import __package__ as base_package
FILE_EXTENSIONS = {"PNG": "png", "JPEG": "jpg", "TIFF": "tiff", "TARGA": "tga","TARGA_RAW": "tga", "OPEN_EXR": "exr"}
def parse_channel_value(value):
"""Return (bake_type, mat_name). mat_name is '' if no material is encoded."""
if "||" in value:
bake_type, mat_name = value.split("||", 1)
return bake_type, mat_name
return value, ""
def load_image(image_path):
# Load an image file as a numpy array
try:
image = bpy.data.images.load(image_path)
pixels = np.array(image.pixels)
width, height = image.size
# Reshape and isolate the desired channel (r, g, b or a)
channel_data = pixels.reshape((height, width, 4))
return channel_data[:, :, :3], channel_data[:, :, 3], image # RGB and Alpha
except Exception as e:
print(f"Error loading image {image_path}: {e}")
return None, None, image
def channel_pack_images_isolate(red_image, green_image, blue_image, alpha_image, imgname, gammashift=False, file_format="PNG"):
archetype_img_data = None
to_remove = []
# Load each image and extract its corresponding channel
if red_image != None:
r_data, r_alpha, r_loaded = load_image(red_image)
packed_image = np.zeros(r_data.shape)
archetype_img_data = r_data
to_remove.append(r_loaded)
if green_image != None:
g_data, g_alpha, g_loaded = load_image(green_image)
packed_image = np.zeros(g_data.shape)
archetype_img_data = g_data
to_remove.append(g_loaded)
if blue_image != None:
b_data, b_alpha, b_loaded = load_image(blue_image)
packed_image = np.zeros(b_data.shape)
archetype_img_data = b_data
to_remove.append(b_loaded)
if alpha_image != None:
a_data, a_alpha, a_loaded = load_image(alpha_image)
packed_image = np.zeros(a_data.shape)
archetype_img_data = a_data
to_remove.append(a_loaded)
#----------------Gamma shift stuff---------------------------------------------
# Apply gamma correction to the RGB channels if noncolour
if red_image is not None:
r_corrected = np.power(r_data[:, :, 0], 1.0 / 2.2) if gammashift else r_data[:, :, 0]
else:
r_corrected = np.zeros_like(archetype_img_data[:, :, 0])
if green_image is not None:
g_corrected = np.power(g_data[:, :, 1], 1.0 / 2.2) if gammashift else g_data[:, :, 1]
else:
g_corrected = np.zeros_like(archetype_img_data[:, :, 0])
if blue_image is not None:
b_corrected = np.power(b_data[:, :, 2], 1.0 / 2.2) if gammashift else b_data[:, :, 2]
else:
b_corrected = np.zeros_like(archetype_img_data[:, :, 0])
# Assign corrected channels to packed_image
packed_image[:, :, 0] = r_corrected # Red
packed_image[:, :, 1] = g_corrected # Green
packed_image[:, :, 2] = b_corrected # Blue
if alpha_image is not None:
a_corrected = np.power(a_data[:, :, 0], 1.0 / 2.2) if gammashift else a_data[:, :, 0]
else:
a_corrected = np.ones_like(archetype_img_data[:, :, 0])
#----------------Gamma shift stuff---------------------------------------------
packed_pixels = np.concatenate((packed_image, a_corrected[:, :, None]), axis=2)
packed_pixels = packed_pixels.flatten()
# Create a new image in Blender and assign the pixels
bpy.data.images.remove(bpy.data.images[imgname]) if imgname in bpy.data.images else False
output_image = bpy.data.images.new(name=imgname, width=archetype_img_data.shape[1], height=archetype_img_data.shape[0], alpha=True, float_buffer=(file_format == "OPEN_EXR"))
output_image["SB_channel_pack_image"] = True
output_image.pixels = packed_pixels
for i in to_remove:
bpy.data.images.remove(i)
output_image.use_fake_user = True
return output_image
class SimpleBake_OT_Channel_Packing(Operator):
"""Perform channel packing"""
bl_idname = "simplebake.channel_packing"
bl_label = "Pack"
bake_operation_id: StringProperty()
pbrtarget_only: BoolProperty(default=False)
in_background: BoolProperty(default=False)
global_mode: StringProperty()
target_obj_name_override: StringProperty(default="")
def delete_components(self,context):
sbp = context.scene.SimpleBake_Props
#Delete externally saved CPT components if the user wants us to do that
del_list = []
for obj_name in self.objects:
if not (obj:=context.scene.objects.get(obj_name)):
continue
for cpt in self.cp_list:
r_img = self.resolve_image(context, cpt.R, obj.name)
g_img = self.resolve_image(context, cpt.G, obj.name)
b_img = self.resolve_image(context, cpt.B, obj.name)
a_img = self.resolve_image(context, cpt.A, obj.name)
if r_img != None and r_img.name in bpy.data.images:
print_message(context, f"Deleting {r_img.name} from local file system")
filepath = bpy.path.abspath(r_img.filepath)
if len(r_img.tiles)>1:
for t in r_img.tiles:
thispath = filepath.replace("<UDIM>", str(t.number))
os.remove(thispath) if os.path.isfile(thispath) else False
else:
os.remove(filepath) if os.path.isfile(filepath) else False
del_list.append(r_img.name)
if g_img != None and g_img.name in bpy.data.images:
print_message(context, f"Deleting {g_img.name} from local file system")
filepath = bpy.path.abspath(g_img.filepath)
if len(g_img.tiles)>1:
for t in g_img.tiles:
thispath = filepath.replace("<UDIM>", str(t.number))
os.remove(thispath) if os.path.isfile(thispath) else False
else:
os.remove(filepath) if os.path.isfile(filepath) else False
del_list.append(g_img.name)
if b_img != None and b_img.name in bpy.data.images:
print_message(context, f"Deleting {b_img.name} from local file system")
filepath = bpy.path.abspath(b_img.filepath)
if len(b_img.tiles)>1:
for t in b_img.tiles:
thispath = filepath.replace("<UDIM>", str(t.number))
os.remove(thispath) if os.path.isfile(thispath) else False
else:
os.remove(filepath) if os.path.isfile(filepath) else False
del_list.append(b_img.name)
if a_img != None and a_img.name in bpy.data.images:
print_message(context, f"Deleting {a_img.name} from local file system")
filepath = bpy.path.abspath(a_img.filepath)
if len(a_img.tiles)>1:
for t in a_img.tiles:
thispath = filepath.replace("<UDIM>", str(t.number))
os.remove(thispath) if os.path.isfile(thispath) else False
else:
os.remove(filepath) if os.path.isfile(filepath) else False
del_list.append(a_img.name)
for iname in del_list:
if iname in bpy.data.images:
bpy.data.images.remove(bpy.data.images[iname])
def find_images(self, context, selection, objname):
if selection == "none":
return None
img = [i for i in bpy.data.images if
"SB_this_bake" in i and i["SB_this_bake"] == selection and
"SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id and
"SB_bake_object" in i and i["SB_bake_object"] == objname and
"SB_bake_material" not in i]
if len(img) != 1:
return None
return img[0]
def find_images_per_mat(self, context, selection, objname, mat_name):
if selection == "none":
return None
img = [i for i in bpy.data.images if
i.get("SB_this_bake") == selection and
i.get("SB_bake_operation_id") == self.bake_operation_id and
i.get("SB_bake_object") == objname and
i.get("SB_bake_material") == mat_name]
if len(img) != 1:
return None
return img[0]
def find_images_merged(self, context, selection):
if selection == "none":
return None
else:
img = ([i for i in bpy.data.images if
"SB_this_bake" in i and i["SB_this_bake"] == selection and
"SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id])
assert(len(img)==1), f"Found {len(img)} images, should have been 1"
img = img[0]
return img
def resolve_image(self, context, raw_value, obj_name):
"""Find the image for a channel value, handling encoded mat names."""
if raw_value == "none":
return None
bake_type, mat_name = parse_channel_value(raw_value)
if self.merged_bake:
return self.find_images_merged(context, bake_type)
elif mat_name:
return self.find_images_per_mat(context, bake_type, obj_name, mat_name)
else:
return self.find_images(context, bake_type, obj_name)
def execute(self, context):
sbp = context.scene.SimpleBake_Props
#Grab what we need from panel
self.merged_bake = sbp.merged_bake
self.merged_bake_name = sbp.merged_bake_name
self.export_folder_per_object = sbp.export_folder_per_object
self.batch_name = sbp.batch_name
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
to_del = []
for cp in sbp.cp_list:
if cp["R"]=="none" and cp["G"]=="none" and cp["B"]=="none" and cp["A"]=="none":
to_del.append(cp.name)
for d in to_del:
if (i:=sbp.cp_list.find(d))!=-1:
sbp.cp_list.remove(i)
self.cp_list = sbp.cp_list
#Just in case
if len(self.cp_list) == 0:
print_message(context, "No channel packing")
return {'FINISHED'}
self.objects = get_bake_objects(context)
#Figure out the save folder for each object
efpo = self.export_folder_per_object
mb = self.merged_bake
mbn = self.merged_bake_name
obj_savefolders = {}
if self.in_background:
p = sbp.export_path.replace("//", "")
base_save_folder = Path(sbp.base_folder_override) / p
else:
base_save_folder = Path(bpy.path.abspath(sbp.export_path))
for obj_name in self.objects:
if not (obj:=context.scene.objects.get(obj_name)):
continue
if efpo and mb:
savefolder = base_save_folder / mbn
obj_savefolders[obj.name] = savefolder #Every object gets the same based on merged bake name
elif efpo:
savefolder = base_save_folder / obj.name
obj_savefolders[obj.name] = savefolder
else:
savefolder = base_save_folder
obj_savefolders[obj.name] = savefolder
#Create each of the CPTs
for obj_name in self.objects:
if not (obj:=context.scene.objects.get(obj_name)):
continue
#Hacky
if not mb: t_name = obj.name
else: t_name = mbn
for cpt in self.cp_list:
file_format = cpt.file_format
cpt_name = cpt.name
extension = FILE_EXTENSIONS[file_format]
if context.preferences.addons[base_package].preferences.capitalise_file_extensions:
extension = extension.upper()
print_message(context, f"Creating packed texture \"{cpt_name}\" for \"{t_name}\" with format {file_format}")
r_img = self.resolve_image(context, cpt.R, obj.name)
g_img = self.resolve_image(context, cpt.G, obj.name)
b_img = self.resolve_image(context, cpt.B, obj.name)
a_img = self.resolve_image(context, cpt.A, obj.name)
if r_img is None and g_img is None and b_img is None and a_img is None:
print_message(context, f"Skipping packed texture \"{cpt_name}\" for \"{t_name}\" - no images found for any channel")
continue
#--Create the texture--
prefs = context.preferences.addons[base_package].preferences
#Determine the name
proposed_name = prefs.img_name_format
if Bip.Sequence.active:
if "%FRAME%" not in proposed_name:
proposed_name = f"{proposed_name}.%FRAME%"
proposed_name = proposed_name.replace("%FRAME%", f"{context.scene.frame_current:04d}")
else:
proposed_name = proposed_name.replace(".%FRAME%", "")
proposed_name = proposed_name.replace("%FRAME%", "")
proposed_name = proposed_name.replace("%OBJ%", t_name)
proposed_name = proposed_name.replace("%BAKEMODE%", self.global_mode)
proposed_name = proposed_name.replace("%BAKETYPE%", cpt.name)
proposed_name = proposed_name.replace("%BATCH%", self.batch_name)
res = f"{sbp.outputwidth}x{sbp.outputheight}"
proposed_name = proposed_name.replace("%RESOLUTION%", res)
proposed_name = proposed_name.replace("%MAT%", "")
imgname = proposed_name
#Do we need gamma correct? (16Bit or Non-Colour)
#EXR is linear — no gamma correction needed or wanted
gammashift = sbp.everything_16bit and file_format != "OPEN_EXR"
#Let's go
def save_external(img, cpt, output_path, file_format):
#Save the image
output_image.file_format = file_format
output_image.filepath_raw = output_path
output_image.alpha_mode = 'CHANNEL_PACKED'
s = bpy.data.scenes.new("SB_CP_Export_Temp")
s.render.image_settings.file_format = file_format
s.view_settings.view_transform = "Standard" #default
s.render.image_settings.color_mode = "RGBA" if a_img != None else "RGB"
if file_format == "OPEN_EXR":
s.render.image_settings.exr_codec = cpt.exr_codec
s.render.image_settings.color_depth = "32"
if file_format == "PNG":
s.render.image_settings.exr_codec = cpt.exr_codec
s.render.image_settings.color_depth = "16" if sbp.everything_16bit else "8"
if "png_compression" in cpt:
s.render.image_settings.compression = cpt.png_compression
else:
s.render.image_settings.compression = 15
if file_format in {"TARGA", "TARGA_RAW"}:
s.render.image_settings.color_depth = "8"
img.save_render(str(output_path), scene=s)
img.source = "FILE"
bpy.data.scenes.remove(s)
#Will we be working with tiled images or not
#If one is tiled, they all wil be. However, they could also be None
tiled = False
tiles = []
if r_img != None and len(r_img.tiles)>1:
tiled = True
tiles = r_img.tiles
if g_img != None and len(g_img.tiles)>1:
tiled = True
tiles = g_img.tiles
if b_img != None and len(b_img.tiles)>1:
tiled = True
tiles = b_img.tiles
if a_img != None and len(a_img.tiles)>1:
tiled = True
tiles = a_img.tiles
#UDIM image
if tiled:
for t in tiles:
this_i_name = imgname + f".{str(t.number)}"
r_path = r_img.filepath.replace("<UDIM>", str(t.number)) if r_img!=None else None
g_path = g_img.filepath.replace("<UDIM>", str(t.number)) if g_img!=None else None
b_path = b_img.filepath.replace("<UDIM>", str(t.number)) if b_img!=None else None
a_path = a_img.filepath.replace("<UDIM>", str(t.number)) if a_img!=None else None
output_image = channel_pack_images_isolate(r_path, g_path, b_path, a_path,\
this_i_name, gammashift=gammashift, file_format=file_format)
output_path = Path(obj_savefolders[obj.name] / (this_i_name + f".{extension}"))
save_external(output_image, cpt, str(output_path), file_format)
output_image["SB_bake_operation_id"] = self.bake_operation_id
#Non-UDIM image
else:
r_path = r_img.filepath if r_img!=None else None
g_path = g_img.filepath if g_img!=None else None
b_path = b_img.filepath if b_img!=None else None
a_path = a_img.filepath if a_img!=None else None
output_image = channel_pack_images_isolate(r_path, g_path, b_path, a_path,\
imgname, gammashift=gammashift, file_format=file_format)
output_path = Path(obj_savefolders[obj.name] / (imgname + f".{extension}"))
save_external(output_image, cpt, str(output_path), file_format)
output_image["SB_bake_operation_id"] = self.bake_operation_id
#------------------------------------------------------
#Hacky - If this is a mergedbake, break out of the object loop
if sbp.merged_bake:
break
if sbp.del_cptex_components:
self.delete_components(context)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Channel_Packing
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+225
View File
@@ -0,0 +1,225 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty
from . import property_group
import re
def format_to_regex(user_input):
# Escape all regex special characters except for *
escaped_input = re.escape(user_input)
# Replace the escaped * with .*
regex_pattern = escaped_input.replace(r'\*', '.*')
# If the original input ends with *, modify the pattern to end with .*$
if user_input.endswith('*'):
regex_pattern = regex_pattern[:-2] + '.*$'
# Check if the original input starts with *
if user_input.startswith('*'):
regex_pattern = '^.*' + regex_pattern[2:]
return regex_pattern
def replace_pattern(text, pattern, replacement):
# Format the pattern to include wildcards
formatted_pattern = format_to_regex(pattern)
# Use re.sub to replace all occurrences of the formatted pattern with the replacement string
result = re.sub(formatted_pattern, replacement, text)
return result
def find_and_replace(col, find, replace, limit_to_sb=False):
for item in col:
if limit_to_sb and "SB_bake_operation_id" not in item:
continue
original_name = item.name
new_name = original_name.replace(find, replace)
# If the new name is different, apply the change
if new_name != original_name:
item.name = new_name
print(f"Renamed '{original_name}' to '{new_name}'")
else:
print(f"No change for '{original_name}'")
def find_and_replace_wildcard(context, col, find, replace, limit_to_sb=False):
sbp = context.scene.SimpleBake_Props
for item in col:
if limit_to_sb and "SB_bake_operation_id" not in item:
continue
item.name = replace_pattern(item.name, find, replace)
class SimpleBake_OT_Purge_Settings(Operator):
bl_idname = "simplebake.purge_settings"
bl_label = "Purge Settings"
bl_description = "WARNING - Purge ALL SimpleBake settings from this Blend file and reset to DEFAULTS. CANNOT BE UNDONE. INCLUDES BLEND FILE PRESETS (but not global presets)"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
#del context.scene["SimpleBake_Props"]
context.scene.property_unset("SimpleBake_Props")
self.report({'INFO'}, "SimpleBake settings restored to defaults")
return {'FINISHED'}
class SimpleBake_OT_Purge_Images(Operator):
bl_idname = "simplebake.purge_images"
bl_label = "Purge Images"
bl_description = "WARNING - Purge all SimpleBake created IMAGES from this blend file. CANNOT BE UNDONE"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
to_remove = []
for i in bpy.data.images:
if "SB_bake_operation_id" in i:
to_remove.append(i.name)
for name in to_remove:
im = bpy.data.images.get(name)
if im != None:
bpy.data.images.remove(im, do_unlink=True)
self.report({'INFO'}, "SimpleBake images have been purged")
return {'FINISHED'}
class SimpleBake_OT_Purge_Objects(Operator):
bl_idname = "simplebake.purge_objects"
bl_label = "Purge Objects"
bl_description = "WARNING - Purge all SimpleBake created OBJECTS from this blend file. CANNOT BE UNDONE"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bpy.ops.simplebake.reverse_geo_nodes_sidestep()
to_remove = []
for o in context.scene.objects:
if "SB_bake_operation_id" in o:
to_remove.append(o.name)
for name in to_remove:
ob = context.scene.objects.get(name)
if ob != None:
if "SB_copy_and_apply_from" in ob:
uh = context.scene.objects.get(ob["SB_copy_and_apply_from"])
if uh !=None:
uh.hide_set(False)
bpy.data.objects.remove(ob, do_unlink=True)
if "SimpleBake_Bakes" in bpy.data.collections:
bpy.data.collections.remove(bpy.data.collections["SimpleBake_Bakes"])
bpy.ops.simplebake.reverse_geo_nodes_sidestep()
self.report({'INFO'}, "SimpleBake objects have been purged")
return {'FINISHED'}
class SimpleBake_OT_restorematerials(Operator):
bl_idname = "simplebake.restorematerials"
bl_label = "Restore"
bl_description = "WARNING - Attempt to restore all materials and purge SimpleBake created MATERIALS from this blend file. CANNOT BE UNDONE"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bpy.ops.simplebake.material_backup(mode="working_restore")
bpy.ops.simplebake.material_backup(mode="master_restore")
to_del= []
for m in bpy.data.materials:
if "SB_bake_operation_id" in m:
to_del.append(m.name)
for m_name in to_del:
m = bpy.data.materials.get(m_name)
if m !=None:
bpy.data.materials.remove(m)
self.report({'INFO'}, "Materials restored")
return {'FINISHED'}
class SimpleBake_OT_findandreplace(Operator):
bl_idname = "simplebake.findandreplace"
bl_label = "Find and replace"
bl_description = "Find and replace names for the given data type. Basic wildcards are supported as *"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
find = sbp.findreplace_find
replace = sbp.findreplace_replace
if sbp.findreplace_type == "image":
col = bpy.data.images
if sbp.findreplace_type == "object":
col = context.scene.objects
if sbp.findreplace_type == "material":
col = bpy.data.materials
if "*" in find:
find_and_replace_wildcard(context, col, find, replace, limit_to_sb=sbp.limit_findandreplace_to_sb)
else:
find_and_replace(col, find, replace, limit_to_sb=sbp.limit_findandreplace_to_sb)
self.report({'INFO'}, "Find and replace done")
return {'FINISHED'}
classes = ([
SimpleBake_OT_Purge_Settings,
SimpleBake_OT_Purge_Images,
SimpleBake_OT_Purge_Objects,
SimpleBake_OT_findandreplace,
SimpleBake_OT_restorematerials
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+821
View File
@@ -0,0 +1,821 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
import os
import sys
from .utils import SBConstants
from .messages import print_message
from .background_and_progress import BakeInProgress as Bip
def remove_and_bypass_invert_node(material_name, invert_node_name):
# Get the material
mat = bpy.data.materials.get(material_name)
if not mat:
print(f"Material '{material_name}' not found")
return
# Ensure the material uses nodes
if not mat.use_nodes:
print(f"Material '{material_name}' does not use nodes")
return
# Get the node tree of the material
nodes = mat.node_tree.nodes
# Find the specified invert node by name
invert_node = nodes.get(invert_node_name)
if not invert_node:
print(f"Invert node named '{invert_node_name}' not found")
return
if invert_node.type != 'INVERT':
print(f"The node named '{invert_node_name}' is not an invert node")
return
# Check the input connections to the invert node
if invert_node.inputs['Color'].is_linked:
input_link = invert_node.inputs['Color'].links[0].from_socket
else:
return
# Check the output connections from the invert node
if invert_node.outputs['Color'].is_linked:
# There could be multiple links, handle each one
output_links = invert_node.outputs['Color'].links
for link in output_links:
# Connect each linked output to the original input link's socket
mat.node_tree.links.new(input_link, link.to_socket)
else:
return
# Delete the invert node
nodes.remove(invert_node)
def create_aov_nodes(node_tree, bake_operation_id, obj, merged_bake=False):
#Find them all
aov_images = []
for i in bpy.data.images:
if "SB_this_bake" in i and "SB_bake_operation_id" in i:
if i["SB_bake_operation_id"] == bake_operation_id and "AOV_" in i["SB_this_bake"]:
aov_images.append(i)
#If this isn't a merged bake we only want the images for this object
if not merged_bake:
aov_images = [img for img in aov_images if img['SB_bake_object'] == obj['SB_copy_and_apply_from']]
#Keep this names only
aov_images = [img.name for img in aov_images]
#Find MO node
mo_node = [n for n in node_tree.nodes if n.bl_idname == 'ShaderNodeOutputMaterial' and n.is_active_output]
assert(len(mo_node)==1)
mo_node = mo_node[0]
base_x, base_y = mo_node.location
i=0
for aov in aov_images:
img = bpy.data.images.get(aov)
if not img:
continue
img_node = node_tree.nodes.new("ShaderNodeTexImage")
img_node.hide = True # collapsed
img_node.location = (base_x, base_y - 160 - i * 40)
img_node.image = img
i+=1
class SimpleBake_OT_Apply_Bakes_To_Original(Operator):
"""Apply the baked textures to the original objects"""
bl_idname = "simplebake.apply_bakes_to_original"
bl_description = "Apply the baked textures to the original objects"
bl_label = "Apply bakes to original"
bake_operation_id: StringProperty()
def execute(self, context):
if not Bip.Sequence.should_run_teardown():
return {'FINISHED'} #Nope out if this is part of a sequence unless last
for m in bpy.data.materials:
if ("SB_baked_orig_objects" in m) and ("SB_bake_operation_id" in m):
if m["SB_bake_operation_id"] == self.bake_operation_id:
os = list(m["SB_baked_orig_objects"])
for o_name in os:
if (obj:=context.scene.objects.get(o_name)):
actual_obj = None
#Is this a proxy object for sidestep?
if "SB_proxy_bake_object" in obj:
actual_obj_name = obj["SB_proxy_bake_object"]
#Find a sidestepped object with this tag
for o in context.scene.objects:
if "SB_replaced_orig_name" in o and o["SB_replaced_orig_name"] == actual_obj_name:
actual_obj = o
else:
actual_obj = obj
if actual_obj == None:
print_message(context, "ERROR: Apply bakes to original object operator couldn't locate original object!!")
return {'CANCELLED'}
for slot in actual_obj.material_slots:
slot.material = m
return {'FINISHED'}
class SimpleBake_OT_Copy_And_Apply(Operator):
"""Copy the baked objects and apply baked textures"""
bl_idname = "simplebake.copy_and_apply"
bl_description = "Copy baked objects and apply bakes"
bl_label = "Copy and apply"
target_object_name: StringProperty()
bake_operation_id: StringProperty()
global_mode: StringProperty()
decals_override: BoolProperty(default=False)
def import_cyclesbake_mat_setup(self, context, new_obj, merged=False):
if self.cyclesbake_mat_format == "emission":
import_mat_name = "SB_cyclesbake_e"
elif self.cyclesbake_mat_format == "background":
import_mat_name = "SB_cyclesbake_b"
else:
import_mat_name = "SB_cyclesbake_p"
rm_list = [mat.name for mat in bpy.data.materials if mat.name == import_mat_name]
for rm_name in rm_list:
mat = bpy.data.materials.get(rm_name)
if mat != None:
bpy.data.materials.remove(mat)
path = os.path.dirname(__file__) + "/resources/copy_and_apply_mats.blend/Material/"
bpy.ops.wm.append(filename=import_mat_name, directory=path)
mat = bpy.data.materials[import_mat_name]
if merged:
mat.name = f"{self.merged_bake_name}_Baked"
mat["SB_bake_operation_id"] = self.bake_operation_id
mat["SB_merged_copy_and_apply_mat"] = True
elif not self.mat_only:
mat.name = f"{new_obj.name}" #Already includes "_baked"
else:
mat.name = f"{self.target_object_name}_baked"
if new_obj !=None:
#Assign to object
new_obj.data.materials.append(mat)
return mat
def import_pbr_mat_setup(self, context, new_obj, merged=False):
sbp = context.scene.SimpleBake_Props
#Decals
if self.decals_override:
if self.used_directx:
import_mat_name = "SB_pbr_directx_decals"
else:
import_mat_name = "SB_standard_pbr_decals"
#Not decals
else:
if self.used_directx:
import_mat_name = "SB_pbr_directx"
else:
import_mat_name = "SB_standard_pbr"
#Import the PBR node setup that we need and assign to object
rm_list = [mat.name for mat in bpy.data.materials if mat.name == import_mat_name]
for rm_name in rm_list:
mat = bpy.data.materials.get(rm_name)
if mat != None:
bpy.data.materials.remove(mat)
path = os.path.dirname(__file__) + "/resources/copy_and_apply_mats.blend/Material/"
bpy.ops.wm.append(filename=import_mat_name, directory=path)
mat = bpy.data.materials[import_mat_name]
#Deal with the case where the user has selected glossy
if not self.used_glossy:
remove_and_bypass_invert_node(import_mat_name, "invert_roughgloss")
remove_and_bypass_invert_node(import_mat_name, "decal_invert_roughgloss")
if not self.used_ccglossy:
remove_and_bypass_invert_node(import_mat_name, "invert_ccroughgloss")
remove_and_bypass_invert_node(import_mat_name, "decal_invert_ccroughgloss")
if self.used_glossy:
gns = [n for n in mat.node_tree.nodes if n.label == SBConstants.PBR_ROUGHNESS]
for n in gns:
n.label = SBConstants.PBR_GLOSSY
gns = [n for n in mat.node_tree.nodes if n.label == f"Decal_{SBConstants.PBR_ROUGHNESS}"]
for n in gns:
n.label = f"Decal_{SBConstants.PBR_GLOSSY}"
if self.used_ccglossy:
gns = [n for n in mat.node_tree.nodes if n.label == SBConstants.PBR_CLEARCOAT_ROUGH]
for n in gns:
n.label = SBConstants.PBR_CLEARCOAT_GLOSS
gns = [n for n in mat.node_tree.nodes if n.label == f"Decal_{SBConstants.PBR_CLEARCOAT_ROUGH}"]
for n in gns:
n.label = f"Decal_{SBConstants.PBR_CLEARCOAT_GLOSS}"
#Assign to object
if new_obj != None:
new_obj.data.materials.append(mat)
if merged:
mat.name = f"{self.merged_bake_name}_Baked"
mat["SB_bake_operation_id"] = self.bake_operation_id
mat["SB_merged_copy_and_apply_mat"] = True
elif not self.mat_only:
mat.name = f"{new_obj.name}" #Already includes "_baked"
else:
mat.name = f"{self.target_object_name}_baked"
#If we are baking alpha or transparency, set the blend mode
if sbp.selected_alpha or sbp.selected_trans:
mat.blend_method = "BLEND"
return mat
def create_cyclesbake_setup(self, context, new_obj):
mat = self.import_cyclesbake_mat_setup(context, new_obj)
node_tree = mat.node_tree
nodes = node_tree.nodes
#The non-specials texture
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] not in SBConstants.ALL_SPECIALS
and i["SB_bake_object"] == self.target_object_name
])
assert len(tex) == 1
tex = tex[0]
node = [n for n in nodes if n.label=="cyclesbake"]
assert len(node) == 1
node = node[0]
node.image = tex
#The specials textures
for bake_type in SBConstants.ALL_SPECIALS:
node = [n for n in nodes if n.label==bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] == bake_type
and i["SB_bake_object"] == self.target_object_name
])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
return mat
def create_cyclesbake_setup_merged(self, context, new_obj):
#Check if we already have the merged texture
for mat in bpy.data.materials:
if "SB_merged_copy_and_apply_mat" in mat\
and "SB_bake_operation_id" in mat\
and mat["SB_bake_operation_id"] == self.bake_operation_id:
#Assign existing merged mat to object and leave
if new_obj is not None:
new_obj.data.materials.append(mat)
return mat
#
# mat_name = f"{self.merged_bake_name}_Baked"
# if mat_name in bpy.data.materials:
# mat = bpy.data.materials[mat_name]
# if "SB_bake_operation_id" in mat and mat["SB_bake_operation_id"] == self.bake_operation_id:
# mat = bpy.data.materials[mat_name]
# #Assign existing merged mat to object and leave
# if new_obj != None:
# new_obj.data.materials.append(mat)
# return mat
mat = self.import_cyclesbake_mat_setup(context, new_obj, merged=True)
node_tree = mat.node_tree
nodes = node_tree.nodes
#The non-specials texture
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] not in SBConstants.ALL_SPECIALS
])
assert len(tex) == 1
tex = tex[0]
node = [n for n in nodes if n.label=="cyclesbake"]
assert len(node) == 1
node = node[0]
node.image = tex
#The specials textures
for bake_type in SBConstants.ALL_SPECIALS:
node = [n for n in nodes if n.label==bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] == bake_type])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
return mat
def create_pbrdecal_setup(self, context, new_obj):
BAKE_OP_PREFIX = "DECALSBASE_"
#Called with the prefix. S2A textures will NOT have the prefix
#Setup
mat = self.import_pbr_mat_setup(context, new_obj)
node_tree = mat.node_tree
nodes = node_tree.nodes
bake_types = SBConstants.ALL_PBR_MODES #Every possible bake mode for PBR
bake_types += SBConstants.ALL_SPECIALS #Every possible bake mode for specials
#Find the master decal alpha
decal_alpha_tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"]== self.bake_operation_id.replace(BAKE_OP_PREFIX, "") and
"SB_this_bake" in i and i["SB_this_bake"] == SBConstants.PBR_ALPHA and
"SB_global_mode" in i and i["SB_global_mode"] == SBConstants.PBRS2A
])
assert(len(decal_alpha_tex)==1)
node = [n for n in nodes if n.label=="Master_Decal_Alpha"]
assert(len(node)==1)
node[0].image = decal_alpha_tex[0]
#Now the base/target object PBR textures:
for bake_type in bake_types:
node = [n for n in nodes if n.label==bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i and
i["SB_bake_operation_id"] == self.bake_operation_id and
"SB_this_bake" in i and i["SB_this_bake"] == bake_type and
"SB_bake_object" in i and i["SB_bake_object"] == self.target_object_name])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
# #Now the Decal/S2A textures:
for bake_type in bake_types:
node = [n for n in nodes if n.label=="Decal_" + bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i and
i["SB_bake_operation_id"] == self.bake_operation_id.replace(BAKE_OP_PREFIX, "") and
"SB_this_bake" in i and i["SB_this_bake"] == bake_type and
"SB_bake_object" in i and i["SB_bake_object"] == self.target_object_name])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
#Now get rid of the unused MixRGB nodes
mnodes = [n for n in nodes if n.bl_idname == "ShaderNodeMix"]
for mnode in mnodes:
a_color_input = [i for i in mnode.inputs if i.identifier=="A_Color"][0]
#factor_float_input = [i for i in mnode.inputs if i.identifier=="Factor_Float"][0]
#color_output = [o for o in mnode.outputs if o.identifier=="Result_Color"][0]
if len(a_color_input.links) == 0:
#fsocket = factor_float_input.links[0].from_socket
#tsocket = color_output.links[0].to_socket
#node_tree.links.new(fsocket, tsocket)
nodes.remove(mnode)
#Finally the AOVs
create_aov_nodes(node_tree, self.bake_operation_id, new_obj)
return mat
def create_pbr_setup_tex_per_mat(self, context, new_obj):
"""Create one PBR material per original material slot using per-material baked images."""
source_obj = context.scene.objects[self.target_object_name]
original_mat_only = self.mat_only
self.mat_only = True # Prevent import_pbr_mat_setup crashing when new_obj is None
# Save face→material-slot mapping from the source before create_object()'s
# materials.clear() resets all polygon material_index values to 0.
if new_obj is not None:
face_mat_indices = [p.material_index for p in source_obj.data.polygons]
mats = []
for slot in source_obj.material_slots:
if slot.material is None:
continue
# master_restore has not run yet when copy_and_apply is called, so slot names
# still carry the "SBM_" prefix — strip it to match the SB_bake_material tag.
mat_name = slot.material.name
if mat_name.startswith("SBM_"):
mat_name = mat_name[4:]
mat = self.import_pbr_mat_setup(context, new_obj=None)
mat.name = f"{self.target_object_name}_{mat_name}_Baked"
node_tree = mat.node_tree
nodes = node_tree.nodes
bake_types = SBConstants.ALL_PBR_MODES + SBConstants.ALL_SPECIALS
for bake_type in bake_types:
node = [n for n in nodes if n.label == bake_type]
if not node:
continue
node = node[0]
tex = [i for i in bpy.data.images if
i.get("SB_bake_operation_id") == self.bake_operation_id and
i.get("SB_this_bake") == bake_type and
i.get("SB_bake_object") == self.target_object_name and
i.get("SB_bake_material") == mat_name]
if len(tex) == 1:
node.image = tex[0]
else:
nodes.remove(node)
create_aov_nodes(node_tree, self.bake_operation_id, new_obj)
if new_obj is not None:
new_obj.data.materials.append(mat)
mats.append(mat)
# Restore face→material-slot assignments now that the baked materials are in place.
if new_obj is not None:
for poly, idx in zip(new_obj.data.polygons, face_mat_indices):
poly.material_index = idx
self.mat_only = original_mat_only
return mats
def create_cyclesbake_setup_tex_per_mat(self, context, new_obj):
"""Create one CyclesBake material per original material slot using per-material baked images."""
source_obj = context.scene.objects[self.target_object_name]
original_mat_only = self.mat_only
self.mat_only = True # Prevent import_cyclesbake_mat_setup crashing when new_obj is None
# Save face→material-slot mapping from the source before create_object()'s
# materials.clear() resets all polygon material_index values to 0.
if new_obj is not None:
face_mat_indices = [p.material_index for p in source_obj.data.polygons]
mats = []
for slot in source_obj.material_slots:
if slot.material is None:
continue
# master_restore has not run yet when copy_and_apply is called, so slot names
# still carry the "SBM_" prefix — strip it to match the SB_bake_material tag.
mat_name = slot.material.name
if mat_name.startswith("SBM_"):
mat_name = mat_name[4:]
mat = self.import_cyclesbake_mat_setup(context, new_obj=None)
mat.name = f"{self.target_object_name}_{mat_name}_Baked"
node_tree = mat.node_tree
nodes = node_tree.nodes
# Non-specials cyclesbake image — per material
tex = [i for i in bpy.data.images if
i.get("SB_bake_operation_id") == self.bake_operation_id and
i.get("SB_this_bake") not in SBConstants.ALL_SPECIALS and
i.get("SB_bake_object") == self.target_object_name and
i.get("SB_bake_material") == mat_name]
if len(tex) == 1:
node = [n for n in nodes if n.label == "cyclesbake"]
if node:
node[0].image = tex[0]
for bake_type in SBConstants.ALL_SPECIALS:
node = [n for n in nodes if n.label == bake_type]
if not node:
continue
node = node[0]
spec_tex = [i for i in bpy.data.images if
i.get("SB_bake_operation_id") == self.bake_operation_id and
i.get("SB_this_bake") == bake_type and
i.get("SB_bake_object") == self.target_object_name and
i.get("SB_bake_material") == mat_name]
if len(spec_tex) == 1:
node.image = spec_tex[0]
else:
nodes.remove(node)
if new_obj is not None:
new_obj.data.materials.append(mat)
mats.append(mat)
# Restore face→material-slot assignments now that the baked materials are in place.
if new_obj is not None:
for poly, idx in zip(new_obj.data.polygons, face_mat_indices):
poly.material_index = idx
self.mat_only = original_mat_only
return mats
def create_pbr_setup(self, context, new_obj):
mat = self.import_pbr_mat_setup(context, new_obj)
node_tree = mat.node_tree
nodes = node_tree.nodes
bake_types = SBConstants.ALL_PBR_MODES #Every possible bake mode for PBR
bake_types += SBConstants.ALL_SPECIALS #Every possible bake mode for specials
#Set the textures
for bake_type in bake_types:
node = [n for n in nodes if n.label==bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] == bake_type
and "SB_bake_object" in i and i["SB_bake_object"] == self.target_object_name])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
#Finally the AOVs
create_aov_nodes(node_tree, self.bake_operation_id, new_obj)
return mat
def create_pbr_setup_merged(self, context, new_obj):
#Should be the only material with our bake id
for mat in bpy.data.materials:
if "SB_merged_copy_and_apply_mat" in mat\
and "SB_bake_operation_id" in mat\
and mat["SB_bake_operation_id"] == self.bake_operation_id:
#Assign existing merged mat to object and leave
if new_obj is not None:
new_obj.data.materials.append(mat)
return mat
#No existing merged bake material
mat = self.import_pbr_mat_setup(context, new_obj, merged=True)
node_tree = mat.node_tree
nodes = node_tree.nodes
bake_types = SBConstants.ALL_PBR_MODES #Every possible bake mode for PBR
bake_types += SBConstants.ALL_SPECIALS #Every possible bake mode for specials
#Set the textures
for bake_type in bake_types:
node = [n for n in nodes if n.label==bake_type]
if len(node) == 0: continue #E.g. Glossy will not be there for standard
else: node = node[0]
tex = ([i for i in bpy.data.images if "SB_bake_operation_id" in i
and i["SB_bake_operation_id"] == self.bake_operation_id
and "SB_this_bake" in i and i["SB_this_bake"] == bake_type
and "SB_merged_bake" in i and i["SB_merged_bake"] == True
and "SB_merged_bake_name" in i and i["SB_merged_bake_name"] == self.merged_bake_name
])
if len(tex) == 1:
tex = tex[0]
node.image = tex
else:
nodes.remove(node)
#Finally the AOVs
create_aov_nodes(node_tree, self.bake_operation_id, new_obj, merged_bake=True)
return mat
def hook_up_glTF_node(self, context, mat):
node_tree = mat.node_tree
nodes = node_tree.nodes
glTF_node = [n for n in nodes if n.label == "gltf"]
assert(len(glTF_node))
glTF_node = glTF_node[0]
target_option = self.glTF_option
target_node = [n for n in nodes if n.label == target_option]
assert(len(target_node))
target_node = target_node[0]
node_tree.links.new(target_node.outputs[0], glTF_node.inputs[0])
def create_object(self,context):
sbp = context.scene.SimpleBake_Props
source_obj = context.scene.objects[self.target_object_name]
if (source_obj.name + "_Baked") in context.scene.objects:
bpy.data.objects.remove(context.scene.objects[source_obj.name + "_Baked"])
new_obj = source_obj.copy()
new_obj.data = source_obj.data.copy()
new_obj["SB_copy_and_apply_from"] = source_obj.name
new_obj["SB_bake_operation_id"] = self.bake_operation_id
new_obj.name = source_obj.name + "_Baked"
new_obj.data.materials.clear()
if "SB_BG_HIDE" in new_obj:
del new_obj["SB_BG_HIDE"]
#Create a collection for our baked objects if it doesn't exist
col_name = "SimpleBake_Bakes_Background" if self.in_background else "SimpleBake_Bakes"
if col_name not in bpy.data.collections:
col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(col)
else:
col = bpy.data.collections[col_name]
if col_name not in context.scene.collection.children:
context.scene.collection.children.link(col)
try: col.color_tag = "COLOR_05"
except AttributeError: pass
#Make sure it's visible and enabled for current view laywer
context.view_layer.layer_collection.children[col_name].exclude = False
context.view_layer.layer_collection.children[col_name].hide_viewport = False
#Link object to our new collection
col.objects.link(new_obj)
#Remove tags from geo nodes sidestep
if "SB_proxy_bake_object" in new_obj:
del new_obj["SB_proxy_bake_object"]
if "SB_sidestepd_orig_target" in new_obj:
del new_obj["SB_sidestepd_orig_target"]
if "SB_replaced_orig_name" in new_obj:
del new_obj["SB_replaced_orig_name"]
#Set active uv to one we used for bake for this object. Remove others.
bake_uv_name = source_obj.get("SB_uv_used_for_bake")
if bake_uv_name and bake_uv_name in new_obj.data.uv_layers:
new_obj.data.uv_layers.active = new_obj.data.uv_layers[bake_uv_name]
del_list = [uvl.name for uvl in new_obj.data.uv_layers if uvl.name != bake_uv_name]
[new_obj.data.uv_layers.remove(new_obj.data.uv_layers[name]) for name in del_list]
#Hide source objects?
if self.hide_source_objects:
source_obj.hide_set(True)
return new_obj
def remove_unused_pbr(self,context,mat):
node_tree = mat.node_tree
nodes = node_tree.nodes
d_nodes_names = [n.name for n in nodes if n.bl_idname == "ShaderNodeDisplacement"]
nm_nodes_names = [n.name for n in nodes if n.bl_idname == "ShaderNodeNormalMap"]
gltf_node_names =[n.name for n in nodes if n.bl_idname == "ShaderNodeGroup" and n.label == "gltf"]
node_names = d_nodes_names + nm_nodes_names + gltf_node_names
for name in node_names:
if (n := nodes.get(name)):
unused = True
for i in n.inputs:
if len(i.links)>0:
unused = False
if unused:
nodes.remove(n)
rr_nodes = [n.name for n in nodes if n.bl_idname == "NodeReroute"]
for name in rr_nodes:
if (n := nodes.get(name)):
unused = True
for o in n.outputs:
if len(o.links)>0:
unused = False
if unused:
nodes.remove(n)
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if not Bip.Sequence.should_run_teardown():
print_message(context, "No object preperation - sequence and not last run")
return {'FINISHED'} #Nope out if this is part of a sequence
used_glossy = True if sbp.rough_glossy_switch == SBConstants.PBR_GLOSSY else False
used_ccglossy = True if sbp.ccrough_glossy_switch == SBConstants.PBR_CLEARCOAT_GLOSS else False
used_directx = True if sbp.normal_format_switch == SBConstants.NORMAL_DIRECTX else False
#Grab what we need from the panel
self.used_glossy = used_glossy
self.used_ccglossy = used_ccglossy
self.used_directx = used_directx
self.hide_source_objects = sbp.hide_source_objects
self.glTF = sbp.create_glTF_node
self.glTF_option = sbp.glTF_selection
self.merged_bake = sbp.merged_bake
self.cyclesbake_mat_format = sbp.cyclesbake_copy_and_apply_mat_format
self.merged_bake_name = sbp.merged_bake_name
self.mat_only = True if sbp.apply_bakes_to_original else False
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
print_message(context, f"Creating prepared object {self.target_object_name}")
tex_per_mat = sbp.tex_per_mat and not self.merged_bake and not self.decals_override
if self.mat_only and not tex_per_mat:
new_obj = None
else:
new_obj = self.create_object(context)
if tex_per_mat and self.global_mode in [SBConstants.PBR, SBConstants.PBRS2A]:
mats = self.create_pbr_setup_tex_per_mat(context, new_obj)
elif tex_per_mat and self.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]:
mats = self.create_cyclesbake_setup_tex_per_mat(context, new_obj)
else:
if self.decals_override:
mat = self.create_pbrdecal_setup(context, new_obj)
elif self.global_mode in [SBConstants.PBR, SBConstants.PBRS2A] and self.merged_bake:
mat = self.create_pbr_setup_merged(context, new_obj)
elif self.global_mode in [SBConstants.PBR, SBConstants.PBRS2A]:
mat = self.create_pbr_setup(context, new_obj)
elif self.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A] and self.merged_bake:
mat = self.create_cyclesbake_setup_merged(context, new_obj)
elif self.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]:
mat = self.create_cyclesbake_setup(context, new_obj)
else:
print_message(context, "Something went wrong with Copy and Apply")
return {'FINISHED'}
mats = [mat]
for mat in mats:
orig_objects = list(mat.get("SB_baked_orig_objects", []))
orig_objects.append(self.target_object_name)
mat["SB_baked_orig_objects"] = orig_objects
mat["SB_bake_operation_id"] = self.bake_operation_id
if not tex_per_mat:
mat = mats[0]
#Hook up glTF node?
if self.glTF:
self.hook_up_glTF_node(context, mat)
#Make sure this happens after the GLTF node is corrected
if self.global_mode in [SBConstants.PBR, SBConstants.PBRS2A] or self.decals_override:
for mat in mats:
self.remove_unused_pbr(context, mat)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Copy_And_Apply,
SimpleBake_OT_Apply_Bakes_To_Original
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+494
View File
@@ -0,0 +1,494 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty, BoolProperty, IntProperty
from pathlib import Path
import os
import sys
import re
from .utils import SBConstants, get_base_folder_patho, select_only_this, select_only_these, get_texture_channels, blender_default_colorspace
from .messages import print_message
from .background_and_progress import BakeInProgress as Bip
FILE_EXTENSIONS = {"PNG": "png", "JPEG": "jpg", "TIFF": "tiff", "TARGA": "tga","TARGA_RAW": "tga", "OPEN_EXR": "exr"}
def apply_scene_col_settings(context, scene):
scene.display_settings.display_device = context.scene.display_settings.display_device
scene.view_settings.view_transform = context.scene.view_settings.view_transform
scene.view_settings.look = context.scene.view_settings.look
scene.view_settings.exposure = context.scene.view_settings.exposure
scene.view_settings.gamma = context.scene.view_settings.gamma
scene.sequencer_colorspace_settings.name = context.scene.sequencer_colorspace_settings.name
return True
class SimpleBake_OT_Save_Images_Externally(Operator):
"""Saved baked images externally"""
bl_idname = "simplebake.save_images_externally"
bl_description = "Save baked images externally"
bl_label = "Save"
bake_operation_id: StringProperty()
bake_mode: StringProperty()
lightmap_apply_colman: BoolProperty(default=False)
def execute(self, context):
sbp = context.scene.SimpleBake_Props
#Set what we need from the panel
self.file_format = sbp.export_file_format
self.folder_per_object = sbp.export_folder_per_object
self.apply_col_man_to_col = sbp.apply_col_man_to_col
self.merged_bake = sbp.merged_bake
self.merged_bake_name = sbp.merged_bake_name
self.export_cycles_col_space = sbp.export_cycles_col_space
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
#Get all the images we want to save externally
imgs = ([img for img in bpy.data.images
if "SB_bake_operation_id" in img and
img["SB_bake_operation_id"] == self.bake_operation_id and
"SB_this_bake" in img and img["SB_this_bake"] == self.bake_mode
])
#Remove any already exported
for img in list(imgs):
exported_list = list(img["SB_exported_list"])
if self.bake_operation_id in exported_list:
print_message(context, f"{img.name} has already been exported")
imgs.remove(img)
#Exit here if there aren't any
if len(imgs) == 0:
print_message(context, f"No images to export, exiting")
return {'FINISHED'}
#Find the save location for each image
save_locations = {}
if self.in_background:
p = sbp.export_path.replace("//", "")
base_save_folder = Path(sbp.base_folder_override) / p
else:
base_save_folder = Path(bpy.path.abspath(sbp.export_path))
print_message(context, f"Saving baked images to {str(base_save_folder)}")
if not self.folder_per_object:#Not folder per object
for img in imgs:
save_locations[img.name] = base_save_folder #All the same
else: #Folder per object
if self.merged_bake:
for img in imgs:
save_locations[img.name] = base_save_folder / self.merged_bake_name #All the same
else:
for img in imgs:
obj_name = img["SB_bake_object"]
if img.get("SB_bake_material"):
save_locations[img.name] = base_save_folder / obj_name / img["SB_bake_material"]
else:
save_locations[img.name] = base_save_folder / obj_name
#Create the base save folder if it doesn't already exist
if not os.path.isdir(str(base_save_folder)):
os.makedirs(str(base_save_folder))
#We want a scene so we can control bit depth etc.
s = bpy.data.scenes.new("SB_Export_Temp")
s.render.image_settings.file_format = self.file_format
#Do we want to apply colour management from current scene?
#CyclesBake
s.view_settings.view_transform = "Standard" #default
if sbp.global_mode == "CyclesBake":
if self.export_cycles_col_space and self.bake_mode != "NORMAL":
print_message(context, "Applying scene col management settings CyclesBake")
apply_scene_col_settings(context, s)
else:
print_message(context, f"No col management settings for {self.bake_mode}")
#PBR
else:
if self.apply_col_man_to_col and self.bake_mode == SBConstants.PBR_DIFFUSE:
print_message(context, "Applying scene col management settings to PBR Diffuse")
apply_scene_col_settings(context, s)
else:
print_message(context, f"No col management settings for {self.bake_mode}")
#Either
if self.lightmap_apply_colman and self.bake_mode == SBConstants.LIGHTMAP:
print_message(context, "Applying scene col management settings lightmap")
apply_scene_col_settings(context, s)
#Bit depth
if self.file_format in ["JPEG", "TARGA", "TARGA_RAW"]:
pass # Skip this entirely
elif (sbp.everything_16bit or
self.bake_mode == SBConstants.PBR_NORMAL and not sbp.no_force_32bit_normals or
self.bake_mode == "NORMAL" and not sbp.no_force_32bit_normals):
s.render.image_settings.color_depth = "16"
elif self.file_format == "OPEN_EXR":
s.render.image_settings.color_depth = "32"
else:
s.render.image_settings.color_depth = "8"
#Channels
if self.file_format == "JPEG":
s.render.image_settings.color_mode = get_texture_channels(context, self.bake_mode)
else:
ua = sbp.texture_bg_col[3] < 1.0
if ua: s.render.image_settings.color_mode = "RGBA"
else: s.render.image_settings.color_mode = get_texture_channels(context, self.bake_mode)
print_message(context, f"Chosen {s.render.image_settings.color_mode} for {self.bake_mode}")
#Extras
if self.file_format == "JPEG":
s.render.image_settings.quality = sbp.jpeg_quality
if self.file_format == "OPEN_EXR":
s.render.image_settings.exr_codec = sbp.exr_codec_export
#Save to disk
for img in imgs:
save_folder = save_locations[img.name]
#Create the folder in the file system if it's not there already
create_dir_if_needed(save_folder)
ext = FILE_EXTENSIONS[self.file_format]
prefs = context.preferences.addons[__package__].preferences
if prefs.capitalise_file_extensions:
ext = ext.upper()
#Some housekeeping to do if we are going to save a UDIM image
udim_marker = "<UDIM>." if len(img.tiles)>1 else ""
if len(img.tiles)>1:
pattern = re.compile(rf"^{re.escape(img.name)}\.(\d{{4,}})\.{re.escape(ext)}$")
for file in save_folder.iterdir():
if file.is_file() and pattern.match(file.name):
print(f"Deleting: {file}") # optional
file.unlink()
save_path = save_folder / (img.name + "." + udim_marker + ext)
print_message(context, f"Save path: {str(save_path)}")
#Save image
#Let's try this
img.save_render(str(save_path).encode('utf-8').decode('utf-8'), scene=s)
#Store full path. We may need it later on. About to update to relative path
full_save_path = str(save_path)
img["SB_full_save_path"] = full_save_path
#Store the bit depth and channels too as this is oddly hard to get back later
img["SB_bit_depth"] = s.render.image_settings.color_depth
img["SB_channels"] = s.render.image_settings.color_mode
img["SB_view_transform"] = s.view_settings.view_transform
img["SB_exr_codec"] = s.render.image_settings.exr_codec
#Some changes needed for UDIM images
img.source = "TILED" if len(img.tiles)>1 else "FILE"
#TRY TO covnert to relative path. Even if in the background, as the append op will fix this
try:
img.filepath = bpy.path.relpath(full_save_path)
except ValueError:
print_message(context, "Converting to relative path failed - probably different drive on Windows")
img.filepath = full_save_path
try:
img.unpack(method="REMOVE")
except:
pass
#Colour space settings for (what is now) external image
if self.file_format == "OPEN_EXR":
#Get default for non-col (for OpenEXR)
img.colorspace_settings.name = blender_default_colorspace(float_buffer=False, col=False)[1]
elif "SB_denoised" in img and img["SB_denoised"] and(
sbp.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]):
img.colorspace_settings.name = sbp.cyclesbake_cs
else:
pass # Keep the defaults from when image was created
#Record that we exported this image for this bake job
exported_list = list(img["SB_exported_list"])
exported_list.append(self.bake_operation_id)
img["SB_exported_list"] = exported_list
print_message(context, f"{img.name} saved externally")
#When all done, remove the temp scene
bpy.data.scenes.remove(s)
return {'FINISHED'}
def create_dir_if_needed(path):
path = str(path)
if not os.path.isdir(path):
os.makedirs(path)
class SimpleBake_OT_Save_Objects_Externally(Operator):
"""Saved baked objects externally"""
bl_idname = "simplebake.save_objects_externally"
bl_description = "Save baked objects externally"
bl_label = "Save"
bake_operation_id: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if not Bip.Sequence.should_run_teardown():
print_message(context, "No external mesh save - sequence and not last run")
return {'FINISHED'} #Nope out if this is part of a sequence
#Set what we need from the panel
self.apply_mods_on_mesh_export = sbp.apply_mods_on_mesh_export
self.apply_transformation = sbp.apply_transformation
self.copy_and_apply = sbp.copy_and_apply
self.export_folder_per_object = sbp.export_folder_per_object
self.merged_bake = sbp.merged_bake
self.merged_bake_name = sbp.merged_bake_name
self.in_background = True if "--background" in sys.argv or sbp.fake_background else False
self.mesh_mode = sbp.export_mesh_individual_or_combined
blend_name = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
mesh_export_name = sbp.mesh_export_name if sbp.mesh_export_name != "%blend_file_name%" else blend_name
#Find objects we want to export. Depends what we are actually doing:
if sbp.apply_bakes_to_original and (sbp.cycles_s2a or sbp.selected_s2a):
if sbp.cycles_s2a:
objs = [sbp.targetobj_cycles]
if sbp.selected_s2a:
objs = [sbp.targetobj]
elif sbp.apply_bakes_to_original:
objs = [i.obj_point for i in sbp.objects_list]
else:
objs = ([o for o in context.scene.objects if "SB_copy_and_apply_from" in o and
"SB_bake_operation_id" in o and o["SB_bake_operation_id"] == self.bake_operation_id])
#Maybe merge the objects
if sbp.merge_export_obj and sbp.export_mesh_individual_or_combined == "combined" and sbp.merged_bake:
print_message(context, "Merging baked objects within the output mesh file")
dup_objects_names = []
for o in objs:
select_only_this(context, o)
bpy.ops.object.duplicate_move()
new_obj = bpy.context.active_object
dup_objects_names.append(new_obj.name)
#Join them together
select_only_these(context, [o:=bpy.data.objects.get(o_name) for o_name in dup_objects_names if o!=None])
bpy.ops.object.join()
#Just the one object now
objs = [context.active_object]
#Clear all materials except the first one
o = context.active_object
i=0
for slot in o.material_slots:
if i != 0:
slot.material = None
i+=1
#Name the mesh
context.active_object.name = f"{mesh_export_name}_Baked"
#Keep track of this for later deletion
merged_mesh_obj_name = context.active_object.name
#Export
if self.in_background:
p = sbp.export_path.replace("//", "")
base_save_folder = Path(sbp.base_folder_override) / p
else:
base_save_folder = Path(bpy.path.abspath(sbp.export_path))
print_message(context, f"Saving exported mesh to {str(base_save_folder)}")
#Create the base save folder if it doesn't already exist
create_dir_if_needed(base_save_folder)
if sbp.export_format == "fbx":
operator = getattr(bpy.ops.export_scene, "fbx")
extension = ".fbx"
#preset_path = bpy.utils.preset_paths('operator/export_scene.fbx/')
use_selection_name = "use_selection"
myargs = {
'check_existing': False,
'use_selection': True,
'use_mesh_modifiers': self.apply_mods_on_mesh_export,
'bake_space_transform': self.apply_transformation,
'path_mode': "STRIP"
}
elif sbp.export_format == "obj":
operator = getattr(bpy.ops.wm, "obj_export")
extension = ".obj"
#preset_path = bpy.utils.preset_paths('operator/wm.obj_export/')
use_selection_name = "export_selected_objects"
myargs = {
'check_existing': False,
'export_selected_objects': True,
'apply_modifiers': self.apply_mods_on_mesh_export,
'path_mode': "STRIP"
}
elif sbp.export_format == "dae":
operator = getattr(bpy.ops.wm, "collada_export")
extension = ".dae"
#preset_path = bpy.utils.preset_paths('operator/wm.obj_export/')
use_selection_name = "selected"
myargs = {
'check_existing': False,
'selected': True,
'apply_modifiers': self.apply_mods_on_mesh_export,
'use_texture_copies': False
}
else:
operator = getattr(bpy.ops.export_scene, "gltf")
extension = ".glb"
preset_path = bpy.utils.preset_paths('operator/export_scene.gltf/')
use_selection_name = "use_selection"
myargs = {
'check_existing': False,
'use_selection': True,
'export_apply': self.apply_mods_on_mesh_export
}
kwargs = None
#If we are using a preset, gather those settings
if (preset_name:=sbp.export_mesh_preset_name) not in ["None",""]:
filepath = sbp.export_mesh_preset_name #Internal name is the path to the preset file
class Container(object):
__slots__ = ('__dict__',)
op = Container()
file = open(filepath, 'r')
# storing the values from the preset on the class
for line in file.readlines()[3::]:
exec(line, globals(), locals())
kwargs = op.__dict__
def do_export(filepath):
if kwargs:
print_message(context,f"Using mesh export preset {preset_name}")
print_message(context, f"Using {sbp.export_format} preset values - {preset_name}")
kwargs["filepath"] = str(filepath).encode('utf-8').decode('utf-8')
kwargs[use_selection_name] = True
operator(**kwargs)
else:
print_message(context,f"No mesh export preset. Using default export settings")
myargs["filepath"] = str(filepath).encode('utf-8').decode('utf-8')
operator(**myargs)
#Not merged bake, sub-folder, combined mesh
if not self.merged_bake and self.export_folder_per_object and self.mesh_mode != 'individual':
filepath = base_save_folder / (mesh_export_name + extension)
#Make sure folder definitely exists
create_dir_if_needed(base_save_folder)
select_only_these(context, objs)
do_export(filepath)
#Folder per object, but not merged bake OR folder per object and merged bake but want individual meshes
elif self.export_folder_per_object and ((not self.merged_bake) or (self.merged_bake and self.mesh_mode=="individual")):
for obj in objs:
if "SB_copy_and_apply_from" in obj:
original_name = obj["SB_copy_and_apply_from"]
else:
original_name = obj.name
filepath = base_save_folder / original_name / (original_name + extension)
#Make sure folder definitely exists
create_dir_if_needed(base_save_folder / original_name)
select_only_this(context, obj)
do_export(filepath)
#Folder per object and merged bake. Combined mesh
elif self.export_folder_per_object and self.merged_bake:
filepath = base_save_folder / self.merged_bake_name / (mesh_export_name + extension)
create_dir_if_needed(base_save_folder / self.merged_bake_name)
select_only_these(context, objs)
do_export(filepath)
#Not folder per object, but we still want individual mesh files
elif self.mesh_mode=="individual":
for obj in objs:
if "SB_copy_and_apply_from" in obj:
original_name = obj["SB_copy_and_apply_from"]
else:
original_name = obj.name
filepath = base_save_folder / (original_name + extension)
#Make sure folder definitely exists
create_dir_if_needed(base_save_folder)
select_only_this(context, obj)
do_export(filepath)
#Not folder per object (either merged bake or not), but not apply to original objects
else:
filepath = base_save_folder / (mesh_export_name + extension)
create_dir_if_needed(base_save_folder)
select_only_these(context, objs)
do_export(filepath)
#If we didn't actually want copy and apply, remove the objects (unless in background
#AND UNLESS we are applying bakes to original objects
if not self.copy_and_apply and not self.in_background and not sbp.apply_bakes_to_original:
[bpy.data.objects.remove(o) for o in objs]
#If we merged the objects for export, remove the duplicate file
if sbp.merge_export_obj and sbp.export_mesh_individual_or_combined == "combined" and sbp.merged_bake:
if o:=bpy.data.objects.get(merged_mesh_obj_name):
bpy.data.objects.remove(o)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Save_Images_Externally,
SimpleBake_OT_Save_Objects_Externally
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,399 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from bpy.props import BoolProperty, StringProperty, IntProperty
import uuid
import numpy as np
import os
from .utils import SBConstants, get_udim_tiles, get_cached_udim_tiles, get_bake_objects, blender_default_colorspace
from .messages import print_message
from .background_and_progress import BakeInProgress as Bip
from .aov import get_aov_colspace, get_aov_type, get_aov_name
from . import __package__ as base_package
def gen_image_name(context, name, globalmode, baketype, mat_name=""):
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
proposed_name = prefs.img_name_format
if SBConstants.PBRAOVS in baketype:
baketype = get_aov_name(context, baketype)
filepath = bpy.data.filepath
if filepath:
filename_no_ext = os.path.splitext(os.path.basename(filepath))[0]
else:
filename_no_ext = ""
if Bip.Sequence.active:
if "%FRAME%" not in proposed_name:
proposed_name = f"{proposed_name}.%FRAME%"
proposed_name = proposed_name.replace("%FRAME%", f"{context.scene.frame_current:04d}")
else:
proposed_name = proposed_name.replace(".%FRAME%", "")
proposed_name = proposed_name.replace("%FRAME%", "")
proposed_name = proposed_name.replace("%OBJ%", name)
proposed_name = proposed_name.replace("%BAKEMODE%", globalmode)
proposed_name = proposed_name.replace("%BAKETYPE%", baketype)
proposed_name = proposed_name.replace("%BATCH%", sbp.batch_name)
proposed_name = proposed_name.replace("%BLEND_FILE_NAME%", filename_no_ext)
res = f"{sbp.outputwidth}x{sbp.outputheight}"
if prefs.abbr_res and sbp.outputwidth % 1024 == 0 and sbp.outputheight % 1024 == 0:
res = f"{int(sbp.outputwidth / 1024)}K"
proposed_name = proposed_name.replace("%RESOLUTION%", res)
if mat_name:
if "%MAT%" in proposed_name:
proposed_name = proposed_name.replace("%MAT%", mat_name)
else:
proposed_name = f"{proposed_name}_{mat_name}"
else:
proposed_name = proposed_name.replace("%MAT%", "")
return proposed_name
class SimpleBake_OT_Bake_Image(Operator):
"""Generate image to be baked to if needed"""
bl_idname = "simplebake.bake_image"
bl_description = "Generates image to be baked to if needed"
bl_label = "GenImage"
need_first_time_settings = []
bake_operation_id: StringProperty()
this_bake: StringProperty()
target_object_name: StringProperty()
global_mode: StringProperty()
bake_mode: StringProperty()
def create_individual_bake_image(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Getting individual baked image")
prefs = context.preferences.addons[base_package].preferences
alias_dict = prefs.get_alias_dict()
if self.this_bake in alias_dict: this_bake = alias_dict[self.this_bake]
else: this_bake = self.this_bake
proposed_name = gen_image_name(context, self.target_object_name, self.global_mode, this_bake)
need_new = False
if proposed_name in bpy.data.images and not self.clear_image:
#It's there, and we aren't going to clear it
#No action for now
i = bpy.data.images[proposed_name]
self.created_images.append(i)
self.set_image_tags(i)
need_new = False
elif proposed_name in bpy.data.images:
#It's there, but we want to clear it
if sbp.replace_previous_bakes:
bpy.data.images.remove(bpy.data.images[proposed_name])
need_new = True
else:
#It's not there
need_new = True
if need_new:
#Actually Create the image
tiled = True if (self.total_tiles > 1 and sbp.auto_detect_udims) else False
ua = sbp.texture_bg_col[3] < 1.0
i = bpy.data.images.new(proposed_name, self.img_width, self.img_height, alpha=ua, float_buffer=self.float_buffer, tiled=tiled)
self.created_images.append(i)
self.set_image_tags(i)
self.need_first_time_settings.append(i)
else:
#self.need_first_time_settings = False
pass
def create_merged_bake_image(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Getting merged bake image")
prefs = context.preferences.addons[base_package].preferences
alias_dict = prefs.get_alias_dict()
if self.this_bake in alias_dict: this_bake = alias_dict[self.this_bake]
else: this_bake = self.this_bake
proposed_name = gen_image_name(context, self.merged_bake_name, self.global_mode, this_bake)
need_new = False
if proposed_name not in bpy.data.images:
need_new = True
else:
i = bpy.data.images[proposed_name]
if "SB_bake_operation_id" in i and i["SB_bake_operation_id"] != self.bake_operation_id:
#It's there, but different bake ID
if self.clear_image:
if sbp.replace_previous_bakes:
bpy.data.images.remove(i)
need_new = True
else:
#Essentially do nothing
pass
elif "SB_bake_operation_id" not in i:
#It's there, but it doesn't even have an ID. This will be a weird coincidence
if self.clear_image:
if sbp.replace_previous_bakes:
bpy.data.images.remove(i)
need_new = True
else:
#Essentially do nothing
pass
elif "SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id:
#It's there, and it's our bake
need_new = False
if need_new:
tiled = True if (self.total_tiles > 1 and sbp.auto_detect_udims) else False
ua = sbp.texture_bg_col[3] < 1.0
i = bpy.data.images.new(proposed_name, self.img_width, self.img_height, alpha=ua, float_buffer=self.float_buffer, tiled=tiled)
#Create all the tiles
# if tiled:
# for t in self.active_tiles:
# i.tiles.new(t)
self.created_images.append(i)
self.set_image_tags(i)
self.need_first_time_settings.append(i)
else:
#Just using existing image, pretend we created it
#i is created up on line 113
self.created_images.append(i)
self.set_image_tags(i)
def create_per_mat_bake_image(self, context):
"""Create one bake image per enabled material for tex_per_mat mode."""
sbp = context.scene.SimpleBake_Props
prefs = context.preferences.addons[base_package].preferences
alias_dict = prefs.get_alias_dict()
this_bake = alias_dict.get(self.this_bake, self.this_bake)
# Get enabled materials for this object from mat_bake_list
all_entries = [e for e in sbp.mat_bake_list if e.obj_name == self.target_object_name]
enabled_mats = [e.mat_name for e in all_entries if e.enabled]
# If the object has no mat_bake_list entries at all (e.g. it's an S2A target object),
# fall back to standard single-image creation.
# If it has entries but all are disabled, skip image creation entirely.
if not all_entries:
self.create_individual_bake_image(context)
return
if not enabled_mats:
return
for mat_name in enabled_mats:
proposed_name = gen_image_name(
context, self.target_object_name, self.global_mode, this_bake,
mat_name=mat_name
)
need_new = False
if proposed_name in bpy.data.images and not self.clear_image:
i = bpy.data.images[proposed_name]
self.created_images.append(i)
self.set_image_tags(i, mat_name=mat_name)
need_new = False
elif proposed_name in bpy.data.images:
if sbp.replace_previous_bakes:
bpy.data.images.remove(bpy.data.images[proposed_name])
need_new = True
else:
need_new = True
if need_new:
tiled = True if (self.total_tiles > 1 and sbp.auto_detect_udims) else False
ua = sbp.texture_bg_col[3] < 1.0
i = bpy.data.images.new(
proposed_name, self.img_width, self.img_height,
alpha=ua, float_buffer=self.float_buffer, tiled=tiled
)
self.created_images.append(i)
self.set_image_tags(i, mat_name=mat_name)
self.need_first_time_settings.append(i)
def set_image_tags(self, i, mat_name=""):
i["SB_bake_object"] = self.target_object_name
i["SB_global_mode"] = self.global_mode
i["SB_this_bake"] = self.this_bake
i["SB_merged_bake"] = self.merged_bake
i["SB_merged_bake_name"] = self.merged_bake_name
i["SB_bake_operation_id"] = self.bake_operation_id
i["SB_float_buffer"] = self.float_buffer
i["SB_scaled"] = False
i["SB_exported_list"] = []
i["SB_denoised"] = False
i["SB_aa"] = False
if mat_name:
i["SB_bake_material"] = mat_name
def set_image_settings(self, context, imgs):
sbp = context.scene.SimpleBake_Props
for i in imgs:
print_message(context, f"Adjusting settings on new image {i.name}")
if self.this_bake == SBConstants.PBR_NORMAL:
gc = (0.5,0.5,1.0,1.0) #Normal ignores the use alpha option
elif self.this_bake == "NORMAL": #CyclesBake normal
gc = (0.5,0.5,1.0,1.0) #Normal ignores the use alpha option
#elif self.use_alpha:
#gc = (0.0,0.0,0.0,0.0)
else:
gc = sbp.texture_bg_col
i.generated_color = gc
#If this is a tiled image, let's go for all the tiles too
#if len(i.tiles) > 1: #Even an untiled image has tile length of 1
if i.source == 'TILED':
area = context.screen.areas[0]
old_type = area.type
area.type = "IMAGE_EDITOR"
area.spaces[0].image = i
with context.temp_override(area=area):
for t in self.active_tiles:
if t==1001:
continue
print(f"Going to add tile {t}")
ua = sbp.texture_bg_col[3] < 1.0
bpy.ops.image.tile_add(number=t, count=1, label="", fill=True, color=gc, generated_type='BLANK', width=self.img_width, height=self.img_height, float=self.float_buffer, alpha=ua)
area.type = old_type
print(f"Len tiles now {len(i.tiles)}")
prefs = context.preferences.addons[base_package].preferences
cs_dict = prefs.get_cs_dict()
if self.this_bake in cs_dict:
cs = cs_dict[self.this_bake]
elif self.global_mode in [SBConstants.CYCLESBAKE,SBConstants.CYCLESBAKE_S2A] and context.scene.cycles.bake_type != "NORMAL":
cs = sbp.cyclesbake_cs
elif SBConstants.PBRAOVS in self.this_bake:
print(f"AOV DETECTED BASED ON {self.this_bake}")
cs = get_aov_colspace(context, self.this_bake)
else:
cs = blender_default_colorspace(float_buffer=False, col=True)[1]
i.colorspace_settings.name = cs
print_message(context, f"Image colour space set to {cs}")
i.use_fake_user = True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
self.merged_bake = sbp.merged_bake
self.total_tiles = 0
self.active_tiles = []
if sbp.auto_detect_udims:
if not self.merged_bake:
#Nice and simple
r = get_cached_udim_tiles(context, self.target_object_name)
self.total_tiles = r["total_tiles"]
self.active_tiles = r["active_tiles"]
print_message(context, f"Object {self.target_object_name} has maximum UDIM tile {self.total_tiles}")
print_message(context, f"Object {self.target_object_name} has active UDIM tiles {self.active_tiles}")
else:
#Way more complicated
self.active_tiles = set()
self.total_tiles = 0
objects = get_bake_objects(context)
for o_name in objects:
if not (o:=context.scene.objects.get(o_name)):
continue
r = get_cached_udim_tiles(context, o.name)
these_active_tiles = r["active_tiles"]
self.active_tiles.update(these_active_tiles)
this_total_tiles = r["total_tiles"]
self.total_tiles = this_total_tiles if this_total_tiles > self.total_tiles else self.total_tiles
self.active_tiles = list(self.active_tiles)
print_message(context, f"Merged bake with maximum UDIM tile {self.total_tiles}")
print_message(context, f"Merged bake with active UDIM tiles {self.active_tiles}")
#Grab what we need from the panel
self.merged_bake_name = sbp.merged_bake_name
#self.use_alpha = sbp.use_alpha
self.img_height = sbp.imgheight
self.img_width = sbp.imgwidth
self.clear_image = sbp.clear_image
self.float_buffer = True if (self.bake_mode == SBConstants.PBR_NORMAL and not sbp.no_force_32bit_normals) or sbp.everything32bitfloat else False
self.need_first_time_settings = []
self.created_images = []
if self.merged_bake:
self.create_merged_bake_image(context)
elif sbp.tex_per_mat:
self.create_per_mat_bake_image(context)
else:
self.create_individual_bake_image(context)
#We had to create a new image
if self.created_images != []:
#Set image settings for images that are freshly created
self.set_image_settings(context, self.need_first_time_settings)
else:
print_message(context, "Using existing image")
return {'FINISHED'}
classes = ([
SimpleBake_OT_Bake_Image
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,148 @@
import bpy
from bpy.types import Operator
from bpy.props import EnumProperty, StringProperty
from bpy.utils import register_class, unregister_class
from .utils import SBConstants
from .messages import print_message
from .background_and_progress import BakeInProgress as Bip
class SimpleBake_OT_Material_Backup(Operator):
"""Backup object materials"""
bl_idname = "simplebake.material_backup"
bl_description = "Backup and restore materials on an object"
bl_label = "Backup/Restore"
mode: EnumProperty(items=[
("initialise", "initialise", ""),
("master_backup", "master_backup", ""),
("master_restore", "master_restore", ""),
("working_backup", "working_backup", ""),
("working_restore", "working_restore", "")
])
target_object_name: StringProperty()
MODE_MASTER_BACKUP = "master_backup"
MODE_MASTER_RESTORE = "master_restore"
MODE_WORKING_BACKUP = "working_backup"
MODE_WORKING_RESTORE = "working_restore"
MODE_INITIALISE = "initialise"
def initialise(self, context):
print_message(context, "Initialising material tags")
for mat in bpy.data.materials:
if "SB_working_dup" in mat: del mat["SB_working_dup"]
if "SB_master_dup" in mat: del mat["SB_master_dup"]
def backup(self, context):
print_message(context, f"Backup materials mode {self.tag}")
obj = context.scene.objects[self.target_object_name]
slots = [slot for slot in obj.material_slots if slot.material != None]
for slot in slots:
mat = slot.material
#Does a dup already exist for this mat. If so, use it and skip this slot
possible_dups = [m for m in bpy.data.materials if self.tag in m and m[self.tag]==mat.name]
if len(possible_dups)>0:
print_message(context, f"Backup of material {mat.name} already exists. Using {possible_dups[0].name}")
slot.material = possible_dups[0]
continue
dup = mat.copy()
dup[self.tag] = mat.name
prefix = self.tag[:4].upper().replace("_","")
dup.name = prefix + "_" + mat.name
slot.material = dup
#In case of crash
mat["SB_orig_fake_user"] = mat.use_fake_user
mat.use_fake_user = True
def restore(self, context):
#Do this scene wide
print_message(context, f"Restoring materials mode {self.tag}")
objs = context.scene.objects
del_list_mat_names = []
for obj in objs:
slots = [slot for slot in obj.material_slots if slot.material != None]
for slot in slots:
mat = slot.material
if self.tag in mat:
del_list_mat_names.append(mat.name)
orig_mat_name = mat[self.tag]
#Don't want to try and restore the placeholder material. Doesn't exist.
#if "SimpleBake_Placeholder" not in orig_mat_name:
if (m:=bpy.data.materials.get(orig_mat_name)):
slot.material = bpy.data.materials[orig_mat_name]
if "SB_orig_fake_user" in slot.material and not slot.material["SB_orig_fake_user"]:
slot.material.use_fake_user = False
for mat_name in del_list_mat_names:
if mat_name in bpy.data.materials:
mat = bpy.data.materials[mat_name]
bpy.data.materials.remove(mat, do_unlink=True)
def execute(self, context):
#Master backup and initialise are one-shot setup — only run on the
#first frame of a sequence (or on every bake when not in a sequence).
if not Bip.Sequence.should_run_setup():
if self.mode == self.__class__.MODE_MASTER_BACKUP:
print_message(context, "No master backup - sequence and not first run")
return{'FINISHED'}
if self.mode == self.__class__.MODE_INITIALISE:
print_message(context, "No materials initialise - sequence and not first run")
return{'FINISHED'}
#Master restore is one-shot teardown — only run on the last frame of
#a sequence (or on every bake when not in a sequence).
if not Bip.Sequence.should_run_teardown():
if self.mode == self.__class__.MODE_MASTER_RESTORE:
print_message(context, "No master restore - sequence and not last run")
return{'FINISHED'}
if self.mode == self.__class__.MODE_WORKING_BACKUP:
self.tag = "SB_working_dup"
self.backup(context)
elif self.mode == self.__class__.MODE_WORKING_RESTORE:
self.tag = "SB_working_dup"
self.restore(context)
elif self.mode == self.__class__.MODE_MASTER_BACKUP:
self.tag = "SB_master_dup"
self.backup(context)
elif self.mode == self.__class__.MODE_MASTER_RESTORE:
self.tag = "SB_master_dup"
self.restore(context)
elif self.mode == self.__class__.MODE_INITIALISE:
self.initialise(context)
return{'FINISHED'}
classes = ([
SimpleBake_OT_Material_Backup
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+154
View File
@@ -0,0 +1,154 @@
import bpy
from bpy.types import Operator
from bpy.props import StringProperty
from bpy.utils import register_class, unregister_class
from .background_and_progress import BakeInProgress
import blf
import gpu
from gpu_extras.batch import batch_for_shader
from datetime import datetime
_handle = None
past_messages = [""]
current_obj = ""
current_mode = ""
debug_only = False
def redraw_3d_viewport():
s = bpy.context.screen
viewport_areas = []
for a in s.areas:
if a.type == "VIEW_3D":
viewport_areas.append(a)
for a in viewport_areas:
a.tag_redraw()
def remove_callback_px():
global _handle
global past_messages
# Check if condition is met
if BakeInProgress.is_baking:
# Reset timer
bpy.app.timers.unregister(remove_callback_px)
bpy.app.timers.register(remove_callback_px, first_interval=5.0)
else:
bpy.app.timers.unregister(remove_callback_px)
past_messages = [""]
try:
bpy.types.SpaceView3D.draw_handler_remove(_handle, 'WINDOW')
_handle = None
except Exception as e:
_handle = None
def draw_callback_px():
global _handle
global past_messages
global current_mode
global current_obj
sbp=bpy.context.scene.SimpleBake_Props
#
# if past_messages[-1] != sbp.fg_status_message:
# past_messages.append(sbp.fg_status_message)
# past_messages = past_messages[-3:]
font_id = 0
blf.size(font_id, 20)
blf.color(font_id, 1, 1, 1, 1)
COUNT=10
pms = past_messages[-1*COUNT:]
i=0
while i < len(pms):
blf.position(font_id, 15, (COUNT-i)*30, 0)
blf.draw(font_id, pms[i])
i+=1
if not BakeInProgress.is_baking:
return True
if BakeInProgress.Sequence.active:
blf.position(font_id, 15, (COUNT+4)*30, 0)
blf.draw(font_id, f"FRAME NUMBER - {bpy.context.scene.frame_current} (End Frame: {sbp.image_sequence_end_frame})")
blf.position(font_id, 15, (COUNT+3)*30, 0)
blf.draw(font_id, f"SIMPLEBAKE - BAKING IN PROGRESS - {sbp.percent_complete}% COMPLETE")
blf.position(font_id, 15, (COUNT+2)*30, 0)
if BakeInProgress.is_baking:
blf.draw(font_id, f"CURRENTLY BAKING | {current_obj} | {current_mode}")
blf.position(font_id, 15, (COUNT+1)*30, 0)
blf.draw(font_id, f"------------------------------------------------------------------------")
def print_message(context, msg, screen=True, debug=False):
#import time
#time.sleep(0.1) # Sleep for 100 milliseconds
if (debug_only and debug) or not debug_only:
print(f"SIMPLEBAKE: -- {msg}")
global _handle
if _handle == None and screen:
args = ()
_handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
bpy.app.timers.register(remove_callback_px, first_interval=5.0)
global past_messages
try:#Because we can be called on Blender load and get that restricted context error
sbp=context.scene.SimpleBake_Props
current_time = datetime.now().time()
formatted_time = current_time.strftime('%H:%M:%S')
sbp.fg_status_message = f"[{formatted_time}] - {msg}"
past_messages.append(sbp.fg_status_message)
redraw_3d_viewport()
except:
pass
global current_mode
global current_obj
if "Starting bake mode:" in msg:
current_mode = msg.replace("Starting bake mode: ","")
if "Starting object:" in msg:
current_obj = msg.replace("Starting object: ","")
class SimpleBake_OT_Print_Message(Operator):
bl_idname = "simplebake.print_message"
bl_label = ""
message: StringProperty()
def execute(self, context):
global _handle
if _handle == None:
args = ()
_handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
print_message(context, self.message)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Print_Message
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,657 @@
import bpy
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
from bpy.utils import register_class, unregister_class
from .utils import SBConstants
from .messages import print_message
from .aov import get_aov_name
class SimpleBake_OT_PrepareObjectMatsPbr(Operator):
"""Prepare an object for PBR baking"""
bl_idname = "simplebake.prepare_object_mats_pbr"
bl_description = "Prepare object for PBR bake"
bl_label = "Prepare"
target_name: StringProperty()
this_bake: StringProperty()
bake_operation_id: StringProperty()
no_bake_image_node: BoolProperty(default=False)
only_bake_image_node: BoolProperty(default=False)
def create_dummy_nodes(self, context, mat):
print_message(context, f"Creating dummy nodes for {self.target_name}")
node_tree = mat.node_tree
nodes = node_tree.nodes
p_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeBsdfPrincipled"]
#Special case for bump
if self.this_bake == SBConstants.PBR_BUMP:
for p_node in p_nodes:
#Nothing connected
if len(p_node.inputs["Normal"].links) == 0:
p_node["SB_Ignore_Normal"]= True
continue
#Connected but not a bump node
bump_node = p_node.inputs["Normal"].links[0].from_node
if bump_node.bl_idname != "ShaderNodeBump":
p_node["SB_Ignore_Normal"]= True
continue
#Nothing connected to bump node height input
if len(bump_node.inputs['Height'].links) == 0:
p_node["SB_Ignore_Normal"]= True
continue
#Must be OK to be treated normally
#p_node.inputs["Normal"]["SB_Ignore"] = False
#if len(bump_node.inputs[2].links) == 0:
#val = bump_node.inputs[2].default_value
#vnode = nodes.new("ShaderNodeValue")
#vnode.outputs[0].default_value = val
#vnode.label = "SimpleBake"
#node_tree.links.new(vnode.outputs[0], bump_node.inputs[2])
#Normal case, not bump
else:
socket_name = SBConstants.get_socket_names()[self.this_bake]
for p_node in p_nodes:
#Get the sockets we care about (correct name + empty)
sockets = [s for s in p_node.inputs if s.name == socket_name and len(s.links)==0]
for socket in sockets:
#If not, get value of the unconnected socket
val = socket.default_value
socket_type = socket.bl_idname
#if socket_type == "NodeSocketColor":
#Not needed, but future proofing
if socket_type.startswith("NodeSocketColor"):
rgb = nodes.new("ShaderNodeRGB")
rgb.outputs[0].default_value = val
rgb.label = "SimpleBake" #????????????????????????????
node_tree.links.new(rgb.outputs[0], socket)
#if socket_type in ["NodeSocketFloat", "NodeSocketFloatFactor", "NodeSocketFloatDistance"]:
if socket_type.startswith("NodeSocketFloat"):
vnode = nodes.new("ShaderNodeValue")
vnode.outputs[0].default_value = val
vnode.label = "SimpleBake" #?????????????????????????????????????????
node_tree.links.new(vnode.outputs[0], socket)
def bypass_muted_mix_shaders(self, mat):
#Let's bypass all the muted mix shader nodes
node_tree = mat.node_tree
nodes = node_tree.nodes
for n in nodes:
if n.bl_idname == "ShaderNodeMixShader" and n.mute:
if len(n.inputs[1].links) > 0:
fs = n.inputs[1].links[0].from_socket
else: fs = False
if len(n.outputs[0].links) > 0:
ts = n.outputs[0].links[0].to_socket
else: ts = False
if fs and ts:
node_tree.links.new(fs, ts)
nodes.remove(n)
def swap_e_for_p(self,mat):
node_tree = mat.node_tree
nodes = node_tree.nodes
enodes = [n for n in nodes if n.bl_idname == "ShaderNodeEmission"]
for e in enodes:
cval = e.inputs[0].default_value
sval = e.inputs[1].default_value
pos = e.location
if len(e.inputs[0].links) > 0:
cfs = e.inputs[0].links[0].from_socket
else:
cfs = False
if len(e.inputs[1].links) > 0:
sfs = e.inputs[1].links[0].from_socket
else:
sfs = False
os = e.outputs[0].links[0].to_socket
#Create principled BSDF
p = nodes.new("ShaderNodeBsdfPrincipled")
if cfs:
node_tree.links.new(cfs, p.inputs["Emission Color"])
else:
p.inputs["Emission Color"].default_value = cval
if sfs:
node_tree.links.new(sfs, p.inputs["Emission Strength"])
else:
p.inputs["Emission Strength"].default_value = sval
p.inputs["Base Color"].default_value = (0.0,0.0,0.0,1.0)
node_tree.links.new(p.outputs[0], os)
p.location = pos
#Last action, remove all the e nodes
[nodes.remove(n) for n in enodes]
def remove_disconnected(self, context, mat):
print_message(context, f"Removing disconnected nodes from {mat.name}")
nodes = mat.node_tree.nodes
#Start with the material output node
#Remove any not in use. So not to confuse matters.
[nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and len(n.inputs[0].links)==0]
#Find the active material output
mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == True]
assert(len(mos) == 1)
active_mo = mos[0]
#Is it Cycles?
if active_mo.target in ["CYCLES", "ALL"]:
#Yes. So we are good. Just remove the others
mos = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False]
else:
#Active not Cycles. Can we find a pre-existing Cycles one somewhere?
mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.target in ["CYCLES","ALL"]]
if len(mos)>0:
#Found existing Cycles one, make it active and remove the others
mos[0].is_active_output = True
#Set to All, just for the viewport candy
mos[0].target = "ALL"
[nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False]
else:
#Active isn't Cycles, and no Cycles in the material. Pick one at random. Force to Cycles
mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial"]
mos[0].is_active_output = True
mos[0].target = "ALL"
[nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False]
#Loop through nodes to remove disconnected nodes that we care about
repeat = False
r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeBsdfPrincipled" and len(n.outputs[0].links) == 0 ]
if len(r) >0: repeat = True #We removed something
r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeEmission" and len(n.outputs[0].links) == 0 ]
if len(r) >0: repeat = True #We removed something
r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeMixShader" and len(n.outputs[0].links) == 0 ]
if len(r) >0: repeat = True #We removed something
#Special case for Shader to RGB node
r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeShaderToRGB" and len(n.outputs[0].links) == 0 ]
if len(r) >0: repeat = True #We removed something
#If we removed any nodes, we need to do this again
if repeat: self.remove_disconnected(context, mat)
def get_active_output_node(self, mat):
nodes = mat.node_tree.nodes
output_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial"]
assert(len(output_nodes)==1)
return output_nodes[0]
def get_surface_shader_node(self, mat):
output_node = self.get_active_output_node(mat)
surface = output_node.inputs.get("Surface")
if surface == None or len(surface.links) == 0:
return None
return surface.links[0].from_node
def setup_pure_p_mat(self, context, mat):
print_message(context, f"Setup for pure P material for {self.target_name}")
node_tree = mat.node_tree
nodes = node_tree.nodes
output_node = self.get_active_output_node(mat)
p_node = self.get_surface_shader_node(mat)
if p_node == None or p_node.bl_idname != "ShaderNodeBsdfPrincipled":
print_message(context, f"Material {mat.name} is not a pure Principled material")
return False
#Create an emission shader and connect to output
emissnode = nodes.new("ShaderNodeEmission")
emissnode.label = "SimpleBake" #??????????????????????????????????
emissnode.location = (output_node.location.x, output_node.location.y+200)
node_tree.links.new(emissnode.outputs[0], output_node.inputs[0])
#Connect whatever is in Principled Shader for this bakemode to the emission
if self.this_bake == SBConstants.PBR_BUMP:
normal_socket = p_node.inputs["Normal"]
if "SB_Ignore_Normal" in p_node:
emissnode.inputs[0].default_value = (0.5,0.5,0.5,1)
else:
bump_node = p_node.inputs["Normal"].links[0].from_node
bump_input = bump_node.inputs['Height'].links[0].from_socket
node_tree.links.new(bump_input, emissnode.inputs[0])
else:
socket_name = SBConstants.get_socket_names()[self.this_bake]
socket = p_node.inputs[socket_name]
node_tree.links.new(socket.links[0].from_socket, emissnode.inputs[0])
return True
def setup_mix_mat(self, context, mat):
print_message(context, f"Setup for mix material for {self.target_name}")
node_tree = mat.node_tree
nodes = node_tree.nodes
output_node = self.get_active_output_node(mat)
surface_node = self.get_surface_shader_node(mat)
if surface_node == None or surface_node.bl_idname != "ShaderNodeMixShader":
print_message(context, f"Material {mat.name} is not a Mix Shader material")
return False
#For every mix shader, create a mixrgb above it
mix_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeMixShader"]
for mix_node in mix_nodes:
#Create RGB mix for every mix shader node
rgbmix = nodes.new("ShaderNodeMixRGB")
rgbmix.location = (mix_node.location.x, mix_node.location.y+200)
rgbmix.label="SimpleBake" #????????????????????????????????????????
rgbmix["SB_rgb_proxy_for"] = mix_node.name
#If there is one, plug the factor from the original mix node into our new mix node
if len(mix_node.inputs[0].links) > 0:
node_tree.links.new(mix_node.inputs[0].links[0].from_socket, rgbmix.inputs[0])
#Else, copy the default value
else:
rgbmix.inputs[0].default_value = mix_node.inputs[0].default_value
#Loop over the new RGBmix nodes
rgbmix_nodes = [n for n in nodes if "SB_rgb_proxy_for" in n]
assert(len(rgbmix_nodes)>0)
for rgbmix_node in rgbmix_nodes:
#Get the original mix node
mix_node = nodes[rgbmix_node["SB_rgb_proxy_for"]]
#Socket 1 (back to p_node)
if len(mix_node.inputs[1].links)>0 and mix_node.inputs[1].links[0].from_node.bl_idname == "ShaderNodeBsdfPrincipled":
origin_p_node = mix_node.inputs[1].links[0].from_node
if self.this_bake == SBConstants.PBR_BUMP:
normal_socket = origin_p_node.inputs["Normal"]
if "SB_Ignore_Normal" in origin_p_node:
rgbmix_node.inputs[1].default_value = (0.5,0.5,0.5,1)
else:
bump_node = origin_p_node.inputs["Normal"].links[0].from_node
bump_input = bump_node.inputs['Height'].links[0].from_socket
node_tree.links.new(bump_input, rgbmix_node.inputs[1])
else:
socket_name = SBConstants.get_socket_names()[self.this_bake]
origin_socket = origin_p_node.inputs[socket_name].links[0].from_socket
node_tree.links.new(origin_socket, rgbmix_node.inputs[1])
#Socket 2 (back to p_node)
if len(mix_node.inputs[2].links)>0 and mix_node.inputs[2].links[0].from_node.bl_idname == "ShaderNodeBsdfPrincipled":
origin_p_node = mix_node.inputs[2].links[0].from_node
if self.this_bake == SBConstants.PBR_BUMP:
normal_socket = origin_p_node.inputs["Normal"]
if "SB_Ignore_Normal" in origin_p_node:
rgbmix_node.inputs[2].default_value = (0.5,0.5,0.5,1)
else:
bump_node = origin_p_node.inputs["Normal"].links[0].from_node
bump_input = bump_node.inputs['Height'].links[0].from_socket
node_tree.links.new(bump_input, rgbmix_node.inputs[2])
else:
socket_name = SBConstants.get_socket_names()[self.this_bake]
origin_socket = origin_p_node.inputs[socket_name].links[0].from_socket
node_tree.links.new(origin_socket, rgbmix_node.inputs[2])
#Socket 1 (back to mix node)
if len(mix_node.inputs[1].links)>0 and mix_node.inputs[1].links[0].from_node.bl_idname == "ShaderNodeMixShader":
origin_mix_node = mix_node.inputs[1].links[0].from_node
#Get the RGB mix for that origin mix node
origin_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == origin_mix_node.name]
assert(len(origin_rgbmix) == 1)
origin_rgbmix = origin_rgbmix[0]
node_tree.links.new(origin_rgbmix.outputs[0], rgbmix_node.inputs[1])
#Socket 2 (back to mix node)
if len(mix_node.inputs[2].links)>0 and mix_node.inputs[2].links[0].from_node.bl_idname == "ShaderNodeMixShader":
origin_mix_node = mix_node.inputs[2].links[0].from_node
#Get the RGB mix for that origin mix node
origin_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == origin_mix_node.name]
assert(len(origin_rgbmix) == 1)
origin_rgbmix = origin_rgbmix[0]
node_tree.links.new(origin_rgbmix.outputs[0], rgbmix_node.inputs[2])
#Back to nowhere
if len(mix_node.inputs[1].links) == 0:
rgbmix_node.inputs[1].default_value = (0,0,0,1)
if len(mix_node.inputs[2].links) == 0:
rgbmix_node.inputs[2].default_value = (0,0,0,1)
#Finally connect to the output
final_mix = output_node.inputs[0].links[0].from_node
final_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == final_mix.name]
assert(len(final_rgbmix)>0)
final_rgbmix = final_rgbmix[0]
node_tree.links.new(final_rgbmix.outputs[0], output_node.inputs[0])
return True
def setup_displacement(self, context, mat):
print_message(context, f"Setup for displacement bake on {self.target_name}")
node_tree = mat.node_tree
nodes = node_tree.nodes
#Find the active MO node
mo_node = None
for n in nodes:
if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output:
mo_node = n
if mo_node == None:
print(f"Error - no active Material Output in material {mat.name}")
return False
#See if anything is plugged into the Displacement Socket
mo_disp_socket = mo_node.inputs["Displacement"]
if len(mo_disp_socket.links) <1:
print_message(context, f"Skipping. Nothing plugged into displacement socket for material {mat.name}")
return True
#Find the from node. Check it's either Displacement or Displacement Vector
disp_node = mo_disp_socket.links[0].from_node
if disp_node.bl_idname not in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]:
return False
#Found disp node
source_socket = None
socket_name = "Height" if disp_node.bl_idname == "ShaderNodeDisplacement" else "Vector"
#Is something plugged into Height/Vector??
if len(disp_node.inputs[socket_name].links)>0:
source_socket = disp_node.inputs[socket_name].links[0].from_socket
else:
#NOTHING PLUGGED IN
if disp_node.inputs[socket_name].bl_idname == "NodeSocketColor":
#No dummy node possible
return False
else:
value_node = nodes.new(type='ShaderNodeValue')
value_node.outputs[0].default_value = disp_node.inputs[socket_name].default_value
source_socket = value_node.outputs[0]
if source_socket == None:
return False
#Create emission node and hook it up
emission_node = nodes.new(type='ShaderNodeEmission')
emission_node.location = (mo_node.location.x, mo_node.location.y+150)
node_tree.links.new(source_socket, emission_node.inputs["Color"])
node_tree.links.new(emission_node.outputs[0], mo_node.inputs["Surface"])
def setup_aov(self, context, mat, aovname):
print_message(context, f"Setup for AOV bake for {aovname}")
node_tree = mat.node_tree
nodes = node_tree.nodes
aov_node = None
#Find the AOV node that we want
for n in nodes:
if n.bl_idname == "ShaderNodeOutputAOV":
if n.aov_name == aovname:
#Found it
aov_node = n
#Give up if we can't find the AOV node we want (for whatver reason)
if aov_node == None:
return False
#Find the active MO node
mo_node = None
for n in nodes:
if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output:
mo_node = n
assert(mo_node!=None)
#Col is prioritised to fit with how vanilla Blender does it
if len(aov_node.inputs['Color'].links)>0:
source_socket = aov_node.inputs['Color'].links[0].from_socket
elif len(aov_node.inputs['Value'].links)>0:
source_socket = aov_node.inputs['Value'].links[0].from_socket
else:
print_message(context, "ERROR - Couldn't find source of AOV node")
return False
#Create emission node and hook it up
emission_node = nodes.new(type='ShaderNodeEmission')
emission_node.location = (mo_node.location.x, mo_node.location.y+150)
node_tree.links.new(source_socket, emission_node.inputs["Color"])
node_tree.links.new(emission_node.outputs[0], mo_node.inputs["Surface"])
def create_bake_image_node(self, mat, mat_name=""):
node_tree = mat.node_tree
nodes = node_tree.nodes
existing_bake_nodes = [n for n in nodes if "SB_bake_image_node" in n]
if len(existing_bake_nodes) !=0:
#Just quit if there is already a node in this material
#e.g. Specials are used more than once. Also object may have same PBR material multiple times
return True
image_node = nodes.new("ShaderNodeTexImage")
#Where is the image we want? This is different for a merged bake or per-mat bake
if not self.merged_bake:
if mat_name:
# tex_per_mat: find the image specific to this material
image_name = ([i.name for i in bpy.data.images if
i.get("SB_bake_object") == self.target_name and
i.get("SB_bake_operation_id") == self.bake_operation_id and
i.get("SB_this_bake") == self.this_bake and
i.get("SB_bake_material") == mat_name
])
else:
image_name = ([i.name for i in bpy.data.images if
"SB_bake_object" in i and i["SB_bake_object"] == self.target_name and
"SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id and
"SB_this_bake" in i and i["SB_this_bake"] == self.this_bake
])
else:
image_name = ([i.name for i in bpy.data.images if
"SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id and
"SB_this_bake" in i and i["SB_this_bake"] == self.this_bake
])
assert len(image_name)==1, f"Found {len(image_name)} images looking for {self.this_bake}"
image_name = image_name[0]
image_node.image = bpy.data.images[image_name]
#Leave it the only node selected
for n in nodes:
n.select = False
image_node.select = True
nodes.active = image_node
image_node["SB_bake_image_node"] = True
return True
def create_dummy_bake_image_node(self, mat):
"""Create a throwaway 1x1 image node for excluded materials so Blender has
somewhere to write their pixels (output is discarded)."""
node_tree = mat.node_tree
nodes = node_tree.nodes
existing_bake_nodes = [n for n in nodes if "SB_bake_image_node" in n]
if existing_bake_nodes:
return
dummy_name = "SB_excluded_mat_dummy"
if dummy_name not in bpy.data.images:
dummy = bpy.data.images.new(dummy_name, 1, 1, alpha=False)
dummy["SB_dummy"] = True
else:
dummy = bpy.data.images[dummy_name]
image_node = nodes.new("ShaderNodeTexImage")
image_node.image = dummy
for n in nodes:
n.select = False
image_node.select = True
nodes.active = image_node
image_node["SB_bake_image_node"] = True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
self.merged_bake = sbp.merged_bake
mats=[]
if not (o:= context.scene.objects.get(self.target_name)):
print_message(context, f"ERROR: Preparing object. Couldn't find {self.target_name}!")
return {'CANCELLED'}
else:
#Get list of materials for target object
mats = [slot.material for slot in o.material_slots if slot.material!=None]
# Build per-material enabled/disabled sets from mat_bake_list (empty = no restrictions)
# Working backup materials have an SBW_ prefix — strip it to get the original name
mat_entries = [e for e in sbp.mat_bake_list if e.obj_name == self.target_name]
enabled_mat_names = {e.mat_name for e in mat_entries if e.enabled}
disabled_mat_names = {e.mat_name for e in mat_entries if not e.enabled}
has_restrictions = bool(mat_entries) # any mat_bake_list entries for this object
def orig_name(mat):
"""Return the original material name, stripping SBW_ (working) and SBM_ (master) backup prefixes."""
name = mat.name
if name.startswith("SBW_"):
name = name[4:]
if name.startswith("SBM_"):
name = name[4:]
return name
#No prep needed for normal or any specials. Just create bake image node (if that)
if (self.this_bake == SBConstants.PBR_NORMAL) or (self.this_bake in SBConstants.ALL_SPECIALS):
print_message(context, "No object prep needed - specials or normal map")
if not self.no_bake_image_node:
for mat in mats:
oname = orig_name(mat)
if self.this_bake in SBConstants.ALL_SPECIALS and "SB_original_mat_name" in mat:
oname = mat["SB_original_mat_name"]
if has_restrictions and oname in disabled_mat_names:
self.create_dummy_bake_image_node(mat)
else:
use_per_mat = sbp.tex_per_mat and not self.merged_bake and has_restrictions
bake_mat_name = oname if use_per_mat else ""
self.create_bake_image_node(mat, mat_name=bake_mat_name)
mat["SB_bake_config"] = self.this_bake
return {'FINISHED'}
#Otherwise, prepare each material for this bake type
for mat in mats:
oname = orig_name(mat)
# Handle excluded materials
if has_restrictions and oname in disabled_mat_names:
print_message(context, f"Material {mat.name} is excluded - creating dummy node")
self.create_dummy_bake_image_node(mat)
continue
if "SB_bake_config" in mat and mat["SB_bake_config"] == self.this_bake:
print_message(context, f"Material {mat.name} is already in correct config for {self.this_bake}")
continue
# mat_name to use when looking up per-material image (use original name, not working backup name)
# Only use per-mat lookup if this object has mat_bake_list entries (i.e. it's a source object,
# not an S2A target object which won't have any entries)
bake_mat_name = oname if (sbp.tex_per_mat and not self.merged_bake and has_restrictions) else ""
if self.only_bake_image_node:
print_message(context, f"Just creating bake image node in mat {mat}")
self.create_bake_image_node(mat, mat_name=bake_mat_name)
continue
#Special case for displacement. Don't need to mess with outher nodes
if self.this_bake == SBConstants.PBR_DISPLACEMENT:
self.setup_displacement(context, mat)
if not self.no_bake_image_node:
self.create_bake_image_node(mat, mat_name=bake_mat_name)
mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES
continue
#Special case for AOVs. Don't need to mess with outher nodes
print(f"--------------- {self.this_bake}")
if SBConstants.PBRAOVS in self.this_bake:
aovname = get_aov_name(context, self.this_bake)
self.setup_aov(context, mat, aovname)
if not self.no_bake_image_node:
self.create_bake_image_node(mat, mat_name=bake_mat_name)
mat["SB_bake_config"] = self.this_bake
continue
#Bypass muted mix shaders
self.bypass_muted_mix_shaders(mat)
#Remove disconnected nodes
self.remove_disconnected(context, mat)
#Swap all e for p
self.swap_e_for_p(mat)
#What kind of material is this?
node_tree = mat.node_tree
nodes = node_tree.nodes
surface_node = self.get_surface_shader_node(mat)
if surface_node != None and surface_node.bl_idname == "ShaderNodeMixShader":
print_message(context, "This is a Mix Material")
self.create_dummy_nodes(context, mat)
if self.setup_mix_mat(context, mat):
mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES
elif(surface_node != None and surface_node.bl_idname == "ShaderNodeBsdfPrincipled"):
#print_message(context, "This is a Pure P material")
self.create_dummy_nodes(context, mat)
if self.setup_pure_p_mat(context, mat):
mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES
else:
print_message(context, "Non-PBR material type - will still create bake image node")
if not self.no_bake_image_node:
self.create_bake_image_node(mat, mat_name=bake_mat_name)
return {'FINISHED'}
classes = ([
SimpleBake_OT_PrepareObjectMatsPbr
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+514
View File
@@ -0,0 +1,514 @@
#LEGACY FIX - Non SB settings now being stored with their full name relative from context.scene. But previously
#stored with just their last name
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
import os
import json
from pathlib import Path
from .utils import clean_file_name, SBConstants, show_message_box
from . import utils
from .messages import print_message
from .ui.objects_list import refresh_mat_bake_list
def fix_legacy(d):
name_mapping = {
"bake_type": "cycles.bake_type",
"use_pass_direct": "render.bake.use_pass_direct",
"use_pass_indirect": "render.bake.use_pass_indirect",
"use_pass_diffuse": "render.bake.use_pass_diffuse",
"use_pass_glossy": "render.bake.use_pass_glossy",
"use_pass_transmission": "render.bake.use_pass_transmission",
"use_pass_emit": "render.bake.use_pass_emit",
"samples": "cycles.samples",
"bake.normal_space": "render.bake.normal_space",
"normal_r": "render.bake.normal_r",
"normal_g": "render.bake.normal_g",
"normal_b": "render.bake.normal_b",
"use_pass_color": "render.bake.use_pass_color",
"bake.margin": "render.bake.margin",
"bake.margin_type": "render.bake.margin_type",
"exr_codec": "render.image_settings.exr_codec",
"cycles_denoise": "cycles.use_denoising",
"cycles_denoiser": "cycles.denoiser",
"cycles_denoising_input_passes": "cycles.denoising_input_passes",
"cycles_denoising_prefilter": "cycles.denoising_prefilter"
}
new_dict = {}
for old_key, value in d.items():
# Use the new key if it exists in the name_mapping, otherwise keep the old key
new_key = name_mapping.get(old_key, old_key)
new_dict[new_key] = value
return new_dict
def deep_setattr(obj, attr, value):
"""Recursively set attributes on an object, including sub-objects."""
pre, _, post = attr.rpartition('.')
return setattr(deep_getattr(obj, pre) if pre else obj, post, value)
def deep_getattr(obj, attr):
"""Recursively get attributes from an object, including sub-objects."""
for part in attr.split('.'):
obj = getattr(obj, part)
return obj
basic_props = ([
"global_mode",
"ray_distance",
"cage_and_ray_multiplier",
"cage_extrusion",
"auto_match_mode",
"selected_s2a",
"s2a_opmode",
"merged_bake",
"merged_bake_name",
"cycles_s2a",
"imgheight",
"imgwidth",
"outputheight",
"outputwidth",
"everything32bitfloat",
#"use_alpha",
"texture_bg_col",
"rough_glossy_switch",
"ccrough_glossy_switch",
"multiply_diffuse_ao",
"multiply_diffuse_ao_percent",
"normal_format_switch",
"tex_per_mat",
"selected_col",
"selected_metal",
"selected_rough",
"selected_normal",
"selected_trans",
"selected_transrough",
"selected_emission",
"selected_emission_strength",
"selected_sss",
"selected_sss_scale",
"selected_ssscol",
"selected_clearcoat",
"selected_clearcoat_rough",
"selected_specular",
"selected_alpha",
"selected_col_mats",
"selected_col_vertex",
"selected_ao",
"selected_thickness",
"selected_curvature",
"selected_lightmap",
"selected_displacement",
"lightmap_apply_colman",
"new_uv_option",
"prefer_existing_sbmap",
"new_uv_method",
"restore_orig_uv_map",
"uvpackmargin",
"average_uv_size",
"expand_mat_uvs",
"auto_detect_udims",
"unwrapmargin",
"uvcorrectaspect",
"channelpackfileformat",
"del_cptex_components",
"save_bakes_external",
"export_folder_per_object",
"export_mesh_individual_or_combined",
"export_format",
"jpeg_quality",
"save_obj_external",
"merge_export_obj",
"mesh_export_name",
"copy_and_apply",
"apply_bakes_to_original",
"hide_source_objects",
"hide_cage_object",
"preserve_materials",
"everything_16bit",
"export_file_format",
"apply_col_man_to_col",
"export_cycles_col_space",
"rundenoise",
"apply_mods_on_mesh_export",
"objects_list_index",
"bgbake",
"memLimit",
"batch_name",
"first_texture_show",
"bgbake_name",
"apply_transformation",
"create_glTF_node",
"glTF_selection",
"export_path",
"move_new_uvs_to_top",
"selected_bump",
"cyclesbake_cs",
#"fbx_preset_name",
#"gltf_preset_name",
"export_mesh_preset_name",
"cyclesbake_copy_and_apply_mat_format",
"clear_image",
"replace_previous_bakes",
"cage_smooth_hard",
"boosted_sample_count",
"no_force_32bit_normals",
"bw_to_rgb",
"keep_internal_after_export",
"ao_sample_count",
"isolate_objects",
"uv_advanced_packing_show",
"uvp_shape_method",
"uvp_scale",
"uvp_rotate",
"uvp_rotation_method",
"uvp_margin_method",
"uvp_lock_pinned",
"uvp_lock_method",
"uvp_merge_overlapping",
"uvp_pack_to",
"showtips",
"presets_show",
"bake_objects_show",
"pbr_settings_show",
"aov_settings_show",
"cyclesbake_settings_show",
"specials_show",
"textures_show",
"export_show",
"admin_settings_show",
"uv_show",
"other_show",
"channelpacking_show",
"bg_status_show",
"image_sequence_enabled",
"image_sequence_start_frame",
"image_sequence_end_frame",
"findreplace_find",
"findreplace_replace",
"findreplace_type",
"do_aa",
"aa_threshold",
"aa_contrast_limit",
"aa_corner_radius",
"join_objs_to_proxy",
"available_uv_maps",
"materials_show",
])
scene_props = ([
"cycles.bake_type",
"render.bake.use_pass_direct",
"render.bake.use_pass_indirect",
"render.bake.use_pass_diffuse",
"render.bake.use_pass_glossy",
"render.bake.use_pass_transmission",
"render.bake.use_pass_emit",
"render.bake.view_from",
"cycles.samples",
"render.bake.normal_space",
"render.bake.normal_r",
"render.bake.normal_g",
"render.bake.normal_b",
"render.bake.use_pass_color",
"render.bake.margin",
"render.bake.margin_type",
"render.image_settings.exr_codec",
"cycles.use_denoising",
"cycles.denoiser",
"cycles.denoising_input_passes",
"cycles.denoising_prefilter"
])
class SimpleBake_OT_preset_save(Operator):
"""Save current SimpleBake settings to preset"""
bl_idname = "simplebake.preset_save"
bl_label = "Save"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
return sbp.preset_name != ""
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if sbp.preset_name != clean_file_name(sbp.preset_name):
message_items = ["ERROR: Preset name can only contain characters that",
"are valid for the external file system"]
show_message_box(context, message_items, "ERROR", icon = "ERROR")
return {'CANCELLED'}
d = {}
for p in basic_props:
try:
val = getattr(sbp, p)
except UnicodeDecodeError as e:
print(f"[SimpleBake] UnicodeDecodeError on prop '{p}': {e}")
if isinstance(val, bpy.types.bpy_prop_array):
d[p] = list(val)
else:
d[p] = val
for p in scene_props:
d[p] = deep_getattr(context.scene, p)
#Grab the objects in the advanced list (if any)
d["objects_list"] = [o.name for o in sbp.objects_list]
d["mat_bake_list"] = [(e.obj_name, e.mat_name, e.enabled) for e in sbp.mat_bake_list]
#Grab the target objects if there is one
if sbp.targetobj != None:
d["pbr_target_obj"] = sbp.targetobj.name
else:
d["pbr_target_obj"] = None
if sbp.targetobj_cycles != None:
d["cycles_target_obj"] = sbp.targetobj_cycles.name
else:
d["cycles_target_obj"] = None
#Cage object if there is one
if context.scene.render.bake.cage_object != None:
d["cage_object"] = context.scene.render.bake.cage_object.name
else:
d["cage_object"] = None
#Channel packed images
cp_images_dict = {}
for cpt in sbp.cp_list:
thiscpt_dict = {}
thiscpt_dict["R"] = cpt.R
thiscpt_dict["G"] = cpt.G
thiscpt_dict["B"] = cpt.B
thiscpt_dict["A"] = cpt.A
thiscpt_dict["file_format"] = cpt.file_format
thiscpt_dict["exr_codec"] = cpt.exr_codec
thiscpt_dict["png_compression"] = cpt.png_compression
cp_images_dict[cpt.name] = thiscpt_dict
if len(cp_images_dict)>0:
d["channel_packed_images"] = cp_images_dict
#AOVs
aovs = []
for aov in sbp.aov_items:
aovs.append([aov.name, aov.cs, aov.enabled])
d["aovs"] = aovs
#UV map list
d["uvs"] = [(i.name, i.uv_name) for i in sbp.uv_items]
#Find where we want to save
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake"
savename = clean_file_name(sbp.preset_name)
if not os.path.isdir(str(p)):
os.makedirs(str(p))
print_message(context, f"Saving preset to {str(p)}")
jsonString = json.dumps(d)
jsonFile = open(str(p / savename), "w")
jsonFile.write(jsonString)
jsonFile.close()
#Refreh the list
bpy.ops.simplebake.preset_refresh()
self.report({"INFO"}, "Preset saved")
return {'FINISHED'}
class SimpleBake_OT_preset_load(Operator):
"""Load selected SimpleBake preset"""
bl_idname = "simplebake.preset_load"
bl_label = "Load"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
try:
sbp.presets_list[sbp.presets_list_index].name
return True
except:
return False
def execute(self, context):
sbp = context.scene.SimpleBake_Props
s = context.scene
utils.suppress_for_preset_load = True
#Load it
loadname = clean_file_name(sbp.presets_list[sbp.presets_list_index].name)
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake" / loadname
print_message(context, f"Loading preset from {str(p)}")
try:
fileObject = open(str(p), "r")
except:
bpy.ops.simplebake.preset_refresh()
self.report({"ERROR"}, f"Preset {loadname} no longer exists")
return {'CANCELLED'}
json_content = fileObject.read()
d = json.loads(json_content)
#Fix the legacy entries
d = fix_legacy(d)
for p in basic_props:
try:
cur = getattr(sbp, p) # what's there now?
val = d[p]
if hasattr(cur, "to_list"):
setattr(sbp, p, tuple(val)) # tuple/list both work; tuple is safe
else:
setattr(sbp, p, val)
setattr(sbp, p, d[p])
except Exception as e:
print(f"Unable to load preset value for {p}: {e}")
for p in scene_props:
try:
deep_setattr(context.scene, p, d[p])
except Exception as e:
print(f"Unable to load preset value for {p}")
print(e)
#Channel packing images
sbp.cp_list.clear()
if "channel_packed_images" in d:
channel_packed_images = d["channel_packed_images"]
if len(channel_packed_images) > 0:
sbp.cp_list.clear()
for imgname in channel_packed_images:
thiscpt_dict = channel_packed_images[imgname]
#Create the list item
li = sbp.cp_list.add()
li.name = imgname
#Set the list item properies
li.R = thiscpt_dict["R"]
li.G = thiscpt_dict["G"]
li.B = thiscpt_dict["B"]
li.A = thiscpt_dict["A"]
li.file_format = thiscpt_dict["file_format"]
li.exr_codec = thiscpt_dict["exr_codec"]
if "png_compression" in thiscpt_dict:
li.png_compression = thiscpt_dict["png_compression"]
else:
li.png_compression = 15
#Refresh the set values in the packed textures list (so it doesn't look like it hasn't been saved)
#Go to first item (just in case the preset loads fewer than we already had
if len(sbp.cp_list) > 0:
sbp.cp_list_index = 0
#And now the objects, if they are here
#Clear list if that was asked for
if sbp.preset_load_clear_obj:
sbp.objects_list.clear()
for obj_name in d["objects_list"]:
if obj_name in context.scene.objects:
#Find where name attribute of each object in the advanced selection list matches the name
l = [o.name for o in sbp.objects_list if o.name == obj_name]
if len(l) == 0:
#Not already in the list
i = sbp.objects_list.add()
i.name = obj_name #Advanced object list has a name and pointers arritbute
i.obj_point = context.scene.objects[obj_name]
if d["pbr_target_obj"] != None and d["pbr_target_obj"] in context.scene.objects:
sbp.targetobj = context.scene.objects[d["pbr_target_obj"]]
if d["cycles_target_obj"] != None and d["cycles_target_obj"] in context.scene.objects:
sbp.targetobj_cycles = context.scene.objects[d["cycles_target_obj"]]
#Cage object
if d["cage_object"] != None and d["cage_object"] in context.scene.objects:
context.scene.render.bake.cage_object = context.scene.objects[d["cage_object"]]
#Mat bake list — refresh first to populate all materials, then apply saved enabled states
refresh_mat_bake_list(context)
if "mat_bake_list" in d:
saved_states = {(obj_name, mat_name): enabled for obj_name, mat_name, enabled in d["mat_bake_list"]}
for entry in sbp.mat_bake_list:
key = (entry.obj_name, entry.mat_name)
if key in saved_states:
entry.enabled = saved_states[key]
#AOVs
bpy.ops.simplebake.refresh_aov_list()
if "aovs" in d:
aovs = d["aovs"]
for aov in aovs:
name = aov[0]
cs = aov[1]
enabled = aov[2]
if name in sbp.aov_items:
i = sbp.aov_items[name]
i.cs = cs
i.enabled = enabled
utils.suppress_for_preset_load = False
self.report({"INFO"}, f"Preset {loadname} loaded")
#UVlist (Must come after the object list is populated)
if "uvs" in d:
bpy.ops.simplebake.sync_uv_list()
for n, uvn in d["uvs"]:
if (i:=sbp.uv_items.find(n)) != -1:
sbp.uv_items[i].uv_name = uvn
return {'FINISHED'}
classes = ([
SimpleBake_OT_preset_load,
SimpleBake_OT_preset_save
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+305
View File
@@ -0,0 +1,305 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from bpy.props import StringProperty
from .presets import basic_props, scene_props
from .messages import print_message
from . import utils
from .ui.objects_list import refresh_mat_bake_list
import json
#TODO - Loading order problem
def deep_setattr(obj, attr, value):
"""Recursively set attributes on an object, including sub-objects."""
pre, _, post = attr.rpartition('.')
return setattr(deep_getattr(obj, pre) if pre else obj, post, value)
def deep_getattr(obj, attr):
"""Recursively get attributes from an object, including sub-objects."""
for part in attr.split('.'):
obj = getattr(obj, part)
return obj
def sb_prop_group_to_dict(context):
"""
Puts all SimpleBake current settings into a dict
"""
sbp = context.scene.SimpleBake_Props
d = {}
for p in basic_props:
try:
val = getattr(sbp, p)
except UnicodeDecodeError as e:
print(f"[SimpleBake] UnicodeDecodeError on prop '{p}': {e}")
if isinstance(val, bpy.types.bpy_prop_array):
d[p] = list(val)
else:
d[p] = val
#Now for the objects
d["objects_list"] = [o.name for o in sbp.objects_list]
d["mat_bake_list"] = [(e.obj_name, e.mat_name, e.enabled) for e in sbp.mat_bake_list]
d["pbr_target_obj"] = sbp.targetobj.name if sbp.targetobj != None else None
d["cycles_target_obj"] = sbp.targetobj_cycles.name if sbp.targetobj_cycles != None else None
#Cage object if there is one
if context.scene.render.bake.cage_object != None:
d["cage_object"] = context.scene.render.bake.cage_object.name
else:
d["cage_object"] = None
#Now the channel pack images
cp_images_dict = {}
for cpt in sbp.cp_list:
thiscpt_dict = {}
thiscpt_dict["R"] = cpt.R
thiscpt_dict["G"] = cpt.G
thiscpt_dict["B"] = cpt.B
thiscpt_dict["A"] = cpt.A
thiscpt_dict["file_format"] = cpt.file_format
thiscpt_dict["exr_codec"] = cpt.exr_codec
thiscpt_dict["png_compression"] = cpt.png_compression
cp_images_dict[cpt.name] = thiscpt_dict
if len(cp_images_dict)>0:
d["channel_packed_images"] = cp_images_dict
#UV map list
d["uvs"] = [(i.name, i.uv_name) for i in sbp.uv_items]
return d
def existing_preset_to_dict(context, friendly_name):
"""
Pulls out an existing saved local preset into a dict
"""
sbp = context.scene.SimpleBake_Props
name = f"SB_local_preset_{friendly_name}"
s = sbp[name]
d = json.loads(s)
return d
def save_preset(context, friendly_name):
"""
Saves the current SimpleBake settings to a local preset of name friendly_name
"""
sbp = context.scene.SimpleBake_Props
sb = sb_prop_group_to_dict(context)
cs = {}
for p in scene_props:
cs[p] = deep_getattr(context.scene, p)
sbp[f"SB_local_preset_{friendly_name}"] = json.dumps(sb | cs)
def _apply_preset_dict(context, d):
"""Apply a settings dict to the current scene. d is modified in-place."""
sbp = context.scene.SimpleBake_Props
utils.suppress_for_preset_load = True
sbp.objects_list.clear()
#First the object list
for obj_name in d["objects_list"]:
if obj_name in context.scene.objects:
#Find where name attribute of each object in the advanced selection list matches the name
l = [o.name for o in sbp.objects_list if o.name == obj_name]
if len(l) == 0:
#Not already in the list
i = sbp.objects_list.add()
i.name = obj_name #Advanced object list has a name and pointers arritbute
i.obj_point = context.scene.objects[obj_name]
if d["pbr_target_obj"] != None and d["pbr_target_obj"] in context.scene.objects:
sbp.targetobj = context.scene.objects[d["pbr_target_obj"]]
if d["cycles_target_obj"] != None and d["cycles_target_obj"] in context.scene.objects:
sbp.targetobj_cycles = context.scene.objects[d["cycles_target_obj"]]
if d["cage_object"] != None and d["cage_object"] in context.scene.objects:
context.scene.render.bake.cage_object = context.scene.objects[d["cage_object"]]
#Mat bake list — refresh first to populate all materials, then apply saved enabled states
refresh_mat_bake_list(context)
if "mat_bake_list" in d:
saved_states = {(obj_name, mat_name): enabled for obj_name, mat_name, enabled in d["mat_bake_list"]}
for entry in sbp.mat_bake_list:
key = (entry.obj_name, entry.mat_name)
if key in saved_states:
entry.enabled = saved_states[key]
del d["objects_list"]
d.pop("mat_bake_list", None)
#Now channel pack
if "channel_packed_images" in d:
channel_packed_images = d["channel_packed_images"]
if len(channel_packed_images) > 0:
sbp.cp_list.clear()
for imgname in channel_packed_images:
thiscpt_dict = channel_packed_images[imgname]
#Create the list item
li = sbp.cp_list.add()
li.name = imgname
#Set the list item properies
li.R = thiscpt_dict["R"]
li.G = thiscpt_dict["G"]
li.B = thiscpt_dict["B"]
li.A = thiscpt_dict["A"]
li.file_format = thiscpt_dict["file_format"]
li.exr_codec = thiscpt_dict["exr_codec"]
if "png_compression" in thiscpt_dict:
li.png_compression = thiscpt_dict["png_compression"]
else:
li.png_compression = 15
#UV maps list
bpy.ops.simplebake.sync_uv_list()
if "uvs" in d:
for n, uvn in d["uvs"]:
if (i:=sbp.uv_items.find(n)) != -1:
sbp.uv_items[i].uv_name = uvn
try:
for k in d:
if k not in scene_props:
try:
setattr(sbp, k, d[k])
except:
pass
else:
try:
deep_setattr(context.scene, k, d[k])
except:
pass
finally:
utils.suppress_for_preset_load = False
def load_preset(context, friendly_name):
_apply_preset_dict(context, existing_preset_to_dict(context, friendly_name))
class SimpleBake_OT_local_preset_save(Operator):
"""Save current SimpleBake settings to local preset"""
bl_idname = "simplebake.local_preset_save"
bl_label = "Save local preset"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
return sbp.local_preset_name != ""
def execute(self, context):
sbp = context.scene.SimpleBake_Props
save_preset(context, sbp.local_preset_name)
bpy.ops.simplebake.local_preset_refresh()
self.report({"INFO"}, f"Local Preset {sbp.local_preset_name} saved")
return {'FINISHED'}
class SimpleBake_OT_local_preset_save_override(Operator):
"""Save current SimpleBake settings to local preset overriding the UI for the name"""
bl_idname = "simplebake.local_preset_save_override"
bl_label = "Save local preset"
name_override: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
save_preset(context, self.name_override)
return {'FINISHED'}
class SimpleBake_OT_local_preset_load(Operator):
"""Load selected SimpleBake preset"""
bl_idname = "simplebake.local_preset_load"
bl_label = "Load local preset"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
try:
sbp.local_presets_list[sbp.local_presets_list_index].name
return True
except:
return False
def execute(self, context):
sbp = context.scene.SimpleBake_Props
load_preset(context, friendly_name=sbp.local_preset_name)
self.report({"INFO"}, f"Local Preset {sbp.local_preset_name} loaded")
return {'FINISHED'}
class SimpleBake_OT_local_preset_load_override(Operator):
"""Load selected SimpleBake preset overriding the UI for the name"""
bl_idname = "simplebake.local_preset_load_override"
bl_label = "Load local preset"
name_override: StringProperty()
def execute(self, context):
load_preset(context, friendly_name=self.name_override)
return {'FINISHED'}
class SimpleBake_OT_Paste_Settings_Clipboard(Operator):
"""Paste SimpleBake settings from clipboard JSON (copied via Copy Settings to Clipboard)"""
bl_idname = "simplebake.paste_settings_clipboard"
bl_label = "Paste Settings from Clipboard"
def execute(self, context):
raw = context.window_manager.clipboard
if not raw or not raw.strip():
self.report({"ERROR"}, "Clipboard is empty")
return {'CANCELLED'}
try:
d = json.loads(raw)
except json.JSONDecodeError as e:
self.report({"ERROR"}, f"Clipboard does not contain valid JSON: {e}")
return {'CANCELLED'}
required = {"objects_list", "pbr_target_obj", "cycles_target_obj", "cage_object"}
if not required.issubset(d.keys()):
self.report({"ERROR"}, "Clipboard JSON does not look like SimpleBake settings")
return {'CANCELLED'}
_apply_preset_dict(context, d)
self.report({"INFO"}, "SimpleBake settings pasted from clipboard")
return {'FINISHED'}
classes = ([
SimpleBake_OT_local_preset_load,
SimpleBake_OT_local_preset_save,
SimpleBake_OT_local_preset_save_override,
SimpleBake_OT_local_preset_load_override,
SimpleBake_OT_Paste_Settings_Clipboard,
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,134 @@
import bpy
from bpy.types import Operator
from bpy.props import StringProperty
from bpy.utils import register_class, unregister_class
from .messages import print_message
from .utils import SBConstants
class SimpleBake_OT_PreviewBakeType(Operator):
"""Preview what will be baked for this bake type by configuring materials"""
bl_idname = "simplebake.preview_bake_type"
bl_label = "Preview"
bl_description = "Preview this bake type in the viewport by configuring object materials"
bake_type: StringProperty()
def execute(self, context):
sbp = context.scene.SimpleBake_Props
bake_type = self.bake_type
# Start by removing any existing preview
bpy.ops.simplebake.restore_preview()
bake_objects = [item.obj_point for item in sbp.objects_list if item.obj_point is not None]
if bake_type in SBConstants.ALL_SPECIALS:
if bake_type == SBConstants.COLOURID:
from .bake_operators.specials_bake_operators import SimpleBake_OT_Setup_Col_ID
SimpleBake_OT_Setup_Col_ID.clear()
for obj in bake_objects:
bpy.ops.simplebake.specials_mats_swapping(mode="record", obj_name=obj.name)
if bake_type == SBConstants.COLOURID:
bpy.ops.simplebake.setup_col_id(obj_name=obj.name)
elif bake_type == SBConstants.VERTEXCOL:
if not obj.data.color_attributes:
continue
bpy.ops.simplebake.import_and_assign_specials(bake_mode=bake_type, obj_name=obj.name)
bpy.ops.simplebake.setup_vertex_col(obj_name=obj.name)
else: # AO, Thickness, Curvature, Lightmap
bpy.ops.simplebake.import_and_assign_specials(bake_mode=bake_type, obj_name=obj.name)
else:
# Initialise / clear any stale material tags from a previous preview or bake
bpy.ops.simplebake.material_backup(mode="initialise")
# Remove node groups (also performs master backup of all object materials).
# Only available on Blender 4.1+, which is the minimum for this addon.
bpy.ops.simplebake.pbr_pre_bake()
# Remove reroute nodes from all object materials
bpy.ops.simplebake.remove_reroutes()
# Per-object: working backup then configure materials for the target bake type
for obj in bake_objects:
bpy.ops.simplebake.material_backup(
target_object_name=obj.name,
mode="working_backup"
)
bpy.ops.simplebake.prepare_object_mats_pbr(
target_name=obj.name,
this_bake=bake_type,
bake_operation_id="",
no_bake_image_node=True
)
# Switch Solid viewports to Material Preview so the preview is visible
sbp.preview_changed_shading = False
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if space.shading.type == 'SOLID':
space.shading.type = 'MATERIAL'
sbp.preview_changed_shading = True
print_message(context, f"Preview active: {bake_type}")
sbp.preview_active = True
sbp.preview_bake_type = bake_type
return {'FINISHED'}
class SimpleBake_OT_RestorePreview(Operator):
"""Restore materials after a bake type preview"""
bl_idname = "simplebake.restore_preview"
bl_label = "Restore Materials"
bl_description = "Restore object materials to their original state after previewing"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if sbp.preview_bake_type in SBConstants.ALL_SPECIALS:
bake_objects = [item.obj_point for item in sbp.objects_list if item.obj_point is not None]
for obj in bake_objects:
bpy.ops.simplebake.specials_mats_swapping(mode="restore", obj_name=obj.name)
else:
# Restore working-mode duplicates first (scene-wide, no object name needed)
bpy.ops.simplebake.material_backup(mode="working_restore")
# Then restore master duplicates (scene-wide)
bpy.ops.simplebake.material_backup(mode="master_restore")
if sbp.preview_changed_shading:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if space.shading.type == 'MATERIAL':
space.shading.type = 'SOLID'
sbp.preview_changed_shading = False
print_message(context, "Preview materials restored")
sbp.preview_active = False
sbp.preview_bake_type = ""
return {'FINISHED'}
classes = [
SimpleBake_OT_PreviewBakeType,
SimpleBake_OT_RestorePreview,
]
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,153 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
import os
from pathlib import Path
from zipfile import ZipFile
import webbrowser
from . import sketchfabapi
from .utils import get_base_folder_patho, clean_file_name, is_blend_saved
from .messages import print_message
def get_file_name():
fullpath = bpy.data.filepath
pathelements = os.path.split(fullpath)
return pathelements[1]
class SimpleBake_OT_Sketchfab_Upload(Operator):
"""Upload selected SimpleBake generated objects to Sketchfab"""
bl_idname = "simplebake.sketchfab_upload"
bl_description = "Upload selected SimpleBake generated objects to Sketchfab. Your blend file must be saved. You can only upload objects created with SimpleBake's 'Copy objects and apply bakes' option. Select those object(s) in the viewport and click upload."
bl_label = "Upload"
@classmethod
def poll(cls,context):
if not is_blend_saved():
return False
if context.mode != "OBJECT":
return False
sbp = context.scene.SimpleBake_Props
objs_list = context.selected_objects
r =[o for o in objs_list if "SB_bake_operation_id" in o]
if len(objs_list) == 0: return False
elif len(r) == len(objs_list): return True
else: return False
def execute(self, context):
print_message(context, "Sketchfab Upload Beginning")
#Get the currently selected objects
objs_list = context.selected_objects
#Get all the textures being used by our objects. Should only have one material each
images_imgs = []
for obj in objs_list:
nodes = obj.material_slots[0].material.node_tree.nodes
for node in nodes:
if node.bl_idname == "ShaderNodeTexImage":
images_imgs.append(node.image)
#Create a temp folder for SFUpload in the folder where blend saved
f = get_base_folder_patho(context) / "SFUpload"
if not os.path.isdir(str(f)):
os.mkdir(str(f))
#Save each image into that folder
writtenfilenames_strlist = []
for img in images_imgs:
#If it is internal only
if img.filepath == "":
#Just set it's file path and save. This one is easy
img.file_format = "PNG"
img.filepath = str(f / clean_file_name(img.name)) + ".png"
img.save()
writtenfilenames_strlist.append(Path(img.filepath).parts[-1])
img.filepath = ""
#If it's already been saved externally, copy it
else:
op = img.filepath
off = img.file_format
img.pack() #Or else changing its filepath will screw it up
img.file_format = "PNG"
img.filepath = str(f / clean_file_name(img.name)) + ".png"
img.save()
writtenfilenames_strlist.append(Path(img.filepath).parts[-1])
img.unpack(method="REMOVE")
img.filepath = op
img.file_format = off
#In either case, add the image name to our list
#Export the fbx (we might have multiple objects)
bpy.ops.object.select_all(action="DESELECT")
for obj in objs_list:
obj.select_set(state=True)
filename = get_file_name().replace(".blend", "")
bpy.ops.export_scene.fbx(filepath=str(f / f"{filename}.fbx"), check_existing=False, use_selection=True, use_mesh_modifiers=True, use_mesh_modifiers_render=True, path_mode="STRIP")
#Zip it up
zip_path = str(f / f"{filename}.zip")
zip = ZipFile(str(zip_path), mode="w")
#First the images
for fn in writtenfilenames_strlist:
zip.write(str(f / fn), arcname=fn)
#And now the fbx
zip.write(str(f / f"{filename}.fbx"), arcname=f"{filename}.fbx")
zip.close()
#Get Sketchfab API key
preferences = context.preferences
addon_prefs = preferences.addons[__package__].preferences
apikey = addon_prefs.apikey
#Call Sketchfab Upload
upload_url = sketchfabapi.upload(zip_path, get_file_name(), apikey)
if not upload_url:
print_message(context, "Upload to Sketchfab failed. See console messages for details")
return False
else:
#Open URL that is returned
webbrowser.open(upload_url, new=0, autoraise=True)
print_message(context, "Upload complete. Your web broswer should have opened.")
#Delete Zip file
#os.remove(zip_path)
return {'FINISHED'}
classes = ([
SimpleBake_OT_Sketchfab_Upload
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+111
View File
@@ -0,0 +1,111 @@
import json
import os
import bpy
from time import sleep
# import the requests library
# http://docs.python-requests.org/en/latest
# pip install requests
import requests
#from .utils import print_message
from .messages import print_message
##
# Uploading a model to Sketchfab is a two step process
#
# 1. Upload a model. If the upload is successful, the API will return
# the model's uid in the `Location` header, and the model will be placed in the processing queue
#
# 2. Poll for the processing status
# You can use your model id (see 1.) to poll the model processing status
# The processing status can be one of the following:
# - PENDING: the model is in the processing queue
# - PROCESSING: the model is being processed
# - SUCCESSED: the model has being sucessfully processed and can be view on sketchfab.com
# - FAILED: the processing has failed. An error message detailing the reason for the failure
# will be returned with the response
#
# HINTS
# - limit the rate at which you poll for the status (once every few seconds is more than enough)
##
SKETCHFAB_DOMAIN = 'sketchfab.com'
SKETCHFAB_API_URL = 'https://api.{}/v3'.format(SKETCHFAB_DOMAIN)
def _get_request_payload(apikey, data={}, files={}, json_payload=False):
"""Helper method that returns the authentication token and proper content
type depending on whether or not we use JSON payload."""
headers = {'Authorization': 'Token {}'.format(apikey)}
if json_payload:
headers.update({'Content-Type': 'application/json'})
data = json.dumps(data)
return {'data': data, 'files': files, 'headers': headers}
def upload(model_file, uploadname, apikey):
"""POST a model to sketchfab.
This endpoint only accepts formData as we upload a file.
"""
#model_endpoint = os.path.join(SKETCHFAB_API_URL, 'models')
model_endpoint = SKETCHFAB_API_URL + '/models'
print_message(bpy.context, f"Sketchfab URL is going to be: {model_endpoint}")
# Optional parameters
name = uploadname
description = 'Uploaded with SimpleBake for Blender 2.8'
#password = 'my-password' # requires a pro account
#private = 1 # requires a pro account
#tags = ['bob', 'character', 'video-games'] # Array of tags
#categories = ['people'] # Array of categories slugs
#license = 'CC Attribution' # License label
isPublished = False, # Model will be on draft instead of published
#isInspectable = True, # Allow 2D view in model inspector
data = {
'name': name,
'description': description,
#'tags': tags,
#'categories': categories,
#'license': license,
#'private': private,
#'password': password,
'isPublished': isPublished,
'source': "simplebake-for-blender"
#'isInspectable': isInspectable
}
f = open(model_file, 'rb')
files = {'modelFile': f}
try:
r = requests.post(
model_endpoint, **_get_request_payload(apikey,
data, files=files))
except requests.exceptions.RequestException as e:
print_message(bpy.context, 'An error occured: {}'.format(e))
return False
finally:
f.close()
print_message(bpy.context, f"Status code from Sketchfab was {r.status_code}");
if r.status_code != requests.codes.created:
print_message(bpy.context, 'Upload failed with error: {}'.format(r.json()))
return False
# Should be https://api.sketchfab.com/v3/models/XXXX
model_url = r.headers['Location']
#For some reason this is wrong. Correct it here.
return model_url.replace("https://api.sketchfab.com/v3/models/", "https://sketchfab.com/3d-models/")
@@ -0,0 +1,820 @@
import bpy
from bpy.types import Operator
from bpy.utils import register_class, unregister_class
from .utils import show_message_box, clean_file_name, SBConstants, specials_selection_to_list, pbr_selections_to_list, blender_refresh, get_cached_udim_tiles
from .messages import print_message
from .ui.objects_list import refresh_bake_objects_list
from pathlib import Path
from .utils import can_write_to_location
from .bake_operators.pbr_bake_support_operators import has_node_groups, convertable_shaders
from . import __package__ as base_package
def check_mat_valid_for_displacement(mat_name, obj_name, messages):
mat = bpy.data.materials.get(mat_name)
if mat == None:
return True
node_tree = mat.node_tree
nodes = node_tree.nodes
valid = False
#groups = [n.name for n in this_node_set if n.bl_idname == "ShaderNodeGroup"]
#Find our MO node
mo = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output]
assert len(mo)==1, "Couldn't find the Material Output node"
mo = mo[0]
#Empty is OK
if len(mo.inputs["Displacement"].links)==0:
valid = True
if len(mo.inputs["Displacement"].links)>0 and not valid:
from_node = mo.inputs["Displacement"].links[0].from_node
#Check if this is straightforward
if from_node.bl_idname in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]:
valid = True
#Maybe it's in a node group
elif from_node.bl_idname == "ShaderNodeGroup":
#Recursive search for the nodes we want
trees = [from_node.node_tree]
for tree in trees:
for node in tree.nodes:
if node.bl_idname == "ShaderNodeGroup":
trees.append(node.node_tree)
continue
if node.bl_idname in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]:
if len(node.outputs[0].links)>0:
if node.outputs[0].links[0].to_node.bl_idname == "NodeGroupOutput":
valid = True
if valid:
return True
else:
messages.append(f"ERROR: Baking Displacement map, but material \"{mat_name}\" on object \"{obj_name}\" has an")
messages.append("invalid node plugged into the Material Output displacement socket.")
messages.append("SimpleBake can only bake a single Displacement node or Vector Displacement node")
messages.append("connected to the Displacement socket of the active Material Output node.")
messages.append("If such a node is inside a node group, it must be connected directly to the Group Output node.")
return False
def find_shader_node_editor():
found_space = False
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == 'NODE_EDITOR':
for space in area.spaces:
if space.type == 'NODE_EDITOR' and space.tree_type == 'ShaderNodeTree':
found_space = True
break
if found_space:
break
if found_space:
break
return found_space
def check_for_connected_viewer_node(context, mat):
sbp = context.scene.SimpleBake_Props
mat.use_nodes = True
node_tree = mat.node_tree
nodes = node_tree.nodes
#onode = find_onode(node_tree)
#Get all nodes with label "Viewer"
viewer_nodes = [n for n in nodes if n.label == "Viewer"]
#Check if any of those viewer nodes are connected to the Material Output
for n in viewer_nodes:
#if n.name == onode.inputs[0].links[0].from_node.name:
if len(n.outputs[0].links)>1:
if n.outputs[0].links[0].to_node.type == "OUTPUT_MATERIAL":
return True
return False
def check_mats_valid_for_pbr(context, mat):
prefs = context.preferences.addons[base_package].preferences
skip = prefs.skip_pbr_nodes_check
allowed_list = convertable_shaders + [
"ShaderNodeBsdfPrincipled", "ShaderNodeMixShader",
"ShaderNodeEmission", "NodeReroute", "ShaderNodeGroup",
"NodeGroupInput", "NodeGroupOutput"]
if skip:
print_message(context, "Skipping check for invalid PBR nodes")
return []
sbp = context.scene.SimpleBake_Props
node_sets = [mat.node_tree.nodes]
invalid_node_names = []
i = 0
while i < len(node_sets):
this_node_set = node_sets[i]
#Find any node groups, and add their node_set to the list
groups = [n.name for n in this_node_set if n.bl_idname == "ShaderNodeGroup"]
for n_name in groups:
if(n:=this_node_set.get(n_name)):
if hasattr(n, "node_tree"):
node_tree = n.node_tree
if hasattr(node_tree, "nodes"):
node_sets.append(n.node_tree.nodes)
#Now check for offending nodes in this node set
offending = [n for n in this_node_set if
len(n.outputs)>0 and
n.outputs[0].type == "SHADER" and
len(n.outputs[0].links) > 0 and
n.bl_idname not in allowed_list]
for o in offending:
invalid_node_names.append(o.name)
#Second case, the shader to RGB node
offending = ([n for n in this_node_set if
len(n.outputs)>0 and
len(n.outputs[0].links) > 0 and
n.bl_idname == "ShaderNodeShaderToRGB"
])
for o in offending:
invalid_node_names.append(o.name)
i+=1
return invalid_node_names
def ensure_valid_material_config(context, obj):
sbp = context.scene.SimpleBake_Props
if "SimpleBake_Placeholder" in bpy.data.materials:
mat = bpy.data.materials["SimpleBake_Placeholder"]
else:
mat = bpy.data.materials.new("SimpleBake_Placeholder")
bpy.data.materials["SimpleBake_Placeholder"].use_nodes = True
# Assign it to object
if len(obj.material_slots) > 0:
#Assign it to every empty slot
for slot in obj.material_slots:
if slot.material == None:
slot.material = mat
else:
# no slots
obj.data.materials.append(mat)
#All materials must use nodes
for slot in obj.material_slots:
mat = slot.material
if mat.use_nodes == False:
mat.use_nodes = True
return True
def ensure_target_object_bake_visibility(obj):
if obj == None:
return
obj.hide_viewport = False
obj.hide_set(False)
obj.hide_select = False
obj.hide_render = False
class SimpleBake_OT_Starting_Checks(Operator):
"""Perform pre-bake starting checks"""
bl_idname = "simplebake.starting_checks"
bl_description = "Perform bake starting checks"
bl_label = "Go"
def universal_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Universal pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
#Universal Checks
if(context.mode != "OBJECT"):
messages.append("ERROR: Not in object mode")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#If exporting, check we can write to the folder given
if sbp.save_bakes_external or sbp.save_obj_external:
path = Path(bpy.path.abspath(sbp.export_path))
#path = path.parent
if not can_write_to_location(path):
messages.append(f"ERROR: Blender reports that it cannot write to the Export Path you have")
messages.append(f'specified under Export settings')
messages.append(f'CANNOT WRITE TO: "{bpy.path.abspath(str(path))}"')
messages.append(f"Try specifying a different export path")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Check no cp textures rely on bakes that are no longer enabled
if sbp.global_mode == SBConstants.PBR:
pbr_bakes = pbr_selections_to_list(context)
special_bakes = specials_selection_to_list(context)
bakes = pbr_bakes + special_bakes
bakes.append("none")
def cp_channel_invalid(value):
"""Return True if this channel value can't be satisfied by the current bake settings."""
if value == "none":
return False
if "||" in value:
# Per-material value — only valid when tex_per_mat is on
if not sbp.tex_per_mat:
return True
bake_type = value.split("||", 1)[0]
else:
bake_type = value
return bake_type not in bakes
def cp_display(value):
"""Human-readable form of a channel value, safe for the | -delimited message box."""
return value.replace("||", " - ")
for cpt in sbp.cp_list:
if cp_channel_invalid(cpt.R):
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.R)}, but you are no longer baking it")
if cp_channel_invalid(cpt.G):
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.G)}, but you are no longer baking it")
if cp_channel_invalid(cpt.B):
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.B)}, but you are no longer baking it")
if cp_channel_invalid(cpt.A):
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.A)}, but you are no longer baking it")
messages = list(set(messages)) #Remove duplicates
if len(messages) >0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Is anything seleccted at all for bake?
if len(bake_objects) == 0:
messages.append("ERROR: Nothing in the bake objects list!")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Check everything selected (or target) is mesh
l = [bpy.data.objects.get(name) for name in bake_objects]
l = [o for o in l if o!=None and o.type != "MESH"]
if len(l)>0:
([messages.append(f"ERROR: Object '{obj.name}' is not mesh") for obj in l])
if sbp.selected_s2a and targetobj != None and targetobj.type != "MESH":
messages.append(f"ERROR: Object '{targetobj.name}' (your target object) is not mesh")
if sbp.cycles_s2a and targetobj_cycles != None and targetobj_cycles.type != "MESH":
messages.append(f"ERROR: Object '{targetobj.name}' (your target object) is not mesh")
if len(messages) > 0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#if sbp.save_bakes_external or sbp.save_obj_external:
#if sbp.save_external_folder != clean_file_name(sbp.save_external_folder):
#message_items = ["ERROR: Folder name can only contain characters that",
#"are valid for the external file system"]
#show_message_box(context, message_items, "ERROR", icon = "ERROR")
#return {'CANCELLED'}
#Ensure valid material config
objs = [bpy.data.objects.get(name) for name in bake_objects]
objs = [o for o in objs if o!=None]
for obj in objs:
ensure_valid_material_config(context, obj)
if targetobj!=None: ensure_valid_material_config(context, targetobj)
if targetobj_cycles!=None: ensure_valid_material_config(context, targetobj_cycles)
#Check object visibility
if len([o for o in context.view_layer.objects if o.type=="MESH"])==0:
messages.append("There must be at least one mesh object visible in the current view layer")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
if sbp.selected_s2a:
ensure_target_object_bake_visibility(sbp.targetobj)
if sbp.cycles_s2a:
ensure_target_object_bake_visibility(sbp.targetobj_cycles)
obj_test_list = bake_objects.copy()
if sbp.selected_s2a and sbp.targetobj != None:
obj_test_list.append(sbp.targetobj.name)
if sbp.cycles_s2a and sbp.targetobj_cycles != None:
obj_test_list.append(sbp.targetobj_cycles.name)
for obj_name in obj_test_list:
if (o:=bpy.data.objects.get(obj_name)):
if o.hide_viewport == True:
messages.append(f"Object '{o.name}' is hidden in viewport (monitor icon in outliner)")
if o.hide_render == True:
messages.append(f"Object '{o.name}' is hidden for render (camera icon in outliner)")
if o.hide_get() == True:
messages.append(f"Object '{o.name}' is hidden in viewport eye (eye icon in outliner)")
if o.hide_select == True:
messages.append(f"Object '{o.name}' is hidden for selection (arrow icon in outliner)")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#None of the objects can have zero faces
for obj_name in bake_objects:
o = bpy.data.objects.get(obj_name)
if o!= None and len(o.data.polygons) < 1: messages.append(f"ERROR: Object '{o.name}' has no faces")
if sbp.selected_s2a and targetobj != None:
if len(targetobj.data.polygons) < 1: messages.append(f"ERROR: Object '{targetobj.name}' has no faces")
if sbp.cycles_s2a and targetobj_cycles != None:
if len(targetobj_cycles.data.polygons) < 1: messages.append(f"ERROR: Object '{targetobj_cycles.name}' has no faces")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Check for viewer nodes still connected
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
mat = slot.material
if mat != None: #It'll get a placeholder material later on if it's none
if check_for_connected_viewer_node(context, mat):
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' has a Viewer node connected to the Material Output")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#glTF
if sbp.create_glTF_node:
if sbp.glTF_selection == SBConstants.AO and not sbp.selected_ao:
messages.append(f"ERROR: You have selected AO for glTF settings (in the 'Other Settings' section), but you aren't baking AO")
if sbp.glTF_selection == SBConstants.LIGHTMAP and not sbp.selected_lightmap:
messages.append(f"ERROR: You have selected Lightmap for glTF settings (in the 'Other Settings' section), but you aren't baking Lightmap")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
prefs = context.preferences.addons[__package__].preferences
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
if o.name != clean_file_name(o.name) and sbp.save_bakes_external and "%OBJ%" in prefs.img_name_format:
messages.append(f"ERROR: You are trying to save external images, but object with name \"{o.name}\" contains invalid characters for saving externally.")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Merged bake checks
if sbp.merged_bake and sbp.merged_bake_name == "":
messages.append(f"ERROR: You are baking multiple objects to one texture set, but the texture name is blank")
if (sbp.merged_bake_name != clean_file_name(sbp.merged_bake_name)) and sbp.save_bakes_external:
messages.append(f"ERROR: The texture name you inputted for baking multiple objects to one texture set (\"{sbp.merged_bake_name}\") contains invalid characters for saving externally.")
if sbp.merged_bake:
if sbp.selected_s2a or sbp.cycles_s2a:
if sbp.s2a_opmode!="automatch":
messages.append("You can't use the Bake Multiple Objects to One Texture Set option when baking to target")
#Export bakes checkes
if (sbp.save_bakes_external or sbp.save_obj_external) and sbp.export_path == "":
messages.append(f"You are saving your bakes and/or mesh externally, but your "
"Export Path (under the Export settings section) is blank")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
# Background bake + UDIMs requires external save to be enabled
if sbp.bgbake == "bg" and not sbp.save_bakes_external:
if sbp.selected_s2a:
udim_check_objects = [targetobj.name] if targetobj else []
elif sbp.cycles_s2a:
udim_check_objects = [targetobj_cycles.name] if targetobj_cycles else []
else:
udim_check_objects = list(bake_objects)
for obj_name in udim_check_objects:
result = get_cached_udim_tiles(context, obj_name, threshold=0.001)
if result["total_tiles"] > 1:
messages.append("ERROR: Bake objects have UDIMs. Background baking with UDIMs")
messages.append('requires "Export Bakes" to be enabled in the Export settings.')
show_message_box(context, messages, "Errors occurred", icon='ERROR')
return {'CANCELLED'}
return {'FINISHED'}
def pbr_no_s2a_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "PBR no S2A pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
#Check if we have a shader node editor to use_nodes
ngs = False
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
if slot.material != None:
ngs = has_node_groups(context, slot.material.name)
break
if ngs:
break
if ngs and not find_shader_node_editor():
messages.append(f"Some or all of your materials have node groups. To process node groups, SimpleBake needs there")
messages.append(f"to be a Shader Node Editor open somewhere in one of your Blender workspaces. You don't seem to have")
messages.append(f"one open anywhere! Open a Shader Node Editor (as if you were editing a material) somewhere in one of")
messages.append(f"your Blender workspaces, then try again")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#If baking displacement, do all materials meet the rules
if sbp.selected_displacement:
mats = {}
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
if slot.material != None:
mats[obj_name] = slot.material.name
for m in mats:
if not check_mat_valid_for_displacement(mats[m], m, messages):
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
for obj_name in bake_objects:
if not (o:=bpy.data.objects.get(obj_name)):
break
#Are UVs OK?
generating_uvs = sbp.new_uv_option or (sbp.tex_per_mat and sbp.expand_mat_uvs)
if not generating_uvs and len(o.data.uv_layers) == 0:
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
if sbp.new_uv_option and sbp.new_uv_method=='CombineExisting' and len(o.data.uv_layers) == 0:
messages.append(f"ERROR: You are combining existing UVs, but object {o.name} has no UVs")
#Do all materials have valid PBR config?
for slot in o.material_slots:
mat = slot.material
result = check_mats_valid_for_pbr(context, mat)
warn = False
if len(result) > 0:
for node_name in result:
messages.append(f"ERROR: Node '{node_name}' in material '{mat.name}' on object '{o.name}' is not valid for PBR bake. Principled BSDFs and/or Emission only!")
warn = True
if warn:
messages.append("NOTE: There is an option to disable this check in the SimpleBake addon preferences, but it is not recommended. Baking PBR with nodes that are not PBR compatible may give unexpected results, Black bakes or even CRASHES!")
#Checked for invalid nodes, now let's check for just absent any useful nodes:
else:
#Do all materials have at least one material output node?
for slot in o.material_slots:
mat = slot.material
mos = [node for node in mat.node_tree.nodes if node.bl_idname == "ShaderNodeOutputMaterial"\
and len(node.inputs[0].links)>0]
if len(mos) == 0:
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' is broken. Must have at least one Material Output node that's plugged in!")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
return {'FINISHED'}
def pbr_s2a_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "PBR S2A pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
ngs = False
for obj_name in bake_objects:
if(o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
if slot.material != None:
ngs = has_node_groups(context, slot.material.name)
break
if ngs:
break
if ngs and not find_shader_node_editor():
messages.append(f"Some or all of your materials have node groups. To process node groups, SimpleBake needs there")
messages.append(f"to be a Shader Node Editor open somewhere in one of your Blender workspaces. You don't seem to have")
messages.append(f"one open anywhere! Open a Shader Node Editor (as if you were editing a material) somewhere in one of")
messages.append(f"your Blender workspaces, then try again")
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#Do all materials have valid PBR config?
warn = False
for obj_name in bake_objects:
if(o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
mat = slot.material
result = check_mats_valid_for_pbr(context, mat)
if len(result) > 0:
for node_name in result:
messages.append(f"ERROR: Node '{node_name}' in material '{mat.name}' on object '{o.name}' is not valid for PBR bake. Principled BSDFs and/or Emission only!")
warn = True
if warn:
messages.append("NOTE: There is an option to disable this check in the SimpleBake addon preferences, but it is not recommended. Baking PBR with nodes that are not PBR compatible may give unexpected results, Black bakes or even CRASHES!")
#Checked for invalid nodes, now let's check for just absent any useful nodes:
else:
#Do all materials have at least one material output node?
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
mat = slot.material
mos = [node for node in mat.node_tree.nodes if node.bl_idname == "ShaderNodeOutputMaterial"\
and len(node.inputs[0].links)>0]
if len(mos) == 0:
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' is broken. Must have at least one Material Output node that's plugged in!")
#Checks for baking displacement
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
for slot in o.material_slots:
if slot.material!=None:
mat = slot.material
if not check_mat_valid_for_displacement(mat.name, o.name, messages):
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
if len(messages)>0:
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
return {'CANCELLED'}
#From this point onward, we only care about the target object
#-------------------------------------------------------------------------
#Do we have a target object?
if targetobj == None:
messages.append("ERROR: You are trying to bake to a target object with PBR Bake, but you have not selected one in the SimpleBake panel")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#If declas, need to do the displacement check again as the target object materials get baked
for slot in targetobj.material_slots:
if slot.material!=None:
mat = slot.material
if not check_mat_valid_for_displacement(mat.name, targetobj.name, messages):
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#Have we got more selected than just the target object?
if len(bake_objects) == 1 and bake_objects[0] == targetobj.name:
messages.append("ERROR: You are trying to bake to a target object, but the only object you have selected is your target")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#If the target object is in the bake objects list, remove it now
i = sbp.objects_list.find(targetobj.name)
if i>-1: sbp.objects_list.remove(i)
#Are UVs OK?
if sbp.new_uv_option == False and len(targetobj.data.uv_layers) == 0:
messages.append(f"ERROR: Object {targetobj.name} has no UVs, and you aren't generating new ones")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#All existing materials must use nodes
for slot in targetobj.material_slots:
if slot.material != None:
if not slot.material.use_nodes:
slot.material.use_nodes = True
return {'FINISHED'}
def cyclesbake_no_s2a_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "CyclesBake no S2A pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
#Are UVs OK?
if not sbp.tex_per_mat:
if sbp.new_uv_option == False and len(o.data.uv_layers) == 0:
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
if sbp.new_uv_option and sbp.new_uv_method=='CombineExisting' and len(o.data.uv_layers) == 0:
messages.append(f"ERROR: You are combining existing UVs, but object {o.name} has no UVs")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
else:
if sbp.expand_mat_uvs == False and len(o.data.uv_layers) == 0:
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
return {'FINISHED'}
def cyclesbake_s2a_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "CyclesBake S2A pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
#Do we actually have a target object?
if targetobj_cycles == None:
messages.append(f"ERROR: You are trying to bake to target with CyclesBake, but there is no target object selected on the SimpleBake panel")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#Have we got more selected than just the target object?
elif len(bake_objects) == 1 and bake_objects[0] == targetobj_cycles.name:
messages.append("ERROR: You are trying to bake selected to active with CyclesBake, but the only object you have selected is your active (target) object")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
#If the target object is in the bake objects list, remove it now
i = sbp.objects_list.find(targetobj_cycles.name)
if i>-1: sbp.objects_list.remove(i)
#Are UVs OK?
elif not sbp.new_uv_option and len(targetobj_cycles.data.uv_layers) == 0:
messages.append(f"ERROR: Object {targetobj_cycles.name} has no UVs, and you aren't generating new ones")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
return {'FINISHED'}
def specials_checks(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Specials pre-bake checks")
bake_objects = self.bake_objects
targetobj = self.targetobj
targetobj_cycles = self.targetobj_cycles
messages = []
if sbp.selected_col_vertex:
if not sbp.selected_s2a and not sbp.cycles_s2a:
for obj_name in bake_objects:
if (o:=bpy.data.objects.get(obj_name)):
if len(o.data.color_attributes) == 0:
messages.append(f"You are trying to bake the active vertex colours, but object {o.name} doesn't have vertex colours")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
if sbp.cycles_s2a or sbp.selected_s2a:
if sbp.selected_s2a: target_obj = sbp.targetobj
else: target_obj = sbp.targetobj_cycles
target_obj_name = target_obj.name
obj_names = [o.obj_point.name for o in sbp.objects_list]
if target_obj_name in obj_names:
obj_names.remove(target_obj_name)
for o_name in obj_names:
if(o:=context.scene.objects.get(o_name)):
if len(o.data.color_attributes) == 0:
messages.append(f"You are trying to bake the active vertex colours, but object {o.name} doesn't have vertex colours")
if len(messages)>0:
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
return {'FINISHED'}
def image_sequence_checks(self,context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Image sequence pre-bake checks")
messages = []
if not sbp.image_sequence_start_frame < sbp.image_sequence_end_frame:
messages.append("When baking an image sequence, the starting frame must be before the end frame. Change this in the Other Settings section of SimpleBake")
show_message_box(context, messages, "Errors occured", "ERROR")
return {'CANCELLED'}
return {'FINISHED'}
def execute(self, context):
blender_refresh()
sbp = context.scene.SimpleBake_Props
refresh_bake_objects_list(context)
#disable_impossible(context)
#sbp.bake_done = False
#Get objects
self.bake_objects = [i.name for i in sbp.objects_list]
self.targetobj = sbp.targetobj
self.targetobj_cycles = sbp.targetobj_cycles
print_message(context, "Starting checks operator")
#Universal checks
r = self.universal_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#PBR Bake Checks - No S2A
if sbp.global_mode == SBConstants.PBR and not sbp.selected_s2a:
r = self.pbr_no_s2a_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#PBR Bake - S2A
if sbp.global_mode == SBConstants.PBR and sbp.selected_s2a:
r = self.pbr_s2a_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#If decals, need to do a normal PBR check on the target object
if sbp.s2a_opmode == "decals":
self.bake_objects= [sbp.targetobj.name]
r = self.pbr_no_s2a_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#CyclesBake - no S2A
if sbp.global_mode == SBConstants.CYCLESBAKE and not sbp.cycles_s2a:
r = self.cyclesbake_no_s2a_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#CyclesBake - S2A
if sbp.global_mode == SBConstants.CYCLESBAKE and sbp.cycles_s2a:
r = self.cyclesbake_s2a_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#Specials
if len(specials_selection_to_list(context)) >0:
r = self.specials_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#Image sequence
if sbp.image_sequence_enabled:
r = self.image_sequence_checks(context)
if 'CANCELLED' in r: return{'CANCELLED'}
#Fix that weird context incorrect error
#Selected object at start must be something and must be mesh
#Find a nominal object that is a mesh and in the current view layer
if context.active_object == None or context.active_object.type != "MESH":
nominal = None
for o in context.view_layer.objects:
if o.type == "MESH":
nominal = o
break
assert(nominal!=None)
bpy.ops.simplebake.select_only_this(object_name=nominal.name)
#Guess everything was OK
return {'FINISHED'}
"""
#Specials Bake
#CAGE OBJECT BROKEN? CHECK IF NOT NONE AND, IF NOT, FLIP THE SWITdisable_impossibleCH TO USE CAGE
"""
classes = ([
SimpleBake_OT_Starting_Checks
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+34
View File
@@ -0,0 +1,34 @@
from . import panel
from . import panel_operators
from . import objects_list
from . import channel_packing_list
from . import preferences
from . import presets_list
from . import presets_list_local
from . import object_uvs_list
from . import panel_helpers
from . import node_bake_panel
def register():
objects_list.register()
panel.register()
panel_operators.register()
channel_packing_list.register()
preferences.register()
presets_list.register()
presets_list_local.register()
object_uvs_list.register()
panel_helpers.register()
node_bake_panel.register()
def unregister():
objects_list.unregister()
panel.unregister()
panel_operators.unregister()
channel_packing_list.unregister()
preferences.unregister()
presets_list.unregister()
presets_list_local.unregister()
object_uvs_list.unregister()
panel_helpers.unregister()
node_bake_panel.unregister()
@@ -0,0 +1,154 @@
import bpy
from bpy.types import Operator, UIList
from bpy.utils import register_class, unregister_class
from ..utils import SBConstants, clean_file_name
class SIMPLEBAKE_UL_CPTexList(UIList):
"""UIList."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
# We could write some code to decide which icon to use here...
custom_icon = 'NODE_COMPOSITING'
# Make sure your code supports all 3 layout types
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name, icon = custom_icon)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class SimpleBake_OT_cptex_add(Operator):
"""Add a SimpleBake CP Texture item"""
bl_idname = "simplebake.cptex_add"
bl_label = "Add"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
return sbp.cp_name != ""
def execute(self, context):
sbp = context.scene.SimpleBake_Props
cp_list = sbp.cp_list
name = clean_file_name(sbp.cp_name)
if name in cp_list:
#Delete it
index = sbp.cp_list.find(name)
sbp.cp_list.remove(index)
li = cp_list.add()
li.name = name
li.R = sbp.cptex_R
li.G = sbp.cptex_G
li.B = sbp.cptex_B
li.A = sbp.cptex_A
li.file_format = sbp.channelpackfileformat
li.exr_codec = sbp.exr_codec_cpts
li.png_compression = sbp.png_compression
sbp.cp_list_index = sbp.cp_list.find(name)
self.report({"INFO"}, "CP texture saved")
return {'FINISHED'}
class SimpleBake_OT_cptex_delete(Operator):
"""Delete the selected channel pack texture"""
bl_idname = "simplebake.cptex_delete"
bl_label = "Delete"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
try:
sbp.cp_list[sbp.cp_list_index].name
return True
except:
return False
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.cp_list.remove(sbp.cp_list_index)
self.report({"INFO"}, "CP texture deleted")
return {'FINISHED'}
class SimpleBake_OT_cptex_set_defaults(Operator):
"""Add some example channel pack textures"""
bl_idname = "simplebake.cptex_set_defaults"
bl_label = "Add examples"
@classmethod
def poll(cls,context):
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
cp_list = sbp.cp_list
#Unity Lit shader. R=metalness, G=AO, B=N/A, A=Glossy.
li = cp_list.add()
li.name = "Unity Lit Shader"
li.file_format = "OPEN_EXR"
li.R = "Metalness"
li.G = SBConstants.AO
li.B = "none"
li.A = "Glossiness"
#Unity Legacy Standard Diffuse. RGB=diffuse, A=alpha.
li = cp_list.add()
li.name = "Unity Legacy Shader"
li.file_format = "OPEN_EXR"
li.R = "Diffuse"
li.G = "Diffuse"
li.B = "Diffuse"
li.A = "Alpha"
#ORM format. R=AO, G=Roughness, B=Metalness, A=N/A.
li = cp_list.add()
li.name = "ORM"
li.file_format = "OPEN_EXR"
li.R = SBConstants.AO
li.G = "Roughness"
li.B = "Metalness"
li.A = "none"
#diffuse plus specular in the alpha channel.
li = cp_list.add()
li.name = "Diffuse and Spec in alpha"
li.file_format = "OPEN_EXR"
li.R = "Diffuse"
li.G = "Diffuse"
li.B = "Diffuse"
li.A = "Specular"
self.report({"INFO"}, "Default textures added")
return {'FINISHED'}
classes = ([
SIMPLEBAKE_UL_CPTexList,
SimpleBake_OT_cptex_add,
SimpleBake_OT_cptex_delete,
SimpleBake_OT_cptex_set_defaults
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,410 @@
import bpy
from bpy.types import Panel, Operator, PropertyGroup
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty, PointerProperty
from bpy.utils import register_class, unregister_class
# Module-level caches prevent enum item lists being garbage-collected mid-draw,
# which would crash Blender when dynamic EnumProperty items are used.
_socket_items_cache = [('0', 'Select a node', '', 0)]
_uv_items_cache = [('__active__', '(Active UV)', '', 0)]
def _get_socket_items(self, context):
global _socket_items_cache
node = getattr(context, 'active_node', None) if context else None
if node is None:
_socket_items_cache = [('0', 'Select a node', '', 0)]
return _socket_items_cache
items = []
for i, socket in enumerate(node.outputs):
if (socket.bl_idname not in ('NodeSocketShader', 'NodeSocketVirtual')
and not socket.hide
and socket.enabled):
items.append((str(i), socket.name, socket.name, len(items)))
if not items:
_socket_items_cache = [('0', 'No bakeable outputs', '', 0)]
return _socket_items_cache
_socket_items_cache = items
return _socket_items_cache
def _get_uv_items(self, context):
global _uv_items_cache
obj = context.active_object if context else None
items = [('__active__', '(Active UV)', 'Use whichever UV map is currently active on the object', 0)]
if obj and obj.type == 'MESH':
for i, layer in enumerate(obj.data.uv_layers):
items.append((layer.name, layer.name, layer.name, i + 1))
_uv_items_cache = items
return _uv_items_cache
class SimpleBakeNodeBakeProps(PropertyGroup):
socket: EnumProperty(
name="Socket",
description="Output socket to bake",
items=_get_socket_items,
)
image_name: StringProperty(
name="Image Name",
default="NodeBake",
description="Name for the baked image in the blend file",
)
width: IntProperty(name="Width", default=1024, min=1, max=16384)
height: IntProperty(name="Height", default=1024, min=1, max=16384)
float_buffer: BoolProperty(
name="32-bit Float",
default=False,
description="Use a 32-bit float image buffer (higher precision, larger memory footprint)",
)
uv_map: EnumProperty(
name="UV Map",
description="UV map to bake to. Defaults to whichever map is currently active on the object",
items=_get_uv_items,
)
replace_with_image: BoolProperty(
name="Replace with Image Texture",
default=False,
description=(
"After baking, insert an Image Texture node and reconnect it "
"to wherever this socket was feeding"
),
)
def _find_active_mat_output(nodes):
"""Return the active Material Output node, or any Material Output, or None."""
active = next((n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial' and n.is_active_output), None)
if active:
return active
return next((n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial'), None)
class SIMPLEBAKE_OT_NodeBake(Operator):
bl_idname = "simplebake.node_bake"
bl_label = "Bake Node Output"
bl_description = (
"Bake the selected output socket of the active node to an image texture. "
"The render engine is temporarily switched to Cycles for baking"
)
@classmethod
def poll(cls, context):
sd = getattr(context, 'space_data', None)
if sd is None or sd.type != 'NODE_EDITOR':
return False
if sd.tree_type != 'ShaderNodeTree':
return False
if context.active_object is None:
return False
node = context.active_node
if node is None:
return False
return any(
s.bl_idname not in ('NodeSocketShader', 'NodeSocketVirtual')
for s in node.outputs
)
# ------------------------------------------------------------------
# Material helpers
# ------------------------------------------------------------------
def _silence_material(self, mat):
"""
Temporarily wire a zero-emission shader into mat's Material Output so
it contributes nothing to an EMIT bake. Returns a restore tuple.
"""
nt = mat.node_tree
mat_out = _find_active_mat_output(nt.nodes)
if mat_out is None:
return None
orig = mat_out.inputs['Surface'].links[0].from_socket if mat_out.inputs['Surface'].is_linked else None
emit = nt.nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = (0.0, 0.0, 0.0, 1.0)
emit.inputs['Strength'].default_value = 0.0
nt.links.new(emit.outputs['Emission'], mat_out.inputs['Surface'])
return (nt, mat_out, emit, orig)
def _restore_material(self, restore_data):
if restore_data is None:
return
nt, mat_out, emit_node, orig_socket = restore_data
nt.nodes.remove(emit_node)
if orig_socket is not None:
nt.links.new(orig_socket, mat_out.inputs['Surface'])
# ------------------------------------------------------------------
# Operator execute
# ------------------------------------------------------------------
def execute(self, context):
props = context.scene.SimpleBakeNodeBake
space = context.space_data
node = context.active_node
node_location = (node.location[0], node.location[1])
# Must be at the root of the node tree, not inside a group
if space.edit_tree is not space.node_tree:
self.report({'ERROR'}, "Exit the node group before baking")
return {'CANCELLED'}
mat = space.id
if not isinstance(mat, bpy.types.Material):
self.report({'ERROR'}, "Could not determine the active material")
return {'CANCELLED'}
obj = context.active_object
mat_names = {s.material.name for s in obj.material_slots if s.material}
if mat.name not in mat_names:
self.report({'ERROR'}, f"'{mat.name}' is not assigned to the active object")
return {'CANCELLED'}
if not obj.data.uv_layers:
self.report({'ERROR'}, "The active object has no UV map — add one before baking")
return {'CANCELLED'}
# Activate the chosen UV map, remember original to restore later
chosen_uv = props.uv_map
orig_uv_index = obj.data.uv_layers.active_index
if chosen_uv != '__active__':
idx = next((i for i, l in enumerate(obj.data.uv_layers) if l.name == chosen_uv), None)
if idx is None:
self.report({'ERROR'}, f"UV map '{chosen_uv}' not found on object")
return {'CANCELLED'}
obj.data.uv_layers.active_index = idx
# Resolve the target socket
socket_idx = int(props.socket)
if socket_idx >= len(node.outputs):
self.report({'ERROR'}, "Socket index out of range — refresh by reselecting the node")
return {'CANCELLED'}
target_socket = node.outputs[socket_idx]
if target_socket.bl_idname in ('NodeSocketShader', 'NodeSocketVirtual'):
self.report({'ERROR'}, "Cannot bake a Shader or Virtual socket")
return {'CANCELLED'}
# Remember downstream links for the optional reconnect.
# Store names/identifiers rather than live Python wrapper references —
# those can become stale after node tree modifications during the bake.
downstream = [(lnk.to_node.name, lnk.to_socket.identifier) for lnk in target_socket.links]
# Create the output image
image_name = props.image_name.strip() or f"{node.name}_{target_socket.name}"
existing = bpy.data.images.get(image_name)
if existing:
bpy.data.images.remove(existing)
image = bpy.data.images.new(
image_name,
width=props.width,
height=props.height,
alpha=False,
float_buffer=props.float_buffer,
)
# --- Set up the target material ---
nt = mat.node_tree
nodes = nt.nodes
links = nt.links
mat_out = _find_active_mat_output(nodes)
created_mat_out = False
if mat_out is None:
mat_out = nodes.new('ShaderNodeOutputMaterial')
mat_out.location = (300.0, 300.0)
created_mat_out = True
orig_surface = mat_out.inputs['Surface'].links[0].from_socket if mat_out.inputs['Surface'].is_linked else None
# Temporary nodes
emit_node = nodes.new('ShaderNodeEmission')
emit_node.location = (mat_out.location[0] - 220, mat_out.location[1] - 80)
img_node = nodes.new('ShaderNodeTexImage')
img_node.name = "SB_NodeBake_Image"
img_node.image = image
img_node.location = (mat_out.location[0] - 420, mat_out.location[1] - 160)
# Make img_node the active image node (Cycles writes bake output here)
for n in nodes:
n.select = False
img_node.select = True
nodes.active = img_node
links.new(target_socket, emit_node.inputs['Color'])
links.new(emit_node.outputs['Emission'], mat_out.inputs['Surface'])
# --- Silence all other materials so they don't pollute the EMIT bake ---
silence_restores = []
for slot in obj.material_slots:
if slot.material and slot.material.name != mat.name:
rd = self._silence_material(slot.material)
if rd:
silence_restores.append(rd)
# --- Save scene state ---
orig_engine = context.scene.render.engine
orig_active_obj = context.view_layer.objects.active
orig_selected = list(context.selected_objects)
orig_mode = context.mode
# Configure for bake
context.scene.render.engine = 'CYCLES'
for o in list(context.selected_objects):
o.select_set(False)
obj.select_set(True)
context.view_layer.objects.active = obj
if orig_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# --- Bake ---
success = False
try:
bpy.ops.object.bake(type='EMIT', use_clear=True)
success = True
except Exception as exc:
self.report({'ERROR'}, f"Bake failed: {exc}")
# --- Restore UV map ---
obj.data.uv_layers.active_index = orig_uv_index
# --- Restore scene state ---
context.scene.render.engine = orig_engine
context.view_layer.objects.active = orig_active_obj
for o in orig_selected:
try:
o.select_set(True)
except Exception:
pass
# --- Restore silenced materials ---
for rd in silence_restores:
self._restore_material(rd)
# --- Restore target material ---
nodes.remove(emit_node)
if created_mat_out:
nodes.remove(mat_out)
elif orig_surface is not None:
links.new(orig_surface, mat_out.inputs['Surface'])
# --- Handle image node ---
# Always remove the bake-helper image node — it was only needed for Cycles
# to know where to write the result. If replacing, a fresh node is created
# below at the correct position, avoiding any stale Python wrapper issues.
bake_helper = nodes.get("SB_NodeBake_Image") or img_node
nodes.remove(bake_helper)
if not success:
bpy.data.images.remove(image)
return {'CANCELLED'}
if props.replace_with_image:
final_node = nodes.new('ShaderNodeTexImage')
final_node.image = image
final_node.parent = node.parent
image_node_height = max(
float(getattr(final_node, "height", 0.0) or 0.0),
float(final_node.dimensions[1] or 0.0),
)
if image_node_height <= 0.0:
image_node_height = 260.0
final_node.location = (node_location[0], node_location[1] + image_node_height + 80.0)
if chosen_uv != '__active__':
uv_node = nodes.new('ShaderNodeUVMap')
uv_node.uv_map = chosen_uv
uv_node.parent = final_node.parent
uv_node.location = (final_node.location[0] - 220, final_node.location[1] - 100)
links.new(uv_node.outputs['UV'], final_node.inputs['Vector'])
for node_name, sock_id in downstream:
to_node = nodes.get(node_name)
if to_node is None:
continue
to_socket = next((s for s in to_node.inputs if s.identifier == sock_id), None)
if to_socket is not None:
links.new(final_node.outputs['Color'], to_socket)
self.report({'INFO'}, f"Baked '{target_socket.name}''{image_name}'")
return {'FINISHED'}
class SIMPLEBAKE_PT_NodeBake(Panel):
bl_label = "Node Bake"
bl_idname = "SIMPLEBAKE_PT_NodeBake"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "SimpleBake"
@classmethod
def poll(cls, context):
sd = context.space_data
return (
sd.type == 'NODE_EDITOR'
and sd.tree_type == 'ShaderNodeTree'
and context.active_object is not None
)
def draw(self, context):
layout = self.layout
props = context.scene.SimpleBakeNodeBake
node = context.active_node
if node is None:
layout.label(text="Select a node to bake", icon='INFO')
return
valid_outputs = [
s for s in node.outputs
if s.bl_idname not in ('NodeSocketShader', 'NodeSocketVirtual')
]
if not valid_outputs:
layout.label(text="No bakeable outputs on this node", icon='ERROR')
return
layout.label(text=node.name, icon='NODE')
layout.prop(props, "socket")
layout.prop(props, "uv_map")
layout.prop(props, "image_name")
row = layout.row(align=True)
row.prop(props, "width", text="W")
row.prop(props, "height", text="H")
layout.prop(props, "float_buffer")
layout.prop(props, "replace_with_image")
layout.separator()
row = layout.row()
row.scale_y = 1.4
row.operator("simplebake.node_bake", text="Bake Node Output", icon='RENDER_STILL')
classes = [
SimpleBakeNodeBakeProps,
SIMPLEBAKE_OT_NodeBake,
SIMPLEBAKE_PT_NodeBake,
]
def register():
for cls in classes:
register_class(cls)
bpy.types.Scene.SimpleBakeNodeBake = PointerProperty(type=SimpleBakeNodeBakeProps)
def unregister():
del bpy.types.Scene.SimpleBakeNodeBake
for cls in reversed(classes):
unregister_class(cls)
@@ -0,0 +1,130 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, PropertyGroup, UIList
from bpy.props import EnumProperty, StringProperty, BoolProperty
class SIMPLEBAKE_UL_UVItems(UIList):
"""Rows: object name + dropdown of its UV maps (edit-active selector)"""
bl_idname = "SIMPLEBAKE_UL_UVItems"
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
sbp = context.scene.SimpleBake_Props
# data == SimpleBake_Props; item == SimpleBakeUVItem
#ob = bpy.data.objects.get(item.object_name)
ob = bpy.data.objects.get(item.name)
row = layout.row(align=True)
sb_uvl = next((l for l in ob.data.uv_layers if l.name=="SimpleBake"), None) if ob and ob.type == 'MESH' else None
# Object name
row.label(
#text=item.object_name or "<missing>",
text=item.name or "<missing>",
icon='UV_EDGESEL' if (ob and getattr(ob, "type", "") == 'MESH') else 'OBJECT_DATA'
)
# UV dropdown (prop_search over ob.data.uv_layers)
if sbp.prefer_existing_sbmap and sb_uvl:
row.label(text="SimpleBake (Locked)")
row.enabled = False
elif ob and ob.type == 'MESH' and getattr(ob.data, "uv_layers", None) and len(ob.data.uv_layers) > 0:
row.prop_search(
item, "uv_name",
ob.data, "uv_layers",
text=""
)
else:
row.label(text="(no UV maps)", icon='ERROR')
def sb_uvlist_sync_from_bake_list(context):
sc = context.scene
sbp = sc.SimpleBake_Props
sbp.uv_items.clear()
obj_names = [o.name for o in sbp.objects_list if bpy.data.objects.get(o.name)]
for name in obj_names:
ob = bpy.data.objects[name]
it = sbp.uv_items.add()
#it.object_name = name
it.name = name
me = getattr(ob, "data", None)
if not me or not getattr(me, "uv_layers", None) or len(me.uv_layers) == 0:
it.uv_name = ""
continue
# Current EDIT-active — use active_index (reliable in Blender 5.0);
# l.active getter is not reliably updated after setting active_index.
active_idx = getattr(me.uv_layers, "active_index", 0)
if not (0 <= active_idx < len(me.uv_layers)):
active_idx = 0
it.uv_name = me.uv_layers[active_idx].name
# keep index in range
sbp.uv_items_index = min(max(0, sbp.uv_items_index), max(0, len(sbp.uv_items)-1))
class SIMPLEBAKE_OT_SyncUVList(Operator):
bl_idname = "simplebake.sync_uv_list"
bl_label = "Synchronise Bake UV List"
bl_description = "Populate/refresh the list of bake objects and their UV maps"
def execute(self, context):
sb_uvlist_sync_from_bake_list(context)
#self.report({'INFO'}, "Bake UV list synchronised")
return {'FINISHED'}
# Helper (optional)
def _first_uv_name(ob: bpy.types.Object) -> str:
me = getattr(ob, "data", None)
if not me or not getattr(me, "uv_layers", None) or len(me.uv_layers) == 0:
return ""
return me.uv_layers[0].name
class SIMPLEBAKE_OT_UVSetAllName(Operator):
"""Attempt to set all edit active UVs of bake objects to the UV map name selected on the panel"""
bl_idname = "simplebake.uv_set_all_name"
bl_label = "Set all"
@classmethod
def poll(self, context):
sbp = context.scene.SimpleBake_Props
if sbp.available_uv_maps in ["","None"]:
return False
else:
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
if not getattr(sbp, "uv_items", None):
return {'CANCELLED'}
for i in sbp.uv_items:
i.uv_name = sbp.available_uv_maps
return {'FINISHED'}
classes = ([
SIMPLEBAKE_UL_UVItems,
SIMPLEBAKE_OT_SyncUVList,
SIMPLEBAKE_OT_UVSetAllName
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,582 @@
import bpy
from ..utils import find_closest_obj, show_message_box, find_3d_viewport
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, PropertyGroup, UIList
from bpy.props import EnumProperty, StringProperty
from ..utils import invalidate_udim_cache
def refresh_bake_objects_list(context):
sbp = context.scene.SimpleBake_Props
if sbp.global_mode == "PBR" and sbp.selected_s2a:
if sbp.s2a_opmode != "automatch":
if sbp.targetobj != None:
if sbp.targetobj.name not in bpy.context.scene.objects:
sbp.targetobj = None
if sbp.global_mode == "CyclesBake" and sbp.cycles_s2a:
if sbp.s2a_opmode != "automatch":
if sbp.targetobj_cycles != None:
if sbp.targetobj_cycles.name not in bpy.context.scene.objects:
sbp.targetobj_cycles = None
objects_list = sbp.objects_list
gone = []
for li in objects_list:
#Is it empty?
if li.obj_point == None:
gone.append(li.name)
#Is it no longer in the scene?
elif li.obj_point.name not in context.scene.objects:
gone.append(li.name)
#Is it not in use anywhere else?
elif len(li.obj_point.users_scene) < 1:
gone.append(li.name)
elif sbp.selected_s2a or sbp.cycles_s2a:
if sbp.s2a_opmode=="automatch" and sbp.auto_match_mode == "name":
for o in sbp.objects_list:
if o.obj_point != None and not o.obj_point.name.lower().endswith("_high"):
gone.append(o.name)
for g in gone:
objects_list.remove(objects_list.find(g))
for i in objects_list:
i.name = i.obj_point.name
#Throw in a refresh of the UV list. WHy not?
bpy.ops.simplebake.sync_uv_list()
def _strip_sb_prefixes(name):
"""Strip SimpleBake backup prefixes (SBW_, SBM_) to recover the original material name."""
if name.startswith("SBW_"):
name = name[4:]
if name.startswith("SBM_"):
name = name[4:]
return name
def refresh_mat_bake_list(context):
"""Keep mat_bake_list in sync with objects_list and their materials."""
sbp = context.scene.SimpleBake_Props
# Build the set of (obj_name, mat_name) pairs that should exist.
# Strip SimpleBake backup prefixes so the list always stores original names.
wanted = set()
for item in sbp.objects_list:
if item.obj_point is None:
continue
obj = item.obj_point
for slot in obj.material_slots:
if slot.material is not None:
wanted.add((obj.name, _strip_sb_prefixes(slot.material.name)))
# Remove stale entries
to_remove = []
for i, entry in enumerate(sbp.mat_bake_list):
if (entry.obj_name, entry.mat_name) not in wanted:
to_remove.append(i)
for i in reversed(to_remove):
sbp.mat_bake_list.remove(i)
# Add missing entries (default enabled=True)
existing = {(e.obj_name, e.mat_name) for e in sbp.mat_bake_list}
for obj_name, mat_name in sorted(wanted):
if (obj_name, mat_name) not in existing:
entry = sbp.mat_bake_list.add()
entry.obj_name = obj_name
entry.mat_name = mat_name
entry.enabled = True
class SIMPLEBAKE_UL_Objects_List(UIList):
"""Bake objects list"""
viable_high_low_bakes = []
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
sbp = context.scene.SimpleBake_Props
custom_icon = 'OBJECT_DATAMODE'
name = item.obj_point.name if item.obj_point != None else "_?_"
target_obj = "???"
cage_obj = ""
#Set the icon if in name mode
if (sbp.selected_s2a or sbp.cycles_s2a) and item.obj_point != None:
if sbp.s2a_opmode=="automatch":
if sbp.auto_match_mode == "name":
custom_icon = "SEQUENCE_COLOR_01" if bpy.app.version < (4,4,0) else "STRIP_COLOR_01"
target_obj = "???"
for o in context.scene.objects:
if name.lower().replace("_high", "_low") == o.name.lower():
target_obj = o.name
custom_icon = "SEQUENCE_COLOR_04" if bpy.app.version < (4,4,0) else "STRIP_COLOR_04"
if name not in __class__.viable_high_low_bakes:
__class__.viable_high_low_bakes.append(name)
#We didn't find a match
if target_obj == "???":
if name in __class__.viable_high_low_bakes:
__class__.viable_high_low_bakes.remove(name)
# Look up cage object by naming convention
cage_obj = ""
if target_obj != "???":
parts = target_obj.split("_")
base_name = '_'.join(parts[:-1]).lower()
for o in context.scene.objects:
if o.name.lower() == f"{base_name}_cage":
cage_obj = o.name
break
#Set the icon if in position mode
if (sbp.selected_s2a or sbp.cycles_s2a) and item.obj_point != None:
if sbp.s2a_opmode=="automatch":
if sbp.auto_match_mode == "position":
co = find_closest_obj(context, name)
if co != None:
custom_icon = "SEQUENCE_COLOR_04" if bpy.app.version < (4,4,0) else "STRIP_COLOR_04"
target_obj = co.name
if name not in __class__.viable_high_low_bakes:
__class__.viable_high_low_bakes.append(name)
else:
custom_icon = "SEQUENCE_COLOR_01" if bpy.app.version < (4,4,0) else "STRIP_COLOR_01"
target_obj = "???"
if name in __class__.viable_high_low_bakes:
__class__.viable_high_low_bakes.remove(name)
# Draw
if self.layout_type in {'DEFAULT', 'COMPACT'}:
col = layout.column()
col.label(text=name, icon=custom_icon)
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch":
col = layout.column()
col.alignment = 'CENTER'
col.label(text=" --> ")
col = layout.column()
icon = "QUESTION" if target_obj == "???" else "CHECKMARK"
col.label(text=target_obj, icon=icon)
if sbp.auto_match_mode == "name":
col = layout.column()
if cage_obj:
col.label(text=cage_obj, icon="MOD_MESHDEFORM")
else:
col.label(text="No cage")
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class SimpleBake_OT_Add_Bake_Object(Operator):
"""Add selected object(s) to the bake list"""
bl_idname = "simplebake.add_bake_object"
bl_label = "Adds a bake object to the list"
@classmethod
def poll(cls, context):
return len(context.selected_objects)
def execute(self, context):
sbp = context.scene.SimpleBake_Props
#Lets get rid of the non-mesh objects from the selections
[obj.select_set(False) for obj in context.scene.objects if obj.type != "MESH"]
#Do we still have an active object?
if context.active_object == None:
#If not, pick one
context.view_layer.objects.active = context.selected_objects[0]
#Add to list if not already in the list
objs = context.selected_objects.copy()
for obj in objs:
#If we are in matching high to low mode, only allow adding of _high objects
if (sbp.selected_s2a or sbp.cycles_s2a):
if sbp.s2a_opmode=="automatch" and sbp.auto_match_mode == "name":
if not obj.name.lower().endswith("_high"):
messages = ([
f"ERROR: Can't add object {obj.name}",
"You have auto match high and low poly selected and",
"you are using name mode.",
"Only object names ending in \"_high\" can be added"
])
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
continue
#Only add if not already on the list
r = [o.name for o in sbp.objects_list if o.name == obj.name]
if len(r) == 0:
n = sbp.objects_list.add()
n.obj_point = obj
n.name = obj.name
#Throw in a refresh
refresh_bake_objects_list(context)
refresh_mat_bake_list(context)
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch":
if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2:
sbp.merged_bake = False
#Invalidate UDIM cache
invalidate_udim_cache()
return{'FINISHED'}
class SimpleBake_OT_Add_Bake_Object_By_Name(Operator):
"""Add specified object(s) to the bake list"""
bl_idname = "simplebake.add_bake_object_by_name"
bl_label = "Adds a bake object to the list"
override_target_obj_name: StringProperty(default="")
def execute(self, context):
sbp = context.scene.SimpleBake_Props
n = sbp.objects_list.add()
obj = context.scene.objects[self.override_target_obj_name]
n.obj_point = obj
n.name = obj.name
#Invalidate UDIM cache
invalidate_udim_cache()
return{'FINISHED'}
class SimpleBake_OT_Remove_Bake_Object(Operator):
"""Remove the selected object from the bake list."""
bl_idname = "simplebake.remove_bake_object"
bl_label = "Removes a bake object from the list"
@classmethod
def poll(cls, context):
#TODO-------
return context.scene.SimpleBake_Props.objects_list
def execute(self, context):
sbp = context.scene.SimpleBake_Props
objects_list = sbp.objects_list
index = sbp.objects_list_index
#Record it's name
name = sbp.objects_list[index].name
objects_list.remove(index)
sbp.objects_list_index = min(max(0, index - 1), len(objects_list) - 1)
#Throw in a refresh
refresh_bake_objects_list(context)
refresh_mat_bake_list(context)
#Also remove from high low list
if name in SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes:
SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes.remove(name)
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch":
if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2:
sbp.merged_bake = False
#Invalidate UDIM cache
invalidate_udim_cache()
return{'FINISHED'}
class SimpleBake_OT_Clear_Bake_Objects_List(Operator):
"""Clear the object list"""
bl_idname = "simplebake.clear_bake_objects_list"
bl_label = "Clears the bake object list"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.objects_list.clear()
#Throw in a refresh
refresh_bake_objects_list(context)
refresh_mat_bake_list(context)
#Clear the high low match list
SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes = []
# if len(SIMPLEBAKE_UL_Objects_List.viable_high_low_bakes)<2:
# sbp.merged_bake = False
#Invalidate UDIM cache
invalidate_udim_cache()
return{'FINISHED'}
class SimpleBake_OT_Move_Bake_Object_List(Operator):
"""Move an object in the list."""
bl_idname = "simplebake.move_bake_object_list"
bl_label = "Moves an item in the bake objects list"
direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""),
('DOWN', 'Down', ""),))
@classmethod
def poll(cls, context):
return context.scene.SimpleBake_Props.objects_list
def move_index(self, context):
""" Move index of an item render queue while clamping it. """
sbp = context.scene.SimpleBake_Props
index = sbp.objects_list_index
list_length = len(sbp.objects_list) - 1 # (index starts at 0)
new_index = index + (-1 if self.direction == 'UP' else 1)
sbp.objects_list_index = max(0, min(new_index, list_length))
def execute(self, context):
sbp = context.scene.SimpleBake_Props
objects_list = sbp.objects_list
index = sbp.objects_list_index
neighbor = index + (-1 if self.direction == 'UP' else 1)
objects_list.move(neighbor, index)
self.move_index(context)
#Throw in a refresh
refresh_bake_objects_list(context)
return{'FINISHED'}
class SimpleBake_OT_Refresh_Bake_Object_List(Operator):
"""Refresh the list to remove objects"""
bl_idname = "simplebake.refresh_bake_object_list"
bl_label = "Refresh the bake objects list"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
refresh_bake_objects_list(context)
refresh_mat_bake_list(context)
#Invalidate UDIM cache
invalidate_udim_cache()
return{'FINISHED'}
#highlight_on = False
class SimpleBake_OT_Set_Highlight_Cols(Operator):
"""Actually modify the object colours"""
bl_idname = "simplebake.set_highlight_cols"
bl_label = "Set highlight cols to objects"
@classmethod
def update_object_list_from_selectd(cls):
sbp = bpy.context.scene.SimpleBake_Props
if (o:=bpy.context.active_object):
if (i:=sbp.objects_list.find(o.name))!=-1:
if sbp.objects_list_index !=i:
sbp.objects_list_index =i
def execute(self, context):
sbp = bpy.context.scene.SimpleBake_Props
#Do this anyway (EEVEE updates even if object selection changed)
__class__.update_object_list_from_selectd()
if not sbp.highlight_on:
return{'CANCELLED'} #Highlight turned off - end timer
#Do nothing more if we aren't in shading mode
any_shading = False
for space in find_3d_viewport():
if space.shading.type == "SOLID":
any_shading = True
if not any_shading:
return{'CANCELLED'}
#return 0.2 #Do nothing. Check back later
def set_col(obj_names, col):
for o_name in obj_names:
if (o:=bpy.data.objects.get(o_name)):
if o.color != col:
if "SB_orig_col" not in o:
o["SB_orig_col"] = o.color
o.color = col
bake_objs = [o.name for o in sbp.objects_list]
all_objs = ([o.name for o in bpy.context.scene.objects if
o.name not in bake_objs and
o.type == "MESH" and
"SB_auto_cage" not in o])
set_col(all_objs, (1,1,1,1))
c = sbp.highlight_col
set_col(bake_objs, (c.r, c.g, c.b, 1.0))
return{'FINISHED'}
class SimpleBake_OT_Highlight_Bake_Objects(Operator):
"""Highlight selected objects in the viewport"""
bl_idname = "simplebake.highlight_bake_objects"
bl_label = "Highlights bake objects in the viewport on this screen"
@classmethod
def poll(cls, context):
sbp = context.scene.SimpleBake_Props
return not sbp.highlight_on
@classmethod
def set_timer(cls):
bpy.app.timers.register(cls.col_bake_objects, first_interval=1.0)
@classmethod
def col_bake_objects(cls):
sbp = bpy.context.scene.SimpleBake_Props
bpy.ops.simplebake.set_highlight_cols()
if not sbp.highlight_on:
return None
else:
return 0.2
@classmethod
def set_display(cls, context):
for space in find_3d_viewport():
space.shading.color_type = "OBJECT"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.highlight_on = True
__class__.set_timer()
__class__.col_bake_objects()
__class__.set_display(context)
return{'FINISHED'}
class SimpleBake_OT_Remove_Highlight(Operator):
"""Remove highlight of selected objects in the viewport"""
bl_idname = "simplebake.remove_highlight"
bl_label = "Removes the highlights for bake objects in the viewport on this screen"
@classmethod
def poll(cls, context):
sbp = context.scene.SimpleBake_Props
return sbp.highlight_on
def uncol_bake_objects(self, context):
sbp = context.scene.SimpleBake_Props
bpy.ops.simplebake.refresh_bake_object_list()
obj_names = [o.name for o in sbp.objects_list]
for o_name in obj_names:
if (o:=bpy.data.objects.get(o_name)):
if "SB_orig_col" in o:
o.color = o["SB_orig_col"]
del o["SB_orig_col"]
def unset_display(self, context):
#for screen in bpy.data.screens:
screen = bpy.context.window.screen
# Iterate through the areas in the current screen
for area in screen.areas:
# Check if the area is a 3D viewport
if area.type == 'VIEW_3D':
# Optionally, you can access the space data of the area
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.color_type = "MATERIAL"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.highlight_on = False
self.uncol_bake_objects(context)
self.unset_display(context)
return{'FINISHED'}
class SimpleBake_OT_Materials_Expand_All(Operator):
"""Expand all objects in the Materials section"""
bl_idname = "simplebake.materials_expand_all"
bl_label = "Expand All"
def execute(self, context):
for item in context.scene.SimpleBake_Props.objects_list:
item.expanded = True
return {'FINISHED'}
class SimpleBake_OT_Materials_Collapse_All(Operator):
"""Collapse all objects in the Materials section"""
bl_idname = "simplebake.materials_collapse_all"
bl_label = "Collapse All"
def execute(self, context):
for item in context.scene.SimpleBake_Props.objects_list:
item.expanded = False
return {'FINISHED'}
classes = ([
SIMPLEBAKE_UL_Objects_List,
SimpleBake_OT_Add_Bake_Object,
SimpleBake_OT_Add_Bake_Object_By_Name,
SimpleBake_OT_Remove_Bake_Object,
SimpleBake_OT_Clear_Bake_Objects_List,
SimpleBake_OT_Move_Bake_Object_List,
SimpleBake_OT_Refresh_Bake_Object_List,
SimpleBake_OT_Highlight_Bake_Objects,
SimpleBake_OT_Remove_Highlight,
SimpleBake_OT_Set_Highlight_Cols,
SimpleBake_OT_Materials_Expand_All,
SimpleBake_OT_Materials_Collapse_All,
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,144 @@
import bpy
from ..bake_operators.pbr_bake_support_operators import has_convertible_shaders
is_dirty1 = True
is_dirty2 = True
is_dirty3 = True
is_dirty4 = True
@bpy.app.handlers.persistent
def _on_depsgraph_update(scene, depsgraph):
global is_dirty1, is_dirty2, is_dirty3, is_dirty4
is_dirty1 = True
is_dirty2 = True
is_dirty3 = True
is_dirty4 = True
check_for_render_inactive_modifiers_cached = False
def check_for_render_inactive_modifiers(context):
def update_cache(result):
global is_dirty1, check_for_render_inactive_modifiers_cached
check_for_render_inactive_modifiers_cached = result
is_dirty1 = False
if not is_dirty1:
return check_for_render_inactive_modifiers_cached
sbp = context.scene.SimpleBake_Props
objects = [i.obj_point for i in sbp.objects_list if i.obj_point is not None]
for obj in objects:
for mod in obj.modifiers:
if mod.show_render and not mod.show_viewport:
update_cache(True)
return True
if sbp.selected_s2a and sbp.targetobj is not None:
for mod in sbp.targetobj.modifiers:
if mod.show_render and not mod.show_viewport:
update_cache(True)
return True
if sbp.cycles_s2a and sbp.targetobj_cycles is not None:
for mod in sbp.targetobj_cycles.modifiers:
if mod.show_render and not mod.show_viewport:
update_cache(True)
return True
update_cache(False)
return False
check_for_convertible_shaders_cached = False
def check_for_convertible_shaders(context):
def update_cache(result):
global is_dirty2, check_for_convertible_shaders_cached
check_for_convertible_shaders_cached = result
is_dirty2 = False
if not is_dirty2:
return check_for_convertible_shaders_cached
sbp = context.scene.SimpleBake_Props
bake_objects = [i.obj_point.name for i in sbp.objects_list]
if sbp.selected_s2a and sbp.s2a_opmode == "decals" and sbp.targetobj is not None:
bake_objects.append(sbp.targetobj.name)
mats = set()
for obj_name in bake_objects:
if not (o := context.scene.objects.get(obj_name)):
continue
for slot in o.material_slots:
if slot.material is not None:
mats.add(slot.material.name)
result = any(has_convertible_shaders(m) for m in mats)
update_cache(result)
return result
check_for_auto_cage_cached = False
def check_for_auto_cage(context):
def update_cache(result):
global is_dirty3, check_for_auto_cage_cached
check_for_auto_cage_cached = result
is_dirty3 = False
if not is_dirty3:
return check_for_auto_cage_cached
sbp = context.scene.SimpleBake_Props
if sbp.global_mode == 'PBR':
if sbp.targetobj == None:
update_cache(False)
return False
else:
tobj_name = sbp.targetobj.name
else: #CyclesBake
if sbp.targetobj_cycles == None:
update_cache(False)
return False
else:
tobj_name = sbp.targetobj_cycles.name
t = [o.name for o in context.scene.objects
if "SB_auto_cage" in o and o["SB_auto_cage"] == tobj_name]
if len(t) > 0:
update_cache(True)
return True
else:
update_cache(False)
return False
check_for_viewport_inactive_modifiers_cached = False
def check_for_viewport_inactive_modifiers(context):
def update_cache(result):
global is_dirty4, check_for_viewport_inactive_modifiers_cached
check_for_viewport_inactive_modifiers_cached = result
is_dirty4 = False
if not is_dirty4:
return check_for_viewport_inactive_modifiers_cached
sbp = context.scene.SimpleBake_Props
objects = [i.obj_point for i in sbp.objects_list if i.obj_point is not None]
for obj in objects:
for mod in obj.modifiers:
if mod.show_viewport and not mod.show_render:
update_cache(True)
return True
update_cache(False)
return False
def _dirty_timer():
global is_dirty1, is_dirty2, is_dirty3, is_dirty4
is_dirty1 = True
is_dirty2 = True
is_dirty3 = True
is_dirty4 = True
return 3.0
def register():
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
bpy.app.timers.register(_dirty_timer, first_interval=3.0, persistent=True)
def unregister():
if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
if bpy.app.timers.is_registered(_dirty_timer):
bpy.app.timers.unregister(_dirty_timer)
@@ -0,0 +1,326 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty
import os
from ..utils import SBConstants, specials_selection_to_list, get_base_folder_patho
from ..messages import print_message
from ..presets_local import sb_prop_group_to_dict, deep_getattr
from ..presets import scene_props
import json
def auto_set_bake_margin():
context = bpy.context
multiplier = 4
current_width = context.scene.SimpleBake_Props.imgwidth
margin = (current_width / 1024) * multiplier
margin = round(margin, 0)
context.scene.render.bake.margin = int(margin)
return True
class SimpleBake_OT_selectall_pbr(Operator):
"""Select all PBR bake types"""
bl_idname = "simplebake.selectall_pbr"
bl_label = "Select All"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.selected_col = True
sbp.selected_metal = True
sbp.selected_rough = True
sbp.selected_normal = True
sbp.selected_trans = True
sbp.selected_transrough = True
sbp.selected_emission = True
sbp.selected_emission_strength = True
sbp.selected_clearcoat = True
sbp.selected_clearcoat_rough = True
sbp.selected_specular = True
sbp.selected_alpha = True
sbp.selected_sss = True
sbp.selected_sss_scale = True
sbp.selected_ssscol = True
sbp.selected_bump = True
sbp.selected_displacement = True
return {'FINISHED'}
class SimpleBake_OT_selectnone_pbr(Operator):
"""Select none PBR bake types"""
bl_idname = "simplebake.selectnone_pbr"
bl_label = "Select None"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.selected_col = False
sbp.selected_metal = False
sbp.selected_rough = False
sbp.selected_normal = False
sbp.selected_trans = False
sbp.selected_transrough = False
sbp.selected_emission = False
sbp.selected_emission_strength = False
sbp.selected_clearcoat = False
sbp.selected_clearcoat_rough = False
sbp.selected_specular = False
sbp.selected_sss = False
sbp.selected_sss_scale = False
sbp.selected_ssscol = False
sbp.selected_bump = False
sbp.selected_displacement = False
if not (sbp.selected_s2a == True and sbp.s2a_opmode == "decals"):
sbp.selected_alpha = False
return {'FINISHED'}
class SimpleBake_OT_increase_texture_res(Operator):
"""Increase texture resolution by 1k"""
bl_idname = "simplebake.increase_texture_res"
bl_label = "+1k"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
x = sbp.imgwidth
sbp.imgwidth = x + 1024
y = sbp.imgheight
sbp.imgheight = y + 1024
while sbp.imgheight % 1024 != 0:
sbp.imgheight -= 1
while sbp.imgwidth % 1024 != 0:
sbp.imgwidth -= 1
result = min(sbp.imgwidth, sbp.imgheight)
sbp.imgwidth = result
sbp.imgheight = result
auto_set_bake_margin()
return {'FINISHED'}
class SimpleBake_OT_decrease_texture_res(Operator):
"""Decrease texture resolution by 1k"""
bl_idname = "simplebake.decrease_texture_res"
bl_label = "-1k"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
x = sbp.imgwidth
sbp.imgwidth = x - 1024
y = sbp.imgheight
sbp.imgheight = y - 1024
if sbp.imgheight < 1:
sbp.imgheight = 1024
if sbp.imgwidth < 1:
sbp.imgwidth = 1024
while sbp.imgheight % 1024 != 0:
sbp.imgheight += 1
while sbp.imgwidth % 1024 != 0:
sbp.imgwidth += 1
result = max(sbp.imgwidth, sbp.imgheight)
sbp.imgwidth = result
sbp.imgheight = result
auto_set_bake_margin()
return {'FINISHED'}
class SimpleBake_OT_increase_output_res(Operator):
"""Increase output resolution by 1k"""
bl_idname = "simplebake.increase_output_res"
bl_label = "+1k"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
x = sbp.outputwidth
sbp.outputwidth = x + 1024
y = sbp.outputheight
sbp.outputheight = y + 1024
while sbp.outputheight % 1024 != 0:
sbp.outputheight -= 1
while sbp.outputwidth % 1024 != 0:
sbp.outputwidth -= 1
result = min(sbp.outputwidth, sbp.outputheight)
sbp.outputwidth = result
sbp.outputheight = result
auto_set_bake_margin()
return {'FINISHED'}
class SimpleBake_OT_decrease_output_res(Operator):
"""Decrease output resolution by 1k"""
bl_idname = "simplebake.decrease_output_res"
bl_label = "-1k"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
x = sbp.outputwidth
sbp.outputwidth = x - 1024
y = sbp.outputheight
sbp.outputheight = y - 1024
if sbp.outputheight < 1:
sbp.outputheight = 1024
if sbp.outputwidth < 1:
sbp.outputwidth = 1024
while sbp.outputheight % 1024 != 0:
sbp.outputheight += 1
while sbp.outputwidth % 1024 != 0:
sbp.outputwidth += 1
result = max(sbp.outputwidth, sbp.outputheight)
sbp.outputwidth = result
sbp.outputheight = result
auto_set_bake_margin()
return {'FINISHED'}
class SimpleBake_OT_panel_show_all(Operator):
"""Show all SimpleBake panel items"""
bl_idname = "simplebake.panel_show_all"
bl_label = "Show all"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.showtips = True
sbp.presets_show = True
sbp.bake_objects_show = True
sbp.materials_show = True
sbp.pbr_settings_show = True
sbp.aov_settings_show = True
sbp.cyclesbake_settings_show = True
sbp.specials_show = True
sbp.textures_show = True
sbp.export_show = True
sbp.uv_show = True
sbp.other_show = True
sbp.channelpacking_show = True
sbp.admin_settings_show = True
return {'FINISHED'}
class SimpleBake_OT_panel_hide_all(Operator):
"""Hide all SimpleBake panel items"""
bl_idname = "simplebake.panel_hide_all"
bl_label = "Hide all"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.showtips = False
sbp.presets_show = False
sbp.bake_objects_show = False
sbp.materials_show = False
sbp.pbr_settings_show = False
sbp.aov_settings_show = False
sbp.cyclesbake_settings_show = False
sbp.specials_show = False
sbp.textures_show = False
sbp.export_show = False
sbp.uv_show = False
sbp.other_show = False
sbp.channelpacking_show = False
sbp.admin_settings_show = False
return {'FINISHED'}
class SimpleBake_OT_Protect_Clear(Operator):
"""If you are online, you likely need to complete the 'I am not a robot' check on the web server. Click here to do that. All will be explained..."""
bl_idname = "simplebake.protect_clear"
bl_label = "Launch web browser"
def execute(self, context):
import webbrowser
webbrowser.open('http://www.toohey.co.uk/SimpleBake/protect_clear.html', new=2)
return {'FINISHED'}
class SimpleBake_OT_Copy_Settings_Clipboard(Operator):
"""Copy all current SimpleBake settings to the clipboard as JSON"""
bl_idname = "simplebake.copy_settings_clipboard"
bl_label = "Copy Settings to Clipboard"
def execute(self, context):
d = sb_prop_group_to_dict(context)
for p in scene_props:
d[p] = deep_getattr(context.scene, p)
context.window_manager.clipboard = json.dumps(d, indent=2)
self.report({"INFO"}, "SimpleBake settings copied to clipboard")
return {'FINISHED'}
class SimpleBake_OT_Export_Path_To_Blend_Location(Operator):
"""Set export path to the location of current blend file"""
bl_idname = "simplebake.export_path_to_blend_location"
bl_label = "Set"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.export_path = get_base_folder_patho(context).as_posix() + "/SimpleBake_Bakes"
sbp.export_path = bpy.path.relpath(sbp.export_path)
return {'FINISHED'}
#def invoke(self, context, event):b
#context.window_manager.fileselect_add(self)
#return {'RUNNING_MODAL'}
classes = ([
SimpleBake_OT_selectall_pbr,
SimpleBake_OT_selectnone_pbr,
SimpleBake_OT_increase_texture_res,
SimpleBake_OT_decrease_texture_res,
SimpleBake_OT_increase_output_res,
SimpleBake_OT_decrease_output_res,
SimpleBake_OT_panel_show_all,
SimpleBake_OT_panel_hide_all,
SimpleBake_OT_Protect_Clear,
SimpleBake_OT_Export_Path_To_Blend_Location,
SimpleBake_OT_Copy_Settings_Clipboard,
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+768
View File
@@ -0,0 +1,768 @@
import bpy
import os
from pathlib import Path
from bpy.utils import register_class, unregister_class
from bpy.types import Panel, AddonPreferences, Operator
from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty
from .. import __package__ as base_package
from .panel import SIMPLEBAKE_PT_main_panel
from .panel import read_settings
import webbrowser
try:
from ..auto_update import VersionControl
except:
VersionControl = None
from ..utils import SBConstants, clean_file_name, get_cs_list, blender_default_colorspace
panel_loc_just_changed = False
class SimpleBake_Preferences(AddonPreferences):
# this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = base_package
apikey: StringProperty(name="Sketchfab API Key: ")
img_name_format: StringProperty(name="Image format string",
default="%OBJ%_%BATCH%_%BAKEMODE%_%BAKETYPE%")
des = "Abbreviate the resolution in the image name where appropriate (e.g. \"2K\" instead of \"2048x2048\")"
abbr_res: BoolProperty(description = des, default = True)
#Aliases
col_default_index = blender_default_colorspace(float_buffer=False, col=True)[0]
non_col_default_index = blender_default_colorspace(float_buffer=False, col=False)[0]
diffuse_alias: StringProperty(name=SBConstants.PBR_DIFFUSE, default=SBConstants.PBR_DIFFUSE)
diffuse_cs: EnumProperty(name="", items=get_cs_list, default=col_default_index)
metal_alias: StringProperty(name=SBConstants.PBR_METAL, default=SBConstants.PBR_METAL)
metal_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
roughness_alias: StringProperty(name=SBConstants.PBR_ROUGHNESS, default=SBConstants.PBR_ROUGHNESS)
roughness_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
glossy_alias: StringProperty(name=SBConstants.PBR_GLOSSY, default=SBConstants.PBR_GLOSSY)
glossy_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
normal_alias: StringProperty(name=SBConstants.PBR_NORMAL, default=SBConstants.PBR_NORMAL)
normal_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
transmission_alias: StringProperty(name=SBConstants.PBR_TRANSMISSION, default=SBConstants.PBR_TRANSMISSION)
transmission_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
#transmissionrough_alias: StringProperty(name=SBConstants.PBR_TRANSMISSION_ROUGH, default=SBConstants.PBR_TRANSMISSION_ROUGH)
#transmissionrough_cs: EnumProperty(name="", items=get_cs_list, default=16)
clearcoat_alias: StringProperty(name=SBConstants.PBR_CLEARCOAT, default=SBConstants.PBR_CLEARCOAT)
clearcoat_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
clearcoatrough_alias: StringProperty(name=SBConstants.PBR_CLEARCOAT_ROUGH, default=SBConstants.PBR_CLEARCOAT_ROUGH)
clearcoatrough_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
clearcoatgloss_alias: StringProperty(name=SBConstants.PBR_CLEARCOAT_GLOSS, default=SBConstants.PBR_CLEARCOAT_GLOSS)
clearcoatgloss_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
emission_alias: StringProperty(name=SBConstants.PBR_EMISSION, default=SBConstants.PBR_EMISSION)
emission_cs: EnumProperty(name="", items=get_cs_list, default=col_default_index)
emission_strength_alias: StringProperty(name=SBConstants.PBR_EMISSION_STRENGTH, default=SBConstants.PBR_EMISSION_STRENGTH)
emission_strength_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
specular_alias: StringProperty(name=SBConstants.PBR_SPECULAR, default=SBConstants.PBR_SPECULAR)
specular_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
alpha_alias: StringProperty(name=SBConstants.PBR_ALPHA, default=SBConstants.PBR_ALPHA)
alpha_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
sss_alias: StringProperty(name=SBConstants.PBR_SSS, default=SBConstants.PBR_SSS)
sss_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
sss_scale_alias: StringProperty(name=SBConstants.PBR_SSS_SCALE, default=SBConstants.PBR_SSS_SCALE)
sss_scale_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
#ssscol_alias: StringProperty(name=SBConstants.PBR_SSSCOL, default=SBConstants.PBR_SSSCOL)
#ssscol_cs: EnumProperty(name="", items=get_cs_list, default=16)
bump_alias: StringProperty(name=SBConstants.PBR_BUMP, default=SBConstants.PBR_BUMP)
bump_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
displacement_alias: StringProperty(name=SBConstants.PBR_DISPLACEMENT, default=SBConstants.PBR_DISPLACEMENT)
displacement_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
ao_alias: StringProperty(name=SBConstants.AO, default=SBConstants.AO)
ao_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
curvature_alias: StringProperty(name=SBConstants.CURVATURE, default=SBConstants.CURVATURE)
curvature_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
thickness_alias: StringProperty(name=SBConstants.THICKNESS, default=SBConstants.THICKNESS)
thickness_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
vertexcol_alias: StringProperty(name=SBConstants.VERTEXCOL, default=SBConstants.VERTEXCOL)
vertexcol_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
colid_alias: StringProperty(name=SBConstants.COLOURID, default=SBConstants.COLOURID)
colid_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
lightmap_alias: StringProperty(name=SBConstants.LIGHTMAP, default=SBConstants.LIGHTMAP)
lightmap_cs: EnumProperty(name="", items=get_cs_list, default=non_col_default_index)
#Misc tweaks and options
no_update_check: BoolProperty(
default=False,
description="Don't automatically check for updates to SimpleBake. Keep at current installed version"
)
ungroup_all_node_groups: BoolProperty(
default=False,
description="Unpack all node groups. Used if you have very unusual node setups in your materials. (e.g. Material Output nodes hidden inside node groups). This will hurt performance, but can avoid crashes with some materials that have lots and lots of nested node groups"
)
skip_pbr_nodes_check: BoolProperty(
default=False,
description="Skip the check for invalid PBR nodes when baking PBR (Diffuse BSDF, Glossy BSDF etc.) NOT RECOMMENDED."
)
dont_replace_nonpbr_shaders: BoolProperty(
default=False,
description="Don't automatically replace non-PBR shaders with PBR equivalents where possible NOT RECOMMENDED."
)
disable_auto_smooth_for_split_normals: BoolProperty(
default=True,
description="The auto-smooth option causes issues when baking normal maps for objects with custom split normals. This option will cause SimpleBake to disable auto-smooth for the bake to stop weird artifacts in the normal map"
)
pbr_sample_count2: IntProperty(
default=16,
description="Override the sample count used for PBR bakes. The default is 2, and this is usually fine for all PBR bakes as they are baked as emission (i.e. no noise is possible)"
)
panel_location: EnumProperty(name="Panel location", default=0, items=[\
("renderpanel", "Render Panel", "Display SimpleBake on the Render Properties Panel (the camera icon)"),\
("npanel", "N-Panel", "Display SimpleBake on the N-Panel (also called the properties panel) in the 3D viewport")\
])
batch_name_max_length: IntProperty(
default=50,
description="Change the maximum length of the batch name property. WARNING: setting this value too high can lead to max path length errors on Windows"
)
togglesorboxes: EnumProperty(name="Toggles or tickboxes", default=0, items=[\
("toggles", "Use toggles", "Changes the appearance of the options on the SimpleBake panel (toggles)"),\
("tickboxes", "Use tickboxes", "Changes the appearance of the options on the SimpleBake panel (tickboxes)")\
])
solidshadingonbake: BoolProperty(
default=True,
description="It is recommended to only use solid shaing during baking to\
increase performance and reduce the chance of crashes"
)
saveonbake: BoolProperty(
default=True,
description="Save blend file prior to any baking operation"
)
disable_other_addons2: BoolProperty(
default=False,
description="Disable other addons when baking to prevent conflicts"
)
check_for_conflicting_addons: BoolProperty(
default=True,
description="Check for addons known to conflict with SimpleBake and prevent bake"
)
purge_after_bake: BoolProperty(
default=False,
description="Purge unused data blocks (materials etc.) from the blend file after bake. Use with caution as this won't only apply to SimpleBake created data"
)
override_optix_block: BoolProperty(
default=False,
description="Baking with Optix enabled has been buggy for years. It is blocked in SimpleBake by default, but can be overriden with this option. Not recommended."
)
capitalise_file_extensions: BoolProperty(
default=False,
description="Capitalise the file extension of exported images (e.g. .PNG instead of .png)"
)
@classmethod
def reset_img_string(cls, context):
prefs = context.preferences.addons[base_package].preferences
prefs.property_unset("img_name_format")
bpy.ops.wm.save_userpref()
@classmethod
def reset_aliases(cls, context):
prefs = context.preferences.addons[base_package].preferences
prefs.property_unset("diffuse_alias")
prefs.property_unset("metal_alias")
prefs.property_unset("roughness_alias")
prefs.property_unset("normal_alias")
prefs.property_unset("transmission_alias")
#prefs.property_unset("transmissionrough_alias")
prefs.property_unset("clearcoat_alias")
prefs.property_unset("clearcoatrough_alias")
prefs.property_unset("clearcoatgloss_alias")
prefs.property_unset("emission_alias")
prefs.property_unset("emission_strength_alias")
prefs.property_unset("specular_alias")
prefs.property_unset("alpha_alias")
prefs.property_unset("bump_alias")
prefs.property_unset("displacement_alias")
prefs.property_unset("sss_alias")
prefs.property_unset("sss_scale_alias")
#prefs.property_unset("ssscol_alias")
prefs.property_unset("ao_alias")
prefs.property_unset("curvature_alias")
prefs.property_unset("thickness_alias")
prefs.property_unset("vertexcol_alias")
prefs.property_unset("colid_alias")
prefs.property_unset("lightmap_alias")
prefs.property_unset("diffuse_cs")
prefs.property_unset("metal_cs")
prefs.property_unset("roughness_cs")
prefs.property_unset("normal_cs")
prefs.property_unset("transmission_cs")
#prefs.property_unset("transmissionrough_cs")
prefs.property_unset("clearcoat_cs")
prefs.property_unset("clearcoatrough_cs")
prefs.property_unset("clearcoatgloss_cs")
prefs.property_unset("emission_cs")
prefs.property_unset("emission_strength_cs")
prefs.property_unset("specular_cs")
prefs.property_unset("alpha_cs")
prefs.property_unset("bump_cs")
prefs.property_unset("displacement_cs")
prefs.property_unset("sss_cs")
prefs.property_unset("sss_scale_cs")
#prefs.property_unset("ssscol_cs")
prefs.property_unset("ao_cs")
prefs.property_unset("curvature_cs")
prefs.property_unset("thickness_cs")
prefs.property_unset("vertexcol_cs")
prefs.property_unset("colid_cs")
prefs.property_unset("lightmap_cs")
bpy.ops.wm.save_userpref()
def get_alias_dict(self):
alias_dict = {}
alias_dict[SBConstants.PBR_DIFFUSE] = clean_file_name(self.diffuse_alias)
alias_dict[SBConstants.PBR_METAL] = clean_file_name(self.metal_alias)
alias_dict[SBConstants.PBR_ROUGHNESS] = clean_file_name(self.roughness_alias)
alias_dict[SBConstants.PBR_GLOSSY] = clean_file_name(self.glossy_alias)
alias_dict[SBConstants.PBR_NORMAL] = clean_file_name(self.normal_alias)
alias_dict[SBConstants.PBR_TRANSMISSION] = clean_file_name(self.transmission_alias)
#alias_dict[SBConstants.PBR_TRANSMISSION_ROUGH] = clean_file_name(self.transmissionrough_alias)
alias_dict[SBConstants.PBR_CLEARCOAT] = clean_file_name(self.clearcoat_alias)
alias_dict[SBConstants.PBR_CLEARCOAT_ROUGH] = clean_file_name(self.clearcoatrough_alias)
alias_dict[SBConstants.PBR_CLEARCOAT_GLOSS] = clean_file_name(self.clearcoatgloss_alias)
alias_dict[SBConstants.PBR_EMISSION] = clean_file_name(self.emission_alias)
alias_dict[SBConstants.PBR_EMISSION_STRENGTH] = clean_file_name(self.emission_strength_alias)
alias_dict[SBConstants.PBR_SPECULAR] = clean_file_name(self.specular_alias)
alias_dict[SBConstants.PBR_ALPHA] = clean_file_name(self.alpha_alias)
alias_dict[SBConstants.PBR_SSS] = clean_file_name(self.sss_alias)
alias_dict[SBConstants.PBR_SSS_SCALE] = clean_file_name(self.sss_scale_alias)
#alias_dict[SBConstants.PBR_SSSCOL] = clean_file_name(self.ssscol_alias)
alias_dict[SBConstants.PBR_BUMP] = clean_file_name(self.bump_alias)
alias_dict[SBConstants.PBR_DISPLACEMENT] = clean_file_name(self.displacement_alias)
alias_dict[SBConstants.AO] = clean_file_name(self.ao_alias)
alias_dict[SBConstants.CURVATURE] = clean_file_name(self.curvature_alias)
alias_dict[SBConstants.THICKNESS] = clean_file_name(self.thickness_alias)
alias_dict[SBConstants.VERTEXCOL] = clean_file_name(self.vertexcol_alias)
alias_dict[SBConstants.COLOURID] = clean_file_name(self.colid_alias)
alias_dict[SBConstants.LIGHTMAP] = clean_file_name(self.lightmap_alias)
return alias_dict
def get_cs_dict(self):
cs_dict = {}
cs_dict[SBConstants.PBR_DIFFUSE] = self.diffuse_cs
cs_dict[SBConstants.PBR_METAL] = self.metal_cs
cs_dict[SBConstants.PBR_ROUGHNESS] = self.roughness_cs
cs_dict[SBConstants.PBR_GLOSSY] = self.glossy_cs
cs_dict[SBConstants.PBR_NORMAL] = self.normal_cs
cs_dict[SBConstants.PBR_TRANSMISSION] = self.transmission_cs
#cs_dict[SBConstants.PBR_TRANSMISSION_ROUGH] = self.transmissionrough_cs
cs_dict[SBConstants.PBR_CLEARCOAT] = self.clearcoat_cs
cs_dict[SBConstants.PBR_CLEARCOAT_ROUGH] = self.clearcoatrough_cs
cs_dict[SBConstants.PBR_CLEARCOAT_GLOSS] = self.clearcoatgloss_cs
cs_dict[SBConstants.PBR_EMISSION] = self.emission_cs
cs_dict[SBConstants.PBR_EMISSION_STRENGTH] = self.emission_strength_cs
cs_dict[SBConstants.PBR_SPECULAR] = self.specular_cs
cs_dict[SBConstants.PBR_ALPHA] = self.alpha_cs
cs_dict[SBConstants.PBR_SSS] = self.sss_cs
cs_dict[SBConstants.PBR_SSS_SCALE] = self.sss_scale_cs
#cs_dict[SBConstants.PBR_SSSCOL] = self.ssscol_cs
cs_dict[SBConstants.PBR_BUMP] = self.bump_cs
cs_dict[SBConstants.PBR_DISPLACEMENT] = self.displacement_cs
cs_dict[SBConstants.AO] = self.ao_cs
cs_dict[SBConstants.CURVATURE] = self.curvature_cs
cs_dict[SBConstants.THICKNESS] = self.thickness_cs
cs_dict[SBConstants.VERTEXCOL] = self.vertexcol_cs
cs_dict[SBConstants.COLOURID] = self.colid_cs
cs_dict[SBConstants.LIGHTMAP] = self.lightmap_cs
return cs_dict
def draw(self, context):
sbp = context.scene.SimpleBake_Props
layout = self.layout
box = layout.box()
row = box.row()
row.scale_y = 2
row.operator("simplebake.release_notes", icon="MOD_WAVE")
if VersionControl != None:
if not VersionControl.at_current:
row = box.row()
row.scale_y = 2
row.alert = True
if VersionControl.just_updated:
row.label(text=f"Please restart Blender")
else:
row.label(text=f"Version {VersionControl.current_version_str} of SimpleBake is available!")
row.operator("simplebake.auto_update", icon="MOD_WAVE")
#Always allow rollback
row = box.row()
row.scale_y = 2
row.operator("simplebake.rollback_update", icon="FILE_REFRESH", text="Rollback to previous version of SimpleBake")
#Support
box = layout.box()
row = box.row()
row.scale_y = 2
row.operator("simplebake.support", icon="HELP")
#Discord
box = layout.box()
row = box.row()
row.scale_y = 2
row.operator("simplebake.discord", icon="OUTLINER_OB_LIGHT")
#Patreon
box = layout.box()
row = box.row()
row.scale_y = 2
row.operator("simplebake.patreon", icon="FUND")
#Sketchfab
box = layout.box()
row = box.row()
row.label(text="Enter your Sketchfab API key below. Don't forget to click \"Save Preferences\" after.")
row = box.row()
row.prop(self, "apikey")
box = layout.box()
row = box.row()
row.label(text="Format for baked image names. Valid variables are:")
row = box.row()
row.label(text="%OBJ% (object name or 'MergedBake')")
row.scale_y = 0.5
row = box.row()
row.label(text="%BATCH% (batch name)")
row.scale_y = 0.5
row = box.row()
row.label(text="%BAKEMODE% (pbr or cycles bake)")
row.scale_y = 0.5
row = box.row()
row.label(text="%BAKETYPE% (diffuse, emission etc.)")
row.scale_y = 0.5
row = box.row()
row.label(text="%RESOLUTION% (e.g. 1024x1024)")
row.scale_y = 0.5
row = box.row()
row.label(text="%BLEND_FILE_NAME% (name of blend file if saved)")
row.scale_y = 0.5
row = box.row()
row.label(text="%FRAME% (Current frame number e.g. 0002 when baking an Image Sequence)")
row.scale_y = 0.5
row = box.row()
row.label(text="%MAT% (material name, when 'Texture Per Material' is enabled)")
row.scale_y = 0.5
row = box.row()
row.prop(self, "img_name_format")
row = box.row()
row.prop(self, "abbr_res", text="Abbreviate resolution")
row = box.row()
row.operator("simplebake.default_imgname_string")
#PBR Aliases
box = layout.box()
row = box.row()
row.label(text="Aliases and colour space settings for PBR bake types")
row = box.row()
row.label(text="WARNING: Sketchfab looks for certain values. Changing these may break SF Upload")
row = box.row()
col = row.column()
col.prop(self, "diffuse_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "diffuse_cs")
row = box.row()
col = row.column()
col.prop(self, "metal_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "metal_cs")
row = box.row()
col = row.column()
col.prop(self, "sss_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "sss_cs")
row = box.row()
col = row.column()
col.prop(self, "sss_scale_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "sss_scale_cs")
row = box.row()
col = row.column()
col.prop(self, "roughness_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "roughness_cs")
row = box.row()
col = row.column()
col.prop(self, "glossy_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "glossy_cs")
row = box.row()
col = row.column()
col.prop(self, "transmission_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "transmission_cs")
row = box.row()
col = row.column()
col.prop(self, "clearcoat_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "clearcoat_cs")
row = box.row()
col = row.column()
col.prop(self, "clearcoatrough_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "clearcoatrough_cs")
row = box.row()
col = row.column()
col.prop(self, "clearcoatgloss_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "clearcoatgloss_cs")
row = box.row()
col = row.column()
col.prop(self, "emission_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "emission_cs")
row = box.row()
col = row.column()
col.prop(self, "emission_strength_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "emission_strength_cs")
row = box.row()
col = row.column()
col.prop(self, "specular_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "specular_cs")
row = box.row()
col = row.column()
col.prop(self, "alpha_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "alpha_cs")
row = box.row()
col = row.column()
col.prop(self, "normal_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "normal_cs")
row = box.row()
col = row.column()
col.prop(self, "bump_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "bump_cs")
row = box.row()
col = row.column()
col.prop(self, "displacement_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "displacement_cs")
#Specials Aliases
box = layout.box()
row = box.row()
row.label(text="Aliases for special bake types")
row = box.row()
col = row.column()
col.prop(self, "ao_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "ao_cs")
row = box.row()
col = row.column()
col.prop(self, "curvature_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "curvature_cs")
row = box.row()
col = row.column()
col.prop(self, "thickness_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "thickness_cs")
row = box.row()
col = row.column()
col.prop(self, "vertexcol_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "vertexcol_cs")
row = box.row()
col = row.column()
col.prop(self, "colid_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "colid_cs")
row = box.row()
col = row.column()
col.prop(self, "lightmap_alias")
col = row.column()
col.alignment = 'RIGHT'
col.prop(self, "lightmap_cs")
#Reset button
box = layout.box()
row = box.row()
row.operator("simplebake.restore_default_aliases")
box = layout.box()
row = box.row()
row.label(text="Tweaks and Misc Options")
row = box.row()
row.prop(self, "solidshadingonbake", text="Solid shading on bake")
row = box.row()
row.prop(self, "saveonbake", text="Save blend file prior to bake")
row = box.row()
row.prop(self, "no_update_check", text="Don't check for SimpleBake updates on Blender start")
row = box.row()
row.prop(self, "ungroup_all_node_groups", text="Unpack ALL node groups in materials")
row = box.row()
row.prop(self, "skip_pbr_nodes_check", text="Skip check for invalid nodes for PBR bake (NOT RECOMMENDED)")
row = box.row()
row.prop(self, "dont_replace_nonpbr_shaders", text="Don't replace non-PBR shaders (NOT RECOMMENDED)")
row = box.row()
row.prop(self, "disable_auto_smooth_for_split_normals", text="Disable auto-smooth for custom split normals")
row = box.row()
row = box.row(align=False)
row.scale_y = 1.4
split = row.split(factor=0.6) # adjust factor to control spacing
split.label(text="Override PBR sample count")
split.prop(self, "pbr_sample_count2", text="")
row = box.row(align=False)
row.scale_y = 1.4
split = row.split(factor=0.6) # adjust factor to control spacing
split.label(text="Batch name max length")
split.prop(self, "batch_name_max_length", text="")
row = box.row()
row.prop(self, "disable_other_addons2", text="DEBUG: Disable other addons on bake")
row = box.row()
row.prop(self, "check_for_conflicting_addons", text="Check for conflicting addons")
row = box.row()
row.prop(self, "purge_after_bake", text="Purge unused data from blend file after bake (CAUTION)")
row = box.row()
row.prop(self, "override_optix_block", text="Override block on baking with Optix (NOT RECOMMENDED)")
row = box.row()
row.prop(self, "capitalise_file_extensions", text="Capitalise file extensions on export")
row = box.row()
row = box.row()
row.alert = True
row.scale_y = 0.5
row.label(text="WARNING: Based on my testing, Blender crashes WAY more when SimpleBake")
row = box.row()
row.alert = True
row.scale_y = 0.5
row.label(text="is displayed in the N-Panel. I haven't been able to find out why")
row = box.row()
row.alert = True
row.scale_y = 0.5
row.label(text="Use with extreme caution!")
row = box.row()
row.scale_y = 1.4
split = row.split(factor=0.2)
col = split.column()
col.label(text="Panel location")
col = split.column()
col.prop(self, "panel_location", text="")
col = split.column()
col.operator("simplebake.change_panel_location")
global panel_loc_just_changed
if panel_loc_just_changed:
row = box.row()
row.alert = True
row.label(text="Restart Blender for changes to take effect")
row = box.row()
row.scale_y = 1.4
split = row.split(factor=0.2)
col = split.column()
col.label(text="Toggles or tickboxes")
col = split.column()
col.prop(self, "togglesorboxes", text="")
col = split.column()
col.operator("simplebake.change_panel_toggles")
class SimpleBake_OT_change_panel_location(Operator):
"""Change the location of the SimpleBake panel"""
bl_idname = "simplebake.change_panel_location"
bl_label = "Change"
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake" / "settings"
savename = "panel_location.txt"
if not os.path.isdir(str(p)):
os.makedirs(str(p))
content = prefs.panel_location
with open(str(p / savename), 'w') as file:
file.write(content)
global panel_loc_just_changed
panel_loc_just_changed = True
return {'FINISHED'}
class SimpleBake_OT_change_panel_toggles(Operator):
"""Change the location of the SimpleBake panel"""
bl_idname = "simplebake.change_panel_toggles"
bl_label = "Change"
def execute(self, context):
prefs = context.preferences.addons[base_package].preferences
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake" / "settings"
savename = "toggles.txt"
if not os.path.isdir(str(p)):
os.makedirs(str(p))
content = prefs.togglesorboxes
with open(str(p / savename), 'w') as file:
file.write(content)
read_settings()
unregister_class(SIMPLEBAKE_PT_main_panel)
register_class(SIMPLEBAKE_PT_main_panel)
self.report({'INFO'}, "Change made")
return {'FINISHED'}
class SimpleBake_OT_restore_default_aliases(Operator):
"""Reset the image name aliases"""
bl_idname = "simplebake.restore_default_aliases"
bl_label = "Restore all aliases and colour space settings to default"
def execute(self, context):
SimpleBake_Preferences.reset_aliases(context)
return {'FINISHED'}
class SimpleBake_OT_default_imgname_string(Operator):
"""Reset the image name string to default (Sketchfab compatible)"""
bl_idname = "simplebake.default_imgname_string"
bl_label = "Restore image string to default"
def execute(self, context):
SimpleBake_Preferences.reset_img_string(context)
return {'FINISHED'}
class SimpleBake_OT_Release_Notes(Operator):
"""View the SimpleBake Release Notes"""
bl_idname = "simplebake.release_notes"
bl_label = "View the SimpleBake Release Notes"
def execute(self, context):
webbrowser.open('http://www.toohey.co.uk/SimpleBake/releasenotes4.html', new=2)
return {'FINISHED'}
class SimpleBake_OT_Support(Operator):
"""Open the SimpleBake Support Page"""
bl_idname = "simplebake.support"
bl_label = "Open the SimpleBake Support Page"
def execute(self, context):
webbrowser.open('http://www.toohey.co.uk/SimpleBake/support', new=2)
return {'FINISHED'}
class SimpleBake_OT_Discord(Operator):
"""Open the SimpleBake Discord"""
bl_idname = "simplebake.discord"
bl_label = "Open the SimpleBake Discord"
def execute(self, context):
webbrowser.open('https://discord.gg/ux9adAM2ZZ', new=2)
return {'FINISHED'}
class SimpleBake_OT_Patreon(Operator):
"""Support SimpleBake on Patreon"""
bl_idname = "simplebake.patreon"
bl_label = "Support SimpleBake on Patreon"
def execute(self, context):
webbrowser.open('https://patreon.com/HaughtyGrayAlien?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink', new=2)
return {'FINISHED'}
classes = ([
SimpleBake_Preferences,
SimpleBake_OT_Release_Notes,
SimpleBake_OT_Support,
SimpleBake_OT_restore_default_aliases,
SimpleBake_OT_default_imgname_string,
SimpleBake_OT_Discord,
SimpleBake_OT_Patreon,
SimpleBake_OT_change_panel_location,
SimpleBake_OT_change_panel_toggles
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,107 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, PropertyGroup, UIList
from bpy.props import EnumProperty
import os
from pathlib import Path
class SIMPLEBAKE_UL_Presets_List(UIList):
"""UIList."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
# We could write some code to decide which icon to use here...
custom_icon = 'PACKAGE'
# Make sure your code supports all 3 layout types
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name, icon = custom_icon)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class SimpleBake_OT_preset_refresh(Operator):
"""Refresh list of SimpleBake presets"""
bl_idname = "simplebake.preset_refresh"
bl_label = "Refresh"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.presets_list.clear()
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake"
try:
presets = sorted(os.listdir(str(p)))
presets = [pr for pr in presets if not os.path.isdir(str(p / pr))]
except Exception as e:
print(repr(e))
self.report({"INFO"}, "No presets found")
return {'CANCELLED'}
if len(presets) == 0:
self.report({"INFO"}, "No presets found")
return {'CANCELLED'}
for preset in presets:
#List should be clear
i = sbp.presets_list.add()
i.name = preset
return {'FINISHED'}
class SimpleBake_OT_preset_delete(Operator):
"""Delete selected SimpleBake preset"""
bl_idname = "simplebake.preset_delete"
bl_label = "Delete"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
try:
sbp.presets_list[sbp.presets_list_index].name
return True
except:
return False
def execute(self, context):
sbp = context.scene.SimpleBake_Props
p = Path(bpy.utils.script_path_user())
p = p.parents[1]
p = p / "data" / "SimpleBake"
index = sbp.presets_list_index
item = sbp.presets_list[index]
p = p / item.name
os.remove(str(p))
#Refreh the list
bpy.ops.simplebake.preset_refresh()
return {'FINISHED'}
classes = ([
SIMPLEBAKE_UL_Presets_List,
SimpleBake_OT_preset_refresh,
SimpleBake_OT_preset_delete
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,115 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator, PropertyGroup, UIList
from bpy.props import EnumProperty
def find_existing_local_presets(context):
"""
Returns a list of all existing SimpleBake local presets
"""
sbp = context.scene.SimpleBake_Props
#Get existing presets
existing_presets = []
for k in list(sbp.keys()):
if k.startswith("SB_local_preset_"):
existing_presets.append(k.replace("SB_local_preset_", ""))
existing_presets = sorted(existing_presets)
return existing_presets
def del_preset(context, all=False, friendly_name=""):
"""
Deletes either a specified preset or all presets
"""
sbp = context.scene.SimpleBake_Props
del_list = []
name = "SB_local_preset_" if all else f"SB_local_preset_{friendly_name}"
for k in list(sbp.keys()):
if k == name:
del_list.append(k)
for d in del_list:
del sbp[d]
class SIMPLEBAKE_UL_Local_Presets_List(UIList):
"""UIList."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
# We could write some code to decide which icon to use here...
custom_icon = 'PACKAGE'
# Make sure your code supports all 3 layout types
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name, icon = custom_icon)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class SimpleBake_OT_local_preset_refresh(Operator):
"""Refresh list of local SimpleBake presets"""
bl_idname = "simplebake.local_preset_refresh"
bl_label = "Local Refresh"
def execute(self, context):
sbp = context.scene.SimpleBake_Props
sbp.local_presets_list.clear()
for preset in find_existing_local_presets(context):
#List should be clear
i = sbp.local_presets_list.add()
i.name = preset
return {'FINISHED'}
class SimpleBake_OT_local_preset_delete(Operator):
"""Delete selected SimpleBake preset"""
bl_idname = "simplebake.local_preset_delete"
bl_label = "Delete local preset"
@classmethod
def poll(cls,context):
sbp = context.scene.SimpleBake_Props
try:
sbp.local_presets_list[sbp.local_presets_list_index].name
return True
except:
return False
def execute(self, context):
sbp = context.scene.SimpleBake_Props
index = sbp.local_presets_list_index
item = sbp.local_presets_list[index]
del_preset(context, friendly_name = item.name)
#Refreh the list
bpy.ops.simplebake.local_preset_refresh()
return {'FINISHED'}
classes = ([
SIMPLEBAKE_UL_Local_Presets_List,
SimpleBake_OT_local_preset_refresh,
SimpleBake_OT_local_preset_delete
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
+748
View File
@@ -0,0 +1,748 @@
import bpy
from bpy.types import Operator, Macro
from bpy.props import StringProperty, BoolProperty
from bpy.utils import register_class, unregister_class
import sys
from pathlib import Path
import base64
import mathutils
import tempfile
import os
import numpy as np
suppress_for_preset_load = False
_disable_impossible_running = False
def blender_refresh():
return True
# print_message(context, "Blender refresh")
# bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
# for area in context.screen.areas:
# area.tag_redraw()
# context.view_layer.update()
# context.view_layer.depsgraph.update()
#Takes a path object
def can_write_to_location(dir_path):
#First check if the folder already exists or not
if not os.path.exists(str(dir_path)):
#Find the first parent that does exist, see if we can write there
last = ""
while not os.path.exists(str(dir_path)):
dir_path = dir_path.parent
if str(dir_path) == last:
break
last = str(dir_path)
test_file_path = dir_path / "test_file.tmp"
try:
# Attempt to create a temporary file in the directory
with open(str(test_file_path), 'w') as temp_file:
temp_file.write("Test")
# If successful, remove the test file
os.remove(str(test_file_path))
return True
except PermissionError:
# Permission denied
return False
except Exception as e:
# Other exceptions could be handled differently if needed
print(f"An error occurred: {e}")
return False
def disable_impossible(context):
global suppress_for_preset_load, _disable_impossible_running
if _disable_impossible_running:
return
_disable_impossible_running = True
try:
sbp = context.scene.SimpleBake_Props
suppress_for_preset_load = True
#Start with a refresh of the objects list
bpy.ops.simplebake.refresh_bake_object_list()
#And AOVs
bpy.ops.simplebake.refresh_aov_list()
#And UVs
bpy.ops.simplebake.sync_uv_list()
if bpy.app.version >= (4,0,0):
sbp.selected_transrough = False
sbp.selected_ssscol = False
if not sbp.save_bakes_external:
sbp.rundenoise = False
sbp.rough_glossy_switch = SBConstants.PBR_ROUGHNESS
sbp.ccrough_glossy_switch = SBConstants.PBR_CLEARCOAT_ROUGH
sbp.normal_format_switch = SBConstants.NORMAL_OPENGL
sbp.multiply_diffuse_ao = "purediffuse"
sbp.do_aa = False
sbp.save_obj_external = False
for cpt in sbp.cp_list:
if "png_compression" not in cpt:
cpt.png_compression = 15
#if sbp.fbx_preset_name == "":
#sbp.fbx_preset_name = "None"
if sbp.multiply_diffuse_ao == "diffusexao" and sbp.global_mode == SBConstants.PBR:
sbp.selected_ao = True
if sbp.s2a_opmode == "decals":
sbp.multiply_diffuse_ao = "purediffuse"
if not sbp.keep_internal_after_export:
sbp.copy_and_apply = False
if sbp.tex_per_mat and (sbp.selected_s2a or sbp.cycles_s2a):
sbp.tex_per_mat = False
if sbp.tex_per_mat and sbp.expand_mat_uvs:
sbp.new_uv_option = True
if sbp.tex_per_mat and not sbp.expand_mat_uvs:
sbp.new_uv_option = False
if not sbp.merged_bake or not (sbp.export_mesh_individual_or_combined == "combined"):
sbp.merge_export_obj = False
if not sbp.merged_bake:
sbp.join_objs_to_proxy = False
finally:
suppress_for_preset_load = False
_disable_impossible_running = False
def get_bake_objects(context):
sbp = context.scene.SimpleBake_Props
#Work out our object selection
objects = []
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch":
from .bake_operators.auto_match_operators import SimpleBake_OT_AutoMatch_Operation as amo
#objects = [context.scene.objects[i] for i in list(amo.hl_matches.keys())]
objects = list(amo.hl_matches.keys())
elif sbp.selected_s2a: #Standard S2A bake or Decals
objects = [sbp.targetobj.name] if sbp.targetobj != None else []
elif sbp.cycles_s2a: #Standard S2A bake
objects = [sbp.targetobj_cycles.name] if sbp.targetobj_cycles != None else []
else:
objects = [i.obj_point.name for i in sbp.objects_list]
return objects
#active_tiles is sorted list of UDIM numbers (1001, 1002 etc.)
#total_tiles is highest tile index (e.g. 11 if the highest numbered active tile is 1011)
def get_udim_tiles(context, object_name, threshold=0.001):
import numpy as np
obj = context.scene.objects.get(object_name)
if not obj or not hasattr(obj.data, 'uv_layers') or not obj.data.uv_layers:
return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0}
uv_layer = obj.data.uv_layers.active
if uv_layer is None:
return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0}
n = len(uv_layer.data)
if n == 0:
return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0}
buf = np.empty(n * 2, dtype=np.float64)
uv_layer.data.foreach_get("uv", buf)
uvs = buf.reshape(-1, 2)
u = uvs[:, 0].copy()
v = uvs[:, 1].copy()
# Snap UV coordinates that sit very close to the upper edge of a tile back
# into that tile, to avoid a vertex at e.g. v=0.9999 being counted as tile 1.
frac_u = u % 1.0
frac_v = v % 1.0
u[frac_u > 1.0 - threshold] -= threshold
v[frac_v > 1.0 - threshold] -= threshold
# Subtract a tiny epsilon before flooring so that vertices sitting exactly
# on an integer boundary (e.g. v=1.0, where 1.0 % 1.0 == 0.0) snap to the
# tile below rather than the tile above.
tu = np.floor(u - 1e-9).astype(np.int32)
tv = np.floor(v - 1e-9).astype(np.int32)
mask = (tu >= 0) & (tu <= 9) & (tv >= 0) & (tv <= 9)
tile_nums = 1001 + tu[mask] + 10 * tv[mask]
import math
u_extent = math.ceil(float(np.max(u))) - math.floor(float(np.min(u)))
v_extent = math.ceil(float(np.max(v))) - math.floor(float(np.min(v)))
bbox_tiles = max(1, u_extent) * max(1, v_extent)
if tile_nums.size == 0:
return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": bbox_tiles}
udim_tiles = set(np.unique(tile_nums).tolist())
return {
"active_tiles": sorted(udim_tiles),
"total_tiles": max(udim_tiles) - 1000,
"bbox_tiles": bbox_tiles
}
# def get_max_udim_tiles_from_bake_objects(context):
# sbp = context.scene.SimpleBake_Props
# objs = [i.name for i in sbp.objects_list]
#
# max = 0
#
# for obj_name in objs:
# try:
# d = get_udim_tiles(context, obj_name)
# if len(d["active_tiles"]) > max:
# max = len(d["active_tiles"])
# except:
# pass
#
# return max
# --- UDIM tile cache -----------------------------------------------------------
# Keyed by object name. Dirty set contains names needing recompute.
_udim_tile_cache: dict = {}
_udim_dirty_objects: set = set()
def invalidate_udim_cache(object_names=None):
"""Mark objects as needing recompute. Pass None to invalidate everything."""
global _udim_dirty_objects, _udim_tile_cache
if object_names is None:
_udim_tile_cache.clear()
_udim_dirty_objects.clear()
else:
for name in object_names:
_udim_dirty_objects.add(name)
@bpy.app.handlers.persistent
def _sb_udim_depsgraph_handler(scene, depsgraph):
for update in depsgraph.updates:
if not update.is_updated_geometry:
continue
id_data = update.id
if isinstance(id_data, bpy.types.Object) and id_data.type == 'MESH':
# Topology change on the object itself
_udim_dirty_objects.add(id_data.name)
elif isinstance(id_data, bpy.types.Mesh):
# UV edits come through as mesh data-block updates —
# find all objects that use this mesh and dirty them
for obj in scene.objects:
if obj.type == 'MESH' and obj.data == id_data:
_udim_dirty_objects.add(obj.name)
def get_cached_udim_tiles(context, object_name, threshold=0.001):
global _udim_tile_cache, _udim_dirty_objects
if object_name in _udim_dirty_objects or object_name not in _udim_tile_cache:
#Cache result from the "real" method
_udim_tile_cache[object_name] = get_udim_tiles(context, object_name, threshold)
#Now clean
_udim_dirty_objects.discard(object_name)
return _udim_tile_cache[object_name]
# ------------------------------------------------------------------------------
def find_closest_obj(context, object_name, max_distance=0.5):
ref_obj = context.scene.objects.get(object_name)
if not ref_obj:
raise ValueError(f"No object found with the name '{object_name}'")
closest_mesh = None
closest_distance = float('inf')
for obj in context.scene.objects:
if obj.type == 'MESH' and obj != ref_obj and "SB_replaced_orig_name" not in obj:
distance = (ref_obj.location - obj.location).length
if distance < closest_distance:
closest_distance = distance
closest_mesh = obj
return closest_mesh if closest_distance <= max_distance else None
class SBConstants:
#Constants
PBR = "PBR"
PBRS2A = "PBR StoA"
PBRS2A_AUTOMATCH = "PBR StoA AutoMatch"
PBRS2A_DECALS = "PBR StoA Decals"
CYCLESBAKE = "CyclesBake"
CYCLESBAKE_S2A = "CyclesBake S2A"
CYCLESBAKES2A_AUTOMATCH = "CyclesBake S2A Auto"
SPECIALS = "Specials"
SPECIALS_PBR_TARGET_ONLY = "specials_pbr_targetonly"
SPECIALS_CYCLES_TARGET_ONLY = "specials_cycles_targetonly"
PBRAOVS = "AOV"
#PBR Names
PBR_DIFFUSE = "Diffuse"
PBR_METAL = "Metalness"
PBR_SSS = "SSS"
PBR_SSS_SCALE = "SSS Scale"
PBR_SSSCOL = "SSS Colour"
PBR_ROUGHNESS = "Roughness"
PBR_GLOSSY = "Glossiness"
PBR_NORMAL = "Normal"
PBR_TRANSMISSION = "Transmission"
PBR_TRANSMISSION_ROUGH = "Transmission Roughness"
PBR_CLEARCOAT = "Clearcoat"
PBR_CLEARCOAT_ROUGH = "Clearcoat Roughness"
PBR_CLEARCOAT_GLOSS = "Clearcoat Glossiness"
PBR_EMISSION = "Emission"
PBR_EMISSION_STRENGTH = "Emission Strength"
PBR_SPECULAR = "Specular"
PBR_ALPHA = "Alpha"
PBR_BUMP = "Bump"
PBR_DISPLACEMENT = "Displacement"
ALL_PBR_MODES = ([PBR_DIFFUSE, PBR_METAL, PBR_SSS,PBR_SSS_SCALE, PBR_SSSCOL, PBR_ROUGHNESS, PBR_GLOSSY,
PBR_NORMAL, PBR_TRANSMISSION, PBR_TRANSMISSION_ROUGH, PBR_CLEARCOAT,
PBR_CLEARCOAT_ROUGH, PBR_CLEARCOAT_GLOSS, PBR_EMISSION, PBR_EMISSION_STRENGTH, PBR_SPECULAR, PBR_ALPHA, PBR_BUMP, PBR_DISPLACEMENT])
#Specials names
THICKNESS = "Thickness"
AO = "Ambient Occlusion"
CURVATURE = "Curvature"
COLOURID = "Colour ID"
VERTEXCOL = "Vertex Colour"
LIGHTMAP = "Lightmap"
ALL_SPECIALS = ([THICKNESS, AO, CURVATURE,COLOURID, VERTEXCOL, LIGHTMAP])
COMP_DENOISE_SPECIALS = ([THICKNESS, AO, LIGHTMAP])
COMP_NODENOISE_SPECIALS = ([CURVATURE, VERTEXCOL, COLOURID])
#UVs
NEWUV_SMART_INDIVIDUAL = "SmartUVProject_Individual"
NEWUV_SMART_ATLAS = "SmartUVProject_Atlas"
NEWUV_COMBINE_EXISTING = "CombineExisting"
#Normals
NORMAL_OPENGL = "OpenGL"
NORMAL_DIRECTX = "DirectX"
@classmethod
def get_socket_names(cls):
v = bpy.app.version
if v >= (4,0,0):
return {
cls.PBR_DIFFUSE: "Base Color",
cls.PBR_METAL: "Metallic",
cls.PBR_SSS: "Subsurface Weight",
cls.PBR_SSS_SCALE: "Subsurface Scale",
cls.PBR_SSSCOL: None,
cls.PBR_ROUGHNESS: "Roughness",
cls.PBR_GLOSSY: "Roughness",
cls.PBR_NORMAL: "Normal",
cls.PBR_TRANSMISSION: "Transmission Weight",
cls.PBR_TRANSMISSION_ROUGH: None,
cls.PBR_CLEARCOAT: "Coat Weight",
cls.PBR_CLEARCOAT_ROUGH: "Coat Roughness",
cls.PBR_CLEARCOAT_GLOSS: "Coat Roughness",
cls.PBR_EMISSION: "Emission Color",
cls.PBR_EMISSION_STRENGTH: "Emission Strength",
cls.PBR_SPECULAR: "Specular IOR Level",
cls.PBR_ALPHA: "Alpha"
}
else:
#Principled socket names
return {
cls.PBR_DIFFUSE: "Base Color",
cls.PBR_METAL: "Metallic",
cls.PBR_SSS: "Subsurface",
cls.PBR_SSS_SCALE: "Subsurface Scale",
cls.PBR_SSSCOL: "Subsurface Color",
cls.PBR_ROUGHNESS: "Roughness",
cls.PBR_GLOSSY: "Roughness",
cls.PBR_NORMAL: "Normal",
cls.PBR_TRANSMISSION: "Transmission",
cls.PBR_TRANSMISSION_ROUGH: "Transmission Roughness",
cls.PBR_CLEARCOAT: "Clearcoat",
cls.PBR_CLEARCOAT_ROUGH: "Clearcoat Roughness",
cls.PBR_CLEARCOAT_GLOSS: "Clearcoat Glossiness",
cls.PBR_EMISSION: "Emission",
cls.PBR_EMISSION_STRENGTH: "Emission Strength",
cls.PBR_SPECULAR: "Specular",
cls.PBR_ALPHA: "Alpha"
}
def get_texture_channels(context, tex):
sbp = context.scene.SimpleBake_Props
if sbp.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]:
return "RGB"
elif tex in [SBConstants.PBR_DIFFUSE, SBConstants.PBR_SSSCOL, SBConstants.PBR_EMISSION, SBConstants.PBR_NORMAL, SBConstants.COLOURID, SBConstants.VERTEXCOL, SBConstants.LIGHTMAP]:
return "RGB"
elif SBConstants.PBRAOVS in tex:
num = int(tex.replace(f"{SBConstants.PBRAOVS}_", ""))
aov = [i for i in sbp.aov_items if i.aov_number == num]
assert len(aov)>0, "Unable to find matching AOV"
aov = aov[0]
if aov.aov_type == "C":
return "RGB"
else:
return "RGB" if sbp.bw_to_rgb else "BW"
else:
return "RGB" if sbp.bw_to_rgb else "BW"
def object_list_to_names_list(context):
sbp = context.scene.SimpleBake_Props
names = sbp.objects_list.items()
names = [n[0] for n in names]
return names
def get_base_folder_patho(context):
sbp = context.scene.SimpleBake_Props
if "--background" in sys.argv or sbp.fake_background:
base_folder = Path(sbp.base_folder_override)
else:
base_folder = Path(bpy.data.filepath).parent
return base_folder
def force_to_object_mode(context):
sbp = context.scene.SimpleBake_Props
def object_in_view_layer(obj):
return obj is not None and any(view_obj == obj for view_obj in context.view_layer.objects)
if not object_in_view_layer(context.active_object):
for item in sbp.objects_list:
obj = item.obj_point
if object_in_view_layer(obj):
context.view_layer.objects.active = obj
break
if context.active_object is None:
for obj in context.view_layer.objects:
if obj.type == "MESH" and obj.visible_get():
context.view_layer.objects.active = obj
break
if context.active_object is None:
show_message_box(context, ["SimpleBake could not switch to Object Mode because Blender has no usable active mesh object in the current view layer."], "Errors occured", icon='ERROR')
return False
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
return context.mode == "OBJECT"
# def check_for_safe_context_class(context):
# sbp = context.scene.SimpleBake_Props
# t = []
# #t.append(base64.b64decode("bGV3aXM=").decode("utf-8"))
# if sbp.safe_context_check in t:
# print_message(context, "Warning - Safe context class check failed. This is not fatal. Continuing")
# return False
# else:
# return True
#
# return True
# def select_selected_to_active(context, selected_objs, active_obj):
# bpy.ops.object.select_all(action="DESELECT")
#
# for obj in selected_objs:
# obj.select_set(state=True)
# active_obj.select_set(state=True)
# context.view_layer.objects.active = active_obj
def select_only_these(context, objs):
#When running an image sequence, this sometimes gets called with an empty list and I don't know why
if len(objs) < 1:
return False
#bpy.ops.object.select_all(action="DESELECT")
[o.select_set(state=False) for o in context.scene.objects]
for obj in objs:
obj.select_set(state=True)
context.view_layer.objects.active = objs[0] #arbitary
def select_only_this(context, obj):
#bpy.ops.object.select_all(action="DESELECT")
[o.select_set(state=False) for o in context.scene.objects]
obj.select_set(state=True)
context.view_layer.objects.active = obj
def pbr_selections_to_list(context):
sbp = context.scene.SimpleBake_Props
selectedbakes = []
selectedbakes.append(SBConstants.PBR_DIFFUSE) if sbp.selected_col else False
selectedbakes.append(SBConstants.PBR_METAL) if sbp.selected_metal else False
selectedbakes.append(SBConstants.PBR_ROUGHNESS) if sbp.selected_rough and sbp.rough_glossy_switch==SBConstants.PBR_ROUGHNESS else False
selectedbakes.append(SBConstants.PBR_GLOSSY) if sbp.selected_rough and sbp.rough_glossy_switch==SBConstants.PBR_GLOSSY else False
selectedbakes.append(SBConstants.PBR_NORMAL) if sbp.selected_normal else False
selectedbakes.append(SBConstants.PBR_TRANSMISSION) if sbp.selected_trans else False
selectedbakes.append(SBConstants.PBR_TRANSMISSION_ROUGH) if sbp.selected_transrough else False
selectedbakes.append(SBConstants.PBR_CLEARCOAT) if sbp.selected_clearcoat else False
selectedbakes.append(SBConstants.PBR_CLEARCOAT_ROUGH) if sbp.selected_clearcoat_rough and sbp.ccrough_glossy_switch==SBConstants.PBR_CLEARCOAT_ROUGH else False
selectedbakes.append(SBConstants.PBR_CLEARCOAT_GLOSS) if sbp.selected_clearcoat_rough and sbp.ccrough_glossy_switch==SBConstants.PBR_CLEARCOAT_GLOSS else False
selectedbakes.append(SBConstants.PBR_EMISSION) if sbp.selected_emission else False
selectedbakes.append(SBConstants.PBR_EMISSION_STRENGTH) if sbp.selected_emission_strength else False
selectedbakes.append(SBConstants.PBR_SPECULAR) if sbp.selected_specular else False
selectedbakes.append(SBConstants.PBR_ALPHA) if sbp.selected_alpha else False
selectedbakes.append(SBConstants.PBR_SSS) if sbp.selected_sss else False
selectedbakes.append(SBConstants.PBR_SSS_SCALE) if sbp.selected_sss_scale else False
selectedbakes.append(SBConstants.PBR_SSSCOL) if sbp.selected_ssscol else False
selectedbakes.append(SBConstants.PBR_BUMP) if sbp.selected_bump else False
selectedbakes.append(SBConstants.PBR_DISPLACEMENT) if sbp.selected_displacement else False
for aov in sbp.aov_items:
if aov.enabled:
selectedbakes.append(SBConstants.PBRAOVS + f"_{aov.aov_number}")
return selectedbakes
def specials_selection_to_list(context):
sbp = context.scene.SimpleBake_Props
selected_specials = []
selected_specials.append(SBConstants.THICKNESS) if sbp.selected_thickness else False
selected_specials.append(SBConstants.COLOURID) if sbp.selected_col_mats else False
selected_specials.append(SBConstants.VERTEXCOL) if sbp.selected_col_vertex else False
selected_specials.append(SBConstants.CURVATURE) if sbp.selected_curvature else False
selected_specials.append(SBConstants.LIGHTMAP) if sbp.selected_lightmap else False
selected_specials.append(SBConstants.AO) if sbp.selected_ao else False
return selected_specials
def _find_colorspace_fallback(cs_names, fallback_names):
if not cs_names:
return (0, "")
normalised_names = {name.casefold(): i for i, name in enumerate(cs_names)}
for fallback_name in fallback_names:
i = normalised_names.get(fallback_name.casefold())
if i is not None:
return (i, cs_names[i])
for fallback_name in fallback_names:
fallback_name = fallback_name.casefold()
for i, cs_name in enumerate(cs_names):
if fallback_name in cs_name.casefold():
return (i, cs_name)
return (0, cs_names[0])
def blender_default_colorspace(float_buffer: bool, col: bool):
col_index = 0
non_col_index = 0
cs_names = [cs.identifier for cs in bpy.types.Image.bl_rna.properties['colorspace_settings'].fixed_type.bl_rna.properties['name'].enum_items]
if bpy.app.version >= (5,0,0):
col_index = 23
non_col_index = 18
elif bpy.app.version >= (4,2,0):
col_index = 20
non_col_index = 17
elif bpy.app.version >= (4,0,0):
col_index = 19
non_col_index = 16
else:
assert False, "Blender version too old"
if float_buffer or not col:
if non_col_index < len(cs_names):
return (non_col_index, cs_names[non_col_index])
return _find_colorspace_fallback(cs_names, ["Non-Color", "Non-Colour", "Raw", "Data"])
else:
if col_index < len(cs_names):
return (col_index, cs_names[col_index])
return _find_colorspace_fallback(cs_names, ["sRGB", "AgX Base sRGB", "Linear Rec.709"])
def get_cs_list(self, context):
cs_items = [(cs.identifier,cs.identifier,"") for cs in bpy.types.Image.bl_rna.properties['colorspace_settings'].fixed_type.bl_rna.properties['name'].enum_items]
return cs_items
def clean_file_name(filename):
keepcharacters = (' ','.','_','~',"-", "[", "]")
return "".join(c for c in filename if c.isalnum() or c in keepcharacters)#.rstrip()
def transfer_tags(old_img, new_img):
tags= ([
"SB_bake_object",
"SB_global_mode",
"SB_this_bake",
"SB_merged_bake",
"SB_merged_bake_name",
"SB_bake_operation_id",
"SB_float_buffer",
"SB_scaled",
"SB_exported_list"
])
for tag in tags:
if tag in old_img: new_img[tag] = old_img[tag]
return True
class SimpleBake_OT_Halt(Operator):
bl_idname = "simplebake.halt"
bl_label = ""
def execute(self, context):
raise Exception
return {'FINISHED'}
def show_message_box(context, messageitems_list, title, icon = 'INFO'):
message=""
for li in messageitems_list:
message+=f"{li}|"
bpy.ops.simplebake.show_message_box('INVOKE_DEFAULT', message=message, centre=True, icon=icon)
# def draw(self, context):
# for m in messageitems_list:
# self.layout.label(text=m)
# context.window_manager.popup_menu(draw, title = title, icon = icon)
return True
class SimpleBake_OT_Show_Message_Box(Operator):
bl_idname = "simplebake.show_message_box"
bl_label = ""
message: StringProperty()
icon: StringProperty(default="INFO")
centre: BoolProperty(default=False)
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
self.restored = False
if self.centre:
self.orig_x = event.mouse_x
self.orig_y = event.mouse_y
w = int(context.window.width/2)
h = int(context.window.height/2)
h = h + (20*len(self.message.split("|")))
context.window.cursor_warp(w, h)
longest = 0
for li in self.message.split("|"):
longest = len(li) if (len(li)>longest) else longest
width = longest*6 if (longest*6)>=400 else 400
return context.window_manager.invoke_props_dialog(self, width = width)
def draw(self, context):
self.layout.label(text="SIMPLEBAKE", icon=self.icon)
message_list = self.message.split("|")
for li in message_list:
row=self.layout.row()
row.scale_y = 0.8
row.alignment = 'LEFT'
row.label(text=li)
if not self.restored and self.centre:
context.window.cursor_warp(self.orig_x, self.orig_y)
self.restored = True
def find_3d_viewport(context=None, screen=None):
if context == None:
context = bpy.context
if screen == None:
screen = bpy.context.window.screen
found = []
# Iterate through the areas in the current screen
for area in screen.areas:
# Check if the area is a 3D viewport
if area.type == 'VIEW_3D':
# Optionally, you can access the space data of the area
for space in area.spaces:
if space.type == 'VIEW_3D':
found.append(space)
return found
def is_blend_saved():
path = bpy.data.filepath
if(path=="/" or path==""):
return False
else:
return True
def is_all_black_or_transparent(img: bpy.types.Image, tol=1e-6):
if not img.has_data:
img.update()
# Blender exposes pixels as RGBA floats in [0..1]; length = w*h*4
px = np.asarray(img.pixels[:], dtype=np.float32).reshape(-1, 4)
rgb = px[:, :3]
a = px[:, 3]
all_black = np.all(np.abs(rgb) <= tol) # R=G=B≈0 everywhere
all_transparent = np.all(a <= tol) # A≈0 everywhere
return all_black, all_transparent
conflicting_addons = []#"ZenUV"]
classes = ([
SimpleBake_OT_Show_Message_Box,
#SimpleBake_OT_Print_Message,
SimpleBake_OT_Halt
])
def register():
global classes
for cls in classes:
register_class(cls)
if _sb_udim_depsgraph_handler not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(_sb_udim_depsgraph_handler)
def unregister():
global classes
for cls in classes:
unregister_class(cls)
if _sb_udim_depsgraph_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(_sb_udim_depsgraph_handler)
+285
View File
@@ -0,0 +1,285 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import FloatProperty, EnumProperty, BoolProperty, StringProperty
from .utils import SBConstants, select_only_this
from .messages import print_message
from .bake_operators.common_bake_support import match_high_low_objects
def rearrange_uvs(obj, uv_name):
uvs = obj.data.uv_layers
orig_active_name = uvs.active.name
orig_active_render_name = [uv.name for uv in uvs if uv.active_render][0]
while uvs[0].name != uv_name:
#Move to bottom
name = uvs[0].name
uvs.active = uvs[0]
uvs.new()
uvs.remove(uvs[0])
uvs[-1].name = name
uvs.active = uvs[orig_active_name]
uvs[orig_active_render_name].active_render = True
def pack_uvs(self, context):
sbp = context.scene.SimpleBake_Props
bpy.ops.uv.select_all(action="SELECT")
#Always average - still relevant for pre-Blender 3.6
if sbp.new_uv_method == "CombineExisting" and self.average_islands:
bpy.ops.uv.average_islands_scale()
n = int(''.join(c for c in bpy.app.version_string if c.isdigit()))
if n >= 360:
bpy.ops.uv.pack_islands("EXEC_DEFAULT", udim_source=sbp.uvp_pack_to, rotate=sbp.uvp_rotate, rotate_method=sbp.uvp_rotation_method, scale=sbp.uvp_scale, merge_overlap=sbp.uvp_merge_overlapping, margin_method=sbp.uvp_margin_method, margin=sbp.uvpackmargin, pin=sbp.uvp_lock_pinned, pin_method=sbp.uvp_lock_method, shape_method=sbp.uvp_shape_method)
else: # Pre Blender 3.6
bpy.ops.uv.pack_islands(rotate=True, margin=sbp.uvpackmargin)
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
class SimpleBake_OT_Process_UVs(Operator):
"""Start the bake operation"""
bl_idname = "simplebake.process_uvs"
bl_description = "Prepare UV maps on the bake objects"
bl_label = "Prepare UVs"
def smart_project_to_atlas(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Smart projecting all objects to atlas map")
#Selection
bpy.ops.object.select_all(action="DESELECT")
for obj in self.bake_objects:
obj.select_set(state=True)
#Make sure we have an active object
if context.active_object == None:
context.view_layer.objects.active = self.bake_objects[0]
for obj in self.bake_objects:
if("SimpleBake" in obj.data.uv_layers):
obj.data.uv_layers.remove(obj.data.uv_layers["SimpleBake"])
obj.data.uv_layers.new(name="SimpleBake")
obj.data.uv_layers["SimpleBake"].active = True
bpy.ops.object.mode_set(mode="EDIT", toggle=False) #Enter edit mode
bpy.ops.mesh.reveal()
bpy.ops.mesh.select_all(action="SELECT")
#Unwrap
bpy.ops.uv.smart_project(angle_limit=self.angle_limit, island_margin=self.island_margin, area_weight=self.area_weight, correct_aspect=self.uvcorrectaspect, scale_to_bounds=self.scale_to_bounds)
#Packing
#pack_uvs(self)
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
return
def combine_existing_to_atlas(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Combining existing UVs into atlas map")
#Selection
# bpy.ops.object.select_all(action="DESELECT")
for obj in bpy.context.selected_objects:
obj.select_set(False)
for obj in self.bake_objects:
obj.select_set(state=True)
#Make sure we have an active object
if context.active_object == None:
context.view_layer.objects.active = self.bake_objects[0]
for obj in self.bake_objects:
if("GENERATED_SimpleBake_Atlas" in obj.data.uv_layers):
obj.data.uv_layers.remove(obj.data.uv_layers["GENERATED_SimpleBake_Atlas"])
u = obj.data.uv_layers.new(name="GENERATED_SimpleBake_Atlas")
u.active = True
#With everything selected, pack into one big map
bpy.ops.object.mode_set(mode="EDIT", toggle=False)
bpy.ops.mesh.reveal()
bpy.ops.mesh.select_all(action="SELECT")
pack_uvs(self, context)
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
return
def smart_project_individual_objects(self, context):
print_message(context, "Smart projecting each object individually")
for obj in self.bake_objects:
select_only_this(context, obj)
if("SimpleBake" in obj.data.uv_layers):
obj.data.uv_layers.remove(obj.data.uv_layers["SimpleBake"])
obj.data.uv_layers.new(name="SimpleBake")
obj.data.uv_layers["SimpleBake"].active = True
bpy.ops.object.mode_set(mode="EDIT", toggle=False)
bpy.ops.mesh.reveal()
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.uv.smart_project(angle_limit=self.angle_limit, island_margin=self.island_margin, area_weight=self.area_weight, correct_aspect=self.uvcorrectaspect, scale_to_bounds=self.scale_to_bounds)
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
return
def per_mat_pack_uvs_to_bounds(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, "Packing UVs for each material into new UV map")
bpy.ops.object.select_all(action="DESELECT")
for obj in self.bake_objects:
# Copy existing UV data into a fresh SimpleBake layer
source_layer = obj.data.uv_layers.active
if "SimpleBake" in obj.data.uv_layers:
obj.data.uv_layers.remove(obj.data.uv_layers["SimpleBake"])
new_layer = obj.data.uv_layers.new(name="SimpleBake")
if source_layer:
for i, item in enumerate(source_layer.data):
new_layer.data[i].uv = item.uv.copy()
new_layer.active = True
select_only_this(context, obj)
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
bpy.ops.mesh.reveal()
# Get enabled materials — strip SBM_/SBW_ prefix for comparison
enabled_mat_names = {
e.mat_name for e in sbp.mat_bake_list
if e.obj_name == obj.name and e.enabled
}
i = 0
for slot in obj.material_slots:
if slot.material is None:
i += 1
continue
mat_name = slot.material.name
if mat_name.startswith("SBW_"): mat_name = mat_name[4:]
if mat_name.startswith("SBM_"): mat_name = mat_name[4:]
if enabled_mat_names and mat_name not in enabled_mat_names:
i += 1
continue
obj.active_material_index = i
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.material_slot_select()
bpy.ops.uv.pack_islands(scale=True, margin=self.island_margin)
i += 1
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
return
def check_prefer_sb_maps(self, context):
if self.prefer_sb_uv_maps:
print_message(context, "Preferring existing UV maps called SimpleBake. Setting them to active")
for obj in self.bake_objects:
if("SimpleBake" in obj.data.uv_layers):
obj.data.uv_layers["SimpleBake"].active = True
return
def execute(self, context):
sbp = context.scene.SimpleBake_Props
#Pull what we need from the panel
self.island_margin = sbp.unwrapmargin
self.uvcorrectaspect = sbp.uvcorrectaspect
self.angle_limit = sbp.angle_limit
self.area_weight = sbp.area_weight
self.scale_to_bounds = sbp.scale_to_bounds
self.average_islands = sbp.average_uv_size
self.prefer_sb_uv_maps = sbp.prefer_existing_sbmap
self.new_uv_option = sbp.new_uv_option
self.new_uv_method = sbp.new_uv_method
self.expand_mat_uvs = sbp.expand_mat_uvs
self.uvpackmargin = sbp.uvpackmargin
#Set the bake objects
if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch":
from .bake_operators.auto_match_operators import SimpleBake_OT_AutoMatch_Operation as bo
hl_matches = bo.hl_matches
self.bake_objects = [context.scene.objects[name] for name in hl_matches.keys()]
elif sbp.selected_s2a: #Covering decals or standard
self.bake_objects =[sbp.targetobj]
elif sbp.cycles_s2a:
self.bake_objects =[sbp.targetobj_cycles]
else:
self.bake_objects = [i.obj_point for i in sbp.objects_list]
#Decide what we are actually doing
if sbp.tex_per_mat and self.expand_mat_uvs:
self.per_mat_pack_uvs_to_bounds(context)
elif self.new_uv_option and len(self.bake_objects)==1:
self.smart_project_individual_objects(context)
elif self.new_uv_option and self.new_uv_method == SBConstants.NEWUV_SMART_INDIVIDUAL:
self.smart_project_individual_objects(context)
elif self.new_uv_option and self.new_uv_method == SBConstants.NEWUV_SMART_ATLAS:
self.smart_project_to_atlas(context)
elif self.new_uv_option and self.new_uv_method == SBConstants.NEWUV_COMBINE_EXISTING:
self.combine_existing_to_atlas(context)
elif not self.new_uv_option:
print_message(context, "We are working with existing UV maps")
self.check_prefer_sb_maps(context)
else:
print_message(context, "Something went wrong processing UVs")
for obj in self.bake_objects:
obj["SB_uv_used_for_bake"] = obj.data.uv_layers.active.name
if self.new_uv_option and sbp.move_new_uvs_to_top:
rearrange_uvs(obj, obj["SB_uv_used_for_bake"])
return {'FINISHED'}
classes = ([
SimpleBake_OT_Process_UVs#,
#SimpleBake_OT_Pack_UVs
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)