[JavaScript, WebGL] Знакомство фронтендера с WebGL: четкие линии (часть 3)

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
12-Июл-2021 15:30

Это история в несколько частей:
Свой .obj парсер, свой webglПервое, что я сделал адаптировал код из песочницы и использовал gl.LINES.
Извиняюсь за качество, код с той песочницей потерял и даже по памяти результат восстановить не могуПоказав дизайнеру, я ожидал услышать: "все идеально, ты отлично поработал!".
Но услышал: "выглядит круто! А теперь добавь текстуры, модель не должна просвечиваться".И тут я понял, что gl.LINES мне никак не помогут с решением задачи. Я пошел не совсем туда. Мне почему-то казалось, что самое важное это линии, но потом понял, что должен был залить цветом модельку и выделить на ней грани поверхностей другим цветом.Я понял, что мне все же нужны uv (текстурные координаты), потому, что без них невозможно закрашивать фигуру правильно, но те uv который генерировал редактор моделей не подходили для закрашивания. Там была какая-то своя логика по генерации координат.Подняв этот вопрос с человеком который показал как парсить obj. Он мне дал новую песочницу в которой показал, как генерировать текстурные координаты, чем вселил новую надежду. Он так же набросал простейший шейдер который рисовал линии. Взяв его решение, я обновил свою песочницу и обновил парсер.
Код парсера в статье я покажу впервые.
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];
}
Дальше, я обновил фрагментный шейдер:
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;
}
Извините, данный ресурс не поддреживается. :( И, о боже! Вот он результат который я так хотел. Да грубо, линии жесткие, но это шаг вперед. Дальше я переписал код шейдера на smoothstep (специальная функция которая позволяет делать линейную интерполяцию) и поменял еще стиль нейминга переменных.
// fragment
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);
}

Красота! Я доволен собой, а дизайнер моей работой. Да есть какие-то мелочи, но это лучшее что я смог тогда родить.
Хотя особо внимательные сразу заметят, что размеры квадратов стали больше, чем на прошлой "грубой" версии.
А я был не особо внимательным, поэтому заметил это только спустя 2 недели. Возможно, эйфория от успеха вскружила мне голову...Доработка шейдераКогда я закончил первую реализация рендера, я пошел делать другие задачи по проекту. Но в течении 2 недель, я понял, что недоволен тем как выглядит модель, она точно не выглядела как на рендере у дизайнера, да еще меня беспокоило, что я толщина линий все равно была какой-то не такой.Мне было не понятно, почему у меня такая крупная сетка на яблоке, хотя в cinema4d и блендере, она довольно мелкая.
Плюс, я решил поделиться со своими переживаниями с коллегой на работе, и когда я ему начал объяснять как работает мой шейдер, я понял, что уже и не помню как я вообще до него допер и при попытке объяснить ему, я начал по новой экспериментировать с шейдером.Для начала я вспомнил трюк из уроков по шейдерам и просто закидывал цвета на основе x координаты и получил для себя интересный результат.
Я понял, что все это время у меня была мелкая сетка, но я почему-то игнорировал ее. Поиграв еще, я наконец-то понял, что зарисовал только 2 грани из 4 у каждой поверхности, что привело к тому, что у меня такая крупная сетка.
У меня не получалось используя `step` и прочее, реализовать нужную мне сетку, я получал какой-то бред.
Тогда, я решил сначала написать топорно и родил такой шейдер.
// part of fragment shader
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.);
}
Я наконец получил нужный результат.
Дальше, за час вместе с докой по функциям из webgl. Я смог переписать код как-то по модному чтоль.
// part of fragment shader
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.);
}
Я получил мелкую сетку. Ура!
Почти, почти!Но, у меня оставалась проблема, что чем ближе к краю, тем хуже различаются линии.
Насчет этого вопроса, я обратился за помощью в чат и мне рассказали про OES_standard_derivatives экстеншена для webgl. Экстеншены это что-то вроде плагинов, которые добавляли в glsl новые функции или включали какие-то возможности в рендере. Добавив в код шейдера fwidth (не забывайте включать экстеншены, до того как соберете программу, а то буду проблемы), функцию которая появилась после подключение экстеншена. Я добился того, чего хотел.
// part of fragment shader
#ifdef GL_OES_standard_derivatives
    fw = fwidth(uv);
#endif
И вот оно!
Самое красивое яблоко на светеВсе, я закончил со своим движком, а результат был божественным. Осталось дело за малым, отрефачить код и добавить анимации.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_webgl, #_javascript, #_webgl, #_3d, #_vanilla (ванилла), #_novichok (новичок), #_frontend, #_javascript, #_webgl
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 19-Апр 08:09
Часовой пояс: UTC + 5