This commit is contained in:
2025-05-02 00:14:28 +08:00
commit 6f27dc11e3
132 changed files with 28609 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

436
scripts/builder/builder.py Normal file
View File

@@ -0,0 +1,436 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import traceback
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
from maya import cmds, mel
from ..builder.maya.util import Maya
from ..common import DNAViewerError
from ..dnalib.dnalib import DNA
from ..model import Joint as JointModel
from .config import AngleUnit, Config, LinearUnit
from .joint import Joint as JointBuilder
from .mesh import Mesh
@dataclass
class BuildResult:
"""
A class used for returning data after finishing the build process
Attributes
----------
@type meshes_per_lod: Dict[int, List[str]]
@param meshes_per_lod: The list of mesh names created group by LOD number
"""
meshes_per_lod: Dict[int, List[str]] = field(default_factory=dict)
def get_all_meshes(self) -> List[str]:
"""
Flatten meshes to single list.
@rtype: List[str]
@returns: The list of all mesh names.
"""
all_meshes = []
for meshes_per_lod in self.meshes_per_lod.values():
all_meshes.extend(meshes_per_lod)
return all_meshes
class Builder:
"""
A builder class used for building the character
Attributes
----------
@type config: Config
@param config: The configuration options used for building the character
@type dna: DNA
@param dna: The DNA object read from the DNA file
@type meshes: Dict[int, List[str]]
@param meshes: A list of meshes created grouped by lod
"""
def __init__(self, dna: DNA, config: Optional[Config] = None) -> None:
self.config = config or Config()
self.dna = dna
self.meshes: Dict[int, List[str]] = {}
self.all_loaded_meshes: List[int] = []
def _build(self) -> bool:
self.new_scene()
self.set_filtered_meshes()
if not self.all_loaded_meshes:
logging.error("No mashes has been loaded.")
return False
self.create_groups()
self.set_units()
self.add_joints()
self.build_meshes()
self.add_ctrl_attributes_on_root_joint()
self.add_animated_map_attributes_on_root_joint()
self.add_key_frames()
return True
def build(self) -> BuildResult:
"""Builds the character"""
self.meshes = {}
try:
filename = Path(self.dna.path).stem
logging.info("******************************")
logging.info(f"{filename} started building")
logging.info("******************************")
self._build()
logging.info(f"{filename} built successfully!")
except DNAViewerError as e:
traceback.print_exc()
raise e
except Exception as e:
traceback.print_exc()
logging.error(f"Unhandled exception, {e}")
raise DNAViewerError(f"Scene creation failed! Reason: {e}") from e
return BuildResult(meshes_per_lod=self.meshes)
def new_scene(self) -> None:
cmds.file(new=True, force=True)
def add_mesh_to_display_layer(self, mesh_name: str, lod: int) -> None:
"""
Add the mesh with the given name to an already created display layer.
@type mesh_name: str
@param mesh_name: The name of the mesh that should be added to a display layer.
@type lod: int
@param lod: The lod value, this is needed for determining the name of the display layer that the mesh should be added to.
"""
if self.config.create_display_layers:
cmds.editDisplayLayerMembers(
f"{self.config.top_level_group}_lod{lod}_layer", mesh_name
)
def _add_joints(self) -> List[JointModel]:
"""
Reads and adds the joints to the scene, also returns a list model objects of joints that were added.
@rtype: List[JointModel]
@returns: The list containing model objects representing the joints that were added to the scene.
"""
joints: List[JointModel] = self.dna.read_all_neutral_joints()
builder = JointBuilder(
joints,
)
builder.process()
return joints
def add_joints(self) -> None:
"""
Starts adding the joints the character, if the character configuration options have add_joints set to False,
this step will be skipped.
"""
if self.config.add_joints:
logging.info("adding joints to character...")
joints = self._add_joints()
if self.config.group_by_lod and joints:
cmds.parent(joints[0].name, self.config.get_top_level_group())
def create_groups(self) -> None:
"""
Creates a Maya transform which will hold the character, if the character configuration options have
create_character_node set to False, this step will be skipped.
"""
if self.config.group_by_lod:
logging.info("building character node...")
cmds.group(world=True, empty=True, name=self.config.get_top_level_group())
cmds.group(
parent=self.config.get_top_level_group(),
empty=True,
name=self.config.get_geometry_group(),
)
cmds.group(
parent=self.config.get_top_level_group(),
empty=True,
name=self.config.get_rig_group(),
)
for lod in self.get_display_layers():
name = f"{self.config.top_level_group}_lod{lod}_layer"
if not cmds.objExists(name):
if self.config.group_by_lod:
cmds.group(
parent=self.config.get_geometry_group(),
empty=True,
name=f"{self.config.top_level_group}_lod{lod}_grp",
)
cmds.select(
f"{self.config.top_level_group}_lod{lod}_grp",
replace=True,
)
if self.config.create_display_layers:
cmds.createDisplayLayer(name=name, noRecurse=True)
def attach_mesh_to_lod(self, mesh_name: str, lod: int) -> None:
"""
Attaches the mesh called mesh_name to a given lod.
@type mesh_name: str
@param mesh_name: The mesh that needs to be attached to a lod holder object.
@type lod: str
@param lod: The name of the mesh that should be added to a display layer.
"""
if self.config.group_by_lod:
parent_node = f"{self.config.get_top_level_group()}|{self.config.get_geometry_group()}|{self.config.top_level_group}_lod{lod}_grp"
cmds.parent(
self.get_mesh_node_fullpath_on_root(mesh_name=mesh_name), parent_node
)
def get_mesh_node_fullpath_on_root(self, mesh_name: str) -> str:
"""
Gets the full path in the scene of a mesh.
@type mesh_name: str
@param mesh_name: The mesh thats path is needed.
@rtype: str
@returns: The full path of the mesh object in the scene
"""
return str(Maya.get_element(f"|{mesh_name}").fullPathName())
def add_ctrl_attributes_on_root_joint(self) -> None:
"""
Adds and sets the raw gui control attributes on root joint.
"""
if self.config.add_ctrl_attributes_on_root_joint and self.config.add_joints:
gui_control_names = self.dna.get_raw_control_names()
for name in gui_control_names:
ctrl_and_attr_names = name.split(".")
self.add_attribute(
control_name=self.config.facial_root_joint_name,
long_name=ctrl_and_attr_names[1],
)
def add_animated_map_attributes_on_root_joint(self) -> None:
"""
Adds and sets the animated map attributes on root joint.
"""
if (
self.config.add_animated_map_attributes_on_root_joint
and self.config.add_joints
):
names = self.dna.get_animated_map_names()
for name in names:
long_name = name.replace(".", "_")
self.add_attribute(
control_name=self.config.facial_root_joint_name, long_name=long_name
)
def add_attribute(self, control_name: str, long_name: str) -> None:
"""
Adds attributes wrapper for internal usage.
"""
cmds.addAttr(
control_name,
longName=long_name,
keyable=True,
attributeType="float",
minValue=0.0,
maxValue=1.0,
)
def add_key_frames(self) -> None:
"""
Adds a starting key frame to the facial root joint if joints are added and the add_key_frames option is set
to True.
"""
if self.config.add_key_frames and self.config.add_joints:
logging.info("setting keyframe on the root joint...")
cmds.currentTime(0)
if cmds.objExists(self.config.facial_root_joint_name):
cmds.select(self.config.facial_root_joint_name, replace=True)
cmds.setKeyframe(inTangentType="linear", outTangentType="linear")
def set_filtered_meshes(self) -> None:
self.all_loaded_meshes = self.get_filtered_meshes()
def get_mesh_indices_filter(self) -> List[int]:
indices = []
for index in range(self.dna.get_mesh_count()):
mesh_name = self.dna.get_mesh_name(index)
for cur_filter in self.config.mesh_filter:
if cur_filter in mesh_name:
indices.append(index)
return indices
def get_filtered_meshes(self) -> List[int]:
if not self.config.mesh_filter and not self.config.lod_filter:
if self.config.meshes:
return self.config.meshes
return list(range(self.dna.get_mesh_count()))
meshes: List[int] = []
meshes_by_lod = self.dna.get_all_meshes_grouped_by_lod()
all_meshes = [mesh_index for meshes in meshes_by_lod for mesh_index in meshes]
mesh_indices_filter = self.get_mesh_indices_filter()
if self.config.lod_filter:
for lod in self.config.lod_filter:
if 0 <= lod < len(meshes_by_lod):
meshes.extend(meshes_by_lod[lod])
if mesh_indices_filter:
return list(set(meshes) & set(mesh_indices_filter))
return meshes
if self.config.mesh_filter:
return list(set(all_meshes) & set(mesh_indices_filter))
return all_meshes
def build_meshes(self) -> None:
"""
Builds the meshes. If specified in the config they get parented to a created
character node transform, otherwise the meshes get put to the root level of the scene.
"""
logging.info("adding character meshes...")
self.meshes = {}
for lod, meshes_per_lod in enumerate(
self.dna.get_meshes_by_lods(self.all_loaded_meshes)
):
self.meshes[lod] = self.build_meshes_by_lod(
lod=lod, meshes_per_lod=meshes_per_lod
)
def build_meshes_by_lod(self, lod: int, meshes_per_lod: List[int]) -> List[str]:
"""
Builds the meshes from the provided mesh ids and then attaches them to a given lod if specified in the
character configuration.
@type lod: int
@param lod: The lod number representing the display layer the meshes to the display layer.
@type meshes_per_lod: List[int]
@param meshes_per_lod: List of mesh indices that are being built.
@rtype: List[MObject]
@returns: The list of maya objects that represent the meshes added to the scene.
"""
meshes: List[str] = []
for mesh_index in meshes_per_lod:
builder = Mesh(
config=self.config,
dna=self.dna,
mesh_index=mesh_index,
)
builder.build()
mesh_name = self.dna.get_mesh_name(index=mesh_index)
meshes.append(mesh_name)
self.add_mesh_to_display_layer(mesh_name, lod)
self.attach_mesh_to_lod(mesh_name, lod)
self.default_lambert_shader(mesh_name)
return meshes
def default_lambert_shader(self, mesh_name: str) -> None:
try:
if self.config.group_by_lod:
names = cmds.ls(f"*|{mesh_name}", l=True)
for item in names:
if item.startswith(f"|{self.config.get_top_level_group()}"):
cmds.select(item, r=True)
break
else:
cmds.select(mesh_name, r=True)
mel.eval("sets -e -forceElement initialShadingGroup")
except Exception as e:
logging.error(
f"Couldn't set lambert shader for mesh {mesh_name}. Reason: {e}"
)
raise DNAViewerError(e) from e
def set_units(self) -> None:
"""Sets the translation and rotation units of the scene from @config"""
linear_unit = self.get_linear_unit()
angle_unit = self.get_angle_unit()
cmds.currentUnit(linear=linear_unit.name, angle=angle_unit.name)
def get_linear_unit(self) -> LinearUnit:
return self.get_linear_unit_from_int(self.dna.get_translation_unit())
def get_angle_unit(self) -> AngleUnit:
return self.get_angle_unit_from_int(self.dna.get_rotation_unit())
def get_linear_unit_from_int(self, value: int) -> LinearUnit:
"""
Returns an enum from an int value.
0 -> cm
1 -> m
@type value: int
@param value: The value that the enum is mapped to.
@rtype: LinearUnit
@returns: LinearUnit.cm or LinearUnit.m
"""
if value == 0:
return LinearUnit.cm
if value == 1:
return LinearUnit.m
raise DNAViewerError(f"Unknown linear unit set in DNA file! value {value}")
def get_angle_unit_from_int(self, value: int) -> AngleUnit:
"""
Returns an enum from an int value.
0 -> degree
1 -> radian
@type value: int
@param value: The value that the enum is mapped to.
@rtype: AngleUnit
@returns: AngleUnit.degree or AngleUnit.radian
"""
if value == 0:
return AngleUnit.degree
if value == 1:
return AngleUnit.radian
raise DNAViewerError(f"Unknown angle unit set in DNA file! value {value}")
def get_display_layers(self) -> List[int]:
"""Gets a lod id list that need to be created for the meshes from @config"""
meshes: List[int] = []
for idx, meshes_per_lod in enumerate(
self.dna.get_meshes_by_lods(self.all_loaded_meshes)
):
if meshes_per_lod:
meshes.append(idx)
return list(set(meshes))

