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
20 changes: 10 additions & 10 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ cube = next((n for n in scene.nodes if n.name == "Cube"), None)
| `geometry_transform` | `Transform` | Geometry transform | ✅ 🟡 |
| `node_to_world` | `Matrix` | Node to world matrix | ✅ 🔴 |
| `node_to_parent` | `Matrix` | Node to parent matrix | ✅ 🟡 |
| `attrib_type` | `ElementType` | Attribute type | ❌ 🟢 |
| `inherit_mode` | `InheritMode` | Transform inherit mode | ❌ 🟡 |
| `visible` | `bool` | Visibility flag | ❌ 🟡 |
| `euler_rotation` | `Vec3` | Euler angles | ❌ 🟡 |
| `attrib_type` | `ElementType` | Attribute type | |
| `inherit_mode` | `InheritMode` | Transform inherit mode | |
| `visible` | `bool` | Visibility flag | |
| `euler_rotation` | `Vec3` | Euler angles | |

---

Expand Down Expand Up @@ -297,12 +297,12 @@ for mesh in scene.meshes:
| `vertex_tangent` | `ndarray` | Tangent vectors (N, 3) | ✅ 🔴🔴 |
| `vertex_bitangent` | `ndarray` | Bitangent vectors (N, 3) | ✅ 🔴🔴 |
| `vertex_color` | `ndarray` | Vertex colors (N, 4) | ✅ 🔴 |
| `faces` | `list[Face]` | Face data | ❌ 🟡 |
| `face_material` | `list[int]` | Face material indices | ❌ 🟡 |
| `skin_deformers` | `list[SkinDeformer]` | Skin deformers | ❌ 🟡 |
| `blend_deformers` | `list[BlendDeformer]` | Blend deformers | ❌ 🟡 |
| `edge_crease` | `list[float]` | Edge sharpness | ❌ 🟢 |
| `vertex_crease` | `list[float]` | Vertex sharpness | ❌ 🟢 |
| `faces` | `list[tuple]` | Face data (index_begin, num_indices) | ✅ |
| `face_material` | `ndarray \| None` | Face material indices | |
| `skin_deformers` | `list[SkinDeformer]` | Skin deformers | |
| `blend_deformers` | `list[BlendDeformer]` | Blend deformers | |
| `edge_crease` | `ndarray \| None` | Edge sharpness | |
| `vertex_crease` | `ndarray \| None` | Vertex sharpness | |

> ⚠️ **Critical**: `vertex_tangent` and `vertex_bitangent` are required for normal mapping!

Expand Down
15 changes: 15 additions & 0 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,21 @@ def main():
print_node_hierarchy(root, max_depth=10)
print()

# Demonstrate find_node and find_material
print("🔍 Find by Name:")
# Try to find common node names
for name in ["Cube", "Camera", "Light", "Armature"]:
node = scene.find_node(name)
if node:
mesh_info = f" (mesh: {node.mesh.name})" if node.mesh else ""
print(f" Found node '{name}'{mesh_info}")
# Try to find materials
for material in scene.materials[:2]: # Check first 2 materials
found = scene.find_material(material.name)
if found:
print(f" Found material '{material.name}'")
print()

# All nodes details
if scene.nodes:
print(f"🎯 Node Details ({len(scene.nodes)} nodes):")
Expand Down
8 changes: 4 additions & 4 deletions tests/test_real_fbx.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def _test_load_fbx_impl():
with ufbx.load_file(fbx_file) as scene:
print("\n✓ Successfully loaded scene!")
print("\nScene Statistics:")
print(f" Nodes: {scene.node_count}")
print(f" Meshes: {scene.mesh_count}")
print(f" Nodes: {len(scene.nodes)}")
print(f" Meshes: {len(scene.meshes)}")
print(f" Materials: {scene.material_count}")
print(f" Textures: {scene.texture_count}")
print(f" Lights: {scene.light_count}")
Expand Down Expand Up @@ -59,7 +59,7 @@ def _test_load_fbx_impl():
print(f"{indent} Scale: ({t.scale.x:.2f}, {t.scale.y:.2f}, {t.scale.z:.2f})")

# Inspect meshes
if scene.mesh_count > 0:
if len(scene.meshes) > 0:
print(f"\n{'-' * 60}")
print("Mesh Details:")
print(f"{'-' * 60}")
Expand Down Expand Up @@ -113,7 +113,7 @@ def _test_load_fbx_impl():
print(f"Quat multiply: {q1} * {q2} = {q3}")

# Test transform to matrix conversion
if scene.node_count > 0:
if len(scene.nodes) > 0:
node = scene.nodes[0]
transform = node.local_transform
matrix = transform.to_matrix()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_type_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_node_properties() -> None:
"""Test node properties type hints"""
try:
scene = ufbx.load_file("test.fbx")
if scene.node_count > 0:
if len(scene.nodes) > 0:
scene.nodes[0]

