Пользователь
0,0
рейтинг
18 января 2015 в 22:54

Разработка → Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 2 из 6 tutorial

Содержание курса



Улучшение кода






Official translation (with a bit of polishing) is available here.




Update:


Внимание, статья 4в даёт новую, более простую версию растеризатора.

Давайте знакомиться, это я.



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

В прошлый раз мы нарисовали проволочную сетку трёхмерной модели, в этот раз мы зальём полигоны. Точнее, треугольники, так как OpenGL практически любой полигон триангулирует, поэтому ни к чему разбирать сложный случай. Напоминаю, что этот цикл статей создан для самостоятельного программирования. Время, которое я здесь привожу — это не время чтения моего кода. Это время написания вашего кода с нуля. Мой код здесь только для того, чтобы сравнить ваш (рабочий) код с моим. Я совсем не являюсь хорошим программистом, поэтому ваш код может быть существенно лучше моего. Любая критика приветствуется, любым вопросам рад.

Пожалуйста, если вы следуете этому туториалу и пишете свой код, выкладывайте его на github.com/code.google.com и им подобные и давайте ссылки в комментариях! Это может хорошо помочь как и вам (другие люди могут чего посоветовать), так и будущим читателям.

Рисуем заполненный треугольник


Итак, тема на сегодня (примерно на два часа для плохо программирующих, но мотивированных студентов): отрисовка двумерных треугольников. В прошлый раз мы разобрали алгоритм Брезенхэма для растеризации отрезка, теперь задача нарисовать заполненный треугольник. Вы будете смеяться, но это нетривиальная задача. Я не знаю почему, но я знаю, что это так. Большинство моих студентов без подсказок проводят над этой задачей существенно больше пары часов. Давайте определимся с методом, а затем будем программировать.

В самом начале давайте рассмотрим вот такой псевдокод:

triangle(vec2 points[3]) {
    vec2 bbox[2] = find_bounding_box(points);
    for (each pixel in the bounding box) {
        if (inside(points, pixel)) {
            put_pixel(pixel);
        }
    }
}

Я очень люблю этот метод. Он простой и рабочий. Найти описывающий прямоугольник крайне просто, проверить принадлежность точки двумерному треугольнику (да и любому выпуклому полигону) тоже просто.

Оффтоп: если мне нужно будет написать код, который будет крутиться на, скажем, самолёте, и этот код должен будет проверять принадлежность точки полигону, я никогда не сяду на этот самолёт. Это на удивление сложная проблема, если мы хотим её решить надёжно.

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

Окей, начальная заглушка будет выглядеть следующим образом:
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    line(t0, t1, image, color);
    line(t1, t2, image, color);
    line(t2, t0, image, color);
}

[...]

    Vec2i t0[3] = {Vec2i(10, 70),   Vec2i(50, 160),  Vec2i(70, 80)};
    Vec2i t1[3] = {Vec2i(180, 50),  Vec2i(150, 1),   Vec2i(70, 180)};
    Vec2i t2[3] = {Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180)};


    triangle(t0[0], t0[1], t0[2], image, red);
    triangle(t1[0], t1[1], t1[2], image, white);
    triangle(t2[0], t2[1], t2[2], image, green);




Как обычно, на гитхабе доступен отпечаток кода. В этом коде всё просто: я даю три треугольника для начальной отладки вашего кода; если внутри функции triangle просто сделать вызов line(), то получим контур треугольника. Как нарисовать заполненный треугольник?

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

Требований можно добавлять гораздо больше, но мы довольствуемся этими тремя.

Традиционно используется line sweeping (заметание отрезком?):
  • Сортируем вершины треугольника по их y-координате
  • Растеризуем параллельно левую и правую границы треугольника
  • Отрисовываем горзонтальный отрезок между левой и правой точкой границы


Тут мои студенты начинают теряться, кто левый, кто правый, да и вообще, в треугольнике три отрезка…
В этот момент я оставляю своих студентов примерно на час, чтение моего кода куда как менее ценно, нежели сравнение своего (выстраданного!) кода с моим.

[прошёл час]

Как рисую я? Ещё раз, если у вас есть лучший метод, то я его с огромным удовольствием возьму на вооружение. Давайте предположим, что у нас есть три точки треугольника, t0,t1,t2, они отсортированы по возрастанию y-координаты.
Тогда граница А будет между t0 и t2, граница Б будет между t0 и t1, а затем между t1 и t2.
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    // sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
    if (t0.y>t1.y) std::swap(t0, t1);
    if (t0.y>t2.y) std::swap(t0, t2);
    if (t1.y>t2.y) std::swap(t1, t2);

    line(t0, t1, image, green);
    line(t1, t2, image, green);
    line(t2, t0, image, red);
}


Здесь у нас граница А нарисована красным, а граница Б зелёным.



Граница Б, к сожалению, составная. Давайте отрисуем нижнюю половину треугольника, разрезав его по горизонтали в точке излома границы Б.

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    // sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
    if (t0.y>t1.y) std::swap(t0, t1);
    if (t0.y>t2.y) std::swap(t0, t2);
    if (t1.y>t2.y) std::swap(t1, t2);

    int total_height = t2.y-t0.y;
    for (int y=t0.y; y<=t1.y; y++) {
        int segment_height = t1.y-t0.y+1;
        float alpha = (float)(y-t0.y)/total_height;
        float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
        Vec2i A = t0 + (t2-t0)*alpha;
        Vec2i B = t0 + (t1-t0)*beta;
        image.set(A.x, y, red);
        image.set(B.x, y, green);
    }
}




Заметьте, что в этот раз у меня получились разрывные отрезки. В отличие от прошлого раза (где мы рисовали прямые) я не заморочился поворотом изображения на 90°. Почему? Это оказывается не всем очевидным моментом. Просто если мы соединим горизонтальными линиями соответствующие пары точек, то пробелы пропадут:



Теперь осталось отрисовать вторую половину треугольника. Это можно сделать, добавив второй цикл:
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    // sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
    if (t0.y>t1.y) std::swap(t0, t1);
    if (t0.y>t2.y) std::swap(t0, t2);
    if (t1.y>t2.y) std::swap(t1, t2);

    int total_height = t2.y-t0.y;
    for (int y=t0.y; y<=t1.y; y++) {
        int segment_height = t1.y-t0.y+1;
        float alpha = (float)(y-t0.y)/total_height;
        float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
        Vec2i A = t0 + (t2-t0)*alpha;
        Vec2i B = t0 + (t1-t0)*beta;
        if (A.x>B.x) std::swap(A, B);
        for (int j=A.x; j<=B.x; j++) {
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
        }
    }
    for (int y=t1.y; y<=t2.y; y++) {
        int segment_height =  t2.y-t1.y+1;
        float alpha = (float)(y-t0.y)/total_height;
        float beta  = (float)(y-t1.y)/segment_height; // be careful with divisions by zero
        Vec2i A = t0 + (t2-t0)*alpha;
        Vec2i B = t1 + (t2-t1)*beta;
        if (A.x>B.x) std::swap(A, B);
        for (int j=A.x; j<=B.x; j++) {
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
        }
    }
}




На этом можно было бы успокоиться, но у меня случается несварение, когда я дважды вижу один и тот же код, да ещё так рядом. Поэтому сделаем его чуть менее читаемым, зато более простым для модификаций.

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
    if (t0.y==t1.y && t0.y==t2.y) return; // i dont care about degenerate triangles
    // sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
    if (t0.y>t1.y) std::swap(t0, t1);
    if (t0.y>t2.y) std::swap(t0, t2);
    if (t1.y>t2.y) std::swap(t1, t2);
    int total_height = t2.y-t0.y;
    for (int i=0; i<total_height; i++) {
        bool second_half = i>t1.y-t0.y || t1.y==t0.y;
        int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y;
        float alpha = (float)i/total_height;
        float beta  = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here
        Vec2i A =               t0 + (t2-t0)*alpha;
        Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta;
        if (A.x>B.x) std::swap(A, B);
        for (int j=A.x; j<=B.x; j++) {
            image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y
        }
    }
}


Отпечаток кода для отрисовки 2d треугольников.

Рисуем модель


Мы умеем уже отрисовывать модель с пустыми треугольниками, давайте их зальём случайным цветом, это поможет нам проверить, насколько хорошо мы закодировали заполнение треугольников. Вот код.

    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec2i screen_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f world_coords = model->vert(face[j]);
            screen_coords[j] = Vec2i((world_coords.x+1.)*width/2., (world_coords.y+1.)*height/2.);
        }
        triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(rand()%255, rand()%255, rand()%255, 255));
    }


Всё просто: как и раньше, пробегаем по всем треугольникам, превращаем мировые координаты в экранные и рисуем треугольники. Подробное описание разных систем координат в последущих статьях. Должно получиться нечто вроде этого:



Плоская тонировка


Давайте теперь убирать эти клоунские цвета и освещать нашу модель.
Капитан Очевидность: «При одной и той же итенсивности света полигон освещён максимально ярко, если свет ему перпендикулярен».

Давайте сравним:




Нулевую освещённость мы получим, если полигон параллелен вектору света.
Перефразируем: интенсивность освещённости равна скалярному произведению вектора света и нормали к данному треугольнику.
Нормаль к треугольнику может быть посчитана просто как векторное произведение двух его рёбер.

    for (int i=0; i<model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec2i screen_coords[3];
        Vec3f world_coords[3];
        for (int j=0; j<3; j++) {
            Vec3f v = model->vert(face[j]);
            screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
            world_coords[j]  = v;
        }
        Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);
        n.normalize();
        float intensity = n*light_dir;
        if (intensity>0) {
            triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255));
        }
    }


Но ведь скалярное произведение может быть отрицательным, что это означает? Это означает, что свет падает позади полигона. Если модель хорошая (обычно не наша забота, а 3д моделеров), то мы просто можем этот треугольник не рисовать. Это позволяет быстро убрать часть невидимых треугольников. В англоязычной литературе называется Back-face culling.


Модель моей головы выглядит детальнее? Ну так в ней четверть миллиона треугольников. Ничего, детали мы добавим позже, получив картинку, которую я дал для затравки в первой статье.

Обратите внимание, внутренняя полость рта нарисовалась поверх губ. Ну а что, такое быстрое отсечение невидимых треугольников убирает всё ненужное только для выпуклых моделей. Эти огрехи мы уберём в следующий раз, закодировав z-buffer.

