Pull to refresh

Comments 65

Кармак пишет 0x5f3759df, я же пишу 1/sqrt(x)
И правильно, на современных процессорах и компиляторах такой «хак» уже не нужен.
Логичнее было бы сравнивать с rsqrtss, иначе мы более точные вычисления гоняем против менее точных.
А на самом деле всё равно, все эти методы сильно отличаются от простой записи 1/sqrt(x).
С удовольствием прочитал про 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

Может кто-нибудь пояснить, почему это стало неопределенным поведением (при условии, что размеры типов совпадают) и в идеале дать ссылку на пункт стандарта?
Это называется 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,
Спасибо. А это будет именно undefined (а не unspecified) behavior?
А если в uint8_t* конвертить? Примерно вот так:
double f;
uint8_t *p;
p=(uint8_t*)&f;
§ 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.
Вот здесь вас уже не понял. Т.е., это понятно, что сама реализация floating-point не регулируется стандартом, но вопрос был в том, как эти байты в приницпе получить, не нарвавшись на UB. Но немного погуглив я нашёл, что на конверсию в char это не распространяется, а uint8_t в любом случае будет char-ом. Ещё раз спасибо за термин «type punning» и направление куда искать.
Я имел в виду лишь то, что при отсутствии регулирования стандартом, мы получаем некроссплатформенность кода как самый минимум. Байты получить можно, а вот что внутри этих байтов будет — сюрприз.
Можно же написать правильно, и написать нормальный коммент. Особенно, когда речь идёт об обучении.
Можно даже ссылку в комменте оставить как в этой статье. Программисты умные, смогут скопировать в браузер.
А приучать писать тупо и не давать даже узнать о таких хаках, ИМХО, не верно.
Хаки — это прекрасно, и никуда они не убегут. Но всему своё время. Конкретно этот текст рассчитан на тех, кто c++ впервые в жизни увидел. Ни к чему им в этот момент 0x5f3759df.
Прочитав абзац «Продолжение следует… незамедлительно», вспомнил небезызвестную инструкцию: всю статью учимся рисовать 2 овала, и в последнем кратко: «Дорисовываем сову».
Гхм. Я не очень понял, в каком месте я предлагаю дорисовать сову. Ведь все коммиты в обеих статьях примерно одинакового размера? Куда там сову упрятать, в пять сотен строк кода? :)

Или вы просто так вспомнили, безотносительно к данному тексту?
Извиняюсь, по ссылке не сходил, т.к. по тексту выглядело, что это ссылка на коммит в репу, в котором добавляются монстры, геймплей и наводится лоск. А это, оказывается, ссылка на вторую часть статьи.
Уф, отлично. А то я уже испугался, что опять несбалансированный текст написал!
Здорово! Всё просто, понятно и наглядно. Давно хотел написать что-то подобное для саморазвития. Попробую заняться на выходных)
Кгм, «этот текст предназначен для тех, кто только осваивает программирование», как это толсто. Вы ведь понимаете, что первая же функция в вашем тексте требует ну ооооочень долгого разъяснения? Мне после пяти лет плюсов читать со скрипом можно, но кому-то кто только начал — это очень сложно.
Да, вкурить в колдовство rgba совсем начинающему непросто. Но можно же спросить в комментариях если что-то совсем непонятно.
Да, правда, попробую более конструктивно: почему бы не опустить упаковку/распаковку цветов (ну или показать с картинками, что в ней делают сдвиги), запихивание картинки в одномерный массив, использование любых типов кроме integer, да и рефакторинг тоже? Будет медленнее, плохо расширяемо и памяти лишней будет жрать, но кому какая разница? Зато можно сосредоточиться на геометрии.

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

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

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

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

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

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

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

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


Ссылку в студию, а то я не понимаю, про какой файл вы говорите.
0) в двумерном массиве пикселей, в котором каждая ячейка — это три числа
1) можно и посмотреть. А можно не смотреть и написать прямо тут, если вы и правда для новичков пишете.
2) у меня все равно не получилось связать рассказ с кодом
3) вроде разобрался, это просто данные, а не какое-то преобразование
Покажите, пожалуйста, объявление двумерного массива? Очень интересно посмотреть.
type arrayName [ height ][ width ]; разве нет?
Окей, давайте считать. Вы мне объявляете массив в стеке. Положим для простоты картинку 1000x1000, это один миллион пикселей. Каждый пиксель четыре байта. В сухом остатке мы кладём в стек четыре мегабайта? У вас какой размер стека?
а зачем 1000 на 1000, а не 200 на 200 или даже 20 на 20? Ну и на стеке совсем не обязательно, можно ж и в куче. А каждый пиксель — это не четыре байта, а 12, если мы делаем через структуру с тремя полями.
Я правильно понял, что вы мне только что предложили сделать игру с разрешением экрана 20 на 20?

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

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

Короче, я лучшего решения не нашёл, если вы найдёте, присылайте пулл реквест.
а блин, правда, надо же конвертировать все равно… снимаю тогда претензию
Давайте я даже конкретный пример приведу:
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


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

Можно пояснение? )
давайте пояснение :)
Опираясь на ваш рисунок, высота прямоугольника будет максимальной при совпадении фиолетового и оранжевого отрезков, в данном случае косинус разницы углов будет нулевым.
В любом другом другом состоянии фиолетового луча разница углов будет компенсировать расстояние от текстуру тем самым сглаживая эффект рыбьего глаза.
Ну собственно да. Косинус — это прилежащий катет, делённый на гипотенузу. Если мы смотрим прямо на стенку, то катет — это оранжевый отрезок, и именно его длина нам нужна. Когда при отрисовке фиолетового луча мы помножим на косинус угла, то гипотенуза (фиолетовый луч) * катет / гипотенуза = катет. Что и требовалось доказать.
UFO just landed and posted this here
UFO just landed and posted this here
Невероятно интересные статьи Вы пишете однако. Понемногу пытаюсь разбираться.
Однако меня интересует вопрос: Почему в функции unpack_color происходит деление на 255?
Кусь
    r = (color >>  0) & 255;
    g = (color >>  8) & 255;
    b = (color >> 16) & 255;
    a = (color >> 24) & 255;

Смотрите, вот я хочу иметь четыре канала 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.
Огромное спасибо автору, заставил немного вспомнить прикладную математику )
Пожалуйста, убедитесь, что вы понимаете, откуда там взялся косинус.

В принципе, всё понятно разьяснено «на пальцах» в этом комменте, спасибо!
Я попробовал набросать на JS на скорую руку по описанию — но возникает иное, волнообразное искажение при взгляде на стены под углом (у меня чересчур завышен множитель высоты стен + «широкоформатное» окно вывода, поэтому это проявляется в большой степени) (видно на скрине на левой стене):
image

На гифке в статье они тоже присутствуют, но не в такой степени (см. правую стену):
image

Это от того, что функция косинуса не в полной мере точно «моделирует» данное искажение? Или есть некие иные причины?
А под прямым углом линии взгляда на стену всё корректно, без подушечности: image
Красивые картинки! Я сейчас слишком далеко от цивилизации, попробовать не могу, давайте вы ;)
Если уменьшить шаг скольжения вдоль луча, не поможет ли? Вообще косинус должен давать идеальную коррекцию подушки, другой вопрос, что у нас и подушка приблизительная. Обратите внимание на искривленные ступеньки по центру экрана, когда вы смотрите прямо на стену.
А, ещё один вариант, у меня виртуальный экран на самом деле искривленный, цилиндрический. Посмотрите разницу проекции тут и в статье про tinyraytracer у меня на гитхабе.
Спасибо за наводку на Gitpod. Однако, я не понял, зачем вы давали ссылки на разные коммиты. Ведь всегда открывается некая сборка некоего кода с одним и тем же предпросмотром README.md.
В чём цель включения Gitpod в статью? Т.е. что конкретно я должен там сделать?
А при чём тут readme? Код-то разный по разным ссылкам? Результат работы разный?
Так README открывается по этой ссылке. Там всегда одно и то же изображение, из того самого README.
Вот это то, что я вижу по ссылке Gitpod первого этапа: image

Вот это по ссылке Gitpod второго этапа: image

Вопрос мой в том, в чём разница-то? Я вижу один и тот же README, где-то что-то собирается. Какой профит в этих ссылках gitpod?

Ну вы код-то запустите. Щёлкните по окошку консоли, нажмите клавишу "вверх", потом клавишу "ввод".

Вот это было совсем неочевидно. Теперь видно, спасибо.
Рекомендую в будущем писать, что делать: мало кто работает в web ide :)
Однако, Gitpod почему-то сегодня не хочет показывать мне PNG:
image
Не знаете, как это вылечить?

Почему бы в образовательных целях не использовать более простой и понятный для новичков вариант исходного кода для Этапа 1(сохранение картинки на диск)? Например такой:


#include <fstream>
using namespace std;

class color
{
public:
    int r;
    int g;
    int b;
};

int main()
{
    int h = 512;
    int w = 512;
    ofstream out("./mypict.ppm");
    out << "P3\n" << h << ' ' << w << '\n'
        << 255 << '\n';

    for (int i = 0; i < h; ++i)
    {
        for (int j = 0; j < w; ++j) 
        {
            color pixel = { 255 * i / h, 255 * j / w, 0 };
            out << pixel.r << ' ' << pixel.g << ' ' 
                << pixel.b << ' ' << '\n';
        }

    }
    out.close();
}

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

Потому что в первом этапе важно не только сохранение картинки на диск, но и представление картинки в памяти, чего я у вас не вижу. Как будет храниться картинка в памяти? Ваш код меня лично только запутывает. Почему int, а не char? Почему class {public: } вместо просто struct? Кстати, обратите внимание, что 255*i/h — это баг, т.к. компилятор вправе расставить скобки как 255*(i/h).

Ну а упаковка цветов спрятана внутри функций, которые легко объясняются по первой же ссылке из поискового запроса rgba pack int. Мне не кажется, что это должно быть сложно.

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

С точки зрения потребления памяти ваш метод хранения изображения экономный, спору нет. Использовать int с 4 байтами на канал, пожалуй, расточительно, char тут больше подходит, соглашусь. Вы правы, каждый пишет так, как ему удобнее, но с педагогической точки зрения, неплохо было бы сказать при описании Этапа 1 про расход памяти, чтобы читателю был понятен выбор более сложного метода.


Я почему так первый фрагмент кода подробно разбираю, попытался ваш код в VS2019 выполнить и вместо градиентной заливки выдает вот такую картинку:
.
Причину найти не смог. Мой упрощенный код родился в попытке реализовать ваш алгоритм. С ним картинка формируется правильно.

Делитесь ссылкой на ваш код и картинки, интересно же!

А какая разница где он поставит скобки в операциях, где есть только умножение и деление?

255×2/5 <> 255x(2/5)?

Ну мы же о компьютерах говорим, а не об арифметике. 2/5 (целочисленное деление) равно нулю. То есть, 255*(2/5) равно нулю. 255*2 равно 510, так что (255*2)/5 равно десяти.

А это зависит от значений переменных и от типа переменных.

Если у вас там int (или его вариации), то да.

В своё время писал рейкастер на java по этому циклу статей. Субъективно, даёт лучший результат, но ощутимо сложнее для понимания, а тут - приятно и легко, делюсь своими наработками :)

Only those users with full accounts are able to leave comments. Log in, please.

Articles