Збереження вокселів для воксельних двигунів в C ++


9

Я намагаюсь написати трохи воксельного двигуна, тому що це весело, але намагаюся знайти найкращий спосіб зберігати фактичні вокселі. Я усвідомлюю, що мені будуть потрібні певні шматки, тому мені не потрібно мати весь світ в пам’яті, і я знаю, що мені потрібно зробити їх з розумною продуктивністю.

Я читав про октриси, і з того, що я розумію, це починається з 1 куба, і в цьому кубі може бути ще 8 кубів, а в усіх цих 8 кубів може бути ще 8 кубів і т. Д. Але я не думаю, що це відповідає моєму воксельному двигуну, тому що всі мої кубики / елементи вокселя будуть точно такого ж розміру.

Отже, ще один варіант - просто створити масив розміром 16 * 16 * 16 і мати один фрагмент, і ви заповните його елементами. А частини, де немає жодних елементів, матимуть значення 0 (0 = повітря). Але я боюся, що це втратить багато пам’яті і не буде дуже швидким.

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

Тож я не можу знайти хорошого рішення, і сподіваюся, що хтось може мені допомогти у цьому. То що б ви використовували і навіщо?

Але ще одна проблема - надання. Просто читати кожен фрагмент і надсилати його до GPU за допомогою OpenGL - це легко, але дуже повільно. Генерувати одну сітку за шматок було б краще, але це означає, що кожного разу, коли я перериваю один блок, я повинен будувати весь шматок, який може зайняти трохи часу, викликаючи незначну, але помітну гикавку, якої я, очевидно, теж не хочу. Так що було б важче. То як би я видавав кубики? Просто створіть усі кубики в одному вершинному буфері за шматок і зробіть це, а може, спробуйте вкласти це в інший потік, чи є інший спосіб?

Дякую!


1
Ви повинні використовувати екземпляр для подачі кубів. Підручник ви можете знайти тут learnnopengl.com/Advanced-OpenGL/Instancing . Для зберігання кубів: у вас є сильні обмеження в пам’яті на апаратному забезпеченні? 16 ^ 3 кубики не здаються занадто великими пам’яттю.
Турми

@Turms Дякуємо за ваш коментар! У мене немає сильних обмежень пам'яті, це просто звичайний ПК. Але я подумав, що якщо кожен найвищий шматок - це 50% повітря, а світ дуже великий, то пам'яті повинно бути небагато. Але це, мабуть, не так, як ти кажеш. Тож я повинен просто піти на 16 * 16 * 16 шматки зі статичною кількістю блоків? А також, ви кажете, я повинен використовувати екземпляр, це дійсно потрібно? Моя ідея полягала в тому, щоб створити сітку для кожного куска, тому що я можу залишити всі невидимі трикутники.

6
Я не рекомендую використовувати екземпляри для кубів, як описано Turms. Це лише зменшить ваші розіграші дзвінків, але нічого не зробить для перевиконання та прихованих облич - адже це пов’язує ваші руки з вирішенням цієї проблеми, оскільки для того, щоб миттєво працювати всі кубики повинні бути однаковими - ви не можете видалити приховані грані деяких кубів або об'єднати копланарні грані у більші одиничні багатокутники.
DMGregory

Вибір найкращого двигуна для вокселів може стати проблемою. Велике запитання, яке потрібно задати собі, - "які операції мені потрібно робити на своїх вокселях?" Що керує операціями. Наприклад, ви стурбовані тим, як важко розібратися, який воксель є де в октябрі. Алгоритми жовтневого дерева чудово підходять для проблем, які можуть генерувати цю інформацію в міру необхідності під час прогулянки по дереву (часто рекурсивно). Якщо у вас є конкретні проблеми, де це занадто дорого, тоді ви можете подивитися на інші варіанти.
Корт Аммон

Ще одне велике питання - як часто оновлюються вокселі. Деякі алгоритми чудові, якщо вони можуть попередньо обробити дані для їх ефективного зберігання, але менш ефективні, якщо дані постійно оновлюються (як це може бути при моделюванні рідини на основі частинок)
Cort Ammon,

