Виртуальные текстуры

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

==1.  Предыстория==
Корни виртуальной текстуры упираются еще в далекий 1998 год, когда Christopher C. Tanner, Christopher J. Migdal и Michael T. Jones (Silicon Graphics Computer Systems) представили в своем докладе, так называемые, clipmaps[1]. Технология clipmaps нашла свое применение на текстурировании ландшафтов и имела различные ограничения, в частности, ее нельзя было использовать для текстурирования произвольной геометрии. Позже технология была развита Джоном Кармаком (John Carmack) из id Software и была применена в игре Rage (до этого на своей заре, в качестве эксперимента использовалась на ландшафтах в QuakeWars) [2]. Так называемая, мегатекстура (или виртуальная текстура), в отличие от clipmaps накладывалась на произвольную геометрию. Далее мы рассмотрим, в чем же отличие от обычных тайловых (тайлинговых) текстур.



==2.  Отличие от обычных тайловых текстур==
Виртуальные текстуры позволяют сделать игровой мир более уникальным, снимают ограничения на ART, позволяют раскрасить его неповторяющимися текстурами. Тоже самое можно, конечно, но сложнее, сделать с тайловыми текстурами. При этом вы получите примерно тот же объем данных на жестком диске и, вдобавок, кучу неприятностей: представьте, как для этого придется разбить геометрию и сколько потребуется видеопамяти. При использовании виртуальных текстур нам не нужно дополнительно разбивать геометрию, а расход памяти фиксирован и ограничен размером текстурного кэша, о котором мы поговорим чуть позже. Кроме того, снимается потребность в переключении текстур при рендеринге геометрии.
==3.  Структура виртуальной текстуры==
Технология виртуального текстурирования представляет собой особый подход к менеджменту текстур. Виртуальная текстура состоит из нескольких mip-уровней, каждый из которых поделен на страницы фиксированного размера. Количество уровней определяется размером виртуальной текстуры и может быть определено по следующей формуле:
n = log2( VtSize / PageSize ) + 1          (1)

где n – количество mip-уровней, VtSize – размер виртуальной текстуры по одной из осей, PageSize – размер страницы по одной из осей. Отметим, что технология, описываемя в данной статье подразумевает, что размеры виртуальной текстуры по вертикали и горизонтали равны и кратны степени двойки, как и размеры страниц. Размер страницы выбирается произвольно исходя из потребностей, так, размер страницы в idTech 5 равен 128 х 128 пикселей[2].
Для удобства будем нумеровать mip-уровни следующим образом: нулевой уровень соответствует размеру текстуры 128х128 пиксел (соответствует размеру одной страницы), первый 256х256, второй 512х512, третий 2048х2048 и так далее. Отметим, что это не соответствует нумерации принятой в OpenGL, т.к. там нулевой mip-уровень соответствует максимальному размеру текстуры. Почему нумерация выбрана данным образом, станет понятно далее. Нумерация страниц в mip-уровнях представлена на рисунке 1.
Virtual texture | Виртуальные текстуры



Рисунок 1 – Нумерация страниц виртуальной текстуры внутри mip-уровней
Как видно из рисунка 1, очевидно, что удобнее всего виртуальную текстуру будет хранить на жестком диске в виде последовательности страниц 0,1,2,3,4… и так далее. Таким образом, доступ к данным страницы просто вычислить по формуле:
PageFileOffset = PageNumber * PageSize * PageSize * Bpp    (2)
где:

PageFileOffset – смещение относительно заголовка файла;

PageNumber – номер страницы;

PageSize – размер страницы;

Bpp – количество байт на пиксель.
Очевидно также, что это не единственный способ хранения виртуальной текстуры. Эффективнее всего хранить страницы в сжатом виде, но это выходит за рамки данной статьи.
==4.  Конвейер==
Весь процесс работы с виртуальной текстурой можно разбить на несколько основных этапов:

1.  Определение видимых страниц виртуальной текстуры

