Source code for cgl.plugins.blender.utils

import glob
import logging
import os
import re
import stringcase

from cgl.core.path.object import PathObject
from cgl.core.utils import read_write

from cgl.core.utils.read_write import load_json, load_yaml

try:
    import bpy
except ModuleNotFoundError:
    pass

logger = logging.getLogger("qtutils")


[docs] def get_namespace(filepath): """ gets the namespace when creating a namespace Args: filepath: current: Returns: namespace """ namespace = "" path_object = PathObject().from_path_string(filepath) if path_object.task == "cam": namespace = "cam" elif path_object.entity_type == "assets": namespace = path_object.shot elif path_object.entity_type == "shots": namespace = "tempNS" return get_next_namespace(namespace)
[docs] def get_top_nodes(): """ Get the top nodes in blender that are not defaults """ ignore = ["Camera", "Cube", "Light"] top_nodes = [] for obj in bpy.context.scene.objects: if obj.name() not in ignore: top_nodes.append(obj) return top_nodes
[docs] def get_mesh_nodes_recursive(root_node): mesh_nodes = [obj for obj in bpy.data.objects if obj.type == "MESH"] return mesh_nodes
[docs] def get_selected_namespace(): """ Returns: namespace of selected object. """ logging.info("Getting Selected Namespace not implemented yet") return None
def create_shot_mask_info( shot_number=1, scene_name="default", frame_start=0, frame_end=240, notes="These are the default settings", ): # Ensure the Video Sequence Editor is available bpy.context.window.workspace = bpy.data.workspaces["Video Editing"] seq = bpy.context.scene.sequence_editor_create() # Create the shot info text shot_info = f"Shot: {shot_number}, Scene: {scene_name}, Frames: {frame_start}-{frame_end}, Notes: {notes}" # Add a text strip with the shot info text_strip = seq.sequences.new_effect( name="ShotInfo", type="TEXT", channel=2, frame_start=frame_start, frame_end=frame_end, ) text_strip.text = shot_info text_strip.location = 0.5, 0.9 # Adjust as needed (x, y) in normalized coordinates
[docs] def select_reference(collection_name): """ Selects all objects in the specified collection. Parameters: collection_name (str): The name of the collection to select objects from. """ # Deselect all objects first bpy.ops.object.select_all(action="DESELECT") # Check if the collection exists in the current scene if collection_name in bpy.data.collections: collection = bpy.data.collections[collection_name] for obj in collection.objects: # Make the object selectable and select obj.select_set(True) return [obj.name for obj in collection.objects] else: print(f"Collection '{collection_name}' not found.") return None
[docs] def get_ref_node(object_name): obj = bpy.data.objects.get(object_name) if obj is None: print(f"Object '{object_name}' not found in the current scene.") return None if obj.library: print(f"Object '{object_name}' is linked from: {obj.library.filepath}") return obj.library else: print(f"Object '{object_name}' is not linked from an external file.") return None
def get_next_namespace(base_name): pattern = r"\d+$" # Regex to find a sequence of digits at the end of a string highest_num = 0 exists = False # Iterate over all collections in the Blender file for coll in bpy.data.collections: if coll.name.startswith(base_name): exists = True # Try to find a numeric suffix in the collection name match = re.search(pattern, coll.name) if match: num = int(match.group()) highest_num = max(highest_num, num) if exists: # If the base name exists, append the next highest number return f"{base_name}{highest_num + 1}" else: # If the base name does not exist, return the base name return base_name
[docs] def get_shape_name(geo): logging.info("Getting Shape Name not implemented yet") return None
[docs] def create_tt(length, tt_object): """ Creates a turntable with frame range of 0-length, around the selected object. Args: length: tt_object: Returns: None """ logging.info("Creating Turntable not implemented yet") return None
[docs] def clean_tt(task=None): """ Cleans up the turntable by deleting the camera and group node. Args: task: Returns: None """ logging.info("Cleaning Turntable not implemented yet") return None
[docs] def get_current_camera(): """ Returns the name of the current camera in the scene. Returns: str: The name of the current camera. """ return bpy.context.scene.camera.name
[docs] def confirm_prompt(title="title", message="message", button="Ok"): """ standard confirm prompt, this is an easy wrapper that allows us to do confirm prompts in the native language of the application while keeping conventions Args: title: message: button: single button is created with a string, multiple buttons created with array """ logging.info("Confirm Prompt not implemented yet") return None
[docs] def load_plugin(plugin_name): """ Loads a plugin by name. Args: plugin_name: The name of the plugin to load. Returns: None """ logging.info("Load Plugin not implemented yet") return None
[docs] def get_playblast_path(path_object): playblast_file = path_object.copy( tree="render", filename="playblast", ext="" ).get_path() return playblast_file
[docs] def playblast_exists(path_object): playblast_file = get_playblast_path(path_object) playblast_files = glob.glob("{}*".format(playblast_file)) if playblast_files: return playblast_files else: return False
[docs] def review_exists(path_object): path_, ext = os.path.splitext(path_object.preview_path) logging.info(path_) files = glob.glob("{}*".format(path_)) if files: return files else: return False
[docs] def basic_playblast(path_object=None, appearance="smoothShaded", cam=None, audio=False): """ Creates a playblast of the scene. Args: path_object: appearance: cam: audio: Returns: None """ logging.info("Basic Playblast not implemented yet") return None
[docs] def create_thumb(path_object, camera=None): """ Creates a thumbnail of the scene. Args: path_object: camera: Returns: None """ logging.info("Create Thumb not implemented yet") return None
# # def create_still_preview(path_object, task='mdl'): # """ # creates a still preview, as is this is built for assets that have turntables. # :param task: # :return: # """ # import cgl.plugins.maya.tasks.lite as lite # import cgl.core.convert as convert # from mtoa.cmds.arnoldRender import arnoldRender # # tt_tasks = ['mdl', 'mld', 'rig'] # pm.select(task) # so = path_object # # lite.set_renderer() # lite.turn_off_env_bg() # lite.set_render_globals(gui=False) # lite.set_file_type('jpeg') # pm.setAttr("defaultArnoldDriver.pre", so.preview_path, type="string") # # if task in tt_tasks: # Scene().create_turntable() # create_env_light('Soft1Front2Backs') # # time.sleep(1) # # camera = pm.ls('turntable_cam*', type='transform')[0] # arnoldRender(1920, 1080, True, True, 'turntable_camera1', ' -layer defaultRenderLayer') # preview = glob.glob('{}*'.format(so.preview_path))[0] # os.rename(preview, so.preview_path) # convert.create_image_thumb(so.preview_path, so.thumb_path) # # so.update_test_project_msd(attr='preview_file') # # so.update_test_project_msd(attr='thumb_file') # if task in tt_tasks: # clean_tt() # pm.delete('env_light')
[docs] def get_hdri_yaml_path(): return f"{CFG.cookbook_dir}/hdri/hdri.yaml"
[docs] def get_hdri_config(): return load_yaml(get_hdri_yaml_path())
[docs] def get_hdri_root(): return get_hdri_yaml_path().split("\\CGL-HDRI")[0].replace("\\", "/")
[docs] def add_hdri_root(filepath): print(get_hdri_root(), filepath, 333) return "{}{}".format(get_hdri_root(), filepath)
[docs] def hdri_widget(): """ Creates a widget for selecting an HDRI map. Returns: None """ logging.info("HDRI Widget not implemented yet") return None
[docs] def create_env_light(tex_name): """ Creates an environment light with the specified texture. Args: tex_name: Returns: None """ logging.info("Create Env Light not implemented yet") return None
[docs] def get_selected_reference() -> list: """ This function returns a list of reference nodes based on the currently selected objects in the scene. Returns: list: a list of reference nodes that correspond to the selected objects in the scene. If no objects are selected, it returns None. If the selected objects do not belong to any references, it logs an error message and returns an empty list. """ selected_objects = bpy.context.selected_objects reference_nodes = [] if not selected_objects: logging.error("No objects selected.") return None for obj in selected_objects: if obj.library: reference_nodes.append(obj.library) else: logging.error(f"Object '{obj.name}' is not linked from an external file.") return reference_nodes
[docs] def default_matrix(): dm = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ] return dm
[docs] def get_matrix(obj, query=False): """ Returns a matrix of values relating to translate, scale, rotate. Args: obj: query: """ logging.info("Get Matrix not implemented yet") return None
[docs] def get_frame_start(): """ gets starting frame from render frame range Returns: int: start frame """ return int(bpy.context.scene.frame_start)
[docs] def get_frame_end(): """ gets end frame from render frame range Returns: int: end frame """ return int(bpy.context.scene.frame_end)
[docs] def get_handle_start(): """ gets the handle before the animation starts Returns: int: handle start """ return int(bpy.context.scene.frame_start)
[docs] def get_handle_end(): """ gets the handle after the animation ends. Returns: int: handle end """ return int(bpy.context.scene.frame_end)
[docs] def set_subdivs(iterations=1, type_=1): """ Args: iterations: default is 1 type_: 1: catclark, 0: None Returns: """ logging.info("Set Subdivs not implemented yet") return None
[docs] def get_frame_range(): """ gets the frame range of the scene Returns: dict: dictionary of frame range values """ return { "start": get_frame_start(), "end": get_frame_end(), "handle_start": get_handle_start(), "handle_end": get_handle_end(), }
[docs] def get_reference_meshes(): """ Returns: a list of all top nodes of references in the scene """ logging.info("Get Reference Meshes not implemented yet") return None
[docs] def valid_task_exists(task): """ Looks in a scene to see if a group node with name task exists and has children. Args: task: name of the group node to look for in the scene """ logging.info("Valid Task Exists not implemented yet") return None
[docs] def check_child_naming(selected=None, children=None): """ checks the naming of children of a selected node Args: selected: the selected node children: the children of the selected node Returns: bool: True if the naming is correct, False otherwise """ logging.info("Check Child Naming not implemented yet") return None
[docs] def set_default_scene_settings(): from cgl.plugins.blender.alchemy import Scene scene_object = Scene().path_object scene = bpy.context.scene scene.eevee.taa_render_samples = 1 scene.eevee.taa_samples = 1 scene.eevee.shadow_cube_size = "2048" scene.render.resolution_x = int( scene_object.project_settings["high_resolution"].split("x")[0] ) scene.render.resolution_y = int( scene_object.project_settings["high_resolution"].split("x")[1] )
[docs] def get_menu_path( cookbook_folder, software, menu_name, menu_file=False, menu_type="menus", cfg=None ): """ returns the menu path for a menu with the given name Args: software (str): software as it appears in cookbook designer menu_name (str): CamelCase menu name menu_file (bool): if True returns a menu path with a menu_name.py file menu_type (str): menus, publish, shelves, context-menus cfg: """ if menu_file: if isinstance(menu_name, dict): menu_name = menu_name["name"] menu_folder = os.path.join( cookbook_folder, software, menu_type, menu_name, "%s.py" % menu_name ) else: menu_folder = os.path.join(cookbook_folder, software, menu_type, menu_name) return menu_folder
[docs] def get_button_path(software, menu_name, button_name, menu_type="menus", cfg=None): """ Args: software (str): software as it appears in cookbook designer. menu_name (str): CamelCase menu name button_name (str): CamelCase button name menu_type (str): menus, publish, shelves, context-menus cfg: """ if cfg is None: cfg = ProjectConfig().getLastConfig() menu_folder = get_menu_path(software, menu_name, menu_type=menu_type, cfg=cfg) button_path = os.path.join(str(menu_folder), button_name + ".py") return button_path
[docs] def create_menu_file(class_name, cfg): """ Creates a Menu File on Disk Args: class_name (str): name of the class """ # read in the menu file menu_path = get_menu_path("blender", class_name, menu_file=True, cfg=cfg) menu_template = os.path.join( cfg.get_cgl_resources_path(), "cookbook", "blender", "PanelTemplate.py", ) menu_lines = read_write.load_text_file(menu_template) changed_lines = [] for l in menu_lines: if l.startswith("class PanelTemplate"): new_l = l.replace("class PanelTemplate", "class %s" % class_name) changed_lines.append(new_l) # TODO if "bl_label = \"WORKBOOK\"")" in l: # TODO if "bl_idname = \"OBJECT_PT_MENU_NAME\"" in l: elif "Panel Template" in l: new_l = l.replace("Panel Template", stringcase.titlecase(class_name)) changed_lines.append(new_l) else: changed_lines.append(l) read_write.save_text_lines(changed_lines, menu_path) # change class_name and lables # write out the menu file to desired location pass
[docs] def create_button_file(class_name, label, menu_name, cfg): """ Creates a Blender Button File on Disk. Args: class_name (str): name of the class label (str): label to appear on the button menu_name (str): name of the parent menu (CamelCase) cfg: """ button_path = get_button_path("blender", menu_name, class_name, cfg) button_template = os.path.join( cfg.get_cgl_resources_path(), "cookbook", "blender", "buttonTemplate.py", ) button_lines = read_write.load_text_file(button_template) changed_lines = [] for l in button_lines: if l.startswith("class ButtonTemplate"): new_l = l.replace("ButtonTemplate", class_name) changed_lines.append(new_l) elif "object.button_template" in l: new_l = l.replace("button_template", stringcase.snakecase(class_name)) changed_lines.append(new_l) elif "bl_label" in l: new_l = l.replace("button_template", label) changed_lines.append(new_l) elif "print" in l: new_l = l.replace("button_template", label) changed_lines.append(new_l) else: changed_lines.append(l) read_write.save_text_lines(changed_lines, button_path)
# Add a row to the Menu File
[docs] def add_buttons_to_menu(menu_name, cfg=None): """ adds buttons from a cgl menu config file to a blender menu Args: menu_name (str): name of the menu to add buttons to cfg: """ menu_file = get_menu_path("blender", menu_name, "%s.py" % menu_name, cfg=cfg) menu_config = os.path.join(ProjectConfig().cookbook_folder, "blender", "menus.cgl") menu_object = read_write.load_json(menu_config) biggest = get_last_button_number(menu_object, "blender", menu_name) if biggest: menu_lines = read_write.load_text_file(menu_file) new_menu_lines = [] for ml in menu_lines: if "pass" in ml: pass # if remove_pass: # continue new_menu_lines.append(ml) if "ADD BUTTONS" in ml: break i = 0 while i < biggest: button_name = get_menu_at(menu_object, "blender", menu_name, i) print("\t\t", button_name) clean_name = stringcase.snakecase(button_name.replace(" ", "")) button_string = f' self.layout.row().operator("object.{clean_name}", text="{button_name}")\n' print("text:::::", button_string) new_menu_lines.append(button_string) i += 1 read_write.save_text_lines(new_menu_lines, menu_file)
[docs] def get_last_button_number(menu_dict, software, menu): for m in menu_dict[software]: if m["name"] == menu: return len(m["buttons"])
[docs] def get_menu_at(menu_dict, software, menu, i): for men in menu_dict[software]: if men["name"] == menu: logging.info(i, len(men["buttons"])) button_at = men["buttons"][i] return button_at["label"]
[docs] def write_layout(outFile=None): """Writes layout to json file Args: outFile (str): path to json file """ from cgl.plugins.blender.alchemy import Scene from cgl.core.path.object import PathObject from cgl.core.utils.read_write import save_json from pathlib import Path scene_object = Scene().path_object if outFile is None: outFile = scene_object.copy(ext="json", task="lay", user="publish").get_path() data = {} for obj in bpy.context.view_layer.objects: if obj.is_instancer: logging.info(5 * "_" + obj.name + 5 * "_") name = obj.name # blender_transform = np.array(obj.matrix_world).tolist() blender_transform = [ obj.matrix_world.to_translation().x, obj.matrix_world.to_translation().y, obj.matrix_world.to_translation().z, obj.matrix_world.to_euler().x, obj.matrix_world.to_euler().y, obj.matrix_world.to_euler().z, obj.matrix_world.to_scale().x, obj.matrix_world.to_scale().y, obj.matrix_world.to_scale().z, ] instanced_collection = obj.instance_collection if instanced_collection: collection_library = return_linked_library(instanced_collection.name) if collection_library: libraryPath = bpy.path.abspath(collection_library.filepath) filename = Path(bpy.path.abspath(libraryPath)).__str__() libObject = PathObject(filename) data[name] = { "name": libObject.asset, "source_path": libObject.path, "blender_transform": blender_transform, } else: logging.info("{} has no instanced collection".format(obj.name)) else: logging.info("{} has no instanced collection".format(obj.name)) save_json(outFile, data) return outFile
[docs] def return_linked_library(collection): """ retrieves the linked libraries manually """ libraries = bpy.data.libraries collection_name = collection.split(".")[0] for i in libraries: if collection in i.name: return i
[docs] def read_layout(outFile=None, linked=False, append=False): """ Reads layout from json file Args: outFile (str): path to json file linked (bool): if True, links the assets append (bool): if True, appends the assets """ from cgl.plugins.blender.alchemy import Scene, import_file_old from cgl.core.path.object import PathObject from cgl.core.utils.read_write import load_json bpy.ops.file.make_paths_absolute() if outFile is None: outFileObject = Scene.path_object.copy( ext="json", task="lay", set_filename=True ).latest_version() outFile = outFileObject.get_path() data = load_json(outFile) for p in sorted(data): logging.info(p) data_path = data[p]["source_path"] blender_transform = data[p]["blender_transform"] transform_data = [] for value in blender_transform: transform_data.append(float(value)) pathToFile = os.path.join(Scene().path_object.root, data_path) pathObject = PathObject(pathToFile) if pathObject.filebase in bpy.data.libraries: lib = bpy.data.libraries[pathObject.filename] bpy.data.batch_remove(ids=([lib])) import_file_old(pathObject.get_path(), linked=linked, append=append) else: import_file_old(pathObject.get_path(), linked=linked, append=append) if p not in bpy.data.objects: obj = bpy.data.objects.new(p, None) bpy.context.collection.objects.link(obj) obj.instance_type = "COLLECTION" obj.instance_collection = bpy.data.collections[pathObject.asset] location = (transform_data[0], transform_data[1], transform_data[2]) obj.location = location rotation = (transform_data[3], transform_data[4], transform_data[5]) obj.rotation_euler = rotation scale = (transform_data[6], transform_data[7], transform_data[8]) obj.scale = scale else: obj = bpy.data.objects[p] logging.info("updating position") logging.info(obj.name) location = (transform_data[0], transform_data[1], transform_data[2]) obj.location = location rotation = (transform_data[3], transform_data[4], transform_data[5]) obj.rotation_euler = rotation scale = (transform_data[6], transform_data[7], transform_data[8]) obj.scale = scale
[docs] def rename_materials(selection=None, material_name=None): """ Sequentially renames materials from given object name if empty , renamed from selected object Args: selection (list): list of objects to rename materials from material_name (str): name of the material to rename to """ if selection == None: selection = bpy.context.selected_objects if material_name == None: if selection.parent: material_name = selection.parent.name else: material_name = selection.name for object in selection: for material_slot in object.material_slots: material_slot.material.name = object.name logging.info(object.name, material_slot.name) if selection: selection = [bpy.data.objects[selection]] for object in selection: for material_slot in object.material_slots: material_slot.material.name = material_name logging.info(object.name, material_slot.name) cleanup_scene_data(bpy.data.materials)
[docs] def get_valid_meshes_list(objects): valid_objects = [] for object in objects: if object and object.type == "MESH": if object.is_instancer == False: valid_objects.append(object) return valid_objects
[docs] def get_materials_from_object(object): valid_materials = [] for material_slot in object.material_slots: material = material_slot.material valid_materials.append(material_slot.material) return valid_materials
[docs] def get_selection(selection=None): from .alchemy import Scene scene_object = Scene().path_object if selection is None: try: selection = bpy.context.selected_objects except: currentScene = scene_object assetName = scene_object.shot obj_in_collection = bpy.data.collections[assetName].all_objects if not selection: selection = bpy.data.objects return selection
[docs] def get_preview_from_texture(inputs, node_tree): texture = None if inputs: color_input = inputs[1] transparent = inputs[2] try: texture = color_input.links[0].from_node except IndexError: logging.info("no texture connected") pass if texture: if texture.type == "TEX_IMAGE": preview_color = texture.image.pixels diffuse_color = [ preview_color[7004], preview_color[7005], preview_color[7006], 1 - transparent.default_value, ] if texture.type == "RGB": simple_color = texture.outputs["Color"].default_value preview_color = [ simple_color[1], simple_color[2], simple_color[3], 1 - transparent.default_value, ] else: preview_color = [1, 1, 1, 1] logging.info("_______COMPLEX NODES PLEASE SET MANUALLY_______") else: inputs = preview_inputs_from_node_tree(node_tree) if inputs: preview_color = [ color_input.default_value[0], color_input.default_value[1], color_input.default_value[2], 1, ] else: preview_color = [1, 1, 1, 1] return preview_color
[docs] def preview_inputs_from_node_tree(node_tree): color_input = None transparent = None valid_node = None if "DEFAULTSHADER" in node_tree: color_input = node_tree["DEFAULTSHADER"].inputs["Color"] transparent = node_tree["DEFAULTSHADER"].inputs["Transparent"] valid_node = node_tree["DEFAULTSHADER"] found = True else: found = False for node in node_tree: if node.type == "BSDF_PRINCIPLED" and not found: color_input = node.inputs[0] transparent = node.inputs["Transmission"] found = True valid_node = node returns = (valid_node, color_input, transparent) if found: logging.info("__________Valid Materials And inputs______________") logging.info(returns) return returns else: returns = (1, 1, 1, 1)
[docs] def setup_preview_viewport_display(object): """ set up the default viewport display color diffuse_color on materials Args: color: Value of the color of the parent menu FloatProperty 4 selection: """ materials = get_materials_from_object(object) for material in materials: node_tree = material.node_tree.nodes inputs = preview_inputs_from_node_tree(node_tree) preview_colors = get_preview_from_texture(inputs, node_tree) for i in range(0, 3): material.diffuse_color[i] = preview_colors[i]
[docs] def get_materials_dictionary(): """ creates a dictionary of the objects and the faces associated with that object Returns: materials (dict): dictionary of the objects and the faces associated with that object """ materials = {} for o in bpy.context.selected_objects: bpy.ops.object.material_slot_remove_unused() # Initialize dictionary of all materials applied to object with empty lists # which will contain indices of faces on which these materials are applied materialPolys = {ms.material.name: [] for ms in o.material_slots} for i, p in enumerate(o.data.polygons): materialPolys[o.material_slots[p.material_index].name].append(i) materials.update({o.name: materialPolys}) return materials
[docs] def read_materials(path_object=None): """ Reads the materials on the shdr task from defined from a json file """ from cgl.plugins.blender import alchemy as alc from cgl.core.utils.read_write import load_json from cgl.plugins.blender.alchemy import Scene if path_object is None: path_object = Scene().path shaders = path_object.copy( task="mld", user="publish", set_filename=True ).latest_version() outFile = shaders.copy(ext="json").get_path() data = load_json(outFile) for obj in data.keys(): object = bpy.data.objects[obj] # data = object.data index = 0 for material in data[obj].keys(): if material not in bpy.data.materials: alc.import_file_old( shaders.get_path(), collection_name=material, type="MATERIAL", linked=False, ) if material not in object.data.materials: object.data.materials.append(bpy.data.materials[material]) face_list = data[obj][material] for face in face_list: object.data.polygons[face].select = True bpy.ops.object.mode_set(mode="EDIT") bpy.context.tool_settings.mesh_select_mode = [False, False, True] object.active_material_index = index bpy.ops.object.material_slot_assign() bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="OBJECT") index += 1
[docs] def add_modifier(obj, mod_name, name): modifier = obj.modifiers.new(name, mod_name) return modifier
[docs] def create_task_on_asset(task, path_object=None, version_up=False): """ Creates a task on disk based on path object Args: task: path_object: type: type of task, mdl, shdr, rig, etc """ from cgl.plugins.blender.alchemy import Scene if path_object is None: path_object = Scene().path newTask = path_object.copy(task=task) if version_up: newTask = newTask.next_minor_version() taskFolder = newTask.copy(filename="").get_path() logging.info("{} creating version".format(taskFolder)) logging.info(newTask.get_path()) sourceFolder = newTask.copy(filename="", tree="source").get_path() renderFolder = newTask.copy(filename="", tree="render").get_path() folders = [sourceFolder, renderFolder] for f in folders: if not os.path.isdir(f): os.makedirs(f) return newTask
[docs] def reorder_list(items, arg=""): """ Reorders list in order of importance, putting rig Args: items: list of items to reorder arg: item to put at the top of the list """ if arg: for i in items: if i == arg: items.remove(i) items.insert(0, arg) return items
[docs] def get_formatted_list(elements, first_item): """ Formats list for blender search mode """ value = [(elements[i], elements[i], "") for i in range(len(elements))] return value
[docs] def remove_linked_environment_dependencies(library): env = library bpy.ops.file.make_paths_absolute() env_path = PathObject(env.filepath) env_layout = env_path.copy(ext="json").get_path() env_asset_collection = bpy.data.collections["{}_assets".format(env_path.asset)] data = load_json(env_layout) for i in data: logging.info(i) name = data[i]["name"] if i in bpy.data.objects: obj = bpy.data.objects[i] unlink_asset(obj) try: bpy.data.collections.remove(env_asset_collection) except KeyError: pass
[docs] def remove_unused_libraries(): libraries = bpy.data.libraries objects = bpy.data.objects instancers = [] libraries_in_scene = [] for obj in objects: if obj.is_instancer: instancers.append(obj) try: for i in instancers: lib = i.instance_collection.library if lib not in libraries_in_scene: libraries_in_scene.append(lib) for lib in libraries: if lib not in libraries_in_scene: logging.info(lib) # bpy.data.libraries.remove(lib) bpy.data.batch_remove(ids=(lib,)) except AttributeError: pass
[docs] def remove_instancers(): for object in bpy.data.objects: filepath = None try: libname = object.data.library except AttributeError: for lib in bpy.data.libraries: libname = object.instance_collection logging.info("_________unlinking__________") logging.info(object) if "proxy" in object.name: name = object.name.split("_")[0] else: name = object.name obj = bpy.data.objects[name] if not libname: bpy.data.batch_remove(ids=([obj])) else: try: filepath = libname.library.filepath except AttributeError: pass remove_unused_libraries()
[docs] def reparent_linked_environemnt_assets(library): env = library bpy.ops.file.make_paths_absolute() env_path = PathObject(env.filepath) env_layout = env_path.copy(ext="json").get_path() data = load_json(env_layout) assets_collection_name = "{}_assets".format(env_path.asset) if assets_collection_name not in bpy.data.collections["env"].children: assets_collection = bpy.data.collections.new(assets_collection_name) bpy.data.collections["env"].children.link(assets_collection) else: assets_collection = bpy.data.collections[assets_collection_name] for i in data: logging.info(i) name = data[i]["name"] if i in bpy.data.objects: obj = bpy.data.objects[i] if assets_collection not in obj.users_collection: assets_collection.objects.link(obj) keep_single_user_collection(obj, assetName=assets_collection_name)
[docs] def keep_single_user_collection(obj, assetName=None): from cgl.plugins.blender.alchemy import Scene if not assetName: scene_object = Scene().path_object assetName = scene_object.shot try: bpy.data.collections[assetName].objects.link(obj) except RuntimeError: pass for collection in obj.users_collection: if collection.name != assetName: collection.objects.unlink(obj)
[docs] def reparent_collections(view_layer): for obj in view_layer: if obj.instance_type == "COLLECTION": if obj.instance_collection: collection = obj.instance_collection logging.info(collection) logging.info(collection.library) if collection.library: path_object = PathObject(collection.library.filepath) create_collection(path_object.type) for collection in bpy.data.collections: if collection.name == path_object.type: # logging.info(collection.name ) if collection not in obj.users_collection: collection.objects.link(obj) keep_single_collections(obj, path_object.type) for collection in bpy.context.scene.collection.children: if len(collection.objects) < 1: logging.info(collection.name) bpy.context.scene.collection.children.unlink(collection)
[docs] def keep_single_collections(obj, collection_name): """ Unlink object from all collections except the one specified Args: obj: object to adjust collection_name: name of the collection to keep """ user_collections = obj.users_collection if len(obj.users_collection) > 1: for collection in user_collections: if collection.name != collection_name: try: logging.info("unlinking {} {}".format(obj.name, collection_name)) collection.objects.unlink(obj) except: pass
[docs] def create_collection(collection_name, parent=None): """ Creates a collection in current scene Args: collection_name (str): name of the collection to create parent (str): name of the parent collection """ if collection_name not in bpy.data.collections: bpy.data.collections.new(collection_name) collection = bpy.data.collections[collection_name] if parent is None: parent = bpy.context.scene.collection else: parent = bpy.data.collections[parent] try: parent.children.link(collection) except RuntimeError: logging.info("{} collection already in scene".format(collection_name)) pass return collection
[docs] def parent_to_collection(obj, collection_name, replace=False): """Parents object to collection Args: obj: blender object collection_name: name of the collection to parent to replace: if True, replaces the object in the collection """ collection = get_collection(collection_name) if not collection: create_collection(collection_name) try: collection.objects.link(obj) except RuntimeError: logging.info("{} already in {} collection".format(obj.name, collection_name)) except AttributeError: raise logging.info("I wasnt able to find the collection {} ".format(collection_name)) return if replace: for col in obj.users_collection: col.objects.unlink(obj) collection.objects.link(obj)
[docs] def return_asset_name(obj): if "proxy" in obj.name: name = obj.name.split("_")[0] return name else: if "." in obj.name: name = obj.name.split(".")[0] else: name = obj.name return name
[docs] def get_lib_from_object(object): instancer = object.is_instancer if not instancer: try: object = bpy.data.object[return_asset_name(object)] except: pass try: library = object.instance_collection.library except AttributeError: return None pass return library
[docs] def return_lib_path(library): from pathlib import Path logging.info(library) library_path = bpy.path.abspath(library.filepath) filename = Path(bpy.path.abspath(library_path)).__str__() return filename
[docs] def create_shot_mask_info(): from cgl.plugins.blender.alchemy import Scene current = bpy.context.scene mSettings = current.render sceneObject = Scene().path_object current.name = sceneObject.filebase scene_info = bpy.context.scene.statistics(bpy.context.view_layer) try: mSettings.metadata_input = "SCENE" except AttributeError: mSettings.use_stamp_strip_meta = 0 mSettings.stamp_font_size = 26 mSettings.use_stamp = 1 mSettings.use_stamp_camera = 1 mSettings.use_stamp_date = 0 mSettings.use_stamp_frame = True mSettings.use_stamp_frame_range = 0 mSettings.use_stamp_hostname = 0 mSettings.use_stamp_labels = 0 mSettings.use_stamp_lens = 1 mSettings.use_stamp_marker = 0 mSettings.use_stamp_memory = 0 mSettings.use_stamp_note = 0 mSettings.use_stamp_render_time = 0 mSettings.use_stamp_scene = 1 mSettings.use_stamp_sequencer_strip = 0 mSettings.use_stamp_time = 1 mSettings.use_stamp_note = True mSettings.stamp_note_text = scene_info logging.info("shot_mask_created")
[docs] def create_object(name, type=None, parent=None, collection=None, namespace=None): if namespace: name = "{}:{}".format(namespace, name) if collection is None: collection = bpy.context.collection.name if name in bpy.data.objects: object = bpy.data.objects[name] logging.info("{} object already exists".format(name)) else: object = bpy.data.objects.new(name, object_data=type) if parent: object.parent = parent parent_to_collection(collection_name=collection, obj=object) return object
[docs] def parent_object(child, parent, keep_transform=True): child.parent = parent if keep_transform: child.matrix_parent_inverse = parent.matrix_world.inverted()
[docs] def clear_parent(objects=None): if objects is None: objects = bpy.context.selected_objects for obj in objects: parent = obj.parent children = obj.children if children: for child in children: parent_object(child, parent)
[docs] def get_objects_in_hirarchy(obj, levels=10): hirarchy = [] def recurse(obj, parent, depth): if depth > levels: return hirarchy.append(obj.name) # logging.info(" " * depth, obj.name) for child in obj.children: recurse(child, obj, depth + 1) recurse(obj, obj.parent, 0) return hirarchy
[docs] def cleanup_file(task="mdl"): """ finds the given task group and deletes everythin ecept for that locator Args: task: task to keep in scene keep_task: asset: """ task_object = get_scene_object(task) tasks_objects = get_objects_in_hirarchy(task_object) all_objects = objects_in_scene() for obj in all_objects: if obj.name not in tasks_objects: logging.info(obj) bpy.data.objects.remove(obj) for collection in collections_in_scene(): if collection.name not in ["Scene Collection", get_scene_collection().name]: logging.info(collection) bpy.data.collections.remove(collection) purge_unused_data()
[docs] def build_default_structure(group, default_list=["FG", "CAM", "RIG"], namespace=None): for layer in default_list: if namespace: layer = "{}:{}".format(namespace, layer) create_object(layer, parent=group) create_object("BG", parent=group, namespace=namespace) create_object("MAIN", parent=group, namespace=namespace)
[docs] def create_folders(path_object): """ Creates the folders for the given path object Args: path_object: path object to create folders for """ import os path_folder = path_object.copy(filename="").get_path() if not os.path.isdir(path_folder): os.makedirs(path_folder)
[docs] def save_to_task(task): """ Saves a copy of the current file into a new version of the given task Args: task: """ from cgl.plugins.blender.alchemy import Scene scene = Scene().path_object new_file = scene.copy(task=task, tree="source", latest=True, set_filename=True) directory = new_file.copy(filename="") next_version = new_file.next_minor_version() create_folders(next_version) Scene().save_file_as(next_version.get_path()) return next_version
[docs] def purge_unused_data(): """ Deletes all the uneused data, ie data that's not currently linked to any scene """ remove_unused_libraries() cleanup_scene_data(bpy.data.collections) cleanup_scene_data(bpy.data.meshes) cleanup_scene_data(bpy.data.objects) cleanup_scene_data(bpy.data.materials) cleanup_scene_data(bpy.data.images) cleanup_scene_data(bpy.data.grease_pencils) cleanup_scene_data(bpy.data.node_groups) cleanup_scene_data(bpy.data.armatures)
[docs] def cleanup_scene_data(data_type): """ Deletes data that's not currently linked to any object in scene , takes in bpy.data.type ie bpy.data.materials Args: data_type: bpy.data.type ie bpy.data.materials """ for child in data_type: if child.users == 0: logging.info(child.name) data_type.remove(child)
[docs] def return_object_list(task): object_list = [] for res in bpy.data.objects[task].children: for materials in res.children: for obj in materials.children: object_list.append(obj) return object_list
[docs] def objects_in_scene(string=False): list_of_objects = [] if string is True: for object in bpy.data.objects: list_of_objects.append(object.name) return list_of_objects return bpy.data.objects
[docs] def collections_in_scene(string=False): list_of_objects = [] if string is True: for collection in bpy.data.collections: list_of_objects.append(collection.name) return list_of_objects else: return bpy.data.collections
[docs] def load_library(path_object, collection_name=None, replace=True): if os.path.exists(path_object.get_path()): if collection_name is None: name = "{}_{}_{}:{}".format( path_object.sequence, path_object.shot, path_object.variant, path_object.task, ) collection_name = name if replace: for lib in bpy.data.libraries: if lib.name == name: bpy.data.libraries.remove(lib) with bpy.data.libraries.load(path_object.get_path(), link=True) as ( data_from, data_to, ): for c in data_from.collections: if c == collection_name: logging.info(c) data_to.collections = [c] return c
[docs] def get_next_namespace(ns, sel=None, type="lay", levels=3): import re from cgl.plugins.blender.alchemy import Scene pattern = "[0-9]+" next = False latest = 0 if sel is None: scene = Scene().path_object layer = get_scene_object() sel = _hirarchy(layer, levels=levels) for name in sel: i = get_object(name) if ns in i.name: name = i.name.split(ns) if name[1].startswith("_"): # logging.info(i.name) split_namespace = i.name.split(":") num = re.findall(pattern, i.name) logging.info(num) if num: latest = int(num[-1]) + 1 next = True name = "{}_{:03d}".format(ns, latest) logging.info(name) return name
def set_framerange(start=1, end=1, current=False): bpy.context.scene.frame_start = start bpy.context.scene.frame_end = end current = bpy.context.scene.frame_current if current: bpy.context.scene.frame_start = current bpy.context.scene.frame_end = current
[docs] def render(preview=False, audio=False): """Renders the current scene. Based on the task we can derive what kind of render and specific render settings. Args: preview: determines if exr is used or not audio: if True renders an mov and setups the audio settings """ from cgl.plugins.blender.alchemy import Scene previewRenderTypes = ["anim", "rig", "mdl", "lay"] file_out = Scene().path_object.render_path.split("#")[0] if preview: bpy.context.scene.render.image_settings.file_format = "JPEG" bpy.context.scene.render.filepath = file_out if audio: bpy.context.scene.render.image_settings.file_format = "FFMPEG" bpy.context.scene.render.ffmpeg.format = "QUICKTIME" bpy.context.scene.render.ffmpeg.audio_codec = "MP3" bpy.ops.render.opengl("INVOKE_DEFAULT", animation=True, view_entity_type=True) else: bpy.context.scene.render.image_settings.file_format = "OPEN_EXR_MULTILAYER" bpy.context.scene.render.filepath = file_out bpy.ops.render.render(animation=True, use_viewport=True)
[docs] def get_framerange(): start = bpy.context.scene.frame_start end = bpy.context.scene.frame_end return start, end
[docs] def scene_elem(elem): return eval("bpy.data.{}".format(elem))
[docs] def get_scene_collection(task=None, path_object=None): from cgl.plugins.blender.alchemy import Scene if not path_object: path_object = Scene().path_object if not task: task = path_object.task name = "{}_{}_{}:{}".format( path_object.seq, path_object.shot, path_object.variant, task ) return get_collection(name)
[docs] def get_object(name, namespace=None): if isinstance(name, str): if namespace: name = "{}:{}".format(namespace, name) if name in bpy.data.objects: return bpy.data.objects[name] else: return None else: return name
[docs] def get_layer(name, namespace=None, set_default_namespace=True): from cgl.plugins.blender.alchemy import Scene scene = Scene().path_object if set_default_namespace is True: if namespace is None: namespace = "{}_{}_{}".format(scene.sequence, scene.shot, scene.variant) layer_name = "{}:{}".format(namespace, name) else: layer_name = name return get_object(layer_name)
[docs] def get_collection_from_path_object(path_object): from cgl.core.path.object import PathObject set_all_paths_relative(False) for i in bpy.data.collections: if i.library: lib_path_object = PathObject(i.library.filepath) if lib_path_object.root == path_object.get_path(): return i set_all_paths_relative(True) return "couldnt find collection for {}".format(path_object.get_path())
[docs] def get_collection(name, namespace=None): if namespace: name = "{}:{}".format(namespace, name) if name in bpy.data.collections: return bpy.data.collections[name] else: return None
[docs] def set_framerange(start, end): bpy.context.scene.frame_start = start bpy.context.scene.frame_end = end bpy.context.scene.frame_current = start
[docs] def selection(object=None, clear=False): if clear: for ob in bpy.data.objects: ob.select_set(False) if object: object.select_set(True) bpy.context.view_layer.objects.active = object
[docs] def select_objects(list): for i in list: selection(i)
[docs] def current_selection(single=False): if single: return bpy.context.object return bpy.context.selected_objects
[docs] def switch_overlays(visible=False): for window in bpy.context.window_manager.windows: screen = window.screen for area in screen.areas: if area.type == "VIEW_3D": for space in area.spaces: if space.type == "VIEW_3D": space.overlay.show_overlays = visible
[docs] def read_matrix(obj, transform_data): location = (transform_data[0], transform_data[1], transform_data[2]) obj.location = location rotation = (transform_data[3], transform_data[4], transform_data[5]) obj.rotation_euler = rotation scale = (transform_data[6], transform_data[7], transform_data[8]) obj.scale = scale
[docs] def set_collection_name(obj=None): from cgl.plugins.blender.alchemy import Scene scene = Scene().path_object if scene.entity_type == "assets": name = "{}_{}_{}:{}".format( scene.sequence, scene.shot, scene.variant, scene.task ) elif scene.entity_type == "shots": name = "{}_{}_{}:{}".format( scene.sequence, scene.shot, scene.variant, scene.task ) if obj is None: if name in bpy.data.collections: logging.info("collection exist") else: if "Collection" in bpy.data.collections: bpy.data.collections["Collection"].name = name else: logging.info("default Collection not found") bpy.context.collection.name = name else: if scene.asset in bpy.data.collections: logging.info("collection exist ") object = bpy.context.object object.users_collection[0].name = name
[docs] def set_context_view_3d(): for window in bpy.context.window_manager.windows: screen = window.screen for area in screen.areas: if area.type == "VIEW_3D": override = {"window": window, "screen": screen, "area": area} bpy.ops.screen.screen_full_area(override)
def _hirarchy(obj, levels=10): hirarchy = [] def recurse(obj, parent, depth): if depth > levels: return hirarchy.append(obj.name) # logging.info(" " * depth, obj.name) for child in obj.children: recurse(child, obj, depth + 1) recurse(obj, obj.parent, 0) return hirarchy
[docs] def rename_collection(current_scene=None): from cgl.plugins.blender.alchemy import Scene if current_scene is None: current_scene = Scene().path_object name = "{}_{}_{}:{}".format( current_scene.seq, current_scene.shot, current_scene.variant, current_scene.task ) collection = get_scene_collection() if not collection: if len(bpy.data.collections) == 1: bpy.data.collections[0].name = name else: try: bpy.data.collections["Collection"].name = name except KeyError: asset_collection = get_collection(current_scene.asset) if asset_collection: asset_collection.name = name logging.info("collection in scene") else: logging.info("failed to rename ") pass
[docs] def delete_object(object_to_delete): bpy.data.objects.remove(object_to_delete, do_unlink=True)
[docs] def get_scene_name(): return bpy.context.scene.name
[docs] def get_scene_object(task=None): from cgl.plugins.blender.alchemy import Scene if not task: task = Scene().path_object.task name = "{}_{}_{}:{}".format( Scene().path_object.sequence, Scene().path_object.asset, Scene().path_object.variant, task, ) object = get_object(name) if not object: try: object = get_object(task) except: logging.info("I wasnt able to find any scene object, please click build") pass return object
def get_items(type): command = "bpy.data.{}".format(type) return eval(command)
[docs] def move_to_project(project, path_object=None): from cgl.core.utils.general import cgl_copy from cgl.plugins.blender.alchemy import Scene context = ["source", "render"] if path_object is None: path_object = Scene().path_object for item in context: fromDir = path_object.copy(entity_type=item, filename=None).get_path() toDir = path_object.copy( entity_type=item, filename=None, project=project ).get_path() cgl_copy(fromDir, toDir)
[docs] def move_linked_libraries_to_project(project=None): from cgl.core.path.object import PathObject bpy.ops.file.make_paths_absolute() for lib in bpy.data.libraries: path_object = PathObject(lib.filepath) logging.info(lib.filepath) move_to_project(project, path_object)
[docs] def update_libraries_project(project=None): from cgl.core.path.object import PathObject from cgl.plugins.blender.alchemy import Scene if project is None: project = Scene().path_object.project bpy.ops.file.make_paths_absolute() for lib in bpy.data.libraries: path_object = PathObject(lib.filepath) new_path = path_object.copy(project=project) lib.filepath = new_path.get_path() logging.info(new_path.get_path()) lib.reload()
[docs] def get_valid_objects_in_scene_hirarchy(): sceneObject = get_scene_object() children = sceneObject.children objects = [] if children: for child in children: for obj in child.children: objects.append(obj) return objects
[docs] def set_object_names_from_library(objects=None): from .msd import path_object_from_source_path bpy.ops.file.make_paths_absolute() if not objects: objects = get_valid_objects_in_scene_hirarchy() for obj in objects: logging.info(222222222222) logging.info(obj) # logging.info(obj[0].name, obj[1].file_path) path_object = path_object_from_source_path(obj["source_path"]) next_version_number = get_next_namespace(path_object.asset) logging.info(path_object.get_path()) logging.info(next_version_number) obj.name = "{}:{}".format(path_object.asset, next_version_number)
[docs] def set_all_paths_relative(default=True): if default: bpy.ops.file.make_paths_relative() else: bpy.ops.file.make_paths_absolute()
[docs] def clean_name(grp): for i in get_objects_in_hirarchy(grp): logging.info(i) object = get_object(i) object.name = i.split("-")[0]
[docs] def get_items(type="assets"): from cgl.plugins.blender.alchemy import Scene from cgl.core.utils.general import load_json scene = Scene().path_object project_msd = load_json(scene.project_msd_path) shots = project_msd[type] shot_list = [] for seq in shots: for shot in shots[seq]: name = "{} {}".format(seq, shot) shot_list.append(name) return shot_list
[docs] def format_list(list): formatted_list = [] for i in list: name = (i, i, i) formatted_list.append(name) return formatted_list
[docs] def search_dialog(list, command, flags=None): try: bpy.utils.unregister_class(SearchItem) except: pass bpy.utils.register_class(SearchItem) bpy.context.scene["alchemy_search"] = str(list) bpy.context.scene["alchemy_command"] = command if flags: bpy.context.scene["alchemy_flags"] = flags
# bpy.ops.magic_browser.search_item('INVOKE_DEFAULT')
[docs] def get_search_selection_task(): from cgl.plugins.blender.alchemy import Scene from cgl.core.utils.general import load_json scene = Scene().path_object project_msd = load_json(scene.project_msd_path) type, sel = bpy.context.scene["alchemy_selection"].split(" ") bpy.context.scene["alchemy_temp"] = bpy.context.scene["alchemy_selection"] context = bpy.context.scene["alchemy_flags"] logging.info(project_msd[context][type][sel]) object = project_msd[context][type][sel] task = [] for i in object: logging.info(i) task.append(i) return task
[docs] def tag_scene(tag, value=None): if not value: value = "{} {}".format( bpy.context.scene["alchemy_temp"], bpy.context.scene["alchemy_selection"] ) bpy.context.scene[tag] = value
# class SearchItem(bpy.types.Operator): # # # """ # imports latest version of selectd task # """ # bl_idname = 'magic_browser.search_item' # bl_label = 'Open Shot' # # bl_property = "alchemy_selection" # # # search = format_list(eval(bpy.context.scene['alchemy_search'])) # logging.info(search) # alchemy_selection = bpy.props.EnumProperty(items=search) # # def execute(self, context): # self.report({'INFO'}, "Selected: %s" % self.alchemy_selection) # bpy.types.Scene.scene_enum = bpy.props.StringProperty(name=self.alchemy_selection) # bpy.types.Scene.scene_enum = self.alchemy_selection + ' publish' # bpy.context.scene['alchemy_selection'] = self.alchemy_selection # eval(bpy.context.scene['alchemy_command']) # # return {'FINISHED'} # # def invoke(self, context, event): # wm = context.window_manager # wm.invoke_search_popup(self) # return {'FINISHED'} #
[docs] def selected_library(selected=None): from pathlib import Path from cgl.core.path.object import PathObject if not selected: selected = bpy.context.selected_objects[0] if "proxy" in selected.name: name = selected.name.split("_")[0] else: if "." in selected.name: name = selected.name.split(".")[0] else: name = selected.name library = selected.instance_collection.library library_path = bpy.path.abspath(library.filepath) filename = Path(bpy.path.abspath(library_path)).__str__() lumber_object = PathObject(filename) lumber_object = lumber_object.copy(tree="source") return lumber_object
[docs] def switch_library(library, path_object): library.filepath = path_object.get_path() library.reload()
# logging.info(library.file_path + ' CHANGED')
[docs] def switch_item_on_library(object, item, value): import os set_all_paths_relative(False) lumber_object = selected_library(object) library = object.instance_collection.library if os.path.isdir(lumber_object.copy(tree="render", filename="").get_path()): latest_resolution = lumber_object.copy(resolution=value) switch_library(library, latest_resolution)
if __name__ == "__main__": # create_menu_file('TomTest', 'Tom Test', # r'F:\FSU-CMPA\COMPANIES\_config\cgl_tools\blender\menus\TomTest\TomTest.py') # create_button_file('ButtonAaa', 'Button Aaa', 'TomTest') add_buttons_to_menu("TomTest")