Відповіді:


23

Збереження блоків як позицій та значень насправді дуже неефективно. Навіть не маючи накладних витрат, спричинених структурою або об'єктом, який ви використовуєте, вам потрібно зберігати 4 різних значення на блок. Використовувати його було б лише за методом "зберігання блоків у фіксованих масивах" (той, який ви описали раніше), коли лише чверть блоків є твердими, і таким чином ви навіть не використовуєте жодних інших методів оптимізації. рахунок.

Octrees насправді чудово підходить для ігор на основі вокселів, оскільки вони спеціалізуються на зберіганні даних із більшими можливостями (наприклад, патчі одного блоку). Щоб проілюструвати це, я використав квадрат (в основному октриси в 2d):

Це мій стартовий набір, що містить плитки 32х32, що б дорівнювало 1024 значень: введіть тут опис зображення

Збереження цього як 1024 окремих значень не здається неефективним, але як тільки ви досягнете розмірів карти, подібних до ігор, таких як Terraria , завантаження екранів займе кілька секунд. А якщо збільшити його до третього виміру, він почне використовувати весь простір у системі.

Квадри (або октриси в 3d) можуть допомогти ситуації. Щоб створити його, ви можете або піти з плиток і згрупувати їх разом, або перейти від однієї величезної комірки і розділити її, поки не досягнете плитки. Я скористаюся першим підходом, тому що це простіше уявити.

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

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

Червоні лінії позначають те, що ми зберігаємо. Кожен квадрат - це лише 1 значення. Це зменшило розмір з 1024 значень до 439, це на 57%.

Але ви знаєте мантру . Пройдемо ще крок далі і згрупуємо їх у комірки:

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

Це зменшило кількість збережених значень до 367. Це лише 36% від початкового розміру.

Вам, очевидно, потрібно зробити цей поділ, поки кожна 4 сусідня клітина (8 сусідніх блоків у 3d) всередині шматка не зберігається всередині однієї комірки, по суті перетворюючи шматок в одну велику клітинку.

Це має і деякі інші переваги, головним чином при зіткненні, але ви можете створити для цього окремий октриг, який піклується лише про те, чи твердий один блок, чи ні. Таким чином, замість того, щоб перевіряти наявність зіткнень для кожного блоку всередині шматка, ви можете просто зробити це проти комірок.


Дякуємо за Ваш відповідь! Здається, що октрі - це шлях. (Оскільки мій воксельний двигун буде тривимірним) у мене є цілі запитання, які я хотів би задати: На вашій останній фотографії показано, що чорні частини мають більші квадрати, оскільки я маю намір мати такий minecraft, як двигун, де ви можете змінити місцевість вокселів, я вважаю за краще зберігати все, що має блок однакового розміру, тому що в іншому випадку це зробить речі дуже складними, це можливо? (я б все одно спростив порожній / повітряний прорізи курсу). По-друге , чи є якийсь підручник про те, як можна було б запрограмувати октрейд? Дякую!

7
@ appmaker1358 це зовсім не проблема. Якщо гравець намагається змінити великий блок, то ви розбиєте його на менші блоки на той момент . Немає необхідності зберігати значення "скелі" 16x16x16, коли можна сказати замість цього "цілий шматок твердої породи", поки це вже не відповідає дійсності.
DMGregory

1
@ appmaker1358 Як зазначає DMGregory, оновити дані, що зберігаються в октрисі, відносно просто. Все, що вам потрібно зробити, - розділити комірку, до якої сталася зміна, поки кожна підклітина не містить лише одного типу блоку. Ось інтерактивний приклад з квадратом . Зробити це також просто. Ви створюєте одну велику клітинку, яка повністю містить шматок, потім рекурсивно проходите через кожну клітинку листя (клітини, у яких немає дітей), перевіряєте, чи є частина місцевості, яку вона представляє, містить кілька типів блоків, якщо так, підрозділяйте клітини
Балінт

