Перейти к содержимому
Поиск
Разработка

Текст в 3D: как создать цифровую архитектуру на Three.js

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

Александр Некрасов
Слово ОК превращено в золотистую 3D-конструкцию в мастерской Three.js

Имя можно показать обычным заголовком. А можно превратить очертания букв в набор пространственных сечений, соединить их рёбрами и получить конструкцию, которую хочется рассмотреть со всех сторон. Такая работа находится на пересечении типографики, процедурной графики и интерфейса: текст становится исходными данными для формы.

Ниже разберём принцип и попробуем собственную интерактивную версию на Three.js. Посмотрим, откуда берутся точки, зачем контурам одинаковая структура, почему отверстия букв нельзя игнорировать и как собрать каркас без тысяч отдельных объектов. Короткие примеры показывают отдельные этапы; готовая мастерская соединяет их в один опыт.

Схема: короткий текст превращается в замкнутые пути и связанный объёмный каркас.
СхемаБуква становится набором пространственных сечений

Текст даёт исходные очертания. Внешние пути и отверстия приводятся к одинаковому числу точек, затем связанные сечения получают глубину и деформацию.

Увеличить изображение ⤢

Идея: текст задаёт форму, а не только подпись

Поводом для эксперимента стала публикация Codrops о проекте BL/S® для Three.js Conference. В ней описан переход между буквами с фиксированными 120 точками: пользовательские символы рисуются на скрытом canvas, превращаются в маску, затем обводятся и сглаживаются. Внутренние отверстия тоже учитываются. Сечения движутся по кривой, меняют форму и скручиваются; выбранные рёбра и диагонали образуют лёгкую пространственную конструкцию. Металлический вид создаётся собственным шейдером и подготовленной картой отражений, без трассировки лучей.

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

Где пригодится: обложка портфолио, интерактивный постер, заставка события, небольшой эксперимент с логотипом. Для меню, цены или длинного текста обычная читаемая типографика остаётся удобнее. Конструкция работает как дополнительный визуальный слой; подпись и смысл должны оставаться доступны вне canvas.

Мастерская: исследуйте форму по одному параметру

Начните с короткого имени или слова — до 12 символов. Наша мастерская получает очертания текста из canvas и показывает собственный переход от округлых петель к буквенным контурам. Сначала рассмотрите исходную форму без сильной деформации, затем меняйте один параметр за раз. Так легче увидеть, что отвечает за глубину, а что — за плотность рёбер или скручивание.

Текст становится формой

Соберите свою 3D-конструкцию

Введите короткое имя, выберите оформление и рассмотрите связанные сечения букв. Текст и настройки обрабатываются в вашем браузере.

Открыть мастерскую отдельно ↗

Отдельная вкладка даёт больше места для управления ракурсом и сохранения изображения.

Продолжите в своём проекте

Исходники, закреплённые зависимости и инструкция локального запуска.

Скачать пример проекта

Для первого знакомства полезен такой маршрут:

  1. Сравните буквы. Выберите простой символ, затем букву с внутренней областью, например O или B. Ищите внешний контур и отверстия.
  2. Добавьте глубину. Плоское сечение превращается в пространственный объект. Посмотрите сбоку, как соединяются его уровни.
  3. Измените деформацию. Небольшое скручивание сохраняет узнаваемость, сильное подчёркивает абстрактную форму.
  4. Сравните плотность и виды. Редкий каркас легче прочитать; частый может выглядеть как почти сплошная поверхность. Сопоставьте каркас, контуры и объёмное оформление.
  5. Остановите движение. На неподвижном объекте проще оценить силуэт, пересечения и мелкие детали.

Если после изменения формы результат стал менее понятным, вернитесь к простому варианту. Здесь важен не максимум всех ползунков, а удачное сочетание. Сравнивайте при одинаковом ракурсе: поворот камеры сам по себе способен сильно изменить впечатление.

Контуры: какие данные нужны от каждой буквы

У символа есть не только ширина и высота. Например, O содержит внешний и внутренний замкнутые пути, а B — внешний путь и два отверстия. В Three.js объект Shape хранит внешний путь, а shape.holes — внутренние. Если выбрать только внешнюю границу, часть характерных деталей исчезнет.

Получить такие данные можно разными способами. В нашей мастерской растровый путь начинается с рисунка текста на canvas системным шрифтом. Далее находятся границы маски: внешний контур, отверстия и отдельные острова. Например, точка над буквой может быть самостоятельным островом. Границы сглаживаются и приводятся к удобному числу точек.

Точность зависит от установленного шрифта и разрешения маски: тонкий штрих или маленькое отверстие способны потеряться на слишком грубом рисунке. Наличие кириллицы в нашем примере не означает поддержку всех языков и всех сложных сочетаний символов. Векторная альтернатива — Font.generateShapes() с отдельным файлом шрифта; его набор глифов и лицензию нужно проверять самостоятельно.

Перед соединением формы приводят к удобному масштабу и центру. Затем вдоль каждого контура выбирают одинаковое количество точек. Лучше учитывать длину пути, чтобы плотность была примерно равномерной: много соседних точек на одном углу и длинный пустой участок на другом осложняют переход.

В нашей модели каждый отдельный замкнутый путь получает 120 точек. Отверстие и отдельный остров — самостоятельные пути, поэтому это не 120 точек суммарно на всё слово. Для изображения может выбираться более редкий набор, чтобы не превышать бюджет сегментов.

В исходниках мастерской эта последовательность разделена на функции: traceMask() находит петли, smoothClosed() сглаживает их, resampleClosed() выбирает точки, а prepareContours() собирает данные для построения. Такая организация позволяет проверить контур до запуска 3D-отображения.

Схема буквы О: внешний контур и внутреннее отверстие отдельно отмечены точками.
СхемаВнешняя граница и отверстие — отдельные замкнутые пути

Для каждой петли модель хранит 120 равномерных точек. Показанные на схеме точки условные; в 3D могут отображаться более редкие рёбра ради ограничения нагрузки.

  1. Пустая область внутри буквы не исчезает: у неё собственная петля.
  2. При переходе между буквами важно сопоставить обход и начальную точку.
  3. Растровая маска и сглаживание дают художественную форму, а не инженерный чертёж.
Увеличить изображение ⤢

Вот самостоятельный пример одного замкнутого пути. Начальная точка не дублируется в конце массива; замыкание ребра выполняется позднее:

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 предоставляет кривые, касательные и методы построения локальных рамок, но согласованная ориентация на резких поворотах всё равно требует проверки. Начинайте с прямого участка и добавляйте сложность постепенно.

Каркас: соединяем уровни рёбрами

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

Каркас: видно, как соединяются сечения — собственная 3D-визуализация текста на 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. Важно выбрать его осознанно, а не ожидать, что любой материал превратит отрезок в цилиндр.

Оформление: цвет, отражения и читаемый силуэт

Начните с однотонного каркаса на контрастном фоне. Такой режим хорошо показывает форму и ошибки соединения. Затем добавляйте акцент: например, более светлые передние рёбра, тёплую палитру или осторожную имитацию блика. Перегруженные эффекты способны скрыть именно те детали, ради которых создавался объект.

Металл: материал подчёркивает пространственную форму — собственная 3D-визуализация текста на Three.js.
Наш интерактивный примерМеталл: материал подчёркивает пространственную форму

Результат нашей мастерской с объёмными рёбрами, освещением и металлическим материалом. Это обычный рендеринг 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; при передаче проекта сохраняйте его лицензию. Для шрифта, текстур и чужих примеров условия проверяются отдельно. Идея изучить процедурную форму не означает права копировать чужое оформление или объявить внешнюю демонстрацию своим исходником.

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