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

от автора

Это история в несколько частей:

Свой .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

И вот оно!

Самое красивое яблоко на свете
Самое красивое яблоко на свете

Все, я закончил со своим движком, а результат был божественным. Осталось дело за малым, отрефачить код и добавить анимации.

ссылка на оригинал статьи https://habr.com/ru/post/567174/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *