Неделя 1: четыре дыры зашиты — а карты целиком нет
Дейв догонял Хардкода и рефакторил его костыли один за другим: почему чисел именно три (это про глаз, а не про цвет), почему камни-близнецы разъезжаются под другой лампой, почему тонкая плёнка глитчит и как свернуть её аналитически, почему дисперсию не подделать сдвигом RGB-каналов.
RGB — это техника со своей областью применимости, и Дейв теперь видит, где она кончается. Но Хардкод сбежал в трещину, а Статика только набрала силу: эталона у него пока нет.
На следующей неделе:
— небо: трюк, ставший формулой, — тут RGB честнее некуда;
— флуоресценция: то, чего три числа не умеют в принципе;
— каустики с дисперсией: два фейка сталкиваются;
— финал: спектральный эталон против RGB — и сколько правда стоит в миллисекундах.
Дейв догонял Хардкода и рефакторил его костыли один за другим: почему чисел именно три (это про глаз, а не про цвет), почему камни-близнецы разъезжаются под другой лампой, почему тонкая плёнка глитчит и как свернуть её аналитически, почему дисперсию не подделать сдвигом RGB-каналов.
RGB — это техника со своей областью применимости, и Дейв теперь видит, где она кончается. Но Хардкод сбежал в трещину, а Статика только набрала силу: эталона у него пока нет.
На следующей неделе:
— небо: трюк, ставший формулой, — тут RGB честнее некуда;
— флуоресценция: то, чего три числа не умеют в принципе;
— каустики с дисперсией: два фейка сталкиваются;
— финал: спектральный эталон против RGB — и сколько правда стоит в миллисекундах.
🔥6
Дейв теперь в Рилсах
https://www.instagram.com/reel/DaFostRIAxo/?igsh=OXd4cnM0b2x5cGFx
В сторис я выложу видос, но телега жрет качество, так что советую глянуть в инсте. Я продумал, на мой взгляд, довольно интересный научнопопулярный формат видео, с роликами о прикольных вещах и парадоксах в математике + забавных историй. Как раз самое то для инсты. Быстро, ненапряжно и легко потреблять.
Первый рилс про парадокс Симпсона. Если вдруг не знаете забавную историю про Беркли в 1973 году. Советую глянуть 🙂
UPD: И в шортсах https://youtube.com/shorts/ZQpwg1fqtOY?si=fXMKK0shl23uvkBp
#парадоксСимпсона #статистика #данные
https://www.instagram.com/reel/DaFostRIAxo/?igsh=OXd4cnM0b2x5cGFx
В сторис я выложу видос, но телега жрет качество, так что советую глянуть в инсте. Я продумал, на мой взгляд, довольно интересный научнопопулярный формат видео, с роликами о прикольных вещах и парадоксах в математике + забавных историй. Как раз самое то для инсты. Быстро, ненапряжно и легко потреблять.
Первый рилс про парадокс Симпсона. Если вдруг не знаете забавную историю про Беркли в 1973 году. Советую глянуть 🙂
UPD: И в шортсах https://youtube.com/shorts/ZQpwg1fqtOY?si=fXMKK0shl23uvkBp
#парадоксСимпсона #статистика #данные
YouTube
Парадокс Симпсона и Беркли
Беркли, 1973: цифры кричат «дискриминация женщин», а по факультетам...
🔥3❤1
Как вам новые форматы?
Anonymous Poll
28%
Новый комикс
17%
Тема со спектром и цветом
21%
Видео про Парадокс Сипсона
52%
Посты
79%
Статья
34%
Интерактивы
Самая загадочная строчка кода
https://youtube.com/shorts/m55BTiN-AJc?si=Xgf80LrTeeqmQthv
В коде Quake III есть строчка, а рядом — // what the fuck?. Кто её написал, спорят до сих пор.
Чтобы свет в шутере считался правильно, направление каждого луча надо нормировать — поделить на длину, а это корень. Тысячи делений на корень за кадр, и на железе конца девяностых не тянуло. Кто-то срезал эту дорогую операцию магическим числом 0x5F3759DF и сдвигом битов.
Вся красота в одной идее: биты дробного числа — это почти его логарифм. Поэтому обратный корень считается почти даром — пара действий над битами и один шаг Ньютона на уточнение. Невидимая математика, которая держала фпс в норме.
Автора так и не нашли. Не Кармак — следы ведут к Гэри Таролли и Грегу Уолшу, но в самой id Software не признался никто.
https://youtube.com/shorts/m55BTiN-AJc?si=Xgf80LrTeeqmQthv
В коде Quake III есть строчка, а рядом — // what the fuck?. Кто её написал, спорят до сих пор.
Чтобы свет в шутере считался правильно, направление каждого луча надо нормировать — поделить на длину, а это корень. Тысячи делений на корень за кадр, и на железе конца девяностых не тянуло. Кто-то срезал эту дорогую операцию магическим числом 0x5F3759DF и сдвигом битов.
Вся красота в одной идее: биты дробного числа — это почти его логарифм. Поэтому обратный корень считается почти даром — пара действий над битами и один шаг Ньютона на уточнение. Невидимая математика, которая держала фпс в норме.
Автора так и не нашли. Не Кармак — следы ведут к Гэри Таролли и Грегу Уолшу, но в самой id Software не признался никто.
YouTube
Самая загадочная строчка кода
В коде Quake III есть строчка, а рядом — // what the fuck?. Кто её ...
🔥10👎1
Художник рисует гладкое небо, а разработчик видит полосы
Дейв вывел в свой мир закатное небо — мягкий переход от оранжевого к синему. А на мониторе по небу пошли полосы. Картинку он не трогал. Поменялось только то, как её упаковали в биты. Разберём, что это.
Откройте любой редактор, залейте плавным градиентом от чёрного к серому, растяните на весь экран. Гладко? Кажется, что да. Но на больших плавных площадях или при импорте глаз вдруг ловит вертикальные полосы. Откуда они, если в текстуре непрерывный переход?
Дело в том, что цвет в кадре хранят побайтно: 8 бит на канал — это 2⁸ = 256 уровней яркости, и между соседними нет ничего, и это без сжатия. Непрерывный градиент квантуется: длинный плавный участок ложится на одну и ту же ступеньку, потом резко прыгает на следующую. Получаются плоские полосы постоянного цвета со швом между ними. Это и есть бандинг (banding) — видимые ступени-полосы на месте плавного градиента, когда уровней яркости не хватает на гладкий переход.
И тут подключается зрение. На границе двух почти одинаковых тонов оно само добавляет контраст — это полосы Маха, эффект латерального торможения в сетчатке. Разница между ступеньками 1/255, физически почти ноль, а кайму по шву вы видите ясно. Глаз не меряет яркость абсолютно, он меряет перепады (закон Вебера) — и ровно на швах квантования перепад вылезает.
В пятницу — почему именно 8 бит, и три способа убрать полосы.
#геймдев #графика #математика #бандинг #квантование #гамма
Дейв вывел в свой мир закатное небо — мягкий переход от оранжевого к синему. А на мониторе по небу пошли полосы. Картинку он не трогал. Поменялось только то, как её упаковали в биты. Разберём, что это.
Откройте любой редактор, залейте плавным градиентом от чёрного к серому, растяните на весь экран. Гладко? Кажется, что да. Но на больших плавных площадях или при импорте глаз вдруг ловит вертикальные полосы. Откуда они, если в текстуре непрерывный переход?
Дело в том, что цвет в кадре хранят побайтно: 8 бит на канал — это 2⁸ = 256 уровней яркости, и между соседними нет ничего, и это без сжатия. Непрерывный градиент квантуется: длинный плавный участок ложится на одну и ту же ступеньку, потом резко прыгает на следующую. Получаются плоские полосы постоянного цвета со швом между ними. Это и есть бандинг (banding) — видимые ступени-полосы на месте плавного градиента, когда уровней яркости не хватает на гладкий переход.
И тут подключается зрение. На границе двух почти одинаковых тонов оно само добавляет контраст — это полосы Маха, эффект латерального торможения в сетчатке. Разница между ступеньками 1/255, физически почти ноль, а кайму по шву вы видите ясно. Глаз не меряет яркость абсолютно, он меряет перепады (закон Вебера) — и ровно на швах квантования перепад вылезает.
В пятницу — почему именно 8 бит, и три способа убрать полосы.
#геймдев #графика #математика #бандинг #квантование #гамма
🔥8
Чтобы убрать полосы, в картинку нарочно подмешивают шум
Идея дизеринга обратная интуиции. Дизеринг (dithering) — это точно отмеренный шум, который подмешивают к цвету перед тем, как округлить его до ближайшей из 256 ступенек. Там, где раньше длинный участок ложился на одну ступеньку, теперь часть пикселей перескакивает на соседнюю — вперемешку. Вместо плоской полосы со швом — мелкая крупа из двух уровней. Глаз усредняет её обратно в плавный переход, которого в битах уже нет.
Но шум шуму рознь. Закинуть Random.Range — белый шум — и картинка реально станет грязной: зерно лезет в глаза. Ordered-дизер (матрица Байера) дёшев, но оставляет регулярную сетку-крест. А синий шум (blue noise) и распространение ошибки (Флойд–Стейнберг) кладут зерно так, что глаз его почти не вычленяет: энергия шума уходит в высокие частоты, к которым зрение нечувствительно. Поэтому современный движок дизерит синим шумом.
И это не только про градиенты. Тот же приём — dither-fade: пунктирная прозрачность для плавного LOD-перехода, волос, волюметрики, мягких теней, и вывод 10-битного кадра в 8 бит без полос.
В пятницу разложим, какой дизер куда и почему синий шум победил.
#геймдев #графика #математика #дизеринг #bluenoise #шейдеры
Идея дизеринга обратная интуиции. Дизеринг (dithering) — это точно отмеренный шум, который подмешивают к цвету перед тем, как округлить его до ближайшей из 256 ступенек. Там, где раньше длинный участок ложился на одну ступеньку, теперь часть пикселей перескакивает на соседнюю — вперемешку. Вместо плоской полосы со швом — мелкая крупа из двух уровней. Глаз усредняет её обратно в плавный переход, которого в битах уже нет.
Но шум шуму рознь. Закинуть Random.Range — белый шум — и картинка реально станет грязной: зерно лезет в глаза. Ordered-дизер (матрица Байера) дёшев, но оставляет регулярную сетку-крест. А синий шум (blue noise) и распространение ошибки (Флойд–Стейнберг) кладут зерно так, что глаз его почти не вычленяет: энергия шума уходит в высокие частоты, к которым зрение нечувствительно. Поэтому современный движок дизерит синим шумом.
И это не только про градиенты. Тот же приём — dither-fade: пунктирная прозрачность для плавного LOD-перехода, волос, волюметрики, мягких теней, и вывод 10-битного кадра в 8 бит без полос.
В пятницу разложим, какой дизер куда и почему синий шум победил.
#геймдев #графика #математика #дизеринг #bluenoise #шейдеры
🔥10
В тёмных сценах кончаются биты — отсюда и грязь
Глаз устроен нелинейно. Разницу между почти-чёрным и чуть-светлее он видит резко; такую же разницу в ярких цветах почти не замечает — это Вебер–Фехнер. Если раскидать 256 уровней равномерно по яркости, в тенях шаг окажется слишком крупным, и глаз увидит банды, а в ярких участках будет с избытком, и они пропадут зря.
Поэтому яркость хранят не линейно. Гамма-коррекция (gamma) — это степенная кривая, по которой кодируют цвет: она отдаёт больше тёмному, где глаз придирчив, и меньше светлому. Стандарт — sRGB, кривая порядка 2.2. Те же 8 бит растягиваются под восприятие, и 256 уровней хватает на гладкую тень. Приём умный.
А «грязь в тенях» — что это физически? Две вещи разом. Первая — банды: даже с гаммой 8 бит в глубоких тенях не хватает, на тёмное приходится горстка бит, и оно идёт ступеньками. Глаз ловит это именно в темноте — в полумраке зрачок раскрывается, и к перепадам в тенях зрение придирчивее всего. Вторая — муть: если считать свет в неправильном пространстве, тёмные тона разъезжаются по оттенку и грязнятся.
В пятницу — что значит «считать в линейном, а кодировать в sRGB», и почему это меняет всё.
#геймдев #графика #математика #гамма #sRGB #linearworkflow
Глаз устроен нелинейно. Разницу между почти-чёрным и чуть-светлее он видит резко; такую же разницу в ярких цветах почти не замечает — это Вебер–Фехнер. Если раскидать 256 уровней равномерно по яркости, в тенях шаг окажется слишком крупным, и глаз увидит банды, а в ярких участках будет с избытком, и они пропадут зря.
Поэтому яркость хранят не линейно. Гамма-коррекция (gamma) — это степенная кривая, по которой кодируют цвет: она отдаёт больше тёмному, где глаз придирчив, и меньше светлому. Стандарт — sRGB, кривая порядка 2.2. Те же 8 бит растягиваются под восприятие, и 256 уровней хватает на гладкую тень. Приём умный.
А «грязь в тенях» — что это физически? Две вещи разом. Первая — банды: даже с гаммой 8 бит в глубоких тенях не хватает, на тёмное приходится горстка бит, и оно идёт ступеньками. Глаз ловит это именно в темноте — в полумраке зрачок раскрывается, и к перепадам в тенях зрение придирчивее всего. Вторая — муть: если считать свет в неправильном пространстве, тёмные тона разъезжаются по оттенку и грязнятся.
В пятницу — что значит «считать в линейном, а кодировать в sRGB», и почему это меняет всё.
#геймдев #графика #математика #гамма #sRGB #linearworkflow
👍6❤1
Среднее двух цветов темнее, чем должно быть
Наложите полупрозрачный туман на сцену или сожмите текстуру в мип — и по краям, на контрастных границах, поползёт грязная тёмная окантовка. Те же тёмные ореолы вылезают на швах освещения. Картинка будто пачкается сама.
Причина — в том, что почти всё считается прямо в sRGB-кодированных числах. Движок, старый шейдер, да и Photoshop по умолчанию берут два пикселя и усредняют их значения. Но значение — это точка на гамма-кривой, а не количество света. Среднее двух значений не равно значению средней яркости — оно всегда уезжает в тёмное. Отсюда тёмные каймы при альфа-блендинге, мутные швы на мипмапах и даунсэмпле, грязь на краях после сглаживания.
Свет складывается линейно — фотоны просто суммируются. Значит, и считать цвет надо в линейном. Линейный workflow (linear space) — это decode → математика → encode: раскодировал sRGB в линейное, сложил-перемножил-осветил, и только на выводе закодировал обратно в sRGB. Тогда каймы исчезают, а 50% серого — это и правда половина света.
В Unity это галочка Color Space → Linear. И тут важное: линейный режим — не «без гаммы». Гамма остаётся на хранении текстур и на выводе кадра, линейной делается только математика между ними. Гамма и линейное не спорят — это две стадии одного конвейера.
И сюда же стыкуется дизер из вторника: разбивать полосы шумом нужно тоже в правильном месте конвейера — на финальном кодировании в 8 бит, иначе зерно ляжет криво.
#геймдев #графика #математика #linearworkflow #гамма #шейдеры
Наложите полупрозрачный туман на сцену или сожмите текстуру в мип — и по краям, на контрастных границах, поползёт грязная тёмная окантовка. Те же тёмные ореолы вылезают на швах освещения. Картинка будто пачкается сама.
Причина — в том, что почти всё считается прямо в 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
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 #оптимизация
Поговорим о дроуколлах. А то про графику мы зашли сразу с дитеринга, давайте пойдёмся по базе. Так сказать с самого начала. Думаю за 2-3 месяца можно рассказать интересно о том, как рендер работает в современном мире в деталях.
Собираете уровень из лоу-поли-пака: деревня, лес, заборы, камни — по паре сотен треугольников на модель. Геометрия копеечная, а кадр проседает. Открываете профайлер и видите странное: видеокарта простаивает, весь кадр съел процессор, а счётчик батчей в статистике рендера ушёл в тысячи. Та же история в другом костюме — клон Minecraft: мир из кубов по 12 треугольников, каждый куб — отдельный объект, и уже десяток чанков кладёт игру. Как так? Ведь треугольников мало и причем тут процессор?
Процессор не рисует — он раздаёт команды. Draw call — это команда «нарисуй вот этот меш вот этим материалом», которую CPU через графический драйвер отправляет видеокарте. И перед каждой такой командой — бумажная работа: собрать состояние (какой шейдер, какие текстуры, какие буферы, как смешивать), драйвер всё это проверит, переведёт на язык GPU и положит в командный буфер. Только после этого видеокарта доберётся до треугольников.
Собственно, ключевое свойство: цена этой бумажной работы от количества треугольников почти не зависит. Это как заказы в интернет-магазине: оформить один заказ на тысячу ручек — пара минут, оформить тысячу заказов по одной ручке — вечер. Товар тот же, вся разница — в накладных расходах на каждый заказ.
Лоу-поли-пак экономит то, что и так ничего не стоит. Миллион треугольников одним вызовом GPU прожуёт легче, чем деревню из тысячи заборов — тысячей вызовов. Поэтому важно следить за числом дроуколлов.
Статья этой недели «Путь одного draw call»: пройдём всю дорогу команды от вызова в коде до пикселей на экране.
#геймдев #графика #математика #gpu #drawcall #оптимизация
🔥19❤5❤🔥1
Поговорим про шину
Вы взяли ту же геометрию, тот же свет, те же материалы — просто заменили текстуры с 1K на 4K, чтобы было почетче. Ни одной новой строки в шейдере. А fps просел, и на телефоне куда сильнее, чем на компе. Профайлер показывает знакомое: ядра GPU недогружены — видеокарта не считает, она ждёт. Ждёт данные.
Дело в том, что у видеокарты две очень разные функции. Считать — умножать, складывать, гонять шейдерную арифметику — она умеет чудовищно быстро, тысячами потоков разом. А доставать данные из памяти — текстуру, вершины, буфер — заметно медленнее: байты лежат в видеопамяти далеко от вычислительных ядер, и их надо провезти по шине через кэши к тому месту, где над ними наконец сделают пару операций. Bandwidth (пропускная способность) — это сколько байт в секунду успевает проехать по этой дороге.
Собственно, ключевое свойство: на современном GPU довезти байт до вычисления дороже, чем само вычисление сделать. 4K-текстура — это вчетверо больше пикселей по каждой стороне, то есть в шестнадцать раз больше байт, которые тащатся по той же шине через тот же кэш. Шейдер не поменялся, а данных, которые надо подвезти, стало в разы больше — и подвоз не поспевает.
Вот почему безобидная замена текстур чего-то стоит, и не только текстур. Похожий механизм — дешёвый на вид полноэкранный эффект: блюр или bloom заняты не арифметикой, а выборкой текстур и чтением-записью целого кадрового буфера — и проходов таких много. На телефоне часто это буквально гоняет кадр во внешнюю память и обратно.
По сути, у кадра две точки для ключевой оптимизации. Одна — сколько GPU может посчитать, другая — сколько данных можно провезти. Уперлись в первый — оптимизируйте шейдеры. Уперлись во второй — арифметику хоть в два раза ускорьте, быстрее не станет; лечить надо другое: мельче текстуры, мипмапы, сжатие, меньше проходов по полноэкранному буферу. Мипмапы, кстати, дают fps не только за счёт сглаживания — уменьшенная копия лежит в памяти компактно и попадает в кэш, а полноразмерная текстура на далёком объекте читается вразнобой и впустую гоняет данные по шине.
А в пятницу в статье «Путь одного draw call» проследим всю дорогу команды — от вызова в коде через шину и стадии GPU до пикселя.
#геймдев #графика #математика #gpu #bandwidth #оптимизация
Вы взяли ту же геометрию, тот же свет, те же материалы — просто заменили текстуры с 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 #оптимизация
Вы нашли в шейдере тяжёлый кусок — дорогой цикл, лишний проход, ветку с кучей математики — и обернули его в 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 #батчинг #оптимизация
Допустим в оптимизации вы гонитесь за числом draw calls: объединяете меши, чистите объекты — и счётчик Batches ползёт вниз. Дело конечно хорошее. А кадр работает всё так же медленно, особенно на мобилке. Смотрите профайлер внимательнее: draw calls упали, а вторая цифра рядом, SetPass calls, — нет. Что это за цифра?
Дело в том, что это две разные вещи. Batch (draw call) — это просто команда «нарисуй» на текущем состоянии. А SetPass случается, только когда состояние надо СМЕНИТЬ: другой шейдер, другая текстура, другой блендинг, другой Z-тест. Сто вызовов на одном материале — это один SetPass и куча дешёвых команд следом. А смена материала между вызовами — это новый SetPass: перенастройка конвейера, и в тяжёлом случае видеокарта сливает всё, что уже запустила, прежде чем переключиться. Это и есть флаш конвейера (pipeline flush).
Собственно, вот тема дня: цену кадру набивает не сколько вы рисуете, а сколько раз между вызовами меняете стейт. Поэтому один материал со статик-батчингом летит — сотни мешей идут одним SetPass'ом; а десяток материалов вперемешку кладёт кадр, хотя вы не добавили ни полигона — вы добавили переключений контекста. Жёстче всего флаш на смене render target: у мобильных тайловых GPU это выгрузка целого тайла в память,.
Техника дальше весьма логичная. Меньше разных материалов и шейдеров, свести похожее в один материал и атлас, сортировать по стейту, чтобы объекты с одним состоянием шли подряд. Батчинг, инстансинг и SRP Batcher по-разному делают одно и то же — держат стейт на месте, чтобы конвейер переставлять приходилось как можно реже.
Подробнее разберу завтра.
#devmath #графика #математика #gpu #батчинг #оптимизация
🔥9✍1
Путь одного draw call
Всю неделю мы считали, чем платим за кадр: в понедельник — командами, во вторник — байтами на шине, в среду — когерентностью варпов, в четверг — сменами состояния. Обещанная статья готова: «Путь одного draw call» — проходим дорогу команды рисования целиком, от строчки в вашем коде до пикселя на экране, со всеми четырьмя валютами на местах.
В общем я довольно подробно описал разные части графического конвейера, и не как стандартную схему и так далее, а более практично в контексте движка.
Читать: https://dev-math.ru/articles/drawcall/
#devmath #графика #математика #gpu #drawcall #оптимизация
Всю неделю мы считали, чем платим за кадр: в понедельник — командами, во вторник — байтами на шине, в среду — когерентностью варпов, в четверг — сменами состояния. Обещанная статья готова: «Путь одного 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
https://youtube.com/shorts/Az1gxLJHjQw?si=oIxRUaGmCBWrtM-B
1999 год, первая PlayStation. Команде нужен целый город в реальном времени — а железо не тянет: мир схлопывается в маленький пузырь вокруг героя, и на голой границе дома выскакивают из пустоты прямо на глазах. Конкуренты прятали слабость железа в пре-рендеренных задниках — красивых, но с намертво прибитой камерой. Silent Hill пошла иначе: границу мира просто залили туманом.
Дальше не видно — и не нужно. Всё, что за молоком, не рисуется вовсе, и кадр укладывается в бюджет. А бонусом — страх: силуэт проступает не на горизонте, а в двух шагах. Ограничение железа стало фирменным ужасом жанра.
Так что самый страшный монстр серии появился из-за бюджета кадра.
#devmath #разработкаигр #математикавиграх #computergraphics #gamedev
YouTube
Туман в Silent Hill — это не хоррор. Это оптимизация
1999 год, первая PlayStation. Команде нужен целый город в реальном ...
❤4🔥3👍2
Камера в 3D стоит на месте. Двигается весь мир вокруг неё
Вы летите камерой по уровню в редакторе: жмёте W — едете вперёд, ведёте мышью — поворачиваете. Камера ощущается обычным объектом сцены, просто через неё смотрят. Так и есть — на уровне движка. Но на уровне рендера всё наоборот.
Там камеры нет вообще. Есть точка (0, 0, 0), в которой намертво сидит наблюдатель и смотрит всегда вдоль одной оси. Когда вы «двигаете камеру вперёд», под капотом происходит обратное: весь мир — уровень, враги, ваша собственная модель — сдвигается назад, чтобы наблюдатель остался в нуле. Повернули камеру вправо? Это мир повернулся влево вокруг вас.
Это не метафора и не «ну как бы». Ровно это делает одна из матриц, через которые проходит каждая вершина по пути на экран: она перемещает не камеру к миру, а мир к камере — обратным преобразованием.
И тут важно не перепутать с другим. Живёт это преобразование в вершинной стадии конвейера — в вершинном шейдере, куда каждая вершина заезжает по пути на экран и пересчитывается заново каждый кадр. Оно не двигает объекты в сцене и ничего в мире не меняет — просто выражает их координаты в системе отсчёта камеры.
Зачем так выворачивать, если можно просто хранить позицию камеры? Затем, что «наблюдатель всегда в нуле и смотрит вдоль одной оси» — это удобная система отсчёта, в которой вся дальнейшая математика (проекция, глубина, отсечение) резко упрощается. Дешевле один раз развернуть мир под наблюдателя, чем на каждом шаге таскать за собой, где болтается камера.
Тут же прячется ответ на вопрос, который наверняка зацепил глаз: если камера всегда в нуле и мир едет ей навстречу — выходит, статичные объекты тоже двигаются? И да, и нет. Отметка Static в движке — это про положение объекта в мире: оно зафиксировано, поэтому его матрицу Model (локальные → мировые координаты) можно применить один раз и запечь вершины прямо в мировом пространстве. Отсюда статик-батчинг, запечённый свет, окклюжн. А матрицу View (мир → камера) всё равно применяют каждый кадр ко всему подряд — и к статике, и к динамике. Поэтому неподвижная стена в мире честно стоит на месте, а по экрану едет, когда вы поворачиваетесь: в мире она не двинулась — двинулись вы, то есть система отсчёта. Коротко: Static — про матрицу Model, «мир вокруг камеры» — про View, это разные матрицы.
А что это за матрицы, почему их три и как одна точка доезжает от локальных координат модели до конкретного пикселя — разберём в пятницу.
#геймдев #графика #математика #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 #рендеринг #математикавиграх
https://youtube.com/shorts/2yb0Pn-n94k?si=I6mKOw7dFLzbh3xX
В зеркале Duke Nukem 3D — не отражение, а построенная за стеной вторая комната.
Движок 96-го не умел в отражения: за зеркалом просто копировали комнату целиком, и твой двойник вставал по ту сторону вживую, кадр за кадром.
#devmath #gamedev #dukenukem #рендеринг #математикавиграх
YouTube
Про отражения в Duke Nukem
В зеркале Duke Nukem 3D — не отражение, а построенная за стеной вто...
🔥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 #рендер #камера
Проблема: две далёкие поверхности — стена и налепленный на неё декаль, земля и дорога — начинают дрожать и «драться за пиксель», стоит чуть двинуть камеру. Первое желание — лезть в материалы, сортировку, освещение. А чинится это обычно в настройках камеры.
Виновник — ближняя плоскость 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