Работа с графом сцены

Весь 3D‑контент в Aspose.3D FOSS для TypeScript находится внутри Scene объекта, организованного в виде дерева Node объектов. Понимание этой иерархии является основой для создания, загрузки и обработки любого 3D‑файла.

Установка

Установите пакет из npm перед запуском любого кода в этом руководстве:

npm install @aspose/3d

Убедитесь, что ваш tsconfig.json включает "module": "commonjs" и "moduleResolution": "node" для корректного разрешения импорта подпутей.

Концепции графа сцены

Граф сцены имеет три уровня:

УровеньКлассРоль
СценаSceneКонтейнер верхнего уровня. Содержит rootNode, animationClips, и assetInfo.
УзелNodeИменованный узел дерева. Может иметь дочерние узлы, сущность, трансформ и материалы.
СущностьMesh, Camera, Light, …Контент, прикреплённый к узлу. Узел может содержать не более одной сущности.

Цепочка наследования основных строительных блоков выглядит так:

A3DObject
  └─ SceneObject
       ├─ Node          (tree structure)
       └─ Entity
            └─ Geometry
                 └─ Mesh   (polygon geometry)

scene.rootNode создаётся автоматически. Вы не создаёте его вручную; вы создаёте дочерние узлы под ним.

Step 1: Create a Scene

import { Scene } from '@aspose/3d';

const scene = new Scene();
console.log(scene.rootNode.name); // '' (empty string — the root node is created with no name)

Новый Scene начинается с пустого корневого узла и без анимационных клипов. Вы создаёте контент, прикрепляя дочерние узлы.

Шаг 2: Добавить дочерние узлы

Используйте createChildNode() чтобы разрастить дерево. Метод возвращает новый Node, так что вы можете цепочкой вызывать дальнейшие вызовы с любого уровня:

import { Scene } from '@aspose/3d';

const scene = new Scene();
const parent = scene.rootNode.createChildNode('parent');
const child = parent.createChildNode('child');

console.log(scene.rootNode.childNodes.length); // 1 (parent)
console.log(parent.childNodes.length);         // 1 (child)

Имена узлов — произвольные строки. Имена не обязаны быть уникальными, но использование осмысленных имен упрощает отладку кода обхода.

Шаг 3: Создать Mesh и задать вершины

Mesh является основным классом геометрии. Добавьте позиции вершин, помещая Vector4 значения в mesh.controlPoints, затем вызовите createPolygon() для определения граней по индексу вершины:

import { Scene } from '@aspose/3d';
import { Mesh } from '@aspose/3d/entities';
import { Vector4 } from '@aspose/3d/utilities';

const scene = new Scene();
const child = scene.rootNode.createChildNode('parent').createChildNode('child');

const mesh = new Mesh('cube');
mesh.controlPoints.push(new Vector4(0, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 1, 0, 1));
mesh.controlPoints.push(new Vector4(0, 1, 0, 1));
mesh.createPolygon(0, 1, 2, 3); // quad face using all four vertices

child.entity = mesh;

console.log(mesh.controlPoints.length); // 4
console.log(mesh.polygonCount);         // 1

Vector4 использует однородные координаты: это w компонент является 1 для позиций и 0 для векторов направления.

Step 4: Set Node Transforms

У каждого узла есть transform свойство с translation, rotation, и scaling. Установить translation переместить узел относительно его родителя:

import { Scene } from '@aspose/3d';
import { Mesh } from '@aspose/3d/entities';
import { Vector4, Vector3 } from '@aspose/3d/utilities';

const scene = new Scene();
const parent = scene.rootNode.createChildNode('parent');
const child = parent.createChildNode('child');

const mesh = new Mesh('cube');
mesh.controlPoints.push(new Vector4(0, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 1, 0, 1));
mesh.controlPoints.push(new Vector4(0, 1, 0, 1));
mesh.createPolygon(0, 1, 2, 3);
child.entity = mesh;

