В выпусках про OpenGL возникла проблема, когда загруженная из файла текстура получалась перевёрнутой вниз головой.
Как оказалось, это вполне стандартное поведение, и о нём даже упоминают на обучающих ресурсах. Картинку нужно перевернуть самостоятельно перед тем, как назначить её текстурой.
Задача переворота шире и универсальнее, чем OpenGL, поэтому я вынес её в отдельную тему.
Картинка это двумерный массив, но её также можно представить как массив строк:
Чтобы перевернуть картинку, надо поменять местами именно целые строки:
Можно заметить, что меняются пары строк, которые расположены на равном расстоянии от середины. Если в картинке нечётное количество строк, то посередине остаётся строка, которая ни с кем не меняется.
Устройство SDL-поверхности
Мы пользуемся кроссплатформенной графической библиотекой SDL2. Загруженная из файла картинка становится поверхностью (тип SDL_Surface), от которой мы можем получить следующие свойства:
- pixels – указатель на массив пикселов картинки, то есть это и есть тот самый массив, о котором речь шла выше
- w – ширина картинки в пикселах
- h – высота картинки в пикселах
- BytesPerPixel – размер пиксела в байтах
- pitch – длина строки в байтах
Обратите внимание, что если умножить размер пиксела в байтах (BytesPerPixel) на ширину картинки (w), мы по идее должны получить длину строки в байтах (pitch). Но это не всегда так. Длина строки может дополнительно выравниваться до границы 4-х байт, поэтому, если мы собираемся копировать строки, руководствоваться надо именно значением pitch.
Общий алгоритм
Организуем цикл движения по строкам. В каждом шаге цикла мы должны попадать на начало следующей строки. Так как массив это на самом деле просто непрерывный отрезок памяти, строки в нём понятие условное. И где начинается каждая строка, нам нужно вычислять самостоятельно.
Очевидно, самая первая строка начинается там же, где начинается сам массив. У этой строки смещение 0 от начала массива. Следующая строка начинается через pitch байт от начала предыдущей. Значит, у неё смещение pitch от начала массива. У следующей строки смещение 2*pitch, затем 3*pitch, и т.д.
Итак, смещения строк у нас есть. Для каждой строки нужно также знать смещение парной строки, с которой она будет меняться. Для этого надо в том же цикле отсчитывать смещения, начиная с конца.
Для строки со смещением 0 парной будет строка со смещением (h - 1) * pitch, для строки со смещением pitch парной будет (h - 2) * pitch, и т.д.
Для повторений в цикле заведём переменную y. Тогда пара смещений строк будет получаться так:
pixels[y * pitch]
pixels[(h - 1 - y) * pitch]
Сама переменная y должна меняться от 0 до h/2. То есть цикл будет доходить только до середины картинки. Парные смещения строк сойдутся там же.
Теперь нужен ещё один цикл, чтобы пройтись уже по каждому байту внутри строки.
Двигаясь по x от 0 до pitch (т.е. это дополнительное смещение относительно смещения по y), мы взаимно меняем байты в двух строках.
Собственно задача решена, и это самое базовое решение.
Что можно улучшить?
Любые улучшения непринципиальны, так как данная перестановка требуется всего один раз и не будет влиять на производительность. Кроме того, оптимизирующие компиляторы настолько вещь в себе, что в стремлении сделать лучше можно иногда сделать только хуже.
Но чисто ради упражнения можно кое-что изменить.
Во-первых, можно избавиться от побайтного копирования. Мы можем пересылать данные сразу по 4 байта, используя тип long int:
Обратим внимание, что до этого pixels был байтовым указателем, а теперь он 4-байтовый. Данные не поменялись, поменялся только тип указателя на них. Если раньше размер элемента по указателю был 1 байт, то теперь он равен 4 байта. И все смещения вида pixels[x] теперь автоматически умножаются на 4. Соответственно pitch в байтах мы делим на 4, чтобы получить размер строки в 4-байтовых элементах.
Наконец, можно использовать функцию memcpy():
С её помощью мы копируем целую строку за один раз, избегая внутреннего цикла. Однако внутри себя memcpy() скорее всего имеет тот же самый цикл. В любом случае, мы полагаемся на эту библиотечную функцию в надежде на то, что она каким-то образом оптимизирована.
Правда, для копирования строки целиком требуется временный буфер памяти размером в одну строку. Мы его динамически выделяем с помощью malloc(), а затем освобождаем с помощью free().
На что ещё тут можно обратить внимание:
Мы не используем привычный цикл по y и не вычисляем в нём смещения строк. Мы заранее приготовили два указателя: для первой и последней строки. Затем, после копирования, к указателю первой строки прибавляется pitch (то есть получаем следующую строку), а из указателя последней опять же вычитается pitch (то есть получаем предыдущую). Эти два указателя сходятся друг к другу, и мы повторяем итерации до тех пор, пока верхний указатель меньше нижнего.