Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 191 additions & 10 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ def print_node_hierarchy(node, depth=0, max_depth=3):

indent = " " * depth
mesh_info = " [mesh]" if node.mesh else ""
print(f"{indent}- {node.name}{mesh_info}")
structure_note = ""
if node.mesh and node.children:
structure_note = " (note: mesh node has children)"

print(f"{indent}- {node.name}{mesh_info}{structure_note}")

# Show node's parent
if depth == 0 and node.parent:
Expand All @@ -154,10 +158,10 @@ def extract_euler_angles(matrix, scale_x, scale_y, scale_z):
r00 = matrix[0, 0] / scale_x
r10 = matrix[1, 0] / scale_x
r20 = matrix[2, 0] / scale_x
r01 = matrix[0, 1] / scale_y
matrix[0, 1] / scale_y
r11 = matrix[1, 1] / scale_y
r21 = matrix[2, 1] / scale_y
r02 = matrix[0, 2] / scale_z
matrix[0, 2] / scale_z
r12 = matrix[1, 2] / scale_z
r22 = matrix[2, 2] / scale_z

Expand Down Expand Up @@ -197,11 +201,14 @@ def print_transform_info(node):
scale_y = math.sqrt(local_matrix[0, 1] ** 2 + local_matrix[1, 1] ** 2 + local_matrix[2, 1] ** 2)
# Column 2: Z-axis basis vector
scale_z = math.sqrt(local_matrix[0, 2] ** 2 + local_matrix[1, 2] ** 2 + local_matrix[2, 2] ** 2)
print(f" Scale: ({scale_x:8.3f}, {scale_y:8.3f}, {scale_z:8.3f})")

scale_note = "uniform" if abs(scale_x - scale_y) < 0.001 and abs(scale_y - scale_z) < 0.001 else "non-uniform"

print(f" Scale: ({scale_x:8.3f}, {scale_y:8.3f}, {scale_z:8.3f}) [{scale_note}]")

# Extract rotation (Euler angles)
rot_x, rot_y, rot_z = extract_euler_angles(local_matrix, scale_x, scale_y, scale_z)
print(f" Rotation: ({rot_x:8.3f}°, {rot_y:8.3f}°, {rot_z:8.3f}°) [XYZ Euler]")
print(f" Rotation: ({rot_x:8.3f}°, {rot_y:8.3f}°, {rot_z:8.3f}°) [derived Euler, from baked matrix, XYZ]")

# Check if there are significant transformations
has_translation = abs(pos_x) > 0.001 or abs(pos_y) > 0.001 or abs(pos_z) > 0.001
Expand All @@ -223,7 +230,7 @@ def print_transform_info(node):

# World transform
world_matrix = node.world_transform
print(" - Transform (World):")
print(" - Transform (World, inherited through hierarchy):")
world_pos_x = world_matrix[0, 3]
world_pos_y = world_matrix[1, 3]
world_pos_z = world_matrix[2, 3]
Expand All @@ -237,6 +244,25 @@ def print_transform_info(node):
print(f" Rotation: ({world_rot_x:8.3f}°, {world_rot_y:8.3f}°, {world_rot_z:8.3f}°) [XYZ Euler]")


def format_texture_info(texture, indent=" "):
"""Format detailed texture information"""
if not texture:
return None

info = []
if texture.name:
info.append(f"{indent}Name: {texture.name}")
if texture.filename:
info.append(f"{indent}File: {texture.filename}")
if texture.relative_filename:
info.append(f"{indent}Relative: {texture.relative_filename}")
if texture.absolute_filename:
info.append(f"{indent}Absolute: {texture.absolute_filename}")
info.append(f"{indent}Type: {texture.type}")

return "\n".join(info) if info else None


def main():
if len(sys.argv) < 2:
print("Usage: python3 basic_usage.py <fbx_file_path>")
Expand All @@ -262,7 +288,10 @@ def main():
print(f" Nodes: {stats['nodes']}")
print(f" - With mesh: {stats['nodes_with_mesh']}")
print(f" - Leaf nodes: {stats['leaf_nodes']}")
print(f" Meshes: {stats['meshes']}")
mesh_note = ""
if stats["meshes"] == 1 and stats["nodes"] > 1:
mesh_note = " (note: many nodes reference a single mesh)"
print(f" Meshes: {stats['meshes']}{mesh_note}")
if stats["meshes"] > 0:
print(f" - Total vertices: {stats['total_vertices']:,}")
print(f" - Total faces: {stats['total_faces']:,}")
Expand All @@ -281,7 +310,7 @@ def main():
print(" System type:")
print(f" - Handedness: {coord_info['handedness']}")
if coord_info["description"]:
print(f" - Description: {coord_info['description']}")
print(f" - Description: {coord_info['description']} (axis conversion likely applied)")
print(" Basis matrix (4x4, column-major):")
matrix = coord_info["matrix"]
for row in range(4):
Expand Down Expand Up @@ -337,7 +366,11 @@ def main():
)

if normals is not None:
print(f" ✓ Normals: shape={normals.shape}")
note = ""
if positions is not None and len(normals) != len(positions):
note = " (note: normal count != vertex count)"

print(f" ✓ Normals: shape={normals.shape}{note}")
if len(normals) > 0:
print(f" First normal: ({normals[0][0]:.3f}, {normals[0][1]:.3f}, {normals[0][2]:.3f})")

Expand All @@ -357,10 +390,158 @@ def main():
# Material information
if scene.materials:
print(f"🎨 Material Details ({len(scene.materials)} materials):")

# Collect statistics
shader_types = {}
shading_models = {}
materials_with_textures = 0

for material in scene.materials:
# Count shader types
shader_type = material.shader_type
shader_types[shader_type] = shader_types.get(shader_type, 0) + 1

# Count shading models
shading_model = material.shading_model_name
if shading_model:
shading_models[shading_model] = shading_models.get(shading_model, 0) + 1

# Count materials with textures (check common maps)
has_texture = any(
[
material.pbr_base_color.texture_enabled and material.pbr_base_color.texture,
material.pbr_normal_map.texture_enabled and material.pbr_normal_map.texture,
material.fbx_diffuse_color.texture_enabled and material.fbx_diffuse_color.texture,
]
)
if has_texture:
materials_with_textures += 1

# Display statistics
print(" Overview:")
print(f" - Materials with textures: {materials_with_textures}")
if shading_models:
print(f" - Shading models: {', '.join(f'{k} ({v})' for k, v in sorted(shading_models.items()))}")
print()

# Display individual materials
print(" Materials:")
for i, material in enumerate(scene.materials):
print(f" [{i}] Material: '{material.name}'")
print(f"\n [{i}] '{material.name}'")

# Shading information
shading_info = []
if material.shading_model_name:
shading_info.append(f"model={material.shading_model_name}")
shading_info.append(f"shader_type={material.shader_type}")
print(f" - Shading: {', '.join(shading_info)}")

# PBR properties
print(" - PBR properties:")

# Base properties
base_color = material.pbr_base_color
if base_color.has_value:
r, g, b, a = base_color.value_vec4
print(f" Base color: RGB({r:.3f}, {g:.3f}, {b:.3f}), A={a:.3f}")
if base_color.texture_enabled and base_color.texture:
print(" Base color texture:")
tex_info = format_texture_info(base_color.texture)
if tex_info:
print(tex_info)

roughness = material.pbr_roughness
if roughness.has_value:
print(f" Roughness: {roughness.value_vec4[0]:.3f}")
if roughness.texture_enabled and roughness.texture:
print(" Roughness texture:")
tex_info = format_texture_info(roughness.texture)
if tex_info:
print(tex_info)

metalness = material.pbr_metalness
if metalness.has_value:
print(f" Metalness: {metalness.value_vec4[0]:.3f}")
if metalness.texture_enabled and metalness.texture:
print(" Metalness texture:")
tex_info = format_texture_info(metalness.texture)
if tex_info:
print(tex_info)

# Extended properties
emission = material.pbr_emission_color
if emission.has_value:
r, g, b, a = emission.value_vec4
print(f" Emission: RGB({r:.3f}, {g:.3f}, {b:.3f})")
if emission.texture_enabled and emission.texture:
print(" Emission texture:")
tex_info = format_texture_info(emission.texture)
if tex_info:
print(tex_info)

opacity = material.pbr_opacity
if opacity.has_value:
alpha = opacity.value_vec4[0]
print(f" Opacity: {alpha:.3f}")
if opacity.texture_enabled and opacity.texture:
print(" Opacity texture:")
tex_info = format_texture_info(opacity.texture)
if tex_info:
print(tex_info)

# Maps
normal_map = material.pbr_normal_map
if normal_map.texture_enabled and normal_map.texture:
print(" Normal map:")
tex_info = format_texture_info(normal_map.texture)
if tex_info:
print(tex_info)

ao_map = material.pbr_ambient_occlusion
if ao_map.texture_enabled and ao_map.texture:
print(" AO map:")
tex_info = format_texture_info(ao_map.texture)
if tex_info:
print(tex_info)

# FBX properties (for compatibility)
fbx_diffuse = material.fbx_diffuse_color
if fbx_diffuse.texture_enabled or (
fbx_diffuse.has_value and any(v != 0 for v in fbx_diffuse.value_vec4[:3])
):
print(" - FBX properties:")
if fbx_diffuse.has_value:
r, g, b, a = fbx_diffuse.value_vec4
print(f" Diffuse color: RGB({r:.3f}, {g:.3f}, {b:.3f})")
if fbx_diffuse.texture_enabled and fbx_diffuse.texture:
print(" Diffuse texture:")
tex_info = format_texture_info(fbx_diffuse.texture)
if tex_info:
print(tex_info)

print()

# Texture information
if scene.textures:
print(f"🖼️ Texture Details ({len(scene.textures)} textures):")
print()

for i, texture in enumerate(scene.textures):
print(f" [{i}] Texture: '{texture.name}'")

# File information
if texture.filename:
print(f" - Filename: {texture.filename}")
if texture.relative_filename:
print(f" - Relative: {texture.relative_filename}")
if texture.absolute_filename:
print(f" - Absolute: {texture.absolute_filename}")

# Texture type
print(f" - Type: {texture.type}")

print()

except FileNotFoundError:
print(f"❌ Error: File not found - {filename}")
sys.exit(1)
Expand Down
8 changes: 1 addition & 7 deletions sfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,13 +459,7 @@ def do_update(argv, config: Config):
old_dir = os.path.join(tmp_dir, "old")
new_dir = os.path.join(tmp_dir, "new")

try:
os.mkdir(tmp_dir)
except FileExistsError:
raise TempExistsError(
f"Temporary directory {tmp_dir} exists, delete it or use '--remove-temp' to automatically remove it"
)

os.mkdir(tmp_dir)
os.mkdir(old_dir)
os.mkdir(new_dir)

Expand Down
36 changes: 15 additions & 21 deletions tests/test_animation_deformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,83 +7,77 @@

def test_scene_has_animation_properties():
"""Test that Scene has animation-related properties"""
assert hasattr(ufbx.Scene, 'anim_stacks')
assert hasattr(ufbx.Scene, 'anim_curves')
assert hasattr(ufbx.Scene, "anim_stacks")
assert hasattr(ufbx.Scene, "anim_curves")


def test_scene_has_deformer_properties():
"""Test that Scene has deformer-related properties"""
assert hasattr(ufbx.Scene, 'skin_deformers')
assert hasattr(ufbx.Scene, 'blend_deformers')
assert hasattr(ufbx.Scene, 'blend_shapes')
assert hasattr(ufbx.Scene, 'constraints')
assert hasattr(ufbx.Scene, "skin_deformers")
assert hasattr(ufbx.Scene, "blend_deformers")
assert hasattr(ufbx.Scene, "blend_shapes")
assert hasattr(ufbx.Scene, "constraints")


def test_anim_stack_class_has_properties():
"""Test that AnimStack class has expected properties"""
expected_properties = ['name', 'time_begin', 'time_end', 'layers']
expected_properties = ["name", "time_begin", "time_end", "layers"]
for prop in expected_properties:
assert hasattr(ufbx.AnimStack, prop), f"AnimStack missing property: {prop}"


def test_anim_layer_class_has_properties():
"""Test that AnimLayer class has expected properties"""
expected_properties = [
'name', 'weight', 'weight_is_animated', 'blended',
'additive', 'compose_rotation', 'compose_scale'
]
expected_properties = ["name", "weight", "weight_is_animated", "blended", "additive", "compose_rotation", "compose_scale"]
for prop in expected_properties:
assert hasattr(ufbx.AnimLayer, prop), f"AnimLayer missing property: {prop}"


def test_anim_curve_class_has_properties():
"""Test that AnimCurve class has expected properties"""
expected_properties = [
'name', 'num_keyframes', 'min_value', 'max_value',
'min_time', 'max_time'
]
expected_properties = ["name", "num_keyframes", "min_value", "max_value", "min_time", "max_time"]
for prop in expected_properties:
assert hasattr(ufbx.AnimCurve, prop), f"AnimCurve missing property: {prop}"


def test_skin_deformer_class_has_properties():
"""Test that SkinDeformer class has expected properties"""
expected_properties = ['name', 'clusters']
expected_properties = ["name", "clusters"]
for prop in expected_properties:
assert hasattr(ufbx.SkinDeformer, prop), f"SkinDeformer missing property: {prop}"


def test_skin_cluster_class_has_properties():
"""Test that SkinCluster class has expected properties"""
expected_properties = ['name', 'num_weights']
expected_properties = ["name", "num_weights"]
for prop in expected_properties:
assert hasattr(ufbx.SkinCluster, prop), f"SkinCluster missing property: {prop}"


def test_blend_deformer_class_has_properties():
"""Test that BlendDeformer class has expected properties"""
expected_properties = ['name', 'channels']
expected_properties = ["name", "channels"]
for prop in expected_properties:
assert hasattr(ufbx.BlendDeformer, prop), f"BlendDeformer missing property: {prop}"


def test_blend_channel_class_has_properties():
"""Test that BlendChannel class has expected properties"""
expected_properties = ['name', 'weight']
expected_properties = ["name", "weight"]
for prop in expected_properties:
assert hasattr(ufbx.BlendChannel, prop), f"BlendChannel missing property: {prop}"


def test_blend_shape_class_has_properties():
"""Test that BlendShape class has expected properties"""
expected_properties = ['name', 'num_offsets']
expected_properties = ["name", "num_offsets"]
for prop in expected_properties:
assert hasattr(ufbx.BlendShape, prop), f"BlendShape missing property: {prop}"


def test_constraint_class_has_properties():
"""Test that Constraint class has expected properties"""
expected_properties = ['name', 'type', 'weight', 'active']
expected_properties = ["name", "type", "weight", "active"]
for prop in expected_properties:
assert hasattr(ufbx.Constraint, prop), f"Constraint missing property: {prop}"

Expand Down
Loading