2.  Анализ пункта 1, формирование списков страниц на загрузку, отсортированных по приоритетам (очередь приоритетов).

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

4.  Обновление текстурного кэша.

5.  Обновление таблицы переадресации.

6.  Рендеринг сцены.
Далее мы рассмотрим каждый пункт более подробно.
===4.1 Определение видимых страниц виртуальной текстуры===
На данном этапе нам нужно определить номера видимых страниц, ну и, соответственно, номер mip-уровня, к которому данная страница принадлежит.
Я пробовал два способа определения видимых страниц виртуальной текстуры. Первый из них основывался на принципе рендеринга в сцены в текстурном пространстве [4]. То есть мы рисуем сцену в текстуру, используя не мировые координаты вершин, а текстурные. При этом размер одной страницы в текстурном пространстве соответствовал одному пикселу. Данный метод имеет кучу проблем, так как при рендеринге в текстурном пространстве стандартные способы отсечения уже не работают, а в шейдер нужно передавать информацию о плоскостях пирамиды видимости (frustum) и рассчитывать clipping distances. Кроме того, необходим ручной backface-culling. Еще одним существенным недостатком данного метода является необходимость определения, не загораживает ли один объект другой, чтобы не допустить рендеринг невидимых в данный момент объектов. Ну и последний недостаток, который заставил меня выбрать другой метод, это необходимость в, так называемом, консервативном растеризаторе (conservative rasterizer) [3]. Дело в том, что если видно только около 50% процентов страницы, то стандартный GPU растеризатор попросту не будет растеризировать ее в один пиксель, таким образом, мы теряем ценную информацию о нужной странице. Учитывая сложность реализации консервативного растеризатора[3] данный метод в нашем случае оказался неэффективным.
Второй метод прост. Он заключается в ренедеринге сцены в текстуру (feedback buffer), при этом в каждом пикселе полученной текстуры кодируется номер страницы виртуальной текстуры. Очевиден недостаток данного метода – он избыточен, так как в разных пикселях feedback буфера может быть закодирована информация об одинаковых страницах. Используя некоторое приближение, на практике размер feedback буфера оказывается достаточным выбрать в десять раз меньше текущего разрешения экрана [5], что в разы снижает нагрузку на анализ данного буфера.
Далее мы рассмотрим, каким образом кодируется информация о видимых страницах и как по виртуальным координатам определяются их номера.
На входе вершинного шейдера мы оперируем виртуальными координатами текстуры (virtUV). Этап получения номера страницы состоит из определения текущего mip-уровня (miplevel) и положения страницы относительно текущего mip-уровня (pageOffset):
pageOffset = virtUV * exp2( miplevel )        (3)
На выходе в pageOffset будет номер страницы по x и по y относительно текущего mip-уровня. Далее не составит сложности простыми арифметическими операциями преобразовать pageOffset в pageNumber (рисунок 1). Здесь есть небольшая тонкость: дело в том, что в шейдере мы можем закодировать число от 0 до 255 на один канал. При размере виртуальной текстуры, используемой в игре Rage (128к x 128k пикселей), на верхнем mip-уровне pageOffset может превысить число 255. Решить это ограничение можно используя дополнительный канал, в котором кодируются дополнительные восемь бит (по четыре на pageOffset.x и pageOffset.y). Таким образом, максимальное количество страниц, которые мы можем закодировать увеличивается до 4095 по x и столько же по y. В оставшийся четвертый канал мы записываем номер mip-уровня страницы. Еще одним способом снять ограничение является использование float текстур с 32-битными каналами, но в нашем случае это накладно, т.к. в последствии нам нужно будет передавать текстуру в оперативную память для ее анализа.
Теперь, когда мы имеем feedback буфер, мы можем приступить к его анализу.
===4.2 Анализ feedback буфера===
На данном этапе нам нужно проанализировать полученный feedback буфер и сформировать очередь страниц на загрузку. Для этого нужно прочитать данные в оперативную память из видео памяти. К счастью OpenGL и DirectX позволяют это сделать, правда ценой медленной скорости передачи данных из GPU.
В цикле мы для каждого пикселя feedback буфера декодируем pageOffset и miplevel, преобразуя их в PageNumber (нумерация страниц изображена на рисунке 1). Здесь нам понадобится дополнительная структура данных, так называемая таблица страниц (pageTable). Физически это обычный массив, размер которого равен общему числу страниц (TotalPages). Каждый элемент представляет собой структуру из положения страницы в текстурном кэше (pageX, pageY) и ее mip-уровеня. Также предлагается создать аналогичного размера массив pageInfoTable. Поскольку предложенная организация расположения страниц внутри mip-уровней отлично подходит для реализации Quad-tree, в pageInfoTable мы можем сохранить дополнительную информацию о странице, а именно о ее потомке, а также переменную для хранения флагов (Cached, Needed). Было решено отделить pageTable и pageInfoTable только ради удобства, подробнее об этом будет сказано чуть позже (на пятом этапе, при создании таблицы переадресации).
Суммируя вышесказанное, грубый алгоритм анализа feedback буфера может быть представлен в виде следующего псевдокода:
while ( читаем_данные_из_feedback_буфера )
{
  pageNumber = декодировать_номер_страницы( текущий_пиксель );
  mipLevel = получить_мип_уровень( текущий_пиксель );
  if ( !( pageInfoTable[pageNumber].flags & CACHED ) )
  {
    if ( !(pageInfoTable[pageNumber].flags & NEEDED) )
    {
      pageInfoTable[pageNumber].flags |= NEEDED;
      PriorityQueueNode * n = new PriorityQueueNode;
      n->pageNumber = pageNumber;
      n->mipLevel = mipLevel;
      priorityQueue.push_back( n );
    }
  }
  else
  {
    int cacheIndex = pageTable[pageIndex].pageY * PageCacheGridSize + pageTable[pageIndex].pageX;
    pageCacheInfo[cacheIndex].time = текущее_время;
  }
}

