Короткий курс комп'ютерної графіки пишемо спрощений opengl своїми руками, стаття 1 з 6

Постановка задачі

Мета цього циклу статей - показати, як працює OpenGL, написавши його (сильно спрощений!) Клон самостійно. На подив часто стикаюся з людьми, які не можуть подолати початковий бар'єр навчання OpenGL / DirectX. Таким чином, я підготував короткий цикл з шести лекцій, після якого мої студенти видають непогані рендери.

Отже, завдання ставиться таким чином: не використовуючи ніяких сторонніх бібліотек (особливо графічних) отримати приблизно такі картинки:

Короткий курс комп'ютерної графіки пишемо спрощений opengl своїми руками, стаття 1 з 6

Я постараюся не перевалити за 500 рядків в кінцевому коді. Моїм студентам потрібно від 10 до 20 годин програмування, щоб почати видавати подібні рендери. На вхід отримуємо текстовий файл з полігональної сіткою + картинки з текстурами, на вихід відрендерене модель. Ніякого графічного інтерфейсу, що запускається просто генерує файл з картинкою.

Оскільки метою є мінімізація зовнішніх залежностей, то я даю своїм студентам тільки один клас, що дозволяє працювати з TGA файлами. Це один з найпростіших форматів, що підтримує картинки в форматі RGB / RGBA / чорно-білі. Тобто, в якості відправної точки ми отримуємо простий спосіб роботи з картинками. Зауважте, єдина функціональність, доступна на самому початку (крім завантаження і збереження зображення), це можливість встановити колір одного пікселя.

Ніяких функцій відтворення відрізків-трикутників, це все доведеться писати вручну.

Я даю свій вихідний код, який пишу паралельно зі студентами, але не рекомендую його використовувати, в цьому просто немає сенсу.

Весь код доступний на гітхабе, тут знаходиться початковий код, який я даю своїм студентам.

output.tga повинен виглядати приблизно так:

алгоритм Брезенхема

Мета першої лекції - зробити рендер в дротяній сітці, для цього потрібно навчитися малювати відрізки.
Можна просто піти і почитати, що таке алгоритм Брезенхема. але більшість моїх студентів буксують, читаючи відразу целочисленную версію, тому давайте напишемо код самі.

Як виглядає найпростіший код, який малює відрізок між двома точками (x0, y0) і (x1, y1)?
Мабуть, якось так:

Снапшот коду доступний на гітхабе.

Проблема цього коду (крім ефективності) це вибір константи, яку я взяв рівній .01.
Якщо раптом ми візьмемо її рівною .1, то наш відрізок буде виглядати ось так:

Ми легко можемо знайти потрібний крок: це просто кількість пікселів, які потрібно намалювати.
Найпростіший (з помилками!) Код виглядає приблизно так:

Обережно: найперший джерело помилок в подібному коді у моїх студентів - це цілочисельне ділення типу (x-x0) / (x1-x0).

Далі, якщо ми спробуємо цим кодом намалювати ось такі лінії:

То з'ясується, що одна лінія хороша, друга з дірками, а третій зовсім немає.

Дірки в одному із сегментів через те, що його висота більше ширини.
Мої студенти часто мені пропонують такий фікс: if (dx> dy) else.
Ну ялинки!

Цей код працює прекрасно. Саме подібної складності код я хочу бачити у фінальній версії нашого рендеру.
Зрозуміло, він неефективний (множинні поділу тощо), але він короткий і читається.
Зауважте, в ньому немає АССЕРТ, в ньому немає перевірок на вихід за межі, це погано.
Але я намагаюся не захаращувати саме цей код, так як його багато читають, при цьому я систематично нагадую про необхідність перевірок.

Отже, попередній код прекрасно працює, але він може бути оптимізований.

Оптимізація - небезпечна річ, потрібно чітко уявляти, на якій платформі буде працювати код.
Оптимізувати код під графічну карту або просто під центральний процесор - зовсім різні речі.
Перед і під час будь-якої оптимізації код потрібно профайліровать.
Спробуйте вгадати, яка операція тут найбільш ресурсномістка?

Для тестів я малюю 1000000 раз 3 відрізка, які ми малювали перед цим. Мій процесор: is Intel® Core (TM) i5-3450 CPU @ 3.10GHz.
Цей код для кожного пікселя викликає конструктор копіювання TGAColor.
А це 1000000 * 3 відрізка * приблизно 50 Пікслей на відрізок. Чимало викликів.
Де почнемо оптимізацію?
Профайлер нам скаже.

Я відкомпільоване код з ключами g ++ -ggdb -g3 -pg -O0; потім запустив gprof:

Зауважимо, що кожний розподіл має один і той же дільник, давайте його винесемо за межі циклу.
Мінлива error дає нам дистанцію до ідеальної прямої від нашого поточного пікселя (x, y).
Кожен раз, як error перевищує один піксель, ми збільшуємо (зменшуємо) y на одиницю, і на одиницю ж зменшуємо помилку.

А навіщо нам потрібні плаваючі точки? Едіственная причина - це одну поділку на dx і порівняння з .5 в тілі циклу.
Ми можемо позбутися плаваючою точки, замінивши змінну error інший, назвемо її error2, вона дорівнює error * dx * 2.
Ось еквівалентний код:

Інша розмова, тепер достатньо прибрати непотрібні копії при виконанні функції, перес колір по посиланню (або просто включивши прапор компіляції -O3) і все готово. Жодного множення, жодного поділу в коді.
Час роботи знизилося з 2.95 секунди до 0.64.

Дротяний рендер.

Тепер все готово для створення дротяного рендеру. Знімок коду і тестова модель знаходяться тут.

Я використовував wavefront obj формат файлу для зберігання моделі. Все, що нам потрібно для рендера, це прочитати з файлу масив вершин виду

v 0.608654 -0.568839 -0.416318
[. ]
це координати x, y, z, одна вершина на рядок файлу

і граней
f 1193/1240/1193 1180/1227/1180 1179/1226/1179
[. ]

Тут нас цікавлять перше число після кожного пропуску, це номер вершини в масиві, який ми прочитали раніше. Таким чином ця строчка каже, що вершини 1193, 1180 и 1179 утворюють трикутник.

Файл model.cpp містить найпростіший парсер.

Пишемо такий цикл в наш main.cpp і вуаля, наш дротяний рендер готовий.

Короткий курс комп'ютерної графіки пишемо спрощений opengl своїми руками, стаття 1 з 6

Наступного разу будемо малювати 2D трикутники і попдравлять наш рендер.