ウィッチャー3のレンダリングの実装方法:雷、ウィッチャーの才能、その他の効果

画像






パート1.ジッパー



このパートでは、Witcher 3:Wild Huntで稲妻をレンダリングするプロセスを見ていきます。



雷のレンダリングは、 レインカーテンエフェクトよりも少し遅れて実行されますが、ダイレクトレンダリングパスでは引き続き発生します。 雷はこのビデオで見ることができます:





すぐに消えてしまうので、0.25の速度で動画を見るのが良いでしょう。



これらは静止画像ではないことがわかります。 時間が経つにつれて、明るさはわずかに変化します。



ニュアンスのレンダリングの観点からは、たとえば、同じ状態のミキシング(追加ミキシング)と深度(チェックが有効、深度記録は実行されない)など、遠くに雨のカーテンを描くことには多くの類似点があります。









雷のないシーン









雷シーン



稲妻の幾何学に関して、ウィッチャー3は木のようなメッシュです。 この稲妻の例は、次のメッシュで表されます。









UV座標と法線ベクトルがあります。 これらはすべて、頂点シェーダーステージで役立ちます。



頂点シェーダー



組み立てられた頂点シェーダーコードを見てみましょう。



vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret
      
      





頂点レインカーテンシェーダーには多くの類似点があるため、ここでは繰り返しません。 11〜18行目にある重要な違いを示します。



  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw
      
      





まず、cb1 [8] .xyzはカメラの位置であり、r2.xyzはワールド空間の位置です。つまり、ライン11はカメラからワールドの位置までのベクトルを計算します。 次に、行12〜15は長さ(worldPos-cameraPos)* 0.000001を計算します。



v2.xyzは、着信ジオメトリの法線ベクトルです。 行16は、間隔[0-1]から間隔[-1; 1]に拡張します。



次に、世界の最終位置が計算されます。



finalWorldPos = worldPos + length(worldPos-cameraPos)* 0.000001 * normalVector

この操作のHLSLコードスニペットは次のようになります。



  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output;
      
      





この操作により、メッシュの小さな「バースト」が発生します(法線ベクトルの方向)。 0.000001を他のいくつかの値に置き換えて実験しました。 結果は次のとおりです。









0.000002









0.000005









0.00001









0.000025



ピクセルシェーダー



さて、頂点シェーダーを理解したので、今度はピクセルシェーダーのアセンブラーコードに取り掛かります!



  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret
      
      





朗報:コードはそれほど長くありません。



悪いニュース:



  3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff)
      
      





...それは何ですか?



正直なところ、これがウィッチャー3シェーダーのアセンブラーコードの一部を見たのはこれが初めてではありません。 しかし、初めて彼に会ったとき、「これは一体何だ?」と思った。



同様のことが、他のTW3シェーダーにも見られます。 このフラグメントでの冒険については説明せず、答えは整数ノイズにあるとだけ言います



  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); }
      
      





ご覧のとおり、ピクセルシェーダーで2回呼び出されます。 このWebサイトのガイドを使用して、スムーズノイズが正しく実装されていることを理解できます。 これについては後で説明します。



行0を見てください。ここでは、次の式に基づいてアニメーション化しています。



アニメーション=経過時間* animationSpeed + TextureUV.x

これらの値は、将来下側に丸められた後( floor )(命令round_ni )、整数ノイズの入力ポイントになります。 通常、2つの整数のノイズ値を計算してから、それらの間の最終的な補間値を計算します(詳細については、libnoiseのWebサイトを参照してください)。



さて、これは整数ノイズですが、前述のすべての値(切り捨て)もすべて浮動小数点です!



ここにはftoiの指示がないことに注意してください 。 CD Projekt Redのプログラマーは、 ここでHLSL asint内部関数を使用したと仮定します 。これは、「 reinterpret_cast 」浮動小数点値の変換を実行し、整数パターンとして扱います。



2つの値の補間重みは、10〜11行目で計算されます。



InterpolationWeight = 1.0-frac(アニメーション);

このアプローチにより、時間の経過とともに値を補間できます。



滑らかなノイズを作成するには、この補間関数をSCurve関数に渡します。



  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; }
      
      











Smoothstep関数[libnoise.sourceforge.net]



この機能は「スムーズステップ」として知られています。 しかし、 アセンブラーコードからわかるように、これはHLSLの内部のsmoothstep関数ではありません 。 内部関数は、値が真になるように制限を適用します。 ただし、 interpolationWeightの範囲は常に[0-1]であることがわかっているため、これらのチェックは安全にスキップできます。



最終値を計算するとき、いくつかの乗算演算が使用されます。 最終的なアルファ出力がノイズ値に応じてどのように変化するかを確認してください。 これは、実際の生活と同じように、レンダリングされた稲妻の不透明度に影響するため便利です。



レディピクセルシェーダー:



  cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); }
      
      





まとめると



このパートでは、The Witcher 3で稲妻をレンダリングする方法について説明しました。



私のシェーダーから出たアセンブラーコードが元のコードと完全に一致することを非常に嬉しく思います!









パート2.愚かな空のトリック



この部分は、前の部分とわずかに異なります。 その中で、スカイシェーダーウィッチャー3のいくつかの側面を紹介したいと思います。



なぜシェーダー全体ではなく「愚かなトリック」なのか? さて、いくつかの理由があります。 まず、Witcher 3スカイシェーダーはかなり複雑な獣です。 2015バージョンのピクセルシェーダーには267行のアセンブラコードが含まれ、Blood and Wine DLCのシェーダーには385行が含まれています。



さらに、彼らは多くの入力を受け取りますが、これは完全な(そして読みやすい!)HLSLコードのリバースエンジニアリングにはあまり役立ちません。



したがって、これらのシェーダーからのトリックの一部のみを表示することにしました。 何か新しいものを見つけたら、その記事を補足します。



2015バージョンとDLC(2016)の違いは非常に顕著です。 特に、星とそのちらつきの計算の違い、太陽をレンダリングするための異なるアプローチが含まれています。ブラッドアンドワインシェーダーは、夜の天の川も計算します。



基本から始めて、愚かなトリックについて話しましょう。



基本



ほとんどの現代のゲームと同様に、ウィッチャー3はスカイドームを使用して空をモデル化します。 ウィッチャー3(2015)でこれに使用される半球を見てください。 注:この場合、このメッシュのバウンディングボックスは[0,0,0]から[1,1,1](Zは上向きの軸)の範囲にあり、UVがスムーズに分布しています。 後で使用します。









スカイドームの背後にある考え方は、 スカイボックスの考え方に似ています(唯一の違いは、使用されるメッシュです)。 頂点シェーダーステージでは、観測者に対してスカイドームを変換します(通常はカメラの位置に応じて)。これにより、空が実際には非常に遠くにあるという錯覚が生じます。



この一連の記事の前の部分を読むと、「The Witcher 3」は逆深度を使用していることがわかります。つまり、遠方の平面は0.0fで、最も近い平面は1.0fです。 スカイドーム出力を完全に遠方の平面で実行するために、ブラウズウィンドウのパラメーターでMinDepthをMaxDepthと同じ値に設定します。









参照ウィンドウの変換中にMinDepthおよびMaxDepthフィールドがどのように使用されるかについては、ここをクリック(docs.microsoft.com)してください。



頂点シェーダー



頂点シェーダーから始めましょう。 ウィッチャー3(2015)では、アセンブラーシェーダーコードは次のとおりです。



  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret
      
      





この場合、頂点シェーダーは、texcoordsとワールド空間内の位置のみを出力に転送します。 Blood and Wineでは 、正規化された法線ベクトルも表示します。 2015年版はよりシンプルなので、検討します。



cb2として指定された定数バッファを見てください:









ここに、世界のマトリックスがあります(100による均一なスケーリングとカメラの位置に相対的な転送)。 複雑なことは何もありません。 cb2_v4とcb2_v5は、頂点位置を間隔[0-1]から間隔[-1; 1]に変換するために使用されるスケール/偏差係数です。 しかし、ここでは、これらの係数はZ軸を「圧縮」します(上方向)。









シリーズの前のパートでは、同様の頂点シェーダーがありました。 一般的なアルゴリズムは、texcoordsをさらに転送し、スケール/偏差係数を考慮して位置を計算し、次にワールド空間でPositionWを計算し、 matWorldmatViewProjを乗算してクリッピング空間の最終位置を計算します。 。



したがって、この頂点シェーダーのHLSLは次のようになります。



  struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; }
      
      





私のシェーダー(左)とオリジナル(右)の比較:









RenderDocの優れた特性は、元のシェーダーの代わりに独自のシェーダーを注入できることです。これらの変更は、フレームの最後までパイプラインに影響します。 HLSLコードからわかるように、最終ジオメトリをズームおよび変換するためのオプションがいくつか用意されています。 それらを試して、非常に面白い結果を得ることができます:









頂点シェーダーの最適化



元の頂点シェーダーの問題に気づきましたか? 行列と行列の頂点乗算は完全に冗長です! これを少なくともいくつかの頂点シェーダーで見つけました(たとえば、シェーダーで遠くの雨カーテン )。 PositionWmatViewProjをすぐに掛けることで最適化できます!



したがって、このコードをHLSLに置き換えることができます。



  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) );
      
      





次のように:



  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW );
      
      





最適化されたバージョンは、次のアセンブリコードを提供します。



  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret
      
      





ご覧のとおり、命令の数を26から12に減らしました。これは非常に大きな変更です。 この問題がゲーム内でどれほど広範囲に広がっているのかわかりませんが、神のためにCD Projekt Redはパッチをリリースするかもしれません。 :)



そして、私は冗談ではありません。 元のRenderDocの代わりに最適化されたシェーダーを挿入すると、この最適化は視覚的には何にも影響しないことがわかります。 正直なところ、CD Projekt Redがマトリックスとマトリックスの頂点乗算を実行することにした理由がわかりません...



太陽



ウィッチャー3(2015)では、大気散乱と太陽の計算は2つの別個のレンダリング呼び出しで構成されています。









ウィッチャー3(2015)-まで









ウィッチャー3(2015)-空と









ウィッチャー3(2015)-with sky + Sun



2015年バージョンの太陽のレンダリングは、幾何学および混合/深度の状態の点でレンダリングに非常に似ています。



一方、 「血とワイン」では 、太陽のある空が1つのパスでレンダリングされます。









ウィッチャー3:血とワイン(2016)-天国へ









The Witcher 3:Blood and Wine(2016)-with Heaven and the Sun



太陽をどのようにレンダリングしても、ある段階では、日光の(正規化された)方向が必要です。 このベクトルを取得する最も論理的な方法は、 球面座標を使用することです。 実際、2つの角度(ラジアン!)を示す2つの値Phithetaのみが必要です。 それらを受け取ったので、 r = 1であると仮定して、それを減らすことができます。 次に、Y軸が上を向くデカルト座標の場合、HLSLで次のコードを記述できます。



  float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir);
      
      





通常、太陽光の方向はアプリケーションで計算され、その後の使用のために定数バッファーに渡されます。



日光の方向を受けて、ピクセルシェーダー「Blood and Wine」のアセンブラーコードを詳しく調べることができます...



  ... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ...
      
      





したがって、まず、 cb12 [0] .xyzはカメラの位置であり、 r0.xyzに頂点位置を格納します(これは頂点シェーダーからの出力です)。 したがって、行100はベクトルworldToCameraを計算します。 しかし、105行目から107行目を見てください。 それらをnormalize(-worldToCamera)として記述できます。つまり、正規化されたcameraToWorldベクトルを計算します。



  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx
      
      





次に、 cameraToWorldsunDirectionベクトルのスカラー積を計算します! 正規化する必要があることに注意してください。 また、この完全な表現を飽和させて、間隔[0-1]に制限します。



いいね! このスカラー積はr1.xに保存されます。 次に適用される場所を見てみましょう...



  152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x
      
      





三位一体の「log、mul、exp」は累乗です。 ご覧のとおり、コサイン(正規化されたベクトルのスカラー積)をある程度上げます。 理由を尋ねるかもしれません。 このようにして、太陽を模したグラデーションを作成できます。 (そして、行155はこのグラデーションの不透明度に影響するため、たとえば、太陽を完全に隠すようにリセットします)。 以下に例を示します。









指数= 54









指数= 2400



このグラデーションを使用して、 skyColorsunColorを補間するために使用します! アーチファクトを回避するには、120行目の値を飽和させる必要があります。



このトリック月のをシミュレートするために使用できることに注意してください(低い指数値で)。 これを行うには、 moonDirectionベクトルが必要です。これは、球面座標を使用して簡単に計算できます。



既製のHLSLコードは、次のスニペットのように聞こえます。



  float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient );
      
      





星の動き



ウィッチャー3の晴れた夜空を微速度撮影すると、星は静的ではなく、空を少し横切っていることがわかります。 私はこれをほとんど偶然に気づき、それがどのように実装されたかを知りたいと思いました。



ウィッチャー3の星は、サイズ1024x1024x6の立方体マップとして表示されるという事実から始めましょう。 考えてみると、これは非常に便利なソリューションであり、キュービックマップのサンプリングの方向を簡単にスナップできることを理解できます。



次のアセンブリコードを見てみましょう。



  159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0
      
      





最終的なサンプリングベクトル(行173)を計算するには、まず、正規化されたworldToCameraベクトル(行159-162)を計算します。



次に、 moonDirectionを使用して2つのベクトル積( 163-164、165-166 )を計算し、後で3つのスカラー積を計算して最終的なサンプリングベクトルを取得します。 HLSLコード:



  float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb;
      
      





自分自身への注意:これは非常によく設計されたコードであり、さらに詳しく調査する必要があります。



読者への注意:この操作について詳しく知っているなら、教えてください!



きらめく星



もう1つ興味深いトリックは、星のちらつきです。 たとえば、晴天でノヴィグラードをさまよう場合、星がきらめくことに気付くでしょう。



これがどのように実装されているのか興味がありました。2015年版と「Blood and Wine」の違い非常に大きいことが判明しました簡単にするために、2015バージョンを検討します。



したがって、前のセクションのstarsColorサンプリングした直後に開始します。



  174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx
      
      





うんこのかなり長いアセンブリコードの最後を見てみましょう。173行目でstarsColor



サンプリングした後、何らかのオフセット値を計算しますこのオフセットは、サンプリングの最初の方向(r2.xyz、行235)を歪めるために使用されます。次に、星の3次マップをサンプリングし、これら2つの値のガンマ補正を実行し(237-242)、乗算します(243)。シンプルでしょ?



まあ、そうでもない。このオフセットについて少し考えてみましょう。この値は、スカイドーム全体で異なる必要があります-均等に点滅する星は非常に非現実的に見えます。オフセットを極力変化させた、我々はバッファ(CB [0] .X)に格納された経過時間定数をUVはスカイドーム(v0.xy)に延伸するという事実を使用して適用します。これらの恐ろしいishr / xor /に慣れていない場合は、稲妻効果については整数ノイズについて読んでください。ご覧のとおり、整数ノイズはここでは4回発生していますが、雷に使用されるノイズとは異なります。結果をさらにランダムにするために、ノイズの入力整数は合計(iadd











)そして、それによってビットが反転されます(内部関数reversebits ; bfrev命令)。



だから、今は遅くなります。最初から始めましょう。



整数ノイズの4つの「反復」があります。アセンブラコードを分析しました。4つの反復すべての計算は次のようになります。



  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B );
      
      





4つの反復すべての最終出力(それらを見つけるには、itof命令に従います):



反復1-r5.x、



反復2-r4.w、



反復3-r1.w、



反復4-r5.y



最後のitofの(行216) )私たちが持っている:



  217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z
      
      





これらの線は、雷の場合と同様に、UVの小数部に基づいて天びんのSカーブの値を計算します。 だから:



  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY );
      
      





ご想像のとおり、これらの係数は、ノイズをスムーズに補間し、サンプリング座標の最終オフセットを生成するために使用されます。



  229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb;
      
      





計算されたオフセットの小さな視覚化は次のとおりです。





starsColorDisturbedを計算する、最も難しい部分が完成します。やった!



次のステップでは、starsColorstarsColorDisturbedの両方に対してガンマ補正を実行し、その後乗算します。



  starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed;
      
      





スター-最後の仕上げ



我々は持っているstarsFinalを r1.xyzに。スター処理の終了時に、次のことが発生します。



  256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000)
      
      





これは、きらめく星や動く星に比べてはるかに簡単です。



したがって、starsFinalを2.5の累乗上げることから始めます。これにより、星の密度を制御できます。かなり賢い。次に、星の最大色をfloat3(1、1、1)に等しくします。



cb0 [9] .wは、星の全体的な可視性を制御するために使用されます。したがって、日中はこの値が1.0(ゼロによる乗算を与える)であり、夜間は0.0であると予想できます。



最終的に、星の可視性を10増やします。これで終わりです。



パート3.ウィッチャーフレア(オブジェクトと輝度マップ)



前述のほとんどすべてのエフェクトとテクニックは、実際にはウィッチャー3とは関係ありませんでした。トーン補正、ケラレ、平均輝度の計算などは、ほとんどすべての現代のゲームに存在します。中毒の影響さえ広まって​​います。



だからこそ、「魔女の本能」のレンダリングの仕組みを詳しく見てみることにしました。ジェラルトは魔女であり、したがって彼の感情は普通の人よりもずっと鋭い。その結果、彼は他の人よりも多くのことを見ることができ、彼の調査に大いに役立ちます。ウィッチャーの才能のメカニズムにより、プレイヤーはそのような痕跡を視覚化することができます。



効果のデモは次のとおりです。





そしてもう一つ、より良い照明で:





ご覧のとおり、2種類のオブジェクトがあります。Geraltが対話できるオブジェクト(黄色のアウトライン)と、調査に関連するトレース(赤いアウトライン)です。Geraltが赤いトレイルを調べた後、黄色になることがあります(最初のビデオ)。画面全体が灰色に変わり、魚眼効果(2番目のビデオ)が追加されていることに注意してください。



この効果は非常に複雑なので、私は彼の研究を3つの部分に分けることにしました。



最初はオブジェクトの選択について、2番目は輪郭の生成について、3番目はこれらすべてを1つの全体に統合することについて説明します。



オブジェクト選択



先ほど言ったように、オブジェクトには2つのタイプがあり、それらを区別する必要があります。ウィッチャー3では、これはステンシルバッファーを使用して実装されます。「トレース」(赤)としてマークされるべきメッシュGBufferを生成するとき、それらは、ステンシル= 8メシでレンダリングされる「興味深い」オブジェクトはステンシル= 4でレンダリングされているように、黄色でマーク



たとえば、次の2つのテクスチャが有するフレームの例を示します見えるウィッチャー本能と対応するステンシルバッファー:















ステンシルバッファを短時間



ステンシルバッファは、ゲームでメッシュにタグを付けるためによく使用されます。特定のメッシュカテゴリには同じIDが割り当てられます。ステンシルテストが成功した場合はReplace演算子でAlways



関数を使用し、それ以外の場合Keep演算子常に使用するという考え方ですD3D11を使用して実装する方法は次のとおりです。







  D3D11_DEPTH_STENCIL_DESC depthstencilState; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue );
      
      





バッファに書き込まれるステンシル値は、API呼び出しでStencilRefとして渡されます。



  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... );
      
      





レンダリングの明るさ



この節では、実装の観点から、R11G11B10_FLOAT形式のフルスクリーンテクスチャが1つあり、興味深いオブジェクトとトレースがチャネルRとGに格納されます。



なぜ明るさの点でこれが必要なのですか?Geraltの本能の半径は限られているため、オブジェクトがプレーヤーに十分近い場合にのみオブジェクトがアウトラインを取得します。



この側面を実際に見てください:











輝度テクスチャをクリーニングし、黒で塗りつぶすことから始めます。



次に、2つの全画面描画呼び出しが行われます。1つ目は「トレース」用、2つ目は興味深いオブジェクト用です:









最初の描画呼び出しはトレースに対して行われます-緑のチャネル:









2番目の呼び出しは、興味深いオブジェクト(赤いチャネル)に対して行われます。









さて、しかし、どのピクセルを考慮するかをどのように決定するのでしょうか?ステンシルバッファを使用する必要があります。



これらの呼び出しのそれぞれについて、ステンシルテストが実行され、以前に「8」(最初の描画呼び出し)または「4」としてマークされたピクセルのみが受け入れられます。



トレース用のステンシルテストの視覚化:









...および興味深いオブジェクトの場合:









この場合、テストはどのように実行されますか?ステンシルテストの基本は良い投稿で見つけることができます一般に、ステンシルテスト式の形式は次のとおりです。



  if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel
      
      





ここで:

StencilRef -値がAPIを呼び出しに渡さ、



StencilReadMaskは -ステンシルが読み取りに使用される値(それは左側に、右側に存在していることに注意)をマスク、



OP -比較演算子は、APIを介して指定され、



StencilValue -値は、現在のバッファのステンシル処理されたピクセル。



バイナリANDを使用してオペランドを計算することを理解することが重要です。



基本を理解したら、これらの描画呼び出しでこれらのパラメーターがどのように使用されるかを見てみましょう。









トレースのステンシル条件









興味深いオブジェクトのステンシル状態



ハ!ご覧のとおり、唯一の違いはReadMaskです。それをチェックしてみましょう!ステンシルテスト式でこれらの値を代入します。



  Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE
      
      





賢い。ご覧のとおり、この場合、ステンシル値を比較するのではなく、ステンシルバッファーの特定のビットが設定されているかどうかを確認します。ステンシルバッファの各ピクセルはuint8形式であるため、値の間隔は[0-255]です。



注:すべてのDrawIndexed(36)呼び出しは、トレースとしてのフットプリントのレンダリングに関連するため、この特定のフレームでは、輝度マップは次の最終形式になります。









しかし、ステンシルテストの前にピクセルシェーダーがあります。28738と28748は両方とも同じピクセルシェーダーを使用します。



  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret
      
      





このピクセルシェーダーは1つのレンダーターゲットのみに書き込むため、24〜27行目は冗長です。



ここで最初に発生するのは、深さのサンプリング(値制限付きのポイントサンプラーを使用)、1行目です。この値は、特殊なマトリックスを乗算し、その後に遠近分割を行うことで世界の位置を再作成します(2-6行目)



Geraltの位置を取得します(cb3 [7] .xyz-これはカメラ位置でないことに注意してください!)、Geraltからこの特定のポイントまでの距離を計算します(7〜9行目)。



このシェーダーでは、次の入力が重要です。



-cb3 [0] .rgb-出力色。形式は、float3(0、1、0)(トレース)またはfloat3(1、0、0)(対象オブジェクト)です。

-cb3 [6] .y-距離スケーリング係数。最終出力の半径と輝度に直接影響します。



後で、Geraltとオブジェクトの間の距離に応じて明るさを計算するためのかなりトリッキーな公式があります。すべての係数が実験的に選択されていると仮定できます。



最終的な出力は、color * strengthです。



HLSLコードは次のようになります。



  struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; }
      
      





オリジナル(左)と私の(右)アセンブラーシェーダーコードの小さな比較。









これは魔女の才能効果最初の段階でした実際、これは最も単純です。



パート4.ウィッチャーフレア(概要マップ)



もう一度、私たちが探索しているシーンを見てみましょう。









魔女の本能の影響の分析の最初の部分では、「明るさマップ」がどのように生成されるかを示しました。



R11G11B10_FLOAT形式のフルスクリーンテクスチャが1つあり、次のようになります。









緑色のチャネルは「トレース」、赤色のチャネルはGeraltが対話できる興味深いオブジェクトを示します。



このテクスチャを受け取ったら、次の段階に進むことができます。これを「コンターマップ」と呼びます。









これは512x512 R16G16_FLOAT形式のちょっと変わったテクスチャです。「ピンポン」のスタイルで実装することが重要です。前のフレームからの等高線マップは、現在のフレームに新しい等高線マップを生成するための入力データです(輝度マップとともに)。



ピンポンバッファーはさまざまな方法で実装できますが、個人的には次の(擬似コード)が一番好きです。



  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; }
      
      





入力が常に[m_outlineIndex]で、出力が常に[!M_outlineIndex]であるこのアプローチは、追加のポストエフェクトの使用に柔軟性を提供します。



ピクセルシェーダーを見てみましょう。



  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret
      
      





ご覧のとおり、出力コンターマップは4つの等しい正方形に分割されており、これが最初に調査する必要があるものです。



  0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0)
      
      





まず、フロア(TextureUV * 2.0)を計算することから始めます。これにより、次のことがわかります。









個々の正方形を決定するには、小さな関数が使用されます。



  float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; }
      
      





関数は、入力float2(0.0、0.0)に対して1.0を返すことに注意してください。



このケースは左上隅で発生します。右上隅で同じ状況を得るには、丸みを帯びたtexcoordsからfloat2(1、0)を引き、緑の正方形の場合はfloat2(0、1)を引き、黄色の正方形の場合はfloat2(1.0、1.0)を引きます。



だから:



  float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 );
      
      





マスクコンポーネントは0または1であり、テクスチャの1つの正方形を担当します。たとえば、mask.rおよびmask.w









mask.r









mask.w



我々が得たマスクを上の動きをみましょう、。行15は、輝度マップをサンプリングします。すべてのrgbaコンポーネントをサンプリングしますが、輝度テクスチャはR11G11B10_FLOAT形式であることに注意してください。この状況では、.aは1.0fであると想定されています。



この操作に使用されるTexcoordsは、frac(TextureUV * 2.0)として計算できますしたがって、この操作の結果は、たとえば次のようになります。









類似を参照してください?



次の段階は非常に巧妙です-4成分のスカラー積(dp4)が実行されます。



  16: dp4 r1.x, r1.xyzw, r2.xyzw
      
      





したがって、左上隅には赤のチャネル(つまり、興味深いオブジェクトのみ)が残り、右上には緑のチャネル(トレースのみ)、右下にはすべてが含まれます(輝度コンポーネント.wには間接的に値1.0が割り当てられているため)。素晴らしいアイデア。スカラー積の結果は次のようになります。









このmasterFilterを受け取ったら、オブジェクトの輪郭を定義する準備ができました。見た目ほど難しくありません。このアルゴリズムは、シャープネスを取得するために使用されるものと非常に似ています-値の最大絶対差を取得する必要があります。



処理内容は次のとおりです。現在処理中のテクセルの横にある4つのテクセルをサンプリングし(重要:この場合、テクセルサイズは1.0 / 256.0です!)、赤と緑のチャネルの最大絶対差を計算します。



  float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference);
      
      





さて、フィルターmaxAbsDifferenceを掛けると...









非常にシンプルで効率的。



輪郭を受け取ったら、前のフレームから輪郭マップをサンプリングします。



次に、「ゴースト」効果を得るために、現在のパスで計算されたパラメーターの一部と等高線マップの値を使用します。