# Test properties
Expand All @@ -81,7 +81,7 @@ def test_mesh_properties() -> None:
"""Test mesh properties type hints"""
try:
scene = ufbx.load_file("test.fbx")
if scene.mesh_count > 0:
if len(scene.meshes) > 0:
mesh: ufbx.Mesh = scene.meshes[0]

# Test integer properties
Expand Down
24 changes: 20 additions & 4 deletions ufbx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,6 @@ class Scene:
def axes(self) -> CoordinateAxes: ...
def find_node(self, name: str) -> Node | None: ...
def find_material(self, name: str) -> Material | None: ...
@property
def node_count(self) -> int: ...
@property
def mesh_count(self) -> int: ...

class Node(Element):
@property
Expand Down Expand Up @@ -553,6 +549,14 @@ class Node(Element):
def node_to_parent(self) -> np.ndarray[Any, Any]: ...
@property
def geometry_transform(self) -> Transform: ...
@property
def attrib_type(self) -> ElementType: ...
@property
def inherit_mode(self) -> InheritMode: ...
@property
def visible(self) -> bool: ...
@property
def euler_rotation(self) -> Vec3: ...

class Mesh(Element):
@property
Expand Down Expand Up @@ -581,6 +585,18 @@ class Mesh(Element):
def indices(self) -> np.ndarray[Any, Any] | None: ...
@property
def materials(self) -> list[Material]: ...
@property
def faces(self) -> list[tuple[int, int]]: ...
@property
def face_material(self) -> np.ndarray[Any, Any] | None: ...
@property
def skin_deformers(self) -> list[SkinDeformer]: ...
@property
def blend_deformers(self) -> list[BlendDeformer]: ...
@property
def edge_crease(self) -> np.ndarray[Any, Any] | None: ...
@property
def vertex_crease(self) -> np.ndarray[Any, Any] | None: ...
def triangulate_face(self, face_index: int) -> None: ...

class Material(Element):
Expand Down
156 changes: 155 additions & 1 deletion ufbx/_ufbx.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ cdef extern from "ufbx-c/ufbx.h":
ctypedef struct ufbx_constraint:
pass

# Find functions from ufbx
ufbx_node* ufbx_find_node(const ufbx_scene *scene, const char *name)
ufbx_material* ufbx_find_material(const ufbx_scene *scene, const char *name)

cdef extern from "ufbx_wrapper.h":

# Scene management
Expand Down Expand Up @@ -419,6 +423,13 @@ cdef extern from "ufbx_wrapper.h":
void ufbx_wrapper_node_get_node_to_parent(const ufbx_node *node, double *matrix16)
void ufbx_wrapper_node_get_geometry_transform(const ufbx_node *node, double *translation3, double *rotation4, double *scale3)

# Node additional properties
int ufbx_wrapper_node_get_attrib_type(const ufbx_node *node)
int ufbx_wrapper_node_get_inherit_mode(const ufbx_node *node)
bint ufbx_wrapper_node_get_visible(const ufbx_node *node)
void ufbx_wrapper_node_get_euler_rotation(const ufbx_node *node, double *xyz)
int ufbx_wrapper_node_get_rotation_order(const ufbx_node *node)

# Mesh access
ufbx_mesh* ufbx_wrapper_scene_get_mesh(const ufbx_scene *scene, size_t index)
const char* ufbx_wrapper_mesh_get_name(const ufbx_mesh *mesh)
Expand All @@ -436,6 +447,19 @@ cdef extern from "ufbx_wrapper.h":
const float* ufbx_wrapper_mesh_get_vertex_colors(const ufbx_mesh *mesh, size_t *out_count)
const uint32_t* ufbx_wrapper_mesh_get_indices(const ufbx_mesh *mesh, size_t *out_count)

# Mesh face data
size_t ufbx_wrapper_mesh_get_face_count(const ufbx_mesh *mesh)
void ufbx_wrapper_mesh_get_face(const ufbx_mesh *mesh, size_t index, uint32_t *index_begin, uint32_t *num_indices)
const uint32_t* ufbx_wrapper_mesh_get_face_material(const ufbx_mesh *mesh, size_t *out_count)
const double* ufbx_wrapper_mesh_get_edge_crease(const ufbx_mesh *mesh, size_t *out_count)
const float* ufbx_wrapper_mesh_get_vertex_crease(const ufbx_mesh *mesh, size_t *out_count)

# Mesh deformers
size_t ufbx_wrapper_mesh_get_num_skin_deformers(const ufbx_mesh *mesh)
ufbx_skin_deformer* ufbx_wrapper_mesh_get_skin_deformer(const ufbx_mesh *mesh, size_t index)
size_t ufbx_wrapper_mesh_get_num_blend_deformers(const ufbx_mesh *mesh)
ufbx_blend_deformer* ufbx_wrapper_mesh_get_blend_deformer(const ufbx_mesh *mesh, size_t index)

