Математика в Gamedev по-простому
496 subscribers
60 photos
1 video
3 files
26 links
Как на самом деле работают стрельба, толпа NPC, графика, физика тканей.

Канал про то, что ИИ не заменит: понимание.

Разборы на пальцах, рабочий код, интерактивы. dev-math.ru

Сотрудничество: @it_bizdev
Download Telegram
В тёмных сценах кончаются биты — отсюда и грязь

Глаз устроен нелинейно. Разницу между почти-чёрным и чуть-светлее он видит резко; такую же разницу в ярких цветах почти не замечает — это Вебер–Фехнер. Если раскидать 256 уровней равномерно по яркости, в тенях шаг окажется слишком крупным, и глаз увидит банды, а в ярких участках будет с избытком, и они пропадут зря.

Поэтому яркость хранят не линейно. Гамма-коррекция (gamma) — это степенная кривая, по которой кодируют цвет: она отдаёт больше тёмному, где глаз придирчив, и меньше светлому. Стандарт — sRGB, кривая порядка 2.2. Те же 8 бит растягиваются под восприятие, и 256 уровней хватает на гладкую тень. Приём умный.

А «грязь в тенях» — что это физически? Две вещи разом. Первая — банды: даже с гаммой 8 бит в глубоких тенях не хватает, на тёмное приходится горстка бит, и оно идёт ступеньками. Глаз ловит это именно в темноте — в полумраке зрачок раскрывается, и к перепадам в тенях зрение придирчивее всего. Вторая — муть: если считать свет в неправильном пространстве, тёмные тона разъезжаются по оттенку и грязнятся.

В пятницу — что значит «считать в линейном, а кодировать в sRGB», и почему это меняет всё.

#геймдев #графика #математика #гамма #sRGB #linearworkflow
👍61
Среднее двух цветов темнее, чем должно быть

Наложите полупрозрачный туман на сцену или сожмите текстуру в мип — и по краям, на контрастных границах, поползёт грязная тёмная окантовка. Те же тёмные ореолы вылезают на швах освещения. Картинка будто пачкается сама.

Причина — в том, что почти всё считается прямо в sRGB-кодированных числах. Движок, старый шейдер, да и Photoshop по умолчанию берут два пикселя и усредняют их значения. Но значение — это точка на гамма-кривой, а не количество света. Среднее двух значений не равно значению средней яркости — оно всегда уезжает в тёмное. Отсюда тёмные каймы при альфа-блендинге, мутные швы на мипмапах и даунсэмпле, грязь на краях после сглаживания.

Свет складывается линейно — фотоны просто суммируются. Значит, и считать цвет надо в линейном. Линейный workflow (linear space) — это decode → математика → encode: раскодировал sRGB в линейное, сложил-перемножил-осветил, и только на выводе закодировал обратно в sRGB. Тогда каймы исчезают, а 50% серого — это и правда половина света.

В Unity это галочка Color Space → Linear. И тут важное: линейный режим — не «без гаммы». Гамма остаётся на хранении текстур и на выводе кадра, линейной делается только математика между ними. Гамма и линейное не спорят — это две стадии одного конвейера.

И сюда же стыкуется дизер из вторника: разбивать полосы шумом нужно тоже в правильном месте конвейера — на финальном кодировании в 8 бит, иначе зерно ляжет криво.

#геймдев #графика #математика #linearworkflow #гамма #шейдеры
🔥5
Дизеринг и гамма-коррекция: почему градиенты «бандятся» и откуда грязь в тёмных сценах
https://dev-math.ru/articles/dithering/

Люблю писать такие статьи. Когда я начинал заниматься что графикой, что трекингом, меня очень часто больше всего увлекало на сколько это мир физических ограничений. Нюансы вывода изображения на экран. Нюансы человеческого зрения и тому подобное. Параллельно изучая такие технические моменты ещё и узнаешь, а как что в этом мире работает. И это то, чем меня увлекла когда-то клиентская разработка в отличии от бекенда. Помимо мнгновенного результата действий, то что ты не живешь в чистой абстракции, а сталкиваешься с нюансами практического мира.