私たちの古い友人に挨拶してください-整数ノイズ。彼はここにいます。アニメーションパラメータ(cb3 [0] .zw)は定数バッファから取得され、時間とともに変化します。



  float2 outlines = masterFilter * maxAbsDifference; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y
      
      





注:魔女の才能を自分で実装したい場合は、整数ノイズを間隔[-1; 1]に制限することをお勧めします(そのWebサイトで述べています)。元のTW3シェーダーには制限はありませんでしたが、それなしではひどいアーティファクトが発生し、アウトラインマップ全体が不安定でした。



次に、輝度マップと同じ方法で等高線マップをサンプリングし(今回はテクセルのサイズは1.0 / 512.0です)、. xコンポーネントの平均値を計算します。



  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0;
      
      





次に、アセンブラコードによって判断して、この特定のピクセルの平均値と値の差が計算され、その後整数ノイズによる歪みが実行されます。



  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0;
      
      





次のステップは、ノイズを使用して「古い」輪郭マップの値を歪めることです。これは、出力テクスチャにブロック感を与えるメインラインです。



次に、他の計算があり、その後、最後に「減衰」が計算されます。



  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0);
      
      





動作中のアウトラインマップを示す短いビデオを次に示します。





フルピクセルシェーダーに興味がある場合は、こちらから入手できますシェーダーはRenderDocと互換性があります。



Witcher 3の元のシェーダーとアセンブラーコードが同一であるにもかかわらず、RenderDocの等高線マップの最終的な外観が変化しているのは興味深いことです(正直なところ、少し面倒です)。



注:最後のパス(次の部分を参照)では、等高線マップの.rチャネルのみが使用されていることがわかります。では、なぜ.gチャンネルが必要なのですか?これは1つのテクスチャ内のある種の「ピンポン」バッファーだと思います。.rには.gチャンネル+新しい値が含まれていることに注意してください。



パート5:ウィッチャーフレア(フィッシュアイと最終結果)



すでに持っているものを簡単にリストしましょう。最初の部分では、ウィッチャーの本能に捧げられ、効果が距離に応じてどの程度顕著であるかを示すフルスクリーンの輝度マップが生成されました。2番目のパートでは、アウトラインマップをより詳細に調査しました。これは、完成したエフェクトのアウトラインとアニメーションを担当します。



最後の段階に来ました。これらすべてを組み合わせる必要があります!最後のパスはフルスクリーンクワッドです。入力データ:カラーバッファー、等高線マップ、および輝度マップ。



宛先:











後:









もう一度、エフェクトを適用したビデオを表示します。





ご覧のとおり、Geraltが見ることも聞くこともできるオブジェクトに輪郭を適用することに加えて、魚眼効果は画面全体に適用され、画面全体(特に角)は灰色になり、本物のモンスターハンターの感覚を伝えます。



完全に組み立てられたピクセルシェーダーコード:



  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret
      
      





82行-したがって、やるべきことがたくさんあります!



まず、入力データを見てみましょう。



  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x );
      
      





効果の大きさの主な値はfisheyeAmountです。Geraltが本能を使い始めると、徐々に0.0から1.0に上昇すると思います。残りの値はほとんど変わりませんが、ユーザーがオプションで魚眼効果を無効にした場合、それらの一部が異なると思われます(これはチェックしませんでした)。



ここで最初に起こることは、シェーダーがグレーの角度の原因となるマスクを計算することです。



  0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000)
      
      





HLSLでは、次のように記述できます。



  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); }
      
      





まず、間隔[-1; 1] UVとその絶対値。次に、トリッキーな「スクイーズ」があります。完成したマスクは次のとおりです。









後でこのマスクに戻ります。



ここで、数行のコードを意図的にスキップし、ズーム効果の原因となるコードを詳しく調べます。



  22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw
      
      





最初に、「2倍」のテクスチャ座標が計算され、減算float2(1、1)が実行されます。



  float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0);
      
      





このようなtexcoordは、次のように視覚化できます。









次に、スカラー積ドット(uv4、uv4)が計算され、マスクが得られます。









上記のtexcoordsを乗算するために使用されます。









重要:左上隅(黒いピクセル)の値は負です。R11G11B10_FLOAT形式の精度が制限されているため、これらは黒(0.0)で表示されます。符号ビットがないため、負の値を格納できません。



次に、減衰係数が計算されます(上記で述べたように、fisheyeAmountは0.0から1.0まで変化します)。



  float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation;
      
      





次に、制限(最大/最小)と1つの乗算が実行されます。



このようにして、オフセットが計算されます。カラーテクスチャのサンプリングに使用される最終的なUVを計算するには、単純に減算を実行します



。float2 colorUV = mainUv-offset;



入力colorUVカラーテクスチャをサンプリングすることにより、角の近くにゆがんだ画像が得られます。









アウトライン



次のステップは、等高線マップをサンプリングして等高線を見つけることです。それは非常に簡単です。まず、興味深いオブジェクトの輪郭をサンプリングするテックスコードを見つけ、次にトラックに対して同じことを行います。



  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y
      
      











等高線マップからの興味深いオブジェクト









等高線マップからのトレース等高線マップ



から.xチャネルのみをサンプリングし、上部の正方形のみを考慮することに注意する価値があります。



ムーブメント



トラックの移動を実装するには、中毒の効果とほぼ同じトリックが使用されます。単位サイズの円が追加され、興味深いオブジェクトとトレース、およびカラーテクスチャのアウトラインマップの8倍をサンプリングします。



見つかった輪郭を8.0で割っただけであることに注意してください。



テクスチャ座標[0-1] 2の空間にいるので、単一ピクセルを囲む半径1の円が存在すると、許容できないアーチファクトが作成されます。









先に進む前に、この半径がどのように計算されるかを調べましょう。これを行うには、不足している行15〜21に戻る必要があります。この半径の計算に関する小さな問題は、その計算がシェーダーの周りに散在していることです(おそらく、コンパイラーによるシェーダーの最適化が原因です)。したがって、ここに最初の部分(15-21)と2番目の部分(41-42)があります。



  15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000)
      
      





ご覧のとおり、各サーフェスの横にある[0.00-0.03]のテクセルのみを考慮し、それらの値を集計し、20を乗算して飽和させます。15〜21行目以降は次のようになります。









そして、41行目以降の方法は次のとおりです。









42行目では、これに0.03を掛けています。この値は、画面全体の円の半径です。ご覧のとおり、画面の端に近づくと半径が小さくなります。



これで、移動の原因となるアセンブラーコードを確認できます。



  40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop
      
      





ここで少し停止しましょう。行40で、時間係数を取得します(just 経過時間* 0.1)。 43行目には、ループ内で取得したカラーテクスチャ用のバッファーがあります。



r0.x(行41-42)は、現在わかっているように、円の半径です。r4.x(44行目)は対象オブジェクトのアウトライン、r4.y(45行目)はトラックのアウトライン(以前は8で割った!)、r4.z(46行目)はループカウンターです。



ご想像のとおり、ループには8つの反復があります。ラジアン単位の角度i * PI_4を計算することから始めます。これにより、2 * PI-完全な円が得られます。角度は時間とともに歪んでいきます。



sincosを使用して、サンプリングポイント(単位円)を決定し、乗算を使用して半径を変更します(行54)。



その後、ピクセルを円で囲み、輪郭と色をサンプリングします。サイクルの後、輪郭と色の平均値(8で割ったため)を取得します。



  float timeParam = time * 0.1; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; }
      
      





カラーサンプリングもほぼ同じ方法で実行されますが、ベースのcolorUVに「単一」の円乗算したオフセットを追加します。



明るさ



サイクルの後、輝度マップをサンプリングし、最終的な輝度値を変更します(輝度マップは輪郭について何も知らないため):



  67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx
      
      





HLSLコード:



  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces );
      
      





灰色のコーナーとすべての最終的な統一



角に近い灰色は、スカラー積(アセンブラーライン69)を使用して計算されます。



  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx;
      
      











その後、2つの補間が続きます。最初の例では、最初に説明したマスクを使用してグレーと「円の色」を組み合わせているため、角がグレーになります。さらに、0.6の係数があり、最終画像の彩度が低下します。









2番目はfisheyeAmountを使用して最初の色と上記の色を組み合わせます。これは、画面が徐々に暗くなり(0.6倍)、隅が灰色になることを意味します!独創的。



HLSL:



  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount );
      
      





これで、オブジェクトの輪郭の追加に進むことができます。



色(赤と黄色)は、定数バッファーから取得されます。



  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting;
      
      











ふう!もうすぐゴールです!



最終的な色があります。魔女の本能の色があります。



このため、単純な追加は適切ではありません。まず、スカラー積を計算します。



  78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) );
      
      





次のようになります。









そして、最後のこれらの値は、色と(飽和した)魔女の才能の間を補間するために使用されます:



  76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 );
      
      











そしてそれだけです。



完全なシェーダーはこちらから入手できます



私(左)とオリジナル(右)のシェーダーの比較:









この記事をお楽しみください!「ウィッチャー本能」の仕組みには多くの素晴らしいアイデアがあり、完成した結果は非常に信じられます。



[分析の前の部分:最初2番目。]



All Articles