场景图

场景图是 Aspose.3D FOSS for Python 的基础数据模型。每个 3D 文件,无论是从磁盘加载还是在内存中构建,均表示为以 Scene.root_node 为根的 Node 对象树。每个节点可以包含子节点以及一个或多个 Entity 对象(meshes、cameras、lights)。了解场景图可让您直接访问场景中每个对象的几何体、材质和空间变换。

安装和设置

从 PyPI 安装库:

pip install aspose-3d-foss

不需要本机扩展、编译器或额外的系统软件包。有关完整的安装说明,请参阅Installation Guide


概述:场景图概念

Aspose.3D FOSS 中的场景图遵循一个直接的包含层次结构:

Scene
└── root_node  (Node)
    ├── child_node_A  (Node)
    │   ├── entity: Mesh
    │   └── transform: translation, rotation, scale
    ├── child_node_B  (Node)
    │   └── child_node_C  (Node)
    │       └── entity: Mesh
    └── ...
ObjectRole
Scene顶层容器。包含 root_nodeasset_infoanimation_clipssub_scenes
Node具名树节点。拥有父节点、零个或多个子节点、零个或多个实体,以及本地 Transform
Entity附加到节点的几何体或场景对象。常见实体类型:MeshCameraLight
Transform节点的本地空间位置、旋转和缩放。世界空间结果可从 global_transform 读取。

步骤详解:以编程方式构建场景图

第 1 步:创建场景

一个新的 Scene 总是以空的 root_node 开始:

from aspose.threed import Scene

scene = Scene()
print(scene.root_node.name)   # "" (empty string — root node has no name by default)
print(len(scene.root_node.child_nodes))  # 0

Scene 是一切的入口点:加载文件、保存文件、创建动画剪辑以及访问节点树。


第2步:创建子节点

使用 create_child_node(name) 将命名节点添加到树中:

from aspose.threed import Scene

scene = Scene()

parent = scene.root_node.create_child_node("parent")
child  = parent.create_child_node("child")

print(parent.name)                          # "parent"
print(child.parent_node.name)               # "parent"
print(len(scene.root_node.child_nodes))     # 1

或者,创建一个独立的 Node 并显式附加它:

from aspose.threed import Scene, Node

scene = Scene()
node = Node("standalone")
scene.root_node.add_child_node(node)

两种方法产生相同的结果。create_child_node 对于内联构造更简洁。


第3步:创建网格实体并附加它

Mesh 用于存储顶点数据(control_points)和面拓扑(polygons)。创建一个,添加几何体,然后将其附加到节点上:

from aspose.threed import Scene, Node
from aspose.threed.entities import Mesh
from aspose.threed.utilities import Vector3, Vector4

scene = Scene()
parent = scene.root_node.create_child_node("parent")
child  = parent.create_child_node("child")

##Create a quad mesh (four vertices, one polygon)
# Note: control_points returns a copy of the internal list; append to
# _control_points directly to actually add vertices. This is a known
# library limitation — a public add_control_point() API is not yet available.
mesh = Mesh("cube")
mesh._control_points.append(Vector4(0, 0, 0, 1))
mesh._control_points.append(Vector4(1, 0, 0, 1))
mesh._control_points.append(Vector4(1, 1, 0, 1))
mesh._control_points.append(Vector4(0, 1, 0, 1))
mesh.create_polygon(0, 1, 2, 3)

child.add_entity(mesh)

print(f"Mesh name:      {mesh.name}")
print(f"Vertex count:   {len(mesh.control_points)}")
print(f"Polygon count:  {mesh.polygon_count}")

Vector4(x, y, z, w) 表示齐次坐标。使用 w=1 表示常规点位置。

create_polygon(*indices) 接受顶点索引并在多边形列表中注册一个面。传入三个索引表示三角形,传入四个索引表示四边形。


第4步:设置节点转换

每个节点都有一个 Transform,用于控制其在局部空间中的位置、方向和大小:

from aspose.threed.utilities import Vector3, Quaternion

##Translate the node 2 units along the X axis
child.transform.translation = Vector3(2.0, 0.0, 0.0)

##Scale the node to half its natural size
child.transform.scaling = Vector3(0.5, 0.5, 0.5)

##Rotate 45 degrees around the Y axis using Euler angles
child.transform.euler_angles = Vector3(0.0, 45.0, 0.0)

变换是累积的:子节点的世界空间位置是其自身变换与所有祖先变换的组合。从 node.global_transform 读取已评估的世界空间结果(不可变,只读)。


第5步:递归遍历场景图

通过递归遍历 node.child_nodes 来遍历整个树:

def traverse(node, depth=0):
    indent = "  " * depth
    entity_type = type(node.entity).__name__ if node.entity else "None"
    print(f"{indent}{node.name} [{entity_type}]")
    for child in node.child_nodes:
        traverse(child, depth + 1)

traverse(scene.root_node)

上述场景的示例输出(根节点名称为空字符串):

 [None]
  parent [None]
    child [Mesh]

对于每个节点包含多个实体的场景,请迭代 node.entities 而不是 node.entity

def traverse_full(node, depth=0):
    indent = "  " * depth
    entity_names = [type(e).__name__ for e in node.entities] or ["None"]
    print(f"{indent}{node.name} [{', '.join(entity_names)}]")
    for child in node.child_nodes:
        traverse_full(child, depth + 1)

traverse_full(scene.root_node)

第6步:保存场景

将文件路径传递给 scene.save()。格式将根据文件扩展名推断:

scene.save("scene.gltf")   # JSON glTF 2.0
scene.save("scene.glb")    # Binary GLB container
scene.save("scene.obj")    # Wavefront OBJ
scene.save("scene.stl")    # STL

对于特定格式的选项,请将 save-options 对象作为第二个参数传入:

from aspose.threed.formats import GltfSaveOptions

opts = GltfSaveOptions()
scene.save("scene.gltf", opts)

提示和最佳实践

  • 为每个节点命名。 为节点提供有意义的名称可以大大简化遍历调试,并确保名称在导出文件中得以保留。
  • 每个节点仅包含一个网格。 将实体与节点保持 1:1 对应可简化变换和碰撞查询。
  • 使用 create_child_node 而非手动附加。 它会自动设置父引用,出错概率更低。
  • 在构建层级后读取 global_transform 只有在所有祖先变换设置完毕后,世界空间的结果才稳定。
  • 遍历时不要修改树结构。 在迭代 child_nodes 时添加或删除子节点会导致不可预测的行为。先收集节点,再进行修改。
  • 控制点使用 Vector4,而不是 Vector3 对于普通顶点位置始终传入 w=1w=0 表示方向向量(而非点)。
  • mesh.control_points 返回副本。 control_points 属性返回 list(self._control_points) —— 向返回的列表追加元素并不会修改网格。以编程方式构建几何体时,请始终直接向 mesh._control_points 追加。这是已知的库限制;公开的变异 API 尚未提供。

常见问题

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'在访问实体属性之前,用 if node.entity is not None 进行保护。没有实体的节点具有 entity = None
即使设置了 translation,网格仍出现在原点transform.translation 会应用本地偏移。如果父节点本身具有非单位变换,世界位置可能会不同。检查 global_transform
scene.save() / 重新加载后子节点缺失某些格式(OBJ)会扁平化层次结构。使用 glTF 或 COLLADA 可保留完整的节点树。
polygon_countmesh.create_polygon(...) 后为 0确认传递给 create_polygon 的顶点索引在范围内(0len(control_points) - 1)。
Node.get_child(name) 返回 None名称区分大小写。确认创建时使用的确切名称字符串。
遍历以意外的顺序访问节点child_nodes 按插入顺序返回子节点(即 add_child_node / create_child_node 被调用的顺序)。
 中文