260
scripts/builder/config.py Normal file
View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
class LinearUnit(Enum):
"""
An enum used to represent the unit used for linear representation.
Attributes
----------
@cm: using cm as unit
@m: using m as unit
"""
cm = 0
m = 1
class AngleUnit(Enum):
"""
An enum used to represent the unit used for angle representation.
Attributes
----------
@degree: using degree as unit
@radian: using radian as unit
"""
degree = 0
radian = 1
@dataclass
class Config:
"""
A class used to represent the config for @Builder
Attributes
----------
@type mesh_filter: List[str]
@param mesh_filter: List of mesh names that should be filtered. Mash names can be just substrings. ["head"] will find all meshes that contins string "head" in its mash name.
@type lod_filter: List[int]
@param lod_filter: List of lods that should be filtered.
@type group_by_lod: bool
@param group_by_lod: A flag representing whether the character should be parented to a character transform node in the scene hierarchy
@type group_by_lod: bool
@param group_by_lod: A flag representing whether the character should be parented to a character transform node in rig hierarchy
@type top_level_group: str
@param top_level_group: Value that is going to be used when creating root group
@type geometry_group: str
@param geometry_group: Value that is going to be used when creating group that contains geometry
@type facial_root_joint_name: str
@param facial_root_joint_name: The name of the facial root joint
@type blend_shape_group_prefix: str
@param blend_shape_group_prefix: prefix string for blend shape group
@type blend_shape_name_postfix: str
@param blend_shape_name_postfix: postfix string for blend shape name
@type skin_cluster_suffix: str
@param skin_cluster_suffix: postfix string for skin cluster name
@type animated_map_attribute_multipliers_name: str
@param animated_map_attribute_multipliers_name: string for frame animated map attribute name
@type create_display_layers: bool
@param create_display_layers: A flag representing whether the created meshes should be assigned to a display layer
@type add_joints: bool
@param add_joints: A flag representing whether joints should be added
@type add_blend_shapes: bool
@param add_blend_shapes: A flag representing whether blend shapes should be added
@type add_skin_cluster: bool
@param add_skin_cluster: A flag representing whether skin should be added
@type add_ctrl_attributes_on_root_joint: bool
@param add_ctrl_attributes_on_root_joint: A flag representing whether control attributes should be added to the root joint
@type add_animated_map_attributes_on_root_joint: bool
@param add_animated_map_attributes_on_root_joint: A flag representing whether animated map attributes should be added to the root joint
@type add_key_frames: bool
@param add_key_frames: A flag representing whether key frames should be added
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
meshes: List[int] = field(default_factory=list)
mesh_filter: List[str] = field(default_factory=list)
lod_filter: List[int] = field(default_factory=list)
group_by_lod: bool = field(default=True)
top_level_group: str = "head"
geometry_group: str = "geometry"
facial_root_joint_name: str = "FACIAL_C_FacialRoot"
blend_shape_group_prefix: str = "BlendshapeGroup_"
blend_shape_name_postfix: str = "_blendShapes"
skin_cluster_suffix: str = "skinCluster"
animated_map_attribute_multipliers_name = "FRM_WMmultipliers"
create_display_layers: bool = field(default=True)
add_joints: bool = field(default=True)
add_blend_shapes: bool = field(default=True)
add_skin_cluster: bool = field(default=True)
add_ctrl_attributes_on_root_joint: bool = field(default=True)
add_animated_map_attributes_on_root_joint: bool = field(default=True)
add_key_frames: bool = field(default=True)
add_mesh_name_to_blend_shape_channel_name: bool = field(default=True)
def get_top_level_group(self) -> str:
return f"{self.top_level_group}_grp"
def get_geometry_group(self) -> str:
return f"{self.geometry_group}_grp"
def get_rig_group(self) -> str:
return f"{self.top_level_group}Rig_grp"
@dataclass
class RigConfig(Config):
"""
A class used to represent the config for @RigBuilder
@type add_rig_logic: bool
@param add_rig_logic: A flag representing whether normals should be added
@type rig_logic_command: str
@param rig_logic_command: The command used to start creating the rig logic using the plugin
@type rig_logic_name: str
@param rig_logic_name: The name of the rig logic node
@type control_naming: str
@param control_naming: The naming pattern of controls
@type joint_naming: str
@param joint_naming: The naming pattern of joints
@type blend_shape_naming: str
@param blend_shape_naming: The naming pattern of blend shapes
@type animated_map_naming: str
@param animated_map_naming: The naming pattern of animated maps
@type gui_path: str
@param gui_path: The location of the gui file
@type left_eye_joint_name: str
@param left_eye_joint_name: The name of the left eye joint
@type eye_gui_name: str
@param eye_gui_name: The name of the control in the gui
@type gui_translate_x: float
@param gui_translate_x: Represents the value that the gui should be additionally translated on the X axis
@type analog_gui_path: str
@param analog_gui_path: The location of the analog gui file
@type left_eye_joint_name: str
@param left_eye_joint_name: The name of the left eye joint
@type right_eye_joint_name: str
@param right_eye_joint_name: The name of the right eye joint
@type central_driver_name: str
@param central_driver_name: The name of the central driver
@type left_eye_driver_name: str
@param left_eye_driver_name: The name of the left eye driver
@type right_eye_driver_name: str
@param right_eye_driver_name: The name of the right eye driver
@type central_aim: str
@param central_aim: The name of the central aim
@type le_aim: str
@param le_aim: The name of the left eye aim
@type re_aim: str
@param re_aim: The name of the right eye aim
@type aas_path: Optional[str]
@param aas_path: The location of the script file
@type aas_method: str
@param aas_method: The method that should be called
@type aas_parameter: Dict[Any, Any]
@param aas_parameter: The parameters that will be passed as the method arguments
"""
add_rig_logic: bool = field(default=True)
rig_logic_command: str = field(default="createEmbeddedNodeRL4")
rig_logic_name: str = field(default="")
control_naming: str = field(default="<objName>.<attrName>")
joint_naming: str = field(default="<objName>.<attrName>")
blend_shape_naming: str = field(default="")
animated_map_naming: str = field(default="")
gui_path: str = field(default=None)
eye_gui_name: str = "CTRL_C_eye"
gui_translate_x: float = 10
analog_gui_path: str = field(default=None)
left_eye_joint_name: str = "FACIAL_L_Eye"
right_eye_joint_name: str = "FACIAL_R_Eye"
central_driver_name: str = "LOC_C_eyeDriver"
left_eye_driver_name: str = "LOC_L_eyeDriver"
right_eye_driver_name: str = "LOC_R_eyeDriver"
left_eye_aim_up_name: str = "LOC_L_eyeAimUp"
right_eye_aim_up_name: str = "LOC_R_eyeAimUp"
central_aim: str = "GRP_C_eyesAim"
le_aim: str = "GRP_L_eyeAim"
re_aim: str = "GRP_R_eyeAim"
aas_path: Optional[str] = field(default=None)
aas_method: str = "run_after_assemble"
aas_parameter: Dict[Any, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.add_mesh_name_to_blend_shape_channel_name:
self.blend_shape_naming = (
f"<objName>{self.blend_shape_name_postfix}.<objName>__<attrName>"
)
else:
self.blend_shape_naming = (
f"<objName>{self.blend_shape_name_postfix}.<attrName>"
)
self.animated_map_naming = (
f"{self.animated_map_attribute_multipliers_name}.<objName>_<attrName>"
)

81
scripts/builder/joint.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Dict, List
from maya import cmds
from ..model import Joint as JointModel
class Joint:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type joints: List[JointModel]
@param joints: data representing the joints
@type joint_flags: Dict[str, bool]
@param joint_flags: A mapping used for setting flags that are used to avoid adding the same joint multiple times
"""
def __init__(self, joints: List[JointModel]) -> None:
self.joints = joints
self.joint_flags: Dict[str, bool] = {}
for joint in self.joints:
self.joint_flags[joint.name] = False
def add_joint_to_scene(self, joint: JointModel) -> None:
"""
Adds the given joint to the scene
@type joint: JointModel
@param joint: The joint to be added to the scene
"""
if self.joint_flags[joint.name]:
return
in_parent_space = True
if cmds.objExists(joint.parent_name):
cmds.select(joint.parent_name)
else:
if joint.name != joint.parent_name:
parent_joint = next(
j for j in self.joints if j.name == joint.parent_name
)
self.add_joint_to_scene(parent_joint)
else:
# this is the first node
cmds.select(d=True)
in_parent_space = False
position = (
joint.translation.x,
joint.translation.y,
joint.translation.z,
)
orientation = (
joint.orientation.x,
joint.orientation.y,
joint.orientation.z,
)
cmds.joint(
p=position,
o=orientation,
n=joint.name,
r=in_parent_space,
a=not in_parent_space,
scaleCompensate=False,
)
self.joint_flags[joint.name] = True
def process(self) -> None:
"""Starts adding all the provided joints to the scene"""
for joint in self.joints:
self.add_joint_to_scene(joint)

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *

View File

@@ -0,0 +1,424 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from dataclasses import dataclass, field
from typing import List, Tuple
from maya import cmds
from maya.api.OpenMaya import MDagModifier, MFnDagNode, MFnMesh, MObject, MPoint
from ...builder.maya.util import Maya
from ...common import SKIN_WEIGHT_PRINT_RANGE
from ...dnalib.dnalib import DNA
from ...model import Point3
@dataclass
class Mesh:
"""
A model class for holding data needed in the mesh building process
Attributes
----------
@type dna_vertex_positions: List[Point3]
@param dna_vertex_positions: Data representing the positions of the vertices
@type dna_vertex_layout_positions: List[int]
@param dna_vertex_layout_positions: Data representing layout position indices of vertices
@type polygon_faces: List[int]
@param polygon_faces: List of lengths of vertex layout indices
@type polygon_connects: List[int]
@param polygon_connects: List of vertex layout position indices
@type derived_mesh_names: List[str]
@param derived_mesh_names: List of mesh names
"""
dna_vertex_positions: List[Point3] = field(default_factory=list)
dna_vertex_layout_positions: List[int] = field(default_factory=list)
polygon_faces: List[int] = field(default_factory=list)
polygon_connects: List[int] = field(default_factory=list)
derived_mesh_names: List[str] = field(default_factory=list)
class MayaMesh:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type mesh_index: int
@param mesh_index: The index of the mesh
@type dna: DNA
@param dna: Instance of DNA
@type blend_shape_group_prefix: str
@param blend_shape_group_prefix: prefix string for blend shape group
@type blend_shape_name_postfix: str
@param blend_shape_name_postfix: postfix string for blend shape name
@type skin_cluster_suffix: str
@param skin_cluster_suffix: postfix string for skin cluster name
@type data: Mesh
@param data: mesh data used in the mesh creation process
@type fn_mesh: om.MFnMesh
@param fn_mesh: OpenMaya class used for creating the mesh
@type mesh_object: om.MObject
@param mesh_object: the object representing the mesh
@type dag_modifier: om.MDagModifier
@param dag_modifier: OpenMaya class used for naming the mesh
"""
def __init__(
self,
mesh_index: int,
dna: DNA,
blend_shape_group_prefix: str,
blend_shape_name_postfix: str,
skin_cluster_suffix: str,
) -> None:
self.mesh_index = mesh_index
self.data: Mesh = Mesh()
self.fn_mesh = MFnMesh()
self.mesh_object: MObject = None
self.dag_modifier: MDagModifier = None
self.dna = dna
self.blend_shape_group_prefix = blend_shape_group_prefix
self.blend_shape_name_postfix = blend_shape_name_postfix
self.skin_cluster_suffix = skin_cluster_suffix
def create_neutral_mesh(self) -> MObject:
"""
Creates the neutral mesh using the config provided for this builder class object
@rtype: om.MObject
@returns: the instance of the created mesh object
"""
self.prepare_mesh()
self.mesh_object = self.create_mesh_object()
self.dag_modifier = self.rename_mesh()
self.add_texture_coordinates()
return self.mesh_object
def create_mesh_object(self) -> MObject:
"""
Gets a list of points that represent the vertex positions.
@rtype: MObject
@returns: Maya objects representing maya mesh functions and the created maya mesh object.
"""
mesh_object = self.fn_mesh.create(
self.get_vertex_positions_from_dna_vertex_positions(),
self.data.polygon_faces,
self.data.polygon_connects,
)
return mesh_object
def get_vertex_positions_from_dna_vertex_positions(self) -> List[MPoint]:
"""
Gets a list of points that represent the vertex positions.
@rtype: List[MPoint]
@returns: List of maya point objects.
"""
vertex_positions = []
for position in self.data.dna_vertex_positions:
vertex_positions.append(
MPoint(
position.x,
position.y,
position.z,
)
)
return vertex_positions
def rename_mesh(self) -> MDagModifier:
"""
Renames the initial mesh object that was created to the name from the configuration.
@rtype: Tuple[MDagModifier]
@returns: Maya object representing the dag modifier.
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
dag_modifier = MDagModifier()
dag_modifier.renameNode(self.mesh_object, mesh_name)
dag_modifier.doIt()
return dag_modifier
def prepare_mesh(self) -> None:
"""
Gets a list of points that represent the vertex positions.
"""
logging.info("==============================")
mesh_name = self.dna.get_mesh_name(self.mesh_index)
logging.info(f"adding mesh: {mesh_name}")
self.data.dna_vertex_positions = self.dna.get_vertex_positions_for_mesh_index(
self.mesh_index
)
self.data.dna_vertex_layout_positions = (
self.dna.get_vertex_layout_positions_for_mesh_index(self.mesh_index)
)
(
self.data.polygon_faces,
self.data.polygon_connects,
) = self.dna.get_polygon_faces_and_connects(self.mesh_index)
def add_texture_coordinates(self) -> None:
"""
Method for adding texture coordinates.
"""
logging.info("adding texture coordinates...")
(
texture_coordinate_us,
texture_coordinate_vs,
texture_coordinate_indices,
) = self.get_texture_data()
self.fn_mesh.setUVs(texture_coordinate_us, texture_coordinate_vs)
self.fn_mesh.assignUVs(self.data.polygon_faces, texture_coordinate_indices)
mesh_name = self.dna.get_mesh_name(self.mesh_index)
cmds.select(mesh_name, replace=True)
cmds.polyMergeUV(mesh_name, distance=0.01, constructionHistory=False)
def get_texture_data(self) -> Tuple[List[float], List[float], List[int]]:
"""
Gets the data needed for the creation of textures.
@rtype: Tuple[List[float], List[float], List[int]] @returns: The tuple containing the list of texture
coordinate Us, the list of texture coordinate Vs and the list of texture coordinate indices.
"""
texture_coordinates = self.dna.get_vertex_texture_coordinates_for_mesh(
self.mesh_index
)
dna_faces = self.dna.get_faces(self.mesh_index)
coordinate_indices = []
for layout_id in range(
len(self.dna.get_layouts_for_mesh_index(self.mesh_index))
):
coordinate_indices.append(
self.dna.get_texture_coordinate_index(self.mesh_index, layout_id)
)
texture_coordinate_us = []
texture_coordinate_vs = []
texture_coordinate_indices = []
index_counter = 0
for vertices_layout_index_array in dna_faces:
for vertex_layout_index_array in vertices_layout_index_array:
texture_coordinate = texture_coordinates[
coordinate_indices[vertex_layout_index_array]
]
texture_coordinate_us.append(texture_coordinate.u)
texture_coordinate_vs.append(texture_coordinate.v)
texture_coordinate_indices.append(index_counter)
index_counter += 1
return texture_coordinate_us, texture_coordinate_vs, texture_coordinate_indices
def add_blend_shapes(self, add_mesh_name_to_blend_shape_channel_name: bool) -> None:
"""Adds blend shapes to the mesh"""
if self.dna.has_blend_shapes(self.mesh_index):
self.create_blend_shapes(add_mesh_name_to_blend_shape_channel_name)
self.create_blend_shape_node()
def create_blend_shape_node(self) -> None:
"""
Creates a blend shape node.
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
nodes = []
for derived_mesh_name in self.data.derived_mesh_names:
nodes.append(derived_mesh_name)
cmds.select(nodes, replace=True)
cmds.select(mesh_name, add=True)
cmds.blendShape(name=f"{mesh_name}{self.blend_shape_name_postfix}")
cmds.delete(f"{self.blend_shape_group_prefix}{mesh_name}")
def create_blend_shapes(
self, add_mesh_name_to_blend_shape_channel_name: bool
) -> None:
"""
Builds all the derived meshes using the provided mesh and the blend shapes data of the DNA.
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
logging.info("adding derived meshes...")
group: str = cmds.group(
empty=True,
name=f"{self.blend_shape_group_prefix}{self.dna.get_mesh_name(self.mesh_index)}",
)
self.data.derived_mesh_names = []
blend_shapes = self.dna.get_blend_shapes(self.mesh_index)
for blend_shape_target_index, blend_shape in enumerate(blend_shapes):
self.create_blend_shape(
blend_shape_target_index,
blend_shape.channel,
group,
add_mesh_name_to_blend_shape_channel_name,
)
cmds.setAttr(f"{group}.visibility", 0)
def create_blend_shape(
self,
blend_shape_target_index: int,
blend_shape_channel: int,
group: str,
add_mesh_name_to_blend_shape_channel_name: bool,
) -> None:
"""
Builds a single derived mesh using the provided mesh and the blend shape data of the DNA.
@type blend_shape_target_index: int
@param blend_shape_target_index: Used for getting a delta value representing the value change concerning the blend shape.
@type blend_shape_channel: int
@param blend_shape_channel: Used for getting the blend shape name from the DNA.
@type group: str
@param group: The transform the new meshes will be added to.
@type add_mesh_name_to_blend_shape_channel_name: bool
@param add_mesh_name_to_blend_shape_channel_name: A flag representing whether mesh name of blend shape channel is added to name when creating it
"""
new_vert_layout = self.get_vertex_positions_from_dna_vertex_positions()
zipped_deltas = self.dna.get_blend_shape_target_deltas_with_vertex_id(
self.mesh_index, blend_shape_target_index
)
for zipped_delta in zipped_deltas:
delta: Point3 = zipped_delta[1]
new_vert_layout[zipped_delta[0]] += MPoint(
delta.x,
delta.y,
delta.z,
)
new_mesh = self.fn_mesh.create(
new_vert_layout, self.data.polygon_faces, self.data.polygon_connects
)
derived_name = self.dna.get_blend_shape_channel_name(blend_shape_channel)
name = (
f"{self.dna.geometry_meshes[self.mesh_index].name}__{derived_name}"
if add_mesh_name_to_blend_shape_channel_name
else derived_name
)
self.dag_modifier.renameNode(new_mesh, name)
self.dag_modifier.doIt()
dag = MFnDagNode(Maya.get_element(group))
dag.addChild(new_mesh)
self.data.derived_mesh_names.append(name)
def add_skin_cluster(self, joint_names: List[str], joint_ids: List[int]) -> None:
"""
Adds skin cluster to the mesh
@type joint_names: List[str]
@param joint_names: Joint names needed for adding the skin cluster
@type joint_ids: List[int]
@param joint_ids: Joint indices needed for setting skin weights
"""
mesh_name = self.dna.get_mesh_name(self.mesh_index)
self._add_skin_cluster(mesh_name, joint_names)
self.set_skin_weights(mesh_name, joint_ids)
def _add_skin_cluster(self, mesh_name: str, joint_names: List[str]) -> None:
"""
Creates a skin cluster object.
@type mesh_name: str
@param mesh_name: The mesh name that is used for skin cluster naming.
@type joints: List[Joint]
@param joints: List of joints used for adding the skin cluster.
"""
logging.info("adding skin cluster...")
maximum_influences = self.dna.get_maximum_influence_per_vertex(self.mesh_index)
cmds.select(joint_names[0], replace=True)
cmds.select(mesh_name, add=True)
skin_cluster = cmds.skinCluster(
toSelectedBones=True,
name=f"{mesh_name}_{self.skin_cluster_suffix}",
maximumInfluences=maximum_influences,
skinMethod=0,
obeyMaxInfluences=True,
)
cmds.skinCluster(
skin_cluster, edit=True, addInfluence=joint_names[1:], weight=0
)
def set_skin_weights(self, mesh_name: str, joint_ids: List[int]) -> None:
"""
Sets the skin weights attributes.
@type mesh_name: str
@param mesh_name: The mesh name that is used for getting the skin cluster name.
@type joint_ids: List[int]
@param joint_ids: List of joint indices used for setting the skin weight attribute.
"""
logging.info("adding skin weights...")
skin_weights = self.dna.get_skin_weight_matrix_for_mesh(self.mesh_index)
# import skin weights
temp_str = f"{mesh_name}_{self.skin_cluster_suffix}.wl["
for vertex_id, skin_weight in enumerate(skin_weights):
if not (vertex_id + 1) % SKIN_WEIGHT_PRINT_RANGE:
logging.info(f"\t{vertex_id + 1} / {len(skin_weights)}")
vertex_infos = skin_weight
# set all skin weights to zero
vertex_string = f"{temp_str}{str(vertex_id)}].w["
cmds.setAttr(f"{vertex_string}0]", 0.0)
# import skin weights
for vertex_info in vertex_infos:
cmds.setAttr(
f"{vertex_string}{str(joint_ids.index(vertex_info[0]))}]",
float(vertex_info[1]),
)
if len(skin_weights) % SKIN_WEIGHT_PRINT_RANGE != 0:
logging.info(f"\t{len(skin_weights)} / {len(skin_weights)}")

