disable (currently unused) addons, update
This commit is contained in:
@@ -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,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
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
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
+183
-70
@@ -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 = {}
|
||||
@@ -129,22 +150,17 @@ def RBST_DatRem_register_properties():
|
||||
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")
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -206,26 +205,27 @@ def tempctrl_shape_type(self, context):
|
||||
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,7 +1098,12 @@ 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'''
|
||||
@@ -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:
|
||||
@@ -1407,6 +1408,16 @@ def get_bone_layers_actions(bone, actions):
|
||||
|
||||
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):
|
||||
|
||||
scene = context.scene
|
||||
@@ -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]
|
||||
# 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
|
||||
|
||||
fcurves = Tools.get_fcurves(ctrl_rig, action)
|
||||
enable_fcu = fcurves.find('animtoolbox.ctrls_enabled')
|
||||
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])))
|
||||
# Getting all the frames where it's turned on and off
|
||||
frames_on = frames[values == 1]
|
||||
frames_off = frames[values == 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])))
|
||||
# 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 values[-1] == 0:
|
||||
frames_remove += list(range(round(frames[-1]), round(action_frame_range[1])))
|
||||
# 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)
|
||||
@@ -1927,8 +1960,19 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
|
||||
|
||||
# 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,6 +1981,7 @@ 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)
|
||||
@@ -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,7 +2300,7 @@ 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)
|
||||
@@ -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)
|
||||
@@ -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'):
|
||||
# if not hasattr(chain, 'pole_bone'):
|
||||
# continue
|
||||
if not chain.pole_bone:
|
||||
continue
|
||||
|
||||
del chain.pole_bone
|
||||
if hasattr(chain,'pole_bone_name'):
|
||||
chain.pole_bone_name
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -5825,7 +5881,6 @@ 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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -574,6 +574,11 @@ 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:
|
||||
@@ -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)
|
||||
@@ -640,6 +645,10 @@ def update_cursor_matrix(context):
|
||||
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,7 +691,10 @@ 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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -205,6 +205,11 @@ class AnimToolBoxObjectSettings(bpy.types.PropertyGroup):
|
||||
|
||||
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,6 +498,7 @@ 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'})
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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,8 +2028,13 @@ 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:
|
||||
@@ -2238,9 +2243,6 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
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:
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_motionpath_callback_px, (self, context,), 'WINDOW', 'POST_VIEW')
|
||||
@@ -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'}
|
||||
|
||||
@@ -2280,6 +2285,11 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
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)
|
||||
return {'PASS_THROUGH'}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user