Pull to refresh

Comments 57

Если внимательно посмотреть на верхнюю часть головы, видна полигональная сетка (чуть ярче, чем внутренности треугольников). От чего этот эффект?
Это обман зрения. Вот я в гимпе просто взял значения пикселей наугад, внутри двух треугольников и на ребре.
Скрытый текст


Человеческий глаз очень чувствителен к смене гладкости поверхности, а наши нормали всего лишь непрерывны, но никак не C1 непрерывны, и тем более не C2 непрерывны.
Видимо такой эффект возникает из-за этого:
Мы считаем интенсивность освещения для каждой вершины треугольника и просто интерполируем интенсивность внутри.
Интересно, что бы было, если бы мы интерполировали не интенсивность, а сами нормали?
Тогда бы закраска называлась не именем Гуро, а именем Фонга.
Gouraud
Скрытый текст


vs Phong
Скрытый текст


совет: это надо открыть отдельно в двух вкладках и переключаться между ними
Отличие только в тонировке? Мне кажется, что модель освещения тоже меняется
Строго только тонировка. В одном случае интерполирована интенсивность, во втором нормали и интенсинвость посчитана для каждго пикселя. Остальное всё строго то же самое.
Яркость почему-то сильно отличается. Может быть вы не нормировали вектора, как сказано ниже?
А ведь вы правы, я уже забыл, как их считал, вот заново посчитанные картинки, тут отличие точно только в интерполяции.

gouraud
Скрытый текст


phong
Скрытый текст


phong-gouraud
Скрытый текст
Поскольку интенсивность в данном случае линейна от значения нормали, ничего бы не изменилось.
Разве что интерполированные нормали дополнительно нормировать на единицу.
Это утверждение неверно, даже для диффузионной модели освещения.
Какое утверждение? Что для линейной функции линеная интерполяция аргумента и линеная интерполяция значения даст одинаковый результат?
Интенсивность освещения не может быть линейно зависимой функцией от значения нормали. Поскольку у нормали нет значения)))
Это вектор.
У вас в модели освещения есть требование — нормаль должна быть нормирована. Если вы будете просто линейно интерполировать нормали, они перестанут быть нормированными.
Да, об этом я вспомнил позже. Но я уже проверил все три варианта закраски — с интерполяцией интенсивности, нормалей и дополнительной нормализацией — первые два ожидаемо совпадают, а третий вариант визуально от первых двух не отличается
Для диффузной модели освещения и плотной сетки они действительно будут отличаться слабо (потому что всегда можно линеаризовать разложив в ряд и остаточные члены в случае близких значений нормали будут малы). Тем не менее, ваше начальное утверждение неверно.

С зеркальными поверхностями же, у закраски Гуро начинаются проблемы, потому что обычно центры бликов не попадают на вершины.

Можете сравнить:
image
Дискуссия началась с того, что был задан вопрос — можно ли интерполяцией нормалей (вместо интерполяции интенсивности) избавить диффузную модель освещения от артефактов на ребрах сетки. Про зеркальную модель освещения речь вообще не шла.

PS. Предлагаю свернуть обсуждение
Возьмите 1 квадрат и поиграйте нормалями. Даже глаза не придется напрягать, чтобы увидеть разницу. Нормали двигайте смело в противоположные стороны. При закраске Гуро получите темный квадрат. При закраске Фонга- светлое пятно в центре, края темные.
PapaBubaDiop прав, тонировка Фонга отличается от тонировки Гуро. Она дороже, но даёт лучшие результаты. В следующей статье проиллюстрирую.
Короткий пример различия тонировок Гуро и Фонга. Вот двумерная сцена, свет падает строго сверху.
Гуро даст одинаковый цвет левой и правой вершине, и всё горизонтальное ребро будет одноцветным.

  ___
 /    \
/      \


Фонг будет интерполировать нормали и сделает самое яркое пятно в центре горизонтального ребра.
О, отличный пример, спасибо. Жаль, что старик Фонг додумался раньше меня :)
Вопрос производительности старик Фонг не решил. И лишь в конце 90-х пошла пьянка под названием шейдеры. Текстурами стали подменять пятна освещенности. Тут же прорисовался бамп-маппинг и… Короче, на Ваш век проблем с быстрой реалистической графикой еще осталось.
Это не обман зрения, а отсутствие нормализации в «пиксельном шейдере». После интерполяции нормали у нас она не нормализованная. Но с Гуро такое не прокатит.
Если вы внимательно посмотрите в код (ну или в текст статьи), то увидите, что картинка нарисована с тонировкой Гуро.
Я же написал, что с Гуро такое не прокатит. Я просто объяснил откуда берется такая «полигональность».
А я написал, что эта картинка сделана с Гуро. Так что объяснение неправильное.
Не очень понял я про преобразование нормалей. Просто транспонировать и взять обратную к матрице Viewport*Projection*ModelView — это неправильно?
Почему неправильно? Именно обратно-транспонированная к (Viewport*Projection*ModelView) будет преобразовывать нормальные векторы.

Скрытый текст
строго говоря, в настоящем OpenGL шейдеры работают в не в экранных координатах, normalized device coordinates (до вьюпорта)
Ковариантный и котравариантный тензор?
Ковариантный вектор и контравариантный вектор преобразуются по тем различными правилам, которые были упомянуты в статье. Может это они и есть?
Ну собственно да. Только людей пугать не будем.
UFO just landed and posted this here
UFO just landed and posted this here
Обрезать треугольник заранее заданной плоскостью отсечения (near plane clipping). Для конкретно моего рендерера Эрвин Куманс написал такой код (он импортировал рендерер в свой софт):
https://mobile.twitter.com/erwincoumans/status/850366818547974146

Зачем в преобразовании нормальных векторов вообще появляется транспонирование? Разве не проще применить сразу обратную без её транспонирования (как и записано в предыдущем шаге преобразований)?

И далее разве утверждение

нормаль к преобразованному объекту получается преобразованием исходной нормали, обратным к транспонированному 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, кроме как пробовать оба варианта. Я с час пытался вывести из содержимого статьи и разных прмеров кода и форумов, но не могу совладать с тем, что помимо того, что где-то матрица умножается на столбец, где-то строка на матрицу, так ещё и матрицы у некоторых представляются как набор столбцов, у других как строк, а у третих хранятся в непрерывном массиве и подразумеваеется хранение по строкам, но умножение идёт как со столбцами. А тут ещё и обращение через транспонирование...

Ответ прошлому себе: да всё правильно, только pointsToVector(center, eye).

где 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. Не лучше ли задавать позицию камеры точкой и двумя векторами (направление взгляда и ориентация верх-низ) вместо двух точек и вектора?

Тут трудно сказать, как лучше. Я тупо сделал совместимо с glu...

Проблема с текстурами была в ошибке copy-paste - z брался у одной и той же вершины для вычисления разных барицентрических координат.

Вы слишком быстрый, я не успеваю отвечать. Вопросы-то ещё остались?

Я, задавая вопрос, спешил устранить все глупые ошибки, и заявить о них прежде, чем мне на них укажут.

Осталось ещё только подозрение, что надо в gluLookAt смещение делать не только от минус начала базиса, но ещё и от минус позиции глаза.

А вопрос под другой статьёй остался про расчёт барицентрических координат в допроективном пространстве. Вопрос по вашей реализации.

Преобразование для нормалей пока не сделал, только интерполяцию самих нормалей вершин для начала — "если головой не вертеть, то должно быть и так нормально". Но почему-то освещение получислось такое, будто Z нормалей из obj файла указывает внутрь модели. Лицо освещено, когда вектор освещения [0, 0, 1], при том что камера, смотрит в отрицательном направлении, если я правильно помню. x и y — в порядке.

картинка

С нормалями к граням такого не было.

x и y — в порядке

Нет, с ними тоже самое.

[1, 0, 0]

Но вообще-то это наверное и правильно, если считать что источник света задаётся направлением на него, а не от.

Вектор нормали имел три координаты. Для преобразования делаем его четырёхмерным, дописывая ноль. После преобразования в четвёртой координате вектора уже не ноль. Если разделить все полученные координаты на четвёртую, в ней получится единица, что соответствует точке, а не вектору. Как же правильно спроецировать вектор обратно в 3d из однородных координат?

Если вектора должны подвергаться и перспективному искажению (точнее инверсии перспективного искажения вершин модели), а освещение рассчитывается после, то разве не надо делать некую аналогичную текстурной и z-буфферу антиперспективную поправку уже для самих векторов? Ведь получается, если просто рассчитывать освещение по нормалям после перспективного искажения, модель будет освещена так, будто она имеет иную форму, чем есть на самом деле. Возникает подозрение, что нормали не должны учитывать ViewPort и Projection матрицы.

Sign up to leave a comment.

Articles