Это история в несколько частей:
Знакомство фронтендера с WebGL: четкие линии (часть 3)
Знакомство фронтендера с WebGL: рефакторинг, анимация (часть 4)
Свой .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
И вот оно!
Все, я закончил со своим движком, а результат был божественным. Осталось дело за малым, отрефачить код и добавить анимации.
Gremlin92
Клёво!