Як я можу покращити швидкість візуалізації гри типу Voxel / Minecraft?


35

Я пишу власний клон Minecraft (також написаний на Java). Зараз він чудово працює. З відстані в огляді 40 метрів я можу легко отримати 60 FPS на своєму MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Але якщо я поставлю відстань огляду на 70 метрів, я досягну лише 15-25 FPS. У реальному Minecraft я можу без проблем поставити відстань перегляду далеко (= 256 м). Отже, моє запитання - що мені робити, щоб покращити гру?

Впроваджені нами оптимізації:

  • Зберігайте лише локальні шматки пам’яті (залежно від відстані перегляду гравця)
  • Знищення фрустуму (спочатку на шматки, потім на блоки)
  • Лише малювання дійсно видимих ​​граней блоків
  • Використання списків на шматок, які містять видимі блоки. Шматки, які стануть видимими, додадуть себе до цього списку. Якщо вони стають невидимими, їх автоматично видаляють із цього списку. Блоки стають видимими, будуючи або руйнуючи сусідній блок.
  • Використання списків на шматок, які містять блоки оновлення. Той самий механізм, що і у списках видимих ​​блоків.
  • Не використовуйте майже жодних newзаяв всередині ігрового циклу. (Моя гра триває близько 20 секунд, поки не буде викликано збирач сміття)
  • Наразі я використовую списки викликів OpenGL. ( glNewList(), glEndList(), glCallList()) Для кожної сторони свого роду блоку.

В даний час я навіть не використовую жодної системи освітлення. Я вже чув про VBO. Але я точно не знаю, що це таке. Однак я зроблю деякі дослідження щодо них. Чи поліпшать вони результати? Перш ніж реалізувати VBO, я хочу спробувати використати glCallLists()та передати список списків дзвінків. Замість цього використовуйте тисячу разів glCallList(). (Я хочу спробувати це, тому що я думаю, що справжній MineCraft не використовує VBO. Правильно?)

Чи є інші хитрощі для підвищення продуктивності?

Профілювання VisualVM показало мені це (профілювання лише 33 кадрів, відстань перегляду 70 метрів):

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

Профілювання з 40 метрів (246 кадрів):

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

Примітка. Я синхронізую безліч методів і блоків коду, тому що я генерую фрагменти в іншій потоці. Я думаю, що придбання блокування для об'єкта є проблемою продуктивності, коли ви робите це багато в ігровому циклі (звичайно, я говорю про час, коли є лише цикл гри і нові шматки не генеруються). Чи це правильно?

Редагування: Після видалення деяких synchronisedблоків та деяких інших невеликих удосконалень. Продуктивність вже набагато краща. Ось мої нові результати профілювання на 70 метрів:

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

Я думаю, що цілком зрозуміло, що selectVisibleBlocksтут проблема.

Спасибі заздалегідь!
Мартійн

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

Я думаю, що я збираюся впровадити VBO якнайшвидше.

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


2
Чи можете ви дати нам знімок профілю на 40 м, щоб ми могли побачити, що може збільшуватись швидше, ніж інший?
Джеймс

Можливо, занадто уточнено, але якщо ви враховуєте, це просто запитати методи, як пришвидшити тривимірну гру, звучати цікаво. Але заголовок може налякати ppl.
Густаво Масьєль

@Gtoknu: Що ви пропонуєте як назву?
Martijn Courteaux

5
Залежно від того, кого ви запитуєте, деякі люди скажуть, що Minecraft теж не такий швидкий.
thedaian

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

Відповіді:


15

Ви згадаєте, що робите фрустрацію на окремих блоках - спробуйте викинути це. Більшість фрагментів візуалізації повинні бути повністю видимими або повністю невидимими.

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

Крім того, ви, здається, використовуєте шматки світової висоти. Зауважте, що Minecraft використовує кубічні розміри розміром 16 × 16 × 16 для своїх списків відображення, на відміну від завантаження та збереження. Якщо ви це зробите, є ще менше причин для того, щоб зірвати окремі шматки.

(Примітка. Я не вивчав код Minecraft. Вся ця інформація є чи то з речей, так і з моїх власних висновків із спостереження за поданням Minecraft, коли я граю.)


Більш загальна порада:

Пам'ятайте, що ваше відображення виконується на двох процесорах: процесорі та графічному процесорі. Коли частота кадрів недостатня, то той чи інший є обмежуючим ресурсом - ваша програма або пов'язана з процесором, або пов'язана з графічним процесором (якщо припустити, що вона не замінюється або не має проблем із плануванням).

