Привет, Хабр!
Решил написать статью-заметку о небольших трюках, которые использую в своем скромном движке. Это скорее заметка самому-себе, и матёрые программисты лишь усмехнутся, но, думаю, новичкам она может пригодится.
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/
Добавить комментарий