JMonkeyEngine и пиксельный хаос

Добавлен , опубликован
Меня интересовал мучил интересовал, что можно успеть накодить что-нибудь за обеденный перерыв? Надеюсь теперь и вас будет терзать любопытство, ведь ответа на этот вопрос здесь нет, а есть нечто другое.
Началась эта короткая история с того, что мне захотелось проверить в действии одну занятную идею на счет хранения, передачи и отображения двухмерной поверхности с произвольным пиксельным заполнением.
Не долго думая, идея была дополнена парой свободно распространяемых текстур, 3D движком JmonkeyEngine3.0 и воплощена в простейшую программу, в которой даже камера не двигается.
Если коротко, то формируется двухмерный массив байтов, запекается в текстуру с одним каналом и передается в шейдер вместе с текстурой-атласом, а на выходе мы получаем то, что получаем - текстуру, каждый пиксель которой выбирается с одной из текстур в атласе.
Для модификации состояния "на лету" используется ByteBuffer, на который и завязана промежуточная текстура. Реализованы два способа редактирования - более быстрый на малых операциях, позволяющий изменить состояние конкретного пикселя и более быстрый при массовом редактировании пикселей, позволяющий одной операцией обновить от одного пикселя, до целой строки пикселей.
В тестовом сценарии используется итоговая текстура 2048*2048 пикселя с разбиением на 256 элементов, при размерах окна 1024*768.
Об оптимизации и практическом применении этого добра еще предстоит подумать, но первое что приходит на ум - 2д-платформер с полной разрушаемостью рельефа до отдельного пикселя.
P.S. На самом деле, приведенные выше результаты были получены хоть и за соизмеримое время, но не в результате кодинга на обеденном перерыве и потому не считаются.
P.P.S. Сжатие скриншота в jpeg несколько приукрасило картинку - на практике никакой тени на границе земли и неба не наблюдается, но получилось неплохо, так что я обязательно возьму этот эффект на вооружение.
0
24
10 лет назад
Отредактирован prog
0
Добавил передвижение камеры (немного странное, на QZAD т.к. лень было контроллер камеры свой писать) и столкнулся с неожиданным поведением смешивающего текстуры шейдера на определенном железе. В связи с чем, выкладываю текущую версию в надежде что кто-то её все-же запустит и отчитается о правильности отображения текстур. Архив с текущей версией можно найти ниже в комментариях.
Решено, быть этому 2д-платформером.
Загруженные файлы
0
14
10 лет назад
0
У меня все нормально отображается.
1
33
10 лет назад
1
2д-платформер с полной разрушаемостью рельефа до отдельного пикселя.
Червяки! \o/
0
24
10 лет назад
Отредактирован prog
0
IceFog, спасибо, с этим отчетом счет становится 5 к 1 в пользу конфигураций железа, на которых все работает.
Кет, я не гожусь в художники чтобы нарисовать столь харизматичных персонажей, кроме того у меня на уме сеттинг, более близкий к рпг и более ориентированный на строительство. Но на самом деле я пока абсолютно уверен только в четырех вещах:
1 - это будет 2д платформер с видом сбоку.
2 - там будут деревья и по ним можно будет лазить.
3 - слабым взрывом и другими способами можно будет смести листву с дерева, оставив голый ствол с ветками.
4 - в обязательном порядке будет мультиплеер, но не факт что к срокам конкурса сандбоксов.
Все остальное может и будет меняться в совершенно неожиданных направлениях.
0
33
10 лет назад
0
prog, тоже неплохой набор.
2
24
10 лет назад
Отредактирован prog
2
Добавлен тайл травы и внесены соответствующие корректировки в заглушку генератора мира. Плюс проведена некоторая оптимизация, в результате чего ощутимо вырос фпс.
upd:
Перед тем как уйти на работу немного доработал систему. Теперь вместо одноканальной текстуры используется трехканальная, что в три раза удорожает передачу данных об изменении состояния фрагмента карты на GPU, но позволяет передавать не только ID текстуры, а еще и два байта дополнительных данных. Эти два байта будут использованы для передачи смещения текстурных координат пикселя, что позволит размещать в любой точке карты любой фрагмент любой текстуры, а не строго по сетке, как это было до сих пор.
Использоваться возможность задать сдвиг будет в основном ради свободного размещения различных небольших объектов, имеющих четкие контуры на текстуре. Например, столы, стулья, печки, деревья и канделябры. Правда на счет деревьев не уверен - их бы по хорошему генерировать программно, да и уместить дерево на текстуре 256*256 довольно проблематично. С другой стороны, возможность использовать произвольную развертку по текстуре вместо сквозной позволяет, например, уместить в одной текстуре листья из центра дерева, листья с краев дерева, кору ствола из центра и кору ствола с краев, а потом программно расставить пиксели дерева с разными смещениями текстуры.
В числе недостатков такого решения - увеличение потребляемой памяти, увеличение потенциальных затрат на передачу по сети, увеличение времени передачи информации об изменении фрагмента карты на GPU, усложнение внесения этих самых изменений в фрагмент карты. Возможно, стоит задуматься об использовании одноканальной текстуры для фрагментов карты, в которых нет пикселей со смещением текстурных координат с переключением на трехканальный режим при необходимости.
Новые скриншоты будут уже вечером - работать надо, а не выбирать текстуру и править генератор мира.
upd:
И так, обещанные скриншоты
На скриншоте слева направо представлены следующие ситуации:
  • наложение нового тайла из атласа на тайл камней с травой, без смешивания с тайлом камней чтобы визуально было понятно где границы фрагмента карты
  • попытка сдвинуть новый тайл в пределах своего участка карты на 10 пикселей, при использовании старой системы
  • тот-же самый сдвиг нового тайла, но уже в новой системе
  • наложение двух экземпляров нового тайла в пределах одного фрагмента карты
Хочу заметить, что ни тайлы поверхности, ни изображающий некий объект новый тайл не являются самостоятельными объектами и представлены общим массивом байт.
Если вместо текстуры из атласа подставлять фиксированный цвет, то получим такой психодел:
0
20
10 лет назад
0
А о процедурной генерации, без текстур-пресетов, не думал? Просто у меня был ещё в школе проект - генерация текстур естественных поверхностей.
зы. читал не всё, может это оно и есть)
2
24
10 лет назад
2
Mihahail, посмотри на последние скриншоты, в которых текстуры заменены для наглядности простыми цветами. Ядро игры оперирует массивом байт, в которых лежат id типа пикселя, а текстуры и дополнительные данные вроде смещения нужны уже в процессе отображения.
В принципе, я вполне могу использовать и процедурную генерацию текстур - от этого в самом ядре мало что изменится, но не вижу большой необходимости. Мороки много с процедурной покраской процедурно сгенерированного мира, а визуально результат меня и на запеченных текстурах устраивает. Я лучше добавлю автоматическую сборку отдельных текстур в атлас и поддержку вариаций одной текстуры.

Следующим этапом у меня в планах запилить полноценный чанк-менеджмент, обеспечивающий сквозную нумерацию пикселей карты, независимо от внутренней структуры - карта разбита на фрагменты 128*128 и каждый фрагмент это отдельный массив для удобства пересылки обновлений по сети и упрощения перерисовки, но работать с этими массивами напрямую это жуткая головная боль. После чего можно будет браться за полноценный генератор мира, а не это жалкое его подобие.
Одна из первых фич генератора, которую я планирую запилить - добавление в слой земли мелких объектов вроде камней и корней. Под корни придется выделить отдельный id и, соответственно, отдельную текстуру, а вот камни, возможно, получится реализовать и на общей для камней текстуре, хотя выделение отдельной текстуры под маленькие камешки разных вариаций даст более интересный визуальный результат.
Также в планах с высоким приоритетом уже упомянутые автоматическая сборка атласа "на лету" и поддержка вариаций текстур. На автоматический сборке атласа, думаю, заострять внимание не обязательно, а вот подробнее рассмотреть поддержку вариаций текстур очень даже стоит.
На данный момент покраска текстурами на основе id пикселя ведется с помощью простейшего шейдера, который на входе получает текстуру атласа (одна для всех) и псевдотекстуру, в которой закодированы id каждого пикселя плюс данные о смещении. Таким образом, у каждого фрагмента карты размером 128*128 в наличии своя уникальная псевдотекстура и ссылка на общую текстуру атласа. А теперь представим, что у нас есть два атласа, которые отличаются только вариациями текстур - назначить каждому фрагменту карты свою ссылку на нужный атлас не составит труда. Следующим вполне логичным шагом было бы разделить атлас для поверхностей и атлас для объектов ведь объекты с фиксированными контурами куда менее вероятно будут иметь большое кол-во вариаций. Более того, разделение атласов даст возможность использовать для объектов текстуры не 128*128, а 256*256 при неизменной структуре карты - под информацию о смещении выделено два байта и на практике остается два неиспользуемых бита т.к. смещение нет смысла делать больше, чем размеры текстуры. С другой стороны, такое разделение несомненно усложнит переход от id пикселя к текстуре и добавление новых id.
Таким образом, результат будет выглядеть примерно так: многостраничный атлас, каждая страница которого содержит все текстуры в отличной от других страниц вариации, а каждый фрагмент карты ссылается на выбранную произвольным образом страницу атласа. Плюс для объектов фиксированной формы будет отдельный атлас без страниц вариаций, компенсирующий этот недостаток увеличенным размером текстур.
1
24
10 лет назад
Отредактирован prog
1
Сперва по статусу - пришла пора реализовать нормальный чанк-менеджмент и полноценный генератор мира, оперирующий не отдельными несвязанными массивами байт, а связанными между собой фрагментами карты. Этот процесс не сопровождается какими-то принципиальными изменениями во внешнем виде, так что новых скриншотов какое-то время не будет.
А теперь немного абстрактных рассуждений: немного подумав, я пришел к мысли, что смещение по текстуре можно использовать не только для отображения, но и для упрощения такого незаменимого процесса, как определение принадлежности пикселя тому или иному объекту. Будут некоторые сложности при наложении в одних координатах нескольких разных объектов и для составных объектов, использующих разное смещение по текстуре в разных частях объекта, а для процедурно генерируемых объектов, имеющих произвольную форму, это и вовсе почти невыполнимая задача.
Таким образом, самое быстрое получение объекта по координатам пикселя будет для объектов фиксированной формы, состоящих из одной текстуры, не имеющих вариаций и использующих только линейное смещение по текстуре. Добавление вариаций к объекту, собирание объекта из разных кусков одной текстуры и прочие манипуляции, остающиеся в рамках фиксированной формы и линейности смещения, по крайней мере для каждого отдельно взятого фрагмента объекта, несколько усложняют алгоритм, но теоретически реализуются без потерь производительности.
Чем это чревато для геймплея? В первую очередь тем, что для интерактивных объектов вроде дверей и сундуков будут максимально использоваться одноэлементные объекты с вариациями в пределах одной текстуры - без вариаций было бы проще и быстрее, но тратить целую текстуру и id материала ради каждой вариации, например, сундука это перебор - система поддерживает всего 256 различных материалов и я не хочу без крайней необходимости повышать этот лимит.
Второе последствие для геймплея - генерируемые объекты сложной формы будут частью рельефа, а не отдельными объектами - никаких дополнительных параметров, задаваемых для всего объекта или особой реакции на активацию - только общие свойства материала.
Ну и напоследок небольшой финт ушами: для повышения вариативности можно разделить текстуру объекта и пресет, по которому заполняются пиксели карты для создания объекта. Тогда при использовании одной текстуры можно получить различные вариации объекта, отличающиеся наличием или отсутствием некоторых пикселей в пресете.

