Новость из категории: Информация

Разработка игре: простой и оптимизированный Ray-casting эффект

Разработка игре: простой и оптимизированный Ray-casting эффект

1. Введение
Эта статья описывает две простые реализации эмиссионной/абсорбционной модели используемой в медиа. Модель вычисляется через пиксели, используя ray-casting эффект на графической плате. В дополнение к очевидному (и довольно жесткому и неэффективному) стандартному подходу, также описан оптимизированный. Данный эффект активно используется в современном цифровом девелопменте - будь то разработка Android приложений или компьютерных игр.

Эмиссионная/абсорбционная модель, используемая в медиа, основывается на положении, что каждая точка в объеме определяется количеством излучаемого (включая рассеянный) и абсорбированного (также включая рассеянный) света. Эта статья рассчитана на практическую реализацию модели, поэтому я не стану вдаваться в теорию. Модель выведена и более подробно описана в [1].

Для решения проблемы дискретного сигнала, мы берем уже знакомый нам алгоритм коэффициента направленности для каждого видимого луча:
I = 0.0;
T = 1.0;
i = n;
while( T > epsilon && i >= 1 )
{
I = I + T*g[i];
T = T*t[i];
--i;
}
I = I + T*I0;

Здесь n – число принятых образцов, I – накопленная интенсивность (цвет), Т – накопленный коэффициент пропускания, g[i] – источник элемента i-ой части луча, t[i] – коэффициент пропускания и I0 - интенсивность внутри объема (фоновой цвет).

