Graf sceny

scene graph jest podstawowym modelem danych Aspose.3D FOSS for Python. Każdy plik 3D, niezależnie od tego, czy został wczytany z dysku, czy utworzony w pamięci, jest reprezentowany jako drzewo obiektów Node z korzeniem w Scene.root_node. Każdy węzeł może zawierać węzły potomne oraz jeden lub więcej obiektów Entity (siatki, kamery, światła). Zrozumienie scene graph daje bezpośredni dostęp do geometrii, materiałów i transformacji przestrzennych każdego obiektu w scenie.

Instalacja i konfiguracja

Zainstaluj bibliotekę z PyPI:

pip install aspose-3d-foss

Nie są wymagane żadne natywne rozszerzenia, kompilatory ani dodatkowe pakiety systemowe. Pełne instrukcje instalacji znajdziesz w Przewodnik instalacji.


Przegląd: Koncepcje grafu sceny

Graf scen w Aspose.3D FOSS opiera się na prostej hierarchii zawartości:

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
SceneKontener najwyższego poziomu. Zawiera root_node, asset_info, animation_clips i sub_scenes.
NodeNazwany węzeł drzewa. Ma rodzica, zero lub więcej dzieci, zero lub więcej encji oraz lokalny Transform.
EntityGeometria lub obiekt sceny dołączony do węzła. Typowe typy encji: Mesh, Camera, Light.
TransformLokalna pozycja, rotacja i skalowanie w przestrzeni węzła. Wynik w przestrzeni światowej odczytywany jest z global_transform.

Krok po kroku: Budowanie grafu sceny programowo

Krok 1: Utwórz scenę

Nowy Scene zawsze zaczyna się od pustego 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 jest punktem wejścia dla wszystkiego: ładowanie plików, zapisywanie plików, tworzenie klipów animacji i dostęp do drzewa węzłów.


Krok 2: Utwórz węzły potomne

Użyj create_child_node(name), aby dodać nazwane węzły do drzewa:

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

Alternatywnie, utwórz samodzielny Node i dołącz go wyraźnie:

from aspose.threed import Scene, Node

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

Oba podejścia dają ten sam wynik. create_child_node jest bardziej zwięzły przy konstrukcji inline.


Krok 3: Utwórz encję siatki i dołącz ją

Mesh przechowuje dane wierzchołków (control_points) i topologię ścian (polygons). Utwórz go, dodaj geometrię, a następnie podłącz go do węzła:

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) reprezentuje współrzędną jednorodną. Użyj w=1 dla zwykłych położeń punktów.

create_polygon(*indices) akceptuje indeksy wierzchołków i rejestruje jedną ścianę w liście wielokątów. Przekaż trzy indeksy dla trójkąta, cztery dla czworokąta.


Krok 4: Ustaw transformacje węzłów

Każdy węzeł ma Transform, który kontroluje jego pozycję, orientację i rozmiar w przestrzeni lokalnej:

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)

Transformacje są kumulatywne: pozycja w przestrzeni świata węzła potomnego jest kompozycją jego własnej transformacji ze wszystkimi transformacjami przodków. Odczytaj oceniony wynik w przestrzeni świata z node.global_transform (niemutowalny, tylko do odczytu).


Krok 5: Przeglądaj graf sceny rekurencyjnie

Przejdź całe drzewo, rekurencyjnie przechodząc przez 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)

Przykładowe wyjście dla sceny zbudowanej powyżej (nazwa węzła głównego jest pustym ciągiem):

 [None]
  parent [None]
    child [Mesh]

Dla scen z wieloma encjami na węzeł, iteruj node.entities zamiast 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)

Krok 6: Zapisz scenę

Przekaż ścieżkę pliku do scene.save(). Format jest wywnioskowany z rozszerzenia pliku:

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

Aby przekazać opcje specyficzne dla formatu, przekaż obiekt save-options jako drugi argument:

from aspose.threed.formats import GltfSaveOptions

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

Wskazówki i najlepsze praktyki

  • Nazwij każdy węzeł. Nadawanie węzłom znaczących nazw znacznie ułatwia debugowanie przeglądania i zapewnia, że nazwy są zachowywane w wyeksportowanym pliku.
  • Jedna siatka na węzeł. Utrzymywanie encji 1:1 z węzłami upraszcza przekształcenia i zapytania kolizyjne.
  • Używaj create_child_node zamiast ręcznego dołączania. Ustawia referencję rodzica automatycznie i jest mniej podatny na błędy.
  • Odczytuj global_transform po zbudowaniu hierarchii. Wynik w przestrzeni świata jest stabilny dopiero po ustawieniu wszystkich transformacji przodków.
  • Nie modyfikuj drzewa podczas przeglądania. Dodawanie lub usuwanie węzłów potomnych podczas iteracji child_nodes spowoduje nieprzewidywalne zachowanie. Najpierw zbierz węzły, potem modyfikuj.
  • Punkty kontrolne używają Vector4, nie Vector3. Zawsze przekazuj w=1 dla zwykłych pozycji wierzchołków; w=0 reprezentuje wektor kierunku (nie punkt).
  • mesh.control_points zwraca kopię. Właściwość control_points zwraca list(self._control_points) — dodawanie elementów do zwróconej listy nie modyfikuje siatki. Zawsze dodawaj do mesh._control_points bezpośrednio podczas programowego budowania geometrii. To znane ograniczenie biblioteki; publiczne API mutacji nie istnieje jeszcze.

Częste problemy

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'Zabezpiecz za pomocą if node.entity is not None przed dostępem do właściwości encji. Węzeł bez encji ma entity = None.
Mesh appears at the origin despite setting translationtransform.translation stosuje lokalne przesunięcie. Jeśli węzeł nadrzędny ma transformację inną niż tożsamość, pozycja w świecie może się różnić. Sprawdź global_transform.
Child nodes missing after scene.save() / reloadNiektóre formaty (OBJ) spłaszczają hierarchię. Użyj glTF lub COLLADA, aby zachować pełne drzewo węzłów.
polygon_count is 0 after mesh.create_polygon(...)polygon_count wynosi 0 po mesh.create_polygon(...). Sprawdź, czy indeksy wierzchołków przekazywane do create_polygon mieszczą się w zakresie (0 do len(control_points) - 1).
Node.get_child(name) returns NoneNazwa jest rozróżniana pod względem wielkości liter. Potwierdź dokładny ciąg nazwy użyty w czasie tworzenia.
Traversal visits nodes in unexpected orderchild_nodes zwraca dzieci w kolejności wstawiania (kolejność, w której wywołano add_child_node / create_child_node).
 Polski