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
[docs]@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)
[docs] 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
[docs]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
[docs] 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)
[docs] def new_scene(self) -> None:
cmds.file(new=True, force=True)
[docs] 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
[docs] 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())
[docs] 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)
[docs] 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
)
[docs] 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())
[docs] 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],
)
[docs] 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
)
[docs] 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,
)
[docs] 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")
[docs] def set_filtered_meshes(self) -> None:
self.all_loaded_meshes = self.get_filtered_meshes()
[docs] 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
[docs] 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
[docs] 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
)
[docs] 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
[docs] 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
[docs] 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)
[docs] def get_linear_unit(self) -> LinearUnit:
return self.get_linear_unit_from_int(self.dna.get_translation_unit())
[docs] def get_angle_unit(self) -> AngleUnit:
return self.get_angle_unit_from_int(self.dna.get_rotation_unit())
[docs] 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}")
[docs] 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}")
[docs] 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))