Знакомство фронтендера с WebGL: четкие линии
Tags: webgl, javascript
Published: 2021-07-06 22:39:46

## Свой .obj парсер, свой webgl

Первое, что я сделал адаптировал код из песочницы и использовал gl.LINES.

![ужасная песочница](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4xm3c19cefflyds938e.jpg)
> (извиняюсь за качество, код с той песочницей потерял и даже по памяти результат восстановить не могу)

Показав дизайнеру, я ожидал услышать, что все идеально/перфектно, ты отлично поработал! Но услышал:
> Выглядит круто! А теперь добавь текстуры, модель не должна просвечиваться...  

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

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

Подняв этот вопрос с человеком который показал парсинг. Он мне дал новую песочницу в которой показал, как генерировать текстурные координаты, чем вселил новую надежду. Он так же набросал простейший шейдер который рисовал линии. Взял его решение, я обновил свою песочницу и обновил парсер.
Код парсера в статье я покажу впервые.  
```js
const uv4 = [[0, 0], [1, 0], [1, 1], [0, 1]]; // захаркоженные координаты текстур

// функция которая парсит .obj и выплевывает вершины с текстурными координатами.
export function getVBForVSTFromObj(obj) {
  const preLines = obj.split(/[\r\n]/).filter(s => s.length);

  // функция которая отдавала все строки по первому вхождению
  const exNs = (a, fchar) =>
    a
      .filter(s => s[0] === fchar)
      .map(s =>
        s
          .split(" ")
          .filter(s => s.length)
          .slice(1)
          .map(Number)
      );

  // та же функция что выше, только для поверхностей (faces) и дополнительно парсила сами поверхности
  const exFs = s =>
    s
      .filter(s => s[0] === "f")
      .map(s =>
        s
          .split(/\s+/)
          .filter(s => s.length)
          .slice(1)
          .map(s => s.split("/").map(Number))
      );

  const vertexList = exNs(preLines, "v"); // получаем все вершины
  const faceList = exFs(preLines); // все поверхности

  const filteredFaceList = faceList.filter(is => is.length === 4); // собираем поверхности только с 4 точками, т.е. квады
  const vertexes = filteredFaceList
    .map(is => {
      const [v0, v1, v2, v3] = is.map(i => vertexList[i[0] - 1]);
      return [[v0, v1, v2], [v0, v2, v3]];
    }) // склеиваем треугольники 
    .flat(4);


  const uvs = Array.from({ length: filteredFaceList.length }, () => [
    [uv4[0], uv4[1], uv4[2]],
    [uv4[0], uv4[2], uv4[3]]
  ]).flat(4); // собираем текстурные координаты под каждую поверхность

  return [vertexes, uvs];
}
```
Дальше, я обновил у себя фрагментный шейдер:

```glsl
precision mediump float;

varying vec2 v_texture_coords; // текстурные координаты из вершинного шейдера
// define позволяет определять константы
#define FN (0.07) // толщина линии, просто какой-то размер, подбирался на глаз
#define LINE_COLOR vec4(1,0,0,1) // цвет линии. красный.
#define BACKGROUND_COLOR vec4(1,1,1,1) // остальной цвет. белый.

void main() {
  if ( 
    v_texture_coords.x < FN || v_texture_coords.x > 1.0-FN ||
    v_texture_coords.y < FN || v_texture_coords.y > 1.0-FN 
  )
    // если мы находимся на самом краю поверхности, то рисуем выставляем цвет линии
    gl_FragColor = LINE_COLOR;
  else 
    gl_FragColor = BACKGROUND_COLOR;
}
```

