495 lines
20 KiB
Python
495 lines
20 KiB
Python
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)
|