Я убеждён, что хороший программист получается только из того, кто кодит дома в своё удовольствие, а не только просиживает штаны на парах в университете. В нашем университете программистов учат на бесконечной череде всяких библиотечных каталогов и прочей скукоте. Брр. Моя цель — показать примеры проектов, которые интересно программировать. Это замкнутый круг: если интересно делать проект, то человек проводит над ним немало времени, набирается опыта, и видит вокруг ещё больше интересного (оно же стало доступнее!), и снова погружается в новый проект. Это называется проектное обучение, вокруг сплошной профит.
Простыня получилась длинная, поэтому я разбил текст на две части:
- Часть первая: отрисовка стен
- Часть вторая: населяем наш мир + оконный интерфейс
Выполнение кода из моего репозитория выглядит вот так:
Это не законченная игра, но только заготовка для студентов. Пример законченной игры, написанной двумя первокурсниками, смотрите во второй части.
Получается, я совсем чуточку вас обманул, я не расскажу как сделать полную игру за одни выходные. Я сделал только 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!) в один клик прямо из браузера.
По этой ссылке gitpod создаст для вас виртуальную машину, запустит VS Code, и откроет терминал на удалённой машине. В истории команд терминала (ткните в консоль и нажмите стрелку вверх) уже полный набор команд, который позволяют скомпилировать код, его запустить и открыть результирующую картинку.
Итак, что нужно понять из этого кода. Первое, цвета я храню в четырёхбайтном целочисленном типе uint32_t. Каждый байт — это компонента R, G, B или A. функции pack_color() и unpack_color() позволяют добираться до индивидуальных компонент каждого цвета.
Второе, двумерную картинку я храню в обычном одномерном массиве. Чтобы добраться до пикселя с координатами (x,y) я не пишу image[x][y], но пишу image[x + y*width]. Если этот способ упаковки двумерной информации в одномерный массив для вас нов, то прямо сейчас возьмите ручку и разберитесь с ним. У меня лично этот этап даже не доходит до головного мозга, обрабатывается прямо в спинном. Трёх- и более -мерные массивы можно упаковать точно так же, но мы выше двух компонент не поднимемся.
Дальше я простым двойным циклом пробегаю мою картинку, заполняю её градиентом, и сохраняю на диск в формате .ppm.
Этап 2: рисуем карту уровня
Нам нужна карта нашего мира. На этом этапе я хочу всего лишь определить структуру данных и нарисовать карту на экране. Примерно так оно должно выглядеть:
Внесённые изменения можно посмотреть тут. Там всё просто: я захардкодил карту в одномерный массив символов, определил функцию отрисовки прямоугольника, да прошёлся по карте, отрисовав каждую клеточку.
Напоминаю, что вот эта кнопка даст запустить код прямо на этом этапе:
Этап 3: добавляем игрока
Что нам нужно, чтобы уметь нарисовать игрока на карте? GPS координат достаточно :)
Добавляем две переменные x и y, и отрисовываем игрока в соответствующем месте:
Внесённые изменения можно посмотреть тут. Про гитпод больше напоминать не буду :)
Этап 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 даёт расстояние до препятствия! Чем не лазерный дальномер?
Внесённые изменения можно посмотреть тут.
Этап 5: сектор обзора
Один луч это прекрасно, но всё же наши глаза видят целый сектор. Давайте назовём угол обзора fov (field of view):
И выпустим 512 лучей (кстати, почему 512?), плавно заметая весь сектор обзора:
Внесённые изменения можно посмотреть тут.
Этап 6: 3Д!
А теперь ключевой момент. Для каждого из 512 лучей мы получили расстояние до ближайшего препятствия, так? А теперь давайте сделаем вторую картинку шириной (спойлер) 512 пикселей; в которой мы для каждого луча будем рисовать один вертикальный отрезок, причём высота отрезка обратно пропорциональна расстоянию до препятствия:
Ещё раз, это ключевой момент создания иллюзии 3Д, убедитесь, что вы понимаете, о чём идёт речь. Рисуя вертикальные отрезки, по факту, мы рисуем частокол, где высота каждого кола тем меньше, чем дальше он от нас находится:
Внесённые изменения можно посмотреть тут.
Этап 7: первая анимация
На этом этапе мы впервые рисуем что-то динамическое (я просто скидываю на диск 360 картинок). Всё тривиально: я изменяю player_a, отрисовываю картинку, сохраняю, изменяю player_a, отрисовываю, сохраняю. Чтобы было чуть веселее, я каждому типу клетки в нашей карте присвоил случайное значение цвета.
Внесённые изменения можно посмотреть тут.
Этап 8: коррекция «рыбьего глаза»
Вы обратили внимание, какой отличный эффект «рыбьего глаза» у нас получается, когда мы смотрим на стенку вблизи? Примерно вот так оно выглядит:
Почему? Да очень просто. Вот мы смотрим на стенку:
Для отрисовки нашей стены мы заметаем фиолетовым лучом наш синий сектор обзора. Возьмём конкретное значение направления луча, как на этой картинке. Длина оранжевого отрезка явно меньше длины фиолетового. Поскольку для определения высоты каждого вертикального отрезка, что мы рисуем на экране, мы делим на расстояние до преграды, рыбий глаз вполне закономерен.
Скорректировать это искажение совсем несложно, посмотрите, как это делается. Пожалуйста, убедитесь, что вы понимаете, откуда там взялся косинус. Нарисовать схему на листочке сильно помогает.
Этап 9: загрузка файла текстур
Настало время разбираться с текстурами. Мне лениво самостоятельно писать загрузчик изображений, поэтому я взял прекрасную библиотеку stb. Я подготовил файл с текстурами для стен, все текстуры квадратные и упакованы в изображение по горизонтали:
На этом этапе я просто гружу текстуры в память. Чтобы проверить работоспособность написанного кода, просто рисую как есть текстуру с индексом 5 в левом верхнем углу экрана:
Внесённые изменения можно посмотреть тут.
Этап 10: рудиментарное использование текстур
Теперь я выкидываю случайно сгенерированные цвета и подкрашиваю мои стены, взяв левый верхний пиксель из соответствующей текстуры:
Внесённые изменения можно посмотреть тут.
Этап 11: текстурируем стены по-настоящему
А вот теперь настал долгожданный момент, когда мы наконец-то увидим кирпичные стены:
Основная идея очень простая: вот мы скользим вдоль текущего луча и останавливаемся в точке x,y. Давайте предположим, что мы остановились на «горизонтальной» стене, тогда y почти целочисленнен (не совсем, т.к. наш способ движения вдоль луча вносит небольшую ошибку). Давайте возьмём дробную часть от x и назовём её hitx. Дробная часть меньше единицы, следовательно, если мы умножим hitx на размер текстуры (у меня 64), то это нам даст столбец текстуры, который нужно нарисовать в этом месте. Осталось его растянуть до нужного размера и дело в шляпе:
В общем, идея крайне примитивная, но требует аккуратного исполнения, так как у нас есть ещё и «вертикальные» стены (те, у которых hitx будет близок к нулю [x целочисленный]). Для них столбец текстуры определяется hity, дробной частью от y. Внесённые изменения можно посмотреть тут.
Этап 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);
}
Ну а вот результат:
Внесённые изменения можно посмотреть тут.
Продолжение следует… незамедлительно
На этой оптимистичной ноте я заканчиваю текущую половину моей простыни, вторая половина доступна тут. В ней мы добавим монстров и слинкуемся с SDL2, чтобы можно было погулять в нашем виртуальном мире.
Комментарии (43)
Dikoy
11.02.2019 02:58Можно же написать правильно, и написать нормальный коммент. Особенно, когда речь идёт об обучении.
Можно даже ссылку в комменте оставить как в этой статье. Программисты умные, смогут скопировать в браузер.
А приучать писать тупо и не давать даже узнать о таких хаках, ИМХО, не верно.haqreu Автор
11.02.2019 08:22Хаки — это прекрасно, и никуда они не убегут. Но всему своё время. Конкретно этот текст рассчитан на тех, кто c++ впервые в жизни увидел. Ни к чему им в этот момент 0x5f3759df.
tbl
11.02.2019 09:36+1Прочитав абзац «Продолжение следует… незамедлительно», вспомнил небезызвестную инструкцию: всю статью учимся рисовать 2 овала, и в последнем кратко: «Дорисовываем сову».
haqreu Автор
11.02.2019 09:45Гхм. Я не очень понял, в каком месте я предлагаю дорисовать сову. Ведь все коммиты в обеих статьях примерно одинакового размера? Куда там сову упрятать, в пять сотен строк кода? :)
Или вы просто так вспомнили, безотносительно к данному тексту?tbl
11.02.2019 11:24Извиняюсь, по ссылке не сходил, т.к. по тексту выглядело, что это ссылка на коммит в репу, в котором добавляются монстры, геймплей и наводится лоск. А это, оказывается, ссылка на вторую часть статьи.
haqreu Автор
11.02.2019 12:06Уф, отлично. А то я уже испугался, что опять несбалансированный текст написал!
Prost971
11.02.2019 12:06+1Здорово! Всё просто, понятно и наглядно. Давно хотел написать что-то подобное для саморазвития. Попробую заняться на выходных)
ganqqwerty
11.02.2019 13:17Кгм, «этот текст предназначен для тех, кто только осваивает программирование», как это толсто. Вы ведь понимаете, что первая же функция в вашем тексте требует ну ооооочень долгого разъяснения? Мне после пяти лет плюсов читать со скрипом можно, но кому-то кто только начал — это очень сложно.
Bookvarenko
11.02.2019 13:58Да, вкурить в колдовство rgba совсем начинающему непросто. Но можно же спросить в комментариях если что-то совсем непонятно.
ganqqwerty
11.02.2019 14:00Да, правда, попробую более конструктивно: почему бы не опустить упаковку/распаковку цветов (ну или показать с картинками, что в ней делают сдвиги), запихивание картинки в одномерный массив, использование любых типов кроме integer, да и рефакторинг тоже? Будет медленнее, плохо расширяемо и памяти лишней будет жрать, но кому какая разница? Зато можно сосредоточиться на геометрии.
Еще я не понял, что такое вот эти магические значения:
ofs << "P6\n" << w << " " << h << "\n255\n";
В этом коммите, что вообще происходит в функцииtexture_column
? Почему колонка, где нам рассказывали про колонки? Что-то умножается, делится на два, в итоге получается колонка. Пока не ясно.
В последнем коммите, что это за чиселки на 97 строке? Почему они такие, а не любые другие?haqreu Автор
11.02.2019 14:46Давайте по пунктам.
0) скажите, пожалуйста, в какой структуре данных вы предлагаете хранить изображение?
1)
Еще я не понял, что такое вот эти магические значения:
ofs << «P6\n» << w << " " << h << "\n255\n";
посмотрите, пожалуйста, сами что это за значения. Я в статье явно указал формат изображения, найти в вики его описание должно быть несложно.
2)
В этом коммите, что вообще происходит в функции texture_column? Почему колонка, где нам рассказывали про колонки? Что-то умножается, делится на два, в итоге получается колонка. Пока не ясно.
Этап 11 довольно подробно рассказывает про колонки. А в функции texture_column на два не делится ничего.
3)В последнем коммите, что это за чиселки на 97 строке? Почему они такие, а не любые другие?
Ссылку в студию, а то я не понимаю, про какой файл вы говорите.ganqqwerty
11.02.2019 18:050) в двумерном массиве пикселей, в котором каждая ячейка — это три числа
1) можно и посмотреть. А можно не смотреть и написать прямо тут, если вы и правда для новичков пишете.
2) у меня все равно не получилось связать рассказ с кодом
3) вроде разобрался, это просто данные, а не какое-то преобразованиеhaqreu Автор
11.02.2019 18:15Покажите, пожалуйста, объявление двумерного массива? Очень интересно посмотреть.
ganqqwerty
11.02.2019 18:41type arrayName [ height ][ width ]; разве нет?
haqreu Автор
11.02.2019 18:45Окей, давайте считать. Вы мне объявляете массив в стеке. Положим для простоты картинку 1000x1000, это один миллион пикселей. Каждый пиксель четыре байта. В сухом остатке мы кладём в стек четыре мегабайта? У вас какой размер стека?
ganqqwerty
11.02.2019 18:53а зачем 1000 на 1000, а не 200 на 200 или даже 20 на 20? Ну и на стеке совсем не обязательно, можно ж и в куче. А каждый пиксель — это не четыре байта, а 12, если мы делаем через структуру с тремя полями.
haqreu Автор
11.02.2019 18:57Я правильно понял, что вы мне только что предложили сделать игру с разрешением экрана 20 на 20?
Покажите мне, пожалуйста, объявление двумерного массива в куче? (кстати да, в куче можно объявить именно двумерный массив, а не массив указателей).ganqqwerty
11.02.2019 19:00А мы какую задачу решаем? если обучения нубов, то уже на двух-трех сотнях результат будет вполне визуализируем. Нет, я думал именно про массив указателей.
haqreu Автор
11.02.2019 19:17+1Мы решаем задачу доставления удовольствия студенту, которая будет стимулировать обучение. На двух сотнях пикселей никто играть не будет, не те времена.
Массив указателей нужно а) инициализировать б) суметь скормить SDL и в) отдать потом обратно операционке. Это сразу десятка два-три строчек кода, которые трудно понимать, которые фрагментируют память и которые практически не решают никакой задачи.
Короче, я лучшего решения не нашёл, если вы найдёте, присылайте пулл реквест.
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
Откуда взялся сегфолт?ganqqwerty
11.02.2019 18:57Я ж не спорю, что в реальной жизни оно упадет и делать так нельзя, я про очень правильный педагогический принцип «Кармак пишет 0x5f3759df, я же пишу 1/sqrt(x)», примененный ко всему, что может хоть как-то отвлекать от геометрической логики.
haqreu Автор
11.02.2019 19:15+1Я только за, но реально, упаковка в одномерный массив — это вообще не проблема, тем более, что оно спрятано внутри set_pixel(x, y).
Vov4ikpa
11.02.2019 21:22Пожалуйста, убедитесь, что вы понимаете, откуда там взялся косинус.
Можно пояснение? )haqreu Автор
11.02.2019 21:23давайте пояснение :)
Vov4ikpa
11.02.2019 22:07Опираясь на ваш рисунок, высота прямоугольника будет максимальной при совпадении фиолетового и оранжевого отрезков, в данном случае косинус разницы углов будет нулевым.
В любом другом другом состоянии фиолетового луча разница углов будет компенсировать расстояние от текстуру тем самым сглаживая эффект рыбьего глаза.haqreu Автор
11.02.2019 22:12Ну собственно да. Косинус — это прилежащий катет, делённый на гипотенузу. Если мы смотрим прямо на стенку, то катет — это оранжевый отрезок, и именно его длина нам нужна. Когда при отрисовке фиолетового луча мы помножим на косинус угла, то гипотенуза (фиолетовый луч) * катет / гипотенуза = катет. Что и требовалось доказать.
scg
12.02.2019 08:37Что-то мне текстуры и спрайты из видео напоминают. :) «Секреты программирования игр» А. Ла Мота?
akhalat
12.02.2019 09:05В комментах ко второй части пришли к такому же выводу :)
habr.com/ru/post/439720/#comment_19740388
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;
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.
Fenzales
haqreu Автор
Ох не уверен.
Fenzales
Логичнее было бы сравнивать с rsqrtss, иначе мы более точные вычисления гоняем против менее точных.
haqreu Автор
А на самом деле всё равно, все эти методы сильно отличаются от простой записи 1/sqrt(x).
akhalat
С удовольствием прочитал про 0x5f3759df, но зацепился за строчку:
Может кто-нибудь пояснить, почему это стало неопределенным поведением (при условии, что размеры типов совпадают) и в идеале дать ссылку на пункт стандарта?
haqreu Автор
Это называется type punning. Стандарт говорит следующее (6.5 Expressions paragraph 7):
akhalat
Спасибо. А это будет именно undefined (а не unspecified) behavior?
А если в uint8_t* конвертить? Примерно вот так:
haqreu Автор
akhalat
Вот здесь вас уже не понял. Т.е., это понятно, что сама реализация floating-point не регулируется стандартом, но вопрос был в том, как эти байты в приницпе получить, не нарвавшись на UB. Но немного погуглив я нашёл, что на конверсию в char это не распространяется, а uint8_t в любом случае будет char-ом. Ещё раз спасибо за термин «type punning» и направление куда искать.
haqreu Автор
Я имел в виду лишь то, что при отсутствии регулирования стандартом, мы получаем некроссплатформенность кода как самый минимум. Байты получить можно, а вот что внутри этих байтов будет — сюрприз.