Вводя флаг Needed, мы исключаем попадание в очередь на загрузку одинаковых страниц, а по флагу Cached мы исключаем загрузку страниц, которые уже загружены в текстурный кэш, в этом случае мы просто обновляем время последнего запроса страницы. В дальнейшем нам понадобится определять, как давно данная страница использовалась, чтобы эффективно обновлять текстурный кэш – эта информация хранится в pageCacheInfo.
При анализе feedback буфера также необходима сортировка страниц на загрузку по приоритетам так, чтобы первыми в очереди на загрузку были страницы с более высоким приоритетом.
===4.3 Загрузка виртуальных страниц===
Данный этап конвейера легко распараллеливается. В данном потоке мы просматриваем очередь, сформированную на этапе анализа, по формуле (2) вычисляем смещение страницы в файле и формируем список загруженных страниц.
===4.4 Обновление текстурного кэша===
Мы подошли вплотную к рассмотрению так называемого текстурного кэша или физической текстуры. Текстурный кэш это обычная текстура, располагающаяся в видео памяти. Так в игре Rage размер этой текстуры составляет 32х32 страницы или 4096х4096 пикселей [2].
Здесь необходимо определить, какие страницы в кэше давно не использовались, для чего нам понадобится вышеупомянутая pageCacheInfo. Далее просто копируем страницу в нужную позицию текстурного кэша, помечаем страницу как Cached и снимаем флаг Cached со страницы, которая была ранее на этом месте.
===4.5 Обновление таблицы переадресации===
Для того чтобы преобразовать виртуальные координаты в физические (или координаты текстурного кэша), на этапе рендеринга сцены нам понадобится таблица переадресации. Для этого нам хорошо подойдет вышеупомянутая pageTable. Сразу после обновления текстурного кэша мы пробегаемся по всем элементам pageTable начиная с первого. При этом, если страница отсутствует в кэше (проверяем флаг Cached), мы приравниваем pageX, pageY и mipLevel текущего элемента pageTable к pageX, pageY и mipLevel родительского. Таким образом, мы пробегаемся по всем листьям/узлам Quad-tree, в каждом хранится положение страницы в кэше и ее mip-уровень. Отметим, что mip-уровень страницы в листе Quad-tree может несоответствовать уровню листа в Quad-tree.
Поскольку напрямую использовать таблицу переадресации pageTable из шейдера мы не можем, здесь нам понадобится еще одна текстура (текстура переадресации). Эта текстура будет иметь то же количество mip-уровней, что и наше Quad-tree. Обычным копированием pageTable заполняем mip-уровни текстуры переадресации. Поскольку элемент pageTable представляет собой трехбайтовую структуру (pageX, pageY, mipLevel), это хорошо совместимо с трехкомпонентными текстурами. Теперь становится понятно, почему мы отделили pageInfoTable от pageTable. Итак, мы подошли к финальному этапу – к рендерингу сцены.
===4.6 Рендеринг сцены===
Итак, мы имеем:

- набор вершин с виртуальными текстурными координатами;

- текстурный кэш;

- текстура переадресации.
Все это мы легко можем использовать в шейдере. Мы рассчитываем текущий texture-lod (в GLSL его можно рассчитать с помощью dFdx, dFdy). Далее с помощью textureLod, на вход которой подаем виртуальные координаты и рассчитанный mip-level (lod), извлекаем из текстуры переадресации координаты страницы в текстурном кэше:
vec4 ind = textureLod( indirectionTable, virtUV, lod );
ind = floor( ind * 255.0 );
vec2 pageXY = ind.xy;
float mipLevel = ind.z;

Далее мы можем рассчитать физические координаты:
vec2 local = fract( virtUV * exp2( mipLevel ) )
vec2 physUV = ( pageXY + local ) / CACHE_SIZE;

Здесь CACHE_SIZE равно количеству страниц в кэше по одной из осей. В нашем случае размер кэша по обеим осям одинаков, т.е. составляет 32 страницы. Общее число страниц в кэше 32х32 = 1024.
Для наглядности мип уровни отображены на рисунке 2:


Virtual texture (show mips) | Виртуальные текстуры

Рисунок 2

==5.  Фильтрация текстур==
Поскольку страницы в кэше хранятся в хаотичном порядке, мы можем столкнутся с проблемой фильтрации текстур на границах страниц. Эффективным способом решения данной проблемы является добавление некоторого числа пикселей на границах страниц. Ребята из id Software использовали размер border-а равным 4 пикселя для каждой из сторон. Т.е. фактически визуально страницы имели размер 120х120 пикселей вместо 128х128 [5].
==6.  Заключение==
На первый взгляд выше описанный конвейер может показаться сложным для реализации, однако, к счастью, его нужно написать один раз. Вместе с тем мы получаем возможность сделать игровой мир более красивым и детализированным. Есть и противники данной технологии. Действительно, объем дискового пространства мегатекстура занимает немалый, а несвоевременная подгрузка страниц в текстурный кеш может выйти в виде артефактов при рендеринге. Пример сему игра Rage. Кроме того, необходимы специфические тулзы, которые поддерживают мегатекстурирование. Однако, несмотря на это, виртуальные текстуры, на мой взгляд, это большой шаг по пути к фотореалистичному рендерингу.

==7. Литература по теме==
1.  TANNER C. C., MIGDAL C. J., JONES M. T.: The clipmap: A virtual mipmap. In SIGGRAPH 1998 (1998)
2.  “idTech 5 Challenges From Texture Virtualization to Massive Parallelization”
3.  GPU Gems 2. Chapter 42. Conservative Rasterization
4.  Sylvain Lefebvre Jérome Darbon Fabrice Neyret «Unified Texture Management for Arbitrary Meshes»
5.  GPU Technology Conference "Using Virtual Texturing to Handle Massive Texture Data"


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


Блог: http://romanlovetext.blogspot.com/