child.transform.translation = new Vector3(2.0, 0.0, 0.0);

globalTransform предоставляет матрицу преобразования в мировом пространстве (только для чтения), вычисленную путем конкатенации всех преобразований предков.

Шаг 5: Обход дерева

Напишите рекурсивную функцию для обхода каждого узла. Проверьте node.entity и node.childNodes на каждом уровне:

function traverse(node: any, depth = 0): void {
    const indent = '  '.repeat(depth);
    const entityType = node.entity ? node.entity.constructor.name : 'none';
    console.log(`${indent}${node.name} [${entityType}]`);
    for (const child of node.childNodes) {
        traverse(child, depth + 1);
    }
}

traverse(scene.rootNode);

Для иерархии, созданной выше, вывод будет следующим:

 [none]
  parent [none]
    child [Mesh]

Имя корневого узла — пустая строка, потому что Scene создаёт его без аргумента имени.

Всегда проверяйте доступ к сущности на null перед приведением к конкретному типу. Не каждый узел содержит сущность.

Шаг 6: Сохранить в glTF или GLB

Используйте GltfSaveOptions для управления форматом вывода. Установите binaryMode = true для создания единого самодостаточного .glb файл; оставьте его false для JSON .gltf + .bin пара sidecar:

import { Scene } from '@aspose/3d';
import { Mesh } from '@aspose/3d/entities';
import { Vector4, Vector3 } from '@aspose/3d/utilities';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';

const scene = new Scene();
const parent = scene.rootNode.createChildNode('parent');
const child = parent.createChildNode('child');

const mesh = new Mesh('cube');
mesh.controlPoints.push(new Vector4(0, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 0, 0, 1));
mesh.controlPoints.push(new Vector4(1, 1, 0, 1));
mesh.controlPoints.push(new Vector4(0, 1, 0, 1));
mesh.createPolygon(0, 1, 2, 3);
child.entity = mesh;

child.transform.translation = new Vector3(2.0, 0.0, 0.0);

const saveOpts = new GltfSaveOptions();
saveOpts.binaryMode = true;                             // write a single .glb file
scene.save('scene.glb', GltfFormat.getInstance(), saveOpts);

console.log('Scene saved to scene.glb');

Передайте GltfFormat.getInstance() в качестве аргумента формата, чтобы библиотека использовала правильный кодировщик независимо от расширения файла.

Советы и лучшие практики

  • Использовать createChildNode() вместо построения Node непосредственно: createChildNode() автоматически устанавливает связь родитель‑дитя и регистрирует узел в дереве.
  • Проверьте node.entity перед доступом к свойствам сущности: многие узлы (групповые узлы, кости, локаторы) не содержат сущность. Всегда проверяйте на null или instanceof тест.
  • Установить translation на дочерних узлах, а не на вершинах меша: изменение transform.translation не разрушительно и совместимо с трансформами родителя.
  • Предпочитайте binaryMode = true для GLB: один .glb файл легче распространять, загружать в браузерах и импортировать в игровые движки, чем разделённый .gltf + .bin формат.
  • Перебирайте через for...of по childNodes: избегайте числовой индексации; используйте итерируемый объект напрямую для обеспечения совместимости в будущем.

Распространённые проблемы

СимптомВероятная причинаИсправление
child.entity = mesh не влияет на экспортСущность назначена на неверный уровень узлаНазначить entity на листовой узел, а не на групповой узел
node.entity всегда nullТолько проверка rootNode самоРекурсивно входить в node.childNodes; rootNode обычно не имеет сущности
Трансформация не отображается в просмотрщике GLBglobalTransform не обновленоglobalTransform вычисляется при сохранении; установить transform.translation перед вызовом scene.save()
GLB создает отдельный .bin sidecarbinaryMode по умолчанию falseУстановить saveOpts.binaryMode = true

См. также

 Русский