Comments 57
Человеческий глаз очень чувствителен к смене гладкости поверхности, а наши нормали всего лишь непрерывны, но никак не C1 непрерывны, и тем более не C2 непрерывны.
Мы считаем интенсивность освещения для каждой вершины треугольника и просто интерполируем интенсивность внутри.Интересно, что бы было, если бы мы интерполировали не интенсивность, а сами нормали?
vs Phong
совет: это надо открыть отдельно в двух вкладках и переключаться между ними
Это вектор.
С зеркальными поверхностями же, у закраски Гуро начинаются проблемы, потому что обычно центры бликов не попадают на вершины.
Можете сравнить:
Гуро даст одинаковый цвет левой и правой вершине, и всё горизонтальное ребро будет одноцветным.
___ / \ / \
Фонг будет интерполировать нормали и сделает самое яркое пятно в центре горизонтального ребра.
Зачем в преобразовании нормальных векторов вообще появляется транспонирование? Разве не проще применить сразу обратную без её транспонирования (как и записано в предыдущем шаге преобразований)?
И далее разве утверждение
нормаль к преобразованному объекту получается преобразованием исходной нормали, обратным к транспонированному M
не упускает того факта, что потом надо ещё транспонировать целиком произведение обратнотраспонированной на нормаль? Что также расширяет первый вопрос о целесообразности транспонирования "туда и обратно".
P. S. Или поскольку нормаль — это вектор, то нам без разницы транспонирована она или нет в записи (важны лишь координаты), а запись такая только ради порядка умножения матрицы на вектор (как в OpenGL)? Значит в OpenGL всегда делается лишняя операция транспонирования? Это не сказывается на производительности (где-то есть наоборот выигрыш за счёт такого подхода)?
https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/05-camera/f07.png
в этой записи идёт скалярное умножение двух векторов, так что никаких транспонирований в реальном коде нет. Транспонирование вектора лишь для корректной матричной записи, разумеется.
Первый раз читал на неделе, казалось всё понял.
Сейчас перечитываю, чтоб реализовать и снова спотыкаюсь.
Matrix Minv = Matrix::identity();
Matrix Tr = Matrix::identity();
for (int i=0; i<3; i++) {
Minv[0][i] = x[i];
Minv[1][i] = y[i];
Minv[2][i] = z[i];
Тут бы пояснить, почему вдруг матрица поворота для пересчёта в новый базис собирается из векторов формирующих новый базис. Если я правильно интуитивно понял, то потому что их координаты суть необходимые повороты соответствующих таких же исходных единичных векторов. Раз они правильно поворачивают единичные вектора, то правильно повернут и любые иные, являющиеся линейной комбинацией. Вроде и ясно, но как-то не математически строго.
Осталось понять какая из двух формул тут в итоге реализуется
Казалось бы вторая, но ведь тогда матрица должна быть обратная. Мы сразу получаем обратную из нового базиса и координат нового центра? И если так, то зачем вообще здесь эта формула, ведь реализация ей не следует, а полагается на некие соображения, "оставшиеся за кадром".
Вторая формула нас интересует. Реализация строго ей следует:
void lookat(const vec3 eye, const vec3 center, const vec3 up) { // check https://github.com/ssloy/tinyrenderer/wiki/Lesson-5-Moving-the-camera
vec3 z = (center-eye).normalize();
vec3 x = cross(up,z).normalize();
vec3 y = cross(z, x).normalize();
mat<4,4> Minv = {{{x.x,x.y,x.z,0}, {y.x,y.y,y.z,0}, {z.x,z.y,z.z,0}, {0,0,0,1}}};
mat<4,4> Tr = {{{1,0,0,-eye.x}, {0,1,0,-eye.y}, {0,0,1,-eye.z}, {0,0,0,1}}};
ModelView = Minv*Tr;
}
Мы строим матрицу ModelView, при помощи которой мы преобразуем любой вектор X=(x,y,z,1) [не забываем про однородные координаты] в базис камеры. Для этого я умножаю вектор X на матрицу переноса, а затем на обратную к M: ModelView * X = M^{-1} * Tr * X => ModelView = M^{-1} * Tr. Вычитание X-O делается при помощи матрицы переноса, а затем множим на матрицу M^{-1}. Теперь последний штрих: матрица M ортонормирована (у неё столбцы - это единичные векторы, ортогональные один другому). А у ортогональных матриц операция обращения и операция транспонирования совпадают. Вот я и смог сразу построить обратную, просто положив векторы в строки матрицы, а не в столбцы.
Спасибо за ответ. Про совпадение обращения и транспонирования для ортогональных, я и подозервал (хотя такие нюансы линейной алгебры я уже забыл). Но остаётся всё-таки вопрос, о том, на основании чего матрица М изначально вообще строится из единичных векторов нового базиса заданных в координатах исходного базиса? Должно быть, я упускаю что-то очевидное.
Эээ... В общем случае, если у нас есть три репера (глобальный, ijk и i'j'k'), и векторы ijk и i'j'k' выражены в глобальном репере, то существует матрица M, которая переводит ijk в i'j'k':
И найти матрицу M можно, помножив матрицы реперов: [ijk]^{-1} x [i'j'k']. Только вот в нашем случае у нас вообще только два репера: глобальный репер [ijk] и репер камеры [i'j'k']. Таким образом, ijk - это единичная матрица :)
А ещё есть дурацкий вопрос опять про вектор столбец vs вектор строка.
Так уж повелось, что у меня вектор представляется строкой и умножается на матрицу (состояющую из массивов-строк): получаемый вектор состоит из скаларяных произведений исходного вектора и столбцов матрицы.
Правильно ли я понимаю, что в этих условиях код должен быть таким?
function CreateViewMatrix(eye, center, up) {
const z = normalizeVector(pointsToVector(eye, center));
const x = normalizeVector(crossProduct(up, z));
const y = normalizeVector(crossProduct(z, x));
const rotation = [
[x[0], y[0], z[0], 0],
[x[1], y[1], z[1], 0],
[x[2], y[2], z[2], 0],
[0, 0, 0, 1],
]
const translation = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[-center[0],-center[1], -center[2], 1]
]
return multiplyMatrices(translation, rotation)
}
// to use like:
//[[x, y, z, 0]] * model * view * projection * viewPort
Не вижу никакой возможности понять, как именно должны располагаться строки/столбцы в матрице rotation, кроме как пробовать оба варианта. Я с час пытался вывести из содержимого статьи и разных прмеров кода и форумов, но не могу совладать с тем, что помимо того, что где-то матрица умножается на столбец, где-то строка на матрицу, так ещё и матрицы у некоторых представляются как набор столбцов, у других как строк, а у третих хранятся в непрерывном массиве и подразумеваеется хранение по строкам, но умножение идёт как со столбцами. А тут ещё и обращение через транспонирование...
где d — это разрешение z-буфера (у меня 255, т.к. я храню его непосредственно в чёрно-белой картинке).
А у меня z-буффер из float. Как быть? Вводить искусственное оганичение или оставит его диапазон от -1 до 1?
После перехода на цепочку матриц стало совсем плохо.
Вот это при ([0, 0, eyeZ], [0, 0, 0], [0, 1, 0]). eyeZ вообще не влияет на изоражение (если не нулевой).
Особенно смущает, что видны только 3 из 10 горизонтальных полос на доске.
А с ([0, 0, -1], [0, -1, 0], [0, 1, 0]), вот так:
результат
Мне казалось, что ViewPort должна вписывать в экран, а не выводить за его пределы.
Ну и влияние барицентрических координат для исправления перспективных искажений полностью исчезло, когда сделал всё по инструкции.
Не пойму, это я тупой или задача нерешаемая. Неужели никто по этим статьям не пытался реализовать рендерер? Неужели ни у кого не возникало тех же вопросов?
Эксперименты с моделью головы (после изначального исправления pointsToVector(eye, center) на pointsToVector(center, eye), не устранившего вышеназыванные проблемы) показали, что gluLookAt в отношении направления взгляда работает хорошо, но расстояние до модели зависит от не позиции eye, как я почему-то ожидал, а от center, что закономерно, ведь для сдвига используются именно его координаты. Не понятно какой тогда смысл в параметрах gluLookAt. Не лучше ли задавать позицию камеры точкой и двумя векторами (направление взгляда и ориентация верх-низ) вместо двух точек и вектора?
Проблема с текстурами была в ошибке copy-paste - z брался у одной и той же вершины для вычисления разных барицентрических координат.
Вы слишком быстрый, я не успеваю отвечать. Вопросы-то ещё остались?
Я, задавая вопрос, спешил устранить все глупые ошибки, и заявить о них прежде, чем мне на них укажут.
Осталось ещё только подозрение, что надо в gluLookAt смещение делать не только от минус начала базиса, но ещё и от минус позиции глаза.
А вопрос под другой статьёй остался про расчёт барицентрических координат в допроективном пространстве. Вопрос по вашей реализации.
Преобразование для нормалей пока не сделал, только интерполяцию самих нормалей вершин для начала — "если головой не вертеть, то должно быть и так нормально". Но почему-то освещение получислось такое, будто Z нормалей из obj файла указывает внутрь модели. Лицо освещено, когда вектор освещения [0, 0, 1], при том что камера, смотрит в отрицательном направлении, если я правильно помню. x и y — в порядке.
картинка
С нормалями к граням такого не было.
Вектор нормали имел три координаты. Для преобразования делаем его четырёхмерным, дописывая ноль. После преобразования в четвёртой координате вектора уже не ноль. Если разделить все полученные координаты на четвёртую, в ней получится единица, что соответствует точке, а не вектору. Как же правильно спроецировать вектор обратно в 3d из однородных координат?
Если вектора должны подвергаться и перспективному искажению (точнее инверсии перспективного искажения вершин модели), а освещение рассчитывается после, то разве не надо делать некую аналогичную текстурной и z-буфферу антиперспективную поправку уже для самих векторов? Ведь получается, если просто рассчитывать освещение по нормалям после перспективного искажения, модель будет освещена так, будто она имеет иную форму, чем есть на самом деле. Возникает подозрение, что нормали не должны учитывать ViewPort и Projection матрицы.
Реализация (на телефоне отрисовывается секунд 15) на js (репозиторий)
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4б из 6