如何在 Python 中遍历 3D 场景图

如何在 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 Noneisinstance(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 所示。.

 中文