19
Знакомство фронтендера с 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];
}
Дальше, я обновил у себя фрагментный шейдер:
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
(специальная функция которая позволяет делать линейную интерполяцию) и поменял еще стиль нейминга переменных.
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 недель, я понял, что недоволен тем как выглядит модель, они точно не выглядели как на рендере у дизайнера, да еще меня беспокоило, что я толщина линий все равно была какой-то не такой.
Мне было не понятно, почему у меня такая крупная сетка на яблоке, хотя в 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.);
}
Я наконец-то получил нужный результат.
Дальше, за час вместе с докой по функциям из 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.);
}
Я получил мелкую сетку. Ура!
Но, у меня оставалась проблема, что чем ближе к краю, тем хуже различаются линии.
Насчет этого вопроса, я обратился за помощью в чат и мне рассказали про OES_standard_derivatives
экстеншена для webgl. Это что-то вроде плагинов, которые добавляли в glsl новые функции или включали какие-то возможности в рендере. Добавив в код шейдера fwidth (не забывайте включать экстеншены, до того как соберете программу, а то буду проблемы), функцию которая появилась после подключение экстеншена. Я добился того, чего хотел.
#ifdef GL_OES_standard_derivatives
fw = fwidth(uv);
#endif
Осталось только написать как я делал анимацию!
19