Текст в 3D: как создать цифровую архитектуру на Three.js
Создаём 3D-конструкцию из имени на Three.js. Разбираем контуры букв, отверстия, морфинг и каркас. Настраиваем глубину, скручивание и оформление в собственной мастерской с примерами кода.

Имя можно показать обычным заголовком. А можно превратить очертания букв в набор пространственных сечений, соединить их рёбрами и получить конструкцию, которую хочется рассмотреть со всех сторон. Такая работа находится на пересечении типографики, процедурной графики и интерфейса: текст становится исходными данными для формы.
Ниже разберём принцип и попробуем собственную интерактивную версию на Three.js. Посмотрим, откуда берутся точки, зачем контурам одинаковая структура, почему отверстия букв нельзя игнорировать и как собрать каркас без тысяч отдельных объектов. Короткие примеры показывают отдельные этапы; готовая мастерская соединяет их в один опыт.
Текст даёт исходные очертания. Внешние пути и отверстия приводятся к одинаковому числу точек, затем связанные сечения получают глубину и деформацию.
Увеличить изображение ⤢Идея: текст задаёт форму, а не только подпись
Поводом для эксперимента стала публикация Codrops о проекте BL/S® для Three.js Conference. В ней описан переход между буквами с фиксированными 120 точками: пользовательские символы рисуются на скрытом canvas, превращаются в маску, затем обводятся и сглаживаются. Внутренние отверстия тоже учитываются. Сечения движутся по кривой, меняют форму и скручиваются; выбранные рёбра и диагонали образуют лёгкую пространственную конструкцию. Металлический вид создаётся собственным шейдером и подготовленной картой отражений, без трассировки лучей.
Мы развиваем эту идею в отдельном учебном опыте: со своими настройками, оформлением, геометрией и объяснениями. Для мастерской выбрали собственный алгоритм обработки маски и перехода от округлых петель к контурам текста. Ниже последовательно разберём его форму, управление и ограничения.
Где пригодится: обложка портфолио, интерактивный постер, заставка события, небольшой эксперимент с логотипом. Для меню, цены или длинного текста обычная читаемая типографика остаётся удобнее. Конструкция работает как дополнительный визуальный слой; подпись и смысл должны оставаться доступны вне canvas.
Мастерская: исследуйте форму по одному параметру
Начните с короткого имени или слова — до 12 символов. Наша мастерская получает очертания текста из canvas и показывает собственный переход от округлых петель к буквенным контурам. Сначала рассмотрите исходную форму без сильной деформации, затем меняйте один параметр за раз. Так легче увидеть, что отвечает за глубину, а что — за плотность рёбер или скручивание.
Соберите свою 3D-конструкцию
Введите короткое имя, выберите оформление и рассмотрите связанные сечения букв. Текст и настройки обрабатываются в вашем браузере.
Открыть мастерскую отдельно ↗Отдельная вкладка даёт больше места для управления ракурсом и сохранения изображения.
Для первого знакомства полезен такой маршрут:
- Сравните буквы. Выберите простой символ, затем букву с внутренней областью, например O или B. Ищите внешний контур и отверстия.
- Добавьте глубину. Плоское сечение превращается в пространственный объект. Посмотрите сбоку, как соединяются его уровни.
- Измените деформацию. Небольшое скручивание сохраняет узнаваемость, сильное подчёркивает абстрактную форму.
- Сравните плотность и виды. Редкий каркас легче прочитать; частый может выглядеть как почти сплошная поверхность. Сопоставьте каркас, контуры и объёмное оформление.
- Остановите движение. На неподвижном объекте проще оценить силуэт, пересечения и мелкие детали.
Если после изменения формы результат стал менее понятным, вернитесь к простому варианту. Здесь важен не максимум всех ползунков, а удачное сочетание. Сравнивайте при одинаковом ракурсе: поворот камеры сам по себе способен сильно изменить впечатление.
Контуры: какие данные нужны от каждой буквы
У символа есть не только ширина и высота. Например, O содержит внешний и внутренний замкнутые пути, а B — внешний путь и два отверстия. В Three.js объект Shape хранит внешний путь, а shape.holes — внутренние. Если выбрать только внешнюю границу, часть характерных деталей исчезнет.
Получить такие данные можно разными способами. В нашей мастерской растровый путь начинается с рисунка текста на canvas системным шрифтом. Далее находятся границы маски: внешний контур, отверстия и отдельные острова. Например, точка над буквой может быть самостоятельным островом. Границы сглаживаются и приводятся к удобному числу точек.
Точность зависит от установленного шрифта и разрешения маски: тонкий штрих или маленькое отверстие способны потеряться на слишком грубом рисунке. Наличие кириллицы в нашем примере не означает поддержку всех языков и всех сложных сочетаний символов. Векторная альтернатива — Font.generateShapes() с отдельным файлом шрифта; его набор глифов и лицензию нужно проверять самостоятельно.
Перед соединением формы приводят к удобному масштабу и центру. Затем вдоль каждого контура выбирают одинаковое количество точек. Лучше учитывать длину пути, чтобы плотность была примерно равномерной: много соседних точек на одном углу и длинный пустой участок на другом осложняют переход.
В нашей модели каждый отдельный замкнутый путь получает 120 точек. Отверстие и отдельный остров — самостоятельные пути, поэтому это не 120 точек суммарно на всё слово. Для изображения может выбираться более редкий набор, чтобы не превышать бюджет сегментов.
В исходниках мастерской эта последовательность разделена на функции: traceMask() находит петли, smoothClosed() сглаживает их, resampleClosed() выбирает точки, а prepareContours() собирает данные для построения. Такая организация позволяет проверить контур до запуска 3D-отображения.
Для каждой петли модель хранит 120 равномерных точек. Показанные на схеме точки условные; в 3D могут отображаться более редкие рёбра ради ограничения нагрузки.
- Пустая область внутри буквы не исчезает: у неё собственная петля.
- При переходе между буквами важно сопоставить обход и начальную точку.
- Растровая маска и сглаживание дают художественную форму, а не инженерный чертёж.
Вот самостоятельный пример одного замкнутого пути. Начальная точка не дублируется в конце массива; замыкание ребра выполняется позднее:
import * as THREE from 'three';
function sampleClosedPath(path, count = 120) {
if (!Number.isInteger(count) || count < 3) {
throw new Error('Нужно минимум 3 точки');
}
if (path.getLength() === 0) throw new Error('Путь пуст');
return Array.from({ length: count }, (_, i) =>
path.getPointAt(i / count).clone()
);
}
const square = new THREE.Shape();
square.moveTo(-1, -1);
square.lineTo(1, -1);
square.lineTo(1, 1);
square.lineTo(-1, 1);
square.closePath();
const contour = sampleClosedPath(square);
Почему одинакового числа мало: нужно также согласовать направление обхода и начальный индекс. Иначе точка верхнего края одной буквы может соединиться с нижней точкой другой, создав нежелательное закручивание. Для отверстий требуется отдельное правило: количество внутренних путей у разных символов не совпадает. Простое соединение «первое с первым» не решает такую задачу для произвольного текста.
Число 120 здесь — удобный пример, а не требование Three.js. Малое количество огрубляет кривые, большое увеличивает работу и число рёбер. Проверяйте контуры отдельно до добавления эффектов: так проще отличить ошибку формы от особенности освещения.
Переход: как одно сечение превращается в другое
Представьте несколько поперечных рамок вдоль глубины объекта. На первой — контур A, на последней — контур B. Между ними вычисляются промежуточные рамки: каждая точка постепенно меняет координаты от своего положения в A к соответствующему положению в B.
В нашей мастерской A — подготовленная округлая петля, B — один из найденных контуров текста. Каждый путь обрабатывается отдельно. Это позволяет показать принцип перехода и сохранить отверстия, не заявляя универсальный морфинг любой буквы в любую другую с автоматическим появлением и исчезновением отверстий.
В модели мастерской framePoint() вычисляет пространственную координату выбранной точки и сечения. Параметр фазы управляет переходом формы, глубина раздвигает уровни, а скручивание меняет ориентацию. Короткая функция makeRing ниже объясняет ту же базовую операцию на прямом участке; это отдельный учебный фрагмент, а не имя API мастерской.
В простом случае используем линейную интерполяцию: p = a × (1 − u) + b × u, где u проходит от 0 до 1. Скручивание добавляет поворот рамки, сужение уменьшает её масштаб в середине. Эти операции изменяют разные свойства, поэтому удобно управлять ими отдельно.
Следующий пример создаёт прямой участок между двумя уже согласованными контурами. Плавный изгиб всей конструкции и обработка нескольких отверстий остаются отдельными задачами:
function makeRing(a, b, u, {
depth = 4, twist = 0.7, pinch = 0.2
} = {}) {
if (a.length !== b.length) throw new Error('Размеры не совпадают');
const t = THREE.MathUtils.clamp(u, 0, 1);
const blend = t * t * (3 - 2 * t);
const scale = 1 - THREE.MathUtils.clamp(pinch, 0, 0.8)
* 4 * t * (1 - t);
const angle = Math.sin(2 * Math.PI * t) * twist;
const cos = Math.cos(angle), sin = Math.sin(angle);
return a.map((point, i) => {
const x = THREE.MathUtils.lerp(point.x, b[i].x, blend) * scale;
const y = THREE.MathUtils.lerp(point.y, b[i].y, blend) * scale;
return new THREE.Vector3(
x * cos - y * sin, x * sin + y * cos, t * depth
);
});
}
При u = 0 получится первое сечение, при u = 1 — последнее. В этой формуле поворот и сужение на краях исчезают, а между краями появляется заданная деформация. Это наш компактный пример; он не воспроизводит фрагмент кода из чужой публикации.
Для движения рамок по кривой недостаточно поменять только координату z. В каждой позиции понадобится ориентировать плоскость сечения по направлению пути. Three.js предоставляет кривые, касательные и методы построения локальных рамок, но согласованная ориентация на резких поворотах всё равно требует проверки. Начинайте с прямого участка и добавляйте сложность постепенно.
Каркас: соединяем уровни рёбрами
На этом этапе решаем, какие соединения показывать. Поперечные рёбра обводят каждую рамку. Продольные соединяют точки соседних рамок. Диагонали добавляют рисунок связей и помогают по-другому прочитать пространственную форму.
Результат нашей мастерской. Рёбра связывают соседние уровни; редкая плотность помогает рассмотреть структуру и отверстия.
Увеличить изображение ⤢Для тонких независимых рёбер подходит LineSegments: каждая пара вершин образует отдельный отрезок. Если собрать координаты в один BufferGeometry, не понадобится отдельный объект на каждую линию. Короткий пример ниже соединяет равные по размеру рамки:
function createFrame(rings, color = '#8fd8d0') {
const values = [];
const addEdge = (a, b) => values.push(a.x, a.y, a.z, b.x, b.y, b.z);
const count = rings[0]?.length ?? 0;
if (count < 3 || rings.some(ring => ring.length !== count)) {
throw new Error('Нужны рамки с одинаковым числом точек');
}
rings.forEach((ring, level) => {
ring.forEach((point, i) => {
addEdge(point, ring[(i + 1) % count]);
if (level > 0) addEdge(rings[level - 1][i], point);
});
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(values, 3));
const material = new THREE.LineBasicMaterial({ color });
return new THREE.LineSegments(geometry, material);
}
// a и b — согласованные массивы Vector2.
// const rings = Array.from({ length: 17 }, (_, i) => makeRing(a, b, i / 16));
// scene.add(createFrame(rings));
Плотность контролируется двумя независимыми числами: количеством точек на контуре и количеством уровней вдоль конструкции. Их одновременное увеличение быстро умножает число соединений. Для выбора ракурса полезен редкий каркас, для детализации — более плотный.
В мастерской действует бюджет до 5000 отображаемых сегментов. Для длинного текста плотность и число сечений могут автоматически уменьшаться, чтобы объект оставался удобным для просмотра. Модель контура сохраняет свои 120 точек, а изображение использует подмножество.
Функция planGeometry() подбирает число отображаемых точек и сечений по плотности и виду, а segmentCount() помогает проверить бюджет. Самостоятельный createFrame в примере выше показывает сборку линий; реальный редактор дополнительно применяет план и выбранный стиль.
Особенность тонких линий: linewidth у LineBasicMaterial не увеличивает толщину WebGL-линий. Если нужны широкие рёбра или настоящий объёмный металл, потребуется другой способ геометрии либо соответствующий addon. Важно выбрать его осознанно, а не ожидать, что любой материал превратит отрезок в цилиндр.
Оформление: цвет, отражения и читаемый силуэт
Начните с однотонного каркаса на контрастном фоне. Такой режим хорошо показывает форму и ошибки соединения. Затем добавляйте акцент: например, более светлые передние рёбра, тёплую палитру или осторожную имитацию блика. Перегруженные эффекты способны скрыть именно те детали, ради которых создавался объект.
Результат нашей мастерской с объёмными рёбрами, освещением и металлическим материалом. Это обычный рендеринг Three.js, без трассировки лучей.
Увеличить изображение ⤢В обычных материалах для объёмных поверхностей, например MeshStandardMaterial, металлический характер зависит от metalness, roughness и окружения. Карта окружения задаёт то, что поверхность отражает. Но она не является трассировкой лучей и не гарантирует физически точные отражения всех соседних объектов.
В режиме «3D-металл» нашего примера рёбра действительно объёмные: они собираются из цилиндров через InstancedMesh, используют MeshStandardMaterial и подготовленное локальное окружение RoomEnvironment / PMREM. Так множество одинаковых деталей можно отрисовывать как экземпляры общей геометрии. Режимы каркаса и контуров используют тонкие LineSegments.
Линейный каркас и поверхность — разные модели изображения. У LineBasicMaterial нет привычного металлического освещения поверхности. Для эффектного внешнего вида можно выбрать собственный шейдер, расширенную линейную геометрию или объёмные рёбра. Это уже следующий слой системы, который не стоит смешивать с проверкой исходных контуров.
Отверстия в нашем опыте остаются отдельными петлями, без заливки. Сильное скручивание способно создать пересечения. Если они мешают читаемости, уменьшите скручивание, сужение или плотность и снова рассмотрите форму сбоку.
Как сравнивать варианты: сохраните одинаковый ракурс, временно остановите вращение и меняйте только цвет или оформление. Оцените внешний силуэт, внутренние отверстия и контраст на маленьком экране. Если с красивым эффектом буква стала непонятной, уменьшите эффект или оставьте рядом обычную подпись.
Анимация и мобильный экран: движение без лишней нагрузки
Анимацию удобно разделить на два процесса. Перестроение геометрии происходит при изменении текста и формы. Поворот готового объекта или камеры происходит во время просмотра. Необязательно вычислять все контуры заново в каждом кадре, если меняется только угол.
У canvas есть размер на странице и внутреннее разрешение. Слишком маленькое внутреннее изображение выглядит размытым; слишком большое увеличивает нагрузку на GPU. На экранах с высоким DPR количество обрабатываемых пикселей растёт особенно быстро. Разумный предел нужно выбирать по качеству и поведению на реальных устройствах.
В своём проекте размер renderer и пропорции камеры можно обновлять так. Фрагмент предполагает уже созданные renderer, scene и перспективную camera:
function resizeScene() {
const canvas = renderer.domElement;
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
Для неподвижной сцены перерисовка нужна после изменений и смены размера, а не обязательно непрерывно. При включённом движении обеспечьте паузу и учитывайте системное предпочтение уменьшенного движения. Когда пример находится вне экрана или вкладка скрыта, непрерывная работа обычно не нужна.
Проверьте не только частоту кадров: кнопки должны оставаться доступны, поворот не должен мешать прокрутке страницы, а параметры — помещаться на ширине 320–390 px. Если WebGL 2 недоступен, вместо пустого чёрного прямоугольника нужен понятный текст и запасной способ посмотреть пример. Современный WebGLRenderer Three.js требует WebGL 2; возможность запуска зависит от браузера, настроек и устройства.
Как перенести опыт в собственный проект
Приведённые фрагменты рассчитаны на модульный проект с подключённым Three.js. Они объясняют ключевые действия, а не заменяют сцену, камеру, обработчики формы и весь загрузчик ресурсов. Для встраивания начните с отдельного небольшого модуля и понятных настроек.
- Определите входные данные. Ограничьте длину текста, выберите шрифт и перечень поддерживаемых символов. Не обещайте любой алфавит без проверки глифов.
- Проверьте форму без эффектов. Внешние и внутренние пути, масштаб, замыкание, обход и начальный индекс должны быть согласованы.
- Разделите расчёт и изображение. Контуры и координаты удобно проверять отдельно от камеры, шейдера и интерфейса.
- Сохраните управление. Сначала простые параметры, затем дополнительные; рядом — действие для возврата к понятному исходному состоянию.
- Освобождайте ресурсы. При замене геометрии вызывайте её
dispose(); материалы и текстуры освобождайте, когда они больше не используются. Удаление объекта из scene само по себе этого не делает. - Проверьте реальные устройства. Короткое и длинное слово, буквы с отверстиями, смену размера, паузу, повторное изменение и отсутствие WebGL.
Three.js распространяется под MIT; при передаче проекта сохраняйте его лицензию. Для шрифта, текстур и чужих примеров условия проверяются отдельно. Идея изучить процедурную форму не означает права копировать чужое оформление или объявить внешнюю демонстрацию своим исходником.
Начинать удобнее с одной понятной пары контуров и спокойного каркаса. Когда совпадают форма, управление и производительность, добавляйте кривой путь, сложное сопоставление отверстий и более выразительное оформление. Тогда каждый эффект имеет объяснимое место, а пример остаётся удобным для посетителя.
