Собственная модель glTF и её загрузка
Оглавление серии «Форма» · Урок 4 из 10.
До этого урока лампа создавалась непосредственно в JavaScript из трёх коробок. Это удобно для знакомства со сценой, но геометрия реального товара обычно поставляется отдельным ресурсом. Теперь перенесём тот же предмет в собственный glTF и загрузим его через GLTFLoader. Ожидаемый результат сохранится: основание, стойка и плафон появятся в прежнем масштабе. Изменится граница ответственности: файл описывает предмет, а приложение отвечает за его загрузку и представление.
Используем авторский учебный образец, включённый в архив. Он не является скачанной моделью коммерческой лампы. Простая конструкция позволяет проследить структуру формата и не добавляет лицензионных вопросов к учебной задаче. В файле есть три узла, три материала и общая геометрия куба. Размеры каждого элемента заданы преобразованиями соответствующего узла. В следующих уроках имена и роли этих деталей будут связывать модель с элементами формы.
Что хранит glTF
glTF описывает сцену, узлы, геометрию, материалы и связи между ними. В нашем текстовом файле JSON геометрические данные расположены в встроенном бинарном буфере, закодированном как data URI. Такой вариант удобен для небольшого автономного примера: не требуется хранить рядом отдельный бинарный файл. У больших моделей обычно выбирают другую упаковку, в том числе GLB. Размер упаковки и удобство доставки следует рассматривать вместе с реальным содержимым модели.
Узел содержит ссылку на mesh и преобразования: перенос, вращение и масштаб. Mesh описывает набор геометрических примитивов, а примитив указывает на атрибуты вершин и материал. Буфер хранит последовательности чисел; accessor определяет, как читать нужный участок. Например, позиция вершины представлена тройкой чисел, а индекс указывает на уже заданную вершину. JSON сам по себе не рисует объект: загрузчик должен прочитать данные и создать соответствующие объекты библиотеки.
Учебный куб использует отдельные вершины для каждой грани. Это позволяет каждой поверхности иметь собственную нормаль и сохранять видимое ребро. Если разделить одну нормаль между гранями, освещение может выглядеть сглаженным, хотя геометрическая форма останется кубической. У процедурной BoxGeometry эти подробности были скрыты внутри библиотеки. Отдельный ресурс делает заметным, что положение вершины и направление её поверхности — разные данные для вывода изображения.
Договорённость о ролях деталей
В узлах записаны имена base, stem и shade. Дополнительно поле extras хранит роль детали. Загрузчик переносит такие дополнительные данные в userData объектов Three.js. Имя может измениться при экспорте модели или получить служебный суффикс, поэтому роль удобна как явная договорённость между автором модели и приложением. Однако произвольный файл не обязан иметь эти роли: перед подключением другого товара его структуру необходимо согласовать.
Название роли здесь не является проверкой безопасности и не даёт полномочий редактировать товар. Это локальный контракт данных. Приложение будет искать только известные роли и применять к ним ограниченные параметры. Не нужно исполнять скрипт из extras или интерпретировать любую строку как имя функции. Формат позволяет хранить пользовательские данные, но смысл и допустимые значения этих данных определяет наше приложение.
Полный модуль загрузки
Ниже полный main.js четвёртого состояния. Новый импорт получает GLTFLoader из того же закреплённого пакета. Вызов loadAsync возвращает Promise; он завершается после получения и разбора нужных ресурсов модели. Результат содержит сцену модели и другие данные, например анимации. Для нашей лампы используем loaded.scene. Добавлять весь объект результата как mesh не нужно: результат загрузки и объект сцены имеют разное назначение.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const host=document.querySelector('#viewport');
const onStatus=value=>document.querySelector('#status').textContent=value;
const onSelect=value=>{const selected=document.querySelector('#selected');if(selected)selected.textContent=`Выбрана деталь: ${value}`;};
const scene=new THREE.Scene();scene.background=new THREE.Color('#ece9f3');
const camera=new THREE.PerspectiveCamera(40,600/420,.1,30);camera.position.set(3,2.5,4);
camera.lookAt(0,.8,0);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(600,420,false);renderer.outputColorSpace=THREE.SRGBColorSpace;
host.append(renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff,0x55506a,2));
const sun=new THREE.DirectionalLight(0xffffff,3);sun.position.set(3,5,2);scene.add(sun);
let model=null,frame=0,closed=false;
function resize() {
const {width,height}=host.getBoundingClientRect();if(width<=0 || height<=0)return;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1,2));
renderer.setSize(width,height,false);camera.aspect=width/height;camera.updateProjectionMatrix();
}
const observer=new ResizeObserver(resize);observer.observe(host);resize();
const controls=new OrbitControls(camera,renderer.domElement);
controls.target.set(0,.8,0);controls.minDistance=2;controls.maxDistance=8;
controls.maxPolarAngle=Math.PI*.49;controls.enablePan=false;controls.enableDamping=true;controls.update();
function reset() {camera.position.set(3,2.5,4);controls.target.set(0,.8,0);controls.update();}
const ready=(async()=>{
const loaded=await new GLTFLoader().loadAsync('/assets/lamp.gltf');
model=loaded.scene;scene.add(model);
onStatus('Объёмное представление готово');
})();
function render(){if(closed)return;
controls.update();
renderer.render(scene,camera);frame=requestAnimationFrame(render);}
render();
ready.catch(error=>onStatus(`Модель не открыта: ${error.name}. Описание доступно в HTML.`));
document.querySelector('#reset').addEventListener('click',reset);
Путь /assets/lamp.gltf относится к локальному серверу Vite. Сам файл лежит в папке public/assets, которую Vite предоставляет как статический ресурс. Импорт модуля загрузчика и запрос модели — два отдельных действия. Библиотека может загрузиться успешно, а модель получить ответ 404 из-за неправильного пути. При самостоятельном разборе смотрите конкретный запрос ресурса, а не делайте вывод о поломке всей Three.js после одной отсутствующей модели.
Переменная model сначала равна null. Это честное состояние: объект ещё не готов. Камера, свет, контроллер и кадровый цикл могут существовать до завершения загрузки. В это время renderer покажет фон, а сообщение будет оставаться промежуточным. После успешного ожидания присваиваем готовую сцену модели и добавляем её в общую сцену. Сообщение о готовом представлении появляется на этой границе, а не сразу после начала сетевого запроса.
Ошибка не должна стирать описание
К Promise готовности подключён обработчик ошибки. Он меняет текст состояния и сообщает, что HTML-описание остаётся доступным. Canvas не содержит единственной копии сведений о товаре, поэтому отсутствие WebGL-ресурса не должно превращать страницу в пустой прямоугольник. Позднее параметры тоже останутся в обычной форме. Если модель недоступна, посетитель сможет читать свойства и выбирать комплектацию, даже когда объёмное представление временно отсутствует.
Ошибки загрузки могут происходить на разных уровнях: отсутствует JSON, не найден внешний буфер, не поддерживается расширение, структура ресурса повреждена. Наш автономный образец убирает часть зависимостей, но не превращает загрузчик в универсальную защиту от недоверенных моделей. Не следует принимать произвольный пользовательский glTF и считать его безопасным только потому, что формат стандартизован. В этой серии ресурс контролирует автор учебного проекта.
Масштаб после импорта
Размеры узлов сохраняют соглашение предыдущих уроков. Основание имеет высоту 0,08, стойка — 1,32, плафон располагается на высоте 1,5. Поэтому прежняя камера и цель управления подходят новой модели. Если подключить ресурс в сантиметрах, он может оказаться в сто раз крупнее ожидаемого. Это объясняет распространённый пустой кадр: объект не обязательно отсутствует, камера могла оказаться внутри него. Перед настройкой материалов сначала проверьте реальные габариты импортированной сцены.
Для самостоятельного наблюдения можно временно переименовать файл модели. Ожидается сообщение об ошибке при сохранённом описании, затем после возвращения пути — прежняя лампа. Другой полезный опыт — изменить только масштаб узла плафона в JSON и посмотреть, какая часть предмета должна измениться. Это упражнение отделяет преобразование узла от координат общего буфера. Учебные файлы при написании не исполнялись; такие действия предлагаются читателю для собственного исследования.
Структура ресурса описана в спецификации glTF 2.0, а загрузка — в GLTFLoader. Теперь модель имеет явные роли деталей и асинхронную границу готовности. В следующем уроке используем эти роли для настройки материалов, сохранив одну и ту же геометрию, свет и камеру при сравнении цветовых вариантов.
Исходники этого шага
Архив серии содержит полную папку lesson-04. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.