Текущая версия рендерера.
@haqreu
карма
244,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (111)

  • +5
    Очень крутой стиль изложения!
    Жаль, что когда начинал заниматься программированием графики не попадались такие ёмкие статьи.

    PS Простите, не удержался: www.everfall.com/paste/id.php?t5ht4mw6kvls
    • 0
      Как дешевле всего под линуксом запустить .bat?
      • 0
        Никогда не проверял под линуксом, возможно, что-то пойдёт не так как задумывалось.
        Я бы попробовал виртуальную машину с XP или попросил кого-нибудь из знакомых расшарить виндовый рабочий стол.
        • 0
          Можете просто выложить скриншот(ы)? :)
          • +6
            как-то так:

            • +15
              О, спасибо большое! На это я могу только ответить прекрасным рейтрейсером, сделанным на постскрипте (должен выполняться непосредственно на принтере!)

              • +1
                Класс!
  • +2
    Божественно. Я, как человек далёкий от компьютерной графики, благодаря вашим статьям, очень увлекся темой. Уже хочется вникать в следующую часть статьи, аналогично тому, как хочется начать читать следующую главу захватывающей книги )
    Заголовок спойлера
    Хотелось бы немного узнать о вас в качестве вступления к следующей статье :)
    • +4
      Присоединяюсь! Расскажите о себе, очень интересно. Статья очень интересная, изложение прекрасное!
  • +1
    Когда я читал своим студентам аналогичный курс, мы совместно пришли к выводу, (навозившись со всеми этими условиями для перестановки порядка вершин и так далее), что наиболее дубовый вариант рисования треугольника — проход по прямоугольнику, который его содержит и проверка того факта, что точка лежит по одну сторону от всех трех его сторон — является самым надежным с точки зрения кодирования и формального анализа. Там можно даже оценку погрешности определения принадлежности точки треугольнику дать. Да. он медленнее, но нам было не до скорости, мы за истину боролись.

    Кроме того, он замечательно разводится по параллельным процессорам.
    • 0
      Гхм. А я что написал в самом-самом начале статьи? Cразу после строчки В самом начале давайте рассмотрим вот такой псевдокод:.
      • +1
        Я об этом варианте и говорил. Жалко, что вы не показали, что внутри inside() происходит.
        • 0
          Да что там может происходить, считаем барицентрические координаты точки относительно треугольника, и дело с концом. Всё, что с негативными координатами, отбрасывается.

          Заодно заменяет билинейную интерполяцию, которая нам нужна будет для текстурирования и для z-буфера.
          • 0
            Можно чуть быстрее, для каждой из трех сторон определить знак выражения D = (х — х1) * (у2 — у1) — (у — у1) * (х2 — х1). Это числитель в формуле для барицентрических координат.
            Если вершины перечислены в правильном порядке (либо по часовой, либо — против часовой), то все знаки знаменателей в формулах для барицентрических координат будут совпадать.

            А так как для нашей модели это справедливо, можно и в ускоряшки поиграть.
            • 0
              Еще быстрее — это посчитать линейные функции, которые задают грани полигона. Затем достаточно посчитать значения этих функций в самом первом пикселе в bounding box. Далее значения этих функций в соседних пикселях вычисляется за счет одного сложения, например f(x +1, y) = f(x, y) + a, где a — соответствующий коэф. в функции грани ax + by + c
              • 0
                Да, но этот метод не позволяет интерполировать. Скорость мне абсолютно побоку, мне компромисс между самым коротким и самым читаемым кодом. Мой код абсолютно нечитаем, это плохо.

                Вот так у меня выглядит отрисовка треугольника с z-буфером. Вы можете написать код лучше моего? С удовольствием его возьму :)

                Вычисление барицентрических координат мне импонирует тем, что получается элегантнаая интерполяция, особенно когда мы добавим текстурные координаты и/или векторы нормали.
                • 0
                  Это не связано с интерполяцией, а просто оптимизация. Таким образом это реализовано в железе в различных GPU. Барицентрические координаты считаются уже после того, как выясняется, что точка внутри треугольника и используются для интерполяции. Нет смысла считать их до того момента, как это выяснится. Это особенно критично при использовании bounding-box алгоритма, поскольку кол-во проверяемых пикселей, не попадающих в полигон, может быть велико. Я не говорю, что у вас так должно быть реализовано, разумеется, просто рассказал про оптимизацию
                  • 0
                    Ага, спасибо, может кому пригодится. К сожалению, это увеличит размер кода, а это для меня неприемлимо. А вот непосредственный просчёт координат для каждой точки прямоугольника может быть кратким и существенно более читаемым, нежели мой код растеризации.
                • 0
                  Если использовать структуры данных не в стиле «хватай мешки, вокзал отходит», можно получить чуть более лаконичный и красивый код.

                  Например, если совместить буфер кадра и 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 и так далее) можно такого хлеба напечь, что мало не покажется.
          • 0
            Не дело с концом. Чтобы избежать графических артефактов, как например дыры между полигонами (упомянуто у вас) или перезаписывание пикселей на смежных гранях, нужно добавлять код для реализации filling convention, например top-left rule. Плюс, для проверки принадлежности пикселя к треугольнику достаточно вычислить значения линейных функций, которые заданными гранями треугольника, и необязательно до этого считать барицентрические координаты. Их можно посчитать потом, как только установлено, что пиксель внутри
    • 0
      Обычная растеризация прекрасно параллелится. Собственно, каждую линию можно считать в отдельном процессе — никаких зависимостей за пределами линии нет.
  • +1
    Браво, очень полезная статья! Глядя на код сразу не разобрался, как рисуете треугольник, но набросал на бумаге и все сразу стало понятно (может быть и Вам стоит схематичный рисуночек добавить в статью, который наверняка есть в лекциях?).

    Кстати по доступности напомнило курсы КГ Ravi Ramamoorthi. Очень помогло разобраться в основах, рекомендую!
    • 0
      Мне казалось, что схематичных рисунков по отрисовке треугольника там как минимум три-четыре…
      • 0
        Я имею ввиду рисунок в произвольный момент движения y от t0.y к t1.y, с показом точек A и B, а так же вектора t1-t0, было бы полезно, мне кажется)
  • +54
    • +2
      Хехе, я с самого начала сказал про 10-20 часов программирования для рендера правой головы.
      И я ни разу не соврал, могу выдать мою голову, она честно отрендерится так, как нарисовано :)
      • 0
        А чем её оцифровывали? Кинектом?
        • +1
          Нет, восстановление 3д модели по трём обычным фотоснимкам.
          • +4
            Поклонники ждут от вас название учебного заведения во Франции, чтоб подать заявку на поступление в этом году, вне зависимости от возраста и основной специализации, а также тотального незнания французского, хехе.
          • 0
            Реквестирую статью и по этой теме…
            • 0
              Ох. Добавляю в todo, но у меня минимум на десяток статей уже расписан график.
              • 0
                Ничего, подожду. Всё равно времени немного, на всё не хватает. :(
      • +1
        Лучше сделайте это, потому что иначе вовсе антиклимакс какой-то :) Просто дайте ссылку на файлик с обьектом головы, и домашку его отрендерить.
        • +2
          Окей, модель доступна в репозитории. Раз уж домашка, то мой текущий код на ней сломается. Почините и выкладывайте скриншоты!
  • +14
    Хорошая статья, но было бы не плохо если Вы добавляли ссылку на предыдущую статью из вашей серии.
    • –3
      В моём профиле есть список всех публикаций.
      • +7
        Все-таки оглавление в начале каждой статьи удобнее — но это опять же только для удобства читателей. В конечном итоге вам решать, делать или не делать. Эти плюшки ценность статьи как таковой не увеличат, просто сделают удобнее прочтение, особенно для тех, кто нагуглит их через 2-3-6 месяцев.
      • +7
        И что? А если вы потом напишете еще 200 постов, а кто-то попадет на эту статью из Гугла или по ссылке с любого другого ресурса? Как ему в вашем профиле искать другие посты?
        В каждом из постов серии должно быть содержание со ссылками на все опубликованные в серии посты. Как здесь, например.
        А теперь посмотрите на свой первый пост: там нет ссылки на вторую часть. Это совершенно неправильно.
        • –13
          В какой момент я вам задолжал, не подскажете? Аккуратнее со словами, пожалуйста.
          • +2
            Здесь подразумевается правило хорошего тона ведения статьи. Если она имеет несколько частей, то разумеется логично предположить, что где-то должно быть написано краткое оглавление, дабы иметь быстрый доступ к статьям.
            Тем самым вы повышаете удобство для пользователей, вы же с расчетом на них пишете статьи? Вроде и мелочь, но было бы приятно увидеть ссылки на другие статьи этой серии.
            • +4
              Такой язык я прекрасно понимаю, спасибо.
  • 0
    Может быть в репозитории рендерера стоит проставить таги для каждого урока?
    • 0
      Мм. Я не очень понял, у меня несколько коммитов на одну статью. Вы что предлагаете в тагах ставить?
      • 0
        Финальный рендерер для главы1, главы2 и т.д. (таги chapter01, chapter02...). Если я сейчас зайду в репо — сходу открыть рендерер для первой главы не получится — и это всего 2 главы пока )

        Ну это так, для удобства читающих…
        • 0
          Хотите права администратора на репозиторий?
          • 0
            Да я как-то не напрашивался ) Давайте я вам вечером напишу, после работы.
        • +4
          Для удобства читающих, добавлены теги на коммиты, которые соответствуют финальным версиям исходников для первой и второй главы.
          • 0
            Супер, спасибо!
          • +1
            в архиве для первой главы в файле main.cpp — самая первая версия функции line вместо финальной, реализующей алгоритм Брезенхэма.
  • 0
    А почему segment_height вы считаете внутри цикла, а не перед ним?
    • 0
      a) да просто потому, что мне всё равно, мне скорость абсолютно не важна здесь
      б) потому, что в финальной версии отрисовки треугольников эта переменная зависит от индекса цикла
  • 0
    Спасибо за статью, легкий стиль изложения, безусловно, импонирует. Однако, показалось, что математических обоснований маловато. Барицентрические координаты затронуты только в обсуждении выше, проекция 3D-модели на 2D есть, но про матрицу проекции упоминаний нет. Предполагается, что у студентов уже есть необходимый бэкграунд?
    • +1
      Вся геометрия будет вынесена в отдельную статью.
  • 0
    .
  • 0
    Майкл Абраш писал такие книжки с оптимизированным кодом еще 2 десятилетия назад. Из наших могу посоветовать Борескова и Ко. Купить сейчас в книжном их, скорее всего, не получится, но интернет всё помнит :)
  • +3
    Вот такой «шедевр» получился без z-сортировки
    Шедевр
    image
    • +1
      Отлично, спасибо! К выходным выложу код z-буфера.
      Попробуйте отрендерить мою голову (осторожно, требует доработки напильником, ~10-20 строк кода).
      • +2
        Вот такая вполне неплохая штука выходит, если прикрутить тупую (sorted Python'а) сортировку по возрастанию минимальной z-координаты (минимальной из трех вершин полигона)
        Работает медленно, но работает
        Штука


        Вот голова, насчет «размеров» изображения уже поколдовал, когда искал посторонние изображения, сделал ручной ввод, а вот двойные слеши в описаниях полигонов переварились не сразу
        Рисовалась почти минуту
        Голова


        А вот с отсортированными полигонами, 7 секунд в плюс сортировки
        Полость носа наружу не торчит


        Вот кот с «z-буфером»
        Кот

        • 0
          Сортировка — с целью корректировки порядка прорисовки
          искал посторонние модели*
          • +1
            Супер, отличные рендеры.
            Сортировка по минимальной вершине — это практически «алгоритм художника». У меня в репозитории код з-буфера уже есть, статья будет через несколько дней.
            • 0
              Глянул в репозиторий, уловил примерную идею z-буфера
              Для того, чтобы вместо такого
              Бррррр


              Получалось такое


              , пришлось убрать требование острого угла между вектором «светильника» и нормалью
              Рендеринг заметно замедлился

              Но все равно заметны погрехи в освещении
              • +1
                Стоп. Освещение и отсечение «задних» поверхностей — это разные вещи, они совпадают только если свет позади камеры.

                Если свет сбоку, то освещённость нужно считать скалярным произведением между нормалью и светом, а отсекать рисование треугольников скалярным произведением между направлением вектора взгляда и нормалью.
                • 0
                  Хм, спасибо, куда-то меня слегка не туда понесло
                  Попробую наковырять поворот модели, может что и получится
                • +1
                  Что-то вроде получилось
                  Формулы нагло нагуглил
                  Например




                  • +3
                    Супер, добавьте небольшую константу к освещённости всех треугольников (около 40 из 255 возможных). Это будет так называемый ambient lighting (свет, отражённый от стен, от пола и прочего). Скалярное произведение с нормалями даёт диффузную компоненту освещённости — это свет непосредственно от источника, падающий на матовую поверхность.

                    • +1
                      Получилось что-то такое
                      image


                      Еще очень мало нравилась монотонная неосвещенная поверхность, поэтому прикрутил зависимость яркости от расстояния от «стен» (от центра, на самом деле)

                      Как-то так
                      Код ОЧЕНЬ плохой, подозреваю

                      Но перепады все-таки слишком резкие, хех
                      • +2
                        Отличные картинки. Перепады можно сгладить, взяв нормальный вектор не к треугольнику, а к каждой его вершине. Массив хранится в строках vn x y z, в строках f x/x/vn1 x/x/vn2 x/x/vn3 число vni — это индекс из массива нормалей.
                        Таким образом, мы получаем три разные интенсивности в вершинах треугольника. Теперь треугольник нужно заливать не одним цветом, а плавным градиентом между тремя. Всё это называется тонировкой Гуро.

                        Должно получиться нечто вроде вот этого
                        • +1
                          Хм, либо где-то у меня косяк (хотя некоторые грани стыкуются хорошо), либо в файле что-то слегка не то
                          Например



                          Хотя я больше верю в проблемы в своей лапше, которую кодом назвать сложно
                          • +1
                            Абсолютно точно у вас просто проблемы с индексацией, у вас одна и та же вершина имеет разный цвет в разных треугольниках. Ищите проблемы в индексации внутри треугольника, вы перепутали не чтение, а порядок отрисовки внутри треугольника. Возможно, в сортировке вершин вы забыли цвет сортировать вместе с координатами.
                            • +2
                              Возможно, в сортировке вершин вы забыли цвет сортировать вместе с координатами.

                              Точно. Телепат из вас тоже неплохой, как и лектор :)
                          • +1
                            проверьте вот этот кусок кода, в нём нужно ещё присваивание цвета сделать

                            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
                            • +1
                              Это я еще не закоммитил тонирование, проблема была действительно в том, что вершины я сортировал, а к нетронутым яркостям обращался по индексу
                              Вроде готовое тонирование
                              image

                              Эта картинка отрисовывалась почти целых 15 секунд, ох
                              Много вычислений и медленный язык
                              • +1
                                Супер. Теперь осталась последняя неиспользованная вещь из .obj файла — это текстурные координаты. Принцип тот же, вы читаете vec2 из массива vt ui vi, среднее число между слешами в f x/x/x x/x/x x/x/x — это текстурные координаты данной вершины в данном треугольнике. Интерполируете их внутри треугольника, умножаете на ширину-высоту текстурного файла и получаете пиксель из файла текстуры. Для цвета, про карты нормалей потом.
                                Вот диффузная текстура
                                • +2
                                  Готово.
                                  Сначала что-то получилось не то с координатами пикселей, поэтому получилось
                                  ОНО
                                  image

                                  Но оказалось достаточным просто перевернуть координаты
                                  Вот так

                                  • +1
                                    А!!! Большое спасибо!

                                    Теперь просто забудьте про векторы нормали, которые вы проинтерполировали, они не будут использоваться в финальном рендере. С uv-координатам, что вы использовали для диффузной текстуры, прочитайте вот эту текстуру.

                                    Там каждый rgb-цвет соответствует xyz-нормальному вектору для данного пикселя.
                                    То есть, внутри отрисовки треугольника вы получаете цвет из диффузной текстуры, а нормаль (и, как следствие, интенсивность) из нормальмапной текстуры. (не забудьте пронормализовать все векторы)

                                    После этого вам останется ещё пара текстур (глянцевость и «мраморность») и рендер закончен.

                                  • +1
                                    да, разумеется, [0,255] из картинки нужно трансформировать в [-1,1] координат нормали
                                  • +1
                                    Да, забыл. Это сработает только для объекта, не подвергшегося никаким вращениям. То есть, для вида, как у меня на рендерах. Иначе нужно преобразовывать нормальные вектора в зависимости от положения камеры. Если у вас просто вращения, то достаточно просто провращать нормальные вектора. Иначе ждите моей статьи про геометрию :)
                                    • +2
                                      Вроде так
                                      Рендер


                                      И я уже начинаю путаться в своей лапше
                                      • +4
                                        Очень, очень хорошо.

                                        Выглядит неплохо, правда, я привык к шрамам на левой щеке :)


                                        Свет не обязан бить в лицо, просто камеру вращать надо вместе с картой нормалей.
                                        Если не вдаваться совсем в дебри, осталось сделать настоящие тени, использовать глянцевую карту.
                                        Вот тут про подсчёт.

                                        Ну и подповерхностное рассеяние (это я не кодировал и сам, но это несложно).

                                        Сколько у вас это в итоге времени заняло?
                                        • +2
                                          Времени заняло много, с утра сижу :)
                                          Затянуло

                                          В координатах и их порядке я и сам путаюсь, немного чищу вот сейчас
                                          • +3
                                            Про настоящие тени ссылка. Легко видеть, что у меня на этом рендере у носа тени нет, например.

                                            Вот примерно такая картинка должна получиться без карты подповерхностного рассеяния:
                                            • 0
                                              Спасибо, но, наверное, буду ждать дальшейших статей
                                              В том, что у меня есть сейчас, я разобраться не могу от слова никак.
                                            • 0
                                              Так, проспался, векторы нормалей поворачивать как-то получается
                                              Кажется, верный бампмаппинг


                                              Теперь я понимаю, как много и как сложно считает видеокарта, хех
                                              • +1
                                                Прекрасно! Только шрам на левую щёку переместите :)
                                              • 0
                                                Например


                                                (как раз вектор освещенности был обращен по Х)
                                                • +1
                                                  сел писать продолжение :)
                                                  • 0
                                                    Удачи
                                                    Я попробую с бликами разобраться
                                                  • +5
                                                    Вот такие вот красивые блики получились

                                                    • +1
                                                      5+
              • 0
                А с какой целью в вашем коде вы трижды производите манипуляции с z-буфером?
                • 0
                  Не понял вопроса, в каком именно месте кода?
                  • 0
                    Ох, я обращался к bakatrouble :) подразумевался код, начиная с первого коммита: github.com/bakatrouble/obj_render/commit/f79680eca035280efa5466f8e4bc73265ab103e5
                  • 0
                    И да, у меня возникла проблема, на которой я застрял прочно: как достать из вектора освещения угол, на который надо повернуть модель для того, чтобы можно было использовать z-буфер и для тени?
                    • 0
                      Ох, вы определённо не можете подождать до шестой статьи :)

                      Вам нужно разобраться, как работает gluLookAt.
                      В инете должно быть полно примеров. Но там реально нужна матричная алгебра, иначе всё будет очень некрасиво.

                      Открою секрет, у меня код уже написан.
                      Репозиторий infographie содержит ужасный (но рабочий) код, который я сейчас переписываю. Есть большие шансы, что geometry.cpp переедет как есть, без изменений. Посмотрите заодно в папку скриншоты, последние две картинки.
                      • 0
                        Посмотрите заодно в папку скриншоты, последние две картинки.

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

                        Проблема возникла как раз в том, чтобы «научиться смотреть с точки зрения источника света»

                        Спасибо, попробую поковырять
                        • 0
                          Да, рендер делается в два прохода: первый раз только z-buffer, цветная картинка ни к чему с камерой, помещённой в источник света, а второй раз с нормального места камеры.

                          Но. Вам недостаточно просто иметь два з-буфера, вам необходимо знать отображение одного в другой (какой пиксель одного какому пикселю другого соответствует). А это 4x4 матрица.
                          • 0
                            Хм, теперь я совсем запутался, планировал делать слегка по-другому (и теперь понял, что не очень-то и верно)

                            Попытаюсь все-таки дотерпеть до продолжения
                        • 0
                          Есть задача куда проще, попробуйте сделать подповерхностное рассеяние (subsurface scattering). Соотв. текстура лежит здесь
                • 0
                  Это для «левой» стороны полигона, для «правой» и для точек между ними в цикле
                  • 0
                    Суть я уловил, но в оригинале всего один цикл. В чем будет конечное отличие в рендеринге?
                    • 0
                      В оригинале все это объединено
                      А у меня всего лишь индийский код
                      • 0
                        Понял вас :). Ну, необъединенные циклы зато легче воспринимаются при чтении такой статьи: compgraph.tpu.ru/zbuffer.htm
  • 0
    «line sweeping» я бы перевел как «проход отрезком». Тут sweeping это в главном своем значении «wide in range or effect». Вообще я бы не стал ставить какое-то русское слово в перевод sweep/sweeping, так как языки не совпадают вовсе. Спасибо за статьи! Очень интересно подалать все как в молодости :)
  • 0
    Увлекательно! С удовольствием читаю все части. С нетерпением жду часть про 1) сглаживание (относительно) низкополигональных сеток 2) вертексные нормали вместо плоскостных.
  • 0
    Реализация на JS (первая часть):
    1. jsfiddle.net/2wvyga24/8/ — простейшая заливка по z-координате.
    2. jsfiddle.net/2wvyga24/15/ — свет. Очень долго не понимал, в чём проблема при рисовании, а, оказывается, при вычислении нормали использовал экранные координаты вместо мировых :)
    • 0
      jsfiddle.net/2wvyga24/18/ интересный эффект. Полигональная сетка с учётом света.
  • 0
    На Scala: github.com/klpx/tinyrenderer/tree/step-2
    Из интересного — тесты на отрисовку треугольников, а также сам алгоритм отрисовки: я нахожу фундамент (ребро, проекция которого соответствует проекции всего треугольника) и крышу треугольника, а затем рисую линии, ограниченные функциями фундамента и крыши. Выглядит примерно так:
      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)
          }
        }
      }
    

    картинка


  • 0
    Сделал на JS c динамическим освещением ^_^
    jsfiddle.net/yecgozrt/2/
  • 0
    Спасибо за ваши статьи еще раз, haqreu!

    Я сейчас как раз на этапе з-буффера, и у меня возникли проблемы:

    В алгоритме рисования линии можно было обойтись вычислением y = y0 * (1. - t) + y1 * t, но вы этого не стали делать, так как «неэффективно». Тем не менее, в алгоритме рисования треугольников вы вовсю пользуетесь этой формулой, в итоге у вас несколько умножений и делений на каждый y в треугольнике, и даже не дали ни одного комментария, почему внезапно мы стали использовать неэффективный код, хотя раньше у нас был эффективный (видимо, чтобы было проще для понимания, но тогда, наверно, можно было и алгоритм брезенхама не давать?)

    В общем, я решил, что мне нужна производительность, и написал код, который, как я надеялся, должен был быть быстрее — https://goo.gl/lzftxO, что, конечно, возможно, и не так. А в третьей статье оказалось, что вы используете подход из предыдущей статьи, чтобы вычислить координату z у точек, и, похоже, мой выстраданный код мало применим для этого :)

    • 0
      Не совсем, вот цитата из предыдущей статьи:



          for (int x=x0; x<=x1; x++) {
              float t = (x-x0)/(float)(x1-x0);
              int y = y0*(1.-t) + y1*t;
      

      [...]
      Этот код работает прекрасно. Именно подобной сложности код я хочу видеть в финальной версии нашего рендера.
      Разумеется, он неэффективен (множественные деления и тому подобное), но он короткий и читаемый.
      • 0
        Ага, я это помню. Если бы только где-нибудь возле строчки
        Итак, предыдущий код прекрасно работает, но он может быть оптимизирован.
        была бы приписка, что оптимизированный код, который мы сейчас получим, не будет использоваться в финальной версии рендера, а предыдущая версия будет, у меня бы наверное, и вопросов не возникло.

        Но если вы считаете, что все и так хорошо, то все и так хорошо. Спасибо еще раз!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.