Комментарии 122
Жаль, что когда начинал заниматься программированием графики не попадались такие ёмкие статьи.
PS Простите, не удержался: www.everfall.com/paste/id.php?t5ht4mw6kvls
Я бы попробовал виртуальную машину с XP или попросил кого-нибудь из знакомых расшарить виндовый рабочий стол.
Кроме того, он замечательно разводится по параллельным процессорам.
Заодно заменяет билинейную интерполяцию, которая нам нужна будет для текстурирования и для z-буфера.
Если вершины перечислены в правильном порядке (либо по часовой, либо — против часовой), то все знаки знаменателей в формулах для барицентрических координат будут совпадать.
А так как для нашей модели это справедливо, можно и в ускоряшки поиграть.
Вот так у меня выглядит отрисовка треугольника с z-буфером. Вы можете написать код лучше моего? С удовольствием его возьму :)
Вычисление барицентрических координат мне импонирует тем, что получается элегантнаая интерполяция, особенно когда мы добавим текстурные координаты и/или векторы нормали.
Например, если совместить буфер кадра и z-буфер (RGBA — 4 байта, Z — 4 байта), можно вычислять итератор на редактируемый пиксель один раз, перед сравнением. Тогда запись в буфер будет происходить за одно присваивание, с использованием того же итератора:
auto p=frame.locate(x,y); //предположим, что frame - ссылка на класс, порожденный от array (или от vector), в котором определен метод auto locate(size_t x, size_t y);
if( (*p).z<Z)
{
(*p)={R,G,B,A,Z};
}
Однако тут (со всеми этими абстракциями, использованием C++14 и так далее) можно такого хлеба напечь, что мало не покажется.
Кстати по доступности напомнило курсы КГ Ravi Ramamoorthi. Очень помогло разобраться в основах, рекомендую!
И я ни разу не соврал, могу выдать мою голову, она честно отрендерится так, как нарисовано :)
В каждом из постов серии должно быть содержание со ссылками на все опубликованные в серии посты. Как здесь, например.
А теперь посмотрите на свой первый пост: там нет ссылки на вторую часть. Это совершенно неправильно.
Тем самым вы повышаете удобство для пользователей, вы же с расчетом на них пишете статьи? Вроде и мелочь, но было бы приятно увидеть ссылки на другие статьи этой серии.
Ну это так, для удобства читающих…
Попробуйте отрендерить мою голову (осторожно, требует доработки напильником, ~10-20 строк кода).
Работает медленно, но работает
Вот голова, насчет «размеров» изображения уже поколдовал, когда искал посторонние изображения, сделал ручной ввод, а вот двойные слеши в описаниях полигонов переварились не сразу
Рисовалась почти минуту
А вот с отсортированными полигонами, 7 секунд в плюс сортировки
Вот кот с «z-буфером»
искал посторонние модели*
Сортировка по минимальной вершине — это практически «алгоритм художника». У меня в репозитории код з-буфера уже есть, статья будет через несколько дней.
Для того, чтобы вместо такого
, пришлось убрать требование острого угла между вектором «светильника» и нормалью
Рендеринг заметно замедлился
Но все равно заметны погрехи в освещении
Если свет сбоку, то освещённость нужно считать скалярным произведением между нормалью и светом, а отсекать рисование треугольников скалярным произведением между направлением вектора взгляда и нормалью.
Попробую наковырять поворот модели, может что и получится
Формулы нагло нагуглил
Еще очень мало нравилась монотонная неосвещенная поверхность, поэтому прикрутил зависимость яркости от расстояния от «стен» (от центра, на самом деле)
Как-то так
Код ОЧЕНЬ плохой, подозреваю
Но перепады все-таки слишком резкие, хех
Таким образом, мы получаем три разные интенсивности в вершинах треугольника. Теперь треугольник нужно заливать не одним цветом, а плавным градиентом между тремя. Всё это называется тонировкой Гуро.
Должно получиться нечто вроде вот этого
Хотя я больше верю в проблемы в своей лапше, которую кодом назвать сложно
if y <= vert[1].y:
if vert[1].y == vert[0].y:
x1 = vert[0].x
x2 = vert[1].x
z1 = vert[0].z
z2 = vert[1].z
error = True
Эта картинка отрисовывалась почти целых 15 секунд, ох
Много вычислений и медленный язык
Вот диффузная текстура
Сначала что-то получилось не то с координатами пикселей, поэтому получилось
Но оказалось достаточным просто перевернуть координаты
Теперь просто забудьте про векторы нормали, которые вы проинтерполировали, они не будут использоваться в финальном рендере. С uv-координатам, что вы использовали для диффузной текстуры, прочитайте вот эту текстуру.
Там каждый rgb-цвет соответствует xyz-нормальному вектору для данного пикселя.
То есть, внутри отрисовки треугольника вы получаете цвет из диффузной текстуры, а нормаль (и, как следствие, интенсивность) из нормальмапной текстуры. (не забудьте пронормализовать все векторы)
После этого вам останется ещё пара текстур (глянцевость и «мраморность») и рендер закончен.
И я уже начинаю путаться в своей лапше
Выглядит неплохо, правда, я привык к шрамам на левой щеке :)
Свет не обязан бить в лицо, просто камеру вращать надо вместе с картой нормалей.
Если не вдаваться совсем в дебри, осталось сделать настоящие тени, использовать глянцевую карту.
Вот тут про подсчёт.
Ну и подповерхностное рассеяние (это я не кодировал и сам, но это несложно).
Сколько у вас это в итоге времени заняло?
Затянуло
В координатах и их порядке я и сам путаюсь, немного чищу вот сейчас
Вот примерно такая картинка должна получиться без карты подповерхностного рассеяния:
В том, что у меня есть сейчас, я разобраться не могу от слова никак.
Теперь я понимаю, как много и как сложно считает видеокарта, хех
Вам нужно разобраться, как работает gluLookAt.
В инете должно быть полно примеров. Но там реально нужна матричная алгебра, иначе всё будет очень некрасиво.
Открою секрет, у меня код уже написан.
Репозиторий infographie содержит ужасный (но рабочий) код, который я сейчас переписываю. Есть большие шансы, что geometry.cpp переедет как есть, без изменений. Посмотрите заодно в папку скриншоты, последние две картинки.
Посмотрите заодно в папку скриншоты, последние две картинки.
Я понял, что надо проходиться depth-тестом по видимым пикселям и с точки зрения источника света, и невидимые с точки зрения источника света пиксели будут находиться в тени
Проблема возникла как раз в том, чтобы «научиться смотреть с точки зрения источника света»
Спасибо, попробую поковырять
Но. Вам недостаточно просто иметь два з-буфера, вам необходимо знать отображение одного в другой (какой пиксель одного какому пикселю другого соответствует). А это 4x4 матрица.
А у меня всего лишь индийский код
1. jsfiddle.net/2wvyga24/8/ — простейшая заливка по z-координате.
2. jsfiddle.net/2wvyga24/15/ — свет. Очень долго не понимал, в чём проблема при рисовании, а, оказывается, при вычислении нормали использовал экранные координаты вместо мировых :)
Из интересного — тесты на отрисовку треугольников, а также сам алгоритм отрисовки: я нахожу фундамент (ребро, проекция которого соответствует проекции всего треугольника) и крышу треугольника, а затем рисую линии, ограниченные функциями фундамента и крыши. Выглядит примерно так:
def separateXBaseAndAngle(p1: Point, p2: Point, p3: Point): (Line, (Line, Line)) = {
val px = Array(p1, p2, p3)
val baseP1 = px.minBy(_.x)
val notP1Points = px.filter(_ ne baseP1)
val baseP2 = notP1Points.maxBy(_.x)
val angleP = notP1Points.filter(_ ne baseP2)(0)
(Line(baseP1, baseP2), (Line(baseP1, angleP), Line(angleP, baseP2)))
}
def drawTriangleNormal(p1: Point, p2: Point, p3: Point) {
val (base, (roof1, roof2)) = separateXBaseAndAngle(p1, p2, p3)
for (x <- base.p1.x to base.p2.x) {
val baseY = base getYByX x
val roof = (if (x <= roof1.p2.x) roof1 else roof2)
if (roof.p1.x == roof.p2.x) {
g.drawLine(x, roof.p1.y, x, roof.p2.y)
} else {
val roofY = roof getYByX x
g.drawLine(x, baseY, x, roofY)
}
}
}
jsfiddle.net/yecgozrt/2/
Я сейчас как раз на этапе з-буффера, и у меня возникли проблемы:
В алгоритме рисования линии можно было обойтись вычислением
y = y0 * (1. - t) + y1 * t
, но вы этого не стали делать, так как «неэффективно». Тем не менее, в алгоритме рисования треугольников вы вовсю пользуетесь этой формулой, в итоге у вас несколько умножений и делений на каждый y
в треугольнике, и даже не дали ни одного комментария, почему внезапно мы стали использовать неэффективный код, хотя раньше у нас был эффективный (видимо, чтобы было проще для понимания, но тогда, наверно, можно было и алгоритм брезенхама не давать?)В общем, я решил, что мне нужна производительность, и написал код, который, как я надеялся, должен был быть быстрее — https://goo.gl/lzftxO, что, конечно, возможно, и не так. А в третьей статье оказалось, что вы используете подход из предыдущей статьи, чтобы вычислить координату z у точек, и, похоже, мой выстраданный код мало применим для этого :)
for (int x=x0; x<=x1; x++) { float t = (x-x0)/(float)(x1-x0); int y = y0*(1.-t) + y1*t;
[...]
Этот код работает прекрасно. Именно подобной сложности код я хочу видеть в финальной версии нашего рендера.
Разумеется, он неэффективен (множественные деления и тому подобное), но он короткий и читаемый.
Итак, предыдущий код прекрасно работает, но он может быть оптимизирован.была бы приписка, что оптимизированный код, который мы сейчас получим, не будет использоваться в финальной версии рендера, а предыдущая версия будет, у меня бы наверное, и вопросов не возникло.
Но если вы считаете, что все и так хорошо, то все и так хорошо. Спасибо еще раз!
снова залез в свой косой код на С под ДОС
когда рисовал закрашенные треугольники, не делал проверку обращения (y1-y0) в 0 и получал артефакты в виде горизонтальных штрихов влево, до начала Х
разобрался с этим (накодив это в питоне и получив ошибку деления на 0), в Си почему-то не вылетало, возможно какие-то косяки с типами данных и значениями близкими, но не равными 0.
голова из цветных лоскутов рисуется и крутится вокруг вертикальной оси с досбокс с ~3..4 FPS
теперь хочу убрать невидимые грани, те что смотрят от наблюдателя. почитал как перемножаются вектора. (у вас там какой-то класс похоже работает, с которыми я не уметь, да и вряд ли он есть в BorlandC 3.1) дюже громоздко и медленно.
поэтому вопрос: в формате OBJ уже есть секция нормалей к вершинам. можно ли из них получить нормали граней? может это будет занимать меньше вычислений?
Можно. нужно взять все нормали вершин грани, сложить и полученный вектор нормализовать. Но вот насколько это вычислительно сложенне или легче - лень сейчас думать, извините
А как вообще уразуметь понятие "нормаль к вершине"? Ведь нормаль это перпендикуляр к плоскости, а вершина - точечный объект.
Три точки плюс три вектора задают криволинейный треугольник, и именно в этом смысл затенения Фонга, например.
(но при условии криволинейных треугольников предыдущие два комментария по ветке имеют мало смысла)
для иллюстрации
Например, вот так: Нормаль вершины
Просто нормаль к вершине -- это не свойство этой вершины, а вручную вводимый объект для художественных целей. Вы сами определяете, куда эта нормаль будет смотреть. Таким образом, не плоскость определяет нормаль, а вы нормалью определяете какую-то конкретную плоскость, в которой лежит эта вершина.
В большинстве случаев, однако, она считается автоматически, как средневзвешенное от граней, окружающих вершину (а каждая грань --- плоскость, которая уже имеет нормаль по определению), что в моделях освещения дает иллюзию гладкости. Но автоматический расчет -- это просто автоматизация процесса для сокращения ручной работы в наиболее частом случае.
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 2 из 6