([песочница](https://codesandbox.io/s/apple-webgl-jd0kq))  
![мои любимые линии](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xcnw797glc4u3ppvgcf.png)  
И, о боже! Вот он результат который я так хотел. Да грубо, линии жесткие, но это шаг вперед. Дальше я переписал код шейдера на `smoothstep` (специальная функция которая позволяет делать линейную интерполяцию) и поменял еще стиль нейминга переменных.

```glsl
precision mediump float;
uniform vec3 uLineColor; // теперь цвета и прочее передаю js, а не выставляю константы
uniform vec3 uBgColor;
uniform float uLineWidth;

varying vec2 vTextureCoords;

// функция которая высчитала на основе uv и "порога" и сколько должна идти плавность
// то есть через threshold я говорил где должен быть один цвет, а потом начинается другой, а с помощью gap определял долго должен идти линейный переход. Чем выше gap, тем сильнее размытость.
// и которая позволяет не выходить за пределы от 0 до 1
float calcFactor(vec2 uv, float threshold, float gap) {
  return clamp(
    smoothstep(threshold - gap, threshold + gap, uv.x) + smoothstep(threshold - gap, threshold + gap, uv.y), 0., 
    1.
  );
}

void main() {
  float threshold = 1. - uLineWidth;
  float gap = uLineWidth + .05;
  float factor = calcFactor(vTextureCoords, threshold, gap);
  // функция mix на основе 3 аргумента выплевывает 1 аргумент или 2, линейно интерпретируя.
  gl_FragColor = mix(vec4(uLineColor, 1.), vec4(uBgColor, 1.), 1. - factor);
}
```

![Все, я свободен, где моя зп?](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/im5k5g14e1jtyu3whwux.png)  
И вот, красота! Дизайнер доволен и я тоже. Да есть какие-то мелочи, но это лучшее что я смог тогда родить.  
Хотя особо внимательные сразу заметят, что размеры квадратов стали больше, чем на прошлой "грубой" версии.
А я был не особо внимательным, поэтому заметил это только спустя 2 недели. Возможно, эйфория от успеха вскружила мне голову...

### Доработка шейдера
Когда я закончил первую реализация рендера, я пошел делать другие задачи по проекту. Но в течении 2 недель, я понял, что недоволен тем как выглядит модель, они точно не выглядели как на рендере у дизайнера, да еще меня беспокоило, что я толщина линий все равно была какой-то не такой.  

Мне было не понятно, почему у меня такая крупная сетка на яблоке, хотя в cinema4d и блендер, она довольно мелкая. 
Плюс, я решил поделиться со своими переживаниями с коллегой на работе, и когда я ему начал объяснять как работает мой шейдер, я понял, что уже и не помню как я вообще до него допер и при попытке объяснить ему, я начал по новой экспериментировать с шейдером.  
Для начала я вспомнил трюк из [уроков по шейдерам](https://www.youtube.com/watch?v=u5HAYVHsasc&list=PLGmrMu-IwbguU_nY2egTFmlg691DN7uE5) и просто закидывал цвета на основе x координаты и получил для себя интересный результат.

![Выглядит модно же](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6ah9gjcb0qjiu3a8x4i.jpg)  

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

![Слишком крупная сетка](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3e3muxpnrblpepnz3k1.jpg)

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

![Лучше сетки, только точки](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73ysuyzffrs8jd2ct0h7.jpg)

Тогда, я решил сначала написать топорно и родил такой шейдер.  
```glsl
if (vTextureCoords.x > uLineWidth && vTextureCoords.x < 1.0 - uLineWidth && vTextureCoords.y > uLineWidth && vTextureCoords.y < 1.0 - uLineWidth) {
    gl_FragColor = vec4(uBgColor, 1.);
  } else {
    gl_FragColor = vec4(uLineColor, 1.);
  }
```
Я наконец-то получил нужный результат.

![Перфекто](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cl9y6j4fblcn9jupfyvk.jpg)

Дальше, за час вместе с докой по функциям из webgl. Я смог переписать код на более близкий к webgl.
```glsl
float border(vec2 uv, float uLineWidth, vec2 gap) {
  vec2 xy0 = smoothstep(vec2(uLineWidth) - gap, vec2(uLineWidth) + gap, uv);
  vec2 xy1 = smoothstep(vec2(1. - uLineWidth) - gap, vec2(1. - uLineWidth) + gap, uv);
  vec2 xy = xy0 - xy1;
  return clamp(xy.x * xy.y, 0., 1.);
}

void main() {
  vec2 uv = vTextureCoords;
  vec2 fw = vec2(uLineWidth + 0.05);

  float br = border(vTextureCoords, uLineWidth, fw);
  gl_FragColor = vec4(mix(uLineColor, uBgColor, br), 1.);
}
```
Я получил мелкую сетку. __Ура!__

![Мама мия](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/klh1gjh1pwn3j6di0aja.png)

Но, у меня оставалась проблема, что чем ближе к краю, тем хуже различаются линии.
Насчет этого вопроса, я обратился за помощью в чат и мне рассказали про `OES_standard_derivatives` экстеншена для webgl. Это что-то вроде плагинов, которые добавляли в glsl новые функции или включали какие-то возможности в рендере. Добавив в код шейдера fwidth (не забывайте включать экстеншены, до того как соберете программу, а то буду проблемы), функцию которая появилась после подключение экстеншена. Я добился того, чего хотел.
```glsl
  #ifdef GL_OES_standard_derivatives
    fw = fwidth(uv);
  #endif
```
Боже, какая красота!
![Лучше не смогу](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9nlr9ym60gnfn7kns72.jpg)
 

Осталось только написать как я делал анимацию!