@ appmaker1358 Більшою проблемою є насправді зворотна - переконайтесь, що октрі не заповниться листям лише одним блоком, що може легко трапитися в грі в стилі Minecraft. Однак існує багато варіантів вирішення проблеми - справа лише в тому, щоб вибрати те, що ви вважаєте за потрібне. І це стає справжньою проблемою лише тоді, коли відбувається багато будівництва.
Луань

Octrees - це не обов'язково найкращий вибір. ось цікаве прочитання: 0fps.net/2012/01/14/an-analysis-of-minecraft-like-engines
Полігном

7

Octrees існують для вирішення саме описаної вами проблеми, що дозволяє щільно зберігати розріджені дані без великих пошукових разів.

Той факт, що ваші вокселі однакового розміру, означає лише, що ваша октарія має фіксовану глибину. напр. для шматка 16х16х16 вам потрібно щонайбільше 5 рівнів дерева:

  • корінь шматка (16x16x16)
    • октант першого рівня (8x8x8)
      • октант другого рівня (4x4x4)
        • октант третього рівня (2x2x2)
          • один воксель (1x1x1)

Це означає, що у вас є щонайбільше 5 кроків, щоб дізнатися, чи є в певному положенні воксель:

  • корінь шматка: чи є цілий шматок однакового значення (наприклад, все повітря)? Якщо так, ми закінчили. Якщо ні...
    • перший рівень: чи все одно значення має октант, який містить цю позицію? Якщо ні...
      • другий рівень ...
        • третій рівень ...
          • тепер ми звертаємось до одного вокселю і можемо повернути його значення.

Набагато коротше, ніж сканувати навіть 1% шляху через масив до 4096 вокселів!

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


Для звернення до дітей шматка, як правило, ми будемо діяти в порядку Мортона , приблизно так:

  1. X- Y- Z-
  2. X- Y- Z +
  3. X- Y + Z-
  4. X- Y + Z +
  5. X + Y- Z-
  6. X + Y- Z +
  7. X + Y + Z-
  8. X + Y + Z +

Отже, наша навігація по вузлу Octree може виглядати приблизно так:

GetOctreeValue(OctreeNode node, int depth, int3 nodeOrigin, int3 queryPoint) {
    if(node.IsAllOneValue)
        return node.Value;

    int childIndex =  0;
    childIndex += (queryPoint.x > nodeOrigin.x) ? 4 : 0;
    childIndex += (queryPoint.y > nodeOrigin.y) ? 2 : 0;
    childIndex += (queryPoint.z > nodeOrigin.z) ? 1 : 0;

    OctreeNode child = node.GetChild(childIndex);

    return GetOctreeValue(
                child, 
                depth + 1,
                nodeOrigin + childOffset[depth, childIndex],
                queryPoint
    );
}

Дякуємо за Ваш відповідь! Здається, октрис - це шлях. Але у мене є 2 питання, хоча ви кажете, що octree швидше, ніж сканування через масив, що правильно. Але мені не потрібно робити цього, оскільки масив може стати статичним, тобто я можу підрахувати, де потрібен куб. То чому мені потрібно сканувати? Друге питання, в останньому ярусі octree (1x1x1), як я знаю, куб куди, оскільки, якщо я правильно його розумію, а вузол octree має ще 8 вузлів, як ви знаєте, який вузол належить до якої 3d позиції ? (Або я повинен це пам’ятати сам?)

Так, ви вже висвітлювали випадок вичерпного масиву 16x16x16 вокселів у вашому запитанні і, здається, відкидаєте слід пам'яті 4K на шматок (припускаючи, що кожен ідентифікатор вокселя - байт) як надмірний. Пошук, про який ви згадали, відбувається під час зберігання списку вокселів із позицією, змушуючи вас сканувати список, щоб знайти воксель у вашій цільовій позиції. 4096 тут - верхня межа довжини списку - зазвичай вона буде меншою, ніж ця, але, як правило, все ж глибша за відповідний пошук октрису.
DMGregory
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.