Comments 37
Ура, снова торт!

Скажите, какая из этих текстур дана в глобальных координатах, а какая в касательных к объекту?
Первая в глобальных. В ней цвет (значения векторов) очень сильно изменяется от положения в пространстве.

Какой формат текстуры предпочтительнее — касательный или глобальный? Почему?
Касательный. Больше диапазон значений, хотя вроде бы для применения нужно на одно умножение векторов больше.
Действительно, синюшняя карта дана в касательной системе координат (репер Френе). А синюшняя она потому, что нормали в карте всё же обычно совпадают с нормалями к базовой поверхности (третья компонента репера).

С обоснованием формата текстуры либо не понял, что вы имеете в виду, либо не согласен.
Наверно чушь. Идея была в том, что мы храним нормали в графическом файле в 8-битных каналах. Это значит, что на каждую координату у нас [0-255] значений. Если мы записываем координаты в глобальном формате, то теряется точность. Но теперь понимаю, что это просто направление, и кол-во возможны вариантов направления не меняется от точки отсчета нулевого направления.

Тогда выгоднее в глобальном из-за лишенго умножения. Так?
Если использовать нормаль с единичной длиной, то имея 8 бит на канал вы не используете весь диапазон возможных значений, а только маленькую часть — поверхность сферы.
Но есть простой хак: в шейдере распаковываем нормаль normalize(normal.xyz), сохраняя в текстуре полный диапазон значений из всех возможных векторов N^3.

Геометрию модели в «глобальных координатах» (на самом деле в локальных координатах модели) сложно искажать — нормаль потребует коррекции. Отсюда вытекает дополнительная сложность при наложения декалей (например следов от пуль) и детальных текстур (высокочастотного шума).
Экономим умножение, да, но если будем использовать глобальную систему, то наткнемся на невозможность анимировать модель.
С чего это вдруг? При анимации просто умножаем на матрицу анимации 3х3 и всё (само собой если анимация в шейдере, сейчас трудно найти где она происходит не в шейдере). И при анимации в глобальных дополнительно выгоднее хранить т.к. ненужно хранить информацию о тангентах и бинормалях чтобы правильно развернуть нормаль. Хранить нормаль в виде касательных выгоднее только потому что точность выше.
Глобальные координаты проще, не надо считать репер Френе.

Но текстуру в касательных координатах можно накладывать на любые модели. Например, если у вас есть кирпичная стена, то вы не будете хранить отдельную текстуру для всех возможных ориентаций стены в сцене. Вы сделаете один маленький кусочек касательной текстуры и все стены получат красивый нормалмап из одного и того же места.
Все пишут, что касательный формат лучше, потому что глобальный не подразумевает искажение модели. Это так, но есть еще одна причина: касательный формат использует всего два цветовых канала — а это на треть меньше, чем глобальный. Одна из самых тяжелых операций на гпу — это чтение из текстуры, чем меньше данных — тем лучше. Особенно актуально для мобилок. И поэтому быстрее выполнить преобразование, чем читать дополнительную информацию.
Строго говоря, в глобальном случае тоже можно хранить только два числа, скажем фи и тета в обычных полярных координатах. Работать с этим, правда, не очень удобно.
Немного слукавил, на самом деле три канала. Просто в некоторых случаях синий можно опустить
У вас ошибка в реализации работы с Z буфером.
Вы делите вершины на [3] элемент сразу в вершинном шейдере. Этого делать нельзя, т.к. афинные преобразования линейны, а вот глубина у нас уже не линейна.
Нужно интерполировать z и w отдельно, и делить попиксельно. То есть код вершинного шейдера переписать вот так:
    virtual Vec4f vertex(int iface, int nthvert) {
        Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
        gl_Vertex = Viewport*Projection*ModelView*gl_Vertex;     // transform it to screen coordinates
        varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity
        return gl_Vertex;                  // project homogenious coordinates to 3d
    }

В коде функции triangle принимать Vec4f, и переделать проверку в буфере глубины как-то так:
Vec3f c = barycentric(pts[0], pts[1], pts[2], P);
float z = pts[0].z*c.x + pts[1].z*c.y + pts[2].z*c.z;
float w = pts[0].w*c.x + pts[1].w*c.y + pts[2].w*c.z;
P.z = std::max(0, std::min(255, z/w+.5));

Случай, когда ваш подход с «сразу делением на w» не работает:
Делаем 2 прямоугольника (горизонтальный и вертикальный) с вершинами:
1 — Vec3f(-100.0, -1.0, 0.0) Vec3f(-100.0, 1.0, 0.0) Vec3f(100.0, -1.0, 0.0) Vec3f(100.0, 1.0, 0.0)
2 — Vec3f(-1.0, -100.0, 0.1) Vec3f(-1.0, 100.0, 0.1) Vec3f(1.0, -100.0, 0.1) Vec3f(1.0, 100.0, 0.1)
Крутим эту пару треугольников вокруг OY и наблюдаем артефакт.
Поэтому все графические API получают 4 параметра, и сразу делить на W в вершинном шейдере ни в коем случае нельзя.

p.s. Проблема растет оттуда же: habrahabr.ru/post/248611/#comment_8238683
Хоть вы там и написали в ответе, что это «муторно» и ненужно, в реале же без этой коррекции мы получим некорректный буфер глубины. Так что коррекция нужна, и она не муторная.
Ммм, я ещё не проснулся, какой артефакт будет в вашей сцене из двух прямоугольников? Z-fighting?

