From 64189e8c48e65ae6fa886b79afedd4fb38d24079 Mon Sep 17 00:00:00 2001 From: popomore Date: Sat, 24 Jan 2026 18:40:23 +0800 Subject: [PATCH] docs: API --- docs/API.md | 1561 +++++++++++++++++++++++++++++++++++++++ examples/basic_usage.py | 73 +- ufbx/__init__.py | 8 + ufbx/__init__.pyi | 301 +++++++- ufbx/_ufbx.pyx | 423 ++++++++++- 5 files changed, 2348 insertions(+), 18 deletions(-) create mode 100644 docs/API.md diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8b6b9a0 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,1561 @@ +# ufbx-python API Documentation + +--- + +## Table of Contents + +- [Scene.anim](#sceneanim) ❌ +- [Scene.anim_curves](#sceneanim_curves) ✅ +- [Scene.anim_layers](#sceneanim_layers) ❌ +- [Scene.anim_stacks](#sceneanim_stacks) ✅ +- [Scene.anim_values](#sceneanim_values) ❌ +- [Scene.audio_clips](#sceneaudio_clips) ❌ +- [Scene.audio_layers](#sceneaudio_layers) ❌ +- [Scene.axes](#sceneaxes) ✅ +- [Scene.blend_channels](#sceneblend_channels) ❌ +- [Scene.blend_deformers](#sceneblend_deformers) ✅ +- [Scene.blend_shapes](#sceneblend_shapes) ✅ +- [Scene.bones](#scenebones) ✅ +- [Scene.cache_deformers](#scenecache_deformers) ❌ +- [Scene.cache_files](#scenecache_files) ❌ +- [Scene.camera_switchers](#scenecamera_switchers) ❌ +- [Scene.cameras](#scenecameras) ✅ +- [Scene.characters](#scenecharacters) ❌ +- [Scene.connections_dst](#sceneconnections_dst) ❌ +- [Scene.connections_src](#sceneconnections_src) ❌ +- [Scene.constraints](#sceneconstraints) ✅ +- [Scene.display_layers](#scenedisplay_layers) ❌ +- [Scene.dom_root](#scenedom_root) ❌ +- [Scene.elements](#sceneelements) ❌ +- [Scene.elements_by_name](#sceneelements_by_name) ❌ +- [Scene.empties](#sceneempties) ✅ +- [Scene.find_material()](#scenefind_material) ✅ +- [Scene.find_node()](#scenefind_node) ✅ +- [Scene.lights](#scenelights) ✅ +- [Scene.line_curves](#sceneline_curves) ❌ +- [Scene.lod_groups](#scenelod_groups) ❌ +- [Scene.markers](#scenemarkers) ❌ +- [Scene.materials](#scenematerials) ✅ +- [Scene.meshes](#scenemeshes) ✅ +- [Scene.metadata](#scenemetadata) ✅ +- [Scene.metadata_objects](#scenemetadata_objects) ❌ +- [Scene.nodes](#scenenodes) ✅ +- [Scene.nurbs_curves](#scenenurbs_curves) ❌ +- [Scene.nurbs_surfaces](#scenenurbs_surfaces) ❌ +- [Scene.nurbs_trim_boundaries](#scenenurbs_trim_boundaries) ❌ +- [Scene.nurbs_trim_surfaces](#scenenurbs_trim_surfaces) ❌ +- [Scene.poses](#sceneposes) ❌ +- [Scene.procedural_geometries](#sceneprocedural_geometries) ❌ +- [Scene.root_node](#sceneroot_node) ✅ +- [Scene.selection_nodes](#sceneselection_nodes) ❌ +- [Scene.selection_sets](#sceneselection_sets) ❌ +- [Scene.settings](#scenesettings) ✅ +- [Scene.shader_bindings](#sceneshader_bindings) ❌ +- [Scene.shaders](#sceneshaders) ❌ +- [Scene.skin_clusters](#sceneskin_clusters) ❌ +- [Scene.skin_deformers](#sceneskin_deformers) ✅ +- [Scene.stereo_cameras](#scenestereo_cameras) ❌ +- [Scene.texture_files](#scenetexture_files) ❌ +- [Scene.textures](#scenetextures) ✅ +- [Scene.unknowns](#sceneunknowns) ✅ +- [Scene.videos](#scenevideos) ❌ + +--- + +## Scene Loading + +```python +import ufbx + +# Load from file +scene = ufbx.load_file("model.fbx") + +# Load from memory +with open("model.fbx", "rb") as f: + data = f.read() +scene = ufbx.load_memory(data) + +# Context manager (auto cleanup) +with ufbx.load_file("model.fbx") as scene: + print(f"Loaded {len(scene.nodes)} nodes") +``` + +--- + +## Scene.metadata + +**Type**: `Metadata` +**Status**: ✅ Complete + +Scene metadata information including version, creator, format, etc. + +```python +meta = scene.metadata +print(f"FBX version: {meta.version / 1000:.1f}") # 7.4 +print(f"Creator: {meta.creator}") # Blender 3.6.0 +print(f"Format: {'ASCII' if meta.ascii else 'Binary'}") +print(f"Filename: {meta.filename}") +``` + +### Metadata Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `ascii` | `bool` | Is ASCII format | ✅ | +| `version` | `int` | FBX version (e.g., 7400 = v7.4) | ✅ | +| `file_format` | `int` | File format version | ✅ | +| `creator` | `str` | Creator application | ✅ | +| `big_endian` | `bool` | Byte order | ✅ | +| `filename` | `str` | Original filename | ✅ | +| `relative_root` | `str` | Relative root path | ✅ | + +--- + +## Scene.settings + +**Type**: `SceneSettings` +**Status**: ✅ Complete + +Scene settings and configuration including units, FPS, coordinate axes. + +```python +settings = scene.settings +fps = settings.frames_per_second # 24.0 +units = settings.unit_meters # 0.01 (centimeters) +axes = settings.axes # Coordinate axes +ambient = settings.ambient_color # (r, g, b) +``` + +### SceneSettings Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `axes` | `CoordinateAxes` | Coordinate system axes | ✅ | +| `unit_meters` | `float` | Units in meters (0.01 = cm) | ✅ | +| `frames_per_second` | `float` | Frame rate | ✅ | +| `ambient_color` | `tuple[float, float, float]` | Ambient light color RGB | ✅ | +| `default_camera` | `str` | Default camera name | ✅ | +| `time_mode` | `int` | Time mode | ✅ | +| `time_protocol` | `int` | Time protocol | ✅ | +| `snap_mode` | `int` | Snap mode | ✅ | +| `original_axis_up` | `int` | Original up axis | ✅ | +| `original_unit_meters` | `float` | Original unit scale | ✅ | + +--- + +## Scene.root_node + +**Type**: `Node | None` +**Status**: ✅ Complete + +Root node of scene hierarchy. All other nodes are children or descendants. + +```python +root = scene.root_node +if root: + print(f"Root node: {root.name}") + print(f"Direct children: {len(root.children)}") + + # Traverse hierarchy + def print_hierarchy(node, indent=0): + print(" " * indent + node.name) + for child in node.children: + print_hierarchy(child, indent + 1) + + print_hierarchy(root) +``` + +**See**: [Node Properties](#node-properties) + +--- + +## Scene.anim + +**Type**: `Anim | None` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +Default animation descriptor for the scene. + +```python +# Not yet available +# if scene.anim: +# print(f"Default animation: {scene.anim.name}") +``` + +### Anim Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Animation name | ❌ | +| `layers` | `list[AnimLayer]` | Animation layers | ❌ | +| `time_begin` | `float` | Start time | ❌ | +| `time_end` | `float` | End time | ❌ | + +--- + +## Scene.unknowns + +**Type**: `list[Unknown]` +**Status**: ✅ Complete + +List of elements that ufbx parsed but doesn't have specific handlers for. + +```python +if scene.unknowns: + print(f"Unknown elements: {len(scene.unknowns)}") + for unknown in scene.unknowns: + print(f" {unknown.name}") +``` + +### Unknown Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Unknown element name | ✅ | + +--- + +## Scene.nodes + +**Type**: `list[Node]` +**Status**: ✅ Complete + +Flat list of all nodes in the scene, regardless of hierarchy. + +```python +print(f"Total nodes: {len(scene.nodes)}") + +# Find nodes with meshes +mesh_nodes = [n for n in scene.nodes if n.mesh] +print(f"Mesh nodes: {len(mesh_nodes)}") + +# Find by name +cube = next((n for n in scene.nodes if n.name == "Cube"), None) +``` + +### Node Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Node name | ✅ | +| `parent` | `Node \| None` | Parent node | ✅ | +| `children` | `list[Node]` | Child nodes | ✅ | +| `mesh` | `Mesh \| None` | Associated mesh | ✅ | +| `light` | `Light \| None` | Associated light | ✅ | +| `camera` | `Camera \| None` | Associated camera | ✅ | +| `bone` | `Bone \| None` | Associated bone | ✅ | +| `is_root` | `bool` | Is root node | ✅ | +| `local_transform` | `ndarray` | Local transform matrix (4x4) | ✅ | +| `world_transform` | `ndarray` | World transform matrix (4x4) | ✅ | +| `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 | ❌ 🟡 | + +--- + +## Scene.meshes + +**Type**: `list[Mesh]` +**Status**: ✅ Complete + +List of all mesh geometry objects. + +```python +print(f"Total meshes: {len(scene.meshes)}") + +for mesh in scene.meshes: + print(f"Mesh: {mesh.name}") + print(f" Vertices: {mesh.num_vertices}") + print(f" Triangles: {mesh.num_triangles}") + print(f" Materials: {len(mesh.materials)}") + + # Access vertex data + positions = mesh.vertex_positions # (N, 3) numpy array + normals = mesh.vertex_normals # (N, 3) numpy array + uvs = mesh.vertex_uvs # (N, 2) numpy array +``` + +### Mesh Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Mesh name | ✅ | +| `num_vertices` | `int` | Vertex count | ✅ | +| `num_indices` | `int` | Index count | ✅ | +| `num_faces` | `int` | Face count | ✅ | +| `num_triangles` | `int` | Triangle count | ✅ | +| `vertex_positions` | `ndarray \| None` | Vertex positions (N, 3) | ✅ | +| `vertex_normals` | `ndarray \| None` | Vertex normals (N, 3) | ✅ | +| `vertex_uvs` | `ndarray \| None` | UV coordinates (N, 2) | ✅ | +| `indices` | `ndarray \| None` | Vertex indices | ✅ | +| `materials` | `list[Material]` | Material list | ✅ | +| `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 | ❌ 🟢 | + +> ⚠️ **Critical**: `vertex_tangent` and `vertex_bitangent` are required for normal mapping! + +--- + +## Scene.materials + +**Type**: `list[Material]` +**Status**: ✅ Complete + +List of all material definitions (100% complete PBR and FBX support). + +```python +print(f"Total materials: {len(scene.materials)}") + +for material in scene.materials: + print(f"Material: {material.name}") + + # Check features + if material.features.pbr: + print(" PBR material") + + # Access PBR properties + base = material.pbr_base_color + if base.has_value: + r, g, b, a = base.value_vec4 + print(f" Base color: RGB({r:.2f}, {g:.2f}, {b:.2f})") + if base.texture: + print(f" Base texture: {base.texture.filename}") +``` + +### Material Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Material name | ✅ | +| `shader_type` | `ShaderType` | Shader type | ✅ | +| `shading_model_name` | `str` | Shading model name | ✅ | +| `features` | `MaterialFeatures` | Feature flags (23 bools) | ✅ | +| `textures` | `list[MaterialTexture]` | Texture mappings | ✅ | + +### Material PBR Properties (47 total) + +All properties return `MaterialMap` objects with `value_vec4`, `texture`, `has_value`, etc. + +**Base**: +- `pbr_base_factor`, `pbr_base_color` + +**Surface**: +- `pbr_roughness`, `pbr_metalness`, `pbr_diffuse_roughness` +- `pbr_specular_factor`, `pbr_specular_color`, `pbr_specular_ior` +- `pbr_specular_anisotropy`, `pbr_specular_rotation` + +**Transmission**: +- `pbr_transmission_factor`, `pbr_transmission_color` +- `pbr_transmission_depth`, `pbr_transmission_scatter` +- `pbr_transmission_dispersion`, `pbr_transmission_roughness`, `pbr_transmission_priority` + +**Subsurface**: +- `pbr_subsurface_factor`, `pbr_subsurface_color`, `pbr_subsurface_radius` +- `pbr_subsurface_scale`, `pbr_subsurface_anisotropy`, `pbr_subsurface_tint_color` +- `pbr_subsurface_type` + +**Sheen**: +- `pbr_sheen_factor`, `pbr_sheen_color`, `pbr_sheen_roughness` + +**Coat**: +- `pbr_coat_factor`, `pbr_coat_color`, `pbr_coat_roughness` +- `pbr_coat_ior`, `pbr_coat_anisotropy`, `pbr_coat_rotation` +- `pbr_coat_normal`, `pbr_coat_affect_base_color`, `pbr_coat_affect_base_roughness` + +**Thin Film**: +- `pbr_thin_film_thickness`, `pbr_thin_film_ior` + +**Emission**: +- `pbr_emission_factor`, `pbr_emission_color` + +**Opacity**: +- `pbr_opacity` + +**Maps**: +- `pbr_normal_map`, `pbr_tangent_map`, `pbr_displacement_map` +- `pbr_ambient_occlusion` + +**Other**: +- `pbr_matte_factor`, `pbr_matte_color` +- `pbr_indirect_diffuse`, `pbr_indirect_specular` +- `pbr_glossiness`, `pbr_coat_glossiness`, `pbr_transmission_glossiness` + +### Material FBX Properties (21 total) + +**Diffuse**: +- `fbx_diffuse_factor`, `fbx_diffuse_color` + +**Specular**: +- `fbx_specular_factor`, `fbx_specular_color`, `fbx_specular_exponent` + +**Reflection**: +- `fbx_reflection_factor`, `fbx_reflection_color` + +**Transparency**: +- `fbx_transparency_factor`, `fbx_transparency_color` + +**Emission**: +- `fbx_emission_factor`, `fbx_emission_color` + +**Ambient**: +- `fbx_ambient_factor`, `fbx_ambient_color` + +**Maps**: +- `fbx_normal_map`, `fbx_bump`, `fbx_bump_factor` +- `fbx_displacement_factor`, `fbx_displacement` +- `fbx_vector_displacement_factor`, `fbx_vector_displacement` + +### MaterialMap Properties + +Each material property (e.g., `pbr_base_color`) returns a `MaterialMap` object: + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `value_vec4` | `tuple[float, float, float, float]` | RGBA value | ✅ | +| `value_int` | `int` | Integer value | ✅ | +| `has_value` | `bool` | Has value set | ✅ | +| `texture` | `Texture \| None` | Associated texture | ✅ | +| `texture_enabled` | `bool` | Texture enabled | ✅ | +| `feature_disabled` | `bool` | Feature disabled | ✅ | +| `value_components` | `int` | Component count (1-4) | ✅ | + +### MaterialFeatures Properties + +Quick boolean flags for material features: + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `pbr` | `bool` | Is PBR material | ✅ | +| `metalness` | `bool` | Has metalness | ✅ | +| `diffuse` | `bool` | Has diffuse | ✅ | +| `specular` | `bool` | Has specular | ✅ | +| `emission` | `bool` | Has emission | ✅ | +| `transmission` | `bool` | Has transmission | ✅ | +| `coat` | `bool` | Has clear coat | ✅ | +| `sheen` | `bool` | Has sheen | ✅ | +| `opacity` | `bool` | Has opacity | ✅ | +| `ambient_occlusion` | `bool` | Has AO | ✅ | +| `matte` | `bool` | Has matte | ✅ | +| `unlit` | `bool` | Is unlit | ✅ | +| `ior` | `bool` | Has IOR | ✅ | +| `diffuse_roughness` | `bool` | Has diffuse roughness | ✅ | +| `transmission_roughness` | `bool` | Has transmission roughness | ✅ | +| `thin_walled` | `bool` | Is thin walled | ✅ | +| `caustics` | `bool` | Has caustics | ✅ | +| `exit_to_background` | `bool` | Exit to background | ✅ | +| `internal_reflections` | `bool` | Has internal reflections | ✅ | +| `double_sided` | `bool` | Is double sided | ✅ | +| `roughness` | `bool` | Has roughness | ✅ | +| `glossiness` | `bool` | Has glossiness | ✅ | +| `coat_roughness` | `bool` | Has coat roughness | ✅ | + +### MaterialTexture Properties + +Material-texture mapping relationship: + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `material_prop` | `str` | Material property name | ✅ | +| `shader_prop` | `str` | Shader property name | ✅ | +| `texture` | `Texture \| None` | Texture object | ✅ | + +--- + +## Scene.textures + +**Type**: `list[Texture]` +**Status**: ✅ Complete (33% - basic properties only) + +List of all texture objects referenced in the scene. + +```python +print(f"Total textures: {len(scene.textures)}") + +for texture in scene.textures: + print(f"Texture: {texture.name}") + print(f" Type: {texture.type}") + print(f" File: {texture.filename}") +``` + +### Texture Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Texture name | ✅ | +| `type` | `TextureType` | Texture type | ✅ | +| `filename` | `str` | Filename | ✅ | +| `absolute_filename` | `str` | Absolute path | ✅ | +| `relative_filename` | `str` | Relative path | ✅ | +| `content` | `bytes` | Embedded texture data | ❌ 🔴 | +| `has_file` | `bool` | Has external file | ❌ 🟡 | +| `file_index` | `int` | File index | ❌ 🟡 | +| `video` | `Video \| None` | Video reference | ❌ 🟡 | +| `layers` | `list[TextureLayer]` | Texture layers | ❌ 🟢 | +| `uv_set` | `str` | UV set name | ❌ 🟡 | +| `wrap_u` | `WrapMode` | U wrap mode | ❌ 🟡 | +| `wrap_v` | `WrapMode` | V wrap mode | ❌ 🟡 | +| `uv_transform` | `Transform` | UV transform | ❌ 🟢 | +| `shader` | `Shader \| None` | Shader reference | ❌ 🟢 | + +--- + +## Scene.videos + +**Type**: `list[Video]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +List of video texture objects. + +```python +# Not yet available +# for video in scene.videos: +# print(f"Video: {video.name}") +``` + +### Video Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Video name | ❌ | +| `filename` | `str` | Relative filename | ❌ | +| `absolute_filename` | `str` | Absolute filename | ❌ | +| `relative_filename` | `str` | Relative filename | ❌ | +| `raw_filename` | `bytes` | Raw filename (non-UTF-8) | ❌ | +| `raw_absolute_filename` | `bytes` | Raw absolute filename | ❌ | +| `raw_relative_filename` | `bytes` | Raw relative filename | ❌ | +| `content` | `bytes` | Embedded video content | ❌ | + +--- + +## Scene.shaders + +**Type**: `list[Shader]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +List of shader objects referenced in materials. + +```python +# Not yet available +# for shader in scene.shaders: +# print(f"Shader: {shader.name}") +``` + +### Shader Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Shader name | ❌ | +| `type` | `ShaderType` | Shader type | ❌ | +| `bindings` | `list[ShaderBinding]` | Shader bindings | ❌ | + +--- + +## Scene.shader_bindings + +**Type**: `list[ShaderBinding]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of shader binding objects. + +```python +# Not yet available +# for binding in scene.shader_bindings: +# print(f"Binding: {binding.name}") +``` + +### ShaderBinding Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Binding name | ❌ | +| `prop_bindings` | `list[ShaderPropBinding]` | Property bindings | ❌ | + +--- + +## Scene.lights + +**Type**: `list[Light]` +**Status**: ✅ Complete + +List of all light objects (point, spot, directional, area). + +```python +print(f"Total lights: {len(scene.lights)}") + +for light in scene.lights: + print(f"Light: {light.name}") + # Additional light properties available +``` + +--- + +## Scene.cameras + +**Type**: `list[Camera]` +**Status**: ✅ Complete + +List of all camera objects. + +```python +print(f"Total cameras: {len(scene.cameras)}") + +for camera in scene.cameras: + print(f"Camera: {camera.name}") + # Additional camera properties available +``` + +--- + +## Scene.bones + +**Type**: `list[Bone]` +**Status**: ✅ Complete + +List of all bone objects used for skeletal animation. + +```python +print(f"Total bones: {len(scene.bones)}") + +for bone in scene.bones: + print(f"Bone: {bone.name}") + # Additional bone properties available +``` + +--- + +## Scene.empties + +**Type**: `list[Empty]` +**Status**: ✅ Complete + +List of empty objects (locators), often used as control objects. + +```python +print(f"Total empties: {len(scene.empties)}") + +for empty in scene.empties: + print(f"Empty: {empty.name}") +``` + +### Empty Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Empty object name | ✅ | + +--- + +## Scene.line_curves + +**Type**: `list[LineCurve]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of line curve objects. + +```python +# Not yet available +``` + +### LineCurve Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Curve name | ❌ | +| `control_points` | `list[Vec3]` | Control points | ❌ | + +--- + +## Scene.nurbs_curves + +**Type**: `list[NurbsCurve]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of NURBS curve objects. + +```python +# Not yet available +``` + +### NurbsCurve Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | NURBS curve name | ❌ | +| `control_points` | `list[Vec4]` | Control points | ❌ | +| `knot_vector` | `list[float]` | Knot vector | ❌ | + +--- + +## Scene.nurbs_surfaces + +**Type**: `list[NurbsSurface]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of NURBS surface objects. + +```python +# Not yet available +``` + +### NurbsSurface Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | NURBS surface name | ❌ | +| `control_points` | `ndarray` | Control points grid | ❌ | +| `knot_vector_u` | `list[float]` | U knot vector | ❌ | +| `knot_vector_v` | `list[float]` | V knot vector | ❌ | + +--- + +## Scene.nurbs_trim_surfaces + +**Type**: `list[NurbsTrimSurface]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of NURBS trimmed surface objects. + +```python +# Not yet available +``` + +### NurbsTrimSurface Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Trim surface name | ❌ | + +--- + +## Scene.nurbs_trim_boundaries + +**Type**: `list[NurbsTrimBoundary]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of NURBS trim boundary objects. + +```python +# Not yet available +``` + +### NurbsTrimBoundary Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Trim boundary name | ❌ | + +--- + +## Scene.procedural_geometries + +**Type**: `list[ProceduralGeometry]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of procedural geometry objects. + +```python +# Not yet available +``` + +### ProceduralGeometry Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Geometry name | ❌ | + +--- + +## Scene.stereo_cameras + +**Type**: `list[StereoCamera]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of stereo camera rig objects. + +```python +# Not yet available +``` + +### StereoCamera Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Stereo camera name | ❌ | +| `left` | `Camera \| None` | Left camera | ❌ | +| `right` | `Camera \| None` | Right camera | ❌ | + +--- + +## Scene.camera_switchers + +**Type**: `list[CameraSwitcher]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of camera switcher objects. + +```python +# Not yet available +``` + +### CameraSwitcher Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Camera switcher name | ❌ | + +--- + +## Scene.markers + +**Type**: `list[Marker]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of marker objects. + +```python +# Not yet available +``` + +### Marker Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Marker name | ❌ | + +--- + +## Scene.lod_groups + +**Type**: `list[LodGroup]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of LOD (Level of Detail) group objects. + +```python +# Not yet available +``` + +### LodGroup Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | LOD group name | ❌ | +| `lod_levels` | `list[LodLevel]` | LOD levels | ❌ | + +--- + +## Scene.skin_deformers + +**Type**: `list[CacheFile]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of cache file references (Alembic, etc.). + +```python +# Not yet available +``` + +--- + +## Scene.cache_deformers + +**Type**: `list[CacheDeformer]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of cache-based deformer objects. + +```python +# Not yet available +``` + +--- + +## Scene.display_layers + +**Type**: `list[DisplayLayer]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of display layer objects for visibility management. + +```python +# Not yet available +``` + +### DisplayLayer Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Layer name | ❌ | +| `visible` | `bool` | Layer visibility | ❌ | +| `ui_color` | `Vec3` | UI display color | ❌ | + +--- + +## Scene.selection_sets + +**Type**: `list[SelectionSet]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of selection set objects. + +```python +# Not yet available +``` + +### SelectionSet Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Selection set name | ❌ | +| `nodes` | `list[SelectionNode]` | Selected nodes | ❌ | + +--- + +## Scene.selection_nodes + +**Type**: `list[SelectionNode]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of selection node objects. + +```python +# Not yet available +``` + +### SelectionNode Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Selection node name | ❌ | +| `target_node` | `Node \| None` | Target node | ❌ | + +--- + +## Scene.characters + +**Type**: `list[Character]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of character objects for animation. + +```python +# Not yet available +``` + +### Character Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Character name | ❌ | + +--- + +## Scene.audio_layers + +**Type**: `list[AudioLayer]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of audio layer objects. + +```python +# Not yet available +``` + +### AudioLayer Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Audio layer name | ❌ | +| `clips` | `list[AudioClip]` | Audio clips | ❌ | + +--- + +## Scene.audio_clips + +**Type**: `list[AudioClip]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of audio clip objects. + +```python +# Not yet available +``` + +### AudioClip Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Audio clip name | ❌ | +| `filename` | `str` | Audio filename | ❌ | +| `absolute_filename` | `str` | Absolute path | ❌ | +| `relative_filename` | `str` | Relative path | ❌ | + +--- + +## Scene.poses + +**Type**: `list[Pose]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of pose data for bind poses. + +```python +# Not yet available +``` + +### Pose Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Pose name | ❌ | +| `bind_pose` | `bool` | Is bind pose | ❌ | +| `bone_poses` | `list[BonePose]` | Bone transformations | ❌ | + +--- + +## Scene.metadata_objects + +**Type**: `list[MetadataObject]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of metadata objects attached to elements. + +```python +# Not yet available +``` + +### MetadataObject Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Metadata object name | ❌ | + +--- + +## Scene.texture_files + +**Type**: `list[TextureFile]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +Unique texture files referenced by the scene (deduplicated texture paths). + +```python +# Not yet available +# for tex_file in scene.texture_files: +# print(f"Texture file: {tex_file.filename}") +``` + +### TextureFile Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `filename` | `str` | Filename | ❌ | +| `absolute_filename` | `str` | Absolute path | ❌ | +| `relative_filename` | `str` | Relative path | ❌ | +| `index` | `int` | File index | ❌ | +| `content` | `bytes` | Embedded content | ❌ | + +--- + +## Scene.elements + +**Type**: `list[Element]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +All elements in the whole file, sorted by ID. + +```python +# Not yet available +# for element in scene.elements: +# print(f"Element: {element.name} (type: {element.type})") +``` + +### Element Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Element name | ❌ | +| `type` | `ElementType` | Element type | ❌ | +| `element_id` | `int` | Unique element ID | ❌ | +| `typed_id` | `int` | Type-specific ID | ❌ | + +--- + +## Scene.connections_src + +**Type**: `list[Connection]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +All connections sorted by source element. + +```python +# Not yet available +# for conn in scene.connections_src: +# # Access connection data +# pass +``` + +### Connection Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `src` | `Element \| None` | Source element | ❌ | +| `dst` | `Element \| None` | Destination element | ❌ | +| `src_prop` | `str` | Source property | ❌ | +| `dst_prop` | `str` | Destination property | ❌ | + +--- + +## Scene.connections_dst + +**Type**: `list[Connection]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +All connections sorted by destination element. + +```python +# Not yet available +# for conn in scene.connections_dst: +# # Access connection data +# pass +``` + +### Connection Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `src` | `Element \| None` | Source element | ❌ | +| `dst` | `Element \| None` | Destination element | ❌ | +| `src_prop` | `str` | Source property | ❌ | +| `dst_prop` | `str` | Destination property | ❌ | + +--- + +## Scene.elements_by_name + +**Type**: `list[NameElement]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +Elements sorted by name and type for efficient lookup. + +```python +# Not yet available +``` + +### NameElement Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Element name | ❌ | +| `type` | `ElementType` | Element type | ❌ | +| `element` | `Element` | Element reference | ❌ | + +--- + +## Scene.dom_root + +**Type**: `DomNode | None` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +DOM root node (enabled if `retain_dom` load option is set). + +```python +# Not yet available +# if scene.dom_root: +# # Access DOM tree +# pass +``` + +### DomNode Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Node name | ❌ | +| `children` | `list[DomNode]` | Child nodes | ❌ | +| `values` | `list[DomValue]` | Node values | ❌ | + +--- + +## Scene.axes + +**Type**: `list[AnimStack]` +**Status**: ✅ Complete + +List of animation stacks (animation takes/clips). + +```python +print(f"Animation stacks: {len(scene.anim_stacks)}") + +for stack in scene.anim_stacks: + print(f"Anim stack: {stack.name}") + # Additional animation properties available +``` + +--- + +## Scene.anim_layers + +**Type**: `list[AnimLayer]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +List of animation layers within animation stacks. + +```python +# Not yet available +# for layer in scene.anim_layers: +# print(f"Anim layer: {layer.name}") +``` + +### AnimLayer Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Layer name | ❌ | +| `weight` | `float` | Layer weight | ❌ | +| `anim_values` | `list[AnimValue]` | Animation values | ❌ | +| `blended` | `bool` | Is blended | ❌ | +| `additive` | `bool` | Is additive | ❌ | + +--- + +## Scene.anim_values + +**Type**: `list[AnimValue]` +**Status**: ❌ Not Implemented +**Priority**: 🟢 Low + +List of animation values. + +```python +# Not yet available +# for value in scene.anim_values: +# # Access animation value data +# pass +``` + +### AnimValue Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Value name | ❌ | +| `default_value` | `float \| Vec3` | Default value | ❌ | +| `curves` | `list[AnimCurve]` | Animation curves | ❌ | + +--- + +## Scene.anim_curves + +**Type**: `list[AnimCurve]` +**Status**: ✅ Complete + +List of all animation curves (keyframe data). + +```python +print(f"Animation curves: {len(scene.anim_curves)}") + +for curve in scene.anim_curves: + # Access keyframe data + pass +``` + +--- + +## Scene.skin_deformers + +**Type**: `list[SkinDeformer]` +**Status**: ✅ Complete + +List of skin deformers used for skeletal animation. + +```python +print(f"Skin deformers: {len(scene.skin_deformers)}") + +for deformer in scene.skin_deformers: + print(f"Skin: {deformer.name}") + # Access bone weights +``` + +--- + +## Scene.skin_clusters + +**Type**: `list[SkinCluster]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +List of skin clusters (bone weight information). + +```python +# Not yet available +# for cluster in scene.skin_clusters: +# print(f"Cluster: {cluster.name}") +``` + +### SkinCluster Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Cluster name | ❌ | +| `bone_node` | `Node \| None` | Bone node | ❌ | +| `vertices` | `list[int]` | Affected vertices | ❌ | +| `weights` | `list[float]` | Vertex weights | ❌ | +| `bind_matrix` | `Matrix` | Bind transform matrix | ❌ | + +--- + +## Scene.blend_deformers + +**Type**: `list[BlendDeformer]` +**Status**: ✅ Complete + +List of blend shape deformers used for morph/shape key animation. + +```python +print(f"Blend deformers: {len(scene.blend_deformers)}") + +for deformer in scene.blend_deformers: + print(f"Blend: {deformer.name}") + # Access blend channels +``` + +--- + +## Scene.blend_channels + +**Type**: `list[BlendChannel]` +**Status**: ❌ Not Implemented +**Priority**: 🟡 Medium + +List of blend channels (morph targets within blend deformers). + +```python +# Not yet available +# for channel in scene.blend_channels: +# print(f"Channel: {channel.name}") +``` + +### BlendChannel Properties (Not Implemented) + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `name` | `str` | Channel name | ❌ | +| `weight` | `float` | Current weight | ❌ | +| `keyframes` | `list[BlendKeyframe]` | Keyframe data | ❌ | + +--- + +## Scene.blend_shapes + +**Type**: `list[BlendShape]` +**Status**: ✅ Complete + +List of individual blend shapes (morph targets). + +```python +print(f"Blend shapes: {len(scene.blend_shapes)}") + +for shape in scene.blend_shapes: + print(f"Shape: {shape.name}") + # Access shape geometry +``` + +--- + +## Scene.constraints + +**Type**: `list[Constraint]` +**Status**: ✅ Complete + +List of constraints (parent, aim, IK, etc.). + +```python +print(f"Constraints: {len(scene.constraints)}") + +for constraint in scene.constraints: + print(f"Constraint: {constraint.name}") + # Access constraint parameters +``` + +--- + +## Scene.axes + +**Type**: `CoordinateAxes` +**Status**: ✅ Complete + +The coordinate system used by the scene. + +```python +axes = scene.axes +print(f"Right: {axes.right}") # 0=+X, 1=+Y, 2=+Z, etc. +print(f"Up: {axes.up}") +print(f"Front: {axes.front}") + +# Common coordinate systems: +# Y-up, Z-front (Blender, Maya): right=0(+X), up=2(+Z), front=1(+Y) +# Z-up, Y-front (3ds Max): right=0(+X), up=2(+Z), front=1(-Y) +``` + +### CoordinateAxes Properties + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `right` | `CoordinateAxis` | Right axis direction | ✅ | +| `up` | `CoordinateAxis` | Up axis direction | ✅ | +| `front` | `CoordinateAxis` | Front axis direction | ✅ | + +--- + +## Scene.find_node() + +**Signature**: `find_node(name: str) -> Node | None` +**Status**: ✅ Complete + +Convenience method to find a node by its name. + +```python +cube = scene.find_node("Cube") +if cube: + print(f"Found node: {cube.name}") + if cube.mesh: + print(f"Has mesh with {cube.mesh.num_vertices} vertices") +else: + print("Node not found") +``` + +--- + +## Scene.find_material() + +**Signature**: `find_material(name: str) -> Material | None` +**Status**: ✅ Complete + +Convenience method to find a material by its name. + +```python +material = scene.find_material("Material.001") +if material: + print(f"Found material: {material.name}") + if material.features.pbr: + print("Is PBR material") +else: + print("Material not found") +``` + +--- + +## Helper Classes + +### Transform Class ✅ + +3D transform (translation, rotation, scale). + +| Property | Type | Description | Status | +|----------|------|-------------|--------| +| `translation` | `Vec3` | Position | ✅ | +| `rotation` | `Quat` | Rotation (quaternion) | ✅ | +| `scale` | `Vec3` | Scale | ✅ | + +```python +transform = Transform() +transform.translation = Vec3(1.0, 2.0, 3.0) +transform.rotation = Quat(0, 0, 0, 1) +transform.scale = Vec3(1.0, 1.0, 1.0) + +matrix = transform.to_matrix() +``` + +### Math Classes ✅ + +- `Vec2(x, y)` - 2D vector +- `Vec3(x, y, z)` - 3D vector +- `Vec4(x, y, z, w)` - 4D vector +- `Quat(x, y, z, w)` - Quaternion +- `Matrix()` - 4x4 matrix + +--- + +## Implementation Status Summary + +### By Module + +| Module | Implemented | Missing | Completion | +|--------|-------------|---------|------------| +| **Material** | 79 / 79 | 0 | 100% ✅ | +| **MaterialMap** | 7 / 7 | 0 | 100% ✅ | +| **MaterialFeatures** | 23 / 23 | 0 | 100% ✅ | +| **MaterialTexture** | 3 / 3 | 0 | 100% ✅ | +| **Texture** | 5 / 15 | 10 | 33% ⚠️ | +| **Scene (Core)** | 21 / 40 | 19 | 53% ⚠️ | +| **Node** | 10 / 17 | 7 | 59% ⚠️ | +| **Mesh** | 10 / 19 | 9 | 53% ⚠️ | + +### Critical Missing Features + +**🔴 Critical Priority** (Required for normal mapping): +1. `Mesh.vertex_tangent` - Tangent vectors for TBN matrix +2. `Mesh.vertex_bitangent` - Bitangent vectors for TBN matrix +3. `Mesh.vertex_color` - Vertex color data +4. `Texture.content` - Embedded texture data + +**🔴 High Priority** (Strongly recommended): +5. `Node.node_to_world` - World transform matrix +6. `Texture.has_file` - Texture type detection +7. `Texture.uv_set` - Multiple UV channels + +**🟡 Medium Priority**: +8. Scene special collections (videos, shaders) +9. Mesh multi-material support +10. Node geometry transform + +**🟢 Low Priority**: +11. NURBS objects (curves, surfaces, etc.) +12. Advanced features (cache, audio, LOD, etc.) + +--- + +**Last Updated**: 2026-01-24 +**Status**: Material system 100% complete, Scene core 53% complete +**Next Goal**: Implement Mesh tangent/bitangent for normal mapping support diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 3ebfc24..5f155b8 100755 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -252,13 +252,17 @@ def format_texture_info(texture, indent=" "): info = [] if texture.name: info.append(f"{indent}Name: {texture.name}") + + # Type information (now correctly aligned with C struct) + info.append(f"{indent}Type: {texture.type}") + + # File paths (now correctly aligned with C struct) 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 @@ -299,6 +303,31 @@ def main(): print(f" Materials: {stats['materials']}") print() + # Scene metadata and settings + meta = scene.metadata + settings = scene.settings + print("📄 Scene Info:") + print(" File:") + print(f" - Version: FBX {meta.version / 1000:.1f}") + print(f" - Format: {'ASCII' if meta.ascii else 'Binary'}") + if meta.creator: + print(f" - Creator: {meta.creator}") + if meta.filename: + print(f" - Filename: {meta.filename}") + print(" Settings:") + unit_name = ( + "cm" if settings.unit_meters == 0.01 else ("m" if settings.unit_meters == 1.0 else f"{settings.unit_meters}m") + ) + print(f" - Units: {unit_name} (1 unit = {settings.unit_meters}m)") + print(f" - FPS: {settings.frames_per_second}") + r, g, b = settings.ambient_color + if r > 0 or g > 0 or b > 0: + print(f" - Ambient color: RGB({r:.2f}, {g:.2f}, {b:.2f})") + if scene.empties: + print(" Special objects:") + print(f" - Empty nodes: {len(scene.empties)}") + print() + # Coordinate system axes = scene.axes coord_info = get_coordinate_system_info(axes) @@ -407,13 +436,11 @@ def main(): 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, - ] - ) + 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 @@ -436,6 +463,26 @@ def main(): shading_info.append(f"shader_type={material.shader_type}") print(f" - Shading: {', '.join(shading_info)}") + # Material features (new API) + features = material.features + active_features = [] + if features.pbr: + active_features.append("PBR") + if features.metalness: + active_features.append("metalness") + if features.roughness: + active_features.append("roughness") + if features.transmission: + active_features.append("transmission") + if features.emission: + active_features.append("emission") + if features.sheen: + active_features.append("sheen") + if features.coat: + active_features.append("clearcoat") + if active_features: + print(f" - Features: {', '.join(active_features)}") + # PBR properties print(" - PBR properties:") @@ -519,6 +566,16 @@ def main(): if tex_info: print(tex_info) + # All textures used by this material (new API) + mat_textures = material.textures + if mat_textures: + print(f" - Texture mappings: {len(mat_textures)} total") + # Show first few mappings as example + for mt in mat_textures[:3]: + print(f" {mt.material_prop} → {mt.texture.name if mt.texture else 'None'}") + if len(mat_textures) > 3: + print(f" ... and {len(mat_textures) - 3} more") + print() # Texture information diff --git a/ufbx/__init__.py b/ufbx/__init__.py index 31586d0..faca7e1 100644 --- a/ufbx/__init__.py +++ b/ufbx/__init__.py @@ -21,6 +21,7 @@ CoordinateAxis, Element, ElementType, + Empty, ErrorType, ExtrapolationMode, InheritMode, @@ -35,6 +36,7 @@ MaterialTexture, Matrix, Mesh, + Metadata, MirrorAxis, Node, ProjectionMode, @@ -43,6 +45,7 @@ Quat, RotationOrder, Scene, + SceneSettings, ShaderType, SkinCluster, SkinDeformer, @@ -55,6 +58,7 @@ UfbxFileNotFoundError, UfbxIOError, UfbxOutOfMemoryError, + Unknown, Vec2, Vec3, Vec4, @@ -84,6 +88,7 @@ "CoordinateAxes", "Element", "ElementType", + "Empty", "ErrorType", "ExtrapolationMode", "InheritMode", @@ -98,6 +103,7 @@ "MaterialTexture", "Matrix", "Mesh", + "Metadata", "MirrorAxis", "Node", "ProjectionMode", @@ -106,6 +112,7 @@ "Quat", "RotationOrder", "Scene", + "SceneSettings", "ShaderType", "SkinCluster", "SkinDeformer", @@ -118,6 +125,7 @@ "UfbxFileNotFoundError", "UfbxIOError", "UfbxOutOfMemoryError", + "Unknown", "Vec2", "Vec3", "Vec4", diff --git a/ufbx/__init__.pyi b/ufbx/__init__.pyi index 9558726..e35570d 100644 --- a/ufbx/__init__.pyi +++ b/ufbx/__init__.pyi @@ -248,13 +248,13 @@ class Texture(Element): @property def name(self) -> str: ... @property + def type(self) -> TextureType: ... + @property def filename(self) -> str: ... @property def absolute_filename(self) -> str: ... @property def relative_filename(self) -> str: ... - @property - def type(self) -> TextureType: ... class AnimStack(Element): @property @@ -338,6 +338,131 @@ class Constraint(Element): @property def active(self) -> bool: ... +class MaterialMap: + """Material property map (value + optional texture)""" + @property + def value_vec4(self) -> tuple[float, float, float, float]: ... + @property + def value_int(self) -> int: ... + @property + def has_value(self) -> bool: ... + @property + def texture(self) -> Texture | None: ... + @property + def texture_enabled(self) -> bool: ... + @property + def feature_disabled(self) -> bool: ... + @property + def value_components(self) -> int: ... + +class MaterialFeatures: + """Material features flags""" + @property + def pbr(self) -> bool: ... + @property + def metalness(self) -> bool: ... + @property + def diffuse(self) -> bool: ... + @property + def specular(self) -> bool: ... + @property + def emission(self) -> bool: ... + @property + def transmission(self) -> bool: ... + @property + def coat(self) -> bool: ... + @property + def sheen(self) -> bool: ... + @property + def opacity(self) -> bool: ... + @property + def ambient_occlusion(self) -> bool: ... + @property + def matte(self) -> bool: ... + @property + def unlit(self) -> bool: ... + @property + def ior(self) -> bool: ... + @property + def diffuse_roughness(self) -> bool: ... + @property + def transmission_roughness(self) -> bool: ... + @property + def thin_walled(self) -> bool: ... + @property + def caustics(self) -> bool: ... + @property + def exit_to_background(self) -> bool: ... + @property + def internal_reflections(self) -> bool: ... + @property + def double_sided(self) -> bool: ... + @property + def roughness(self) -> bool: ... + @property + def glossiness(self) -> bool: ... + @property + def coat_roughness(self) -> bool: ... + +class MaterialTexture: + """Material texture mapping""" + @property + def material_prop(self) -> str: ... + @property + def shader_prop(self) -> str: ... + @property + def texture(self) -> Texture | None: ... + +class Metadata: + """Scene metadata""" + @property + def ascii(self) -> bool: ... + @property + def version(self) -> int: ... + @property + def file_format(self) -> int: ... + @property + def creator(self) -> str: ... + @property + def big_endian(self) -> bool: ... + @property + def filename(self) -> str: ... + @property + def relative_root(self) -> str: ... + +class SceneSettings: + """Scene settings""" + @property + def axes(self) -> CoordinateAxes: ... + @property + def unit_meters(self) -> float: ... + @property + def frames_per_second(self) -> float: ... + @property + def ambient_color(self) -> tuple[float, float, float]: ... + @property + def default_camera(self) -> str: ... + @property + def time_mode(self) -> int: ... + @property + def time_protocol(self) -> int: ... + @property + def snap_mode(self) -> int: ... + @property + def original_axis_up(self) -> int: ... + @property + def original_unit_meters(self) -> float: ... + +class Empty(Element): + """Empty node (null object)""" + @property + def name(self) -> str: ... + +class Unknown(Element): + """Unknown element type""" + @property + def name(self) -> str: ... + class Scene: @classmethod def load_file(cls, filename: str) -> Scene: ... @@ -347,6 +472,10 @@ class Scene: def __enter__(self) -> Scene: ... def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any | None) -> None: ... @property + def metadata(self) -> Metadata: ... + @property + def settings(self) -> SceneSettings: ... + @property def nodes(self) -> list[Node]: ... @property def meshes(self) -> list[Mesh]: ... @@ -359,6 +488,10 @@ class Scene: @property def bones(self) -> list[Bone]: ... @property + def empties(self) -> list[Empty]: ... + @property + def unknowns(self) -> list[Unknown]: ... + @property def textures(self) -> list[Texture]: ... @property def anim_stacks(self) -> list[AnimStack]: ... @@ -431,6 +564,170 @@ class Mesh(Element): class Material(Element): @property def name(self) -> str: ... + @property + def shader_type(self) -> ShaderType: ... + @property + def shading_model_name(self) -> str: ... + @property + def features(self) -> MaterialFeatures: ... + @property + def textures(self) -> list[MaterialTexture]: ... + + # PBR material maps + @property + def pbr_base_factor(self) -> MaterialMap: ... + @property + def pbr_base_color(self) -> MaterialMap: ... + @property + def pbr_roughness(self) -> MaterialMap: ... + @property + def pbr_metalness(self) -> MaterialMap: ... + @property + def pbr_diffuse_roughness(self) -> MaterialMap: ... + @property + def pbr_specular_factor(self) -> MaterialMap: ... + @property + def pbr_specular_color(self) -> MaterialMap: ... + @property + def pbr_specular_ior(self) -> MaterialMap: ... + @property + def pbr_specular_anisotropy(self) -> MaterialMap: ... + @property + def pbr_specular_rotation(self) -> MaterialMap: ... + @property + def pbr_transmission_factor(self) -> MaterialMap: ... + @property + def pbr_transmission_color(self) -> MaterialMap: ... + @property + def pbr_transmission_depth(self) -> MaterialMap: ... + @property + def pbr_transmission_scatter(self) -> MaterialMap: ... + @property + def pbr_transmission_scatter_anisotropy(self) -> MaterialMap: ... + @property + def pbr_transmission_dispersion(self) -> MaterialMap: ... + @property + def pbr_transmission_roughness(self) -> MaterialMap: ... + @property + def pbr_transmission_extra_roughness(self) -> MaterialMap: ... + @property + def pbr_transmission_priority(self) -> MaterialMap: ... + @property + def pbr_transmission_enable_in_aov(self) -> MaterialMap: ... + @property + def pbr_subsurface_factor(self) -> MaterialMap: ... + @property + def pbr_subsurface_color(self) -> MaterialMap: ... + @property + def pbr_subsurface_radius(self) -> MaterialMap: ... + @property + def pbr_subsurface_scale(self) -> MaterialMap: ... + @property + def pbr_subsurface_anisotropy(self) -> MaterialMap: ... + @property + def pbr_subsurface_tint_color(self) -> MaterialMap: ... + @property + def pbr_subsurface_type(self) -> MaterialMap: ... + @property + def pbr_sheen_factor(self) -> MaterialMap: ... + @property + def pbr_sheen_color(self) -> MaterialMap: ... + @property + def pbr_sheen_roughness(self) -> MaterialMap: ... + @property + def pbr_coat_factor(self) -> MaterialMap: ... + @property + def pbr_coat_color(self) -> MaterialMap: ... + @property + def pbr_coat_roughness(self) -> MaterialMap: ... + @property + def pbr_coat_ior(self) -> MaterialMap: ... + @property + def pbr_coat_anisotropy(self) -> MaterialMap: ... + @property + def pbr_coat_rotation(self) -> MaterialMap: ... + @property + def pbr_coat_normal(self) -> MaterialMap: ... + @property + def pbr_coat_affect_base_color(self) -> MaterialMap: ... + @property + def pbr_coat_affect_base_roughness(self) -> MaterialMap: ... + @property + def pbr_thin_film_factor(self) -> MaterialMap: ... + @property + def pbr_thin_film_thickness(self) -> MaterialMap: ... + @property + def pbr_thin_film_ior(self) -> MaterialMap: ... + @property + def pbr_emission_factor(self) -> MaterialMap: ... + @property + def pbr_emission_color(self) -> MaterialMap: ... + @property + def pbr_opacity(self) -> MaterialMap: ... + @property + def pbr_indirect_diffuse(self) -> MaterialMap: ... + @property + def pbr_indirect_specular(self) -> MaterialMap: ... + @property + def pbr_normal_map(self) -> MaterialMap: ... + @property + def pbr_tangent_map(self) -> MaterialMap: ... + @property + def pbr_displacement_map(self) -> MaterialMap: ... + @property + def pbr_matte_factor(self) -> MaterialMap: ... + @property + def pbr_matte_color(self) -> MaterialMap: ... + @property + def pbr_ambient_occlusion(self) -> MaterialMap: ... + @property + def pbr_glossiness(self) -> MaterialMap: ... + @property + def pbr_coat_glossiness(self) -> MaterialMap: ... + @property + def pbr_transmission_glossiness(self) -> MaterialMap: ... + + # FBX material maps + @property + def fbx_diffuse_factor(self) -> MaterialMap: ... + @property + def fbx_diffuse_color(self) -> MaterialMap: ... + @property + def fbx_specular_factor(self) -> MaterialMap: ... + @property + def fbx_specular_color(self) -> MaterialMap: ... + @property + def fbx_specular_exponent(self) -> MaterialMap: ... + @property + def fbx_reflection_factor(self) -> MaterialMap: ... + @property + def fbx_reflection_color(self) -> MaterialMap: ... + @property + def fbx_transparency_factor(self) -> MaterialMap: ... + @property + def fbx_transparency_color(self) -> MaterialMap: ... + @property + def fbx_emission_factor(self) -> MaterialMap: ... + @property + def fbx_emission_color(self) -> MaterialMap: ... + @property + def fbx_ambient_factor(self) -> MaterialMap: ... + @property + def fbx_ambient_color(self) -> MaterialMap: ... + @property + def fbx_normal_map(self) -> MaterialMap: ... + @property + def fbx_bump(self) -> MaterialMap: ... + @property + def fbx_bump_factor(self) -> MaterialMap: ... + @property + def fbx_displacement_factor(self) -> MaterialMap: ... + @property + def fbx_displacement(self) -> MaterialMap: ... + @property + def fbx_vector_displacement_factor(self) -> MaterialMap: ... + @property + def fbx_vector_displacement(self) -> MaterialMap: ... def load_file(filename: str) -> Scene: ... def load_memory(data: bytes) -> Scene: ... diff --git a/ufbx/_ufbx.pyx b/ufbx/_ufbx.pyx index 8ece87e..425c0df 100644 --- a/ufbx/_ufbx.pyx +++ b/ufbx/_ufbx.pyx @@ -13,6 +13,11 @@ np.import_array() # C declarations cdef extern from "ufbx-c/ufbx.h": + ctypedef struct ufbx_vec3: + double x + double y + double z + ctypedef struct ufbx_vec4: double x double y @@ -157,20 +162,144 @@ cdef extern from "ufbx-c/ufbx.h": const char* data size_t length + # Forward declarations - pointers only + ctypedef struct ufbx_dom_node + ctypedef struct ufbx_scene + ctypedef struct ufbx_shader + ctypedef struct ufbx_video + + # List structures + ctypedef struct ufbx_node_list: + void** data + size_t count + ctypedef struct ufbx_connection_list: + void** data + size_t count + + # Full ufbx_element structure matching ufbx.h layout ctypedef struct ufbx_element: ufbx_string name - + void* props # ufbx_props - treated as opaque + unsigned int element_id + unsigned int typed_id + ufbx_node_list instances + int type # ufbx_element_type + ufbx_connection_list connections_src + ufbx_connection_list connections_dst + ufbx_dom_node* dom_node + ufbx_scene* scene + + # Forward declaration for shader + # (already declared above) + + # Full ufbx_material structure matching ufbx.h layout ctypedef struct ufbx_material: - ufbx_element element + # Element fields (union expanded as struct fields) + ufbx_string name + void* props # ufbx_props + unsigned int element_id + unsigned int typed_id + ufbx_node_list instances + int type # ufbx_element_type + ufbx_connection_list connections_src + ufbx_connection_list connections_dst + ufbx_dom_node* dom_node + ufbx_scene* scene + # Material-specific fields ufbx_material_fbx_maps fbx ufbx_material_pbr_maps pbr ufbx_material_features features int shader_type + ufbx_shader* shader ufbx_string shading_model_name + ufbx_string shader_prop_prefix ufbx_material_texture_list textures - ctypedef struct ufbx_scene: + # Metadata structure + ctypedef struct ufbx_metadata: + bint ascii + unsigned int version + int file_format + ufbx_string creator + bint big_endian + ufbx_string filename + ufbx_string relative_root + + # Time/Animation enums + ctypedef enum ufbx_time_mode: + pass + ctypedef enum ufbx_time_protocol: pass + ctypedef enum ufbx_snap_mode: + pass + ctypedef enum ufbx_coordinate_axis: + pass + + # Coordinate axes structure + ctypedef struct ufbx_coordinate_axes: + int right + int up + int front + + # Scene settings structure + ctypedef struct ufbx_scene_settings: + ufbx_coordinate_axes axes + double unit_meters + double frames_per_second + ufbx_vec3 ambient_color + ufbx_string default_camera + ufbx_time_mode time_mode + ufbx_time_protocol time_protocol + ufbx_snap_mode snap_mode + ufbx_coordinate_axis original_axis_up + double original_unit_meters + + # Forward declarations for element types + ctypedef struct ufbx_empty: + # Element fields (union expanded) + ufbx_string name + void* props + unsigned int element_id + unsigned int typed_id + ufbx_node_list instances + int type + ufbx_connection_list connections_src + ufbx_connection_list connections_dst + ufbx_dom_node* dom_node + ufbx_scene* scene + + ctypedef struct ufbx_unknown: + # Element fields (union expanded) + ufbx_string name + void* props + unsigned int element_id + unsigned int typed_id + ufbx_node_list instances + int element_type + ufbx_connection_list connections_src + ufbx_connection_list connections_dst + ufbx_dom_node* dom_node + ufbx_scene* scene_ptr + # Unknown-specific fields + ufbx_string type_name + ufbx_string super_type + ufbx_string sub_type + + # List structures + ctypedef struct ufbx_empty_list: + ufbx_empty** data + size_t count + ctypedef struct ufbx_unknown_list: + ufbx_unknown** data + size_t count + + # Scene structure with metadata and settings + ctypedef struct ufbx_scene: + ufbx_metadata metadata + ufbx_scene_settings settings + ufbx_empty_list empties + ufbx_unknown_list unknowns + ctypedef struct ufbx_mesh: pass ctypedef struct ufbx_node: @@ -181,12 +310,49 @@ cdef extern from "ufbx-c/ufbx.h": pass ctypedef struct ufbx_bone: pass + + # Forward declarations for texture-related types + # (ufbx_video already declared above) + ctypedef struct ufbx_blob: + void* data + size_t size + ctypedef struct ufbx_texture_layer_list: + void** data + size_t count + + # Full ufbx_texture structure matching ufbx.h layout ctypedef struct ufbx_texture: - ufbx_element element + # Element fields (union expanded as struct fields) + ufbx_string name + void* props # ufbx_props + unsigned int element_id + unsigned int typed_id + ufbx_node_list instances + int element_type # ufbx_element_type (renamed to avoid conflict with texture.type) + ufbx_connection_list connections_src + ufbx_connection_list connections_dst + ufbx_dom_node* dom_node + ufbx_scene* scene + # Texture-specific fields - MUST follow element fields + int type # ufbx_texture_type - comes IMMEDIATELY after element + # File paths ufbx_string filename - ufbx_string relative_filename ufbx_string absolute_filename - int type + ufbx_string relative_filename + # Raw (non-UTF-8) paths + ufbx_blob raw_filename + ufbx_blob raw_absolute_filename + ufbx_blob raw_relative_filename + # Embedded content + ufbx_blob content + # Video reference + ufbx_video* video + # File info + unsigned int file_index + bint has_file + # Layered textures + ufbx_texture_layer_list layers + ctypedef struct ufbx_anim_stack: pass ctypedef struct ufbx_anim_layer: @@ -969,7 +1135,7 @@ cdef class Texture(Element): """Texture name""" if self._scene._closed: raise RuntimeError("Scene is closed") - cdef bytes name_bytes = self._texture.element.name.data[:self._texture.element.name.length] + cdef bytes name_bytes = self._texture.name.data[:self._texture.name.length] return name_bytes.decode('utf-8', errors='replace') @property @@ -1373,6 +1539,209 @@ cdef class Constraint(Element): return ufbx_wrapper_constraint_get_active(self._constraint) +cdef class Metadata: + """Scene metadata""" + cdef Scene _scene + cdef const ufbx_metadata* _metadata + + @staticmethod + cdef Metadata _create(Scene scene, const ufbx_metadata* metadata): + """Create Metadata from C struct""" + cdef Metadata obj = Metadata.__new__(Metadata) + obj._scene = scene + obj._metadata = metadata + return obj + + @property + def ascii(self): + """Whether the file is ASCII format""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._metadata.ascii + + @property + def version(self): + """FBX version (e.g., 7400 for 7.4)""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._metadata.version + + @property + def file_format(self): + """File format""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._metadata.file_format + + @property + def creator(self): + """Creator application name""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes creator_bytes = self._metadata.creator.data[:self._metadata.creator.length] + return creator_bytes.decode('utf-8', errors='replace') + + @property + def big_endian(self): + """Whether the file is big-endian""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._metadata.big_endian + + @property + def filename(self): + """Original filename""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes filename_bytes = self._metadata.filename.data[:self._metadata.filename.length] + return filename_bytes.decode('utf-8', errors='replace') + + @property + def relative_root(self): + """Relative root path""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes root_bytes = self._metadata.relative_root.data[:self._metadata.relative_root.length] + return root_bytes.decode('utf-8', errors='replace') + + +cdef class SceneSettings: + """Scene settings""" + cdef Scene _scene + cdef const ufbx_scene_settings* _settings + + @staticmethod + cdef SceneSettings _create(Scene scene, const ufbx_scene_settings* settings): + """Create SceneSettings from C struct""" + cdef SceneSettings obj = SceneSettings.__new__(SceneSettings) + obj._scene = scene + obj._settings = settings + return obj + + @property + def axes(self): + """Coordinate axes""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return CoordinateAxes( + self._settings.axes.right, + self._settings.axes.up, + self._settings.axes.front + ) + + @property + def unit_meters(self): + """Units in meters (e.g., 0.01 for centimeters)""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.unit_meters + + @property + def frames_per_second(self): + """Animation frames per second""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.frames_per_second + + @property + def ambient_color(self): + """Ambient color (r, g, b)""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return ( + self._settings.ambient_color.x, + self._settings.ambient_color.y, + self._settings.ambient_color.z + ) + + @property + def default_camera(self): + """Default camera name""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes camera_bytes = self._settings.default_camera.data[:self._settings.default_camera.length] + return camera_bytes.decode('utf-8', errors='replace') + + @property + def time_mode(self): + """Time mode""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.time_mode + + @property + def time_protocol(self): + """Time protocol""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.time_protocol + + @property + def snap_mode(self): + """Snap mode""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.snap_mode + + @property + def original_axis_up(self): + """Original up axis""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.original_axis_up + + @property + def original_unit_meters(self): + """Original unit in meters""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + return self._settings.original_unit_meters + + +cdef class Empty(Element): + """Empty node (null object)""" + cdef Scene _scene + cdef ufbx_empty* _empty + + @staticmethod + cdef Empty _create(Scene scene, ufbx_empty* empty): + """Create Empty from C struct""" + cdef Empty obj = Empty.__new__(Empty) + obj._scene = scene + obj._empty = empty + return obj + + @property + def name(self): + """Empty name""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes name_bytes = self._empty.name.data[:self._empty.name.length] + return name_bytes.decode('utf-8', errors='replace') + + +cdef class Unknown(Element): + """Unknown element type""" + cdef Scene _scene + cdef ufbx_unknown* _unknown + + @staticmethod + cdef Unknown _create(Scene scene, ufbx_unknown* unknown): + """Create Unknown from C struct""" + cdef Unknown obj = Unknown.__new__(Unknown) + obj._scene = scene + obj._unknown = unknown + return obj + + @property + def name(self): + """Unknown element name""" + if self._scene._closed: + raise RuntimeError("Scene is closed") + cdef bytes name_bytes = self._unknown.name.data[:self._unknown.name.length] + return name_bytes.decode('utf-8', errors='replace') + + cdef class Scene: """FBX Scene - manages lifetime of all scene data""" cdef ufbx_scene* _scene @@ -1406,6 +1775,20 @@ cdef class Scene: def load_memory(cls, data): return load_memory(data) + @property + def metadata(self): + """Scene metadata (file info, creator, version, etc.)""" + if self._closed: + raise RuntimeError("Scene is closed") + return Metadata._create(self, &self._scene.metadata) + + @property + def settings(self): + """Scene settings (axes, units, FPS, etc.)""" + if self._closed: + raise RuntimeError("Scene is closed") + return SceneSettings._create(self, &self._scene.settings) + @property def nodes(self): """Get all nodes in the scene""" @@ -1474,6 +1857,30 @@ cdef class Scene: cdef size_t count = ufbx_wrapper_scene_get_num_bones(self._scene) return [self._get_bone(i) for i in range(count)] + @property + def empties(self): + """Get all empty nodes in the scene""" + if self._closed: + raise RuntimeError("Scene is closed") + cdef size_t count = self._scene.empties.count + cdef list result = [] + cdef size_t i + for i in range(count): + result.append(Empty._create(self, self._scene.empties.data[i])) + return result + + @property + def unknowns(self): + """Get all unknown elements in the scene""" + if self._closed: + raise RuntimeError("Scene is closed") + cdef size_t count = self._scene.unknowns.count + cdef list result = [] + cdef size_t i + for i in range(count): + result.append(Unknown._create(self, self._scene.unknowns.data[i])) + return result + @property def textures(self): """Get all textures in the scene""" @@ -2161,7 +2568,7 @@ cdef class Material(Element): """Material name""" if self._scene._closed: raise RuntimeError("Scene is closed") - cdef bytes name_bytes = self._material.element.name.data[:self._material.element.name.length] + cdef bytes name_bytes = self._material.name.data[:self._material.name.length] return name_bytes.decode('utf-8', errors='replace') @property