Files
2026-03-17 14:58:51 -06:00

588 lines
24 KiB
Python

import bpy
from bpy.types import ObjectShaderFx
import mathutils
from .. Functions import node_functions
from .. Functions import image_functions
from .. Functions import constants
from .. Functions import visibility_functions
from .. Functions import material_functions
blender_version = bpy.app.version
class BakeUtilities():
C = bpy.context
D = bpy.data
O = bpy.ops
all_materials = None
image_texture_nodes = None
bake_settings = None
bake_image = None
render_engine = None
selected_objects = None
image_size = None
parent_operator = None
tex_node_name = None
def __init__(self,parent_operator,selected_objects, bake_settings):
self.C = bpy.context
self.D = bpy.data
self.parent_operator = parent_operator
self.render_engine = self.C.scene.render.engine
self.selected_objects = selected_objects
self.all_materials = self.D.materials
self.selected_materials = material_functions.get_selected_materials(self.selected_objects)
self.bake_settings = bake_settings
self.baked_images = []
self.image_texture_nodes = set()
self.image_size = [int(self.C.scene.img_bake_size),
int(self.C.scene.img_bake_size)]
image_name = bake_settings.bake_image_name
self.bake_image = image_functions.create_image(image_name, self.image_size)
def setup_engine(self):
# setup engine
if self.render_engine == 'BLENDER_EEVEE':
self.C.scene.render.engine = 'CYCLES'
# setup device type
self.cycles_device_type = self.C.preferences.addons['cycles'].preferences.compute_device_type
if self.cycles_device_type == 'OPTIX':
self.C.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
# setup samples
if self.bake_settings.pbr_bake:
self.C.scene.cycles.samples = self.bake_settings.pbr_samples
if self.bake_settings.lightmap_bake:
self.C.scene.cycles.samples = self.bake_settings.lightmap_samples
if self.bake_settings.ao_bake:
self.C.scene.cycles.samples = self.bake_settings.ao_samples
self.C.scene.render.resolution_percentage = 100
def set_active_uv_to_lightmap(self):
bpy.ops.object.set_active_uv(uv_slot=2)
def checkPBR(self):
for material in self.selected_materials:
self.active_material = material
# check if pbr node exists
check_ok = node_functions.check_pbr(self.parent_operator,material)
if not check_ok :
self.parent_operator.report({'INFO'}, "Material " + material.name + " has no PBR Node !")
return check_ok
def unwrap_selected(self):
if self.bake_settings.unwrap:
self.O.object.add_uv(uv_name=self.bake_settings.uv_name)
# apply scale on linked
sel_objects = self.C.selected_objects
scene_objects = self.D.objects
linked_objects = set()
for sel_obj in sel_objects:
for scene_obj in scene_objects:
if sel_obj.data.original is scene_obj.data and sel_obj is not scene_obj:
linked_objects.add(sel_obj)
# do not apply transform if linked objects in selection
if not len(linked_objects)>0:
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
self.O.object.mode_set(mode='EDIT')
self.O.mesh.reveal()
self.O.mesh.select_all(action='SELECT')
self.O.uv.smart_project(island_margin=self.bake_settings.unwrap_margin)
self.O.object.mode_set(mode='OBJECT')
def create_bake_material(self,material_name_suffix):
bake_materials = []
selected_materials = []
for obj in self.selected_objects:
for slot in obj.material_slots:
selected_materials.append(slot.material)
# switch to ao material if we are on org and ao was already baked
visibility_functions.switch_baked_material(True,"scene")
for obj in self.selected_objects:
for slot in obj.material_slots:
material = slot.material
bake_material_name = material.name + material_name_suffix
# check if material was already baked and continue
if material_name_suffix in material.name:
bake_materials.append(material)
continue
# if not, copy material or take one out of the previewsly filled bake list
else :
bake_material = list(filter(lambda material: material.name == bake_material_name, bake_materials))
if len(bake_material) == 0:
bake_material = material.copy()
bake_material.name = bake_material_name
bake_materials.append(bake_material)
slot.material = bake_material
else:
bake_material = bake_material[0]
slot.material = bake_material
index = bake_material.name.find(".")
if index == -1:
obj.bake_version = ""
else:
obj.bake_version = bake_material.name[index:]
material.use_fake_user = True
# remove duplicate entries
self.selected_materials = list(set(bake_materials))
def add_gltf_material_output_node(self, material):
nodes = material.node_tree.nodes
name = "glTF Material Output"
gltf_node_group = bpy.data.node_groups.new(name, 'ShaderNodeTree')
gltf_node_group.inputs.new("NodeSocketFloat", "Occlusion")
thicknessFactor = gltf_node_group.inputs.new("NodeSocketFloat", "Thickness")
thicknessFactor.default_value = 0.0
gltf_node_group.nodes.new('NodeGroupOutput')
gltf_node_group_input = gltf_node_group.nodes.new('NodeGroupInput')
specular = gltf_node_group.inputs.new("NodeSocketFloat", "Specular")
specular.default_value = 1.0
specularColor = gltf_node_group.inputs.new("NodeSocketColor", "Specular Color")
specularColor.default_value = [1.0,1.0,1.0,1.0]
gltf_node_group_input.location = -200, 0
gltf_settings_node = nodes.get(name)
if gltf_settings_node is None:
gltf_settings_node = nodes.new('ShaderNodeGroup')
gltf_settings_node.name = name
gltf_settings_node.node_tree = bpy.data.node_groups[name]
return gltf_settings_node
def add_gltf_settings_node(self, material):
nodes = material.node_tree.nodes
# create group data
gltf_settings = bpy.data.node_groups.get('glTF Settings')
if gltf_settings is None:
bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree')
# add group to node tree
gltf_settings_node = nodes.get('glTF Settings')
if gltf_settings_node is None:
gltf_settings_node = nodes.new('ShaderNodeGroup')
gltf_settings_node.name = 'glTF Settings'
gltf_settings_node.node_tree = bpy.data.node_groups['glTF Settings']
# create group inputs
if gltf_settings_node.inputs.get('Occlusion') is None:
gltf_settings_node.inputs.new('NodeSocketFloat','Occlusion')
return gltf_settings_node
def add_image_texture_node(self, material):
nodes = material.node_tree.nodes
# add image texture
if self.bake_settings.lightmap_bake:
self.tex_node_name = self.bake_settings.texture_node_lightmap
if self.bake_settings.ao_bake:
self.tex_node_name = self.bake_settings.texture_node_ao
image_texture_node = node_functions.add_node(material, constants.Shader_Node_Types.image_texture, self.tex_node_name)
image_texture_node.image = self.bake_image
self.bake_image.colorspace_settings.name = "Linear FilmLight E-Gamut"
nodes.active = image_texture_node
# save texture nodes and pbr nodes for later
self.image_texture_nodes.add(image_texture_node)
return image_texture_node
def save_metal_value(self):
for material in self.selected_materials:
pbr_node = node_functions.get_pbr_node(material)
# save metal value
metallic_value = pbr_node.inputs["Metallic"].default_value
pbr_node["original_metallic"] = metallic_value
pbr_node.inputs["Metallic"].default_value = 0
# save metal image
if pbr_node.inputs["Metallic"].is_linked:
# get metal image node, save it in pbr node and remove connection
metal_image_node_socket = pbr_node.inputs["Metallic"].links[0].from_socket
self.metal_image_node_output = metal_image_node_socket
node_functions.remove_link(material,metal_image_node_socket,pbr_node.inputs["Metallic"])
def load_metal_value(self):
for material in self.selected_materials:
pbr_node = node_functions.get_pbr_node(material)
pbr_node.inputs["Metallic"].default_value = pbr_node["original_metallic"]
# reconnect metal image
if hasattr(self,"metal_image_node_output"):
node_functions.make_link(material,self.metal_image_node_output,pbr_node.inputs["Metallic"])
def add_uv_node(self,material):
uv_node = node_functions.add_node(material, constants.Shader_Node_Types.uv, "Second_UV")
uv_node.uv_map = self.bake_settings.uv_name
return uv_node
def position_gltf_setup_nodes(self,material,uv_node,image_texture_node,gltf_settings_node):
nodes = material.node_tree.nodes
# uv node
pbr_node = node_functions.get_pbr_node(material)
pos_offset = mathutils.Vector((-900, 400))
loc = pbr_node.location + pos_offset
uv_node.location = loc
# image texture
loc = loc + mathutils.Vector((300, 0))
image_texture_node.location = loc
# ao node
loc = loc + mathutils.Vector((300, 0))
gltf_settings_node.location = loc
nodes.active = image_texture_node
def add_node_setup(self):
for material in self.selected_materials:
# AO
if self.bake_settings.ao_bake:
uv_node = self.add_uv_node(material)
image_texture_node = self.add_image_texture_node(material)
if blender_version <= (3, 3):
gltf_settings_node = self.add_gltf_settings_node(material)
else:
gltf_settings_node = self.add_gltf_material_output_node(material)
# position
self.position_gltf_setup_nodes(material,uv_node,image_texture_node,gltf_settings_node)
# linking
node_functions.make_link(material, uv_node.outputs["UV"],image_texture_node.inputs['Vector'])
node_functions.make_link(material, image_texture_node.outputs['Color'], gltf_settings_node.inputs['Occlusion'])
# LIGHTMAP
if self.bake_settings.lightmap_bake:
image_texture_node = self.add_image_texture_node(material)
uv_node = self.add_uv_node(material)
# position
image_texture_node.location = mathutils.Vector((-500, 200))
uv_node.location = mathutils.Vector((-700, 200))
# linking
node_functions.make_link(material, uv_node.outputs["UV"],image_texture_node.inputs['Vector'])
def bake(self,bake_type):
channels_to_bake = bake_type
self.baked_images = []
denoise = self.bake_settings.denoise
# no denoise
if not denoise:
channel = bake_type[0]
image = self.bake_images(self.bake_image,channel,denoise)
image_functions.save_image(image,False)
return
# bake channels for denoise
for channel in channels_to_bake:
image_name = self.bake_image.name + "_" + channel
image = image_functions.create_image(image_name,self.bake_image.size)
self.change_image_in_nodes(image)
baked_channel_image = self.bake_images(image,channel,denoise)
image_functions.save_image(image,True)
self.baked_images.append(baked_channel_image)
self.denoise()
def change_image_in_nodes(self,image):
for image_texture_node in self.image_texture_nodes:
if image_texture_node.name == self.tex_node_name:
image_texture_node.image = image
def bake_images(self, image, channel,denoise):
if channel == "NRM":
print("Baking Normal Pass")
self.C.scene.cycles.samples = 1
self.O.object.bake(type="NORMAL", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
if channel == "COLOR":
print("Baking Color Pass")
self.C.scene.cycles.samples = 1
self.O.object.bake(type="DIFFUSE", pass_filter={'COLOR'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
if channel == "AO":
if not denoise:
self.O.object.bake('INVOKE_DEFAULT',type="AO", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
else:
self.O.object.bake(type="AO", use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
if channel == "NOISY":
print("Baking Diffuse Pass")
if not denoise:
self.O.object.bake('INVOKE_DEFAULT',type="DIFFUSE", pass_filter={'DIRECT', 'INDIRECT'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
else:
self.O.object.bake(type="DIFFUSE", pass_filter={'DIRECT', 'INDIRECT'}, use_clear=self.bake_settings.bake_image_clear, margin=self.bake_settings.bake_margin)
return image
def denoise(self):
# denoise
if self.bake_settings.lightmap_bake:
denoised_image_path = node_functions.comp_ai_denoise(self.baked_images[0],self.baked_images[1],self.baked_images[2])
self.bake_image.filepath = denoised_image_path
self.bake_image.source = "FILE"
self.change_image_in_nodes(self.bake_image)
# blur
if self.bake_settings.ao_bake and self.bake_settings.denoise:
blur_image_path = node_functions.blur_bake_image(self.baked_images[0],self.baked_images[1])
self.bake_image.filepath = blur_image_path
self.bake_image.source = "FILE"
self.change_image_in_nodes(self.bake_image)
def add_lightmap_flag(self):
for obj in self.selected_objects:
obj.hasLightmap = True
def cleanup(self):
# set back engine
# self.C.scene.render.engine = self.render_engine
self.C.preferences.addons['cycles'].preferences.compute_device_type = self.cycles_device_type
# cleanup images
if self.bake_settings.cleanup_textures:
for img in self.D.images:
if self.bake_image.name in img.name and ("_COLOR" in img.name or "_NRM" in img.name or "_NOISY" in img.name) :
self.D.images.remove(img)
# show image
visibility_functions.show_image_in_image_editor(self.bake_image)
class PbrBakeUtilities(BakeUtilities):
active_material = None
parent_operator = None
def __init__(self,parent_operator,selected_objects, bake_settings):
super().__init__(parent_operator,selected_objects,bake_settings)
self.selected_materials = material_functions.get_selected_materials(selected_objects)
self.parent_operator = parent_operator
def ready_for_bake(self,material):
# check if not baked material
if "_Bake" in material.name:
print("Skipping cause already baked : " + material.name)
return False
print("\n Checking " + material.name + "\n")
# check if renderer not set to optix
self.setup_engine()
# check if selected to active is on
bpy.context.scene.render.bake.use_selected_to_active = False
# check if pbr node exists
check_ok = node_functions.check_pbr(self.parent_operator,material) and node_functions.check_is_org_material(self.parent_operator,material)
if not check_ok :
self.parent_operator.report({'INFO'}, "Material " + material.name + " has no PBR Node !")
return False
# copy texture nodes if they are linked multiple times
nodes = material.node_tree.nodes
image_textrure_nodes = node_functions.get_nodes_by_type(nodes,constants.Node_Types.image_texture)
for image_texture_node in image_textrure_nodes:
node_functions.remove_double_linking(material,image_texture_node)
return True
def bake_materials_on_object(self):
for material in self.selected_materials:
self.active_material = material
if not (self.ready_for_bake(material)):
continue
self.add_bake_plane()
self.bake_pbr()
self.create_pbr_bake_material("_Bake")
self.create_nodes_after_pbr_bake()
self.cleanup_nodes()
visibility_functions.switch_baked_material(True,"visible")
def add_bake_plane(self):
material = self.active_material
bake_plane = self.D.objects.get(material.name + "_Bake")
if bake_plane is not None:
self.parent_operator.report({'INFO'}, 'Delete Bake Plane')
return
self.O.mesh.primitive_plane_add(size=2, location=(2, 0, 0))
bake_plane = self.C.object
bake_plane.name = material.name + "_Bake"
bake_plane.data.materials.append(material)
def bake_pbr(self):
material = self.active_material
material.use_fake_user = True
nodes = material.node_tree.nodes
pbr_node = node_functions.get_pbr_node(material)
pbr_inputs = node_functions.get_pbr_inputs(pbr_node)
image_texture_node = None
# mute texture mapping
if self.bake_settings.mute_texture_nodes:
node_functions.mute_all_texture_mappings(material, True)
for pbr_input in pbr_inputs.values():
# -----------------------TESTING--------------------#
# skip if input has no connection
if not pbr_input.is_linked:
continue
# -----------------------IMAGE --------------------#
image_name = material.name + "_" + pbr_input.name
# find image
bake_image = self.D.images.get(image_name)
# remove image
if bake_image is not None:
self.D.images.remove(bake_image)
bake_image = self.D.images.new(image_name, width=self.image_size[0], height=self.image_size[1])
bake_image.name = image_name
image_texture_node = node_functions.add_node(material,constants.Shader_Node_Types.image_texture,"PBR Bake")
image_texture_node.image = bake_image
nodes.active = image_texture_node
# -----------------------SET COLOR SPACE--------------------#
if pbr_input is not pbr_inputs["base_color_input"]:
bake_image.colorspace_settings.name = "Non-Color"
# -----------------------BAKING--------------------#
if pbr_input is pbr_inputs["normal_input"]:
node_functions.link_pbr_to_output(material, pbr_node)
self.O.object.bake(type="NORMAL", use_clear=True)
else:
node_functions.emission_setup(material, pbr_input.links[0].from_socket)
self.O.object.bake(type="EMIT", use_clear=True)
# unmute texture mappings
node_functions.mute_all_texture_mappings(material, False)
# delete plane
self.O.object.delete()
# cleanup nodes
node_functions.remove_node(material,"Emission Bake")
node_functions.remove_node(material,"PBR Bake")
node_functions.reconnect_PBR(material, pbr_node)
def create_pbr_bake_material(self,material_name_suffix):
# -----------------------CREATE MATERIAL--------------------#
org_material = self.active_material
bake_material_name = org_material.name + material_name_suffix
bake_material = bpy.data.materials.get(bake_material_name)
if bake_material is not None:
bpy.data.materials.remove(bake_material)
# and create new from org. material
bake_material = org_material.copy()
bake_material.name = bake_material_name
self.bake_material = bake_material
def create_nodes_after_pbr_bake(self):
# -----------------------SETUP VARS--------------------#
org_material = self.active_material
bake_material = self.bake_material
nodes = bake_material.node_tree.nodes
pbr_node = node_functions.get_pbr_node(bake_material)
pbr_inputs = node_functions.get_pbr_inputs(pbr_node)
for pbr_input in pbr_inputs.values():
if not pbr_input.is_linked:
continue
# -----------------------REPLACE IMAGE TEXTURES--------------------#
first_node_after_input = pbr_input.links[0].from_node
tex_node = node_functions.get_node_by_type_recusivly(bake_material,first_node_after_input,constants.Node_Types.image_texture,True)
bake_image_name = org_material.name + "_" + pbr_input.name
bake_image = self.D.images.get(bake_image_name)
# if no texture node found (baking procedural textures) add new one
if tex_node is None:
tex_node = node_functions.add_node(bake_material,constants.Shader_Node_Types.image_texture,bake_image_name)
# keep org image if nothing changed
if bake_image is None:
org_image = self.D.images.get(tex_node.image.org_image_name)
tex_node.image = org_image
else:
image_functions.save_image(bake_image)
tex_node.image = bake_image
# -----------------------LINKING--------------------#
if pbr_input is pbr_inputs["normal_input"]:
normal_node = first_node_after_input
normal_node.inputs["Strength"].default_value = 1
if normal_node.type == constants.Node_Types.bump_map:
bump_node = normal_node
normal_node = node_functions.add_node(bake_material,constants.Shader_Node_Types.normal,"Normal from Bump")
normal_node.location = bump_node.location
nodes.remove(bump_node)
node_functions.make_link(bake_material, tex_node.outputs[0], normal_node.inputs["Color"])
node_functions.make_link(bake_material, normal_node.outputs["Normal"], pbr_input)
else:
node_functions.make_link(bake_material,tex_node.outputs[0], pbr_input)
# -----------------------SET COLOR SPACE--------------------#
if pbr_input is not pbr_inputs["base_color_input"]:
tex_node.image.colorspace_settings.name = "Non-Color"
self.active_material = bake_material
return bake_material
def cleanup_nodes(self):
bake_material = self.active_material
node_functions.remove_unused_nodes(bake_material)