Этот текст предназначен для тех, кто только осваивает программирование. Основная идея в том, чтобы показать этап за этапом, как можно самостоятельно сделать игру a la Wolfenstein 3D. Внимание, я совершенно не собираюсь соревноваться с Кармаком, он гений и его код прекрасен. Я же целюсь совсем в другое место: я использую огромную вычислительную мощность современных компьютеров для того, чтобы студенты могли создавать забавные проекты за несколько дней, не погрязая в дебрях оптимизации. Я специально пишу медленный код, так как он существенно короче и просто понятнее. Кармак пишет 0x5f3759df, я же пишу 1/sqrt(x). Мы преследуем разные цели.

Я убеждён, что хороший программист получается только из того, кто кодит дома в своё удовольствие, а не только просиживает штаны на парах в университете. В нашем университете программистов учат на бесконечной череде всяких библиотечных каталогов и прочей скукоте. Брр. Моя цель — показать примеры проектов, которые интересно программировать. Это замкнутый круг: если интересно делать проект, то человек проводит над ним немало времени, набирается опыта, и видит вокруг ещё больше интересного (оно же стало доступнее!), и снова погружается в новый проект. Это называется проектное обучение, вокруг сплошной профит.

Простыня получилась длинная, поэтому я разбил текст на две части:


Выполнение кода из моего репозитория выглядит вот так:


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

Получается, я совсем чуточку вас обманул, я не расскажу как сделать полную игру за одни выходные. Я сделал только 3Д движок. Монстры у меня не бегают, да и главный персонаж не стреляет. Но, по крайней мере, этот движок я написал за одну субботу, можете проверить историю коммитов. В принципе, воскресенья вполне достаточно, чтобы сделать нечто играбельное, то есть, в одни выходные можно уложиться.

На момент написания этого текста репозиторий содержит 486 строк кода:

haqreu@daffodil:~/tinyraycaster$ cat *.cpp *.h | wc -l
486

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

Итак, я разбиваю весь код на этапы, стартуя с голого компилятора C++. Как и в предыдущих моих статьях по графике (тыц, тыц, тыц), я придерживаюсь правила «один этап = один коммит», так как github позволяет очень удобно просматривать историю изменений кода.

Этап 1: сохранение картинки на диск


Итак, поехали. До оконного интерфейса нам ещё очень далеко, для начала мы будем просто сохранять картинки на диск. Итого, нам нужно уметь хранить картинку в памяти компьютера и сохранять её на диск в формате, который поймёт какая-нибудь сторонняя программа. Я хочу получить вот такой файл:



Вот так выглядит полный C++ код, который рисует то, что нам нужно:

#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <cassert>

uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255) {
    return (a<<24) + (b<<16) + (g<<8) + r;
}

void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) {
    r = (color >>  0) & 255;
    g = (color >>  8) & 255;
    b = (color >> 16) & 255;
    a = (color >> 24) & 255;
}

void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h) {
    assert(image.size() == w*h);
    std::ofstream ofs(filename);
    ofs << "P6\n" << w << " " << h << "\n255\n";
    for (size_t i = 0; i < h*w; ++i) {
        uint8_t r, g, b, a;
        unpack_color(image[i], r, g, b, a);
        ofs << static_cast<char>(r) << static_cast<char>(g) << static_cast<char>(b);
    }
    ofs.close();
}

int main() {
    const size_t win_w = 512; // image width
    const size_t win_h = 512; // image height
    std::vector<uint32_t> framebuffer(win_w*win_h, 255); // the image itself, initialized to red

    for (size_t j = 0; j<win_h; j++) { // fill the screen with color gradients
        for (size_t i = 0; i<win_w; i++) {
            uint8_t r = 255*j/float(win_h); // varies between 0 and 255 as j sweeps the vertical
            uint8_t g = 255*i/float(win_w); // varies between 0 and 255 as i sweeps the horizontal
            uint8_t b = 0;
            framebuffer[i+j*win_w] = pack_color(r, g, b);
        }
    }

    drop_ppm_image("./out.ppm", framebuffer, win_w, win_h);

    return 0;
}

Если у вас под рукой нет компилятора, то это не беда, при наличии учётной записи на гитхабе этот код можно посмотреть, отредактировать и запустить (sic!) в один клик прямо из браузера.

Open in Gitpod

По этой ссылке gitpod создаст для вас виртуальную машину, запустит VS Code, и откроет терминал на удалённой машине. В истории команд терминала (ткните в консоль и нажмите стрелку вверх) уже полный набор команд, который позволяют скомпилировать код, его запустить и открыть результирующую картинку.

Итак, что нужно понять из этого кода. Первое, цвета я храню в четырёхбайтном целочисленном типе uint32_t. Каждый байт — это компонента R, G, B или A. функции pack_color() и unpack_color() позволяют добираться до индивидуальных компонент каждого цвета.

Второе, двумерную картинку я храню в обычном одномерном массиве. Чтобы добраться до пикселя с координатами (x,y) я не пишу image[x][y], но пишу image[x + y*width]. Если этот способ упаковки двумерной информации в одномерный массив для вас нов, то прямо сейчас возьмите ручку и разберитесь с ним. У меня лично этот этап даже не доходит до головного мозга, обрабатывается прямо в спинном. Трёх- и более -мерные массивы можно упаковать точно так же, но мы выше двух компонент не поднимемся.

Дальше я простым двойным циклом пробегаю мою картинку, заполняю её градиентом, и сохраняю на диск в формате .ppm.



Этап 2: рисуем карту уровня


Нам нужна карта нашего мира. На этом этапе я хочу всего лишь определить структуру данных и нарисовать карту на экране. Примерно так оно должно выглядеть:



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

Напоминаю, что вот эта кнопка даст запустить код прямо на этом этапе:

Open in Gitpod



Этап 3: добавляем игрока


Что нам нужно, чтобы уметь нарисовать игрока на карте? GPS координат достаточно :)



Добавляем две переменные x и y, и отрисовываем игрока в соответствующем месте:



Внесённые изменения можно посмотреть тут. Про гитпод больше напоминать не буду :)

Open in Gitpod



Этап 4: виртуальный дальномер aka трассировка первого луча


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



А теперь я хочу иметь возможность скользить вдоль оранжевого луча. Как это делать? Предельно просто. Давайте рассмотрим зелёный прямоугольный треугольник. Мы знаем, что cos(player_a) = a/c, и что sin(player_a) = b/c.



Что будет, если я произвольно возьму значение c (положительное) и посчитаю x = player_x + c*cos(player_a) и y = player_y + c*sin(player_a)? Мы окажемся в фиолетовой точке; варьируя параметр c от нуля до бесконечности, мы можем заставить скользить эту фиолетовую точку вдоль нашего оранжевого луча, причём c — это расстояние от (x,y) до (player_x, player_y)!

Сердце нашего графического движка — это вот такой цикл:

   float c = 0;   
    for (; c<20; c+=.05) {
        float x = player_x + c*cos(player_a);
        float y = player_y + c*sin(player_a);
        if (map[int(x)+int(y)*map_w]!=' ') break;
   }

Мы двигаем точку (x,y) вдоль луча, если она натыкается на препятствие на карте, то прерываем цикл, и переменная c даёт расстояние до препятствия! Чем не лазерный дальномер?



Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 5: сектор обзора


Один луч это прекрасно, но всё же наши глаза видят целый сектор. Давайте назовём угол обзора fov (field of view):



И выпустим 512 лучей (кстати, почему 512?), плавно заметая весь сектор обзора:


Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 6: 3Д!


А теперь ключевой момент. Для каждого из 512 лучей мы получили расстояние до ближайшего препятствия, так? А теперь давайте сделаем вторую картинку шириной (спойлер) 512 пикселей; в которой мы для каждого луча будем рисовать один вертикальный отрезок, причём высота отрезка обратно пропорциональна расстоянию до препятствия:



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



Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 7: первая анимация


