среда, 8 февраля 2017 г.

Разработка собственного игрового движка.




Начало разработки собственного игрового движка. Постараюсь делиться прогрессом.









четверг, 3 мая 2012 г.

Simply Compute Shader Example

Одной из главных особенностей DirectX 11 является наличие Compute Shader. У меня появилась идея написать простой пример на эту тему. Прежде всего позвольте мне сказать, что вычислительные шейдеры являются чем-то потрясающим! Они открывают так много возможностей, что сразу теряешься в идеях, где бы их опробовать в действии. В этом примере будет показано, как создать Compute Shader и затем использовать его для запуска потоков, которые просто выведут идентификатор потока в текстуру.
 Device device = new Device(DriverType.Hardware, DeviceCreationFlags.Debug, FeatureLevel.Level_11_0);
ComputeShader compute = Helper.LoadComputeShader(device, "SimpleCompute.hlsl", "main"); Texture2D uavTexture;
UnorderedAccessView computeResult = Helper.CreateUnorderedAccessView(device, 1024, 1024, Format.R8G8B8A8_UNorm, out uavTexture);
device.ImmediateContext.ComputeShader.Set(compute);
device.ImmediateContext.ComputeShader.SetUnorderedAccessView(computeResult, 0);device.ImmediateContext.Dispatch(32, 32, 1);
Texture2D.ToFile(device.ImmediateContext, uavTexture, ImageFileFormat.Png, "uav.png");

Верьте или нет, но это весь код обработчика.

Вот что именно нужно сделать в коде:
1) Создать графическое устройство DX11, с тем чтобы использовать Compute Shaders 5.0
2) Загрузить/скомпилить код HLSL в объект ComputeShader.
3) Создать объект UnorderedAccesdView  1024 x 1024, который будет использоваться для хранения данных.
4) Передать ComputeShader и UnorderedAccesdView на графическое устройство.
5) Запустить ComputeShader путем вызова метода Dispatch (32 x 32 x 1).
6) Сохранить текстуру на жесткий диск.

Код HLSL еще проще:

RWTexture2D<float4> Output;

[numthreads(32, 32, 1)]
void main( uint3 threadID : SV_DispatchThreadID )
{
    Output[threadID.xy] = float4(threadID.xy / 1024.0f, 0, 1);
}

Как вы можете видеть объект RWTexture2D используется для хранения данных. Шейдер -настроен для запуска 32 x 32 x 1 потоков в группе потоков. Это означает, что процессор запустит 32 x 32 x 1 группу  потоков, то есть  (32x32) x (32x32) x 1 потоки запустятся по отдельности. Это равняется одному потоку на пиксел в выходных данных UAV. Так в UAV, цвет устанавливается равным идентификатору потока - результат работы этого кода приводится на следующем изображении :



Довольно просто, да? Но не очень интересно. Мы могли бы легко сделать что-то подобное с pixel Shader (хотя нам пришлось бы растрировать полноэкранный квад для этого).

Мы должны попытаться сделать то, что показывает силу вычислительного шейдера. Что-то, что нельзя было сделать в шейдерах раньше. Как насчет рисования некоторых примитивов, таких как линии или круги? Для рисования линий, давайте использовать алгоритм ЦДА. Он переводится на HLSL очень легко.

void Plot(int x, int y)
{
   Output[uint2(x, y)] = float4(0, 0, 1, 1);
}

void DrawLine(float2 start, float2 end)
{
    float dydx = (end.y - start.y) / (end.x - start.x);
    float y = start.y;
    for (int x = start.x; x <= end.x; x++)    
   {
        Plot(x, round(y));        y = y + dydx;   
   }
}

Для рисования кругов давайте использовать Midpoint Circle алгоритм. Для краткости я не буду обьяснять его сейчас. Для этого в моей главной функции CS, я просто добавлю этот код:

if (threadID.x == 1023 && threadID.y == 1023)
{
    DrawLine(float2(0, 0), float2(1024, 1024));
    DrawLine(float2(0, 1023), float2(1023, 0));
    DrawCircle(512, 512, 250);
    DrawCircle(0, 512, 250);
}

Результат этого кода приводится на следующем рисунке:



Я подозреваю, что вам, возможно покажется довольно странным, написание шейдера, который рисует примитивы. Но этот пример определенно помогает проиллюстрировать мощь Compute Shader.

Effects in DirectX 11

