Traversal: обход дерева загруженной модели

Любая загруженная 3D-модель в Three.js представлена не единым объектом, а иерархическим деревом. Вершиной этого дерева является экземпляр THREE.Object3D, внутри которого могут находиться десятки или сотни дочерних элементов: группы (THREE.Group), меши (THREE.Mesh), кости (THREE.Bone), источники света и вспомогательные объекты.

Загрузчики форматов (GLTFLoader, FBXLoader, OBJLoader и другие) создают сложные графы объектов, где каждый элемент связан родительскими и дочерними связями. Такая структура называется сценографическим деревом (scene graph).

Пример типичной структуры модели:

Object3D (root)
 ├── Group
 │    ├── Mesh
 │    ├── Mesh
 │    └── Mesh
 ├── Group
 │    ├── Mesh
 │    └── Mesh
 └── Light

Работа с такими структурами требует обхода дерева — последовательного прохода по всем объектам внутри модели.

Основные задачи обхода:

  • поиск конкретных элементов по имени;
  • изменение материалов или геометрии;
  • включение и отключение теней;
  • применение анимаций;
  • настройка видимости;
  • оптимизация сцены.

Для этих задач Three.js предоставляет встроенные методы обхода.


Базовый метод traverse()

Класс THREE.Object3D содержит метод:

object.traverse(callback)

Метод выполняет рекурсивный обход всех дочерних элементов объекта и передает каждый найденный элемент в функцию-колбэк.

Пример:

model.traverse(function(child) {
    console.log(child);
});

Если model — это загруженная модель (например gltf.scene), то будут перечислены:

  • группы
  • меши
  • камеры
  • источники света
  • кости
  • вспомогательные элементы

Обход выполняется глубинным способом (Depth-First Traversal): сначала родитель, затем все его потомки.


Пример обхода загруженной модели

После загрузки GLTF-модели:

loader.load("model.glb", function(gltf) {

    const model = gltf.scene;

    model.traverse(function(object) {
        console.log(object.type);
    });

    scene.add(model);

});

В консоли можно увидеть последовательность:

Scene
Group
Mesh
Mesh
Mesh
Bone
SkinnedMesh

Это позволяет определить структуру объекта и понять, какие элементы доступны для дальнейших изменений.


Фильтрация по типу объекта

Чаще всего требуется работать только с определёнными типами элементов. Наиболее распространённый случай — поиск мешей (THREE.Mesh).

model.traverse(function(child) {

    if (child.isMesh) {
        console.log("Mesh найден:", child);
    }

});

Свойство isMesh является быстрым способом проверки типа.

Другие полезные проверки:

child.isGroup
child.isLight
child.isBone
child.isCamera
child.isSkinnedMesh

Такая фильтрация значительно упрощает обработку сложных моделей.


Изменение материалов всех мешей

Один из самых распространённых сценариев — массовая замена материала.

model.traverse(function(child) {

    if (child.isMesh) {

        child.material = new THREE.MeshStandardMaterial({
            color: 0x00ffcc
        });

    }

});

Обход дерева позволяет изменить внешний вид всей модели независимо от того, насколько глубоко внутри структуры находится меш.


Включение теней для всех объектов

При загрузке моделей тени обычно не активированы автоматически. Обход дерева позволяет быстро включить их для всех элементов.

model.traverse(function(child) {

    if (child.isMesh) {

        child.castShadow = true;
        child.receiveShadow = true;

    }

});

После этого модель начинает корректно взаимодействовать с освещением сцены.


Поиск объекта по имени

Во многих 3D-редакторах (Blender, Maya) элементам можно задавать имена. Эти имена сохраняются в модели и доступны в Three.js.

Обход используется для поиска конкретного элемента.

let target;

model.traverse(function(child) {

    if (child.name === "Door") {
        target = child;
    }

});

После этого можно выполнять действия:

target.rotation.y = Math.PI / 2;

Альтернативный способ — встроенный метод:

model.getObjectByName("Door");

Однако traverse() даёт больше гибкости при сложных условиях поиска.


Обработка материалов и текстур

Иногда требуется изменить свойства материалов у всех объектов.

Пример изменения параметров:

model.traverse(function(child) {

    if (child.isMesh) {

        child.material.metalness = 0.8;
        child.material.roughness = 0.2;

    }

});

Другой пример — отключение фрустумного отсечения:

child.frustumCulled = false;

Такой подход полезен для динамических сцен или анимаций.


Работа с SkinnedMesh

Анимированные модели содержат объекты типа SkinnedMesh.

