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

Данная статья написана в тесном сотрудничестве (спасибо создателям XMPP) с haqreu, автором данного курса.Мы начали масштабный рефакторинг кода, направленный на достижение максимальной компактности и читаемости. Мы сознательно пошли на отказ от ряда возможных и даже очевидных оптимизаций для получения максимально доступного для понимания кода учебных примеров.P. S haqreu буквально на днях выложит статью о шейдерах! UPD: ВНИМАНИЕ! Раздел, начиная с номера 3.1, 3.14 и 3.141 и далее, будет о тонкостях реализации основы основ компьютерной графики — линейной алгебры и вычислительной геометрии. О принципах графики пишет haqreu, я же буду писать о том, как это можно внятно запрограммировать! image1 Общие положения Предыдущие статьи цикла показывают, что для написания программного отрисовщика нужно реализовать изрядную долю алгоритмов и математических объектов, относящихся к линейной алгебре и геометрии. В первую очередь, речь идет, конечно же, о векторах и матрицах. Мы используем векторы и матрицы мелких размерностей, поэтому нам удобно размещать их на стеке. Мы реализовали их при помощи шаблонов vec и mat. В комментариях к одной из статей я показал, что (по крайней мере, когда в роли компилятора GCC), использование циклов даже в таких мелких случаях дает более короткий и компактный машинный код, а также страхует от глупых опечаток при поэлементном наборе, например, операции матричного умножения.Код в данной статье написан для стандарта версии C++98, поэтому страшноват (верните мне auto!). Возможно, я подготовлю отдельную статью с тем же кодом, но с применением свежего стандарта. Держитесь, шаблоны с переменным количеством параметров, для вас будет работенка!2 Что интересного случилось в процессе слияния и рефакторинга? 2.1 Повсеместное использование size_t для индексов массивов Напомню, что пункт 18.1 стандарта С++, ссылаясь на пункт 7.11 стандарта C, определяет size_t как беззнаковый целый тип. Для нас это удобно, потому как, во-первых, это прямо соответствует смыслу индекса в массиве, а во-вторых, для проверки того факта, что i действительно находится в границах массива, нам достаточно проверить одно условие: (i < размер_массива) вместо двух: (i >= 0) && (i < размер_массива). Неуместный int будет изо всех мест убран.2.2 Странный цикл for Вот цикл for, который некоторые могут счесть странным: for(size_t i=Dim;i--;) {}; Этот цикл переберет все значения i от Dim-1 до 0. Перебор начнется с Dim-1, потому как перед входом в тело цикла будет выполнена сначала проверка, что i отлична от нуля, затем декремент i, и только после этого — вход в цикл. После заключительной итерации мы получим i=0 (цикл должен завершиться), но все равно должны будем (по смыслу операции i--) вычесть из беззнаковой i единицу. Это действие вполне определено стандартом, так что ничего страшного не произойдет — мы просто получим значение, равное f00.svg. Почему сделан именно этот цикл, а не традиционный for (size_t i=0; i=0; i--) Умница-компилятор, увидев тождественно истинное выражение [беззнаковое целое] >=0 просто заменит его на истину: for (size_t i=Dim-1; true; i--), что приведет к бесконечному зацикливанию. Вторая: такая запись короче на целых три символа. К циклу for (а также к вопросу, «Что быстрее, ++i или i++»), мы в дальнейших статьях вернемся.Мы можем пойти дальше: дело в том, что большинство наших операций очень тривиальны, и тело цикла, их выполняющего, состоит из одной строки. haqreu предложил поступить так: templatevec operator-(vec lhs, const vec& rhs) { for (size_t i=Dim; i--; lhs[i]-=rhs[i]); return lhs; } Фактически, мы убрали нашу операцию внутрь заголовка цикла, сделав тело цикла пустым.2.3 Код шаблона для вектора В нашем шаблоне для вектора определены на настоящий момент пять методов, два из которых являются операторами: это операторы взятия константной и не константной ссылки на элемент вектора []. При помощи макроса assert из  они проверяют, что переданный им индекс остается в пределах массива, что очень важно для обнаружения совсем глупых ошибок. В релизной версии мы добавим к ключам компилятора -D NDEBUG, что приведет к удалению макроса из кода. По закону Мерфи, после этого все должно будет сломаться, но мы это преодолеем.Исходный текст индексных операторов number_t& operator [](size_t index) { assert (index

const number_t& operator [](size_t index) const { return items[index]; } Метод fill (const number_t& val=0) заполняет вектор константой. По умолчанию это ноль.Исходный текст метода fill static vec fill (const number_t& val=0) { vec ret; for (size_t i=Dim; i--; ret[i]=val); return ret; } Методы norm () и normalize () предназначены для вычисления длины вектора и его нормировки, соответственно. number_t norm () const { return std: sqrt ((*this) * (*this)); } Для вычисления нормы используется тот факт, что это всего лишь корень квадратный из скалярного произведения вектора с самим собой. Очень кратко, емко, и в то же время тесно связано с теорией.Теперь нормировка вектора: vec normalize () const { return (*this)/norm (); } Опять же, все точно по определению: взяли себя и поделили на свою же длину. Обратите внимание, что эта функция возвращает отнормированную копию исходного вектора, а не изменяет его координаты таким образом, чтобы он стал единичным.Также отмечаю повсеместное использование const в данном коде. Это, с одной стороны, защищает от глупых ошибок, причем уже на этапе компиляции. С другой стороны, const дает компилятору больше сведений для проведения оптимизаций. Также замечу, что здесь нет ни одной директивы inline. Это связано с тем, что мы всю оптимизацию будем делать позже. Кроме того, с -O3 GCC становится настолько сообразительным, что сам выполняет встраивание. Станет ли он автоматически встраивать наши функции без явного inline, мы опять же будем рассматривать в последующих статьях.2.4 Бинарные операции над векторами и скалярами Операторы бинарных операций вынесены за пределы класса vec. Они полностью соответствуют определениям этих операций в теории: Просмотреть исходные тексты template number_t operator*(const vec&lhs, const vec& rhs) { number_t ret=0; for (size_t i=Dim; i--; ret+=lhs[i]*rhs[i]); return ret; }

templatevec operator+(vec lhs, const vec& rhs) { for (size_t i=Dim; i--; lhs[i]+=rhs[i]); return lhs; }

templatevec operator-(vec lhs, const vec& rhs) { for (size_t i=Dim; i--; lhs[i]-=rhs[i]); return lhs; }

templatevec operator*(vec lhs, const number_t& rhs) { for (size_t i=Dim; i--; lhs[i]*=rhs); return lhs; } Заостряю ваше внимание на тактике применения левого операнда (lhs) в трех последних реализациях. На вход мы получаем его копию, после чего над этой копией работаем и возвращаем. Если бы мы получали его по константной ссылке, нам пришлось бы делать копирование самостоятельно. Здесь очень удачно совпали свойства наших векторов и языка C++, чем мы и воспользовались. В реализации же скалярного умножения нам и вовсе не нужно копировать векторы — всю работу мы выполняем по константной ссылке.2.5 Отдельно — о реализации деления вектора на скаляр Так как все мы знаем математику, можем с удовольствием сказать «Для деления вектора на скаляр мы можем использовать умножение на величину, обратную этому скаляру». И настрочить вот такое: /////////////////////////////деление вектора на скаляр templatevec operator/(vec lhs, const Number& rhs) { return (lhs*(static_cast(1)/rhs)); } Важный момент — как мы поступили с единицей — мы обернули ее в static_cast, чтобы у нее всегда был правильный тип при делении. Однако, проведя простой тест: #include using namespace std; int main () { const double a=100.8765; const double b=1.2345; cout.precision (100); cout << a/b <<'\n' << a*(1/b)<<'\n'; return 0; } Мы можем увидеть, что результаты не сошлись: 81.7144592952612 356384634040296077728271484375 81.7144592952612 498493181192316114902496337890625 Получим более точное значение отношения при помощи maxima: (%i5) fpprec:100; (%i6) bfloat(100.8765/1.2345); (%o6) 8.17144592952612 356384634040296077728271484375b1 Видим, что значение, полученное прямым делением, является более точным. Это связано с накоплением погрешности при арифметических операциях. Поэтому не будем жадничать, а напишем отдельную реализацию оператора деления: templatevec operator/(vec lhs, const number_t& rhs) { for (size_t i=Dim; i--; lhs[i]/=rhs); return lhs; } 2.6 Операция погружения вектора в пространство большей размерности Она нам понадобится в одной из версий растеризатора. Суть операции состоит в том, что мы формируем вектор большей размерности, методом fill заполняем его константой, которую нам передали, а потом копируем в него координаты нашего вектора меньшей размерности. template vec embed (const vec &v, const number_t& fill=1) { // погружение вектора vec ret = vec:: fill (fill); for (size_t i=Dim; i--; ret[i]=v[i]); return ret; } 2.7 Операция проектирования вектора Эта операция наоборот, из вектора большей размерности делает вектор меньшей размерности, лишние координаты при этом просто отбрасываются: template vec proj (const vec &v) { //проекция вектора vec ret; for (size_t i=len; i--; ret[i]=v[i]); return ret; } 2.8 Переопределение оператора << для вывода векторов в поток ostream Для отладки потребовалось реализовать вывод наших векторов на терминал. Для этого мы доопределили оператор template std: ostream& operator<<(std::ostream& out,const vec& v) { out<<"{ "; for (size_t i=0; i

© Habrahabr.ru