Если отсутствует начальный луч (T Ј e), требуется (например, ситуация является маловероятной для коэффициента пропускания, который меньше e) алгоритм представленный выше, записать более качественно, используя цикл, который запоминает фиксированное количество элементов:
I = 0.0;
T = 1.0;
for( int i=0; i {
I = I + T*s[i];
T = T*t[i];
}
I = I + T*I0;

Следующие две секции попытка реализацией этого алгоритма на GPU.

Разработка игре: простой и оптимизированный Ray-casting эффект

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

Метод основывается на заполнении бокс `filled' 3d текстурой(или 2d текстурой с растяжением по оси z), что даст значения g[i] и t[i]. `Filled' означает, что текстура должна находиться именно в боксе, т.е. бокс должен охватывать текстуры с координатами [0,1]3 равномерно (учтите, что поле в пространстве может быть трансформировано в произвольные аффинные преобразования). Основная идея заключается в том, чтобы просто сделать бокс реконструированных видимых лучей, охваченных для каждого пикселя, а затем рассчитать I и T по алгоритму приведенному выше.

2.1. Реконструкция луча
Выборка будет происходить в пространстве текстуры, поэтому нам необходимо найти часть видимого луча, которая лежит в кубе текстуры с координатами [0,1]3 . По тому, как мы хотим использовать луч для выборки остальных лучей, будет удобней часть луча с начальной точкой поместить в вектор (длина вектора равна интервалу выборки Ds, указывается в направлении вектора) и тогда часть выборки будет охвачена.

Как уже было сказано, для простого случая, начальные точки для этого сегмента (и луча направленного в пространство текстуры) можно легко рассчитать с позиции нынешнего фрагмента используя трансформацию в матрицу размером 4x4 - `World-To-Texture. Тем не менее, я буду описывать другой подход. Он выглядит более сложным и затратным (на самом деле, он является простым сценарием), но является более гибким и будет присутствовать в следующем разделе, где обобщается описанная техника.

Вершина шейдера проходит ограничитель позиции и z-компонента видимого пространства (значение линейной глубины) пиксельного шейдера. Следующий HLSL код показывает, как сегмент восстанавливается из полученной информации:
// Для начала получаем значение NDC
input.ClipPosition.xy /= input.ClipPosition.w;

// Затем мы можем использовать 'focal lengths' для реконструкции
// направления луча в видимом пространстве (заметьте, что он ненормированный,
// но работает при z-component равному 1).
float3 rayDir_VS = float3(
input.ClipPosition.x / WorldToClip[0][0],
input.ClipPosition.y / WorldToClip[1][1],
1
);

// Теперь с легкостью реконструируем начальную точку
// и строим вектор в видимой плоскости...
float3 rayStart_VS = rayDir_VS * input.Depth;
float3 rayIncr_VS = normalize( rayDir_VS ) * g_SamplingInterval;

// ... и трансформируем текстуры пространства
float3 rayStart_TS = mul( float4(rayStart_VS,1), g_ViewToTex ).xyz;
float3 rayIncr_TS = mul( rayIncr_VS, (float3x3)g_ViewToTex );

Количество выборок эффективно увеличивает вектор, позволяя добавлять начальную точку до конечной позиции, не превышая длины лежащей в объеме. Поскольку мы уже имеем прирост вектора в пространстве текстуры с объемом [0,1]3, мы можем просмотреть каждую ось в отдельности, а затем вычислить минимум:
// Сколько может использоваться луч rayIncr_TS
// прежде, чем мы запустим блок куба
float3 l = float3(
rayIncr_TS.x > 0 ? (1-rayStart_TS.x)/rayIncr_TS.x
: -rayStart_TS.x/rayIncr_TS.x,
rayIncr_TS.y > 0 ? (1-rayStart_TS.y)/rayIncr_TS.y
: -rayStart_TS.y/rayIncr_TS.y,
rayIncr_TS.z > 0 ? (1-rayStart_TS.z)/rayIncr_TS.z
: -rayStart_TS.z/rayIncr_TS.z
);

// Минимальное количество выборок для трех осей
int numSamples = min( l.x, min(l.y, l.z) );

// Наконец фиксируем numSamples, что бы избежать
// проблем с цифрами и указать шейдерному компилятору,
// что отбор выборок прекратится после последней итерации
numSamples = clamp( 0, 1000, numSamples );

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

Разработка игре: простой и оптимизированный Ray-casting эффект

2.2. Выборка
Процесс выборки, описанный во введении, легко переводится в HLSL код, только следует учесть одну тонкость.

Возникающий объем должен быть независимым от выборки, так что есть основания предполагать, что значения, взятые в текстуре, считаются как единицы на длину. Источник элементов не вызывает проблем, он просто сокращается линейно интервалу выборки. Однако существует проблема с коэффициентом поглощения. Потому как поглощение света процесс экспоненциальный, нельзя найти к нему линейный фактор, который можно было применить ко всему масштабу, соответствующего определенной выборке. Если Т коэффициент поглощения в расчете единицы на длину и Т является коэффициентом расстояния Ds, то синтезируя две формулы получаем t=TDs. Чтобы избежать зависания функции в цикле выборки я, обычно, делаю предположение, что:
а) объем выборки имеет фиксированный интервал и коэффициент поглощения имеет оптимальное значение на этом интервале, или:
б) полностью отсутствует абсорбция.

Эта функция производит выборку:
float4 Integrate_Fixed(
float3 rayStart_TS, float3 rayIncr_TS,
int numSamples, float4 samplingParams )
{
float3 I = 0;
float T = 1;
float3 texCoord = rayStart_TS;
for( int i=0; i
{

// начинаем выборку (rgb источник элемента, a коэффициент поглощение)
float4 tex = g_EmiAbsTexture.SampleLevel(
g_ClampedSampler, texCoord, 0 );

// накапливаем I и T
I += T*tex.rgb;
T *= pow( f.a, g_SamplingInterval );

// следующий шаг
texCoord += rayIncr_TS;
}
I *= g_SamplingInterval;
return float4( I, T );
}

2.3. Проблемы
Описанный способ на сегодняшний день очень легко осуществить, требуется только один проход над геометрическим объемом, и нет нужды в каких-либо дополнительных целевых показателях. Тем не менее, у него есть серьезные ограничения:

Он работает, если камера вблизи плоскости не пересекается с объемом. Если это произойдет, некоторые пиксели будут обрезаны и ray-casting для них невыполним.

Геометрия, ограниченная областью, поскольку алгоритм используется для реконструкции лучей.

Он не даст результатов, если правильное геометрическое тело пересечет объем, потому как реконструкция лучей не принимает это в расчет.

Используется фиксированный и, вероятно, не оптимальный объем выборки.

Следующий раздел содержит некоторые улучшения, для решения этих проблем.

Разработка игре: простой и оптимизированный Ray-casting эффект

3. Оптимизированный способ
Во-первых, оптимизируем алгоритм реконструкции лучей, а во-вторых, оптимизируем стратегию внедрения выборки.

3.1. Оптимизация алгоритма реконструкции
В данном подразделе давайте корректно усовершенствуем реконструкцию лучей для произвольных выпуклых объемов, даже если камера будет находится вблизи или правильное геометрическое тело пресечет объем. Однако, что бы достичь этой цели, нам необходимо добавить два визуальных значения: линейную длину буфера, которая хранит координаты твердого тела в видимой области z-компонента (которая используется в любом случае) и другая длина буфера, которая хранит глубину фрагмента объема(ов). Помещение этих графических значений обратно в буфер разрешения зачастую сопровождается значительной тратой ресурсов (памяти и времени) — в большинстве случаев ресурсов все-таки достаточно для осуществления ray-casting на низких разрешениях и перемещения результат смешивания обратно в буфер. Базовый набросок алгоритма выглядят так:
input.ClipPosition.xy /= input.ClipPosition.w;
float2 screenTexCoord =
input.ClipPosition*float2(0.5, -0.5) + float2(0.5,0.5);
float3 rayDir_VS = float3(
input.ClipPosition.x * g_SamplingParams.z,
input.ClipPosition.y * g_SamplingParams.w,
1
);

// startDepth всегда корректен, даже когда впереди стоящий
// фрагмент был обрезан(тогда текстура имеет вид clipNear).
float startDepth = g_VolumeDepthTexture.Sample(
g_ClampedPointSampler, screenTexCoord );

// endDepth является фоном объема или геометрической фигуры,
// смотря какая фигура ближе.
float sceneDepth = g_SolidDepthTexture.Sample(
g_ClampedPointSampler, screenTexCoord );
float endDepth = min( input.Depth, sceneDepth );

// Зажим фрагмента, который находится позади геометрии
clip( endDepth - startDepth );

Эти данные могут быть использованы для инициализации параметров луча, как показано в предыдущем разделе. Но при этом могут быть реализованы незначительные изменения в методе совершенствования структуры выборки.

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

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

Следующий HSLS фрагмент показывает процедуру:
float3 rayStart_VS = rayDir_VS * startDepth;
float3 rayEnd_VS = rayDir_VS * endDepth;
float3 rayDiff_VS = rayDir_VS * (endDepth-startDepth);

// Берем вектор с точками входа и выхода в видимом пространстве
float3 rayDiff_TS = mul( rayDiff_VS, (float3x3)g_ViewToTex );

// Масштабируем один текстел
float3 m = abs( rayDiff_TS ) * g_TextureSize;
float scale = 1.0 / max( m.x, max(m.y, m.z) );
float3 rayIncr_TS = rayDiff_TS * scale;

// Установка numSamples и samplingInterval.
// Они рассчитываются от соотношения длинны.
int numSamples = length(rayDiff_TS) / length(rayIncr_TS);
numSamples = clamp( 0, 1000, numSamples );
float samplingInterval = 1.0/numSamples * length( rayDiff_VS );

// Наконец, перемещаем точку вход на половину прироста вектора
// внутри объема в надежде простого закрытия средних точек
float3 rayStart_TS = mul( float4(rayStart_VS,1), g_ViewToTex ).xyz;
rayStart_TS += rayIncr_TS * 0.5;

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

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

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

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

Разработка игре: простой и оптимизированный Ray-casting эффект

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

Если даже простой подход вам не понятен, то гораздо правильным решением для вас станет доверить разработку своей игры специалистам сайта https://app-android.ru. Они выполнят любую поставленную задачу быстро и максимально качественно.

Рейтинг статьи

Оценка
2/5
голосов: 1
Ваша оценка статье по пятибальной шкале:
 
 
   

Поделиться

Похожие новости

Комментарии

^ Наверх