Итак. Мы разбирали по кускам тему всю неделю. Теперь полная статья с разбором, интерактивами и так далее. Бандинг, дизеринг, как работает гамма-коррекция, что такое линейное цветовое пространство и зачем оно надо (да и попутно объясняется зачем была нужна гамма).

На сайте произошел большой редизайн. Светлая тема мне нравится наверное чуть больше. Но в основном так как я решил делать игру во вселенной своих комиксов и рилсов. Позже анонс будет размещён на сайте по игре (думаю я управлюсь за 4 месяца ну или хотя бы постараюсь). А так репосты и 🔥 как всегда приветствуются. Это мотивирует писать статьи и дальше. Ведь кому-то это интересно.

#devmath #графика #квантование #дизеринг #sRGB #linearworkflow
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8
Лоу-поли, а тормозит: кто съел ваш FPS?

Поговорим о дроуколлах. А то про графику мы зашли сразу с дитеринга, давайте пойдёмся по базе. Так сказать с самого начала. Думаю за 2-3 месяца можно рассказать интересно о том, как рендер работает в современном мире в деталях.

Собираете уровень из лоу-поли-пака: деревня, лес, заборы, камни — по паре сотен треугольников на модель. Геометрия копеечная, а кадр проседает. Открываете профайлер и видите странное: видеокарта простаивает, весь кадр съел процессор, а счётчик батчей в статистике рендера ушёл в тысячи. Та же история в другом костюме — клон Minecraft: мир из кубов по 12 треугольников, каждый куб — отдельный объект, и уже десяток чанков кладёт игру. Как так? Ведь треугольников мало и причем тут процессор?

Процессор не рисует — он раздаёт команды. Draw call — это команда «нарисуй вот этот меш вот этим материалом», которую CPU через графический драйвер отправляет видеокарте. И перед каждой такой командой — бумажная работа: собрать состояние (какой шейдер, какие текстуры, какие буферы, как смешивать), драйвер всё это проверит, переведёт на язык GPU и положит в командный буфер. Только после этого видеокарта доберётся до треугольников.

Собственно, ключевое свойство: цена этой бумажной работы от количества треугольников почти не зависит. Это как заказы в интернет-магазине: оформить один заказ на тысячу ручек — пара минут, оформить тысячу заказов по одной ручке — вечер. Товар тот же, вся разница — в накладных расходах на каждый заказ.

Лоу-поли-пак экономит то, что и так ничего не стоит. Миллион треугольников одним вызовом GPU прожуёт легче, чем деревню из тысячи заборов — тысячей вызовов. Поэтому важно следить за числом дроуколлов.

Статья этой недели «Путь одного draw call»: пройдём всю дорогу команды от вызова в коде до пикселей на экране.

#геймдев #графика #математика #gpu #drawcall #оптимизация
🔥195❤‍🔥1
Поговорим про шину

Вы взяли ту же геометрию, тот же свет, те же материалы — просто заменили текстуры с 1K на 4K, чтобы было почетче. Ни одной новой строки в шейдере. А fps просел, и на телефоне куда сильнее, чем на компе. Профайлер показывает знакомое: ядра GPU недогружены — видеокарта не считает, она ждёт. Ждёт данные.

Дело в том, что у видеокарты две очень разные функции. Считать — умножать, складывать, гонять шейдерную арифметику — она умеет чудовищно быстро, тысячами потоков разом. А доставать данные из памяти — текстуру, вершины, буфер — заметно медленнее: байты лежат в видеопамяти далеко от вычислительных ядер, и их надо провезти по шине через кэши к тому месту, где над ними наконец сделают пару операций. Bandwidth (пропускная способность) — это сколько байт в секунду успевает проехать по этой дороге.

Собственно, ключевое свойство: на современном GPU довезти байт до вычисления дороже, чем само вычисление сделать. 4K-текстура — это вчетверо больше пикселей по каждой стороне, то есть в шестнадцать раз больше байт, которые тащатся по той же шине через тот же кэш. Шейдер не поменялся, а данных, которые надо подвезти, стало в разы больше — и подвоз не поспевает.

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

По сути, у кадра две точки для ключевой оптимизации. Одна — сколько GPU может посчитать, другая — сколько данных можно провезти. Уперлись в первый — оптимизируйте шейдеры. Уперлись во второй — арифметику хоть в два раза ускорьте, быстрее не станет; лечить надо другое: мельче текстуры, мипмапы, сжатие, меньше проходов по полноэкранному буферу. Мипмапы, кстати, дают fps не только за счёт сглаживания — уменьшенная копия лежит в памяти компактно и попадает в кэш, а полноразмерная текстура на далёком объекте читается вразнобой и впустую гоняет данные по шине.

А в пятницу в статье «Путь одного draw call» проследим всю дорогу команды — от вызова в коде через шину и стадии GPU до пикселя.

#геймдев #графика #математика #gpu #bandwidth #оптимизация
🔥19👍4❤‍🔥1
Обернул шейдер в if, чтобы считать реже. А быстрее не стало

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

Видеокарта прячет задержку памяти тем, что держит не один поток, а тысячи. Один встал на выборке из памяти — планировщик тут же пускает считать другой, у которого данные уже приехали; пока первый ждёт свою порцию, вычислители заняты чужой работой. Вот зачем на GPU тысячи потоков. Мало потоков— прятать нечем, и тогда видеокарта реально встаёт на сотни тактов ожидания.

Но тут и зарыта плата. Одно ядро GPU — это не маленький процессор, а простая и небыстрая линия: ни хитрого предсказания ветвлений, ни глубоких кэшей, один поток на нём медленнее, чем на CPU. Сила — в количестве. И эти линии не независимы: они идут строем, группами (у NVIDIA — по 32), и вся группа исполняет одну и ту же инструкцию разом, каждая над своими данными. Это SIMT — одна команда, много потоков. Представьте не тысячу гонцов, у каждого свой маршрут, а колонну, марширующую в ногу: шаг у всех общий.

Собственно, вот и разгадка вашего if. У этого есть имя — дивергенция ветвлений (branch divergence, ещё говорят «расхождение варпа»). Когда потоки одной группы на развилке расходятся, железу приходится прогнать обе ветки по очереди, отключая на каждой те линии, которых там нет. Группа идёт так же долго, как все пути её потоков вместе взятые. Ваш «пропустить дорогой код» экономит, только если его пропускает вся колонна разом. Один поток свернул в тяжёлую ветку — и остальные тридцать один стоят и ждут его.

По сути, ветвления тут не под запретом — платите вы не за if, а за расхождение внутри группы. Противоположность расхождения — когерентность: это когда соседние потоки заняты одним и тем же, все сворачивают в одну сторону и шагают в ногу. Такая когерентная ветка почти бесплатна — вторая сторона просто не исполняется.

Пример. Материал с enum-режимом и switch (_Mode) в шейдере (классический ubershader, куда свалили все варианты освещения). Если режим один на весь материал — uniform-параметр, — весь варп читает одно значение и идёт одной веткой, остальные просто не исполняются: расхождения нет, дёшево. Но стоит выбирать режим попиксельно — из маски материалов, material-ID в G-буфере, слоёв терреина — и соседние пиксели одного варпа уходят в разные ветки: на стыках материалов варп прогоняет все режимы по очереди.

И та же боль в raymarching и SDF: часть лучей упирается в поверхность за пару шагов, часть марширует до предела — и вся группа топчется в цикле, пока не закончит самый глубокий луч. Ранний выход по одному лучу ничего не даёт: время группы держит самый медленный поток.

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

#devmath #графика #математика #gpu #simt #оптимизация
🔥11👍1
Гонитесь за числом дроуколлов, а тормозит другое

Допустим в оптимизации вы гонитесь за числом draw calls: объединяете меши, чистите объекты — и счётчик Batches ползёт вниз. Дело конечно хорошее. А кадр работает всё так же медленно, особенно на мобилке. Смотрите профайлер внимательнее: draw calls упали, а вторая цифра рядом, SetPass calls, — нет. Что это за цифра?

Дело в том, что это две разные вещи. Batch (draw call) — это просто команда «нарисуй» на текущем состоянии. А SetPass случается, только когда состояние надо СМЕНИТЬ: другой шейдер, другая текстура, другой блендинг, другой Z-тест. Сто вызовов на одном материале — это один SetPass и куча дешёвых команд следом. А смена материала между вызовами — это новый SetPass: перенастройка конвейера, и в тяжёлом случае видеокарта сливает всё, что уже запустила, прежде чем переключиться. Это и есть флаш конвейера (pipeline flush).

Собственно, вот тема дня: цену кадру набивает не сколько вы рисуете, а сколько раз между вызовами меняете стейт. Поэтому один материал со статик-батчингом летит — сотни мешей идут одним SetPass'ом; а десяток материалов вперемешку кладёт кадр, хотя вы не добавили ни полигона — вы добавили переключений контекста. Жёстче всего флаш на смене render target: у мобильных тайловых GPU это выгрузка целого тайла в память,.

Техника дальше весьма логичная. Меньше разных материалов и шейдеров, свести похожее в один материал и атлас, сортировать по стейту, чтобы объекты с одним состоянием шли подряд. Батчинг, инстансинг и SRP Batcher по-разному делают одно и то же — держат стейт на месте, чтобы конвейер переставлять приходилось как можно реже.

Подробнее разберу завтра.

#devmath #графика #математика #gpu #батчинг #оптимизация
🔥91
Путь одного draw call

Всю неделю мы считали, чем платим за кадр: в понедельник — командами, во вторник — байтами на шине, в среду — когерентностью варпов, в четверг — сменами состояния. Обещанная статья готова: «Путь одного draw call» — проходим дорогу команды рисования целиком, от строчки в вашем коде до пикселя на экране, со всеми четырьмя валютами на местах.

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

Читать: https://dev-math.ru/articles/drawcall/

#devmath #графика #математика #gpu #drawcall #оптимизация
🔥12
Туман в Silent Hill — это оптимизация
https://youtube.com/shorts/Az1gxLJHjQw?si=oIxRUaGmCBWrtM-B

1999 год, первая PlayStation. Команде нужен целый город в реальном времени — а железо не тянет: мир схлопывается в маленький пузырь вокруг героя, и на голой границе дома выскакивают из пустоты прямо на глазах. Конкуренты прятали слабость железа в пре-рендеренных задниках — красивых, но с намертво прибитой камерой. Silent Hill пошла иначе: границу мира просто залили туманом.

Дальше не видно — и не нужно. Всё, что за молоком, не рисуется вовсе, и кадр укладывается в бюджет. А бонусом — страх: силуэт проступает не на горизонте, а в двух шагах. Ограничение железа стало фирменным ужасом жанра.

Так что самый страшный монстр серии появился из-за бюджета кадра.

#devmath #разработкаигр #математикавиграх #computergraphics #gamedev
4🔥3👍2
Камера в 3D стоит на месте. Двигается весь мир вокруг неё

Вы летите камерой по уровню в редакторе: жмёте W — едете вперёд, ведёте мышью — поворачиваете. Камера ощущается обычным объектом сцены, просто через неё смотрят. Так и есть — на уровне движка. Но на уровне рендера всё наоборот.

Там камеры нет вообще. Есть точка (0, 0, 0), в которой намертво сидит наблюдатель и смотрит всегда вдоль одной оси. Когда вы «двигаете камеру вперёд», под капотом происходит обратное: весь мир — уровень, враги, ваша собственная модель — сдвигается назад, чтобы наблюдатель остался в нуле. Повернули камеру вправо? Это мир повернулся влево вокруг вас.

Это не метафора и не «ну как бы». Ровно это делает одна из матриц, через которые проходит каждая вершина по пути на экран: она перемещает не камеру к миру, а мир к камере — обратным преобразованием.

И тут важно не перепутать с другим. Живёт это преобразование в вершинной стадии конвейера — в вершинном шейдере, куда каждая вершина заезжает по пути на экран и пересчитывается заново каждый кадр. Оно не двигает объекты в сцене и ничего в мире не меняет — просто выражает их координаты в системе отсчёта камеры.

Зачем так выворачивать, если можно просто хранить позицию камеры? Затем, что «наблюдатель всегда в нуле и смотрит вдоль одной оси» — это удобная система отсчёта, в которой вся дальнейшая математика (проекция, глубина, отсечение) резко упрощается. Дешевле один раз развернуть мир под наблюдателя, чем на каждом шаге таскать за собой, где болтается камера.

Тут же прячется ответ на вопрос, который наверняка зацепил глаз: если камера всегда в нуле и мир едет ей навстречу — выходит, статичные объекты тоже двигаются? И да, и нет. Отметка Static в движке — это про положение объекта в мире: оно зафиксировано, поэтому его матрицу Model (локальные → мировые координаты) можно применить один раз и запечь вершины прямо в мировом пространстве. Отсюда статик-батчинг, запечённый свет, окклюжн. А матрицу View (мир → камера) всё равно применяют каждый кадр ко всему подряд — и к статике, и к динамике. Поэтому неподвижная стена в мире честно стоит на месте, а по экрану едет, когда вы поворачиваетесь: в мире она не двинулась — двинулись вы, то есть система отсчёта. Коротко: Static — про матрицу Model, «мир вокруг камеры» — про View, это разные матрицы.

А что это за матрицы, почему их три и как одна точка доезжает от локальных координат модели до конкретного пикселя — разберём в пятницу.

#геймдев #графика #математика #3d #камера #движки
🔥8👍3
Про отражения в Duke Nukem
https://youtube.com/shorts/2yb0Pn-n94k?si=I6mKOw7dFLzbh3xX

В зеркале Duke Nukem 3D — не отражение, а построенная за стеной вторая комната.

Движок 96-го не умел в отражения: за зеркалом просто копировали комнату целиком, и твой двойник вставал по ту сторону вживую, кадр за кадром.

#devmath #gamedev #dukenukem #рендеринг #математикавиграх
🔥4
Почему далекие поверхности мерцают?

Проблема: две далёкие поверхности — стена и налепленный на неё декаль, земля и дорога — начинают дрожать и «драться за пиксель», стоит чуть двинуть камеру. Первое желание — лезть в материалы, сортировку, освещение. А чинится это обычно в настройках камеры.

Виновник — ближняя плоскость near, выставленная слишком близко к нулю. Классика: near = 0.01, чтобы близкое не обрезалось, и far = 1000, чтобы видно было далеко. Выглядит безобидно. Но точность буфера глубины распределена не поровну. При near = 0.01 и far = 1000 у дальней плоскости соседние значения глубины (в типичном 24-битном буфере) ошибаются почти на 6 единиц — всё, что ближе ~6 единиц друг к другу, буфер там уже не различает. Отсюда мерцание.

И вот что неочевидно: далёкую точность рушит именно ближняя плоскость. Сдвиньте near с 0.01 до 1 при том же far — и различимость глубины вдали улучшится примерно в сто раз. Даже 0.01 → 0.1 даёт ×10. Решает не разность far − near, а их отношение far/near: чем оно больше, тем хуже вдали, а главный рычаг — не тянуть near к нулю.

Практический вывод, который снимает целый класс мерцания: держите near настолько далеко, насколько терпит сцена, а far — настолько близко, насколько можно. Чем у́же диапазон, тем чище глубина. Прежде чем крутить материалы — проверьте эти два числа.

А почему точность вообще нелинейна — это прямое следствие проекции и деления на w. Разберём в пятницу: в статье будет интерактив, где видно кривую глубины и что с ней делает ползунок near.

#геймдев #графика #математика #zfighting #рендер #камера
🔥17
Где на экране этот треугольник
https://dev-math.ru/articles/spaces/

Всю неделю я рассказывал о том где именно находится вершина. Что камеру в 3D на самом деле не двигают — двигается весь мир вокруг неё. Что далёкие поверхности мерцают не из-за материалов, а из-за near, прижатого к нулю. Обещанная статья готова: проходим дорогу одной вершины целиком — от строчки в вашем коде до конкретного пикселя на экране.

Плюс разбор, почему отметка Static и статик-батчинг в Unity вообще не спорят с «мир едет навстречу камере».

Да, если серия нравится, и статья интересна. Делимся ей с друзями и ставим 🔥. А то я что, не блогер, это не сказать.

#devmath #графика #математика #3d #mvp #проекция
🔥14
Квиз: Как игры рисуют кадр?
https://youtube.com/shorts/F6enWvT6lQk?si=pXOCdl87qk5F6BiC

А что вы думали? Статьи просто так? А ну взяли ручки и проходим контрольную. По статье по дроуколлам и пространствам.

Собрал небольшой видео-квиз. Такие штуки вообще проходить полезно, так как информация лучше запоминается, когда с ней сталкиваешься несколько раз в разных контекстах. Хотя мы пока разбираем только базу.

#геймдев #игры #графика #квиз #devmath
🔥5
Как вы получаете пиксель

Далёкие провода ЛЭП, перила моста, ванты, тонкая антенна на крыше — вроде на месте, но стоит чуть повести камерой, и линия начинает рваться: тут кусок есть, тут его нет, и всё это дрожит. Первое объяснение, которое приходит в голову: «ну, деталь мельче пикселя, разрешения не хватает». Оно почти верное, но только почти.

Растеризатор (узел, который превращает три вершины в закрашенные пиксели) не считает, какую долю пикселя накрыл треугольник. Для него пиксель — вообще не маленький квадратик, который можно зацепить краем. Пиксель — это одна точка в его центре. И треугольник закрашивает пиксель тогда и только тогда, когда накрывает эту точку. Так записано в правилах растеризации Direct3D, так же ведут себя OpenGL и Vulkan.

Собственно, отсюда и мерцание. Он не «слишком маленький» — он проходит между центрами пикселей. Камера сдвинулась на полпикселя: где-то центры попали внутрь, где-то нет — по линии побежали мерцающие дырки.

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

Ну и крайний случай: треугольник может не нарисоваться совсем. Мелкий треугольник или длинный тонкий слайвер проскакивает в зазоре между центрами и не даёт в этом кадре ни одного фрагмента, хотя площадь у него есть и вершины на месте. Сдвинулись на полпикселя — даёт. Для геометрии размером около пикселя вопрос «виден или нет» решает субпиксельная позиция.

И из того же правила растёт вопрос, о котором обычно не думают. А если точка-сэмпл легла ровно на ребро, общее у двух соседних треугольников меша? Закрасить у обоих — по шву пройдёт двойная краска, на прозрачности это заметный тёмный рубец. Не закрасить ни у кого — по мешу побежит строчка дырок. Железо этот случай разруливает, и разруливает одинаково у всех.

Как именно растеризатор решает «внутри или снаружи», как он поступает с сэмплом на общем ребре и кто из тысячи треугольников, попавших в один пиксель, остаётся на экране — разберём в пятницу.

#devmath #графика #математика #рендер #растеризация #gamedev
🔥4