upd:
Кажется пора создавать отдельный проект - ведение этого подобия WIP-а в формате блога начинает вызывать у меня дискомфорт своим внешним видом.
0
20
10 лет назад
0
А я вот с удовольствием читаю тут.
0
24
10 лет назад
0
Mihahail, ну так текста меньше не станет, просто хочется немного его структурировать.
0
24
10 лет назад
Отредактирован prog
0

17.05.2014

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

О будущей оптимизации

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

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

Стресс-тест

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

upd:

Размышления о продвинутом чанк-менеджменте

В данный момент в разработке находится базовый механизм выгрузки лишних фрагментов карты, которые находятся далеко от объектов, вызывающих загрузку - игроков, камер, якорей и так далее. Данный алгоритм подразумевает простейший способ определения фрагментов карты, которые подлежат выгрузке: нет вызывающих загрузку объектов в непосредственной близости - выгружаем.
Такая простая реализация несомненно позволит сократить затраты памяти и устранить падение игры из-за того, что память закончилась. В то-же время, этот до боли простой алгоритм чреват неприятными последствиями, среди которых замирание всего, что слишком далеко от игрока и необходимость дергать диск или соединение с сервером так часто, как часто игрок пересекает границу фрагмента карты.
Первое, что можно сделать, это ввести "серую зону" между границей "видимости" загружающих карту объектов и реальной границей выгрузки фрагментов - увеличиваем время жизни фрагментов карты, а значит сокращаем кол-во обращений к диску. В некоторой степени это помогает и с проблемой замирающих объектов в выгруженных фрагментах карты - посещенные игроком фрагменты карты выгружаются не сразу, а значит и у объектов в этих фрагментах остается больше времени что-то сделать.
Недостаток этого метода в том, что механизм выгрузки фрагментов карты становится несколько непредсказуемым, а значит на него нельзя полагаться в вещах, требующих гарантированного наличия загруженного фрагмента карты в определенном месте.
И вот, совершенно внезапно, мне пришла мысль - а почему бы не поместить фрагменты карты, подлежащие выгрузке, в очередь с приоритетами и фильтровать их по какому-то признаку, позволяющему определить, на сколько же важен тот или иной фрагмент карты.
В таком случае можно будет выгружать только фрагменты карты с низкой важностью для системы, а важные фрагменты карты выгружать только при крайней необходимости.
Над тем, по каким критериям назначать приоритет выгрузки для фрагментов карты еще нужно подумать, но навскидку можно назвать пару вариантов:
  • расстояние до игрока и других источников принудительной загрузки (на практике это скорее критерий попадания в очередь выгрузки, чем критерий для сортировки)
  • кол-во изменений в фрагменте карты за определенный промежуток времени (надежный критерий, но требующий дополнительного учета каждого изменения)
  • кол-во загрузок-выгрузок фрагмента карты за определенный промежуток времени (адаптивный критерий, позволяющий стабилизировать некоторые системы, хорош как вспомогательный для какого-то другого критерия)
  • кол-во обращений к фрагменту карты на определенном промежутке времени, считая за одно обращение вызов функции world.getChunkAt(x,y) вне зависимости от того, что дальше будет происходить с полученным фрагментом карты. (достаточно сбалансированный способ на мой взгляд, дающий приличную точность с минимальной дополнительной нагрузкой)
  • фиксированный показатель значимости, который назначается на основании содержимого фрагмента и изменяется при изменении содержимого (отличается от полного учета изменений тем, что обрабатываются только особые случаи вроде добавления и удаления конкретных объектов)

upd:

18.05.2014

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

Размышления на тему рендеринга

На данный момент используется относительно простой процесс рендеринга сцены:
  • данные об id материала и смещении хранятся в массиве байт и при необходимости запекаются в текстуру для каждого фрагмента карты
  • текстуры с данными передаются шейдеру путем натягивания на прямоугольники, размещенные на сцене
  • для каждого фрагмента карты на основе полученной текстуры выполняется конвертация данных в реальные координаты пикселей на текстуре-атласе
  • для отображения отправляются полученные из текстуры-атласа цвета пикселей
  • персонажи и прочие подвижные объекты должны вроде как накладываться сверху, но за их отсутствием пока не накладываются
Такой подход хорош его простотой - например, добавление передвижных объектов сводится к тому, чтобы добавить их на сцену и выдать им другой шейдер, не пытающийся рассматривать текстуру, как набор id материалов. С другой стороны, попытка реализовать специальные эффекты, которые бы использовали исходные данные о материалах пикселей поверхности, превращается в форменную пытку т.к. данные о материалах теряются на этапе обработки каждого отдельного фрагмента карты, а для эффектов желательно иметь данные о всех соседних пикселях, а не только о тех, которые принадлежат конкретному фрагменту карты.
Решением этой проблемы может быть добавление промежуточного этапа в процесс рендеринга, а именно, можно отдавать на выходе из шейдера для отдельных фрагментов карты не итоговый цвет, а только id материала и пересчитанные текстурные координаты (из относительных в абсолютные). Затем эти данные обрабатываются любыми дополнительными фильтрами, в том числе и преобразующим id материала в цвет пикселя с текстуры, но действующими в рамках экрана, а не в рамках каждого отдельного объекта.
Таким образом можно избавиться от необходимости хранить ссылку на текстуру-атлас для каждого отдельного фрагмента карты, а значит и вносить изменения в атлас становится куда проще за счет того, что ссылка на атлас есть только у экранного фильтра. Кто-то может сказать, что в таком случае теряется возможность использовать страничные вариации атласа, о которых говорилось ранее, но это не совсем так - вместо ссылок на конкретные страницы атласа в свойствах фрагмента карты можно хранить номер вариации.
Возникает еще один вопрос - как передать из шейдера для объекта в шейдер для экрана информацию о том, какую вариацию текстуры использовать, но решение тривиально - для передачи данных об id материала и смещении достаточно трех каналов (R, G и B), в то время как канал A остается не задействованным (при передаче данных от ядра игры в шейдер используется трехканальная текстура, в то время как дальше используется уже четырехканальная, независимо от того, записывается ли что-то в альфа-канал).
Получается, что можно записать номер вариации в альфа-канал и радоваться жизни - экранный фильтр запросто использует эти данные чтобы выбрать правильную страницу атласа, а на прозрачность результирующего изображения такое шаманство не повлияет. Более того, нет реальной необходимости в 256 вариациях атласа, так что имеется потенциал для передачи какой-то дополнительной информации.
Более того, в перспективе можно перейти от трехканальной текстуры для передачи данных в шейдер к четырехканальной и получить возможность выбирать вариацию текстуры атласа не в рамках фрагмента карты, а в рамках каждого отдельного пикселя, но это обойдется в дополнительные затраты памяти и пока не нужно.
Загруженные файлы
0
24
10 лет назад
Отредактирован prog
0
Собственно, проект создан и прошел предмодерацию, дальнейшая активность по проекту будет уже там, да и старые записи постепенно перекочуют в ресурсы проекта.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.