Comments 96
1) Массивы индексов перешивал, меняя местами пары элементов;
2) Закрашивал экран попиксельно, двигаясь по перемешанным массивам.
Другим лобовым решением было бы составить список из всех 320×200 возможных координат, перетасовать его (можно даже заранее, и вставить в код уже перетасованным), и закрашивать пиксели по списку; но для этого понадобилось бы как минимум 320×200×2 = 125КБ памяти — пятая часть всей памяти компьютера! (Помните ведь, что 640КБ должно было хватить любому?)
На всякий случай напомню, что НОК(320,200) = 1600
Правда, только двумя массивами, без битовой магии, вроде той, что описана в статье, вряд ли получится добиться однородного распределения. А вот четырьмя (как в моей демке), получается неплохо.
С квадратом получилось бы неплохо — достаточно бегать по обоим массивам и после каждого прогона циклически сдвигать один из них на один элемент. С произвольной областью можно сделать два массива по 320 и отбрасывать всё что за границей экрана.
На самом деле можно пофиксить заменой si
на yy[si - 1]
: http://jsfiddle.net/4gxgy07a/ и будет норм =).
Ну, как уже было написано в статье — держать в памяти весь массив пикселей было, мягко говоря, затратно.
Это закраска красным при смерти, а не рендер.
Решение в лоб:
текстура, каждый пиксель имеет свое значение от 0 до width*height.
В шейдер передаем счетчик и если значение у пикселя меньше счетчика — закращиваем его красным.
Voila
Во времена Wolfenstein 3D не было никаких видеоускорителей у игроков.
Значит «то время» — это позже…
Во времена ремейков. Ремейки начали появляться почти сразу после релиза, так что не понятно. Про 3dfx я ничего не знаю. Но положим мы говорим про ускорители с поддержкой OpenGL.
glReadPixels есть начиная с OpenGL 1.0
Значит читаем буффер цвета через glReadPixels, а потом каждый кадр заливаем несколько пикселей красным и запихиваем это в текстуру, показываем на экран. Ну и далее по тексту.
С производительностью проблем не будет, потому что мы вообще ничего не делаем в кадре кроме запихивания текстуры в видео память и отображения её одним дипом.
Вообще, ИМХО, ремейки не стали повторять этот эффект не потому, что не могут. А потому что он при нормальном разрешении смотрится гораздо хуже.
2) Что именно смотрится хуже? Если пиксели стали слишком мелкие, можно тем же самым кодом заливать экран маленькими квадратиками или любыми другими фигурками.
Значит читаем буффер цвета через glReadPixels,
К сожалению, это очень тормознутая функция (например, на многих видеокартах ATI годов эдак около 2005-2007). Я эту функцию использовал для моделирования отражённого сигнала от оптодатчика и тут-то и выяснилось, что на картах ATI она жутко тормозит (раз в 10 по сравнению с nVidia).
А искать раритетную видуху того времени и писать тест, как-то не входит в сферу моих интересов.
Так что спорить не буду.
Вот.
Там нажмите «выполнить задание» и выберите rotate.txt. Если каждый кадр станет замирать на несколько секунд (и десятки секунд) — ваша карта тормознутая. У меня где-то FPS около 3-4 на nVidia 450 и задание выполнено за 9 секунд примерно.
Всегда сохранение скриншотов делали через glReadPixels.
Даже в те времена эта операция легко давала десятки FPS.
Может быть вы напоролись на кривые драйвера…
Вряд ли драйвера виноваты — я опробовал около десятка разных видеокарт и на разных компьютерах. У ATI эта функция существенно тормозила — тут обработку смело можно было оставлять на ночь. :)
Ему 20 выше крыши…
P.S.
Кстати, я уж и забыл с чего всё началось. Нам надо glReadPixels сделать только один раз, а не 20 раз в секунду. Чтобы получить актуальный кадр. Дальше просто в памяти закрашиваем и отправляем на рендер.
Даже для основного ГСЧ использовалась таблица заранее перетасованных значений, выдающая одну и ту же псевдослучайную последовательность при каждом запуске.
Позже, во времена DOOM и сетевой игры, стало критически важным, чтобы у всех игроков ГСЧ генерировал в точности одну и ту же последовательность значений, потому что по сети передавались только нажатия на клавиши, а обрабатывал их каждый компьютер сам по себе.
В лоб пришло бы на ум все же использовать монте-карло — ГПСЧ с постепенным увеличением кол-ва генерируемых пар. Но это даже без прикидок очевидно слишком затратное по ресурсам решение.
15 2 14 13
11 6 7 8
9 16 5 12
4 3 1 10
© сам придумал :)
1. Генерация такой текстуры с уникальными, хорошо разложенными координатами. В итоге, получаем массив.
2. Собственно, использование массива для задачи.
Такой подход даст максимальный перформанс и при этом не нужно было бы тратить место для хранения массива, если он большой.
Для первой задачи нужен какой-то алгоритм с генерацией уникальных координат. При этом, чтобы при любых входных данных получалось бы красивое рандомное распределение. Входные данные: размер массива, т.е. два числа X, Y.
Частности — квадратный или прямоугольный.
далее Z = Z — 1 и повторяем процедуру
недостаток только в том что нам нужно каждый раз читать видеопамять на предмет не закрашенной точки N раз, где N [1… W*H]
Лобовым решением было бы использовать генератор случайных чисел и перед закраской каждого пикселя проверять, не закрашен ли он до сих пор. Это было бы крайне неэффективно: к тому моменту, когда на экране остаётся последний незакрашенный пиксель, потребуется порядка 320×200 вызовов ГСЧ, чтобы в него «попасть».—точно так же, для закраски последней точки потребуется порядка 320×200 операций, а на всю заливку целиком — порядка (320×200)² операций, т.е. несколько минут на 286-12МГц.
Пользователь раньше заснёт со скуки, чем такая заливка завершится.
это не тупой перебор точек на предмет закрашено или нет
а ровно X * Y случайных чисел, т.е 320×200 операций, не более
хотя смотря что вы понимаете под операцией
пример:
2 х 2
случайное число
2 (0..3) — красим x=0, y=1
2 (0..2) — красим x=1, y=1 (пропускаем x=0, y=1 т.к. оно уже закрашено)
0 (0..1) — красим x=0, y=0
0 (0..0) — красим x=1, y=1 (опять же, первый закрашен, пропускаем)
итого 4 операции, и не 16
Сколько операций потребуется, чтобы найти незакрашенный?
в моем случае их в 2 раза меньше, как сумма 1..n, где n = 320х200
я уже не помню как посчитать такты, забылось, но мне кажется что на ассемблере при прямой работе с видеопамятью, не так страшен черт
в два раза меньше = 2 048 000 000
т.е. это тот же порядок и он совершенно прав. Тем более атомарных операций может быть даже больше (как минимум — рассчет позиции в массиве, получение значения, сравнение с необходимым и увеличение счетчика для каждого пикселя), так что операций будет даже больше, чем он сказал, но порядок указан правильно: сложность квадратическая, операций миллиарды, а должна быть линейной, операций должно быть десятки тысяч.
вот только если просто последовательно закрашивать экран 320 х 200 сплошным текстом, то это уже будут сотни тысяч операций, ни как не десятки
мне пришла идея, как мне кажется куда проще
Вместо простой установки пикселя по RND, вы собрались генерить RND и ещё что-то там сканировать? В чём простота?
нужно каждый раз читать видеопамять
IBM VGA takes an average of 1.1 microseconds to complete each memory read (единственная цифра которую я нашел)
Получаем ~900кБ(скорость чтения байт в сек)/64кБ(размер фреймбуфера) = 14 (чтений всего экрана в секунду)
Не очень понимаю весь ажиотаж вокруг таких тем. Подобные трюки с разной степенью "извращенности" использовались на спектрумах и коммодорах задолго до пришествия id. Помню на спектруме, чтобы не проверять все ли пиксели перекрасились, попросту после определенного количества циклов все байты в экранной области заполнялись #FFами..
P.S.: игрушка культовая конечно была.
320 * 200 /8 = 8 000 байт, чуть меньше 8 килобайт. Немало, но совсем не 125 килобайт, в 16 раз меньше.
На самом деле это как бы счетчик, проходящий все 2^N состояний, просто не последовательно. В плис, кстати, это самый простой вариант реализации счетчиков любой длины. Сам использую такие на МК Atmel — если сохранять значение при выключении прибора в EEPROM, а при включении загружать оттуда, получается почти random.
Все же ограниченная производительность способствует поиску интересных решений)
Первоначально хотел составить динамический список (std::list) из пикселей, но в нем нельзя обращаться к произвольному элементу быстро. Затем была мысль AVL дерева из всех пикселей,
Сколько памяти потребуется на хранение этих структур данных?
— Х-координату (9 бит)
— Y-координату (8 бит)
— «указатели» на левую и правую ветвь (достаточно по 16 бит)
— флаг состояния вершины дерева (2 бита)
Итого, 8+9+2*16+2 = 51 бит на элемент. Т.е. 7 байт. Всего 320*200*7 = 448000 = 437.5 Кб. Немало, но в 640 влезает. На момент гибели игрока в игре можно пожертвовать текстурами/какими-либо еще данными, которые потом загрузить с диска, чтобы освободить данный объем памяти. Вариант, конечно, не лучший, но реализуемый (ну, и на 286 обычно был 1 Мб).
— Х-координату (9 бит)
Если не гнаться за чистотой, можно и 8 обойтись. Считаем, что экран — 256 пикселей и делаем свертку, дублируя левую часть справа.
Тут еще выравнивание будет хромать, если по 7 байт на элемент взять. Да и если обрезать указатель до 16 бит, надо как раз выравнивать так, чтобы справа были «мнимые нули» в адресе.
Немало, но в 640 влезает. На момент гибели игрока в игре можно пожертвовать текстурами/какими-либо еще данными, которые потом загрузить с диска, чтобы освободить данный объем памяти. Вариант, конечно, не лучший, но реализуемый (ну, и на 286 обычно был 1 Мб).
Если мы хотим уложиться в 640к, то 437 не влезет никак, слишком много там данных системы, драйверов, утилит. А так эта игра работала емнип работала и с памятью свыше 1 Мб, так что все равно не так критично. Хотя это и будет забивание гвоздей микроскопом по сравнению со сдвиговым регистром.
Тут еще выравнивание будет хромать, если по 7 байт на элемент взять
Выравнивание на 286? :) Ну, может быть будет медленнее такта на 3)
Да и если обрезать указатель до 16 бит
Зачем? Я специально написал указатель в кавычках — это может быть просто индекс от массива с размером элемента 7 байт. Таких элементов будет 64000, т.е. как раз укладываемся в 16 бит. Ну а чтобы получить реальный адрес, надо будет умножить на 7 (или умножить на 8 и вычесть 1).
Если мы хотим уложиться в 640к, то 437 не влезет никак,
Насколько я помню, при правильном подходе было >500К свободной основной памяти, т.к. когда её становилось меньше, начинались проблемы с играми и некоторыми программами.
Конечно, писать такой мелкий эффект на такой объем памяти никто в здравом уме не станет, но мы же рассматриваем лишь теоретическую часть вопроса.
Выравнивание на 286? :) Ну, может быть будет медленнее такта на 3)
Копейка рубль бережет :P
Зачем? Я специально написал указатель в кавычках — это может быть просто индекс от массива с размером элемента 7 байт. Таких элементов будет 64000, т.е. как раз укладываемся в 16 бит. Ну а чтобы получить реальный адрес, надо будет умножить на 7 (или умножить на 8 и вычесть 1).
Умножить на 8 и вычесть единицу это не умножить на 7 же. 100*7 <> 100*8 — 1. Вычесть нужно индекс, т.е. 100*8 — 100. Если указатель выровнен, то это будет просто умножение, т.е. сдвиг. Все как всегда — скорость vs память.
Я запамятовал, как будет адресоваться память в этом случае — сегментами или одним куском? Если сегментами, то мы еще попадем на расщепление линейного адреса; последний элемент будет размазан между двумя сегментами; арифметические операции придется делать на двух регистрах. Думаю, слишком много накладных расходов.
Копейка рубль бережет :P
«Если каждый месяц откладывать немного денег, то уже через год вы увидите, как мало у вас набралось» :)
Судя по гифке, эффект отрабатывает примерно 10К точек в секунду — больше не надо, иначе он потеряет смысл. Это дает примерно 1200 тактов на точку при тактовой частоте 12 МГц. Немного, но и не так уж мало. Если и возникнет проблема с такой реализацией, так это из-за того, что в AVL дереве операция удаления элемента занимает до Log2(N) итераций, т.е. до 16 в нашем случае. Плюс еще нужен ГПСЧ, чтобы определять, где остановиться.
Вычесть нужно индекс, т.е. 100*8 — 100
Это и имелось в виду)
Я запамятовал, как будет адресоваться память в этом случае — сегментами или одним куском?
Сегментами. Но в реальном режиме адрес в памяти равен s*16 + D, где S — значение сегментного регистра. Т.е. умножение на 7 надо проводить на 19-ти разрядах, после чего 15 старших отправить в сегментный регистр, а 4 младших — в регистр адреса (типа ВХ). Никаких проблем с выходом за границу сегмента не будет.
Судя по гифке, эффект отрабатывает примерно 10К точек в секунду — больше не надо, иначе он потеряет смысл.
Не надо судить по гифке, лучше судить по исходникам: весь эффект целиком (64К пикселей) занимает 70 кадров, т.е. ровно секунду (в соответствии с частотой развёртки VGA).
Можно даже воспользоваться эмулятором и перепроверить: да, ровно секунду.
Дело в том, что для разделения 16-битного значения на координаты x и y его пришлось бы делить с остатком на 320 или на 200, и затраты на такую операцию деления с лихвой бы скомпенсировали всё ускорение от перехода на 16-битный РСЛОС.
В случае с VGA-режимом 13h (с линейной организацией видеопамяти) можно и не делить. 16-битное число можно использовать в качестве индекса в сегменте 0xA000 для записи пикселей в видеобуфер. Но Кармак использовал планарный VGA-режим и, возможно, счёл, что проще использовать 17-битный РСЛОС и использовать уже написанную функцию быстрого вывода пикселей (функция использует битовый сдвиг и таблицу поиска ylookup для вычисления смещения начала строки), чем городить огород.
VGA-адаптер зачастую имел больше 64KB памяти, но в линейном режиме они были недоступны. В планарных режимах (mode X) этого ограничения не было и можно было использовать несколько страниц видеопамяти для дешёвой организации двойной (и даже тройной) буферизации, но при этом возникали «лишние трудности» при расчёте адресов и переключении плоскостей вывода.
rndval
на координаты пришлось бы в любом случае, поскольку в действительности закрашивается не весь экран целиком, а только viewport; рамка вокруг него (чем меньше viewport, тем быстрее работает игра) и статусбар внизу экрана остаются незакрашенными. Вот для этого клиппинга и нужна проверка координат по отдельности.
Попиксельная заливка экрана в Wolfenstein 3D