Перейти к содержанию

Сцена, камера и первый предмет

Оглавление серии «Форма» · Урок 1 из 10.

На странице товара фотография показывает один подготовленный ракурс. Конфигуратор решает другую задачу: связывает выбранные параметры с изображением предмета. Начнём с небольшого собственного объекта — настольной лампы «Форма». У неё есть основание, стойка и прямоугольный плафон. Простая геометрия позволит разобраться в устройстве сцены без поиска моделей и дополнительных загрузок. В конце урока браузер должен показать лампу на светлом фоне, а рядом останется обычное текстовое описание.

Предполагается, что вы уже умеете подключать модуль JavaScript к HTML и находить элементы через DOM. Знание специального редактора моделей не требуется. Все размеры пока условные; договоримся считать одну единицу сцены одним метром. Это соглашение внутри примера, а не автоматическое свойство Three.js. Число в координате само по себе не содержит единицу измерения, поэтому участники проекта должны договориться о масштабе заранее.

Три участника изображения

Сцена хранит предметы, источники света и их расположение. Камера определяет точку наблюдения и способ проекции. Renderer преобразует сцену, увиденную камерой, в изображение на canvas. Если создать только сцену и добавить в неё лампу, видимого результата ещё не будет. Если камера направлена в другую сторону, renderer может работать исправно, но показывать один фон. Различать эти обязанности полезно при любой диагностике пустого изображения.

Выберем перспективную камеру. Удалённые предметы в такой проекции выглядят меньше, а близкие — больше. Для карточки товара это знакомое представление, хотя в техническом чертеже иногда удобнее ортографическая проекция. Значение 40 задаёт вертикальный угол обзора в градусах. Второй аргумент — отношение ширины области к высоте. Последние два аргумента определяют ближнюю и дальнюю границы видимого пространства, а не размеры самого предмета.

В нашем случае лампа занимает примерно полтора метра по вертикали, а камера стоит на расстоянии нескольких условных метров. Поэтому диапазона от 0,1 до 30 достаточно для предмета. Очень широкий диапазон без причины ухудшает полезную точность глубины и осложняет понимание сцены. Здесь не требуется охватывать одновременно микроскопические детали и дальние здания: предмет один и его масштаб известен. Поставим камеру в точку (3, 2.5, 4) и направим её к середине лампы.

Полный модуль первого состояния

Для серии закреплены Three.js 0.180.0 и Vite 7.1.7. Их версии записаны в полном package.json архива, а Node.js выбран 22.14.0. Мы используем конкретный опубликованный профиль, а не автоматически обновляемые зависимости. Пакеты читатель устанавливает самостоятельно в папке первого состояния. Команда npm run dev запускает учебный сервер на loopback; при подготовке статьи она не выполнялась. Устанавливать Three.js отдельной глобальной командой не нужно.

Ниже полный main.js шага. HTML содержит #viewport и #status. CSS задаёт области фиксированную высоту и ограничивает общую ширину страницы. В этом первом состоянии сам renderer получает размеры 600 на 420. В следующем уроке согласуем размеры с реальной областью браузера. Сейчас фиксированная величина помогает увидеть, какие определения отвечают за изображение, прежде чем добавлять наблюдение за изменениями интерфейса.

import * as THREE from 'three';
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);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;
model=new THREE.Group();
for(const [name,size,position,color] of [
 ['base',[1,.08,1],[0,.04,0],'#272532'],['stem',[.09,1.32,.09],[0,.74,0],'#777780'],['shade',[.75,.35,.6],[0,1.5,0],'#8044ca']]) {
 const part=new THREE.Mesh(new THREE.BoxGeometry(...size),new THREE.MeshStandardMaterial({color,roughness:.5}));
 part.name=name;part.position.set(...position);model.add(part);
}
scene.add(model);onStatus('Объёмное представление готово');
const ready=Promise.resolve();
function render(){if(closed)return;
renderer.render(scene,camera);frame=requestAnimationFrame(render);}
render();

Group объединяет детали лампы. У каждой детали собственная геометрия, материал и позиция; у группы появляется возможность перемещать весь предмет целиком. Геометрия BoxGeometry задаёт форму поверхности. Материал определяет взаимодействие этой поверхности со светом. Mesh соединяет их. Именно mesh добавляется в сцену как видимый объект. Название детали пока нужно для чтения исходника, но позднее станет частью контракта выбора и настройки.

Основание имеет высоту 0,08 и стоит центром на высоте 0,04. Следовательно, его нижняя грань совпадает с нулём по вертикали. Стойка имеет высоту 1,32 и центр 0,74: её нижняя грань находится на верхней поверхности основания. Эти числа не выбраны случайно. Координата центра и полный размер определяют положение крайних точек. Если поставить основание центром в нуле, половина его высоты окажется ниже плоскости, которую считаем поверхностью стола.

Зачем нужны источники света

MeshStandardMaterial моделирует освещаемую поверхность. Он не обязан быть видимым в тёмной сцене без источников света. Добавим полусферический свет, который задаёт мягкую разницу между верхней и нижней стороной предмета, и направленный свет сбоку. Так станет проще различать грани. Настройка света помогает показать форму, но не подтверждает, что цвет на экране совпадает с физическим цветом будущего товара при любом освещении.

Выходное цветовое пространство renderer задаём явно как sRGB. Это распространённое пространство экранного изображения. Цвет материала, освещение и вывод работают совместно; одинаковый шестнадцатеричный цвет в CSS и освещаемом материале может выглядеть по-разному. Не стоит компенсировать это случайным изменением каждой поверхности. Позднее отдельно разберём материалы и палитру, сохранив одинаковые параметры света при сравнении вариантов одной лампы.

Функция render запрашивает следующий кадр через requestAnimationFrame. Такой цикл пока прост: состояние не изменяется, но изображение повторно выводится в каждом кадре. После добавления камеры с плавным затуханием это даст понятный механизм обновления. Для неподвижного предмета можно было бы рисовать только при изменении данных, однако тогда потребуется явно учитывать все причины изменения. К этому различию вернёмся при обсуждении ресурсов и жизненного цикла.

Как читать ожидаемый результат

После самостоятельной установки зависимостей и открытия адреса Vite ожидается прямоугольный плафон фиолетового цвета, тонкая стойка и тёмное основание. Камера смотрит немного сверху и сбоку. Сообщение под областью говорит о готовом объёмном представлении. Это ожидаемый результат разбора исходника; пример в процессе написания не запускался. Самостоятельно сравните расположение трёх деталей с их размерами, прежде чем менять материалы или положение камеры.

Если виден только фон, сначала проверьте направление камеры и наличие mesh в сцене. Если исчезают отдельные поверхности, рассмотрите свет и положение предмета относительно ближней границы камеры. Если модуль вообще не загружается, смотрите сообщение браузера и способ запуска: импорт пакета обрабатывает Vite, обычное открытие HTML через file:// не соответствует среде примера. Каждая из этих причин принадлежит своему уровню и требует своего исправления.

Текстовое описание лампы находится в HTML и доступно независимо от WebGL. Canvas не становится полноценным описанием товара только потому, что изображение выглядит аккуратно. Сведения о составе, размерах и параметрах должны оставаться читаемыми и без него. В следующих шагах добавим обычные элементы формы, а 3D будет отражать их состояние. Основные роли сцены, камеры и renderer описаны в официальной документации Three.js; закреплённый исходник серии соответствует релизу r180.

Исходники этого шага

Архив серии содержит полную папку lesson-01. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.