# Material access
ufbx_material* ufbx_wrapper_scene_get_material(const ufbx_scene *scene, size_t index)
size_t ufbx_wrapper_mesh_get_num_materials(const ufbx_mesh *mesh)
Expand Down Expand Up @@ -1898,6 +1922,26 @@ cdef class Scene:
cdef int f = ufbx_wrapper_scene_get_axes_front(self._scene)
return CoordinateAxes(r, u, f)

def find_node(self, name):
"""Find a node by name. Returns None if not found."""
if self._closed:
raise RuntimeError("Scene is closed")
cdef bytes name_bytes = name.encode('utf-8')
cdef ufbx_node* node = ufbx_find_node(self._scene, name_bytes)
if node != NULL:
return Node._create(self, node)
return None

def find_material(self, name):
"""Find a material by name. Returns None if not found."""
if self._closed:
raise RuntimeError("Scene is closed")
cdef bytes name_bytes = name.encode('utf-8')
cdef ufbx_material* material = ufbx_find_material(self._scene, name_bytes)
if material != NULL:
return Material._create(self, material)
return None

@property
def lights(self):
"""Get all lights in the scene"""
Expand Down Expand Up @@ -2237,9 +2281,39 @@ cdef class Node(Element):
transform.translation = Vec3(translation[0], translation[1], translation[2])
transform.rotation = Quat(rotation[0], rotation[1], rotation[2], rotation[3])
transform.scale = Vec3(scale[0], scale[1], scale[2])

return transform

@property
def attrib_type(self):
"""Attribute type (ElementType enum)"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
return ElementType(ufbx_wrapper_node_get_attrib_type(self._node))

@property
def inherit_mode(self):
"""Transform inherit mode (InheritMode enum)"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
return InheritMode(ufbx_wrapper_node_get_inherit_mode(self._node))

@property
def visible(self):
"""Visibility flag"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
return ufbx_wrapper_node_get_visible(self._node)

@property
def euler_rotation(self):
"""Euler rotation angles in degrees (Vec3)"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef double xyz[3]
ufbx_wrapper_node_get_euler_rotation(self._node, xyz)
return Vec3(xyz[0], xyz[1], xyz[2])


cdef class Mesh(Element):
"""Polygonal mesh geometry"""
Expand Down Expand Up @@ -2422,6 +2496,86 @@ cdef class Mesh(Element):
result.append(Material._create(self._scene, material))
return result

@property
def faces(self):
"""Face data as list of (index_begin, num_indices) tuples"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = ufbx_wrapper_mesh_get_face_count(self._mesh)
cdef list result = []
cdef uint32_t index_begin, num_indices
for i in range(count):
ufbx_wrapper_mesh_get_face(self._mesh, i, &index_begin, &num_indices)
result.append((index_begin, num_indices))
return result

@property
def face_material(self):
"""Face material indices as numpy array"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = 0
cdef const uint32_t* data = ufbx_wrapper_mesh_get_face_material(self._mesh, &count)
if data == NULL or count == 0:
return None
cdef np.npy_intp shape[1]
shape[0] = <np.npy_intp>count
return np.PyArray_SimpleNewFromData(1, shape, np.NPY_UINT32, <void*>data)

@property
def skin_deformers(self):
"""Skin deformers attached to this mesh"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = ufbx_wrapper_mesh_get_num_skin_deformers(self._mesh)
cdef list result = []
cdef ufbx_skin_deformer* deformer
for i in range(count):
deformer = ufbx_wrapper_mesh_get_skin_deformer(self._mesh, i)
if deformer != NULL:
result.append(SkinDeformer._create(self._scene, deformer))
return result

@property
def blend_deformers(self):
"""Blend deformers attached to this mesh"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = ufbx_wrapper_mesh_get_num_blend_deformers(self._mesh)
cdef list result = []
cdef ufbx_blend_deformer* deformer
for i in range(count):
deformer = ufbx_wrapper_mesh_get_blend_deformer(self._mesh, i)
if deformer != NULL:
result.append(BlendDeformer._create(self._scene, deformer))
return result

@property
def edge_crease(self):
"""Edge crease values as numpy array"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = 0
cdef const double* data = ufbx_wrapper_mesh_get_edge_crease(self._mesh, &count)
if data == NULL or count == 0:
return None
cdef np.npy_intp shape[1]
shape[0] = <np.npy_intp>count
return np.PyArray_SimpleNewFromData(1, shape, np.NPY_FLOAT64, <void*>data)

@property
def vertex_crease(self):
"""Vertex crease values as numpy array"""
if self._scene._closed:
raise RuntimeError("Scene is closed")
cdef size_t count = 0
cdef const float* data = ufbx_wrapper_mesh_get_vertex_crease(self._mesh, &count)
if data == NULL or count == 0:
return None
cdef np.npy_intp shape[1]
shape[0] = <np.npy_intp>count
return np.PyArray_SimpleNewFromData(1, shape, np.NPY_FLOAT32, <void*>data)


cdef class MaterialMap:
"""Material map (direct access to ufbx_material_map fields)"""
Expand Down
Loading