Я никогда не поверю, что Microsoft сделали доброе дело, так усложнив DirectX11. Effects framework - API которое поддерживало загрузку и использование файлов эффектов сгруппированных в HLSL код, рендерринг через passes и techniques – больше не являются неотъемлемой частью D3DX. Вместо этого они предоставили исходный код этой библиотеки, и это означает, что вы можете скомпилировать его самостоятельно!

Итак сделаем это: Зайдите в DX SDK подпапку "Samples\C++\Effects11", откройте "Effects11_*.sln".

Чтобы использовать эффекты API в своем проекте вы должны включить этот заголовок: "YOUR_DX_SDK_PATH\Samples\C++\Effects11\Inc\D3dx11effect.h" and link with this lib: "YOUR_DX_SDK_PATH\Samples\C++\Effects11\Debug\D3DX11EffectsD.lib (Debug) or "YOUR_DX_SDK_PATH\Samples\C++\Effects11\Release\D3DX11Effects.lib" (Release), as well as with "d3dcompiler.lib" (в обеих конфигурациях).

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

// Compile effect from HLSL file into binary Blob in memory
ID3D10Blob *effectBlob = 0, *errorsBlob = 0;
HRESULT hr = D3DX11CompileFromFile("Effect1.fx", 0, 0, 0, "fx_5_0", 0, 0, 0, &effectBlob, &errorsBlob, 0);
assert(SUCCEEDED(hr) && effectBlob);
if (errorsBlob) errorsBlob->Release();
// Create D3DX11 effect from compiled binary memory block
ID3DX11Effect *g_Effect;
hr = D3DX11CreateEffectFromMemory(effectBlob->GetBufferPointer(), effectBlob->GetBufferSize(), 0, g_Dev, &g_Effect);
assert(SUCCEEDED(hr));
effectBlob->Release();

Одного эффекта не достаточно. Вам нужно получить объект, который представляет "pass" чтобы использовать его. Если вы получите технику с эффектом (по индексу или по имени), то от нее получите и "pass".

ID3DX11EffectTechnique *g_EffectTechnique; // No need to be Release()-d.
g_EffectTechnique = g_Effect->GetTechniqueByIndex(0);
assert(g_EffectTechnique && g_EffectTechnique->IsValid());
ID3DX11EffectPass *g_EffectPass; // No need to be Release()-d.
g_EffectPass = g_EffectTechnique->GetPassByIndex(0);
assert(g_EffectPass && g_EffectPass->IsValid());

Теперь у вас есть этот объект, и вы можете применить настройки этого пасса к deviceContext во время рендеринга:

g_EffectPass->Apply(0, g_Ctx);
g_Ctx->Draw(3, 0);

Но все-таки одна проблема остается. В DirectX 11 вам нужно передать указатель на байт-код  вместе со скомпилированным шейдером при создании input layout - шаг, который вы, вероятно, не можете пропустить. К счастью, есть способ получить доступ к этому указателю. Он хранится внутри загруженного эффекта. Вам просто нужно пройти через два дескриптора, как здесь:

D3DX11_PASS_SHADER_DESC effectVsDesc;
g_EffectPass->GetVertexShaderDesc(&effectVsDesc);
D3DX11_EFFECT_SHADER_DESC effectVsDesc2;
effectVsDesc.pShaderVariable->GetShaderDesc(effectVsDesc.ShaderIndex, &effectVsDesc2);
const void *vsCodePtr = effectVsDesc2.pBytecode;
unsigned vsCodeLen = effectVsDesc2.BytecodeLength;
ID3D11InputLayout *g_InputLayout;
D3D11_INPUT_ELEMENT_DESC inputDesc[] = { /* ... */ };
hr = g_Dev->CreateInputLayout( inputDesc, _countof(inputDesc), vsCodePtr, vsCodeLen, &g_InputLayout);

К счастью, дела обстоят так, что effect framework не добавляет больше функциональности чем HLSL шейдеры поддерживающиеся в D3D11, поэтому вы можете его не использовать. Определение этих методов и passess не столь важно, в конце концов ...

пятница, 27 апреля 2012 г.

SSAO - Простой и практический подход.

