Source code for scfile.structures.models.transforms
"""
Scene transformation functions.
"""
from dataclasses import replace
from typing import Callable, TypeAlias
import numpy as np
from scfile.consts import ModelDefaults
from scfile.structures.models.animation import AnimationClip
from .enums import AnimationTranslation, SkeletonHierarchy, SkeletonSpace, UVOrigin, UVSign
from .mesh import ModelMesh
from .scene import ModelScene
from .skeleton import SkeletonBone
SceneTransform: TypeAlias = Callable[[ModelScene], ModelScene]
[docs]
def unique_names(scene: ModelScene) -> ModelScene:
"""Ensure all meshes have unique names."""
seen_names: set[str] = set()
meshes: list[ModelMesh] = []
for mesh in scene.meshes:
name = mesh.name or "noname"
base_name, count = name, 2
unique_name = f"{base_name}"
while unique_name in seen_names:
unique_name = f"{base_name}_{count}"
count += 1
seen_names.add(unique_name)
meshes.append(replace(mesh, name=unique_name))
return replace(scene, meshes=meshes)
[docs]
def flip_uv(scene: ModelScene) -> ModelScene:
"""Flip V axis (TOP_LEFT → BOTTOM_LEFT)."""
meshes: list[ModelMesh] = []
for mesh in scene.meshes:
if mesh.uv_origin == UVOrigin.BOTTOM_LEFT and mesh.uv_sign == UVSign.POSITIVE:
meshes.append(mesh)
continue
new_mesh = replace(mesh)
new_mesh.uv1 = mesh.uv1.copy()
new_mesh.uv2 = mesh.uv2.copy()
new_mesh.uv1[:, 1] = 1.0 - new_mesh.uv1[:, 1]
new_mesh.uv2[:, 1] = 1.0 - new_mesh.uv2[:, 1]
new_mesh.uv_origin = UVOrigin.BOTTOM_LEFT
new_mesh.uv_sign = UVSign.POSITIVE
meshes.append(new_mesh)
return replace(scene, meshes=meshes)
[docs]
def invert_uv(scene: ModelScene) -> ModelScene:
"""Invert V axis sign (POSITIVE → NEGATIVE)."""
meshes: list[ModelMesh] = []
for mesh in scene.meshes:
if mesh.uv_sign == UVSign.NEGATIVE:
meshes.append(mesh)
continue
new_mesh = replace(mesh)
new_mesh.uv1 = mesh.uv1.copy()
new_mesh.uv2 = mesh.uv2.copy()
new_mesh.uv1[:, 1] *= -1.0
new_mesh.uv2[:, 1] *= -1.0
new_mesh.uv_sign = UVSign.NEGATIVE
meshes.append(new_mesh)
return replace(scene, meshes=meshes)
[docs]
def skeleton_to_local(scene: ModelScene) -> ModelScene:
"""Convert bone positions (GLOBAL → LOCAL)."""
if scene.skeleton.space == SkeletonSpace.LOCAL:
return scene
new_bones: list[SkeletonBone] = []
for bone in scene.skeleton.bones:
new_bone = replace(bone)
new_bone.position = bone.position.copy()
parent_id = bone.parent_id
while parent_id > ModelDefaults.ROOT_BONE_ID:
parent = new_bones[parent_id]
new_bone.position -= parent.position
parent_id = parent.parent_id
new_bones.append(new_bone)
new_skeleton = replace(scene.skeleton, bones=new_bones, space=SkeletonSpace.LOCAL)
return replace(scene, skeleton=new_skeleton)
[docs]
def build_hierarchy(scene: ModelScene) -> ModelScene:
"""Build bone children tree (FLAT → BUILT)."""
if scene.skeleton.hierarchy == SkeletonHierarchy.BUILT:
return scene
new_bones: list[SkeletonBone] = [replace(bone) for bone in scene.skeleton.bones]
for bone in new_bones:
if not bone.is_root:
parent = new_bones[bone.parent_id]
parent.children.append(bone)
new_skeleton = replace(scene.skeleton, bones=new_bones, hierarchy=SkeletonHierarchy.BUILT)
return replace(scene, skeleton=new_skeleton)
[docs]
def animation_to_absolute(scene: ModelScene) -> ModelScene:
"""Add rest pose positions to animation deltas (DELTA → ABSOLUTE)."""
if scene.animation.translation == AnimationTranslation.ABSOLUTE:
return scene
skeleton = scene.skeleton
positions = np.array([bone.position for bone in skeleton.bones], dtype=np.float32)
new_clips: list[AnimationClip] = []
for clip in scene.animation.clips:
new_translations = clip.translations.copy()
new_translations += positions[np.newaxis, :, :]
new_clips.append(replace(clip, translations=new_translations))
new_animation = replace(
scene.animation,
clips=new_clips,
translation=AnimationTranslation.ABSOLUTE,
)
return replace(scene, animation=new_animation)