# \[Перевод] Рисовать линии - это сложно> Нарисовать линию - что может быть проще? Но в WebGL это та ещё головная боль. Разбираем, почему gl.LINES не работает как надо, и учимся рисовать красивые линии через триангуляцию и шейдеры.**Published:** 2017-07-29**Canonical:** <https://grishy.dev/ru/posts/drawing-lines-is-hard/>**Translations:** [Русский](https://grishy.dev/ru/posts/drawing-lines-is-hard.md)![\[Перевод\] Рисовать линии - это сложно](https://grishy.dev/_astro/cover.Bww_yU1m.avif)---Рисование линий может и не звучит как что-то очень сложное, но довольно сложно сделать в *OpenGL* и особенно в *WebGL*. Ниже я рассмотрю несколько различных техник рисования 2D и 3D линий и дополню каждый из них маленьким демо.Все исходники вы можете найти здесь: <https://github.com/mattdesl/webgl-lines>## Простые линии[Interactive demo](https://mattdesl.github.io/webgl-lines/native/index.html)WebGL есть поддержка линий с помощью `gl.LINES`, `gl.LINE_STRIP` и `gl.LINE_LOOP`. Звучит прекрасно, неправда ли? На самом все деле не все так хорошо, и вот несколько причин:* Драйверы могут по-разному рисовать/сглаживать линии, и вы не можете получить одинаково выглядящую картинку на всех девайсах или браузерах.
* Ещё максимальная ширина линии зависит от реализации. Например люди, использующие [ANGLE](https://chromium.googlesource.com/angle/angle) получат максимальную ширину линии равной *1.0*, что довольно бесполезно. На моём новом ноутбуке с Yosemite линии могут быть шириной до *\~10*.
* Невозможность поменять стиль поворота линии или её конца.
* MSAA не поддерживается большинством устройств, и большинство браузеров не поддерживают это для закадрового буфера (off-screen buffers). Из-за этого у вас могут быть зубчатые линии.
* Для штрихованных/пунктирных линий `glLineStipple` или устарел или не поддерживается в WebGL.В некоторых проектах приведённые выше ограничения не помеха и `gl.LINES` оказывается приемлемым, но в большинстве случаев это не подходит для качественного продукта.## Триангуляция линии[Interactive demo](https://mattdesl.github.io/webgl-lines/triangles/index.html)Что такое триангуляция: [читать в Wikipedia ->](https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B8%D0%B0%D0%BD%D0%B3%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_\(%D0%B3%D0%B5%D0%BE%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%8F\))Обычно это решают с помощью разбиения линии на треугольники или треугольные полосы и рендерят полосы как обычную геометрию. Этот способ даёт больше контроля над отображением линии, позволяет добавить линии "колпачок" на конце, повороты линии, их соединение и т.д. Это также позволяет рисовать линии более творчески и интересно, как, например, в демонстрации выше.Для создания такого эффекта обычно получают нормаль для каждой точки вдоль пути и расширение наружу на половину толщины с обеих сторон. На примеры реализации можете посмотреть в [polyline-normals](https://www.npmjs.com/package/polyline-normals). Отдельную часть линии называют митром, в демке выше они чередуются по цветам (серый/оранжевый). Как соединение митров \[miter] объясняется с помощью математики, можете посмотреть в [этой дискуссии](https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader).Вам понадобятся более продвинутые сетки для создания "колпачков" на торце линии, скошенных соединений и т.д. Обработка этих случаев может быть довольно сложной, как можно увидеть в исходниках [Vaser C/C++](https://github.com/tyt2y3/vaserenderer)Для сглаживания у вас есть несколько вариантов:* Надеяться что MSAA поддерживается и вам никогда не понадобится рендерить линии в закадровый буфер
* Добавить больше треугольников по краям линии![](https://github.com/tyt2y3/vaserenderer/raw/master/docs/sample_images/fade_intro_1.png)![](https://github.com/tyt2y3/vaserenderer/raw/master/docs/sample_images/fade_intro_2.png)![](https://github.com/tyt2y3/vaserenderer/raw/master/docs/sample_images/fade_intro_3.png)![](https://github.com/tyt2y3/vaserenderer/raw/master/docs/sample_images/fade_intro_4.png)* Использовать для линии [текстуру с градиентом](https://github.com/mattdesl/lwjgl-basics/wiki/LibGDX-Finger-Swipe#5-anti-aliasing-and-stroke-effects). Это довольно просто, но плохо масштабируется
* В фрагментном шейдере, вычислять сглаживание основанное на прогнозируемом размере линии на экране.
* Рендерить [предварительно фильтрованную gl.LINES](http://people.csail.mit.edu/ericchan/articles/prefilter/) на втором проходе, по краю вашей линии*Примечание:* Недостатком этого способа можно считать острые края. Когда угол соединения между двумя сегментами очень острый, длина митра экспоненциально растёт и стремится к бесконечности, что вызывает артефакты при рендеринге. В некоторых приложениях это не проблема, в других же вы можете просто ограничить длину митры или соединить с другой митрой (т.е. скосить), когда угол слишком острый.В Triangles демо выше используется [extrude-polyline](https://github.com/mattdesl/extrude-polyline). Маленький модуль, который находится в разработке, для построения триангулированного меша из 2D ломаной. В итоге в него планируется добавить поддержку скруглённых соединений/окончаний и правильного ограничения митров.## Использование вершинного шейдера[Interactive demo](https://mattdesl.github.io/webgl-lines/expanded/index.html)Триангуляция может значительно усложнить ваш код и меш нужно будет перестраивать, когда изменяется тип соединения. Если вам надо простую линию в WebGL, это может быть немного перебор.Демка выше просто растягивает линию \[stroke] в вертексном шейдере, где толщина задаётся передачей значения в uniform. Мы создаем две вершины для каждой точки нашего пути и передаем [нормали линии и длину митра](https://www.npmjs.com/package/polyline-normals) как атрибуты вершины. Каждая пара имеет одну перевёрнутую нормаль (или митра), так что две точки расталкиваются от центра, образуя толстую линию.```glsl
attribute vec2 position;
attribute vec2 normal;
attribute float miter;
uniform mat4 projection;

void main() {
    // Передвинуть точку вдоль нормали на половину толщины
    vec2 p = position.xy + vec2(normal * thickness/2.0 * miter);
    gl_Position = projection * vec4(p, 0.0, 1.0);
}
```Эффект внутренней линии слева (нажмите, чтобы запустить анимацию) создан в фрагментном шейдере, используя заданное расстояние от центра. Мы можем также добавить линии штрихи, градиенты, свечение и другие эффекты. Для этого нам нужно ещё раз пройти по вершинам, используя `distanceAlongPath` (расстояние от начала пути) как параметр при вычислении.Код реализации этого подхода может быть абстрагирован в свой собственный модуль. Для ThreeJS это уже сделано в [three-line-2d](https://github.com/mattdesl/three-line-2d), включая штрихованные линии.## Screen-Space Projected LinesПредыдущее демо работало хорошо для 2D (ортогональных) линий, но может не работать так как вы хотите в 3D пространстве. Чтобы линия была с постоянной толщиной, независимо от положения в трёхмерном пространстве, нам нужно растянуть линию после проецирования в пространство экрана.[Interactive demo](https://mattdesl.github.io/webgl-lines/projected/index.html)Как и в прошлой демке, нам нужно представить каждую точку дважды (зеркально центру), так что они направлены в разные стороны. Однако, вместо того, чтобы вычислять нормаль и длину митра на стороне **CPU**, мы будем делать это в **вертексном шейдере**. Для этого на нужно отправить атрибуты в вершинный шейдер: `next` и `previous` позиции на всем пути.В вертексном шейдере, мы вычисляем наше соединение и насколько надо растянуть линию *\[extrusion]* на экране, для получения постоянной толщины. Чтобы работать в экранном пространстве, нам нужно использовать постоянную однородности *\[illusive homogeneous component]*, обозначаемому как `W`. Также известной как "перспектива деления". Узнать больше на [английском кратко](https://stackoverflow.com/questions/17269686/why-do-we-need-perspective-division) и на [русском с выводом](https://triplepointfive.github.io/ogltutor/tutorials/tutorial12.html). Это даёт нам нормализованные координаты на экране \[Normalized Device Coordinates], которые лежат в диапазоне `[-1, 1]`. Затем мы корректируем соотношение сторон, прежде чем что-то делать с линиями. Эту операцию мы проделываем и с `previous` и `next` позиций на протяжении всего пути.```glsl
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;
```Так же нужно обработать крайние случаи для первой и последней точки линии, но тем не менее, обработка простого сегмента выглядит так.```glsl
//normal of line (B - A)
vec2 dir = normalize(nextScreen - currentScreen);
vec2 normal = vec2(-dir.y, dir.x);

// раздвинуть от центра & откорректировать на соотношение сторон
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* для масштабирования ширины линии, когда они углубляются в сцену. Это поможет создать ощущение глубины.## Другие подходыКак и для многих вещей в *WebGL*, есть десятки способов нарисовать линии. Все демки были сделаны с достаточно маленькими абстракциями, поэтому вы можете все их рассмотреть и выбрать что вам подходит для вашего конкретного случая. Некоторые другие подходы, которые также имеют право на жизнь:* [Трафаретная геометрия](http://www.glprogramming.com/red/chapter14.html#name13)\
  Крутой трюк работающий на трафаретном буфере для создания полигонов без использования триангуляции. Однако, этот подход вообще не работает с любым MSAA. \[[1][]]\[[2][]]\[[3][]]

* [Loop-Blinn Curve Rendering](https://web.archive.org/web/2023/https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-25-rendering-vector-art-gpu)\
  Независимые от разрешения кубические сплайны рендеринга, идеально для глифов шрифта.

* [Растеризация штрихов](https://perfectionkills.com/exploring-canvas-drawing-techniques/)\
  Можно использовать для создания кистей как в Photoshop.

* [Single Pass Wireframe Rendering](https://web.archive.org/web/2017/http://ar3f.in/wireframe.html)\
  Аналогично процедурно генерируем линиям в демо, но лучше подходят для создания 3D линий в режиме wireframes. \[[1](https://dl.acm.org/doi/10.1145/1179849.1180035)]

* [Геометрический шейдер](https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader)\
  Это позволило бы создать линиям множество различных заглушек и соединений. Правда геометрические шейдеры не поддерживаются в WebGL.

* [Analytic Distance Fields](https://www.shadertoy.com/view/lts3Df)\
  Позволяет рендерить толстые сглаженные линии как в 2D так и в 3D. Но есть свои особенности из-за использования одного поля для quad и distance. Это не очень практично, но и даёт прикольные эффекты (например [размытие в движении](https://www.shadertoy.com/view/MdB3Dw))[1]: https://zrusin.blogspot.com/2006/07/hardware-accelerated-polygon-rendering.html[2]: https://web.archive.org/web/2023/http://phoboslab.org/log/2012/09/ejecta[3]: https://github.com/GoodBoyDigital/pixi.js## Используемые модулиПри создании демок использовалось с десяток свободных модулей из [npmjs.com](https://www.npmjs.com/). Вот список этих модулей:* [adaptive-bezier-curve](https://www.npmjs.com/package/adaptive-bezier-curve)
* [arc-to](https://www.npmjs.com/package/arc-to)
* [extrude-polyline](https://www.npmjs.com/package/extrude-polyline)
* [polyline-normals](https://www.npmjs.com/package/polyline-normals)
* [quad-indices](https://www.npmjs.com/package/quad-indices)
* [gl-vec3](https://www.npmjs.com/package/gl-vec3), [gl-mat4](https://www.npmjs.com/package/gl-mat4)## Дополнительная литература* [CesiumJS - Robust Polyline Rendering](http://cesiumjs.org/2013/04/22/Robust-Polyline-Rendering-with-WebGL/)
* [MapboxGL - Drawing Antialiased Lines in WebGL](https://web.archive.org/web/2023/https://www.mapbox.com/blog/drawing-antialiased-lines/)
* [NVIDIA - GPU Path Rendering](https://www.graphics.rwth-aachen.de/media/teaching_files/kilgard_2012_pathrendering.pdf)
* [Prefiltered Lines](http://bitsavers.trailing-edge.com/pdf/dec/tech_reports/WRL-98-2.pdf)---Написание этого поста очень затянулось, так как я не особо разбирался в теме *OpenGL* и не знал, как перевести часть терминов с сохранением их первоначального смысла. Поэтому я пока не буду переводить статьи по темам, где не силен в терминах. Ждите статей по типу «Создание эффекта туннеля».Также я хочу составлять список слов, которые я часто смотрел в переводчике, в конце поста. Ведь моя главная цель - разобраться в теме и подучить английский. А лучший способ это узнать - попробовать объяснить это другому.gl hf---<https://github.com/grishy/blog/blob/hugo/content/post/drawing-lines-is-hard.md>