Глобальное освещение (GI) — термин, используемый в компьютерной графике для вычисления освещения, вызванного неким взаимодействием между ближайшими поверхностями. Довольно часто термин GI используют только при окрашивании поверхности обьектов лучом отраженным от обьектов окружающей среды. Прямое освещение непосредственно от источника света – легко вычисляется в режиме реального времени на современном оборудовании, но мы не можем сказать то же самое о GI потому, что нам нужно обработать информацию о ближайших поверхностях для каждой точки в сцене, а управлять этим процессом еще довольно сложно. Однако есть определенные методы вычислений GI, которыми управлять не так сложно. Когда свет падает на сцену,или отражается от поверхности, то в сцене могут иметь место некоторые точки, которые имеют меньше шансов получить порцию света: уголки, трещиты между объектами, складки, и др. Это приводит к появлению тех областей, которые в результате будут темнее, чем окружающие их обьекты.

Предпосылки

Оригинальная реализация Crytek базируется на  буфере глубины и работает примерно следующим образом: для каждого пикселя в буфере глубины опрашиваются несколько точек в 3D пространстве вокруг него, проецируются обратно в пространство экрана для сравнения глубин этих пикселей с глубиной  текущего пикселя, с целью понять - выбранные пиксели находится впереди (не загажденный) или за (загражденный) текущим пикселем.
Буфер заграждений создается из усредненных значений дистанций между выборками. Однако у этого метода есть некоторые проблемы (такие, как самозатенение, появление хало), о них я расскажу позднее.

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


Алгоритм

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

Дистанция "d" до окклюдера
Угол между нормалью "N" и векторм между occluder и occludee "V".

С учетом этих двух факторов выводится простая формула для вычисления затенения:

 Occlusion = max( 0.0, dot( N, V) ) * ( 1.0 / ( 1.0 + d ) )

Первое выражение max( 0.0, dot( N,V ) ), основывается на интуитивном предположении, что точки над вершиной вносят больший вклад в затенение, чем точки находящиеся левее или правее. Второе выражение  ( 1.0 / ( 1.0 + d ) )  смягчает  линейность эффекта зафисимую от расстояния. Можно использовать квадратичное затухания или воспользоваться любой другой функцией, это просто дело вкуса. Алгоритм очень прост: выбираем несколько соседей вокруг текущего пиксела и накапливаем их вклад в затенение с использованием формулы выше.

Чтобы выбрать окклюдеры, я использую 4 выборки (<1,0>, <-1,0>, <0,1>, <0,-1>) которые повернуты на 45 ° и 90 ° и отражаются в последствии случайным образом используя текстуру случайных нормалей.

Это код шейдеров HLSL для эффекта, который должен применяться к полноэкранному квадрату:

sampler g_buffer_norm; 
sampler g_buffer_pos; 
sampler g_random; 
float random_size; 
float g_sample_rad; 
float g_intensity; 
float g_scale; 
float g_bias; 
 
struct PS_INPUT 
     float2 uv : TEXCOORD0; 
}; 
 
struct PS_OUTPUT 
     float4 color : COLOR0; 
}; 
 
float3 getPosition(in float2 uv) 
    return tex2D(g_buffer_pos,uv).xyz; 
 
float3 getNormal(in float2 uv) 
    return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f); 
 
float2 getRandom(in float2 uv) 
     return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f); 
 
float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 
   float3 diff = getPosition(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity; 
 
PS_OUTPUT main(PS_INPUT i) 
   PS_OUTPUT o = (PS_OUTPUT)0; 
 
   o.color.rgb = 1.0f; 
   const float2 vec[4] = {float2(1,0),float2(-1,0), 
                                     float2(0,1),float2(0,-1)}; 
 
float3 p = getPosition(i.uv); 
float3 n = getNormal(i.uv); 
float2 rand = getRandom(i.uv); 
 
float ao = 0.0f; 
float rad = g_sample_rad/p.z; 
 
//**SSAO Calculation**// 
int iterations = 4; 
for (int j = 0; j < iterations; ++j) 
  float2 coord1 = reflect(vec[j],rand)*rad; 
  float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, 
                          coord1.x*0.707 + coord1.y*0.707); 
   
  ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n); 
  ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n); 
  ao += doAmbientOcclusion(i.uv,coord2, p, n); 
ao/=(float)iterations*4.0; 
//**END**// 
 
//Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc. 
return o; 
}

Эта техника очень напоминает технику описанную в "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Основные различия в структуре выборки и функции AO. Он также может рассматриваться как Image-Space версия  "Hardware Accelerated Ambient Occlusion Techniques on GPUs". Некоторые пояснения по коду:

Радиус делится на p.z, для изменения масштаба в зависимости от расстояния до камеры. Если пропустить этот раздел, все точки на экране будут использовать же радиус выборки, и результат не будет учитывать перспективу.
В цикле for,
coord1 это первоначальная выборка координат на 90 °.
coord2 является те же координаты, повернутые на 45 °.

Результаты



Как видите, код не сложный и не большой. Результаты не самозатеняются и хало почти нет.
Это две основные проблемы всех алгоритмов SSAO, которые используют только буфер глубины в качестве исходных данных, вы можете увидеть их в этих изображениях:

Self-occlusion появляется, потому что традиционные алгоритмы формирования выборок внутри области вокруг каждого пикселя, поэтому на плоских поверхностях которые не «occluded», по меньшей мере, половина из выборок помечены как «occluded». Из за этого получается сероватый цвет для общей окклюзии. Белое хало вокруг объектов возникает, потому что в этих областях self-occlusion не выполняется. Таким образом избавление от self-occlusion на самом деле хорошо помогает избавиться от хало. При перемещении камеры результат окклюзии изменяется, если следовать этому методу. Если вы склоняетесь к качеству вместо скорости, можно использовать два или несколько проходов алгоритма (повторяющиеся цикл for в коде) с различными радиусами, один для захвата более глобальных AO, а другие для детализации небольших трещин.


Продолжаем

Я описал упрощенную реализацию SSAO, которая очень хорошо подходит для игр.
Однако ее легко улучшить если принимать во внимание скрытые от камеры поверхности, для получения более высокого качества. Это потребует три буфера: две позиции/глубина буферов (front/back faces) и один буфер нормалей. Но вы можете сделать это только с двумя буферами: хранить глубину front/back faces в красном и зеленом каналах буфера, соответственно, а затем восстановить позицию каждого из них. Таким образом у вас будет один буфер для позиций и второй буфер для нормалей. Таковы результаты при использовании 16 выборок для каждой позиции буфера:


left: front faces occlusion, right: back faces occlusion
Что бы применить это, нужно просто добавить вызова функции doAmbientOcclusion()
Это дополнительный код, который необходимо добавить:
внутри цикла for, добавьте эти вызовы:
ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n); 
ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n);

Добавьте эти две функции шейдер: 

 float3 getPositionBack(in float2 uv) 

    return tex2D(g_buffer_posb,uv).xyz; 

float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) 

    float3 diff = getPositionBack(tcoord + uv) - p; 
   const float3 v = normalize(diff); 
   const float d = length(diff)*g_scale; 
   return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d)); 
}

Добавьте семлер с именем «g_buffer_posb», содержащий позицию backFaces (для его создания вам нужно рисовать сцену с передних граней).  Еще небольшое изменение, которое может помочь, на этот раз улучшить скорость, является добавление простой системы LOD (уровень детализации) в наш шейдер. Измените фиксированное количества итераций в соответствии с:

 int iterations = lerp(6.0,2.0,p.z/g_far_clip);

Переменная «g_far_clip» - это расстояние до дальней плоскости отсечения, которое должно быть передано в шейдер. Теперь количество итераций, применяемое к каждой точке зависит от расстояния до камеры. Таким образом для дальних точек выполнется меньше выборок, это позволяет повысить производительность без заметного ухучшения качества. Я не использовал это при измерении производительности (см. ниже), но все-же:




Так-же полезно рассмотреть, как этот метод сравнивается с трассировкой лучей AO. Цель этого сравнения заключается в том, чтобы увидеть, будет ли этот метод приравниваться к реальному AO при использовании достаточного количества выборок.


Left: the SSAO presented here, 48 samples per pixel (32 for front faces and 16 for back faces), no blur. Right: Ray traced AO in Mental Ray. 32 samples, spread = 2.0, maxdistance = 1.0; falloff = 1.0.

И совет на последок: не планируйте подключить шейдер в свой конвейер и автоматически получить реалистичный результат. Несмотря на эту реализацию, что-бы получить соотношение хорошей производительности и качества SSAO, вы должны настроить его как можно тщательнее для своих нужд и добиться максимальной производительности.
Добавление или удаление выборок и размытия, изменяя интенсивность и т.д. Вам следует также понять, подходит ли такой алгоритм SSAO для вас. Если у вас есть много динамических объектов в сцене, то возможно нет необходимости в SSAO вообще, может быть использование LightMap-ов достаточно для ваших целей, так- как они могут обеспечить лучшее качество для статических сцен. Я надеюсь, что вы извлекли пользу от этого метода. Весь код, представленный в этой статье доступен по mit-license.
Оригинал статьи можно найти здесь a-simple-and-practical-approach-to-ssao




Physically Based Rendering