リトルDirectXとHLSLのトリック

こんにちは、Habr! 控えめなエンジンで使用する小さなトリックに関する記事を書くことにしました。 これはむしろ私自身への注意事項であり、経験豊富なプログラマーはにやにや笑いますが、初心者には便利だと思います。



1. HLSLの行列



頂点シェーダーで、頂点の法線(接線、従法線)を回転する必要があり、4x4のワールドマトリックスがあるとします。 しかし、マトリックスに配線されたシフトは必要ありません。 次に、単純に行列を3x3に減らします。



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





ところで、3x3回転行列から逆行列を取得する必要があり、同時に直交である場合は、単純に転置します。



 float33 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);
      
      





ここでは、レンダリングする3つのピークを送信します。 また、シェーダーでは、たとえば、それらからフルスクリーンクワッドを構築できます。



 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.ピクセルシェーダーなしでレンダリングする



もう1つの便利な機能は、ピクセルシェーダーなしでレンダリングすることです。 これにより、場合によってはレンダリングの時間を大幅に最適化できます。 たとえば、深度を準備するとき、またはシャドウをレンダリングするとき。 パイプラインにピクセルシェーダーをインストールしません。



 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.アルファからカバレッジ



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-prepas段階でのみ行われます。 最終パスの後、MSAAバッファーを解決するだけで十分です。アルファテストは通常​​のジオメトリのように滑らかになります(正しい解像度のHDR MSAAバッファーは別の記事のトピックです)。



比較スクリーンショット






5.法線のアンチエイリアスをスクリーニングする



このアイデアは、前の段落の実装後に思いついたものです。 スクリーンスペースで計算されたUVオフセットを持つ通常のテクスチャーからスーパースキルを習得しています。 Z-prepasではForward +アプローチを使用しているため、このような操作は最小限です。



 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.レンズフレア



レンズ効果の描画にも同様のアプローチを使用します。 スプライトの構築の直前に行う可視性チェックのみ。 最初に、画面の端から効果がどれだけ離れているかを確認します。 次に、深度バッファ内のオブジェクトによってオーバーラップするエフェクトの割合をチェックします。 両方のチェックに合格したら、スプライトを作成します。



 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レンダリング



もう1つの優れた例は、GPU Gemsのジオメトリシェーダーを使用したParallel-Split Shadow Mapsの最適化です。 各分割でオブジェクトをレンダリングするために個別のディップを送信する代わりに、ビデオカードを使用してジオメトリを複製し、1つのディップで異なるレンダーターゲットにレンダリングできます。



 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とサイドインデックスをcubmapのベクトルに変換します



cubmapを使用する場合に便利です。



 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.ガウスフィルターの最適化



まず、ガウスを最適化する簡単な方法です。 ハードウェアフィルタリングを使用します-事前に計算されたシフトを使用して、隣接する2つのピクセルを選択します。 したがって、サンプルの総数を最小化します。



 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; }
      
      





これで全部ですよ。この資料が誰かに役立つことを願っています。



All Articles