Что-то я не могу понять про нелинейность глубины.
Прямоугольник из-под низа будет плаво вылазить поверх вверхнего треугольника, когда будет становиться все параллельнее взгляду. Словами сложно описать, вы сделайте такую модельку, и покрутите, все сразу увидите.
Артефакта не вижу, но спасибо за замечание. Будет время — поправлю.
Скрытый текст
Да, типа такого креста надо покрутить.
У меня нет сейчас возможности показать на примере. Могу показать потерю перспективной информации на текстурах. Создаем пустой проект в RenderMonkey с текутрированный кубиком. Добавляем в вершинном шейдере gl_Position /= gl_Position.w;
Результат такой. Слева без деления на w в вершинном. Справа — с делением на w:
Скрытый текст


p.s. Сейчас думаю, что насчет буфера глубины возможно был не прав (надо проверить), т.к. скорее всего у нас будет частный случай, при котором оно будет работать. Однако в общем случае нужно делать так, как я описал. Отсутствие деления на w в вершинном шейдере автоматом это решит проблему с текстурированием, т.к. текстурные координаты будут вычисляться по еще линейному xyz.
С глубиной всё в порядке, с текстурами согласен. Поправлю при случае, спасибо.

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

Выкладки у вас верные, но вы их делаете для вершин. И забываете что дальше у вас линейная интерполяция.
После перспективного деления глубина должна меняться не линейно. Вот смотрите, слева сцена (так же без Y), а справа я нарисовал буфер глубины, какой он должен быть после перспективного деления:

а зеленой линией нарисовано то, что сделает линейная интерполяция, т.е. что будет записано в буфере глубины в вашем случае.
Да, вы правы. Если для отрезка (x0,y0,h0);(x1,y1,h1) сделать интерполяцию по-настоящему в 3d, то она будет прилично отличаться от интерполяции в 2d, если координаты h1 и h1 сильно отличаются.
Так, задний ход, поправки неверные. Отдельная статья про коррекцию перспективного искажения на подходе.
Не совсем, засветы на правой картинке шире засветов на диффузной. Вопрос коэффициентов во взвешенной сумме.
Поменяйте, пожалуйста, «артиста» на «дизайнера» или «художника» хотя-бы… :)
Очень частая ошибка, которая повсеместно в интернете — забывают, что сумма коэффициентов Kd + Ks = 1. из-за этого модель начинает отражать больше света, чем получает от источника. У вас эта ошибка присутствует.
Я не делал вообще никаких предположений о моих материалах, поэтому это не ошибка.
Более того, я ещё и модель с glow map выложу скоро, получил разрешение от художника.
Очень понравился ваш цикл статей, поэтому я, что называется, решил совместить полезное с полезным и написать рендерер на расте.

К сожалению пока не успел догнать вас, нахожусь лишь на третьем шаге. Имеется голова с диффузной текстурой. Сейчас по-моему удалось избавиться от артефактов, которые по всей видимости возникают из-за арифметики с плавающими числами. Т.к. раст не позволяет автоматически конвертировать числовые типы, то чтобы не приводить числа по десять раз туда-сюда в каждой строчке я передаю в функцию triangle векторы с дробными координатами и оперирую ими до самого вывода пикселя. К тому же треугольник я отрисовываю не горизонтальными полосками, а вертикальными. В результате мое изображение немного отличается от вашего, приведенного в третьей статье. Например, есть достаточно заметное отличие на веке левого глаза.
Картинка



Код выложен на гитхабе, может быть кому-то еще будет интересно. Или опытные «растаманы» подскажут мне, что можно улучшить :)
К тому же треугольник я отрисовываю не горизонтальными полосками, а вертикальными.
Но зачем? Это может сильно ухудшить производительность.
Изначально начал так реализовывать, когда взялся самостоятельно писать метод. Производительность в принципе может пострадать, но массив с пикселями тоже можно перевернуть, да и задача здесь все-таки не в производительности.
т.к. раст не позволяет автоматически конвертировать числовые типы

Раст позволяет это делать немного по-другому, но из-за бага в компиляторе, я до недавнего времени, пока не обошёл это, висел в первой части. Сегодня только закоммитил доделанные операции с числами, вот тут можно посмотреть: github.com/torkve/habropengl/blob/master/src/vec.rs

В смысле, конвертировать конечно придётся, но всё в одном месте.
Спасибо, ваше решение весьма интересное и очень кстати, два unwrap без проверки, правда, немного смущают. Но вообще я больше жаловался на сложности работы с разными числовыми типами, где нужны постоянные касты. После других языков это кажется избыточным и некрасивым, хотя с другой стороны заставляет больше задумываться о дизайне кода.
Это на самом деле очень правильно, потому что в оригинальном коде, например, есть очень много кастов между u8/u32/i32, которые легко в определённых ситуациях могут выдать нам мусор, и единственное, что спасает — это допущение, что мы не будем рендерить картинки такого большого размера.
Так что мои допущения с unwrap(), по большому счёту, такие же: я предполагаю, что мы будем инициализировать точки исключительно типами, которые можно свободно конвертировать в/из float, т.е. все те же стандартные численные типы с теми же условиями переполнения.
Касательно «тонировки Гуро» и вообще слова тонировка — оно в русском больше относится к колхозному тюнингу автомобилей, чем к графике. Стандартно это «закраска Гуро» или «модель освещения Гуро» или «затенение Гуро».
Only those users with full accounts are able to leave comments. Log in, please.