Рисовать линии сложно

  • Эта публикация - перевод статьи. Ее автор - MATT DESLAURIERS. Оригинал доступен по ссылке ниже:

Рисование линий может не выглядеть как ракетостроение, но чертовски трудно преуспеть в OpenGL, особенно в WebGL. Здесь я исследую несколько различных техник для рендеринга 2D и 3D линий, и сопровождаю каждую из них небольшой демонстрацией.

Источник для демонстрации можно найти здесь: 

https://github.com/mattdesl/webgl-lines

Примитивы линий

WebGL включает поддержку линий gl.LINES, gl.LINE_STRIP и gl.LINE_LOOP. Звучит отлично, правда? На самом деле, нет. Вот только несколько проблем с этим:

  • Драйверы могут реализовывать рендеринг / фильтрацию немного по-другому, и вы можете не получить согласованный рендеринг на разных устройствах или в браузерах.
  • Максимальная ширина линии зависит от драйвера. Например, пользователи, использующие ANGLE, получат максимум 1,0, что довольно бесполезно. На моей новой машине Yosemite ширина линии достигает 10.
  • Отсутствие контроля над стилями соединения строк или заглушки
  • MSAA поддерживается не на всех устройствах, и большинство браузеров не поддерживают его для закадровых буферов. Вы можете получить неровные линии
  • Для пунктирных / пунктирных линий glLineStippleустарел и не существует в WebGL

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

Триангулированные линии

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

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

Более сложные сетки, возможно, должны испускать новые геометрии для концевых колпачков, конических соединений, растушевки и так далее. Обработка этих крайних случаев может быть довольно сложной, как вы можете видеть из исходного кода Vaser C / C ++.

Для сглаживания у вас есть несколько вариантов:

  • надеюсь, что MSAA поддерживается и вам никогда не понадобятся строки, отображаемые за пределами экрана
  • добавьте больше треугольников для заштрихованных краев обводки (как на этом изображении)
  • используйте текстурный поиск для градации альфы; очень легко, но плохо масштабируется
  • в фрагментном шейдере вычислите сглаживание на основе прогнозируемой шкалы обводки в пространстве экрана
  • сделать предварительный фильтрgl.LINES вторым проходом по краю штриха

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

Вышеприведенная демонстрация Triangles использует extrude-polyline, небольшой модуль незавершенного производства для построения триангулированной сетки из 2D-полилинии. В конце концов он направлен на поддержку круглых соединений / заглушек и правильного ограничения митры.

Расширение в Vertex Shader

Триангуляция может значительно усложнить ваш код, и сетка должна быть перестроена при изменении стиля обводки и соединения. Если вам просто нужны простые толстые линии в WebGL, это может быть немного излишним.

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

attribute vec2 position;
attribute vec2 normal;
attribute float miter;
uniform mat4 projection;

void main() {
    //push the point along its normal by half thickness
    vec2 p = position.xy + vec2(normal * thickness/2.0 * miter);
    gl_Position = projection * vec4(p, 0.0, 1.0);
}

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

Для реализации этого подхода в ThreeJS, включая штриховые черты, см. Three-line-2d.

Экранные проекционные линии экрана

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

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

В вершинном шейдере мы вычисляем наше соединение и экструзию в пространстве экрана, чтобы обеспечить постоянную толщину. Чтобы работать в пространстве экрана, нам нужно использовать призрачный однородный компонент W . Также известный как «деление перспективы». Это дает нам Нормализованные координаты устройства (NDC), которые лежат в диапазоне [-1, 1]. Затем мы корректируем соотношение сторон, прежде чем расширять наши линии. Мы также делаем то же самое для previousи nextпозиции на пути:

mat4 projViewModel = projection * view * model;

//into clip space
vec4 currentProjected = projViewModel * vec4(position, 1.0);

//into NDC space [-1 .. 1]
vec2 currentScreen = currentProjected.xy / currentProjected.w;

//correct for aspect ratio (screenWidth / screenHeight)
currentScreen.x *= aspect;

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

//normal of line (B - A)
vec2 dir = normalize(nextScreen - currentScreen);
vec2 normal = vec2(-dir.y, dir.x);

//extrude from center & correct aspect ratio
normal *= thickness/2.0;
normal.x /= aspect;

//offset by the direction of this point in the pair (-1 or 1)
vec4 offset = vec4(normal * direction, 0.0, 1.0);
gl_Position = currentProjected + offset;

Обратите внимание, что здесь нет попытки объединить два сегмента. Этот подход иногда предпочтительнее, чем митра, поскольку он не решает проблемы острых краев. В круге скручивания в вышеприведенном демо не используются никакие косые соединения.

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

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

Для реализации этого подхода в ThreeJS см. THREE.MeshLine от @thespite.

Другие подходы

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

  • Геометрия трафарета
    Классный трюк с использованием буфера трафарета для создания сложных полигонов без триангуляции. Тем не менее, он не получает MSAA вообще. [1] [2] [3]
  • Визуализация кривой Loop-Blinn
    Независимая от разрешения визуализация кубического сплайна, идеально подходит для глифов шрифтов.
  • Растрированные мазки
    Можно использовать для создания таких функций, как кисти Photoshop.
  • Однопроходный рендеринг
    каркаса Аналогично демонстрации проецируемых линий, но лучше подходит для 3D каркасов [1]
  • Геометрические шейдеры
    Это позволит проецируемым линиям испускать различные заглавные буквы / соединения. Однако геометрические шейдеры не поддерживаются в WebGL.
  • Аналитические поля расстояний
    Использование одного поля квадратов и расстояний в фрагментном шейдере также позволяет получить толстые сглаженные линии в 2D и 3D. Это не очень практично и может работать плохо, но может также включать некоторые интересные эффекты (например, размытие в движении).

Используемые модули

Демонстрации были составлены из десятков модулей с открытым исходным кодом на npmjs.org. Некоторые из связанных с путями модулей:

Дальнейшее чтение#