View File

@@ -0,0 +1,204 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from typing import List, Tuple, Union
from maya import cmds, mel
from maya.api.OpenMaya import MFnMesh, MGlobal
from maya.api.OpenMayaAnim import MFnSkinCluster
from ...builder.maya.util import Maya
from ...common import DNAViewerError
class MayaSkinWeights:
"""
A class used for reading and storing skin weight related data needed for adding skin clusters
"""
no_of_influences: int
skinning_method: int
joints: List[str]
vertices_info: List[List[Union[int, float]]]
def __init__(self, skin_cluster: MFnSkinCluster, mesh_name: str) -> None:
self.no_of_influences = cmds.skinCluster(skin_cluster.name(), q=True, mi=True)
self.skinning_method = cmds.skinCluster(skin_cluster.name(), q=True, sm=True)
self.joints = self.get_skin_cluster_influence(skin_cluster)
self.vertices_info = self.get_skin_weights_for_mesh_name(
skin_cluster, mesh_name
)
def get_skin_cluster_influence(self, skin_cluster: MFnSkinCluster) -> List[str]:
"""
Gets a list of joint names that are influences to the skin cluster.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: The functionalities of a maya skin cluster object
@rtype: List[str]
@returns: The list if names of the joints that influence the skin cluster
"""
influences: List[str] = cmds.skinCluster(skin_cluster.name(), q=True, inf=True)
if influences and not isinstance(influences[0], str):
influences = [obj.name() for obj in influences]
return influences
def get_skin_weights_for_mesh_name(
self,
skin_cluster: MFnSkinCluster,
mesh_name: str,
) -> List[List[Union[int, float]]]:
"""
Gets the skin weights concerning the given mesh.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: The functionalities of a maya skin cluster object
@type mesh_name: str
@param mesh_name: The name of the mesh
@rtype: List[List[Union[int, float]]]
@returns: A list of list of weight indices and the weight values
"""
mesh = Maya.get_element(mesh_name)
components = MGlobal.getSelectionListByName(f"{mesh_name}.vtx[*]").getComponent(
0
)[1]
weights_data, chunk = skin_cluster.getWeights(mesh, components)
iterator = [
weights_data[i : i + chunk] for i in range(0, len(weights_data), chunk)
]
vertices_info = []
for weights in iterator:
vertex_weights: List[float] = []
vertices_info.append(vertex_weights)
for i, weight in enumerate(weights):
if weight:
vertex_weights.append(i)
vertex_weights.append(weight)
return vertices_info
def get_skin_weights_data(mesh_name: str) -> Tuple[MFnMesh, MFnSkinCluster]:
"""
Gets the maya objects that manipulate the mesh node and the skin cluster for a given mesh name.
@type mesh_name: str
@param mesh_name: The name of the mesh
@rtype: Tuple[MFnMesh, MFnSkinCluster]
@returns: The maya object that manipulate the mesh node and the skin cluster for a given mesh name.
"""
skin_cluster_name = mel.eval(f"findRelatedSkinCluster {mesh_name}")
if skin_cluster_name:
skin_cluster = MFnSkinCluster(Maya.get_element(skin_cluster_name))
mesh_node = MFnMesh(Maya.get_element(mesh_name))
return mesh_node, skin_cluster
raise DNAViewerError(f"Unable to find skin for given mesh: {mesh_name}")
def get_skin_weights_from_scene(mesh_name: str) -> MayaSkinWeights:
"""
Gets the instance of this class filled with data from the scene for a given mesh name.
@type mesh_name: str
@param mesh_name: The mesh name
@rtype: MayaSkinWeights
@returns: An instance of this class with the data from the scene
"""
_, skin_cluster = get_skin_weights_data(mesh_name)
return MayaSkinWeights(skin_cluster, mesh_name)
def get_file_joint_mappings(
skin_weights: MayaSkinWeights, skin_cluster: MFnSkinCluster
) -> List[int]:
"""
Returns a list of object indices representing the influences concerning the joint names specified in the skin weight model.
@type skin_weights: MayaSkinWeights
@param skin_weights: The instance of the model storing data about skin weights
@type skin_cluster: MFnSkinCluster
@param skin_cluster: An object for working with functions concerning a skin cluster in maya
@rtype: List[int]
@returns: a list of indices representing the influences concerning the given joints
"""
file_joint_mapping: List[int] = []
for joint_name in skin_weights.joints:
file_joint_mapping.append(
skin_cluster.indexForInfluenceObject(Maya.get_element(joint_name))
)
return file_joint_mapping
def set_skin_weights_to_scene(mesh_name: str, skin_weights: MayaSkinWeights) -> None:
"""
Sets the skin weights to the scene.
@type mesh_name: str
@param mesh_name: The mesh name
@type skin_weights: MayaSkinWeights
@param skin_weights: The object containing data that need to be set to the scene.
"""
mesh_node, skin_cluster = get_skin_weights_data(mesh_name)
file_joint_mapping = get_file_joint_mappings(skin_weights, skin_cluster)
import_skin_weights(skin_cluster, mesh_node, skin_weights, file_joint_mapping)
logging.info("Set skin weights ended.")
def import_skin_weights(
skin_cluster: MFnSkinCluster,
mesh_node: MFnMesh,
skin_weights: MayaSkinWeights,
file_joint_mapping: List[int],
) -> None:
"""
Imports the skin weights to the scene using the joint mapping and the data provided in the model containing the weights.
@type skin_cluster: MFnSkinCluster
@param skin_cluster: An object for working with functions concerning a skin cluster in maya
@type mesh_node: MFnMesh
@param mesh_node: An object for working with functions concerning meshes in maya
@type skin_weights: MayaSkinWeights
@param skin_weights: The instance of the model storing data about skin weights
@type file_joint_mapping: List[int]
@param file_joint_mapping: a list of indices representing the influences concerning joints
"""
temp_str = f"{skin_cluster.name()}.wl["
for vtx_id in range(cmds.polyEvaluate(mesh_node.name(), vertex=True)):
vtx_info = skin_weights.vertices_info[vtx_id]
vtx_str = f"{temp_str}{str(vtx_id)}].w["
cmds.setAttr(f"{vtx_str}0]", 0.0)
for i in range(0, len(vtx_info), 2):
cmds.setAttr(
f"{vtx_str}{str(file_joint_mapping[int(vtx_info[i])])}]",
vtx_info[i + 1],
)

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Union
from maya.api.OpenMaya import (
MDagPath,
MFnDagNode,
MFnTransform,
MGlobal,
MSpace,
MVector,
)
from ...common import DNAViewerError
class Maya:
"""A utility class used for interfacing with maya transforms"""
@staticmethod
def get_element(name: str) -> Union[MDagPath, MFnDagNode]:
"""gets the Union[MDagPath, MFnDagNode] object of the element with the given name
@type name: str
@param name: The name of the element to be retrieved
@rtype: Union[MDagPath, MFnDagNode]
@returns: A OpenMaya object representing the given element
"""
try:
sellist = MGlobal.getSelectionListByName(name)
except Exception as exception:
raise DNAViewerError(f"Element with name:{name} not found!") from exception
try:
return sellist.getDagPath(0)
except Exception:
return sellist.getDependNode(0)
@staticmethod
def get_transform(name: str) -> MFnTransform:
"""gets the transform of the element with the given name
@type element: str
@param element: The element name that we want the transform of
@rtype: MFnTransform
@returns: A MFnTransform object representing the given elements transform
"""
return MFnTransform(Maya.get_element(name))
@staticmethod
def get_translation(element: str, space: int = MSpace.kObject) -> MVector:
"""gets the translation of the element with the given name
@type element: str
@param element: The element name that we want the translation of
@type space: str
@param space: A string value representing the translation space (default is "world")
@rtype: MVector
@returns: A MVector object representing the given elements translation
"""
return MFnTransform(Maya.get_element(element)).translation(space)
@staticmethod
def set_translation(
element: str, translation: MVector, space: int = MSpace.kObject
) -> None:
"""sets the translation of the element with the given name
@type element: str
@param element: The element name that we want to set the translation of
@type translation: MVector
@param translation: The new translation value
@type space: str
@param space: A string value representing the translation space (default is "object")
"""
element_obj = Maya.get_transform(element)
element_obj.setTranslation(translation, space)