На этом этапе мы впервые рисуем что-то динамическое (я просто скидываю на диск 360 картинок). Всё тривиально: я изменяю player_a, отрисовываю картинку, сохраняю, изменяю player_a, отрисовываю, сохраняю. Чтобы было чуть веселее, я каждому типу клетки в нашей карте присвоил случайное значение цвета.


Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 8: коррекция «рыбьего глаза»


Вы обратили внимание, какой отличный эффект «рыбьего глаза» у нас получается, когда мы смотрим на стенку вблизи? Примерно вот так оно выглядит:



Почему? Да очень просто. Вот мы смотрим на стенку:



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

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



Open in Gitpod



Этап 9: загрузка файла текстур


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



На этом этапе я просто гружу текстуры в память. Чтобы проверить работоспособность написанного кода, просто рисую как есть текстуру с индексом 5 в левом верхнем углу экрана:


Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 10: рудиментарное использование текстур


Теперь я выкидываю случайно сгенерированные цвета и подкрашиваю мои стены, взяв левый верхний пиксель из соответствующей текстуры:


Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 11: текстурируем стены по-настоящему


А вот теперь настал долгожданный момент, когда мы наконец-то увидим кирпичные стены:



Основная идея очень простая: вот мы скользим вдоль текущего луча и останавливаемся в точке x,y. Давайте предположим, что мы остановились на «горизонтальной» стене, тогда y почти целочисленнен (не совсем, т.к. наш способ движения вдоль луча вносит небольшую ошибку). Давайте возьмём дробную часть от x и назовём её hitx. Дробная часть меньше единицы, следовательно, если мы умножим hitx на размер текстуры (у меня 64), то это нам даст столбец текстуры, который нужно нарисовать в этом месте. Осталось его растянуть до нужного размера и дело в шляпе:



В общем, идея крайне примитивная, но требует аккуратного исполнения, так как у нас есть ещё и «вертикальные» стены (те, у которых hitx будет близок к нулю [x целочисленный]). Для них столбец текстуры определяется hity, дробной частью от y. Внесённые изменения можно посмотреть тут.

Open in Gitpod



Этап 12: время рефакторить!


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

    for (size_t frame=0; frame<360; frame++) {
        std::stringstream ss;
        ss << std::setfill('0') << std::setw(5) << frame << ".ppm";
        player.a += 2*M_PI/360;

        render(fb, map, player, tex_walls);
        drop_ppm_image(ss.str(), fb.img, fb.w, fb.h);
    }

Ну а вот результат:


Внесённые изменения можно посмотреть тут.

Open in Gitpod

