Skip to content

Instantly share code, notes, and snippets.

@Pakmanv
Created May 5, 2025 05:27
Show Gist options
  • Select an option

  • Save Pakmanv/e210a01dd2e082a1ed8a7160b278ddcf to your computer and use it in GitHub Desktop.

Select an option

Save Pakmanv/e210a01dd2e082a1ed8a7160b278ddcf to your computer and use it in GitHub Desktop.
vvqcsfmgenerator
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