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

Меня зовут Алекс, я программист в Handcrafted Games, где работаю над нашей дебютной игрой. В течение следующих месяцев я буду делиться различными визуальными приемами, которые мы сочли полезными, и объяснять, как применять их для себя.

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

Лично мне очень приятно открывать для себя эти методы, поэтому я постараюсь провести вас через творческий процесс, вместо того, чтобы просто дать вам код для копирования и вставки, и, надеюсь, вы сможете разделить часть этого волнения со мной.

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

Мне известна задача поместить фрагмент текста в рамку. Я использовал много систем с графическим интерфейсом пользователя (и написал некоторые из них сам), где подобные вещи происходят постоянно, поэтому сначала это казалось упражнением в базовой геометрии. Перенос слов может быть сложной задачей, но в этом нет ничего страшного. После завершения первого прототипа я понял, почему использование изображений фиксированного размера не поможет.

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

Фу. Не достаточно для нашей цели. Я решил вернуться и посмотреть еще несколько комиксов и, надеюсь, понять, как действовать дальше.

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

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

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

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

Каким-то образом этот многоугольник должен стать гладким овалом, и оказывается, что нам нужно сделать два относительно простых преобразования, чтобы этого добиться. Во-первых, давайте соединим эти вершины на ребрах, чтобы получить выпуклый многоугольник.

Полученный многоугольник (зеленый) - это выпуклая оболочка множества точек, составляющих нашу форму текста. Есть много алгоритмов, которые могут сделать это за нас, и для нашего небольшого набора точек не имеет большого значения, какой из них мы выберем. Поиск в Google по запросу Алгоритм выпуклой оболочки приводит к множеству реализаций и вариантов, для этого подойдет любой правильный.

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

Это уже намного лучше, конечно, не так уж далеко от желаемой формы, нужно только сгладить. Мы выполним итеративный процесс подразделения, в котором мы добавляем новые вершины и смещаем старые к центрам их новых соседей (это намного проще, чем кажется). Вот иллюстрация, которая лучше объясняет эту технику:

Для каждого существующего ребра (синий) мы добавляем новую вершину (красный) в его средней точке. Затем мы смещаем старые вершины (зеленые) к медиане их соответствующих новых соседей (красные).

На изображении выше показан результат трехкратного последовательного применения процесса подразделения. Мы начинаем видеть убывающую отдачу после трех итераций, и именно столько мы решили использовать в нашей игре. Желаемая форма получена, но нам все еще не хватает важной детали.

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

Мы будем использовать шейдер, реализующий оператор Собеля, который вычисляет очертания нашего многоугольника, исследуя его пиксели, и мы будем использовать эту информацию, чтобы закрасить наши контуры черным цветом. Вот пример реализации оператора Собеля в GLSL.

float sobel(in sampler2D texture, in vec2 step, in vec2 uv) {
  float tl = texture(texture, uv + vec2(-step.x, step.y)).b;
  float left = texture(texture, uv + vec2(-step.x, 0.0)).b;
  float bl = texture(texture, uv + vec2(-step.x, -step.y)).b;
  float top = texture(texture, uv + vec2(0.0, step.y)).b;
  float bottom = texture(texture, uv + vec2(0.0, -step.y)).b;
  float tr = texture(texture, uv + vec2(step.x, step.y)).b;
  float right = texture(texture, uv + vec2(step.x, 0)).b;
  float br = texture(texture, uv + vec2(step.x, -step.y)).b;
 
  float x = tl + 2.0 * left + bl - tr - 2.0 * right - br;
  float y = -tl - 2.0 * top - tr + bl + 2.0 * bottom + br;
  return clamp(sqrt(x*x + y*y), 0.0, 1.0);
}

После запуска этого на нашей сетке мы снова получаем этот результат.

Совсем неплохо. Все, что осталось, - это применить сглаживание, чтобы сгладить эти грубые края. Я решил использовать FXAA, но любой метод постобработки AA должен работать. В нашем движке мы рисуем пузыри речи во внеэкранном буфере, чтобы мы могли запускать FXAA для них отдельно от остальной сцены. Если вы создаете 3D-игру, то, скорее всего, у вас уже есть этап сглаживания, и вам не нужно делать это отдельно.

И вот он, красивый речевой пузырь, который может поместиться практически в любой кусок текста, который мы ему добавляем (с незначительной настройкой). На этом этапе вы можете подумать о способах добавления анимации или дополнительных эффектов, таких как тень.

Это была моя первая статья о визуальных приемах, которые мы используем в нашей игре. В ближайшие недели я буду делать больше подобных постов. Если вы нашли это полезным и интересным, загляните на наш сайт и подпишитесь на нас в Twitter, и мы сообщим вам, когда у нас появится что-то новое для вас.