DEV Community

Cover image for Знакомство фронтендера с WebGL: четкие линии
Nikita Nafranets
Nikita Nafranets

Posted on • Edited on

3

Знакомство фронтендера с WebGL: четкие линии

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

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

ужасная песочница

(извиняюсь за качество, код с той песочницей потерял и даже по памяти результат восстановить не могу)

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

Выглядит круто! А теперь добавь текстуры, модель не должна просвечиваться...

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

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

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

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];
}
Enter fullscreen mode Exit fullscreen mode

Дальше, я обновил у себя фрагментный шейдер:

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;
}
Enter fullscreen mode Exit fullscreen mode

(песочница)

мои любимые линии

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

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);
}
Enter fullscreen mode Exit fullscreen mode

Все, я свободен, где моя зп?

И вот, красота! Дизайнер доволен и я тоже. Да есть какие-то мелочи, но это лучшее что я смог тогда родить.

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

Доработка шейдера

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

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

Для начала я вспомнил трюк из уроков по шейдерам и просто закидывал цвета на основе x координаты и получил для себя интересный результат.

Выглядит модно же

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

Слишком крупная сетка

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

Лучше сетки, только точки

Тогда, я решил сначала написать топорно и родил такой шейдер.

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.);
  }
Enter fullscreen mode Exit fullscreen mode

Я наконец-то получил нужный результат.

Перфекто

Дальше, за час вместе с докой по функциям из webgl. Я смог переписать код на более близкий к webgl.

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.);
}
Enter fullscreen mode Exit fullscreen mode

Я получил мелкую сетку. Ура!

Мама мия

Но, у меня оставалась проблема, что чем ближе к краю, тем хуже различаются линии.
Насчет этого вопроса, я обратился за помощью в чат и мне рассказали про OES_standard_derivatives экстеншена для webgl. Это что-то вроде плагинов, которые добавляли в glsl новые функции или включали какие-то возможности в рендере. Добавив в код шейдера fwidth (не забывайте включать экстеншены, до того как соберете программу, а то буду проблемы), функцию которая появилась после подключение экстеншена. Я добился того, чего хотел.

  #ifdef GL_OES_standard_derivatives
    fw = fwidth(uv);
  #endif
Enter fullscreen mode Exit fullscreen mode

Боже, какая красота!
Лучше не смогу

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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay