Пишем рендер. Часть 1. Введение, подготовка и первая точка

Пишем рендер. Часть 1. Введение, подготовка и первая точка

Небольшое вступление

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

Коротко о себе. Я программист с порядочным опытом коммерческой разработки....в вебе. Опыт в геймдеве у меня тоже есть, но он почти не связан именно с разработкой игр и никак не связан с компьютерной графикой. С универа у меня остались какие-то знания по плюсам, но это было давно и неправда. Поэтому в сущности статья будет представлять собой конспект в ходе обучения.

Я нашел вот этот цикл статей на хабре.

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

Пишем рендер. Часть 1. Введение, подготовка и первая точка

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

Тем не менее я надеюсь, что мне хватит навыков эти прорехи заполнить. Ну и я постараюсь записать то что получилось.

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

Ну и да, предполагается что читатель по крайней мере базово знаком с синтаксисом С++ и смутно помнит что-то из школьной геометрии.

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

С дисклеймерами и лирикой покончено, можно приступать.

Что должно получиться?

В самом финале у нас должна получиться относительно небольшая программа на чистом C++, которая на входе получает полигональную сетку и текстуру. На выходе - tga файл с отрендеренным отображением. Прелесть в том что мы в сущности пишем сильно упрощенный аналог OpenGL.

(здесь и далее курсивом будут приводится прямые цитаты из оригинальной статьи)

***
Поскольку целью является минимизация внешних зависимостей, то я даю своим студентам только один класс, позволяющий работать с TGA файлами. Это один из простейших форматов, поддерживающий картинки в формате RGB/RGBA/чёрно-белые. То есть, в качестве отправной точки мы получаем простой способ работы с картинками. Заметьте, единственная функциональность, доступная в самом начале (помимо загрузки и сохранения изображения), это возможность установить цвет одного пикселя.
Никаких функций отрисовки отрезков-треугольников, это всё придётся писать вручную.
***

Подготовка

Поскольку я последний раз работал с Visual Studio (далее VS) тысячелетия назад, у меня возникли проблемы уже на этапе создания базового проекта. Потому я приведу здесь некоторые моменты, которые, надеюсь, вам помогут сделать то же самое. С установкой VS надуюсь вы справитесь сами.

- Открыв VS вам потребуется создать новый проект. Из вороха предложенных вариантов вам нужно выбрать в фильтре по языкам C++ и далее выбрать Empty project. Запомните путь к проекту. У вас должен получиться примерно такой проект:

Пишем рендер. Часть 1. Введение, подготовка и первая точка

- Далее мы должны добавить библиотеку для работы с .tga изображениями.
Для начала вам нужно её скачать из репозитория автора, пройдя вот по этой ссылке на github, там нажать на кнопку code в верхнем левом углу и выбрать download zip. В этом архиве нас интересуют файлы tgaimage.cpp и tgaimage.h. Это та самая библиотека которая нам понадобиться

- Теперь добавим эти файлы в наш проект. Для меня неприятным сюрпризом стало то, что нельзя просто перетащить файлы в условный проект. Вернее можно, но добавятся просто ярлыки на эти файлы и если вы в дальнейшем соберетесь перенести проект - есть шанс что вы забудете по разным углам часть зависимостей. Потому берем и ручками копируем файлы tgaimage.cpp и tgaimage.h в папку с проектом.

Пишем рендер. Часть 1. Введение, подготовка и первая точка

Думали файлы добавились вам в проект? Чёрта с два. Идём в солюшен эксплорер (на скриншоте выше), кликаем правой кнопкой на "папке" Source Files. В открывшемся меню выбираем Add -> Existing Item и наконец выбираем наши два файла в папке с проектом. Ура, наша библиотека (тот самый класс для работы с TGA) уже в проекте.

- Теперь создаём наш ключевой файл проекта - main.cpp. Тут у вас два путя: точно так же добавить файл из существующего архива или создать его самим. Для второго в Source Files кликаем Add -> New Item. Там выбираем Visual C++ -> C++ File (.cpp). Не забудьте внизу указать имя main.cpp

Рисуем первую точку

Теперь если вы добавили чистый файл, то откройте его и добавьте следующий код

#include "tgaimage.h" const TGAColor white = TGAColor(255, 255, 255, 255); const TGAColor red = TGAColor(255, 0, 0, 255); int main(int argc, char** argv) { TGAImage image(100, 100, TGAImage::RGB); image.set(52, 41, red); image.flip_vertically(); // i want to have the origin at the left bottom corner of the image image.write_tga_file("output.tga"); return 0; }

Давайте резаберем этот код.

Сперва у нас подключается файл заголовка для нашей tga библиотеки

#include "tgaimage.h"

Хорошо. Мы ведь хотим использовать функционал из библиотеки в нашем коде.

Далее мы создаём две константы содержащие цвета, которыми мы будем красить наши точки.

const TGAColor white = TGAColor(255, 255, 255, 255); const TGAColor red = TGAColor(255, 0, 0, 255);

TGAColor - это структура объявленная в файле заголовка и у нее есть конструктор с подходящими параметрами. Пока вроде всё сходится.

Далее у нас начинается ключевая точка программы - функция main, являющаяся точкой входа в любую программу на C++. Мы создаём переменную image, которая будет представлять наше конечное изображение. Можно воспринимать эту переменную как матрицу 100 на 100 пикселей где каждый пиксель имеет соответствующие координаты. И тут мы видим кое что загадочное:

TGAImage image(100, 100, TGAImage::RGB);

Как у js'ника тут у меня возникает вопрос: а что выполняется при создании переменной с типом TGAImage. То есть понятно что он объявлен в файле заголовка, но код который должен взять наши параметры и что-то с ними сделать, находится в файле tgaimage.cpp, а мы его не подключали. Что за дела?

У того кто хорошо знаком с C++ такой вопрос может вызвать недоумение, тем не менее для Javascript программиста, который только кое-как ориентируется в синтаксисе всё не так очевидно. Дело в том что в js и с++ разный подход к области видимости переменных. В js по умолчанию переменные и методы объявляются в локальной области видимости, которая обычно ограничена границами текущего файла и если мы хотим что-то добавить в глобальную область видимости - это нужно сделать явно. В С++ же при наличии, к примеру, двух файлов как у нас, их содержимое по умолчанию оказывается в одной области видимости, которая тут называется пространством имён, и тут уже если мы хотим создать какую-то обособленное пространство имён нам нужно отдельно это указывать.

Так зачем же мы в таком случае подключали заголовочный файл если у нас всё находится в одном пространстве имён?

Дело в том что компилятор C++ обрабатывает каждый отдельный .cpp файл как отдельную единицу кода и не знает какие имена объявлены в других единицах компиляции. Таким образом пришлось бы писать абсолютно одинаковые реализации классов в каждом файле где они используются.

Файлы же заголовка содержит объявление только имён и типов как мы видим, без реализации в основном и когда мы делаем #include "tgaimage.h" то содержимое хэдер файла будет на этапе препроцессинга добавлено в код нашего main.cpp файла в месте объявления. Таким образом у нас будут нужные имена на этапе компиляции кода.

Не спрашивайте про то, как и в какой момент реализация функций из .cpp файлов подменяет наше объявление из методов, думаю мы вместе с вами сможем разобраться в этом позже.

Продолжим. Далее у нашей переменнной image, которая представляет собой нашу будущую картинку, мы с помощью метода set устанавливаем точку красного цвета в координаты x = 52, y = 41

image.set(52, 41, red);

Затем автор зачем-то переворачивает наше картинку по вертикали.

image.flip_vertically();

Зачем? Дело в том что исторически точка начала координат в графике считается от верхнего левого угла, то есть ось ординат Y как бы перевёрнута

Пишем рендер. Часть 1. Введение, подготовка и первая точка

Поэтому автор решил перевернуть картинку чтобы оси координат привычно шли из левого нижнего угла

Пишем рендер. Часть 1. Введение, подготовка и первая точка

Наконец мы сохраняем содержимое переменной image в файл output.tga который появится в папке с проектом и затем завершаем программу.

image.write_tga_file("output.tga"); return 0;

И вот мы можем нажать CTRL+S чтобы сохранить изменения и мы готовы к первому запуску

Поехали

Наконец мы можем нажать F5 и увидеть как выполнится наша программа. Прекрасно, не правда ли?

Пишем рендер. Часть 1. Введение, подготовка и первая точка
Пишем рендер. Часть 1. Введение, подготовка и первая точка

Мы можем увидеть что в папке с проектом появился вожделенный .tga файл. Ура! Время посмотреть что же у нас получилось, например с помощью Gimp.

Пишем рендер. Часть 1. Введение, подготовка и первая точка

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

***

В следующей главе мы перейдём на новый уровень и будем рисовать ЛИНИИ.

До новых встреч и с нетерпением жду комментариев от людей, которые объяснят мне где я был неправ.

Пишем рендер. Часть 1. Введение, подготовка и первая точка
2121
26 комментариев

Комментарий недоступен

3
Ответить
Автор

Я коренной дтфер и статьи не мои. Если поймут полтора человека - это уже успех, а главное что я сам разбираюсь пока статью пишу. Плюс там на хабре все умные дофига, так что буду для родного болотца писать.

4
Ответить

с нетерпением жду комментариев от людей, которые объяснят мне где я был неправ.

Собственно

1
Ответить

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

Еще, зачем рендерить точки? Сразу бы объяснял про контекст, буфферы, матрицы, весь графон на них держится. Ну или начал бы с пайплайна…

2
Ответить
Автор

С анимешными девочками действительно провал.
А вот если начинать объяснять про контексты и буфферы с матрицами с ходу - это верный способ задушиться и уйти в проститутки. Надо кушать материал маленькими понятными порциями и по чуть-чуть углублять.

1
Ответить

Жду продолжение цикла статей. Сам пишу иногда что-то эдакое на opengl для души. Каждый раз захожу чуть дальше. Надеюсь не забросишь)

1
Ответить
Автор

Я тож надеюсь )

1
Ответить