Поиск таких элементов:

model.traverse(function(child) {

    if (child.isSkinnedMesh) {

        console.log("Найден скелетный меш");

    }

});

После обнаружения можно получить доступ к:

  • child.skeleton
  • child.bindMatrix
  • child.geometry

Это используется при работе с анимацией персонажей.


Метод traverseVisible()

Помимо обычного обхода существует метод:

object.traverseVisible(callback)

Он работает аналогично traverse(), но игнорирует невидимые объекты.

Пример:

model.traverseVisible(function(child) {
    console.log(child.name);
});

Если у объекта установлено:

object.visible = false

он не будет участвовать в обходе.

Это полезно для оптимизации при больших сценах.


Метод traverseAncestors()

Метод используется для движения вверх по иерархии, к родительским элементам.

object.traverseAncestors(callback)

Пример:

mesh.traverseAncestors(function(parent) {
    console.log(parent.name);
});

Последовательно будут выведены:

Group
Scene

Метод удобен для:

  • поиска родительской группы
  • определения контекста объекта
  • работы с трансформациями

Рекурсивная природа обхода

Метод traverse() реализован рекурсивно. Внутренний принцип работы можно представить так:

function traverse(object, callback) {

    callback(object);

    for (let child of object.children) {
        traverse(child, callback);
    }

}

Каждый объект содержит массив:

object.children

в котором находятся все дочерние элементы.

Такая архитектура делает обход дерева естественным и эффективным.


Оптимизация обхода больших моделей

Модели из CAD-систем или сложные сцены могут содержать тысячи объектов. Частый обход дерева может создавать нагрузку.

Способы оптимизации:

1. Ограничение типа объектов

if (!child.isMesh) return;

2. Кэширование результатов

Вместо повторного обхода можно сохранить найденные объекты:

const meshes = [];

model.traverse(function(child) {
    if (child.isMesh) meshes.push(child);
});

Дальнейшая работа ведётся напрямую с массивом meshes.

3. Однократная обработка

Обход дерева выполняется один раз после загрузки модели.


Практический пример полной обработки модели

Типичная последовательность действий после загрузки GLTF:

loader.load("car.glb", function(gltf) {

    const model = gltf.scene;

    model.traverse(function(child) {

        if (child.isMesh) {

            child.castShadow = true;
            child.receiveShadow = true;

            child.material.envMapIntensity = 1.2;

        }

    });

    scene.add(model);

});

В этом примере:

  • найден каждый меш;
  • включены тени;
  • настроена интенсивность отражений.

Визуальный анализ структуры модели

Обход дерева часто используется для диагностики структуры сцены.

model.traverse(function(child) {

    console.log(
        child.type,
        child.name,
        child.children.length
    );

});

Это позволяет понять:

  • какие элементы присутствуют;
  • сколько уровней вложенности;
  • какие части можно анимировать.

Использование обхода при интерактивности

При взаимодействии с пользователем (например, кликах мыши) необходимо определить, какой объект был выбран.

После получения объекта из Raycaster можно выполнить обход его родителей:

intersected.object.traverseAncestors(function(parent) {

    if (parent.name === "Car") {
        console.log("Выбран автомобиль");
    }

});

Таким образом определяется принадлежность объекта к более крупной структуре.


Обход и управление группами

Модели часто содержат логические группы:

Car
 ├── Body
 ├── Wheels
 └── Glass

Обход дерева позволяет выполнять операции над целыми категориями объектов.

model.traverse(function(child) {

    if (child.name.includes("Wheel")) {
        child.rotation.x += 0.1;
    }

});

Это удобно для синхронных анимаций.


Отладка структуры сцены

В процессе разработки полезно визуализировать дерево объектов.

Простейший способ:

function printTree(object, level = 0) {

    console.log(
        "-".repeat(level),
        object.name || object.type
    );

    object.children.forEach(child => {
        printTree(child, level + 1);
    });

}

Вызов:

printTree(model);

выводит иерархию всей модели.


Роль обхода в архитектуре Three.js

Traversal является фундаментальной частью работы со сценографическим деревом. Через обход дерева выполняются:

  • изменение параметров объектов;
  • настройка освещения и материалов;
  • поиск элементов;
  • обработка анимаций;
  • оптимизация сцены.

Методы traverse, traverseVisible и traverseAncestors предоставляют полный набор инструментов для перемещения по иерархии объектов любой сложности. В сочетании с типами Object3D, Mesh, Group и SkinnedMesh они формируют основу управления структурой загруженных 3D-моделей в Three.js.