117
scripts/builder/mesh.py Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from typing import List
from ..builder.maya.mesh import MayaMesh
from ..dnalib.dnalib import DNA
from .config import Config
class Mesh:
"""
A builder class used for adding joints to the scene
Attributes
----------
@type dna: DNA
@param dna: The location of the DNA file
@type mesh_index: int
@param mesh_index: The mesh index we are working with
@type joint_ids: List[int]
@param joint_ids: The joint indices used for adding skin
@type joint_names: List[str]
@param joint_names: The joint names used for adding skin
@type config: Config
@param config: The build options that will be applied when creating the mesh
@type mesh: MayaMesh
@param mesh: The builder class object for creating the meshes
@type dna: DNA
@param dna: The DNA object that was loaded in
"""
def __init__(
self,
config: Config,
dna: DNA,
mesh_index: int,
) -> None:
self.mesh_index: int = mesh_index
self.joint_ids: List[int] = []
self.joint_names: List[str] = []
self.config = config
self.dna = dna
self.mesh = MayaMesh(
self.mesh_index,
self.dna,
blend_shape_group_prefix=self.config.blend_shape_group_prefix,
blend_shape_name_postfix=self.config.blend_shape_name_postfix,
skin_cluster_suffix=self.config.skin_cluster_suffix,
)
def build(self) -> None:
"""Starts the build process, creates the neutral mesh, then adds normals, blends shapes and skin if needed"""
self.create_neutral_mesh()
self.add_blend_shapes()
self.add_skin_cluster()
def create_neutral_mesh(self) -> None:
"""Creates the neutral mesh"""
self.mesh.create_neutral_mesh()
def add_blend_shapes(self) -> None:
"""Reads in the blend shapes, then adds them to the mesh if it is set in the build options"""
if self.config.add_blend_shapes:
logging.info("adding blend shapes...")
self.mesh.add_blend_shapes(
self.config.add_mesh_name_to_blend_shape_channel_name
)
def add_skin_cluster(self) -> None:
"""Adds skin cluster to the mesh if it is set in the build options"""
if self.config.add_skin_cluster and self.config.add_joints:
self.prepare_joints()
if self.joint_names:
self.mesh.add_skin_cluster(self.joint_names, self.joint_ids)
def prepare_joints(self) -> None:
"""
Gets the joint indices and names needed for the given mesh.
"""
self.prepare_joint_ids()
joints = self.dna.read_all_neutral_joints()
self.joint_names = []
for joint_id in self.joint_ids:
self.joint_names.append(joints[joint_id].name)
def prepare_joint_ids(self) -> None:
joints_temp: List[int] = []
joint_indices = self.dna.get_all_skin_weights_joint_indices_for_mesh(
self.mesh_index
)
self.joint_ids = []
if any(joint_indices):
for row in joint_indices:
for column in row:
joints_temp.append(column)
self.joint_ids = list(set(joints_temp))
self.joint_ids.sort()
else:
lod = self.dna.get_lowest_lod_containing_meshes([self.mesh_index])
if lod:
self.joint_ids = self.dna.get_joint_indices_for_lod(lod)

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_loader
from pathlib import Path
from types import ModuleType
from typing import Optional
from maya import cmds, mel
from maya.api.OpenMaya import MSpace, MVector
from ..builder.maya.util import Maya
from ..common import ANALOG_GUI_HOLDER, GUI_HOLDER, RIG_LOGIC_PREFIX, DNAViewerError
from ..dnalib.dnalib import DNA
from .builder import Builder
from .config import RigConfig
class RigBuilder(Builder):
"""
A builder class used for building meshes
"""
def __init__(self, dna: DNA, config: Optional[RigConfig] = None) -> None:
super().__init__(dna=dna, config=config)
self.config: Optional[RigConfig]
self.eye_l_pos: MVector
self.eye_r_pos: MVector
def _build(self) -> None:
if super()._build():
self.add_gui()
self.add_analog_gui()
self.add_rig_logic()
self.run_additional_assemble_script()
def run_additional_assemble_script(self) -> None:
"""
Runs an additional assemble script if specified in the character configuration.
"""
if self.config.aas_path:
logging.info("running additional assemble script...")
try:
module_name = Path(self.config.aas_path).stem
script = self.source_py_file(module_name, self.config.aas_path)
script_method = getattr(script, self.config.aas_method)
script_method(
self.config.get_top_level_group(),
self.config.get_rig_group(),
self.config.aas_parameter,
)
except Exception as e:
raise DNAViewerError(f"Can't run aas script. Reason: {e}") from e
def add_rig_logic(self) -> None:
"""
Creates and adds a rig logic node specified in the character configuration.
"""
if (
self.config.add_rig_logic
and self.config.add_joints
and self.config.add_skin_cluster
and self.config.add_blend_shapes
and self.config.aas_path
and self.config.analog_gui_path
and self.config.gui_path
):
logging.info("adding rig logic...")
try:
cmds.loadPlugin("embeddedRL4.mll")
self.config.rig_logic_name = f"{RIG_LOGIC_PREFIX}{self.dna.name}"
dna = self.dna.path.replace("\\", "/")
mel_command = self.config.rig_logic_command
mel_command += f' -n "{self.config.rig_logic_name}"'
mel_command += f' -dfp "{dna}"'
mel_command += f' -cn "{self.config.control_naming}"'
mel_command += f' -jn "{self.config.joint_naming}"'
mel_command += f' -bsn "{self.config.blend_shape_naming}"'
mel_command += f' -amn "{self.config.animated_map_naming}"; '
logging.info(f"mel command: {mel_command}")
mel.eval(mel_command)
except Exception as e:
logging.error(
"The procedure needed for assembling the rig logic was not found, the plugin needed for this might not be loaded."
)
raise DNAViewerError(
f"Something went wrong, skipping adding the rig logic... Reason: {e}"
) from e
def add_gui(self) -> None:
"""
Adds a gui according to the specified gui options. If none is specified no gui will be added.
"""
if self.config.gui_path:
logging.info("adding gui...")
self.import_gui(
gui_path=self.config.gui_path,
group_name=GUI_HOLDER,
)
self.position_gui(GUI_HOLDER)
self.add_ctrl_attributes()
self.add_animated_map_attributes()
def add_ctrl_attributes(self) -> None:
"""
Adds and sets the raw gui control attributes.
"""
gui_control_names = self.dna.get_raw_control_names()
for name in gui_control_names:
ctrl_and_attr_names = name.split(".")
self.add_attribute(
control_name=ctrl_and_attr_names[0],
long_name=ctrl_and_attr_names[1],
)
def add_animated_map_attributes(self) -> None:
"""
Adds and sets the animated map attributes.
"""
names = self.dna.get_animated_map_names()
for name in names:
long_name = name.replace(".", "_")
if self.config.gui_path:
self.add_attribute(
control_name=self.config.animated_map_attribute_multipliers_name,
long_name=long_name,
)
def position_gui(self, group_name: str) -> None:
"""Sets the gui position to align with the character eyes"""
if not cmds.objExists(self.config.eye_gui_name) or not cmds.objExists(
self.config.left_eye_joint_name
):
logging.warning(
"could not find joints needed for positioning the gui, leaving it at its default position..."
)
return
gui_y = (
Maya.get_transform(self.config.eye_gui_name).translation(MSpace.kObject).y
)
eyes_y = (
Maya.get_transform(self.config.left_eye_joint_name)
.translation(MSpace.kObject)
.y
)
delta_y = eyes_y - gui_y
if isinstance(self.config.gui_translate_x, str):
try:
logging.warning(
"gui_translate_x should be a float, trying to cast the value to float..."
)
self.config.gui_translate_x = float(self.config.gui_translate_x)
except ValueError:
logging.error("could not cast string value to float")
return
Maya.get_transform(group_name).translateBy(
MVector(self.config.gui_translate_x, delta_y, 0), MSpace.kObject
)
def add_analog_gui(self) -> None:
"""
Adds an analog gui according to the specified analog gui options. If none is specified no analog gui will be
added.
"""
if self.config.analog_gui_path and self.config.add_joints:
logging.info("adding analog gui...")
self.import_gui(
gui_path=self.config.analog_gui_path,
group_name=ANALOG_GUI_HOLDER,
)
if self.dna.joints.names:
self.add_eyes()
self.add_eye_locators()
def add_eyes(self) -> None:
"""Add eyes to the analog gui"""
self.eye_l_pos = Maya.get_translation(self.config.left_eye_joint_name)
self.eye_r_pos = Maya.get_translation(self.config.right_eye_joint_name)
Maya.set_translation(
self.config.central_driver_name,
Maya.get_translation(self.config.facial_root_joint_name),
)
delta_l = Maya.get_translation(
self.config.left_eye_aim_up_name
) - Maya.get_translation(self.config.left_eye_driver_name)
delta_r = Maya.get_translation(
self.config.right_eye_aim_up_name
) - Maya.get_translation(self.config.right_eye_driver_name)
Maya.set_translation(self.config.left_eye_driver_name, self.eye_l_pos)
Maya.set_translation(
self.config.right_eye_driver_name,
self.eye_r_pos,
)
Maya.set_translation(
self.config.left_eye_aim_up_name,
MVector(
self.eye_l_pos[0] + delta_l[0],
self.eye_l_pos[1] + delta_l[1],
self.eye_l_pos[2] + delta_l[2],
),
)
Maya.set_translation(
self.config.right_eye_aim_up_name,
MVector(
self.eye_r_pos[0] + delta_r[0],
self.eye_r_pos[1] + delta_r[1],
self.eye_r_pos[2] + delta_r[2],
),
)
def add_eye_locators(self) -> None:
"""Add eye locators to the analog gui"""
eye_l_locator_pos = Maya.get_translation(self.config.le_aim)
eye_r_locator_pos = Maya.get_translation(self.config.re_aim)
central_aim_pos = Maya.get_translation(self.config.central_aim)
eye_middle_delta = (self.eye_l_pos - self.eye_r_pos) / 2
eye_middle = self.eye_r_pos + eye_middle_delta
Maya.set_translation(
self.config.central_aim,
MVector(eye_middle[0], eye_middle[1], central_aim_pos[2]),
)
Maya.set_translation(
self.config.le_aim,
MVector(self.eye_l_pos[0], self.eye_l_pos[1], eye_l_locator_pos[2]),
)
Maya.set_translation(
self.config.re_aim,
MVector(self.eye_r_pos[0], self.eye_r_pos[1], eye_r_locator_pos[2]),
)
def source_py_file(self, name: str, path: str) -> Optional[ModuleType]:
"""
Used for loading a python file, used for additional assemble script.
@type name: str
@param name: The name of the module.
@type path: str
@param path: The path of the python file.
@rtype: Optional[ModuleType]
@returns: The loaded module.
"""
path_obj = Path(path.strip())
if (
path
and path_obj.exists()
and path_obj.is_file()
and path_obj.suffix == ".py"
):
spec = spec_from_loader(name, SourceFileLoader(name, path))
module = module_from_spec(spec)
spec.loader.exec_module(module)
return module
raise DNAViewerError(f"File {path} is not found!")
def import_gui(self, gui_path: str, group_name: str) -> None:
"""
Imports a gui using the provided parameters.
@type gui_path: str
@param gui_path: The path of the gui file that needs to be imported.
@type group_name: str
@param group_name: The name of the transform that holds the imported asset.
"""
cmds.file(gui_path, i=True, groupReference=True, groupName=group_name)