Маленькие трюки DirectX и HLSL

от автора

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

1. Матрицы в HLSL

Допустим в вертексном шейдере нам нужно повернуть нормаль (тангенту, бинормаль) вертекса и у нас есть мировая матрица 4х4. Но сдвиг, зашитый в матрицу, нам не нужен. Тогда просто приводим матрицу к 3х3:

output.Normal = mul(input.Normal.xyz, (float3x3)RotM);

Кстати, если вам нужно получить инверсную матрицу от матрицы поворота 3х3, и при этом она ортогональна, то достаточно ее просто транспонировать:

float3х3 invMat = transpose(Mat);

Или же можно обойтись и без этого, если вам нужно лишь получить трансформированный инверсной матрицей вектор — тогда достаточно изменить порядок умножения матрицы и вектора:

float3 outVector = mul((float3x3)RotM, inVector.xyz);

Вы наверняка знаете, что для доступа к элементу матрицы можно использовать запись типа:

float value = World._m30;

Однако синтаксис позволяет получить сразу несколько значений из матрицы. Например получить перемещение из матрицы трансформации:

float3 objPosition = World._m30_m31_m32;

2. Рендер без вертексного буфера

В DX11 есть замечательная возможность отправить на рендер вершины, не создавая для этого вершинный буфер. Код для C# и врапера SharpDX:

System.IntPtr n_IntPtr = new System.IntPtr(0); device.ImmediateContext.InputAssembler.InputLayout = null; device.ImmediateContext.InputAssembler.SetVertexBuffers(0, 0, n_IntPtr, n_IntPtr, n_IntPtr); device.ImmediateContext.InputAssembler.SetIndexBuffer(null, Format.R32_UInt, 0); device.ImmediateContext.Draw(3, 0);

Здесь мы отправляем на рендер три вершины. А в шейдере, для примера, мы можем построить из них полноэкранный квад:

struct VertexInput { 	uint VertexID : SV_VertexID; }; struct PixelInput { 	float4 Position : SV_POSITION; };  PixelInput DefaultVS(VertexInput input) { 	PixelInput output = (PixelInput)0;  	uint id = input.VertexID;  	float x = -1, y = -1; 	x = (id == 2) ? 3.0 : -1.0; 	y = (id == 1) ? 3.0 : -1.0;  	output.Position = float4(x, y, 1.0, 1.0); 	return output; }

3. Рендер без пиксельного шейдера

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

pass GS_PSSM {     SetVertexShader(CompileShader(vs_5_0, ShadowMapVS()));     SetGeometryShader(CompileShader(gs_5_0, ShadowMapGS()));     SetPixelShader(NULL);      SetBlendState(NoBlending, float4(0.0f, 0.0f, 0.0f, 0.0f), 0xFFFFFFFF);     SetDepthStencilState(EnableDepth, 0); }

Или же:

device.ImmediateContext.PixelShader.Set(null);

В обоих случаях пиксельный шейдер не будет выполнен, и в рендер таргет будет записана интерполированная в вертексном шейдере глубина.
Можно пойти дальше, и установить пиксельный шейдер, который ни чего не возвращает:

void ZPrepasPS(PixelInputZPrePass input) {     float4 albedo = AlbedoMap.Sample(Aniso, input.UV.xy);     if (albedo.w < AlphaTest.x)         discard; }

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

4. Alpha to coverage

В DX10/11 появилась замечательная возможность аппаратно, с помощью MSAA, сглаживать альфатест. Если простыми словами, то это возможность в пиксельном шейдере самостоятельно указать, сколько семплов каждого пикселя MSAA рендертаргета прошли тест.

static const float2 MSAAOffsets8[8] = {     float2(0.0625, -0.1875), float2(-0.0625, 0.1875),     float2(0.3125, 0.0625), float2(-0.1875, -0.3125),     float2(-0.3125, 0.3125), float2(-0.4375, -0.0625),     float2(0.1875, 0.4375), float2(0.4375, -0.4375) };  void ZPrepasPSMS8(PixelInputZPrePass input, out uint coverage : SV_Coverage) {     coverage = 0;      [branch]     if (AlphaTest.x <= 1 / 255.0)         coverage = 255;     else     {         float2 tc_ddx = ddx(input.UV.xy);         float2 tc_ddy = ddy(input.UV.xy);          [unroll]         for (int i = 0; i < 8; i++)         {             float2 texelOffset = MSAAOffsets8[i].x * tc_ddx + v2MSAAOffsets8[i].y * tc_ddy;             float temp = AlbedoMap.Sample(Aniso, input.UV.xy + texelOffset).w;              if (temp >= 0.5)                 coverage |= 1 << i;         }     } }

У меня учет альфатеста происходит только на стадии Z-препаса. После финального прохода нам достаточно выполнить резолв MSAA буфера и наш альфатест сгладится подобно обычной геометрии (правильный резолв HDR MSAA буфера тема для отдельной статьи).

Сравнительные скрины


5. Экранный антиальясинг нормалей

Данная идея мне пришла после внедрения предыдущего пункта. Я выполняю суперскмплинг из текстуры нормали со смещением UV, вычисленным в скринспейсе. Так как я использую подход Forward+ c Z-препасом, то такая операция стоит минимально.

static const float2 MSAAOffsets4[4] = {     float2(-0.125, -0.375), float2(0.375, -0.125),     float2(-0.375, 0.125), float2(0.125, 0.375) };  float3 ONormal = float3(0,0,0); float2 tc_ddx = ddx(input.UV.xy); float2 tc_ddy = ddy(input.UV.xy); [unroll] for (int i = 0; i < 4; i++) {     float2 texelOffset = MSAAOffsets4[i].x * tc_ddx + MSAAOffsets4[i].y * tc_ddy;     float4 temp = NormalMap.Sample(Aniso, input.UV.xy + texelOffset*1.5);     ONormal += temp.ywy; } ONormal *= 0.25; Normal = ONormal * 2.0f - 1.0f;

Сравнительные скрины


6. Нормали двухсторонней геометрии

Что бы избежать артефактов освещения, для даблсайд треугольников нужно инвертировать нормаль, если мы смотрим на обратную их сторону:

float3 FinalPS(PixelInput input, bool isFrontFace : SV_IsFrontFace) : SV_Target {     input.Normal *= (1 - isFrontFace * 2);     ...

7. Узнать размер текстуры в шейдере

Сам такой возможностью не пользуюсь, так как есть сомнения относительно ее производительности, однако кому-то может оказаться полезным:

Texture2D texture;  uint width, height; texture.GetDimensions(width, height);

8. Спрайты геометрическими шейдерами

С появлением геометрических шейдеров стало возможно делать различные оптимизации. Например ускорить рендер спрайтов. В видеокарту отправляются единичные вертексы, содержащие всю информацию о спрайте. В геометрическом шейдере из них конструируется полноценный спрайт:

struct VS_IN {     float4 Position : POSITION;     float4 UV       : TEXCOORD0;     float4 Rotation : TEXCOORD1;     float4 Color    : TEXCOORD2; };  struct VS_OUT {     float4 Position : SV_POSITION;     float4 UV       : TEXCOORD0;     float4 Rotation : TEXCOORD1;     float4 Color    : TEXCOORD2; };  struct GS_OUT {     float4 Position : SV_POSITION;     float2 TexCoord	: TEXCOORD0;     float4 Color    : TEXCOORD1; }  VS_OUT GSSprite_VS( VS_IN Input ) {     VS_OUT Output;          float2 center = (Input.Position.xy + Input.Position.zw) * 0.5;     float2 size = (Input.Position.zw - center)*2.0;      Output.Position = float4(center, size);     Output.UV = Input.UV;     Output.Color = Input.Color;     Output.Rotation = Input.Rotation;      return Output; }  [maxvertexcount(6)]   void GSSprite_GS(point VS_OUT In[1], inout TriangleStream<GS_OUT> triStream) {     GS_OUT p0 = (GS_OUT) 0;     GS_OUT p1 = (GS_OUT) 0;     GS_OUT p2 = (GS_OUT) 0;     GS_OUT p3 = (GS_OUT) 0;      In[0].Position.xy = In[0].Position.xy * Resolution.zw * 2.0 - 1.0;     In[0].Position.y = -In[0].Position.y;      float2 r = float2(In[0].Rotation.x, -In[0].Rotation.y);     float2 t = float2(In[0].Rotation.y, In[0].Rotation.x);      p0.Position = float4(In[0].Position.xy + (-In[0].Position.z * r + In[0].Position.w * t) * Resolution.zw, 0.5, 1.0);     p0.TexCoord = In[0].UV.xy;     p0.Color = In[0].Color;      p1.Position = float4(In[0].Position.xy + (In[0].Position.z * r + In[0].Position.w * t) * Resolution.zw, 0.5, 1.0);     p1.TexCoord = In[0].UV.zy;     p1.Color = In[0].Color;      p2.Position = float4(In[0].Position.xy + (In[0].Position.z * r - In[0].Position.w * t) * Resolution.zw, 0.5, 1.0);     p2.TexCoord = In[0].UV.zw;     p2.Color = In[0].Color;      p3.Position = float4(In[0].Position.xy + (-In[0].Position.z * r - In[0].Position.w * t) * Resolution.zw, 0.5, 1.0);     p3.TexCoord = In[0].UV.xw;     p3.Color = In[0].Color;      triStream.Append(p0);     triStream.Append(p1);     triStream.Append(p2);     triStream.RestartStrip();      triStream.Append(p0);     triStream.Append(p2);     triStream.Append(p3);     triStream.RestartStrip(); }

По моим замерам такой подход дает порядка 20-30% ускорения как на слабом, так и мощном железе.

9. Lens Flare

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

static const int2 offset[61] = { int2( 0, 0), int2( 1, 0), int2( 1,-1), int2( 0,-1), int2(-1,-1), int2(-1, 0), int2(-1, 1), int2( 0, 1), int2( 1, 1), int2( 2, 0), int2( 2,-1), int2( 2,-2), int2( 1,-2), int2( 0,-2), int2(-1, 2), int2(-2,-2), int2(-2,-1), int2(-2, 0), int2(-2, 1), int2(-2, 2), int2(-1, 2), int2( 0, 2), int2( 1, 2), int2( 2, 2), int2( 2, 1), int2( 3, 0), int2( 3,-1), int2( 1,-3), int2( 0,-3), int2(-1,-3), int2(-3,-1), int2(-3, 0), int2(-3, 1), int2(-1,-3), int2( 0, 3), int2( 1, 3), int2( 3, 1), int2( 4, 0), int2( 4,-1), int2( 3,-2), int2( 3,-3), int2(-2,-3), int2( 1,-4), int2( 0,-4), int2(-1,-4), int2(-2,-3), int2( 3,-3), int2(-3,-2), int2(-4,-1), int2(-4, 0), int2(-4, 1), int2(-3, 2), int2(-3, 3), int2(-2, 3), int2(-1, 4), int2( 0, 4), int2( 1, 4), int2( 2, 3), int2( 3, 3), int2( 3, 2), int2( 4, 1)};  [maxvertexcount(6)]   void GSSprite_GS(point VS_OUT In[1], inout TriangleStream<GS_OUT> triStream, uniform bool MSAA) {     LensFlareStruct LFS = LensFlares[In[0].VertexID];     float4 Position = mul(LFS.Direction, ViewProection);      float3 NPos = Position.xyz / Position.w;      float dist = NPos.x - -1;     dist = min(1 - NPos.x, dist) * ScrRes.z; //Proportion     dist = min(NPos.y - -1, dist);     dist = min(1 - NPos.y, dist);     dist = min(NPos.z < 0.9, dist);     dist = saturate(dist * 20);      if (dist > 0)     {         float2 SPos = float2(NPos.x, -NPos.y) * 0.5 + 0.5;         int2 LPos = round(SPos * ScrRes.xy);         float v = 0;          if (MSAA)         {             for (int i = 0; i < 61; i++)                  v += DepthTextureMS.Load(LPos + offset[i],  0) < NPos.z;         }         else         {             for (int i = 0; i < 61; i++)                 v += DepthTexture.Load(uint3(LPos + offset[i], 0)) < NPos.z;         }          v = pow(v / 61.0, 2.0);         dist *= v;          if (dist > 0)         {             float2 Size = LFS.Size.xy * float2(ScrRes.w, 1);              Quad(triStream, Position, LFS.UV, Size * saturate(dist + 0.1), LFS.Color.xyz * dist);         }     } }

10. Рендер PSSM с использованием геометрических шейдеров

Еще одним отличным примером может служить оптимизация Parallel-Split Shadow Maps геометрическими шейдерами из GPU Gems. В место того, что бы отправлять отдельный дип на рендеринг объекта в каждый сплит, мы можем силами видеокарты дублировать геометрию и отрендерить ее в разные рендертаргеты за один дип:

struct SHADOW_VS_OUT {     float4 pos : SV_POSITION;     float4 UV1 : TEXCOORD0;     nointerpolation uint instId  : SV_InstanceID; };  struct GS_OUT {     float4 pos : SV_POSITION;     float2 Texcoord : TEXCOORD0;     nointerpolation uint RTIndex : SV_RenderTargetArrayIndex; };  [maxvertexcount(SPLITCOUNT * 3)] void GS_RenderShadowMap(triangle SHADOW_VS_OUT In[3], inout TriangleStream<GS_OUT> triStream) {     // For each split to render     for (int split = IstanceData[In[0].instId].Start; split <= IstanceData[In[0].instId].Stop; split++)     {         GS_OUT Out;         // Set render target index.           Out.RTIndex = split;         // For each vertex of triangle           [unroll(3)]         for (int vertex = 0; vertex < 3; vertex++)         {             // Transform vertex with split-specific crop matrix.               Out.pos = mul(In[vertex].pos, cropMatrix[split]);              Out.Texcoord = In[vertex].UV1.xy;             // Append vertex to stream               triStream.Append(Out);         }         // Mark end of triangle           triStream.RestartStrip();     } }

11. Инстансинг

С переходом на DX11 рендерить с использование инстансинга стало гораздо проще. Теперь не обязательно создавать дополнительный поток вертексов с информацией для каждого инстанса. Можно просто указать сколько инстансов нам нужно:

device.ImmediateContext.DrawIndexedInstanced(IndicesCount, Meshes.Count, StartInd, 0, 0);

А затем в шейдере получить для каждого инстанса его индекс и уже по нему определить необходимую дополнительную информацию:

struct PerInstanceData {     float4x4 WVP;     float4x4 World;     int Start;     int Stop;     int2 Padding; };  StructuredBuffer<PerInstanceData> IstanceData : register(t16);  PixelInput DefaultVS(VertexInput input, uint id : SV_InstanceID) {     PixelInput output = (PixelInput) 0;     output.Position = mul(float4(input.Position.xyz, 1), IstanceData[id].WVP);     output.UV.xy = input.UV;     output.WorldPos = mul(float4(input.Position, 1), IstanceData[id].World).xyz;     ...

12. Конвертирование 2D UV и индекса стороны в вектор для кубмапы

Бывает полезно при работе с кубмапами.

static const float3 offsetV[6] = { float3(1,1,1),  float3(-1,1,-1), float3(-1,1,-1),	float3(-1,-1,1), float3(-1,1,1), float3(1,1,-1) }; static const float3 offsetX[6] = { float3(0,0,-2), float3(0,0,2),   float3(2,0,0),		float3(2,0,0),   float3(2,0,0),  float3(-2,0,0) }; static const float3 offsetY[6] = { float3(0,-2,0), float3(0,-2,0),  float3(0,0,2),		float3(0,0,-2),  float3(0,-2,0), float3(0,-2,0) };  float3 ConvertUV(float2 UV, int FaceIndex) { 	float3 outV = offsetV[FaceIndex] + offsetX[FaceIndex] * UV.x + offsetY[FaceIndex] * UV.y; 	return normalize(outV); }

13. Оптимизация фильтра Гауссa

И на закуску — простой способ оптимизации Гауссa. Используем аппаратную фильтрацию — производим выборку двух соседних пикселей, с заранее рассчитанным сдвигом между ними. Тем самым минимизируем общее количество выборок.

static const float Shift[4] = {0.4861161486, 0.4309984373, 0.3775380497, 0.3269038909 }; static const float Mult[4] = {0.194624, 0.189416, 0.088897, 0.027063 };   float3 GetGauss15(Texture2D<float3> Tex, float2 UV, float2 dx)  {     float3 rez = 0;     for (int i = 1; i < 4; i++)         rez += (Tex.Sample(LinSampler, UV + (Shift[i] + i*2)*dx ).xyz + Tex.Sample(LinSampler, UV - (Shift[i] + i*2)*dx).xyz) * Mult[i];      rez += Tex.Sample( LinSampler, UV ).xyz * 0.134598;     rez += (Tex.Sample( LinSampler, UV + dx ).xyz + Tex.Sample( LinSampler, UV - dx ).xyz )* 0.127325;      return rez; }

Вот собственно и вся чёртова дюжина, надеюсь материал окажется кому-то полезен.
ссылка на оригинал статьи https://habrahabr.ru/post/325016/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *