Как я могу улучшить скорость рендеринга игр типа Voxel / Minecraft?


35

Я пишу свой собственный клон Minecraft (также написан на Java). Это прекрасно работает прямо сейчас. При расстоянии просмотра 40 метров я могу легко набрать 60 кадров в секунду на своем MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Но если я поставлю расстояние просмотра на 70 метров, я достигну только 15-25 FPS. В реальном Minecraft, я могу поставить расстояние просмотра далеко (= 256 м) без проблем. Итак, мой вопрос: что я должен сделать, чтобы улучшить мою игру?

Оптимизации, которые я реализовал:

  • Храните в памяти только локальные фрагменты (в зависимости от расстояния просмотра игрока)
  • Отбор Frustum (сначала на куски, затем на блоки)
  • Только рисование реально видимых граней блоков
  • Использование списков на блок, которые содержат видимые блоки. Куски, которые становятся видимыми, добавят себя в этот список. Если они становятся невидимыми, они автоматически удаляются из этого списка. Блоки становятся (не) видимыми при строительстве или уничтожении соседнего блока.
  • Использование списков на блок, которые содержат блоки обновления. Тот же механизм, что и в видимых списках блоков.
  • Не используйте почти никаких newутверждений внутри игрового цикла. (Моя игра длится около 20 секунд, пока не будет вызван сборщик мусора)
  • Я использую списки вызовов OpenGL на данный момент. ( glNewList(), glEndList(), glCallList()) Для каждой стороны своего рода блока.

В настоящее время я даже не использую какую-либо систему освещения. Я уже слышал о VBO. Но я не знаю точно, что это. Тем не менее, я сделаю некоторые исследования о них. Будут ли они улучшить производительность? Перед реализацией VBO, я хочу попытаться использовать glCallLists()и передать список списков вызовов. Вместо этого, используя тысячу разglCallList() . (Я хочу попробовать это, потому что я думаю, что настоящий MineCraft не использует VBO. Верно?)

Существуют ли другие приемы для повышения производительности?

Профилирование VisualVM показало мне это (профилирование только для 33 кадров при расстоянии просмотра 70 метров):

введите описание изображения здесь

Профилирование с 40 метров (246 кадров):

введите описание изображения здесь

Заметка: я синхронизирую много методов и блоков кода, потому что я генерирую куски в другом потоке. Я думаю, что получение блокировки для объекта - это проблема производительности, когда вы делаете это в игровом цикле (конечно, я говорю о том времени, когда есть только игровой цикл и новые порции не генерируются). Это правильно?

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

введите описание изображения здесь

Я думаю, что довольно ясно, что selectVisibleBlocksпроблема здесь.

Заранее спасибо!
Мартейн

Обновление : после некоторых дополнительных улучшений (таких как использование циклов for вместо каждого, буферизация переменных вне циклов и т. Д.), Я теперь могу довольно хорошо пробежать расстояние просмотра 60.

Я думаю, что я собираюсь реализовать VBO как можно скорее.

PS: Весь исходный код доступен на GitHub:
https://github.com/mcourteaux/CraftMania


2
Можете ли вы дать нам снимок профиля на 40 м, чтобы мы могли видеть, что может увеличиваться быстрее, чем другой?
Джеймс

Может быть, слишком конкретизировано, но, если учесть, просто вопрос о том, как ускорить 3D-игру, звучит интересно. Но название может напугать чел.
Густаво Масиэль

@Gtoknu: Что вы предлагаете в качестве заголовка?
Мартин Курто

5
В зависимости от того, кого вы спросите, некоторые люди скажут, что Minecraft на самом деле тоже не такой быстрый.
Thedaian

Я думаю, что что-то вроде «Какие методы могут ускорить 3D-игру» должно быть намного лучше. Подумайте о чем-нибудь, но постарайтесь не использовать слово «лучший» или попытаться сравнить с какой-нибудь другой игрой. Мы не можем точно сказать, что они используют в некоторых играх.
Густаво Масиэль

Ответы:


15

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

Minecraft восстанавливает только список отображения / буфер вершин (я не знаю, какой он использует), когда блок модифицируется в данном чанке, и я тоже . Если вы изменяете список отображения каждый раз, когда меняется представление, вы не получаете преимущества от списков отображения.

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

(Примечание: я не изучал код Minecraft. Вся эта информация является либо слухом, либо моими собственными выводами из наблюдения за рендерингом Minecraft во время игры.)


Более общий совет:

Помните, что ваш рендеринг выполняется на двух процессорах: CPU и GPU. Если ваша частота кадров недостаточна, то один или другой является ограничивающим ресурсом - ваша программа либо связана с процессором, либо с графическим процессором (при условии, что она не обменивается или не имеет проблем с расписанием).

Если ваша программа работает на 100% ЦП (и не имеет неограниченной другой задачи для выполнения), то ваш ЦП выполняет слишком много работы. Вы должны попытаться упростить его задачу (например, меньше отбраковывать) в обмен на то, чтобы графический процессор делал больше. Я сильно подозреваю, что это ваша проблема, учитывая ваше описание.

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


2
Отличный отзыв, ваши исследования, упомянутые в вашей вики, были очень полезны для меня! +1
Густав Масиэль

@OP: отображать только видимые лица (не блоки ). Патологический, но монотонный кусок 16x16x16 будет иметь около 800 видимых граней, в то время как содержащиеся в нем блоки будут иметь 24 000 видимых граней. Как только вы это сделаете, ответ Кевина содержит следующие наиболее важные улучшения.
AndrewS

@KevinReid Есть несколько программ, которые помогут с отладкой производительности. Например, AMD GPU PerfStudio сообщает вам, связан ли его процессор или графический процессор, и на GPU какой компонент связан (текстура против фрагмента против вершины и т. Д.). И я уверен, что у Nvidia тоже есть нечто подобное.
Акалтар

3

Что так часто вызывает Vec3f.set? Если вы создаете то, что хотите рендерить с нуля, каждый кадр, то это определенно то место, где вы хотели бы начать ускорять его. Я не большой пользователь OpenGL, и я не знаю много о том, как Minecraft рендерит, но кажется, что математические функции, которые вы используете, убивают вас прямо сейчас (просто посмотрите, сколько времени вы проводите в них и сколько раз их зовут - смерть от тысячи порезов зовет их).

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

Если в вашем профиле правильно указано количество вызовов, вы звоните очень много раз. (10 миллионов звонков на Vec3f.set ... ой!)


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

2

Мое описание (из моих собственных экспериментов) здесь применимо:

Что для более эффективного рендеринга вокселов: готовый VBO или геометрический шейдер?

Minecraft и ваш код, вероятно, используют конвейер с фиксированной функцией; мои собственные усилия были с GLSL, но суть в целом применима, я чувствую:

(Из памяти) Я сделал усечку, которая была на полблока больше, чем усеченность экрана. Затем я проверил центральные точки каждого блока (у майнкрафта есть 16 * 16 * 128 блоков ).

Грани в каждом из них имеют промежутки в массиве элементов VBO (многие грани кусков разделяют одно и то же VBO до тех пор, пока оно не будет «полным»; думайте как malloc; те, у которых одинаковая текстура в том же VBO, где это возможно) и индексы вершин для севера грани, южные грани и т. д. являются смежными, а не смешанными. Когда я рисую, я делаю glDrawRangeElementsдля северных граней, с нормой, уже спроектированной и нормализованной, в униформе. Затем я делаю южные грани и так далее, поэтому нормалей нет ни в одном VBO. Для каждого фрагмента мне нужно только испускать видимые грани - например, только в центре экрана нужно рисовать левую и правую стороны, например; это просто GL_CULL_FACEна уровне приложения.

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

Также важно управление текстурным атласом и сортировка граней по текстуре и помещение граней с той же текстурой в ту же VBO, что и у других кусков. Вы хотите избежать слишком большого количества изменений текстуры и сортировки граней по текстуре и т. Д., Чтобы минимизировать количество пролетов вglDrawRangeElements . Объединение смежных граней одной плитки в большие прямоугольники также было большой проблемой. Я говорю о слиянии в другом ответе, приведенном выше.

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


Мне нравится идея оптимизации вашего усеченного конуса. Но не смешиваете ли вы термины «блок» и «кусок» в своем объяснении?
Мартин Курто

возможно - да. Кусок блоков - это блок блоков на английском языке.
Будет

1

Откуда все ваши сравнения ( BlockDistanceComparator)? Если это из функции сортировки, может ли она быть заменена на сортировку по основанию (которая асимптотически быстрее, а не основана на сравнении)?

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

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

а затем в псевдокоде

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

Извините, если это неверная структура Java (я не касался Java с тех пор, как учился в старших классах), но, надеюсь, вы поняли идею.


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

1

Да, используйте VBO и CULL, но это касается практически каждого игры. То, что вы хотите сделать, это только визуализировать куб, если он виден игроку, И если блоки касаются особым образом (скажем, кусок, который вы не видите, потому что он подземный), вы добавляете вершины блоков и делаете это почти как «больший блок», или в вашем случае - кусок. Это называется жадной сеткой, и это резко повышает производительность. Я занимаюсь разработкой игры (на основе вокселей), и в ней используется алгоритм жадных сеток.

Вместо рендеринга все так:

оказывать

Это делает это так:

render2

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

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

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

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}

1
И стоит ли это того? Кажется, что система LOD будет более подходящей.
MichaelHouse

0

Казалось бы, ваш код тонет в объектах и ​​вызовах функций. Измеряя числа, не похоже, что происходит какая-то внутренняя связь.

Вы можете попытаться найти другую среду Java или просто поработать с настройками той, которая у вас есть, но простой и простой способ сделать ваш код не быстрым, а гораздо менее медленным - по крайней мере, внутренне в Vec3f остановить Кодирование ООО *. Сделайте каждый метод самодостаточным, не вызывайте какие-либо другие методы только для выполнения некоторой простой задачи.

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

* Слишком объектно-ориентированный


Да, вы сэкономите память, но потеряете процессор! Так что ООО не слишком хорош в играх в реальном времени.
Густаво Масиэль

Как только вы начинаете профилирование (а не только выборку), любое встраивание, которое обычно выполняет JVM, исчезает. Это похоже на квантовую теорию, нельзя что-то измерить без изменения результата: p
Майкл

@Gtoknu Это не всегда так, на каком-то уровне OOO вызовы функций начинают занимать больше памяти, чем встроенный код. Я бы сказал, что есть хорошая часть кода, о которой идет речь, это точка безубыточности для памяти.
аааааааааааа

0

Вы также можете попытаться разбить математические операции до побитовых операторов. Если у вас есть 128 / 16, попробуйте сделать оператор побитового: 128 << 4. Это очень поможет с вашими проблемами. Не пытайтесь заставить вещи работать на полной скорости. Обновляйте свою игру со скоростью 60 или около того, и даже разбивайте ее на другие вещи, но вам придется уничтожать или размещать воксели, или вам нужно составить список задач, который снизит ваш fps. Вы можете сделать частоту обновления около 20 для юридических лиц. И что-то вроде 10 для мировых обновлений и / или поколения.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.