Якщо ваша програма працює на 100% процесора (і не має необмеженого іншого завдання для виконання), то ваш процесор виконує занадто багато роботи. Вам слід спробувати спростити його завдання (наприклад, зробити менше вибивання) в обмін на те, щоб GPU зробив більше. Я сильно підозрюю, що це ваша проблема, враховуючи ваш опис.

З іншого боку, якщо GPU - це межа (на жаль, зазвичай немає зручних моніторів завантаження 0% -100%), тоді вам слід задуматися над тим, як надіслати їй менше даних або вимагати, щоб він заповнив менше пікселів.


2
Чудова довідка, ваші дослідження щодо цього, згадані у вашій вікі, дуже допомогли мені! +1
Густаво Масьєль

@OP: відображати лише видимі обличчя (не блоки ). Патологічний, але монотонний відрізок розміром 16х16х16 матиме майже 800 видимих ​​граней, тоді як у блоках міститься 24000 видимих ​​граней. Після того, як ви зробите це, відповідь Кевіна містить наступні найважливіші вдосконалення.
AndrewS

@KevinReid Є деякі програми, які допоможуть налагодити продуктивність. Наприклад, AMD GPU PerfStudio повідомляє вам, чи пов'язаний його процесор чи GPU, а для GPU - який компонент пов'язаний (текстура проти фрагмента проти вершини тощо). І я впевнений, що у Nvidia теж є щось подібне.
акалтар

3

Що так закликає Vec3f.set? Якщо ви будуєте те, що хочете зробити з нуля кожен кадр, тоді це точно, де ви хочете почати його прискорювати. Я не дуже користувач OpenGL, і не знаю багато про те, як надає Minecraft, але здається, що математичні функції, які ви використовуєте, вбивають вас зараз (просто подивіться, скільки часу ви витрачаєте на них і скільки разів їх називають - смерть на тисячу скорочень, що їх кличуть).

В ідеалі ваш світ буде сегментований таким чином, щоб ви могли групувати речі для візуалізації разом, будуючи об’єкти буферних вершин та повторно використовуючи їх у кількох кадрах. Вам потрібно буде змінити VBO лише в тому випадку, якщо світ, який він представляє, якимось чином зміниться (як користувач редагує його). Тоді ви можете створити / знищити VBO для того, що ви представляєте, оскільки він потрапляє в діапазон видимості, щоб зменшити споживання пам’яті, ви отримаєте хіт лише тому, що створено VBO, а не кожен кадр.

Якщо підрахунок "виклику" у вашому профілі правильний, ви дуже багато разів називаєте дуже багато речей. (10 мільйонів дзвінків на Vec3f.set ... ой!)


Я використовую цей метод для тонни речей. Він просто встановлює три значення для вектора. Це набагато краще, ніж щоразу виділяти новий об’єкт.
Martijn Courteaux

2

Мій опис (з мого власного експерименту) тут застосовний:

Що для рендерингу вокселів, що ефективніше: попередньо виготовлений VBO або шейдер для геометрії?

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

(З пам’яті) Я зробив фрустум, який був на півблоку більший, ніж екранний фруст. Потім я перевірив центральні точки кожного блоку (у Minecraft є 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;
};

а потім у pseudoCode

// 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 є щось подібне, але це стосується баз даних, зовсім не те саме. Вони можуть створити заключний клас з громадськими членами, я думаю, це працює.
Тераот

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 виклики функцій починають займати більше пам'яті, ніж вбудований код. Я б сказав, що є хороша частина коду, про який йдеться, яка є навколо точки беззбитковості для пам'яті.
aaaaaaaaaaaa

0

Ви також можете спробувати розбити операції Math до бітових операторів. Якщо у вас є 128 / 16, спробуйте зробити оператор побітового: 128 << 4. Це дуже допоможе у вирішенні ваших проблем. Не намагайтеся змусити роботу працювати на повній швидкості. Зробіть оновлення гри зі швидкістю 60 чи щось, і навіть розбийте це на інші речі, але вам доведеться робити знищення та розміщення вокселів, або вам доведеться скласти список тодо, який знизить ваш кадр в секунду. Ви можете зробити швидкість оновлення близько 20 для юридичних осіб. І щось на зразок 10 для світових оновлень та генерацій.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.