Created
May 5, 2025 05:27
-
-
Save Pakmanv/e210a01dd2e082a1ed8a7160b278ddcf to your computer and use it in GitHub Desktop.
vvqcsfmgenerator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import maya.cmds as cmds | |
| import os | |
| import re | |
| # Tool Name | |
| TOOL_NAME = "VV_qc_SFM_Generator" | |
| # Default shaders to exclude | |
| DEFAULT_SHADERS = ["lambert1", "particleCloud1", "shaderGlow1", "standardSurface1"] | |
| # Global dictionary to store shader mappings (now including texture info) | |
| shader_mapping = {} # Format: {cleaned_name: {'original': original_name, 'texture': texture_name, 'bumpmap': bumpmap_name}} | |
| # List of available material types for $surfaceprop | |
| MATERIAL_TYPES = [ | |
| "none", "Concrete", "Metal", "Wood", "Flesh", "Glass", "Plastic", "Dirt", "Tile" | |
| ] | |
| # List of class types for the dropdown, with "none" as the first/default option | |
| CLASS_TYPES = [ | |
| "none", "character", "prop", "setelement", "set", "animation", "fx" | |
| ] | |
| # ValveBiped skeleton hierarchy | |
| VALVE_BIPED_HIERARCHY = [ | |
| "ValveBiped.Bip01_Pelvis", | |
| " ValveBiped.Bip01_Spine", | |
| " ValveBiped.Bip01_Spine1", | |
| " ValveBiped.Bip01_Spine2", | |
| " ValveBiped.Bip01_Spine4", | |
| "ValveBiped.Bip01_L_Clavicle", | |
| " ValveBiped.Bip01_L_UpperArm", | |
| " ValveBiped.Bip01_L_Forearm", | |
| " ValveBiped.Bip01_L_Hand", | |
| " ValveBiped.Bip01_L_Finger0", | |
| " ValveBiped.Bip01_L_Finger01", | |
| " ValveBiped.Bip01_L_Finger02", | |
| " ValveBiped.Bip01_L_Finger1", | |
| " ValveBiped.Bip01_L_Finger11", | |
| " ValveBiped.Bip01_L_Finger12", | |
| " ValveBiped.Bip01_L_Finger2", | |
| " ValveBiped.Bip01_L_Finger21", | |
| " ValveBiped.Bip01_L_Finger22", | |
| " ValveBiped.Bip01_L_Finger3", | |
| " ValveBiped.Bip01_L_Finger31", | |
| " ValveBiped.Bip01_L_Finger32", | |
| " ValveBiped.Bip01_L_Finger4", | |
| " ValveBiped.Bip01_L_Finger41", | |
| " ValveBiped.Bip01_L_Finger42", | |
| "ValveBiped.Bip01_R_Clavicle", | |
| " ValveBiped.Bip01_R_UpperArm", | |
| " ValveBiped.Bip01_R_Forearm", | |
| " ValveBiped.Bip01_R_Hand", | |
| " ValveBiped.Bip01_R_Finger0", | |
| " ValveBiped.Bip01_R_Finger01", | |
| " ValveBiped.Bip01_R_Finger02", | |
| " ValveBiped.Bip01_R_Finger1", | |
| " ValveBiped.Bip01_R_Finger11", | |
| " ValveBiped.Bip01_R_Finger12", | |
| " ValveBiped.Bip01_R_Finger2", | |
| " ValveBiped.Bip01_R_Finger21", | |
| " ValveBiped.Bip01_R_Finger22", | |
| " ValveBiped.Bip01_R_Finger3", | |
| " ValveBiped.Bip01_R_Finger31", | |
| " ValveBiped.Bip01_R_Finger32", | |
| " ValveBiped.Bip01_R_Finger4", | |
| " ValveBiped.Bip01_R_Finger41", | |
| " ValveBiped.Bip01_R_Finger42", | |
| "ValveBiped.Bip01_Neck1", | |
| " ValveBiped.Bip01_Head1", | |
| "ValveBiped.Bip01_L_Thigh", | |
| " ValveBiped.Bip01_L_Calf", | |
| " ValveBiped.Bip01_L_Foot", | |
| " ValveBiped.Bip01_L_Toe0", | |
| "ValveBiped.Bip01_R_Thigh", | |
| " ValveBiped.Bip01_R_Calf", | |
| " ValveBiped.Bip01_R_Foot", | |
| " ValveBiped.Bip01_R_Toe0" | |
| ] | |
| # Function to get plural form of class type (returns empty string for "none") | |
| def get_plural_class(class_type): | |
| if class_type == "none": | |
| return "" | |
| plurals = { | |
| "character": "characters", | |
| "prop": "props", | |
| "setelement": "setelements", | |
| "set": "sets", | |
| "animation": "animations", | |
| "fx": "fx" # fx remains unchanged as it’s often plural already | |
| } | |
| return plurals.get(class_type, class_type + "s") # Default to adding 's' if not in dict | |
| # Function to get diffuse texture name from shader | |
| def get_texture_name(shader): | |
| file_nodes = cmds.listConnections(shader, source=True, destination=False, type="file") | |
| if file_nodes: | |
| texture_path = cmds.getAttr(f"{file_nodes[0]}.fileTextureName") | |
| if texture_path: | |
| texture_name = os.path.splitext(os.path.basename(texture_path))[0] | |
| return texture_name # e.g., "robot_Blue_C" | |
| return None | |
| # Function to get bump/normal map name from shader | |
| def get_bumpmap_name(shader): | |
| bump_nodes = cmds.listConnections(shader, source=True, destination=False, type="bump2d") | |
| if not bump_nodes: | |
| return None | |
| file_nodes = cmds.listConnections(bump_nodes[0], source=True, destination=False, type="file") | |
| if file_nodes: | |
| texture_path = cmds.getAttr(f"{file_nodes[0]}.fileTextureName") | |
| if texture_path: | |
| bumpmap_name = os.path.splitext(os.path.basename(texture_path))[0] # e.g., "robot_Blue_n" | |
| return bumpmap_name | |
| return None | |
| # Function to clean up shader names (remove "_shader" and optionally "blinn" with numbers) | |
| def clean_shader_name(shader, has_texture=False): | |
| name = shader.replace("_shader", "") | |
| if has_texture: | |
| name = re.sub(r'blinn\d*', '', name, flags=re.IGNORECASE) | |
| name = name.rstrip('_') | |
| return name | |
| # Function to generate the .qc file and optionally .vmt files | |
| def generate_qc_file(output_path, qc_file_name, model_name, main_geos, geometry_list, geometry_format, material_type, scale_value, class_type, vmt_output_path=None, generate_vmt=False, animation_includes=None): | |
| if not os.path.exists(output_path): | |
| os.makedirs(output_path) | |
| if generate_vmt and vmt_output_path and not os.path.exists(vmt_output_path): | |
| os.makedirs(vmt_output_path) | |
| shader_list = get_shader_list() | |
| cleaned_shaders = [] | |
| texture_names = {} | |
| plural_class = get_plural_class(class_type) | |
| path_prefix = f"{plural_class}/" if plural_class else "" # Empty if "none" | |
| for shader in shader_list: | |
| texture_name = get_texture_name(shader) | |
| cleaned_name = clean_shader_name(shader, has_texture=bool(texture_name)) | |
| if texture_name: | |
| texture_parts = texture_name.split('_') | |
| shader_suffix = '_'.join(texture_parts[:-1]) # "robot_Blue" | |
| texture_names[cleaned_name] = texture_name | |
| cleaned_shaders.append(f"{cleaned_name}_{shader_suffix}") | |
| else: | |
| cleaned_shaders.append(cleaned_name) | |
| qc_content = f"""// .qc was generated using VVqcSFMGenerator | |
| // Model Definition | |
| $modelname "{path_prefix}{model_name}/{model_name}.mdl" | |
| // Scale | |
| $scale {scale_value} | |
| """ | |
| # Add $includemodel lines if any animations are specified | |
| if animation_includes: | |
| qc_content += "// Included Animation Models\n" | |
| for anim in animation_includes: | |
| qc_content += f'$includemodel "{anim}"\n' | |
| qc_content += "\n" | |
| qc_content += f"""// Main Geometry References | |
| """ | |
| for main_geo in main_geos: | |
| qc_content += f'$model "{main_geo}" "{main_geo}.{geometry_format}"\n' | |
| qc_content += f""" | |
| // Bodygroups and Animation | |
| $bodygroup "default" | |
| {{ | |
| studio "{main_geos[0]}.{geometry_format}" | |
| blank | |
| }} | |
| """ | |
| for geo in geometry_list: | |
| if geo not in main_geos: | |
| qc_content += f'$bodygroup "{geo}"\n{{\n studio "{geo}.{geometry_format}"\n blank\n}}\n' | |
| qc_content += f'$sequence "idle" "{main_geos[0]}.{geometry_format}" fps 1\n' | |
| qc_content += f""" | |
| // Collision Model | |
| $collisionmodel "{main_geos[0]}.{geometry_format}" | |
| {{ | |
| $concave | |
| $maxconvexpieces 10000 | |
| }} | |
| """ | |
| qc_content += f""" | |
| // Materials and Texture Groups | |
| """ | |
| if material_type != "none": | |
| qc_content += f'$surfaceprop "{material_type.lower()}"\n' | |
| qc_content += f'$cdmaterials "{path_prefix}{model_name}"\n' | |
| if cleaned_shaders: | |
| qc_content += f"""$texturegroup skinfamilies | |
| {{ | |
| // Example of multiple skin variants: {{ "normal" "bloody" }} | |
| """ | |
| for shader in cleaned_shaders: | |
| qc_content += f' {{ "{shader}" }}\n' | |
| qc_content += "}\n" | |
| qc_content += f""" | |
| // Vertex Limit | |
| $maxverts 65530 | |
| """ | |
| qc_file_path = os.path.join(output_path, qc_file_name) | |
| with open(qc_file_path, 'w') as qc_file: | |
| qc_file.write(qc_content) | |
| message = f"QC file generated at:\n{qc_file_path}" | |
| if generate_vmt and vmt_output_path and cleaned_shaders: | |
| for shader in shader_list: | |
| texture_name = get_texture_name(shader) | |
| bumpmap_name = get_bumpmap_name(shader) | |
| cleaned_shader = clean_shader_name(shader, has_texture=bool(texture_name)) | |
| final_shader_name = cleaned_shader | |
| if texture_name: | |
| texture_parts = texture_name.split('_') | |
| shader_suffix = '_'.join(texture_parts[:-1]) # e.g., "robot_Blue" | |
| final_shader_name = f"{cleaned_shader}_{shader_suffix}" | |
| diffuse_texture = f"{path_prefix}{model_name}/{texture_name}" # Use path_prefix | |
| else: | |
| diffuse_texture = f"{path_prefix}{model_name}/{cleaned_shader}_c" | |
| # Use the actual bump map name if present, otherwise default to _n | |
| bumpmap_texture = f"{path_prefix}{model_name}/{bumpmap_name}" if bumpmap_name else f"{path_prefix}{model_name}/{final_shader_name}_n" | |
| vmt_content = f'''"VertexLitGeneric" | |
| {{ | |
| "$basetexture" "{diffuse_texture}" | |
| "$bumpmap" "{bumpmap_texture}" | |
| "$phong" "0" | |
| "$phongboost" "0" | |
| "$phongexponent" "0" | |
| "$rimlight" "0" | |
| "$rimlightboost" "0" | |
| "$rimlightexponent" "0" | |
| "$selfillum" "0" | |
| "$translucent" "0" | |
| "$alpha" "1" | |
| "$detail" "" | |
| "$envmap" "" | |
| "$envmaptint" "[0 0 0]" | |
| }} | |
| ''' | |
| vmt_file_path = os.path.join(vmt_output_path, f"{final_shader_name}.vmt") | |
| with open(vmt_file_path, 'w') as vmt_file: | |
| vmt_file.write(vmt_content) | |
| message += f"\nVMT files generated at:\n{vmt_output_path}" | |
| cmds.confirmDialog(title="Success", message=message, button="OK") | |
| # Function to get only geometry (meshes) in the scene | |
| def get_geometry_list(): | |
| geometry_list = [] | |
| for node in cmds.ls(type="transform", long=True): | |
| shapes = cmds.listRelatives(node, shapes=True, fullPath=True) | |
| if shapes and cmds.objectType(shapes[0], isType="mesh"): | |
| if (not cmds.objectType(node, isType="joint") and | |
| not cmds.objectType(node, isType="locator") and | |
| not cmds.objectType(node, isType="camera") and | |
| cmds.listRelatives(node, children=True, type="transform") is None): | |
| geometry_list.append(cmds.ls(node, shortNames=True)[0]) | |
| return geometry_list | |
| # Function to get all joint nodes in the scene | |
| def get_joint_list(): | |
| return cmds.ls(type="joint", long=True) | |
| # Function to get all shaders in the Hypershade, excluding default shaders | |
| def get_shader_list(): | |
| shaders = cmds.ls(mat=True) | |
| return [shader for shader in shaders if shader not in DEFAULT_SHADERS] | |
| # Function to select objects with the selected shader | |
| def select_shader_members(shader_list_field): | |
| selected_shader = cmds.textScrollList(shader_list_field, query=True, selectItem=True) | |
| if selected_shader: | |
| clean_selected = selected_shader[0].rstrip('*') | |
| shader_info = shader_mapping.get(clean_selected) | |
| if shader_info and shader_info['original']: | |
| cmds.hyperShade(objects=shader_info['original']) | |
| # Function to select geometry in viewport | |
| def select_geometry_in_viewport(geometry_list_field): | |
| selected_geo = cmds.textScrollList(geometry_list_field, query=True, selectItem=True) | |
| if selected_geo: | |
| cmds.select(selected_geo, replace=True) | |
| # Function to select joints in viewport | |
| def select_joints_in_viewport(joint_list_field): | |
| selected_joints = cmds.textScrollList(joint_list_field, query=True, selectItem=True) | |
| if selected_joints: | |
| cleaned_joints = [joint.split(" (")[0] for joint in selected_joints] | |
| cmds.select(cleaned_joints, replace=True) | |
| # Function to rename geometry | |
| def rename_geometry(geometry_list_field, geometry_count_field): | |
| selected_geo = cmds.textScrollList(geometry_list_field, query=True, selectItem=True) | |
| if selected_geo: | |
| current_name = selected_geo[0] | |
| new_name = cmds.promptDialog( | |
| title="Rename Geometry", | |
| message="Enter new name:", | |
| text=current_name, | |
| button=["OK", "Cancel"], | |
| defaultButton="OK", | |
| cancelButton="Cancel", | |
| dismissString="Cancel" | |
| ) | |
| if new_name == "OK": | |
| new_name = cmds.promptDialog(query=True, text=True) | |
| if new_name: | |
| cmds.rename(selected_geo[0], new_name) | |
| refresh_geometry_list(geometry_list_field, geometry_count_field) | |
| # Function to rename joint | |
| def rename_joint(joint_list_field, joint_count_field): | |
| selected_joint = cmds.textScrollList(joint_list_field, query=True, selectItem=True) | |
| if selected_joint: | |
| current_name = selected_joint[0].split(" (")[0] if " (" in selected_joint[0] else selected_joint[0] | |
| new_name = cmds.promptDialog( | |
| title="Rename Joint", | |
| message="Enter new name:", | |
| text=current_name, | |
| button=["OK", "Cancel"], | |
| defaultButton="OK", | |
| cancelButton="Cancel", | |
| dismissString="Cancel" | |
| ) | |
| if new_name == "OK": | |
| new_name = cmds.promptDialog(query=True, text=True) | |
| if new_name: | |
| cmds.rename(current_name, new_name) | |
| refresh_joint_list(joint_list_field, joint_count_field) | |
| # Function to rename shader | |
| def rename_shader(shader_list_field, shader_count_field): | |
| selected_shader = cmds.textScrollList(shader_list_field, query=True, selectItem=True) | |
| if selected_shader: | |
| clean_selected = selected_shader[0].rstrip('*') | |
| shader_info = shader_mapping.get(clean_selected) | |
| if shader_info and shader_info['original']: | |
| current_name = clean_selected | |
| new_name = cmds.promptDialog( | |
| title="Rename Shader", | |
| message="Enter new name:", | |
| text=current_name, | |
| button=["OK", "Cancel"], | |
| defaultButton="OK", | |
| cancelButton="Cancel", | |
| dismissString="Cancel" | |
| ) | |
| if new_name == "OK": | |
| new_name = cmds.promptDialog(query=True, text=True) | |
| if new_name: | |
| cmds.rename(shader_info['original'], new_name) | |
| refresh_shader_list(shader_list_field, shader_count_field) | |
| # Function to refresh the entire UI by reopening the tool | |
| def refresh_ui(): | |
| if cmds.window(TOOL_NAME, exists=True): | |
| cmds.deleteUI(TOOL_NAME) | |
| create_ui() | |
| # Function to show ValveBiped NC window | |
| def show_valvebiped_window(): | |
| window_name = "ValveBipedNCWindow" | |
| if cmds.window(window_name, exists=True): | |
| cmds.deleteUI(window_name) | |
| cmds.window(window_name, title="ValveBiped NC Hierarchy", widthHeight=(300, 400)) | |
| cmds.columnLayout(adjustableColumn=True) | |
| tree_view = cmds.treeView(numberOfButtons=0, height=350) | |
| # Build the hierarchy | |
| parent_stack = [] | |
| for line in VALVE_BIPED_HIERARCHY: | |
| stripped_line = line.strip() | |
| indent_level = (len(line) - len(stripped_line)) // 4 # Each indent is 4 spaces | |
| joint_name = stripped_line | |
| # Adjust parent stack based on indent level | |
| while len(parent_stack) > indent_level: | |
| parent_stack.pop() | |
| parent = parent_stack[-1] if parent_stack else None | |
| cmds.treeView(tree_view, edit=True, addItem=(joint_name, parent)) | |
| if indent_level >= len(parent_stack): | |
| parent_stack.append(joint_name) | |
| cmds.button(label="Close", command=lambda _: cmds.deleteUI(window_name)) | |
| cmds.showWindow(window_name) | |
| # VVLowerCaseAll Tool | |
| def vv_lowercase_all(): | |
| """Converts all namable objects in the scene to lowercase.""" | |
| # Get all objects in the scene (long names to handle hierarchy) | |
| all_objects = cmds.ls(long=True) | |
| # Sort by depth (deepest first) to avoid renaming conflicts | |
| all_objects.sort(key=lambda x: x.count('|'), reverse=True) | |
| renamed_count = 0 | |
| for obj in all_objects: | |
| try: | |
| # Get the short name (last part after '|') | |
| short_name = obj.split('|')[-1] | |
| # Convert to lowercase | |
| new_short_name = short_name.lower() | |
| if new_short_name != short_name: | |
| cmds.rename(obj, new_short_name) | |
| renamed_count += 1 | |
| except Exception as e: | |
| print(f"Error renaming '{obj}': {str(e)}") | |
| # Notify user of results | |
| if renamed_count > 0: | |
| cmds.confirmDialog(title="VVLowerCaseAll", message=f"Renamed {renamed_count} objects to lowercase.", button="OK") | |
| else: | |
| cmds.confirmDialog(title="VVLowerCaseAll", message="No objects needed renaming.", button="OK") | |
| # Refresh the UI to reflect changes | |
| refresh_ui() | |
| # Export .fbx Tool (No "Export cancelled" warning) | |
| def export_fbx(): | |
| """Exports all objects in the scene as an FBX file with an autofilled name.""" | |
| maya_file_path = cmds.file(query=True, sceneName=True) | |
| if not maya_file_path: | |
| cmds.warning("Please save the Maya scene before exporting.") | |
| return | |
| model_name = os.path.splitext(os.path.basename(maya_file_path))[0] | |
| default_fbx_name = f"{model_name}_mtb.fbx" | |
| default_directory = os.path.dirname(maya_file_path) | |
| fbx_path = cmds.fileDialog2( | |
| fileMode=0, | |
| caption="Export as FBX", | |
| okCaption="Export", | |
| fileFilter="FBX (*.fbx)", | |
| startingDirectory=default_directory, | |
| dialogStyle=2, | |
| defaultFileName=default_fbx_name | |
| ) | |
| if fbx_path: | |
| cmds.select(all=True) | |
| cmds.file(fbx_path[0], force=True, options="v=0;", type="FBX export", exportSelected=False) | |
| cmds.confirmDialog(title="Export .fbx", message=f"Exported FBX to:\n{fbx_path[0]}", button="OK") | |
| # VVcometRename Tool Functions | |
| def string_replace(input_str, search_str, replace_str): | |
| """Replaces all occurrences of `search_str` with `replace_str` in `input_str`. Supports regular expressions.""" | |
| if not search_str: | |
| return input_str | |
| return re.sub(search_str, replace_str, input_str) | |
| def get_short_name(obj): | |
| """Returns the short name of an object (the part after the last '|').""" | |
| return obj.split('|')[-1] | |
| def do_rename(mode): | |
| """Performs the renaming operation based on the selected mode.""" | |
| if mode == 4: # Global Search and Replace | |
| search = cmds.textField('tfSearch', query=True, text=True) | |
| replace = cmds.textField('tfReplace', query=True, text=True) | |
| if not search: | |
| cmds.warning("Search field is empty!") | |
| return | |
| # Get all objects in the scene and sort them in reverse hierarchical order | |
| all_objs = cmds.ls(long=True) | |
| all_objs.sort(key=lambda x: x.count('|'), reverse=True) # Sort by depth (deepest first) | |
| for obj in all_objs: | |
| try: | |
| short_name = get_short_name(obj) | |
| new_short_name = string_replace(short_name, search, replace) | |
| if new_short_name != short_name: # Only rename if the name changes | |
| new_name = cmds.rename(obj, new_short_name) | |
| print(f"Renamed '{obj}' to '{new_name}'") | |
| except Exception as e: | |
| print(f"Error renaming '{obj}': {str(e)}") | |
| return | |
| # For other modes, ensure objects are selected | |
| selected_objs = cmds.ls(selection=True, long=True) | |
| if not selected_objs: | |
| cmds.warning("No objects selected!") | |
| return | |
| search = cmds.textField('tfSearch', query=True, text=True) | |
| replace = cmds.textField('tfReplace', query=True, text=True) | |
| prefix = cmds.textField('tfPrefix', query=True, text=True) | |
| suffix = cmds.textField('tfSuffix', query=True, text=True) | |
| rename = cmds.textField('tfRename', query=True, text=True) | |
| start = cmds.intField('ifNumber', query=True, value=True) | |
| padding = cmds.intField('ifPadding', query=True, value=True) | |
| # Sort selected objects in reverse hierarchical order | |
| selected_objs.sort(key=lambda x: x.count('|'), reverse=True) | |
| for i, obj in enumerate(selected_objs): | |
| try: | |
| short_name = get_short_name(obj) | |
| new_short_name = "" | |
| if mode == 0: # Search and Replace | |
| if not search: | |
| cmds.warning("Search field is empty!") | |
| return | |
| new_short_name = string_replace(short_name, search, replace) | |
| elif mode == 1: # Prefix | |
| if not prefix: | |
| cmds.warning("Prefix field is empty!") | |
| return | |
| new_short_name = f"{prefix}{short_name}" | |
| elif mode == 2: # Suffix | |
| if not suffix: | |
| cmds.warning("Suffix field is empty!") | |
| return | |
| new_short_name = f"{short_name}{suffix}" | |
| elif mode == 3: # Rename and Number | |
| if not rename: | |
| cmds.warning("Rename field is empty!") | |
| return | |
| number = start + i | |
| padded_number = f"{number:0{padding}d}" | |
| new_short_name = f"{rename}{padded_number}" | |
| # Rename the object | |
| new_name = cmds.rename(obj, new_short_name) | |
| print(f"Renamed '{obj}' to '{new_name}'") | |
| except Exception as e: | |
| print(f"Error renaming '{obj}': {str(e)}") | |
| def vv_comet_rename(): | |
| """Main UI for the VVcometRename tool.""" | |
| if cmds.window('vvCometRenameWin', exists=True): | |
| cmds.deleteUI('vvCometRenameWin') | |
| cmds.window('vvCometRenameWin', title="VVcometRename - 1.20", widthHeight=(310, 400)) | |
| cmds.columnLayout(adjustableColumn=True) | |
| cmds.separator(style='in', height=8) | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 250)]) | |
| cmds.text(label="Search:", align="right") | |
| cmds.textField('tfSearch') | |
| cmds.setParent('..') | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 250)]) | |
| cmds.text(label="Replace:", align="right") | |
| cmds.textField('tfReplace') | |
| cmds.setParent('..') | |
| cmds.separator(style='none', height=4) | |
| cmds.button(label="Search And Replace", command=lambda *args: do_rename(0), annotation="Searches for Search text and replaces with Replace text. Replace CAN be blank to remove text, or CAN be a part of or contain search string in it.") | |
| cmds.button(label="Global Search And Replace", command=lambda *args: do_rename(4), annotation="Searches and replaces text across ALL objects in the scene, without needing to select anything.") | |
| cmds.separator(style='none', height=10) | |
| cmds.separator(style='in', height=8) | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 250)]) | |
| cmds.text(label="Prefix:", align="right") | |
| cmds.textField('tfPrefix') | |
| cmds.setParent('..') | |
| cmds.separator(style='none', height=4) | |
| cmds.button(label="Add Prefix", command=lambda *args: do_rename(1), annotation="Adds prefix text before the current name of each object.") | |
| cmds.separator(style='none', height=10) | |
| cmds.separator(style='in', height=8) | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 250)]) | |
| cmds.text(label="Suffix:", align="right") | |
| cmds.textField('tfSuffix') | |
| cmds.setParent('..') | |
| cmds.separator(style='none', height=4) | |
| cmds.button(label="Add Suffix", command=lambda *args: do_rename(2), annotation="Adds suffix text after the current name of each object.") | |
| cmds.separator(style='none', height=10) | |
| cmds.separator(style='in', height=8) | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 250)]) | |
| cmds.text(label="Rename:", align="right") | |
| cmds.textField('tfRename') | |
| cmds.setParent('..') | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 60)]) | |
| cmds.text(label="Start #:", align="right") | |
| cmds.intField('ifNumber', value=1, minValue=0) | |
| cmds.setParent('..') | |
| cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 50), (2, 60)]) | |
| cmds.text(label="Padding:", align="right") | |
| cmds.intField('ifPadding', value=0, minValue=0) | |
| cmds.setParent('..') | |
| cmds.separator(style='none', height=4) | |
| cmds.button(label="Rename And Number", command=lambda *args: do_rename(3), annotation="Renames each object with the base rename text, then adds a number after each, with the specified number of zero padding in front of the number.") | |
| cmds.separator(style='in', height=8) | |
| cmds.showWindow('vvCometRenameWin') | |
| # Function to create the UI | |
| def create_ui(): | |
| if cmds.window(TOOL_NAME, exists=True): | |
| cmds.deleteUI(TOOL_NAME) | |
| maya_file_path = cmds.file(query=True, sceneName=True) | |
| if not maya_file_path: | |
| cmds.warning("Please save the Maya scene before running this script.") | |
| return | |
| model_name = os.path.splitext(os.path.basename(maya_file_path))[0] | |
| output_directory = os.path.dirname(maya_file_path) | |
| cmds.window(TOOL_NAME, title="VV .qc SFM Generator", width=400) | |
| # Add menu bar | |
| cmds.menuBarLayout() | |
| cmds.menu(label="Tools") | |
| cmds.menuItem(label="ValveBiped NC", command=lambda _: show_valvebiped_window()) | |
| cmds.menuItem(label="VVcometRename", command=lambda _: vv_comet_rename()) | |
| cmds.menuItem(label="VVLowerCaseAll", command=lambda _: vv_lowercase_all()) | |
| cmds.menuItem(label="Export .fbx", command=lambda _: export_fbx()) | |
| cmds.setParent('..') | |
| cmds.columnLayout(adjustableColumn=True) | |
| cmds.button(label="Refresh UI", command=lambda _: refresh_ui()) # Reset to default color | |
| cmds.separator(height=10, style="single") | |
| cmds.text(label="Model Name:") | |
| model_name_field = cmds.textField(text=model_name) | |
| cmds.text(label=".qc File Name:") | |
| qc_file_name_field = cmds.textField(text=f"{model_name}.qc") | |
| cmds.text(label="Output Directory:") | |
| output_path_field = cmds.textField(text=output_directory) | |
| cmds.button(label="Browse", command=lambda _: browse_output_directory(output_path_field)) | |
| cmds.rowLayout(numberOfColumns=3, adjustableColumn=2) | |
| cmds.text(label="Geometry Format:") | |
| geometry_format_radio = cmds.radioButtonGrp( | |
| labelArray2=["SMD", "DMX"], | |
| numberOfRadioButtons=2, | |
| select=1 | |
| ) | |
| class_type_menu = cmds.optionMenuGrp(label="Class") | |
| for class_type in CLASS_TYPES: | |
| cmds.menuItem(label=class_type) | |
| cmds.optionMenuGrp(class_type_menu, edit=True, select=1) # Default to "none" | |
| cmds.setParent('..') | |
| cmds.separator(height=10, style="single") | |
| ANN_MAIN_GEO = "Double-click to remove from main geometry" | |
| ANN_SCENE_GEO = "Double-click to rename geometry" | |
| ANN_JOINTS = "Double-click to rename joint" | |
| ANN_SHADERS = "Double-click to rename shader (* indicates bump map or extra connection)" | |
| cmds.text(label="Main Geometry:") | |
| main_geo_list_field = cmds.textScrollList(allowMultiSelection=True, height=100, | |
| selectCommand=lambda: select_geometry_in_viewport(main_geo_list_field), | |
| doubleClickCommand=lambda: remove_from_main_geo(main_geo_list_field, geometry_list_field, geometry_count_field), | |
| annotation=ANN_MAIN_GEO) | |
| cmds.button(label="Add Selected to Main", command=lambda _: add_to_main_geo(geometry_list_field, main_geo_list_field)) | |
| cmds.button(label="Remove Selected from Main", command=lambda _: remove_from_main_geo(main_geo_list_field, geometry_list_field, geometry_count_field)) | |
| cmds.text(label="Scene Geometry:") | |
| geometry_list_field = cmds.textScrollList(allowMultiSelection=True, height=150, | |
| selectCommand=lambda: select_geometry_in_viewport(geometry_list_field), | |
| doubleClickCommand=lambda: rename_geometry(geometry_list_field, geometry_count_field), | |
| annotation=ANN_SCENE_GEO) | |
| geometry_count_field = cmds.text(label="Objects: 0") | |
| refresh_geometry_list(geometry_list_field, geometry_count_field) | |
| cmds.text(label="Scene Joints:") | |
| joints_list_field = cmds.textScrollList(allowMultiSelection=True, height=150, | |
| selectCommand=lambda: select_joints_in_viewport(joints_list_field), | |
| doubleClickCommand=lambda: rename_joint(joints_list_field, joint_count_field), | |
| annotation=ANN_JOINTS) | |
| cmds.rowLayout(numberOfColumns=1, adjustableColumn=1) | |
| joint_count_field = cmds.text(label="Objects: 0") | |
| cmds.setParent('..') | |
| cmds.button(label="Refresh Joints", command=lambda _: refresh_joint_list(joints_list_field, joint_count_field)) | |
| refresh_joint_list(joints_list_field, joint_count_field) | |
| cmds.rowLayout(numberOfColumns=3, adjustableColumn=2) | |
| material_type_menu = cmds.optionMenuGrp(label="Select Material Type") | |
| for mat in MATERIAL_TYPES: | |
| cmds.menuItem(label=mat) | |
| cmds.optionMenuGrp(material_type_menu, edit=True, select=1) | |
| cmds.columnLayout(adjustableColumn=True) | |
| cmds.rowLayout(numberOfColumns=2) | |
| cmds.text(label=" " * 5 + "$scale") # Add 5 spaces (~30px shift) before $scale | |
| scale_field = cmds.floatField(value=1.0, precision=2, minValue=0.01, width=100) | |
| cmds.setParent('..') | |
| cmds.setParent('..') | |
| generate_vmt_checkbox = cmds.checkBox(label="Generate VMT Files", value=False) | |
| cmds.setParent('..') | |
| cmds.text(label="Shader List:") | |
| shader_list_field = cmds.textScrollList(allowMultiSelection=True, height=150, | |
| selectCommand=lambda: select_shader_members(shader_list_field), | |
| doubleClickCommand=lambda: rename_shader(shader_list_field, shader_count_field), | |
| annotation=ANN_SHADERS) | |
| shader_count_field = cmds.text(label="Shaders: 0") | |
| refresh_shader_list(shader_list_field, shader_count_field) | |
| cmds.separator(height=10, style="single") | |
| cmds.text(label="Material Project (auto-filled based on Model Name and Class):") | |
| cdmaterials_field = cmds.textField(text=f"{get_plural_class(CLASS_TYPES[0])}{model_name}/", editable=False) | |
| # New Animation Include List | |
| cmds.text(label="Include .mdl Animations:") | |
| animation_include_list_field = cmds.textScrollList(allowMultiSelection=True, height=150, | |
| annotation="List of .mdl files to include in the .qc file (e.g., $includemodel)") | |
| cmds.button(label="Add .mdl $includemodel", command=lambda _: add_animation_files(animation_include_list_field)) | |
| cmds.button(label="Remove .mdl $includemodel", command=lambda _: remove_animation_files(animation_include_list_field)) | |
| cmds.text(label="VMT Output Directory:") | |
| vmt_output_path_field = cmds.textField(text="") | |
| cmds.button(label="Browse", command=lambda _: browse_vmt_output_directory(vmt_output_path_field)) | |
| cmds.text(label="") | |
| cmds.button(label="Generate File", command=lambda _: generate_qc_from_ui( | |
| model_name_field, qc_file_name_field, output_path_field, geometry_list_field, | |
| main_geo_list_field, geometry_format_radio, material_type_menu, scale_field, | |
| class_type_menu, vmt_output_path_field, generate_vmt_checkbox, animation_include_list_field | |
| ), backgroundColor=[0.7, 1.0, 0.7]) # Pastel green | |
| cmds.rowLayout(numberOfColumns=1) | |
| cmds.text(label="Version 1.1, please do not distribute", align="center") | |
| cmds.setParent('..') | |
| cmds.showWindow(TOOL_NAME) | |
| # Helper Functions for UI Interactions | |
| def browse_output_directory(output_path_field): | |
| output_path = cmds.fileDialog2(fileMode=3, caption="Select Output Directory", okCaption="Select") | |
| if output_path: | |
| cmds.textField(output_path_field, edit=True, text=output_path[0]) | |
| def browse_vmt_output_directory(vmt_output_path_field): | |
| vmt_output_path = cmds.fileDialog2(fileMode=3, caption="Select VMT Output Directory", okCaption="Select") | |
| if vmt_output_path: | |
| cmds.textField(vmt_output_path_field, edit=True, text=vmt_output_path[0]) | |
| def refresh_geometry_list(geometry_list_field, geometry_count_field): | |
| geometry_list = get_geometry_list() | |
| cmds.textScrollList(geometry_list_field, edit=True, removeAll=True) | |
| for geo in geometry_list: | |
| cmds.textScrollList(geometry_list_field, edit=True, append=geo) | |
| cmds.text(geometry_count_field, edit=True, label=f"Objects: {len(geometry_list)}") | |
| def refresh_main_geo_list(main_geo_list_field): | |
| current_items = cmds.textScrollList(main_geo_list_field, query=True, allItems=True) or [] | |
| cmds.textScrollList(main_geo_list_field, edit=True, removeAll=True) | |
| for item in current_items: | |
| if cmds.objExists(item): | |
| cmds.textScrollList(main_geo_list_field, edit=True, append=item) | |
| def refresh_joint_list(joint_list_field, joint_count_field): | |
| joint_list = get_joint_list() | |
| cmds.textScrollList(joint_list_field, edit=True, removeAll=True) | |
| for joint in joint_list: | |
| cmds.textScrollList(joint_list_field, edit=True, append=cmds.ls(joint, shortNames=True)[0]) | |
| total_joints = len(joint_list) | |
| cmds.text(joint_count_field, edit=True, label=f"Objects: {total_joints}") | |
| def refresh_shader_list(shader_list_field, shader_count_field): | |
| global shader_mapping | |
| shader_mapping.clear() | |
| shader_list = get_shader_list() | |
| cmds.textScrollList(shader_list_field, edit=True, removeAll=True) | |
| for shader in shader_list: | |
| texture_name = get_texture_name(shader) | |
| bumpmap_name = get_bumpmap_name(shader) | |
| cleaned_shader = clean_shader_name(shader, has_texture=bool(texture_name)) | |
| display_name = cleaned_shader | |
| if texture_name: | |
| texture_parts = texture_name.split('_') | |
| shader_suffix = '_'.join(texture_parts[:-1]) # "robot_Blue" | |
| display_name = f"{cleaned_shader}_{shader_suffix}" | |
| ui_display_name = f"{display_name}*" if bumpmap_name else display_name | |
| shader_mapping[display_name] = {'original': shader, 'texture': texture_name, 'bumpmap': bumpmap_name} | |
| cmds.textScrollList(shader_list_field, edit=True, append=ui_display_name) | |
| cmds.text(shader_count_field, edit=True, label=f"Shaders: {len(shader_list)}") | |
| def add_to_main_geo(geometry_list_field, main_geo_list_field): | |
| selected_geo = cmds.textScrollList(geometry_list_field, query=True, selectItem=True) | |
| if selected_geo: | |
| for geo in selected_geo: | |
| cmds.textScrollList(main_geo_list_field, edit=True, append=geo) | |
| cmds.textScrollList(geometry_list_field, edit=True, removeItem=geo) | |
| def remove_from_main_geo(main_geo_list_field, geometry_list_field, geometry_count_field): | |
| selected_geo = cmds.textScrollList(main_geo_list_field, query=True, selectItem=True) | |
| if selected_geo: | |
| for geo in selected_geo: | |
| cmds.textScrollList(main_geo_list_field, edit=True, removeItem=geo) | |
| cmds.textScrollList(geometry_list_field, edit=True, append=geo) | |
| refresh_geometry_list(geometry_list_field, geometry_count_field) | |
| def add_animation_files(animation_include_list_field): | |
| """Opens a file browser to add multiple .mdl files to the animation include list.""" | |
| mdl_files = cmds.fileDialog2(fileMode=4, caption="Select .mdl Animation Files", okCaption="Select", fileFilter="*.mdl") | |
| if mdl_files: | |
| for mdl_file in mdl_files: | |
| # Extract path after 'models/' or use the full relative path if 'models/' not found | |
| mdl_path = mdl_file | |
| if "models/" in mdl_path: | |
| mdl_path = mdl_path.split("models/")[-1] | |
| # Avoid duplicates | |
| current_items = cmds.textScrollList(animation_include_list_field, query=True, allItems=True) or [] | |
| if mdl_path not in current_items: | |
| cmds.textScrollList(animation_include_list_field, edit=True, append=mdl_path) | |
| def remove_animation_files(animation_include_list_field): | |
| """Removes selected .mdl files from the animation include list.""" | |
| selected_items = cmds.textScrollList(animation_include_list_field, query=True, selectItem=True) | |
| if selected_items: | |
| for item in selected_items: | |
| cmds.textScrollList(animation_include_list_field, edit=True, removeItem=item) | |
| def generate_qc_from_ui(model_name_field, qc_file_name_field, output_path_field, geometry_list_field, main_geo_list_field, geometry_format_radio, material_type_menu, scale_field, class_type_menu, vmt_output_path_field, generate_vmt_checkbox, animation_include_list_field): | |
| model_name = cmds.textField(model_name_field, query=True, text=True) | |
| qc_file_name = cmds.textField(qc_file_name_field, query=True, text=True) | |
| output_path = cmds.textField(output_path_field, query=True, text=True) | |
| geometry_list = cmds.textScrollList(geometry_list_field, query=True, allItems=True) or [] | |
| main_geos = cmds.textScrollList(main_geo_list_field, query=True, allItems=True) or [] | |
| geometry_format = "smd" if cmds.radioButtonGrp(geometry_format_radio, query=True, select=True) == 1 else "dmx" | |
| material_type = cmds.optionMenuGrp(material_type_menu, query=True, value=True) | |
| scale_value = cmds.floatField(scale_field, query=True, value=True) | |
| class_type = cmds.optionMenuGrp(class_type_menu, query=True, value=True) | |
| vmt_output_path = cmds.textField(vmt_output_path_field, query=True, text=True) | |
| generate_vmt = cmds.checkBox(generate_vmt_checkbox, query=True, value=True) | |
| animation_includes = cmds.textScrollList(animation_include_list_field, query=True, allItems=True) or [] | |
| if not model_name or not qc_file_name or not output_path or not geometry_list or not main_geos: | |
| cmds.warning("Please fill in all fields and ensure geometry is selected.") | |
| return | |
| if generate_vmt and not vmt_output_path: | |
| cmds.warning("Please select a VMT Output Directory when 'Generate VMT Files' is checked.") | |
| return | |
| generate_qc_file(output_path, qc_file_name, model_name, main_geos, geometry_list, geometry_format, material_type, scale_value, class_type, vmt_output_path if generate_vmt else None, generate_vmt, animation_includes) | |
| # Entry Point | |
| if __name__ == "__main__": | |
| create_ui() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment