Русский
Русский
English
Статистика
Реклама

Игровые движки

Стоит ли в играх перерисовывать только ту часть CANVAS, которая изменилась? Или проще стереть все и нарисовать заново?

15.03.2021 16:12:10 | Автор: admin

Привет. Меня зовут Александр Птичкин. Вот уже 8 лет я обучаю созданию мультфильмов и анимации; 3 года из них я посвятил разработке своего игрового 2.5D HTML5 движка под названием PointJS. За 8 лет вращения в этой индустрии я наработал много материала, которым хочу поделиться тут с вами в блоге. Это мой первый пост на Хабр. Судите строго :) Ваши предложения по улучшению дальнейших статьей пишите в комментариях. Поехали!

Меня часто спрашивают, почему я в своем игровом движке PointJS перерисовываю весь CAVAS заново, а не обновляю только ту часть, на которой произошли изменения.

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

Поначалу звучит очень логично. Где то на краю экрана пробежал вражеский персонаж, и только ту часть CANVAS мы и хотим перерисовать, сэкономив ресурсы. Это было настолько правдоподобно, что я за ночь изрисовал лист A4 в поисках оптимального решения этой задачи. И вот что получилось.

Перерисовывать часть CANVAS мы можем только при выполнении двух условий:

  1. Игровой фон должен быть разбит на части и не являться целым изображением. Судите сами: если фон - целое изображение, то его все равно придется где-то перерисовать. И раз оно цельное, то перерисовать придется полностью, а значит, ни о какой перерисовке отдельной части CANVAS речи быть не может. Дальше в статье вы поймете, что я имею в виду и почему так

  2. Камера в игре должна стоять. Потому что если камера движется, то и вся сцена движется, а значит, всю ее и надо перерисовать

Уже два этих условия выше отсекают уйму игр и механик. А мы еще даже не начали разбираться.

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

Я беру готовую сцену из Maya (персонажей, фоны и анимацию я создаю в программе Autodesk Maya, а потом рендерю в формате 2D). Это облегчает дальнейшую работу со сценой и смену ракурса. Но об этом в другом посте.

Затем отключаю все элементы сцены, кроме земли, и вывожу ее изображение. Рендерю с разрешением 3000 на 3000px.

Но на изображении также есть пустые png-области, которые не нужны. Можно было, конечно, поправить в Maya, но мне лень. Я удаляю их с помощью скрипта на php. Таким образом на выходе у меня ровное изображение размером 2786 на 1998. Да, кстати, я написал уйму полезных скриптов для ускорения работы и для того, чтобы не использовать Photoshop, а делать все быстрее и проще. Получился такой собственный Photoshop, но на php :)

Вообще идея разбить фоновое изображение и другие большие объекты на части уже давно используется в играх (и не только). Например, разработчики Diablo 2 делали то же самое. Ведь у них все было растровое (спрайты), и была создана лишь иллюзия 3D. А идея в следующем. Фоновое изображение может быть 10000px на 10000px, экраны же пользователей чаще всего 1920 на 1080px (мобильные и того меньше). Т.е. за пределами экрана, где мы ничего не видим, все равно будет происходить перерисовка фона. На рисунке СИНЕЕ - это рамка камеры в игре (рамка камеры равна размеру экрана монитора пользователя). Все, что за рамкой, мы видеть не должны.

Нам надо от этого избавляться, ведь лишняя перерисовка - лишняя нагрузка. Даже программа анимации Anime Studio Pro (Moho) начинает висеть, если части тела персонажа состоят из картинок 1920 на 1080 (в программе мы их, конечно, уменьшим, но оригиналы-то никуда не денутся и вес файла тоже). Для человеческой руки достаточно иметь изображение 300 на 300px. Что уж говорить о браузерной игре...

Давайте большое изображение разобьем в Photoshop на равные квадраты или прямоугольники (равные, чтобы потом при сборке не устраивать себе танцы с бубном). Например, разобьем на 8 строк и 8 столбиков. Так мы получили 8*8 = 64 прямоугольника размером 348 на 250px. Как разбить изображение в Photoshop, у меня рассказано в этом уроке.

Теперь у нас есть много картинок gif, из которых можно воссоздать, как пазл, изображение фона. Теперь давайте соберем этот пазл на javascript в нашей игре.

Для этого переименуем все файлы в папке, присвоив им числовые названия, начиная с 1 или 0 (как вам удобно.) Я бы делал с 1, так как потом надо будет делать цикл, начиная с 0, и сам 0 выпадет, потому что к переменной при первой итерации добавится 1. Руками переименовывать бессмысленно и долго, а у нас все-таки 21 век на дворе. На MAC Book я это делаю за один клик в программе Name Manager. Например, есть папка с фотографиями под именем img. Я просто перетаскиваю эту папку на экран программы, указываю номер первой фотографии и жму Rename.

Если будете делать руками, не перепутайте порядок! Когда Photoshop рендерил нам разбитый фон, он делал это тоже в определенном порядке, последовательно проходя циклом строки, а потом столбцы. И если вы будете ставить нумерацию кое-как, то потом нужные куски пазла могут оказаться где угодно. Кстати это неплохой способ зашифровать игру (или усложнить ее чтение), создав свой порядок сборки фоток из папки. Языку Javascript без разницы, в каком порядке пробегать столбики и строки, так что лишней нагрузки вы не добавите. Вернемся к примеру: у нас есть последовательность от 0 до 64 следовательно, 64 кусочка.

Далее пишем на javascript простой код, который соберет все это в единое изображение.

Для этого стоит создать массив под названием fon = [], в который впоследствии поместить все изображения в нужном порядке, и создать переменную xy, которая в будущем станет номером фотографии (xy будет принимать участие в формировании ссылки на путь к файлу).

var xy = 0, fon = [];

Затем создаем два цикла, один из которых будет идти по X и добавлять фотографии по горизонтали, а другой - по Y, переходить на новую строку и тем самым добавлять фотографии по вертикали.

for (var x=0; x<8; x++) {for (var y=0; y<8; y++) {  }}

Цикл по x<8 говорит о том, что изображение было разбито на 8 строк. А цикл по y - что было 8 столбиков. Умножив 8 на 8, мы должны точно получить количество картинок в папке img. А их напомню 64.

При каждой итерации этих циклов мы будем прибавлять единичку к xy и добавлять в массив fon выбранное определенным образом изображение.

xy++;fon.push(изображение1);

В игровом движке PointJS есть метод, который добавляет в игру изображение game.newImageObject({ });. Отмечу, что подробнее о том, как устроен движок, о его командах и уроки по нему вы можете посмотреть в соответствующем плей листе. Итак, теперь мы будем каждый раз добавлять в массив fon новый game.newImageObject.

Давайте посмотрим на код целиком:

var xy = 0, fon = []; for (var x=0; x<8; x++) {for (var y=0; y<8; y++) {  xy++;    fon.push(      game.newImageObject({         file : "img/" + xy + ".gif", // путь к файлу         x : 0 + 348*y, // y это шаг. сначала изобаржени будет по x на 400, потом на 400*2 потом н 400*3         y : 0 + 250*x, // тоже смое и по оси y         w : 348,          h : 250      })    )  }};

Давайте посмотрим на строку file : "img/" + xy + ".gif". Благодаря конкатенации строк и тому, что xy у нас переменная, которая всегда увеличивается на 1, мы формируем в ней динамический путь к изображениям:

file : "img/" + xy + ".gif", // путь к файлу // оригинал// то что вышлоfile : "img/" + 1 + ".gif"// img/1.gif file : "img/" + 2 + ".gif"// img/2.giffile : "img/" + 3 + ".gif"// img/3.giffile : "img/" + 4 + ".gif"// img/4.gif//и так далее...

Получили следующее. Это уже мы смотрим в окне браузера

Теперь я предлагаю вам для наглядности чуть подпортить фон и сделать в формуле выше w=346, а h=248. Это нужно, чтобы между клетками появился зазор 2px, и мы смогли увидеть, из чего все же этот фон состоит.

var xy = 0, fon = []; for (var x=0; x<8; x++) {for (var y=0; y<8; y++) {  xy++;    fon.push(      game.newImageObject({         file : "img/" + xy + ".gif", // путь к файлу         x : 0 + 348*y, // y это шаг. сначала изобаржени будет по x на 400, потом на 400*2 потом н 400*3         y : 0 + 250*x, // тоже смое и по оси y         w : 346,          h : 248      })    )  }};

Это, разумеется, еще не все. Пока мы никак не оптимизировали игру - мы лишь расчленили и заново собрали одно изображение. И даже наоборот: еще больше нагрузили компьютер циклами. А теперь сделаем интересную операцию - будем рисовать только те квадраты, которые видим на экране (т.е. которые попадают в рамку камеры в движке). Для тех, кто не знает, за отрисовку всего, что есть в движке, отвечает команда draw();. Напишем условие:

for (var i in fon) {if (fon[i].isInCamera()) fon[i].draw();}

Смотрим, что получилось. Все блоки, что не попали в камеру (на рисунке они прозрачные), больше не рисуются. Вот это и есть первая оптимизация фонового изображения. Дальнейшие изображения я не смогу показать из окна браузера, так как рамка камеры - это и есть размер экрана, а я физически не могу показать то, что находится за экраном. Но я могу нарисовать это, например, в After Effects. Еще одна полезнейшая программа для жизни аниматора и монтажа.

Однако, если вы помните, мы говорили о другом - как очищать CANVAS? Целиком или кусочком?. Сейчас мы провернули хорошую подготовительную работу, чтобы четко ответить на этот вопрос. Потому что предварительный ответ мы получили в начале статьи: если камера движется, то картинка сменяется полностью и тогда придется рисовать все без исключения.

Приступим. Я предлагаю разместить на нашем верно отрисованном фоне несколько объектов: персонажей, дома и деревья. Теперь представим ситуацию: мы пьем чай и не играем, следовательно, наш персонаж не двигается. Однако вокруг него бегает другой персонаж (наверняка с дурными намерениями). Вот я нарисовал траекторию, по которой он пробежал (сделал за ним шлейф):

Посмотрим, какие блоки он задел и что надо перерисовать на сцене. Эти участки я выделил красным.

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

Обсудим на уровне идеи. Допустим, мы создадим квадрат/прямоугольник по размеру бегущего персонажа. Хотя нет, даже не так. Зачем создавать, когда можно просто вызывать StaticBox() этого объекта? Да, в движке есть команда getStaticBox(); - область загруженного изображения, равная размерам этого изображения. Теперь напишем простое условие, в котором будем проверять, каких объектов сцены коснулся этот StaticBox();. Вы помните, что у нас уже был массив fon = [];? А помните, мы загружали деревья и дома? Их нельзя разместить в массиве fon. Значит, для них создадим отдельный массив obj, и будем проверять сразу оба массива. Итак, пишем код:

var collisionFon = []; // создали массив в котором будем хранить всех с кем столкнулись из фонаvar collisionObj = []; // создали массив в котором будем хранить всех с кем столкнулись из obj for (var i in fon) {if (player2.isStaticIntersect(fon[i].getStaticBox())) {collisionFon.push(fon[i])  }}for (var i in obj) {if (player2.isStaticIntersect(obj[i].getStaticBox())) {collisionObj.push(obj[i])  }}

Уже на этой стадии обработка циклов может отнять у компьютера много ресурсов. Однако мы двигаемся дальше. Я специально на картинке выше провел персонажа так, чтобы он не коснулся дома, но коснулся клетки фона. Зачем? Все просто: если перерисовать в этом месте фон, а потом персонажа, не тронув дома, то фон окажется выше этого дома

И вот главный вывод статьи:

Значит нам нужно проверять еще и родительские связи!

Перерисовав фон и персонажа, нам следует узнать, а не перекроет ли этот фон еще что-нибудь. Т.е. необходимо пробежаться циклом по всем элементам, что попали в массивы collisionFon и collisionObj, и посмотреть, каких еще объектов они касаются на сцене, а точнее - какие объекты находятся над ними. В моем движке есть функция проверки оси Z. По сути там реализована эмуляция оси Z через сравнение осей Y, но сейчас это не столь важно. Самое главное, что это большой блок, который тоже будет поглощать ресурсы. Ведь теперь все блоки, которые предполагается перерисовать, нужно собрать в общий массив, чтобы сравнить их оси Y, чтобы выяснить, кто из них выше, а кто ниже. Короче, я даже формулы писать не буду. Думаю, вы поняли: расчеты будут громоздкими и нагрузят компьютер больше, чем если бы мы просто стерли весь CANVAS и нарисовали его заново. Я даже сомневаюсь, что смогу их провести, а в том, что это безотказно отработает в MMORPG-играх, вообще не уверен. С учетом всех наложений и косвенных (не на прямую) попаданий на здания, к красной зоне я добавил и зеленю область которую тоже нужно перерисовать.

При этом вы заметили, что в примере пробежал только один вражеский персонаж. И мы рассмотрели лишь частный случай, когда игрок стоит на месте, а камера не двигается, чтобы выполнить условие выше. А сколько движухи получилось! Даже если мы все же сделаем расчеты, придется перерисовывать почти половину CANVAS. Так зачем тратить столько сил, менять механику движка, обработав при этом небольшой частный случай и в итоге перерисовав полCANVAS? Я еще понимаю, если бы перерисовке подверглись два-три блока, но, кажется даже при самом простом варианте малой кровью не отделаться. Впрочем, решение, конечно, за вами!


Что ж, на этом я завершаю свою первую статью. Надеюсь, вы не очень утомились. Если есть какие-то мнения по поводу вышеизложенного - пишите в комментариях, обсудим. Повторюсь, в общении рождается истина. А она нужна, чтобы улучшать движок. Всем спасибо и до встречи в новых статьях.

Автор текста и всех материалов статьи: Александр Птичкин, основатель образовательного проекта Mult-uroki. Написано специально для habr.com. Копирование или иное использование материала без письменного разрешения автора запрещено.

Подробнее..

Перевод Клон Doom в 13 килобайтах JavaScript

17.06.2020 08:10:07 | Автор: admin
В прошлом году я участвовал в соревнованиях JS13K 2019, на которых людям предлагается разрабатывать игры в менее чем 13 КБ кода на JavaScript. Я участвовал с клоном Doom, который назвал Ещё один клон Doom (Yet Another Doom Clone).


Поиграть в него можно здесь. Исходный код выложен сюда.

Зачем создавать клон Doom?


Зачем писать FPS на JavaScript всего в 13 КБ (с учётом сжатия)? По нескольким причинам. Но лучше всего на этот вопрос отвечает раздел FAQ соревнований JS13K Можно ли использовать WebGL?:

Да, но может быть сложно уместить его в 13 килобайта, если вы планируете писать FPS.

Кроме того, в то время я как раз написал 3D-рендерер и хотел поработать над ним ещё. К тому же мне нравится создавать сильно сжатый код. (Например, много лет назад я создал язык и написал компилятор для нового языка, предназначенный специально для использования в код-гольфинге.)

Именно поэтому я выбрал FPS. Остаётся вопрос: Почему Doom? На него ответить проще: если вы хотите написать FPS, и чтобы он при этом был небольшим, то Doom практически самый минималистичный вариант.

Причина, по которой Doom настолько прост (по современным стандартам) понятна: Doom должен был работать на железе на пять порядков более медленном, чем сегодня. За ту же цену сейчас можно собрать машину, которая способна выполнять в сотню тысяч раз больше работы, чем Pentium 1994 года. Поэтому я решил, что будет интересно попробовать воссоздать нечто наподобие Doom, но вместо ограничений производительности использовать ограничения объёма кода.


Фундамент: 3D-рендерер на JavaScript


Движок игры я начал строить на основе написанного ранее 3D-рендерера, который я хотел реализовать как можно более простым. Оказалось, что благодаря своей простоте он к тому же довольно мал: ядро движка 3D-рендеринга заняло всего примерно 5 КБ сжатого JavaScript, то есть на игру осталось всего 8 КБ. Я решил, что ситуация не так уж и плоха. Если я не будут использовать большие спрайты или файлы объектов, то всё должно получиться.

Движение игрока



Для перехода от 3D-рендерера к движку 3D-игры, по сути, требуется только одно изменение: управляемое игроком движение камеры и повторный рендеринг после каждого движения. Для реализации этого я поместил игрока внутрь параллелепипеда и работал над движком, пока движение не стало правильным. Doom имел очень характерный стиль движения, который я стремился воссоздать.

Первый шаг к этому реализация ускорения движения игрока в направлении его взгляда до какой-то (высокой) максимальной скорости. Я добавил больше ускорения, чем в оригинальном Doom, потому что сегодня это ощущается привычнее. Важнее для воссоздания ощущения Doom было добавление раскачивания камеры при движении игрока.

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

Если ничего не добавлять, то игрок будет продолжать раскачивание с того самого состояния, в котором он находился во время остановки. Это выглядит очень неестественно. Чтобы это исправить, потребовались крошечные изменения: у игрока есть переменная текущего состояния анимации в цикле (по модулю 2 пи), и когда игрок прекращает двигаться во всех направлениях, то переменная сбрасывается на ноль. Это делает движение более чётким, а его начало и завершение выглядят правильно.

Также я добавил одну особенность, которой не было в Doom небольшую величину поворота камеры вдоль продольной оси на каждом шаге. (В Doom его не было, потому что движок позволял выполнять поворот только по оси Z, то есть игрок даже не мог взглянуть вверх.)

Затем я добавил ограничение движения. Нет никакого смысле создавать сложные лабиринты в стиле Doom, если игрок может проходить сквозь стены. Для правильной реализации ограничений нужно сделать игрока сферой, стены плоскостями и вычислять пересечения сферы с плоскостью. Я беспокоился, что это займёт слишком большую часть наших 13 КБ.

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

Реализовать эту систему было довольно сложно, особенно обработать случаи одновременного пересечения лучей с несколькими стенами. Во некоторых ситуациях система даёт сбои, поэтому при создании дизайна уровней я вручную тестировал их на баги и проектировал их так, чтобы баги распознавания коллизий не возникали. (Главное ведь, чтобы работало...)

Object Lathing



Чтобы в игре были объекты, мне нужно каким-то образом их описать. Логичным способом было бы использование формата файлов .obj. К сожалению, он довольно большой и неуклюжий. (Я использовал его в своём предыдущем 3D-рендерере для загрузки стандартного чайника.)

Поэтому вместо него я решил написать минималистичный метод точёных объектов (object lathe):
мы задаём 2D-контур, представленный в виде ряда точек, обтачиванием вращаем точки по кругу для генерации 3D-объекта. Это позволяет создавать красивые объекты, занимающие мало пространства. Например, для описания показанного выше кувшина потребовалось примерно двадцать байтов.

Задолго до разработки игры я также создал тщательно минимизированную до 300 байт реализацию подразделения поверхностей (surface subdivisioning) (которую ранее использовал для 3D-рендерера). Но в конечном итоге мне не хватило места и от неё пришлось отказаться. Однако я особо не жалею, потому что она не добавляла в игру ничего важного.

Игровой цикл


Всё описанное выше было довольно статичным, по миру перемещалась только камера. Однако для создания игры требуется интерактивность. И здесь нам пригодится игровой цикл.

Мы будем использовать совершенно стандартный игровой цикл. Изначально в коде он выглядел так:

      function game_step(now) {          if (game_is_over) return;          frame = requestAnimationFrame(game_step);  player_check_dead();  player_move();          player_clip_walls();          player_fall();          player_set_position();          lights.map(light => light.compute_shadowmap());          camera.draw_scene();          objects.map(object => object.update(dt));          garbage_collect();      }

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

Оружие и враги



Шутер от первого лица не особо интересен, если нам нечем и не в кого стрелять.

Я начал с реализации стрельбы. Я не был уверен, хватит ли у меня места на несколько видов оружия (в результате его и не хватило), поэтому решил, что в шутере логично будет использовать пулемёт (chaingun) из Doom.

Модель оружия это просто несколько цилиндров, созданных при помощи object lathe. Первым этапом здесь была реализация естественного движения с оружием. Для этого я снова воспользовался уже имеющимся отслеживанием движения вверх-вниз при ходьбе: пулемёт движется вверх-вниз и в стороны, в зависимости от текущего состояния цикла ходьбы игрока.


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

Распознавание попаданий реализовано как простое сканирование испускаемым лучом (в нём используется тот же код, что и в распознавании коллизий со стенами при движении игрока). При попадании создаются частицы взрыва с небольшой вспышкой.

Для сканирования попаданий пришлось внести в систему испускания лучей несколько изменений. Во-первых, поскольку первая версия проверяла коллизии только по горизонтали, мне нужно было теперь проверять, произошла ли коллизия на нужной высоте по Z. Это сделать было не особо сложно: вычисляем, насколько далеко объект от игрока, из наклона игрока по поперечной оси определяем, какой должна быть высота пули, и если она находится в пределах ограничивающего параллелепипеда объекта, то коллизия произошла.

(Чтобы можно было стрелять по потолку и полу, потребовалось ещё одно небольшое дополнение. В противном случае при высоком прицеливании не было бы взрывов, и это казалось бы неправильным. Вся остальная система распознавания коллизий определяет коллизии только по горизонтали, а эти коллизии ориентированы по вертикали, а значит, им требуется специализированный код. На самом деле, написать его было не так сложно: записываем начальную высоту по Z и наклон игрока по поперечной оси (угол относительно плоскости xy), а затем для каждого полигона всей карты проверяем, насколько далеко должен растянуться луч, чтобы попасть по потолку и полу этого полигона; затем проверяем, действительно ли при движении луча на такое расстояние он сталкивается с полигоном в этом месте.)

После этого я сортирую все потенциальные коллизии и выбираю ближайшую.

Для стрельбы также требуется тряска экрана, и, поскольку в игре нет боеприпасов, замедление движения игрока. Важно, чтобы у игрока была причина не держать всю игру палец на спусковом крючке.


Затем я перешёл к задаче стрельбы по врагам. Сначала я создал очень простую модель-коробку персонажа. Он полностью состоит из кубов, благодаря чему его очень легко хранить: сохраняя просто модель куба, я могу растягивать его в произвольных направлениях и создать врага. Простейший враг состоит из восьми кубов.

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

Сначала блоки, из которых состоял враг, просто разлетались сквозь стены и пропадали с экрана. Это сбивало с толку, поэтому я добавил эффект отскакивания блоков от стен. Если говорить кратко, то отскоки совершенно физически недостоверны. Коллизии снова реализованы при помощи пересечения лучей. Если ближайшая стена полностью находится на плоскости xz, то объекты меняют знак свой скорости по y. Если стена полностью на плоскости, то меняется знак скорости по x. Во всех остальных случаях меняются знаки скоростей по x и y.

Когда позиция объекта меньше, чем высота пола текущей комнаты, скорость по z меняет свой знак и уменьшается на 20%. Каждую секунду объект замедляется на 50% (как будто к нему применяется какое-то гипотетическое трение).

ИИ врагов


Несмотря на то, что я изучал и имел дело в работе с "ИИ" (со множеством кавычек), враги в игре довольно неинтеллектуальны. У врагов есть позиция и поворот. Они постоянно ходят туда и обратно, пока не пробудятся по одной из двух причин:

(а) игрок находится в поле их зрения

или

(б) игрок подстреливает врага, находящегося в поле зрения.

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

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

Кроме того, в момент смерти враг запрашивает помощь у других врагов, которых может видеть (снова при помощи испускания лучей). Благодаря этому сложно устранять врагов по одному из-за угла, потому что это скучная механика, и не стоит её поощрять.

Текстуры


Пока картинка выглядит очень скучной, ведь используется всего одна текстура. Я знал, что никак не смогу уместить в игру нарисованные от руки текстуры, даже самые маленькие, поэтому сгенерировал их процедурно. Я решил использовать всего три текстуры: плитки пола, кирпичный паттерн для стен и простой шум Перлина для лавы и взрывов.




Генерация кирпичей довольно тривиальна. Отрисовываем горизонтальные линии через каждые N шагов, а затем вертикальные линии через каждые N со смещением на N/2 в чётных строках. Код выглядит так:

      make_texture(cartesian_product_map(range(256),range(256),(y,x) => {          if ((y%64) <= 2 || Math.abs(x-(((y/64)|0)%2)*128) <= 2) {              return [0,0,0,1];          } else {              var r = .9-perlin_noise[x*256+y]/20;              return [r,r,r,1];          }      }).flat())

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

Шум Перлина визуально гораздо красивее, чем другие типы шумов. Его случайность выглядит намного естественнее, чем простой белый шум. Генерация шума Перлина довольно сложна, поэтому я просто дам ссылку на Википедию и скажу, что сделал так же. Также я добавил небольшое количество шума Перлина к кирпичному паттерну, чтобы он был менее скучным.

Анимации врагов



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


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

Звук


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

Благодаря последовательности небольших изменений мне удалось сократить его почти в два раза. Самая большая экономия получилась благодаря удалению map из массива в воспроизводимый файл WAV и замене его на вызов функции WebAudio, получающей непосредственно массив float. Ещё одна серьёзная экономия получилась благодаря замене куче операторов case на операции поиска в массиве. То есть я реализовал принцип мультиплексора и вычислил все возможные случаи внутри массива, а затем просто выбирал индекс нужного значения. Это медленнее, зато код короче. После замены циклов for на map и создания вспомогательной функции clamp код оказался достаточно коротким, чтобы можно было написать красивые звуковые эффекты.

Затем я захотел сделать так, чтобы аудиосистема реагировала на то, где находится игрок в 3D-пространстве. Я думал, что эта часть потребует много усилий, но оказалось, что это довольно просто, и теперь в коде есть функция createStereoPanner, выполняющая эту задачу.

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

Описание карты



Одной из уникальных особенностей Doom были уровни. Каждая карта больше напоминала лабиринт, по которому игрок бегал в поисках выхода. Для создания клона это был обязательный компонент.

Однако для простого хранения карт потребовалось бы слишком много места: при наивном задании (т.е. при сохранении позиций всех стен) даже несколько простейших уровней занимали более килобайта. Поэтому карты записываются как набор многоугольников (которые могут быть и вогнутыми). У каждого многоугольника есть высота пола и потолка, а если два соседних многоугольника имеют общее ребро, то игрок может пройти из одного в другой (иногда спускаясь ниже или поднимаясь).

Для создания кода, превращающего многоугольники в трёхмерные комнаты, потребовалось довольно много усилий. Хоть и легко разместить стены вокруг каждого многоугольника, основной сложностью стало определение того, когда стены быть не должно из-за двух соседних рёбер. Чтобы упростить процесс, проём создаётся только тогда, когда у двух стен обе координаты совпадают точно.

Изначально я создавал карты, вручную задавая координаты многоугольников, но у такого подхода было две проблемы: во-первых, они занимали много места, и, что более важно, на создание тратилась куча времени. Если я хотел успеть спроектировать достаточное количество карт, то нужно было придумать что-то получше.

Возможно, хорошая идея позволит одновременно решить обе проблемы. Я реализовал очень крошечный язык программирования turtle для задания расположения областей карты. То есть вместо использования черепашки для рисования я применил её для создания многоугольников, заменив карандаш функцией задания многоугольников. Изначально этот язык был очень похож на реализацию принципа черепашки: команды перемещали черепаху, поворачивали её и заставляли двигаться на определённое расстояние.

Я даже добавил функции, позволяющие создавать циклы for и вызовы/возвраты, чтобы можно было, например, изготавливать лестницы, а затем многократно применять функцию make-stairs там, где мне были нужны лестницы.

К сожалению, проектировать карты всё равно было сложно, потому что для этого требовалось программирование черепашки на языке ассемблера, который разрабатывался как максимально компактный. У меня был всего месяц на создание всей игры, поэтому я никак не мог себе позволить потратить несколько дней на проектирование карт. Более того, на самом деле это не экономило особо много пространства. Так как ограничение в 13 килобайта относится к размеру запакованного zip исходного кода, вполне можно было использовать дублирование одной функции: сжатие zip сэкономит пространство.

Поэтому код черепашки стал таким:

      function run_turtle(commands) {          var regions = []          var turtle_location = [ZERO];          var floor_height = 4;          var ceil_height = 40;          var do_later = [];          for (var i = 0; i < commands.length;) {              var cmd = commands[i++];              // Get the "opcode" and "argument" to the command      var [low, high] = [cmd&31, cmd>>5];              // Opcode 0 is a "make polygon" command              if (high <= 1) {                  var coordinates = [turtle_location[0]]                  coordinates.push(...range(low).map(x=> {                      var dydx = NewVector(((commands[i]>>4)-7)*8,                                           ((commands[i++]%16)-7)*8, 0)                      turtle_location.unshift(turtle_location[0].add(dydx));                      return turtle_location[0];                  }));                  regions.push(new MapPolygon(coordinates,                                              floor_height,                                              ceil_height))                  // Opcode 1 is a "goto" command                  if (high == 1) {                      regions.pop();                  }              }              // Opcodes 4: adjust ceiling              floor_height += 2*(low-15)*(high==4);              // Opcodes 5: adjust floor      ceil_height += 4*(low-15)*(high==5);              // Opcodes 6: backtrack              turtle_location.splice(0,low*(high==6));          }          return [regions, do_later];      }

Чтобы упростить создание карт, я избавился от большей части языка и создал гораздо более простой язык. Я убрал циклы, вызовы функций, повороты и реализовал всего два опкода: make-polygon и goto. Опкод make-polygon получает количество вершин и последовательность байтов, где старшие 4 бита определяют сдвиг по оси x, а младшие 4 бита сдвиг по оси y. После достижения последней вершины цикл завершается и черепашка создаёт многоугольник. Опкод move просто перемещает черепашку в новое место для создания нового многоугольника.

После создания нескольких карт я посмотрел, какие части моего языка turtle занимают больше всего места. Я понял, что трачу много места на перемещение черепашки от многоугольника к многоугольнику. Как можно сократить этот процесс? Обратим внимание, что обычно многоугольники не изолированы: они соединяются с другими многоугольниками вершиной. Поэтому я добавил ещё одну функцию backtrack. При каждом движении черепашки она добавляет своё местоположение в стек. Backtrack извлекает позицию черепашки определённое количество значений назад. Это очень эффективно: более 95% перемещений черепашки было заменено командами backtrack.

Редактор карт



Для удобства создания карт я быстро набросал WYSIWYG-интерфейс. Этот редактор компилирует карты на язык turtle, чтобы их можно было использовать в игре.

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

Так как этот редактор не будет находиться в составе игры, я мог не волноваться о качестве кода или удобстве редактора. Он невероятно уродлив и некрасив, в нём используются непонятные горячие клавиши. (Хотите добавить новый многоугольник? Выберите созданную вершину и нажмите Shift-A для выделения соседнего ребра, а затем E (для экструдирования). Хотите добавить вершину к уже существующему ребру? Shift-A и S (для разбиения). Нужно удалить многоугольник? Shift-Q. Если знать, что делать, то всё вполне логично, но не особо интуитивно понятно.)

Мысли об экономии пространства


Для успешного создания полной игры в 13 килобайтах сжатого JavaScript требуется постоянно помнить о занимаемом кодом пространстве.

Основная цель заключается в том, чтобы всегда понимать размер кода. Я загрузил скрипт на Python для отслеживания каждого изменения файла исходников и пересборки пакета. Он показывал количество свободных байтов, оставшихся для самой игры, чтобы они всегда были видимы и я мог понять, стоит ли внесённое изменение повышения сложности.

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

      var pairs = (lst,fn) => lst.slice(0,-1).map((x,i)=>fn(x,lst[i+1],i))      var transpose = (mat) => mat[0].map((x,i) => mat.map(x => x[i]))      var range = (N,a) => Array(N).fill().map((_,x)=>x+(a||0));      var reshape = (A,m) =>          range(A.length/m).map(x=>A.slice(x*m,(x+1)*m));      var urandom = _ => Math.random()*2 - 1;      var push = (x,y) => (x.push(y), x);      var clamp = (x,low,high) => Math.min(Math.max(low, x), high)      var sum = (x) => x.reduce((a,b)=>a+b)

Также я реализовал собственную математику для матриц и векторов, чтобы код был максимально сжатым. Все повороты задаются как произведения матриц 4x4: гораздо более эффективными были бы кватернионы, но они занимают больше места, поэтому я их не использовал.

В самом некрасивом моём коде я сделал так, что умножения матриц 4x4 стали и очень эффективными, и одновременно очень короткими; для этого пришлось потрудиться:

    var mat_product_symbolic = B =>        `(a,b)=>[${reshape(range(16),4).map(c=>B[0].map((_,i)=> B.reduce((s,d,j)=>`${s}+b[${d[i]}]*a[${c[j]}]`,0))).flat()}]`;    multiply = eval(mat_product_symbolic(reshape(range(16),4));

Для постоянного отображения текущего размера сборки требовался автоматизированный процесс сборки. Сначала скрипт сборки просто запускал uglifier, за которым выполнялся стандартный zip, чтобы я мог отслеживать занимаемое пространство. Вскоре я осознал, что есть оптимизации, которые позволят мне автоматизировать процесс, чтобы ещё больше сжать код. Я написал короткий скрипт, определяющий все имена переменных webgl и заменяющий их короткими однобуквенными именами (потому что uglify этого не делал). Затем я модифицировал эту программу так, чтобы она переписывала длинные имена функций WebGL, например, framebufferTexture2D, коротким трёхсимвольным кодом вида e2m (я брал восьмой символ с конца, второй символ с конца и третий символ с начала). Ближе к концу разработки я узнал о advzip, который ещё больше помог мне с улучшением сжатия.
Подробнее..

Перевод Рендеринг кадра Cyberpunk 2077

18.12.2020 14:16:13 | Автор: admin

Введение


Инженеры по рендерингу подвержены двум проклятьям. Во-первых, мы теряем способность смотреть на реальность, не вспоминая постоянно, как удивительно сложно вычислять распространение света и моделировать материалы.

Во-вторых, когда мы начинаем играть в любую игру, мы не можем удержаться от того, чтобы выполнить реверс-инжиниринг её технологий рендеринга (что особенно бесит в многопользовательских играх да хватит в меня стрелять, я сюда пришёл, чтобы просто посмотреть, как камни отбрасывают тени!).

Поэтому когда я купил Cyberpunk 2077, мне обязательно нужно было посмотреть, как он рендерит кадр. Выполнять захваты этой игры через RenderDoc очень легко, так что у меня не было отговорок.

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

Эта статья ни в коем случае не является серьёзной попыткой реверс-инжиниринга.

Для этого мне не хватает ни времени, ни таланта. Кроме того, я оправдываю свою некачественную работу следующим: на самом деле так лучше.

Я думаю, лучше мечтать о том, каким может быть рендеринг (да и вообще что угодно), только слегка вдохновляясь от внешних источников (в данном случае это захваты RenderDoc), а не знать точно, что происходит внутри.

Если мы знаем, то мы знаем, больше не остаётся никакой тайны. Именно незнание заставляет нас мыслить, и иногда мы в точности угадываем то, что происходит, но в других случаях мы можем достичь чего-то лучшего придумать нечто новое Разве это не чудесно?

В статье по большей мере представлено изучение одного захвата. Я открывал второй, чтобы заполнить некоторые пробелы, но пока это всё.

Захваты выполнялись при высоких настройках графики, без RTX и DLSS, поскольку RenderDoc их не поддерживает (возможно, пока?). Я отключил Motion blur и другие неинтересные постэффекты и сделал так, чтобы игрок перемещался на всех захватах. Это даёт чуть больше понимания о передаче доступа к данным предыдущих кадров.

Кроме того, не использовалась никакая инсайдерская информация, так проще и веселее.

Основы


На первый взгляд, описать ядро рендеринга Cyberpunk 2077 можно очень кратко. Это классический отложенный рендерер (deferred renderer) с довольно ванильной схемой g-буфера. Мы не видим здесь безумного количества буферов, как, например, в Spiderman компании Suckerpunch, выпущенной на PS4. Нет здесь и сложной упаковки битов и реинтерпретации каналов.


  • Нормали формата 10.10.10.2 с 2-битным альфа-каналом, зарезервированным для того, чтобы помечать волосы.
  • Albedo в формате 10.10.10.2. Непонятно, что здесь делает альфа-канал, похоже для всего отрисовываемого он равен единице, но, возможно, так только в тех захватах, которые у меня есть.
  • Metalness, Roughness, Translucency и Emissive в формате 8.8.8.8 (в порядке RGBA)
  • Z-буфер и стенсил-буфер. Последний, похоже, используется для изоляции типов объектов/материалов. Движущиеся объекты помечены. Кожа, автомобили, растительность, волосы, дороги. Сложно понять значение каждой части, но общий смысл вам ясен...

Если взглянуть на кадр хронологически, то он начинается с множества отрисовок UI (которые я не исследовал), множества копий из буфера CPU в константы вершинных шейдеров, после чего идёт обновление карт теней (подробнее об этом позже) и, наконец, предварительный проход глубин.


Этот предварительный проход глубин частичен (не отрисовывает всю сцену) и используется только для снижения объёма перерисовки в последующем проходе g-буфера.

По сути, во всех отрисовках геометрии используется инстансинг и некая разновидность bindless-текстур. Могу предположить, что это была большая часть обновления движка из The Witcher 3 для адаптации под современное оборудование.

К сожалению, из-за bindless смотреть на захват renderDoc довольно напрягающе при выборочной проверке я не могу найти особо много разных шейдеров в проходе g-буфера. Вероятно, это признак того, что художникам не разрешили создавать шейдеры при помощи визуальных графов?

Другие предположения: я не вижу в g-буфере никакой сортировки спереди назад, а предварительный проход глубин рендерит все типы геометрии, не только стены. Похоже, для этого нет никакой обработки (кистей, образующих BSP), а художники не размечали вручную объекты для предварительного прохода, поскольку в рендер попадают довольно плохие перекрывающие объекты. Предполагаю, что после усечения список объектов сортируется по шейдеру, а далее инстансы отрисовки динамически формируются в CPU.

Титры в начале игры не упоминают технологию Umbra (которая использовалась The Witcher 3), поэтому предположу, что CDPr реализовала собственную систему видимости. Её эффективность очень сложно оценить, поскольку видимость это проблема балансировки GPU и CPU, однако, похоже, в захвате присутствует довольно много отрисовок, не вносящих вклад в изображение, хотя за это я не ручаюсь. Кроме того, похоже на то, что иногда рендеринг может отображать скрытые комнаты, поэтому, движок, кажется, не использует систему ячеек и порталов. Думаю, что для таких больших миров художникам непрактично выполнять большой объём ручной работы для вычисления видимости.


Наконец, я не вижу никакого усечения, выполняемого на стороне GPU, с использованием пирамид глубин и тому подобного; нет усечения на уровне треугольников или кластеров, как и нет прогнозируемых отрисовок (predicated draws), поэтому предполагаю, что всё усечение по пирамиде видимости и перекрытию выполняется на стороне CPU.

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

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

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

Декали также используют в качестве входной информации стенсил глубин; сложно сказать, для чего для чтения глубин, стенсила, или того и другого. Но поскольку большинство декалей создано на основе мешей, им должна требоваться глубина, поэтому предположим, что здесь используется стенсил


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


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

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

Обратите также внимание на пятна в буфере нормалей. Вероятно, это артефакт упаковки нормалей, поскольку они, похоже, не соотносятся с пятнами на готовом изображении.


Освещение, часть 1: аналитические источники освещения


Очевидно, что анализ отложенного рендеринга не может закончиться на g-буфере; мы разделили затенение на две части, поэтому теперь нужно посмотреть, как реализовано освещение.

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


Тем не менее, достаточно очевидно, что после выполнения всей работы с буфером глубин/g-буфером, наступает черёд этапа суммирования всего, на котором выполняется множество операций, связанных с глубинами.

Сначала он упаковывает нормали и roughness в RGBA8 при помощи кодирования нормалей на основе таблиц поиска по принципу best-fit (эта техника создана компанией Crytek), затем создаёт mip-пирамиду min-max значений глубин.


Затем пирамида используется для создания чего-то, напоминающего объёмную (volumetric) текстуру для кластерного освещения.


То есть, исходя из увиденного, это похоже на кластерную систему отложенного освещения.

Кажется, кластеры это фрагменты 32x32 пикселя в экранном пространстве (фрокселы, froxels) с 64 z-срезами. Однако похоже, что освещение выполняется с разрешением в 16x16 тайлов, и целиком реализовано при помощи косвенного выполнения вычислительных шейдеров.

Рискну предположить, что так получилось из-за того, что вычислительные шейдеры специализируются на материалах и свете, присутствующих в тайле, а затем выполняются в соответствующем порядке такая схема часто используется в современных системах отложенного рендеринга (см., например, презентации Call of Duty Black Ops 3 и Uncharted 4 на эту тему).

На выходе прохода аналитического освещения получаются два буфера RGBA16; похоже, это вычисления diffuse и specular. Учитывая выбранные мной опции освещения сцены, не удивлюсь, что в ней присутствуют только прожекторные/точечные/сферические источники света и линейные/капсульные источники. Большинство источников освещения в Cyberpunk неоновые, поэтому поддержка линейных источников просто обязательна.

Также можно заметить, что большая часть освещения не подвергается затенению; кроме того, кажется, я ни разу не видел нескольких теней под одним объектом/аватаром. Я уверен, что движок не имеет ограничений в этом отношении, но все источники освещения тщательно прорабатывались авторами, аккуратно расставлявшими отбрасывающие тени источники. Не удивлюсь, если источникам освещения вручную задавали ограничивающие объёмы, чтобы избежать утечек освещения.


Освещение, часть 2: тени


Но увиденное нами выше не означает, что в Cyberpunk 2077 используются простые тени, всё совсем наоборот определённо использовалось множество трюков, и большинство из них не так просто подвергнуть реверс-инжинирингу!

Во-первых, перед предварительным проходом глубин происходит серия отрисовок в нечто, напоминающее карту теней (shadowmap). Подозреваю, что это CSM (каскадные карты теней), но в изученном мной захвате они нигде не используются, в них только выполняется рендеринг. Это указывает на то, что система обновляет карты теней на протяжении нескольких кадров (вероятно, с учётом только статичных объектов?).


Эти эффекты, растянутые на несколько кадров, сложно передать в захвате, поэтому нельзя понять, существуют ли другие системы кэширования (например, см. тени Black Ops 3, сжатые в деревья квадрантов).

Интересным выглядит один аспект если достаточно быстро перемещаться по уровню (например, на машине), то можно заметить, что теням нужно время, чтобы догнать вас, и они постепенно усиливаются довольно любопытным образом. Похоже на то, что применяется смещение глубины с точки зрения солнца, которое со временем ослабляется. Интересно!


Тени от солнца вычисляются заранее и записываются в буфер экранного пространства перед проходом вычисления освещения; думаю, это нужно для упрощения вычислительных шейдеров и более оптимальной нагрузки на GPU. Этот буфер генерируется в проходе, задействующем довольно много текстур, две из которых выглядят похожими на CSM. Одна из них это точно CSM, в моём случае с пятью элементами в массиве текстур, где срезы с 0 по 3 являются разными каскадами, а последний срез выглядит таким же, как и срез 0, но немного с другой точки зрения.

Если кто-то возьмётся за эту работу, то ему хорошенько придётся потрудиться над реверс-инжинирингом!


Все остальные тени в сцене представлены в некой форме VSM (variance shadow maps), многократно вычисляемых инкрементно в течение времени. Я видел, что используются карты размером 512x512 и 256x256, а в моих захватах на кадр рендерились пять карт теней, но предполагаю, что это зависит от настроек. Большинство из них, похоже, используются только как render target, поэтому, опять же, может оказаться так, что для завершения их рендеринга требуется несколько кадров. Одна из них размывается (VSM) в срез массива текстур я видел некоторые такие массивы с 10 срезами и с 20 срезами.


Наконец, в захватах есть то, что в настройках игры называется контактными тенями (contact shadows) тени малой дальности в экранном пространстве, вычисленные при помощи raymarching-а. Похоже, что они вычисляются самими шейдерами вычисления освещения, что логично, ведь они знают об источниках освещения и их направлениях

В целом, тени и просты, и сложны одновременно. Схема создания с CSM, VSM и опциональным raymarching-ом не особо удивляет, но уверен, что дьявол таится в деталях о том, как все они генерируются и подвергаются постепенному усилению. Очевидные артефакты в игре встречаются редко, за что стоит поблагодарить всю систему, которая хорошо справляется с работой даже в таком открытом мире!

Освещение, часть 3: всё остальное...


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


Во-первых, есть очень хороший проход SSAO половинного разрешения. Он вычисляется сразу после описанного выше общего прохода суммирования и использует упакованные в RGBA8 нормали и roughness, а не из g-буфера.

Похоже, что он вычисляет наклонные нормали и конусы апертур. Конкретную технику вычисления определить невозможно, но это определённо что-то типа HBAO-GTAO. Сначала глубина, нормали/roughness и векторы движения подвергаются даунсэмплингу до половинного разрешения. Затем проход вычисляет Ambient Occlusion текущего кадра, а последующие выполняют двунаправленную фильтрацию временное репроецирование. Паттерн дизеринга тоже довольно равномерный, предположу, что это градиентный шум Жорже.

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


Во-вторых, нужно рассмотреть косвенное освещение. После прохода кластеризации освещения есть серия отрисовок, обновляющих массив текстур, похожих на развёрнутые сферически (или двойным параболоидом?) зонды. Всё это снова распределяется на несколько кадров, не все срезы этого массива обновляются в каждом кадре. В захватах легко заметить, что некоторые части массива зондов обновляются данными новых зондов, генерируя на лету mip-карты, предположительно, с предварительной фильтрацией GGX.


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

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


Здесь используется стандартное объёмное освещение, вычисленное в 3D-текстуре с временным репроецированием. Raymarching ограничивается глубиной сцены, вероятно, для экономии ресурсов, но это, в свою очередь, иногда может приводить к утечкам и артефактам репроецирования. Однако в большинстве случаев они малозаметны.


Здесь всё снова становится очень интересным. Сначала используется потрясающий проход отражений в экранном пространстве (Screen-Space Reflection), на котором опять используется буфер упакованных нормалей/roughness, а значит, поддерживаются размытые отражения; всё это выполняется в полном разрешении (по крайней мере, при моих настройках графики).

В нём применяются данные цвета предыдущего кадра до композитинга UI (для репроецирования используются векторы движения). И получается довольно много шума, даже если для дизеринга применяется текстура синего шума!


Далее идёт косвенное diffuse/ambient GI (Global Illumination). Используется g-буфер и серия объёмных текстур 64x64x64, которые сложно расшифровать. Исходя из входных и выходных данных, можно предположить, что объём центрирован относительно камеры и содержит индексы какого-то вычисленного свечения, возможно, сферических гармоник или чего-то подобного.

Освещение очень мягкое/низкочастотное и косвенные тени в этом проходе не особо заметны. Возможно, это даже динамическое GI!

Оно точно является объёмным, это даёт преимущество однородности освещения для всех объектов, как подвижных, так и статических, и эта согласованность видна в игре.


Наконец, выполняется общий композитинг всего: зондов specular, SSR, SSAO, diffuse GI, аналитического освещения. Этот проход снова создаёт два буфера, один из которых походит на окончательное освещение, а второй содержит только то, что похоже на части specular.

И здесь мы видим то, о чём я говорил в начале. Основная часть освещения берётся не из аналитических источников освещения! Здесь мы не видим привычных трюков со множеством заполняющих источников, добавленных художниками (хотя дизайн освещения определённо выполнен очень тщательно) бОльшую часть сцены создаёт косвенное освещение. Это косвенное освещение не такое точное, как в движках, более активно использующих запекания GI и сложное кодирование, но оно очень однородное и позволяет сохранить высокочастотные эффекты при помощи двух очень высококачественных проходов экранного пространства, проходов AO и отражений.



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

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

Всё остальное


В движке есть множество других эффектов, которые мы не будем рассматривать ради краткости статьи и чтобы я не сошёл с ума. Очень интересны волосы, похоже, что рендерится несколько срезов глубин и они частично вставляются в g-буфер с предварительно вычисленным освещением и странным эффектом нормалей (фальшивая анизотропия?). Ещё один важный эффект, который я не буду рассматривать это затенение просвечивания/кожи.


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

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


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


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

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



Я написал в конце, потому что дальше я не разбирался. Например, я не изучал подробности стека постэффектов, там нет ничего особо неожиданного. Его большой частью, разумеется, является Bloom, который почти добавляет ещё один слой косвенного освещения. Разумеется, он реализован качественно, стабилен и обширен.

Разумеется, там есть эффект глубины поля зрения, тональная коррекция и автоматическая выдержка Также присутствуют все эффекты деградации изображения, которые можно ожидать от игр и которые вы скорее всего захотите отключить: зерно плёнки, lens flares, motion blur, хроматическая аберрация Даже композитинг UI выполняется нетривиально, всё реализовано на вычислительных шейдерах, но у меня нет времени на анализ Теперь, сняв этот груз с души, я могу, наконец, попробовать насладиться игрой! Пока!
Подробнее..

Перевод Вычисляем баллистические траектории в играх

02.02.2021 12:18:59 | Автор: admin

При разработке видеоигр часто встречается задача вычисления угла выстрела для попадания в цель. Она настолько распространена, что я писал код для её решения в буквальном смысле для каждой игры, над которой работал.

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

Уравнения движения


Задача всегда начинается одинаково. У нас есть стреляющий и цель: под каким углом нужно стрелять снарядом, чтобы он поразил цель?

Существует четыре основных уравнения движения. В статье мы воспользуемся только одним.


Если объяснять на словах, то конечная позиция РАВНА исходной позиции ПЛЮС скорость, умноженная на время ПЛЮС половина ускорения, умноженная на время в квадрате. Это простое уравнение, для его решения необходимо немного алгебры и несколько тригонометрических тождеств.

Освежим знания


Прежде чем начать, давайте вкратце освежим память.


Если дан снаряд с постоянной скоростью S и углом выстрела (theta), то мы можем вычислить компоненты скорости x и y. Или если есть S и мы каким-то образом найдём y, то можем вычислить и x.

Мы используем алгебру.

Algebra

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


Дальность


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

Существует очень простое уравнение максимальной дальности на плоской поверхности. Мы сразу же ринемся в омут с головой и начнём с обобщённого вида.

Если дан снаряд с постоянной скоростью (S) и гравитацией (G), то какой будет его максимальная дальность полёта?



  1. Подставим известные нам переменные (y0, S, G) в основное уравнение движения.
  2. Применим формулу корней квадратного уравнения. Отбросим меньшее значение.
  3. Подставим t в x = S*cos *t и упростим.

Демо


Для тестирования и визуализации я создал демо на Unity. В нём используются чайники, стреляющие чайниками. Пиф-паф!

Демо: Unity-демо в WebGL

В демо есть несколько ползунков. В видео показан индикатор дальности стрельбы нашего чайника-турели. При увеличении скорости увеличивается дальность. При снижении гравитации дальность тоже растёт. Всё довольно просто.


Угол стрельбы для попадания по неподвижной мишени


Теперь начинается интересное.

Если снаряд имеет постоянную скорость (S), а гравитация равна (G), то под каким углом его нужно выстреливать, чтобы попасть в неподвижную мишень?


Бах. Теперь у нас есть два уравнения и два неизвестных. Давайте их проанализируем.

  1. Первое уравнение, два неизвестных (t, )
  2. Второе уравнение, два неизвестных (t, )
  3. Вычислить t из (1)
  4. Подставить (3) в (2)
  5. Тригонометрическая подстановка: sin /cos = tan
  6. Тригонометрическая подстановка: 1/(cos )^2 = 1 + (tan )^2
  7. Развернём и преобразуем
  8. Формула корней квадратного уравнения
  9. Умножим верхнюю/нижнюю часть на -S^2/x. Перенесём S^4/x^2 под корень
  10. Применим к каждой части арктангенс

Та-да! В результате мы получили два угла. Один высокий и один низкий. Вот как это выглядит на практике.


Визуальное несовершенство


Взгляните на показанный выше gif. Когда чайник начинает стрелять, всё выглядит довольно неплохо. Высокая дуга красива и радует глаз. Низкая дуга кажется чёткой и эффективной.

Однако при увеличении дальности всё становится не таким красивым. Низкая дуга почти плоская. Высокая дуга чрезмерно высока. В этом и заключается проблема снаряда с постоянной скоростью. Он выглядит красиво, только когда цель находится на границах его радиуса дальности.

Существует ли способ получше?

Скорость горизонтального перемещения


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

Такой подход имеет множество преимуществ. Во-первых, он всегда выглядит красиво!

Во-вторых, его дизайн более интуитивен. Дизайнеров не волнует абсолютная скорость. Им важно, что турель имеет дальность 20 метров и что для перемещения на это расстояние снарядам требуется 1 секунда. Они не обязаны пользоваться строящим графики калькулятором, чтобы менять значения баланса. А художественные изменения не должны влиять на геймплейные механики.

В-третьих, так проще попадать по движущейся мишени. Чуть позже я раскрою это подробнее.

Вот как это выглядит:


Вычисление скорости горизонтального перемещения


Если дан снаряд с горизонтальной скоростью (S) и пиковой высотой (y_peak), то какими должны быть скорость и гравитация для поражения неподвижной мишени?


  1. Основное уравнение движения
  2. Решаем (1), подставив 2
  3. Зададим, что y_peak (пользовательская константа) снаряд достигает во время (1/2)t
  4. Зададим, что y_end (высота цели) снаряд достигает во время t
  5. Магия!
  6. Ещё магия!
  7. Вектор стрельбы равен (S, v.y) с гравитационным ускорением g

Вуаля! Хотя постойте-ка. Магия? Это жульничество! Да, но вполне оправданное.

Пункты (3) и (4) это ещё два уравнения с двумя неизвестными. Я ленивый и не хочу их записывать. Плюс я запутаюсь и перепутаю знак, поэтому позволю компьютеру решить их за меня.

Точнее, я воспользовался Wolfram Alpha. Рекомендую каждому иметь Wolfram в своём инструментарии, он довольно полезен.


Если a+c == 2b, то y0, y_peak и y_end лежат на одной прямой. То есть мы стреляем по прямой.

Скорость горизонтального перемещения при подвижной мишени


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

Именно здесь проявляются все достоинства скорости горизонтального перемещения. Задав скорость в плоскости земли, очень просто выполнить вычисления для подвижной мишени.


  1. Где X позиция мишени, а V её скорость
  2. Возводим обе части в квадрат.
  3. Преобразуем в квадратное уравнение
  4. Применяем формулу корней квадратного уравнения

Пункты с 5 по 9 см. в предыдущем разделе.


Меня это очень радует. Пиу-пиу-пиу!

Постоянная скорость с подвижной мишенью


А что если нам нужно поразить подвижную мишень снарядом с постоянной скоростью? Ой-ёй. Это очень запутанная задача! Даже не знаю, как к ней подступиться.

За всю мою карьеру мне не доводилось её решать. Обычно в играх не нужна точная артиллерия. Это просто неинтересно! Вместо этого мы приблизительно вычисляем будущую позицию и целимся в случайную точку рядом с ней. Игроки воспринимают артиллерийский огонь как дождь из глупых снарядов, а не как гарантированную смерть с лазерным наведением.

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

Уравнения четвёртой степени


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


Квадратные уравнения имеют простое и изящное решение в виде формулы корней квадратного уравнения. Кубические уравнения решаемы несколькими разными способами. Однако уравнения четвёртой степени это настоящая головная боль.

Решение таких уравнений находится далеко за рамками этой статьи. Честно говоря, и за пределами моих математических способностей. К счастью для нас, в книге 1990 года Graphics Gems I есть код для решения уравнений четвёртого порядка. Я использовал этот код для своего демо. Не могу гарантировать его точности и численной устойчивости, используйте его крайне осмотрительно.

Способ первый


Итак, давайте его решим. Каким должен быть угол выстрела снарядом с постоянной скоростью по движущейся мишени? Этот способ взят из поста 2007 года Джеймса Макнейлла и дополнен информацией Райана Джакетта.


  1. Где P позиция мишени, а V скорость мишени
  2. Возводим обе части в квадрат
  3. Преобразуем
  4. Вычисляем коэффициенты уравнения четвёртого порядка и вставляем в SolveQuartic
  5. Используем t для вычисления позиции мишени при вычислении траектории до неподвижной точки.

Способ работает. Все сложные задачи выполняет SolveQuartic. Затем мы используем решение для неподвижной мишени, изложенное выше.

Способ второй


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


Чёрт возьми. 32 шага!? Это хуже, чем кажется.

17 объявляем переменные.

811 объявляем систему уравнений. Четыре уравнения, четыре неизвестных d, e, f, t.

1215 вычисляем по (8) величину d. Перемножаем d^2 на будущее.

1619 вычисляем по (10) величину f. Перемножаем f^2 на будущее.

2024 вычисляем по (9) величину e. Перемножаем e^2 на будущее.

2527 вычисляем по (11) величину e^2. Подставляем d^2 и f^2.

2830 приравниваем (27) к (24). Умножаем на t^2 и преобразуем в уравнение четвёртой степени.

31 подставляем коэффициенты в SolveQuartic.

32 подставляем положительные вещественные корни в (14), (18), (23) для d, e, f.

Код довольно короткий. Объявлению переменных отведено больше строк, чем самим вычислениям! Разумеется, кроме SolveQuartic.



Предупреждение


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

Рассматривайте этот код не как готовое решение, а как опорную точку.

Инструменты


При создании этого поста я использовал несколько инструментов. Многие из них были для меня новыми.


Синтаксис LaTeX ужасен, его сложно учить. Все формулы LaTeX можно найти здесь. Вот пример:


Заключение


Вот и всё. Я потратил на этот пост гораздо больше времени, чем ожидал. Я решил задачу, которую никогда не решал прежде и изучил несколько новых инструментов. И это того стоило.

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

Исходный код

Проект Unity

Unity-демо в WebGL
Подробнее..

Детские шалости как Roblox стала одной из самых дорогих игровых компаний современности

17.03.2021 12:07:26 | Автор: admin

Убыточная детская платформа для создания компьютерных игр Roblox с третьей попытки вышла на биржу, и её рыночная оценка тут же побила все самые смелые прогнозы. Сейчас она стоит больше, чем Take-Two (издатель Grand Theft Auto) и Ubisoft (разработчик серии Assaisins Creed) вместе взятые. Под катом о том, как стартап, который часто называют новым Minecraft, смог добиться такого результата.

Добро пожаловать в метавселенную

Roblox развлекательная платформа, которую придумал американский предприниматель Девид Базуки. Часть её геймплея построена на создании новых игр и миров, которыми пользователи делятся с друзьями и незнакомцами. Название продукта составное: это словослияние robot и blocks.

Чтобы разобраться в том, что это за продукт, нужно отмотать до 1989 года. Именно тогда Девид Базуки вместе со своим братом Грегом основал компанию KnowledgeRevolution. Она разработала двухмерный симулятор физических экспериментов, который стали использовать в американских школах.

Программа представляла собой полигон, на котором с помощью рычагов, планок, блоков и летающих снарядов пользователь моделировал реальные явления. Хотя это и был образовательный софт, школьники всё равно воспринимали его как игру. Довольно быстро Базуки понял, что пользователи делают вещи, выходящие за пределы учебника физики. Они моделировали автокатастрофы, падение зданий и проводили другие опыты. В 1998 году продукт за 20 млн $ купила компания MSC Software. В 2002 году Девид занимал в ней пост вице-президента и главного менеджера, но покинул это место, чтобы заняться собственными проектами и инвестициями.

В интервью Forbes Базуки говорил, что после сделки с MSC мог уйти на покой и больше никогда не работать, но решил применить накопленные знания и запустить новый продукт, в котором люди могли бы придумывать свои интерактивные миры, создавать их и делиться ими с другими. Разработка первой версии проекта заняла всего полтора года. Уже в 2006 году состоялся релиз.

Девид БазукиДевид Базуки

Новый продукт совместил в себе черты видеоигры и соцсети. Сам Базуки характеризует его, как иммерсивную развлекательную платформу. Среди её конкурентов часто называют Minecraft, однако делают это зачастую по незнанию. Главное отличие Roblox от других игроков рынка в том, чтоэто не песочница с открытым миром, а полноценный, поддерживающий программирование на Lua конструктор для создания и продажи собственных видеоигр.

В 2017 году Roblox стал самой популярной в США площадкой для аудитории до 18 лет. Согласно данным comScore, ежемесячно дети проводили в Roblox 51,5 млнчасов. Это больше, чем в YouTube (19,4 млн), Netflix (3,4 млн), Steam (0,52) и Reddit (0,07) вместе взятых.

Roblox построен так, что учит детей не только весело проводить время с друзьями, но и зарабатывать деньги своим интеллектуальным трудом. В 2020 году компания планировала выплатить авторам самых интересных игр 250 миллионов долларов.

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

Инструменты для создания миров бесплатны, как и большинство доступных игр. Основную часть прибыли Roblox получает от необязательных покупок, за которые пользователи рассчитываются игровой валютой robux (4,99 $ = 400 robux). Создатели игр получают выплаты в этой же валюте но могут сконвертировать её в настоящие доллары.

Есть у Roblox и образовательный потенциал. В Китае корпорация Tencent выпустила Roblox Education Edition это версия платформы, которая должна помочь учителям и их студентам в обучении программированию, гейм-дизайну и другим областям знаний, связанным с наукой, технологиями, проектированием и математикой.

Убытки при растущем спросе

Выручка компании стабильно росла все последние годы:

2018

325 млн долларов

2019

508 млн долларов

2020

925 млн долларов

Несмотря на быстрый рост, как и многие стартапы-единороги, Roblox убыточен:

2018

97,2 млн долларов

2019

86 млн долларов

2020

203 млн долларов за первые три квартала. Годом ранее за схожий период было потеряно всего 46,3 млн

Приложение Roblox доступно на большинстве актуальных платформ: Windows, macOS, iOS, Android, Xbox One. Под конец 2020 года количество активных игроков достигло 199 млн человек, 8 млн из которых разработчики, создавшие около 20 миллионов игр. 345тыс. таких разработчиков зарабатывают деньги, размещая контент на платформе. Чтобы понимать масштабы, можно сравнить с Minecraft: в прошлом году в него ежемесячно играл 131 млн человек.

Одна из причин растущего спроса карантины из-за эпидемии коронавируса в 2020 году. Дети, у которых пропала возможность общаться в реальности, стали гулять в виртуальных метавселенных, построенных внутри Roblox. Девид Базуки считает, что часть социальных активностей, к которым мы привыкли, в 2021 году серьёзно трансформируется:

В 2021 году мы увидим изменения в том, как люди играют, работают, учатся или просто проводят время. Некоторые из этих связей переместятся в Metaverse цифровое место, где люди собираются и взаимодействуют между собой в миллионах виртуальных 3D-приключений.

Девид Базуки в своей колонке для Wired

Как считает предприниматель, интерактивные ивенты для большого количества людей скоро станут нормой. Вероятно, исходя из этих мыслей в конце прошлого года в Roblox провели интерактивный концерт рэпера LilNasX.

Выход на биржу

Перед мартовским публичным размещением компания дважды пыталась выйти на биржу.

Сначала в декабре 2020 года, но листинг отложили после громких IPO Airbnb и DoorDash. Тогда в Roblox посчитали, что лучше провести размещение в более спокойный момент, когда внимание инвесторов не будет приковано к другим компаниям. Вторая попытка была в январе 2021, но тогда вмешалась Комиссия по ценным бумагам и биржам, которая попросила Roblox изменить методологию подсчёта выручки.

Быстрый рост платформы был невозможен без внушительных инвестиций. В январе 2021 года компания привлекла 520 млн долларов в раунде серии H, который возглавили Dragoneer Investment Grop и Altimeter Capital. После этого рыночная оценка компании резко выросла с 4,15 млрд долларов до 29,5 млрд.

На этом стремительный взлёт не закончился: кульминацией для Roblox стал выход на биржу в марте. В первый же день компанию оценили в 45 млрд долларов. Для сравнения, оценка Valve оператора сервиса Steam и разработчика игр Half-Life, Counter-Strike, Dota 2 и Portal в 2019 году составила 10 млрд.

Инвесторы поверили в идею о том, что Roblox одна из самых ценных игровых компаний в мире. Сейчас она стоит больше, чем Unity, Take-Two, и даже Electronic Arts. Причина, отчасти, в том, что инвесторы оценили выросший внутри сервиса маркетплейс, наполненный играми пользователей, а такого больше нет ни у кого на рынке.

Человеку со стороны всё ещё может показаться, что на платформе Roblox есть только примитивные мини-игры в стилистике Minecraft. Это не совсем так: инструменты платформы и её движок даже сейчас позволяют выпускать более сложные проекты с красивой графикой, чем пользуются некоторые пользователи. Вполне возможно, что и персонажей в стилистике, напоминающей игрушки Lego, скоро тоже могут сменить более детализированные аватары: выступавшая на концерте модель Lil Nas X была первым опытом внедрения в движок персонажа с имитацией настоящей кожи.

Сейчас Roblox планирует выходить на новые рынки с локализованными продуктами. Благодаря рывку, который в компании совершили за время пандемии и заработанным на бирже деньгам, это стало вполне реально.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru