import bpy.ops as O import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh from io import StringIO from threading import Thread from queue import Queue, Empty from dataclasses import dataclass from dataclasses import field from typing import List ########################################################### ########################################################### # This set of utility functions are courtesy of LorenzWieseke # # Modified by Naxela # # https://github.com/Naxela/The_Lightmapper/tree/Lightmap-to-GLB ########################################################### class Node_Types: output_node = 'OUTPUT_MATERIAL' ao_node = 'AMBIENT_OCCLUSION' image_texture = 'TEX_IMAGE' pbr_node = 'BSDF_PRINCIPLED' diffuse = 'BSDF_DIFFUSE' mapping = 'MAPPING' normal_map = 'NORMAL_MAP' bump_map = 'BUMP' attr_node = 'ATTRIBUTE' class Shader_Node_Types: emission = "ShaderNodeEmission" image_texture = "ShaderNodeTexImage" mapping = "ShaderNodeMapping" normal = "ShaderNodeNormalMap" ao = "ShaderNodeAmbientOcclusion" uv = "ShaderNodeUVMap" mix = "ShaderNodeMixRGB" def select_object(self,obj): C = bpy.context try: O.object.select_all(action='DESELECT') C.view_layer.objects.active = obj obj.select_set(True) except: self.report({'INFO'},"Object not in View Layer") def select_obj_by_mat(self,mat): D = bpy.data for obj in D.objects: if obj.type == "MESH": object_materials = [ slot.material for slot in obj.material_slots] if mat in object_materials: select_object(self,obj) def save_image(image): filePath = bpy.data.filepath path = os.path.dirname(filePath) try: os.mkdir(path + "/tex") except FileExistsError: pass try: os.mkdir(path + "/tex/" + str(image.size[0])) except FileExistsError: pass if image.file_format == "JPEG" : file_ending = ".jpg" elif image.file_format == "PNG" : file_ending = ".png" savepath = path + "/tex/" + \ str(image.size[0]) + "/" + image.name + file_ending image.filepath_raw = savepath # if "Normal" in image.name: # bpy.context.scene.render.image_settings.quality = 90 # image.save_render( filepath = image.filepath_raw, scene = bpy.context.scene ) # else: image.save() def get_file_size(filepath): size = "Unpack Files" try: path = bpy.path.abspath(filepath) size = os.path.getsize(path) size /= 1024 except: print("error getting file path for " + filepath) return (size) def scale_image(image, newSize): if (image.org_filepath != ''): image.filepath = image.org_filepath image.org_filepath = image.filepath image.scale(newSize[0], newSize[1]) save_image(image) def check_only_one_pbr(self,material): check_ok = True # get pbr shader nodes = material.node_tree.nodes pbr_node_type = Node_Types.pbr_node pbr_nodes = find_node_by_type(nodes,pbr_node_type) # check only one pbr node if len(pbr_nodes) == 0: self.report({'INFO'}, 'No PBR Shader Found') check_ok = False if len(pbr_nodes) > 1: self.report({'INFO'}, 'More than one PBR Node found ! Clean before Baking.') check_ok = False return check_ok # is material already the baked one def check_is_org_material(self,material): check_ok = True if "_Bake" in material.name: self.report({'INFO'}, 'Change back to org. Material') check_ok = False return check_ok def clean_empty_materials(self): for obj in bpy.data.objects: for slot in obj.material_slots: mat = slot.material if mat is None: print("Removed Empty Materials from " + obj.name) bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.ops.object.material_slot_remove() def get_pbr_inputs(pbr_node): base_color_input = pbr_node.inputs["Base Color"] metallic_input = pbr_node.inputs["Metallic"] specular_input = pbr_node.inputs["Specular"] roughness_input = pbr_node.inputs["Roughness"] normal_input = pbr_node.inputs["Normal"] pbr_inputs = {"base_color_input":base_color_input, "metallic_input":metallic_input,"specular_input":specular_input,"roughness_input":roughness_input,"normal_input":normal_input} return pbr_inputs def find_node_by_type(nodes, node_type): nodes_found = [n for n in nodes if n.type == node_type] return nodes_found def find_node_by_type_recusivly(material, note_to_start, node_type, del_nodes_inbetween=False): nodes = material.node_tree.nodes if note_to_start.type == node_type: return note_to_start for input in note_to_start.inputs: for link in input.links: current_node = link.from_node if (del_nodes_inbetween and note_to_start.type != Node_Types.normal_map and note_to_start.type != Node_Types.bump_map): nodes.remove(note_to_start) return find_node_by_type_recusivly(material, current_node, node_type, del_nodes_inbetween) def find_node_by_name_recusivly(node, idname): if node.bl_idname == idname: return node for input in node.inputs: for link in input.links: current_node = link.from_node return find_node_by_name_recusivly(current_node, idname) def make_link(material, socket1, socket2): links = material.node_tree.links links.new(socket1, socket2) def add_gamma_node(material, pbrInput): nodeToPrincipledOutput = pbrInput.links[0].from_socket gammaNode = material.node_tree.nodes.new("ShaderNodeGamma") gammaNode.inputs[1].default_value = 2.2 gammaNode.name = "Gamma Bake" # link in gamma make_link(material, nodeToPrincipledOutput, gammaNode.inputs["Color"]) make_link(material, gammaNode.outputs["Color"], pbrInput) def remove_gamma_node(material, pbrInput): nodes = material.node_tree.nodes gammaNode = nodes.get("Gamma Bake") nodeToPrincipledOutput = gammaNode.inputs[0].links[0].from_socket make_link(material, nodeToPrincipledOutput, pbrInput) material.node_tree.nodes.remove(gammaNode) def apply_ao_toggle(self,context): all_materials = bpy.data.materials ao_toggle = context.scene.toggle_ao for mat in all_materials: nodes = mat.node_tree.nodes ao_node = nodes.get("AO Bake") if ao_node is not None: if ao_toggle: emission_setup(mat,ao_node.outputs["Color"]) else: pbr_node = find_node_by_type(nodes,Node_Types.pbr_node)[0] remove_node(mat,"Emission Bake") reconnect_PBR(mat, pbr_node) def emission_setup(material, node_output): nodes = material.node_tree.nodes emission_node = add_node(material,Shader_Node_Types.emission,"Emission Bake") # link emission to whatever goes into current pbrInput emission_input = emission_node.inputs[0] make_link(material, node_output, emission_input) # link emission to materialOutput surface_input = nodes.get("Material Output").inputs[0] emission_output = emission_node.outputs[0] make_link(material, emission_output, surface_input) def link_pbr_to_output(material,pbr_node): nodes = material.node_tree.nodes surface_input = nodes.get("Material Output").inputs[0] make_link(material,pbr_node.outputs[0],surface_input) def reconnect_PBR(material, pbrNode): nodes = material.node_tree.nodes pbr_output = pbrNode.outputs[0] surface_input = nodes.get("Material Output").inputs[0] make_link(material, pbr_output, surface_input) def mute_all_texture_mappings(material, do_mute): nodes = material.node_tree.nodes for node in nodes: if node.bl_idname == "ShaderNodeMapping": node.mute = do_mute def add_node(material,shader_node_type,node_name): nodes = material.node_tree.nodes new_node = nodes.get(node_name) if new_node is None: new_node = nodes.new(shader_node_type) new_node.name = node_name new_node.label = node_name return new_node def remove_node(material,node_name): nodes = material.node_tree.nodes node = nodes.get(node_name) if node is not None: nodes.remove(node) def lightmap_to_ao(material,lightmap_node): nodes = material.node_tree.nodes # -----------------------AO SETUP--------------------# # 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 ao_group = nodes.get('glTF Settings') if ao_group is None: ao_group = nodes.new('ShaderNodeGroup') ao_group.name = 'glTF Settings' ao_group.node_tree = bpy.data.node_groups['glTF Settings'] # create group inputs if ao_group.inputs.get('Occlusion') is None: ao_group.inputs.new('NodeSocketFloat','Occlusion') # mulitply to control strength mix_node = add_node(material,Shader_Node_Types.mix,"Adjust Lightmap") mix_node.blend_type = "MULTIPLY" mix_node.inputs["Fac"].default_value = 1 mix_node.inputs["Color2"].default_value = [3,3,3,1] # position node ao_group.location = (lightmap_node.location[0]+600,lightmap_node.location[1]) mix_node.location = (lightmap_node.location[0]+300,lightmap_node.location[1]) make_link(material,lightmap_node.outputs['Color'],mix_node.inputs['Color1']) make_link(material,mix_node.outputs['Color'],ao_group.inputs['Occlusion']) ########################################################### ########################################################### # This utility function is modified from blender_xatlas # and calls the object without any explicit object context # thus allowing blender_xatlas to pack from background. ########################################################### # Code is courtesy of mattedicksoncom # Modified by Naxela # # https://github.com/mattedicksoncom/blender-xatlas/ ########################################################### def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): blender_xatlas = importlib.util.find_spec("blender_xatlas") if blender_xatlas is not None: import blender_xatlas else: return 0 packOptions = bpy.context.scene.pack_tool chartOptions = bpy.context.scene.chart_tool sharedProperties = bpy.context.scene.shared_properties context = bpy.context if obj.type == 'MESH': context.view_layer.objects.active = obj if obj.data.users > 1: obj.data = obj.data.copy() #make single user copy uv_layers = obj.data.uv_layers #setup the lightmap uvs uvName = "UVMap_Lightmap" if sharedProperties.lightmapUVChoiceType == "NAME": uvName = sharedProperties.lightmapUVName elif sharedProperties.lightmapUVChoiceType == "INDEX": if sharedProperties.lightmapUVIndex < len(uv_layers): uvName = uv_layers[sharedProperties.lightmapUVIndex].name if not uvName in uv_layers: uvmap = uv_layers.new(name=uvName) uv_layers.active_index = len(uv_layers) - 1 else: for i in range(0, len(uv_layers)): if uv_layers[i].name == uvName: uv_layers.active_index = i obj.select_set(True) #save all the current edges if sharedProperties.packOnly: edgeDict = dict() for obj in selected_objects: if obj.type == 'MESH': tempEdgeDict = dict() tempEdgeDict['object'] = obj.name tempEdgeDict['edges'] = [] print(len(obj.data.edges)) for i in range(0,len(obj.data.edges)): setEdge = obj.data.edges[i] tempEdgeDict['edges'].append(i) edgeDict[obj.name] = tempEdgeDict bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') else: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') bpy.ops.object.mode_set(mode='OBJECT') fakeFile = StringIO() blender_xatlas.export_obj_simple.save( context=bpy.context, filepath=fakeFile, mainUVChoiceType=sharedProperties.mainUVChoiceType, uvIndex=sharedProperties.mainUVIndex, uvName=sharedProperties.mainUVName, use_selection=True, use_animation=False, use_mesh_modifiers=True, use_edges=True, use_smooth_groups=False, use_smooth_groups_bitflags=False, use_normals=True, use_uvs=True, use_materials=False, use_triangles=False, use_nurbs=False, use_vertex_groups=False, use_blen_objects=True, group_by_object=False, group_by_material=False, keep_vertex_order=False, ) file_path = os.path.dirname(os.path.abspath(blender_xatlas.__file__)) if platform.system() == "Windows": xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe") elif platform.system() == "Linux": xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender") #need to set permissions for the process on linux subprocess.Popen( 'chmod u+x "' + xatlas_path + '"', shell=True ) #setup the arguments to be passed to xatlas------------------- arguments_string = "" for argumentKey in packOptions.__annotations__.keys(): key_string = str(argumentKey) if argumentKey is not None: print(getattr(packOptions,key_string)) attrib = getattr(packOptions,key_string) if type(attrib) == bool: if attrib == True: arguments_string = arguments_string + " -" + str(argumentKey) else: arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) for argumentKey in chartOptions.__annotations__.keys(): if argumentKey is not None: key_string = str(argumentKey) print(getattr(chartOptions,key_string)) attrib = getattr(chartOptions,key_string) if type(attrib) == bool: if attrib == True: arguments_string = arguments_string + " -" + str(argumentKey) else: arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) #add pack only option if sharedProperties.packOnly: arguments_string = arguments_string + " -packOnly" arguments_string = arguments_string + " -atlasLayout" + " " + sharedProperties.atlasLayout print(arguments_string) #END setup the arguments to be passed to xatlas------------------- #RUN xatlas process xatlas_process = subprocess.Popen( r'"{}"'.format(xatlas_path) + ' ' + arguments_string, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True ) #shove the fake file in stdin stdin = xatlas_process.stdin value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly stdin.write(value) stdin.flush() #Get the output from xatlas outObj = "" while True: output = xatlas_process.stdout.readline() if not output: break outObj = outObj + (output.decode().strip() + "\n") #the objects after xatlas processing # print(outObj) #Setup for reading the output @dataclass class uvObject: obName: string = "" uvArray: List[float] = field(default_factory=list) faceArray: List[int] = field(default_factory=list) convertedObjects = [] uvArrayComplete = [] #search through the out put for STARTOBJ #then start reading the objects obTest = None startRead = False for line in outObj.splitlines(): line_split = line.split() if not line_split: continue line_start = line_split[0] # we compare with this a _lot_ # print(line_start) if line_start == "STARTOBJ": print("Start reading the objects----------------------------------------") startRead = True # obTest = uvObject() if startRead: #if it's a new obj if line_start == 'o': #if there is already an object append it if obTest is not None: convertedObjects.append(obTest) obTest = uvObject() #create new uv object obTest.obName = line_split[1] if obTest is not None: #the uv coords if line_start == 'vt': newUv = [float(line_split[1]),float(line_split[2])] obTest.uvArray.append(newUv) uvArrayComplete.append(newUv) #the face coords index #faces are 1 indexed if line_start == 'f': #vert/uv/normal #only need the uvs newFace = [ int(line_split[1].split("/")[1]), int(line_split[2].split("/")[1]), int(line_split[3].split("/")[1]) ] obTest.faceArray.append(newFace) #append the final object convertedObjects.append(obTest) # print(convertedObjects) #apply the output------------------------------------------------------------- #copy the uvs to the original objects # objIndex = 0 print("Applying the UVs----------------------------------------") # print(convertedObjects) for importObject in convertedObjects: bpy.ops.object.select_all(action='DESELECT') obTest = importObject bpy.context.scene.objects[obTest.obName].select_set(True) context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName] bpy.ops.object.mode_set(mode = 'OBJECT') obj = bpy.context.active_object me = obj.data #convert to bmesh to create the new uvs bm = bmesh.new() bm.from_mesh(me) uv_layer = bm.loops.layers.uv.verify() nFaces = len(bm.faces) #need to ensure lookup table for some reason? if hasattr(bm.faces, "ensure_lookup_table"): bm.faces.ensure_lookup_table() #loop through the faces for faceIndex in range(nFaces): faceGroup = obTest.faceArray[faceIndex] bm.faces[faceIndex].loops[0][uv_layer].uv = ( uvArrayComplete[faceGroup[0] - 1][0], uvArrayComplete[faceGroup[0] - 1][1]) bm.faces[faceIndex].loops[1][uv_layer].uv = ( uvArrayComplete[faceGroup[1] - 1][0], uvArrayComplete[faceGroup[1] - 1][1]) bm.faces[faceIndex].loops[2][uv_layer].uv = ( uvArrayComplete[faceGroup[2] - 1][0], uvArrayComplete[faceGroup[2] - 1][1]) # objIndex = objIndex + 3 # print(objIndex) #assign the mesh back to the original mesh bm.to_mesh(me) #END apply the output------------------------------------------------------------- #Start setting the quads back again------------------------------------------------------------- if sharedProperties.packOnly: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') for edges in edgeDict: edgeList = edgeDict[edges] currentObject = bpy.context.scene.objects[edgeList['object']] bm = bmesh.new() bm.from_mesh(currentObject.data) if hasattr(bm.edges, "ensure_lookup_table"): bm.edges.ensure_lookup_table() #assume that all the triangulated edges come after the original edges newEdges = [] for edge in range(len(edgeList['edges']), len(bm.edges)): newEdge = bm.edges[edge] newEdge.select = True newEdges.append(newEdge) bmesh.ops.dissolve_edges(bm, edges=newEdges, use_verts=False, use_face_split=False) bpy.ops.object.mode_set(mode='OBJECT') bm.to_mesh(currentObject.data) bm.free() bpy.ops.object.mode_set(mode='EDIT') #End setting the quads back again------------------------------------------------------------ print("Finished Xatlas----------------------------------------")