如何在 Python 中遍历 3D 场景图
在 Aspose.3D FOSS 中,场景图是一个树形结构的 Node 对象根于 scene.root_node.。每个 3D 文件,无论是从 OBJ、glTF、STL、COLLADA 还是 3MF 加载的,都会生成相同的树结构。了解如何遍历它可以让你检查几何体、统计多边形、按类型过滤对象,以及处理复杂场景的特定部分。.
分步指南
步骤 1:安装并导入
从 PyPI 安装 Aspose.3D FOSS::
pip install aspose-3d-foss导入所需的类::
from aspose.threed import Scene
from aspose.threed.entities import Mesh所有公共类都位于 aspose.threed 或其子包(aspose.threed.entities, aspose.threed.utilities).
步骤 2:从文件加载场景
使用静态 Scene.from_file() 方法打开任何受支持的格式。格式会根据文件扩展名自动检测::
scene = Scene.from_file("model.gltf")你也可以使用显式的加载选项打开::
from aspose.threed import Scene
from aspose.threed.formats import ObjLoadOptions
options = ObjLoadOptions()
options.enable_materials = True
scene = Scene()
scene.open("model.obj", options)加载后,, scene.root_node 是树的根节点。所有导入的节点都是该节点的子节点或后代。.
步骤 3:编写递归遍历函数
最简单的遍历以深度优先顺序访问每个节点::
from aspose.threed import Scene
from aspose.threed.entities import Mesh
def traverse(node, depth=0):
prefix = " " * depth
entity_name = type(node.entity).__name__ if node.entity else "-"
print(f"{prefix}[{entity_name}] {node.name}")
for child in node.child_nodes:
traverse(child, depth + 1)
scene = Scene.from_file("model.gltf")
traverse(scene.root_node)示例输出::
[-]
[-] Armature
[Mesh] Body
[Mesh] Eyes
[-] Ground
[Mesh] Plane<button class=“hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50” title=“Copy code”
<div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div>
<div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div>
注意:: 根节点的名称为空(""),因此第一行显示 [-] 后面没有名称。.
node.child_nodes 按插入顺序返回子节点(即导入器或用户添加它们的顺序)。.
步骤 4:访问每个节点上的实体属性
使用 node.entity 获取附加到节点的第一个实体,或 node.entities 遍历它们所有。访问特定格式属性之前先检查类型::
from aspose.threed import Scene
from aspose.threed.entities import Mesh
def print_entity_details(node, depth=0):
indent = " " * depth
for entity in node.entities:
if isinstance(entity, Mesh):
mesh = entity
print(f"{indent}Mesh '{node.name}':")
print(f"{indent} vertices : {len(mesh.control_points)}")
print(f"{indent} polygons : {mesh.polygon_count}")
print(f"{indent} cast_shadows : {mesh.cast_shadows}")
print(f"{indent} receive_shadows: {mesh.receive_shadows}")
for child in node.child_nodes:
print_entity_details(child, depth + 1)
scene = Scene.from_file("model.gltf")
print_entity_details(scene.root_node)node.transform.translation, node.transform.rotation,,并且 node.transform.scaling 提供局部空间变换。. node.global_transform 给出已评估的世界空间结果(带有 global_transform.scale 用于世界空间缩放)。.
步骤 5:按实体类型过滤节点
要仅对特定实体类型进行操作,请添加一个 isinstance 在遍历中加入 guard::
from aspose.threed import Scene
from aspose.threed.entities import Mesh
def find_mesh_nodes(node, results=None):
if results is None:
results = []
for entity in node.entities:
if isinstance(entity, Mesh):
results.append(node)
break # One match per node is enough
for child in node.child_nodes:
find_mesh_nodes(child, results)
return results
scene = Scene.from_file("model.gltf")
mesh_nodes = find_mesh_nodes(scene.root_node)
print(f"Found {len(mesh_nodes)} mesh node(s)")
for n in mesh_nodes:
print(f" {n.name}")此模式适用于批量操作:对每个网格节点应用变换、替换材质或导出子树。.
步骤 6:收集所有网格并打印顶点计数
在一次遍历中汇总网格统计信息::
from aspose.threed import Scene
from aspose.threed.entities import Mesh
def collect_meshes(node, results=None):
if results is None:
results = []
if isinstance(node.entity, Mesh):
results.append((node.name, node.entity))
for child in node.child_nodes:
collect_meshes(child, results)
return results
scene = Scene.from_file("model.gltf")
meshes = collect_meshes(scene.root_node)
print(f"Total meshes: {len(meshes)}")
total_verts = 0
total_polys = 0
for name, mesh in meshes:
verts = len(mesh.control_points)
polys = mesh.polygon_count
total_verts += verts
total_polys += polys
print(f" {name}: {verts} vertices, {polys} polygons")
print(f"Scene totals: {total_verts} vertices, {total_polys} polygons")常见问题
| 问题 | 解决方案 |
|---|---|
AttributeError: 'NoneType' object has no attribute 'polygons' | 使用 Guard with if node.entity is not None 或 isinstance(node.entity, Mesh) 在访问网格属性之前。没有实体的节点返回 None. |
| 遍历提前停止 | 确保递归深入到 node.child_nodes.。如果仅迭代 scene.root_node.child_nodes (非递归),你会错过所有后代。. |
| 收集结果中缺少网格 | 检查文件格式是否保留层次结构。OBJ 会将所有几何体展平为单一节点层级。使用 glTF 或 COLLADA 以支持深层层次结构。. |
node.entity 只返回第一个实体 | 使用 node.entities (完整列表) 当节点携带多个实体时。. node.entity 是…的简写 node.entities[0] 当 entities 非空。. |
collect_meshes 对已加载的 STL 返回 0 结果 | STL 文件通常会生成一个单一的平面节点,直接在其下只有一个网格实体 root_node. 检查 root_node.child_nodes[0].entity 直接。. |
| 节点名称为空字符串 | 某些格式(binary STL、some OBJ files)不存储对象名称。节点将拥有空的 name 字符串;请改用索引进行标识。. |
常见问答
场景图可以有多深??
没有硬性限制。Python 的默认递归深度限制(1000 帧)适用于递归遍历函数。对于非常深的层级,可将递归改写为显式栈::
from collections import deque
from aspose.threed import Scene
scene = Scene.from_file("deep.gltf")
queue = deque([(scene.root_node, 0)])
while queue:
node, depth = queue.popleft()
print(" " * depth + node.name)
for child in node.child_nodes:
queue.append((child, depth + 1))遍历时可以修改树结构吗??
不要从中添加或删除节点 child_nodes 在遍历它时。先在第一遍收集要修改的节点,然后在第二遍应用更改。.
如何按名称查找特定节点??
使用 node.get_child(name) 通过名称查找直接子节点。进行深度搜索时,使用名称过滤器遍历树::
def find_by_name(root, name):
if root.name == name:
return root
for child in root.child_nodes:
result = find_by_name(child, name)
if result:
return result
return None
target = find_by_name(scene.root_node, "Wheel_FL")是否 node.entity 总是返回 Mesh 吗??
不。节点可以容纳任何实体类型:: Mesh, Camera, Light,,或自定义实体。始终使用 isinstance(node.entity, Mesh) 在使用特定于网格的属性之前进行检查。.
如何获取节点的世界空间位置??
读取 node.global_transform.translation. 这是在世界空间中评估后的位置信息,考虑了所有祖先的变换。它是只读的;修改 node.transform.translation 以重新定位节点。.
我可以在不编写遍历的情况下统计场景中的总多边形数吗??
不能直接通过 API;没有 scene.total_polygon_count 属性。使用 collect_meshes 并求和 mesh.polygon_count 在结果上,如步骤 6 所示。.