Продолжение следует… незамедлительно


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

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


  1. Fenzales
    10.02.2019 21:23

    Кармак пишет 0x5f3759df, я же пишу 1/sqrt(x)
    И правильно, на современных процессорах и компиляторах такой «хак» уже не нужен.


    1. haqreu Автор
      10.02.2019 21:29
      +2

      1. Fenzales
        11.02.2019 10:27
        +1

        Логичнее было бы сравнивать с rsqrtss, иначе мы более точные вычисления гоняем против менее точных.


        1. haqreu Автор
          11.02.2019 10:55

          А на самом деле всё равно, все эти методы сильно отличаются от простой записи 1/sqrt(x).


    1. akhalat
      11.02.2019 00:12

      С удовольствием прочитал про 0x5f3759df, но зацепился за строчку:

      In terms of C standards, reinterpreting a floating point value as an integer by dereferencing a casted pointer to it is considered undefined behavior

      Может кто-нибудь пояснить, почему это стало неопределенным поведением (при условии, что размеры типов совпадают) и в идеале дать ссылку на пункт стандарта?


      1. haqreu Автор
        11.02.2019 00:24

        Это называется type punning. Стандарт говорит следующее (6.5 Expressions paragraph 7):

        An object shall have its stored value accessed only by an lvalue expression that has one of the following types — a type compatible with the effective type of the object,


        1. akhalat
          11.02.2019 00:42

          Спасибо. А это будет именно undefined (а не unspecified) behavior?
          А если в uint8_t* конвертить? Примерно вот так:

          double f;
          uint8_t *p;
          p=(uint8_t*)&f;
          


          1. haqreu Автор
            11.02.2019 00:48

            § 6.7.1.8 There are three floating-point types: float, double, and long double. The type double provides at least as much precision as float, and the type long double provides at least as much precision as double. The set of values of the type float is a subset of the set of values of the type double; the set of values of the type double is a subset of the set of values of the type long double. The value representation of floating-point types is implementation-defined.


            1. akhalat
              11.02.2019 02:09

              Вот здесь вас уже не понял. Т.е., это понятно, что сама реализация floating-point не регулируется стандартом, но вопрос был в том, как эти байты в приницпе получить, не нарвавшись на UB. Но немного погуглив я нашёл, что на конверсию в char это не распространяется, а uint8_t в любом случае будет char-ом. Ещё раз спасибо за термин «type punning» и направление куда искать.


              1. haqreu Автор
                11.02.2019 02:12

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


  1. Dikoy
    11.02.2019 02:58

    Можно же написать правильно, и написать нормальный коммент. Особенно, когда речь идёт об обучении.
    Можно даже ссылку в комменте оставить как в этой статье. Программисты умные, смогут скопировать в браузер.
    А приучать писать тупо и не давать даже узнать о таких хаках, ИМХО, не верно.


    1. haqreu Автор
      11.02.2019 08:22

      Хаки — это прекрасно, и никуда они не убегут. Но всему своё время. Конкретно этот текст рассчитан на тех, кто c++ впервые в жизни увидел. Ни к чему им в этот момент 0x5f3759df.


  1. tbl
    11.02.2019 09:36
    +1

    Прочитав абзац «Продолжение следует… незамедлительно», вспомнил небезызвестную инструкцию: всю статью учимся рисовать 2 овала, и в последнем кратко: «Дорисовываем сову».


    1. haqreu Автор
      11.02.2019 09:45

      Гхм. Я не очень понял, в каком месте я предлагаю дорисовать сову. Ведь все коммиты в обеих статьях примерно одинакового размера? Куда там сову упрятать, в пять сотен строк кода? :)

      Или вы просто так вспомнили, безотносительно к данному тексту?


      1. tbl
        11.02.2019 11:24

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


        1. haqreu Автор
          11.02.2019 12:06

          Уф, отлично. А то я уже испугался, что опять несбалансированный текст написал!


  1. Prost971
    11.02.2019 12:06
    +1

    Здорово! Всё просто, понятно и наглядно. Давно хотел написать что-то подобное для саморазвития. Попробую заняться на выходных)


  1. ganqqwerty
    11.02.2019 13:17

    Кгм, «этот текст предназначен для тех, кто только осваивает программирование», как это толсто. Вы ведь понимаете, что первая же функция в вашем тексте требует ну ооооочень долгого разъяснения? Мне после пяти лет плюсов читать со скрипом можно, но кому-то кто только начал — это очень сложно.


    1. Bookvarenko
      11.02.2019 13:58

      Да, вкурить в колдовство rgba совсем начинающему непросто. Но можно же спросить в комментариях если что-то совсем непонятно.


    1. ganqqwerty
      11.02.2019 14:00

      Да, правда, попробую более конструктивно: почему бы не опустить упаковку/распаковку цветов (ну или показать с картинками, что в ней делают сдвиги), запихивание картинки в одномерный массив, использование любых типов кроме integer, да и рефакторинг тоже? Будет медленнее, плохо расширяемо и памяти лишней будет жрать, но кому какая разница? Зато можно сосредоточиться на геометрии.

      Еще я не понял, что такое вот эти магические значения:
      ofs << "P6\n" << w << " " << h << "\n255\n";

      В этом коммите, что вообще происходит в функции texture_column? Почему колонка, где нам рассказывали про колонки? Что-то умножается, делится на два, в итоге получается колонка. Пока не ясно.

      В последнем коммите, что это за чиселки на 97 строке? Почему они такие, а не любые другие?


      1. haqreu Автор
        11.02.2019 14:46

        Давайте по пунктам.
        0) скажите, пожалуйста, в какой структуре данных вы предлагаете хранить изображение?

        1)

        Еще я не понял, что такое вот эти магические значения:
        ofs << «P6\n» << w << " " << h << "\n255\n";

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

        2)
        В этом коммите, что вообще происходит в функции texture_column? Почему колонка, где нам рассказывали про колонки? Что-то умножается, делится на два, в итоге получается колонка. Пока не ясно.

        Этап 11 довольно подробно рассказывает про колонки. А в функции texture_column на два не делится ничего.

        3)
        В последнем коммите, что это за чиселки на 97 строке? Почему они такие, а не любые другие?


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


        1. ganqqwerty
          11.02.2019 18:05

          0) в двумерном массиве пикселей, в котором каждая ячейка — это три числа
          1) можно и посмотреть. А можно не смотреть и написать прямо тут, если вы и правда для новичков пишете.
          2) у меня все равно не получилось связать рассказ с кодом
          3) вроде разобрался, это просто данные, а не какое-то преобразование


          1. haqreu Автор
            11.02.2019 18:15

            Покажите, пожалуйста, объявление двумерного массива? Очень интересно посмотреть.


            1. ganqqwerty
              11.02.2019 18:41

              type arrayName [ height ][ width ]; разве нет?


              1. haqreu Автор
                11.02.2019 18:45

                Окей, давайте считать. Вы мне объявляете массив в стеке. Положим для простоты картинку 1000x1000, это один миллион пикселей. Каждый пиксель четыре байта. В сухом остатке мы кладём в стек четыре мегабайта? У вас какой размер стека?


                1. ganqqwerty
                  11.02.2019 18:53

                  а зачем 1000 на 1000, а не 200 на 200 или даже 20 на 20? Ну и на стеке совсем не обязательно, можно ж и в куче. А каждый пиксель — это не четыре байта, а 12, если мы делаем через структуру с тремя полями.


                  1. haqreu Автор
                    11.02.2019 18:57

                    Я правильно понял, что вы мне только что предложили сделать игру с разрешением экрана 20 на 20?

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


                    1. ganqqwerty
                      11.02.2019 19:00

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


                      1. haqreu Автор
                        11.02.2019 19:17
                        +1

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

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

                        Короче, я лучшего решения не нашёл, если вы найдёте, присылайте пулл реквест.


                        1. ganqqwerty
                          11.02.2019 23:23

                          а блин, правда, надо же конвертировать все равно… снимаю тогда претензию


              1. haqreu Автор
                11.02.2019 18:50

                Давайте я даже конкретный пример приведу:

                ssloy@daffodil:~/tmp$ cat test.cpp 
                #include <cstdint>
                
                int main() {
                    uint32_t array[ 1920 ][ 1680 ];
                    return array[344][563];
                }
                
                ssloy@daffodil:~/tmp$ g++ -O0 test.cpp -o test && ./test
                Segmentation fault
                


                Откуда взялся сегфолт?


                1. ganqqwerty
                  11.02.2019 18:57

                  Я ж не спорю, что в реальной жизни оно упадет и делать так нельзя, я про очень правильный педагогический принцип «Кармак пишет 0x5f3759df, я же пишу 1/sqrt(x)», примененный ко всему, что может хоть как-то отвлекать от геометрической логики.


                  1. haqreu Автор
                    11.02.2019 19:15
                    +1

                    Я только за, но реально, упаковка в одномерный массив — это вообще не проблема, тем более, что оно спрятано внутри set_pixel(x, y).


  1. ganqqwerty
    11.02.2019 18:05

    del


  1. Vov4ikpa
    11.02.2019 21:22

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

    Можно пояснение? )


    1. haqreu Автор
      11.02.2019 21:23

      давайте пояснение :)


      1. Vov4ikpa
        11.02.2019 22:07

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


        1. haqreu Автор
          11.02.2019 22:12

          Ну собственно да. Косинус — это прилежащий катет, делённый на гипотенузу. Если мы смотрим прямо на стенку, то катет — это оранжевый отрезок, и именно его длина нам нужна. Когда при отрисовке фиолетового луча мы помножим на косинус угла, то гипотенуза (фиолетовый луч) * катет / гипотенуза = катет. Что и требовалось доказать.


  1. Vov4ikpa
    11.02.2019 22:07

    del


  1. scg
    12.02.2019 08:37

    Что-то мне текстуры и спрайты из видео напоминают. :) «Секреты программирования игр» А. Ла Мота?


    1. akhalat
      12.02.2019 09:05

      В комментах ко второй части пришли к такому же выводу :)
      habr.com/ru/post/439720/#comment_19740388


  1. almalk454
    12.02.2019 09:37

    Невероятно интересные статьи Вы пишете однако. Понемногу пытаюсь разбираться.
    Однако меня интересует вопрос: Почему в функции unpack_color происходит деление на 255?

    Кусь
        r = (color >>  0) & 255;
        g = (color >>  8) & 255;
        b = (color >> 16) & 255;
        a = (color >> 24) & 255;


    1. haqreu Автор
      12.02.2019 09:56

      Смотрите, вот я хочу иметь четыре канала r,g,b и a, они все варьируются между 0 и 255 включительно (беззнаковый байт). Но когда я хочу присвоить цвет одного пикселя, я не хочу делать четыре присваивания, я хочу одно. Вопрос почему оставим за рамками текущего обсуждения.

      Поэтому я беру беззнаковый тип, который имеет размер четыре байта, uint32_t. Теперь встаёт вопрос, как конвертировать одно в другое. В одну сторону очень просто:

      uint32_t color = r + 256*g + 256*256*b + 256*256*256*a


      Почему так? Попробуйте записать каждую компоненту в двоичном виде, и вспомните, что умножение на 256 — это побитовый сдвиг влево на 8 бит. А вот как обратно? Да точно так же. Давайте возьмём чистый зелёный цвет, чему будет равно значение color?

      uint32_t color = 0 + 256*255 + 256*256*0 + 256*256*256*0


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

      
      uint8_t r = color % 256;
      uint8_t g = (color/256) % 256;
      uint8_t b = (color/(256*256)) % 256;
      uint8_t a = (color/(256*256*256)) % 256;
      


      Ваш вопрос, зачем мне остаток от деления на 256. Попробуйте сами на бумажке: мой цвет — 65280. Проще всего это видно, если записать в двоичной системе. 65280 в двоичной системе как выглядит? Это тридцать два бита (четыре группы по восемь бит, каждая группа — наш цветовой канал):

      
      00000000 00000000 11111111 00000000
          a       b        g        r
      


      Чтобы получить красную компоненту, мне нужно взять число 00000000 00000000 11111111 00000000 и оттуда вытащить младшие 8 бит. Я сделаю побитовое сравнение с 255 (в двоичной системе это восемь битов единичек):

      
      00000000 00000000 11111111 00000000  &   // цвет, 65280 в десятичной
      00000000 00000000 00000000 11111111      // маска, 255 в десятичной
      


      Побитовое И мне даст просто 0, что и требовалось доказать. Оно откинет старшие, ненужные мне биты. Да, забыл. Остаток от деления на (2 в степени n) — это то же самое, что и побитовое И с числом ((2 в степени n)-1). А обычное целочисленное деление на (2 в степени n) — это побитовый сдвиг вправо на n бит.

      Теперь хочу вытащить зелёный канал. Я сначала сдвину вправо на восемь бит (поделю на 256), а затем сделаю побитовое И с числом 255, чтобы оставшиеся синий и альфа мне не мешали:

      
       00000000 00000000 11111111 00000000      // цвет, 65280 в десятичной
      (00000000 00000000 11111111 00000000>>8)  // цвет, поделенный на 256
       00000000 00000000 00000000 11111111      // результат деления
       00000000 00000000 00000000 11111111      // маска, 255 в десятичной
      


      Ну и получим нашу зелёную компоненту как (65280/256)%256 = 255.