Rekayasa terbalik dari rendering The Witcher 3

Bagian pertama dari terjemahan ada di sini . Pada bagian ini, kita akan berbicara tentang efek ketajaman, kecerahan rata-rata, fase bulan dan fenomena atmosfer selama hujan.

Bagian 6. Mempertajam


Pada bagian ini, kita akan melihat lebih dekat efek post-processing lain dari The Witcher 3 - Sharpen.

Mengasah membuat gambar output sedikit lebih tajam. Efek ini diketahui oleh kami dari Photoshop dan editor grafis lainnya.

Dalam The Witcher 3, penajaman memiliki dua opsi: rendah dan tinggi. Saya akan berbicara tentang perbedaan di antara mereka di bawah ini, tetapi untuk sekarang, mari kita lihat screenshotnya:

gambar

Opsi "Rendah" - hingga

gambar

Opsi "Rendah" - setelah


Opsi tinggi - hingga


Opsi "Tinggi" - setelah

Jika Anda ingin melihat perbandingan yang lebih rinci (interaktif), lihat bagian dalam Panduan Kinerja The Witcher 3 Nvidia . Seperti yang Anda lihat, efeknya terutama terlihat pada rumput dan dedaunan.

Di bagian posting ini, kita akan mempelajari bingkai dari awal permainan: Saya memilihnya dengan sengaja, karena di sini kita melihat relief (jarak jarak jauh) dan kubah langit.


Dalam hal input, mengasah memerlukan buffer warna t0 (LDR setelah koreksi nada dan suar lensa) dan buffer kedalaman t1 .

Mari kita periksa kode assembler untuk pixel shader:

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb3[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_input_ps_siv v0.xy, position
dcl_output o0.xyzw
dcl_temps 7
0: ftoi r0.xy, v0.xyxx
1: mov r0.zw, l(0, 0, 0, 0)
2: ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t1.xyzw
3: mad r0.x, r0.x, cb12[22].x, cb12[22].y
4: mad r0.y, r0.x, cb12[21].x, cb12[21].y
5: max r0.y, r0.y, l(0.000100)
6: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
7: mad_sat r0.y, r0.y, cb3[1].z, cb3[1].w
8: add r0.z, -cb3[1].x, cb3[1].y
9: mad r0.y, r0.y, r0.z, cb3[1].x
10: add r0.y, r0.y, l(1.000000)
11: ge r0.x, r0.x, l(1.000000)
12: movc r0.x, r0.x, l(0), l(1.000000)
13: mul r0.z, r0.x, r0.y
14: round_z r1.xy, v0.xyxx
15: add r1.xy, r1.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)
16: div r1.xy, r1.xyxx, cb3[0].zwzz
17: sample_l(texture2d)(float,float,float,float) r2.xyz, r1.xyxx, t0.xyzw, s0, l(0)
18: lt r0.z, l(0), r0.z
19: if_nz r0.z
20: div r3.xy, l(0.500000, 0.500000, 0.000000, 0.000000), cb3[0].zwzz
21: add r0.zw, r1.xxxy, -r3.xxxy
22: sample_l(texture2d)(float,float,float,float) r4.xyz, r0.zwzz, t0.xyzw, s0, l(0)
23: mov r3.zw, -r3.xxxy
24: add r5.xyzw, r1.xyxy, r3.zyxw
25: sample_l(texture2d)(float,float,float,float) r6.xyz, r5.xyxx, t0.xyzw, s0, l(0)
26: add r4.xyz, r4.xyzx, r6.xyzx
27: sample_l(texture2d)(float,float,float,float) r5.xyz, r5.zwzz, t0.xyzw, s0, l(0)
28: add r4.xyz, r4.xyzx, r5.xyzx
29: add r0.zw, r1.xxxy, r3.xxxy
30: sample_l(texture2d)(float,float,float,float) r1.xyz, r0.zwzz, t0.xyzw, s0, l(0)
31: add r1.xyz, r1.xyzx, r4.xyzx
32: mul r3.xyz, r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000)
33: mad r1.xyz, -r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000), r2.xyzx
34: max r0.z, abs(r1.z), abs(r1.y)
35: max r0.z, r0.z, abs(r1.x)
36: mad_sat r0.z, r0.z, cb3[2].x, cb3[2].y
37: mad r0.x, r0.y, r0.x, l(-1.000000)
38: mad r0.x, r0.z, r0.x, l(1.000000)
39: dp3 r0.y, l(0.212600, 0.715200, 0.072200, 0.000000), r2.xyzx
40: dp3 r0.z, l(0.212600, 0.715200, 0.072200, 0.000000), r3.xyzx
41: max r0.w, r0.y, l(0.000100)
42: div r1.xyz, r2.xyzx, r0.wwww
43: add r0.y, -r0.z, r0.y
44: mad r0.x, r0.x, r0.y, r0.z
45: max r0.x, r0.x, l(0)
46: mul r2.xyz, r0.xxxx, r1.xyzx
47: endif
48: mov o0.xyz, r2.xyzx
49: mov o0.w, l(1.000000)
50: ret


50 baris kode assembler terlihat seperti tugas yang layak. Mari kita mulai mengatasinya.

Pertajam Generasi Nilai


Langkah pertama adalah memuat buffer kedalaman (baris 1). Perlu dicatat bahwa "The Witcher 3" menggunakan kedalaman terbalik (1,0 - tutup, 0,0 - jauh). Seperti yang Anda ketahui, kedalaman perangkat keras terikat secara nonlinier (lihat artikel ini untuk detailnya ).

Baris 3-6 menyediakan cara yang sangat menarik untuk mengaitkan kedalaman perangkat keras ini [1,0 - 0,0] dengan nilai-nilai [hampir-jauh] (kami atur pada tahap MatrixPerspectiveFov). Pertimbangkan nilai-nilai dari buffer konstan:


Memiliki nilai "tutup" 0,2 dan nilai "jauh" 5000, kita dapat menghitung nilai cb12_v21.xy sebagai berikut:

cb12_v21.y = 1.0 / near
cb12_v21.x = - (1.0 / near) + (1.0 / near) * (near / far)


Sepotong kode ini cukup umum di TW3 shader, jadi saya pikir ini hanya sebuah fungsi.

Setelah mendapatkan "kedalaman piramida visibilitas", baris 7 menggunakan skala / distorsi untuk membuat koefisien interpolasi (di sini kami menggunakan saturate untuk membatasi nilai pada interval [0-1]).


cb3_v1.xy dan cb3_v2.xy - ini adalah kecerahan dari efek penajaman pada jarak pendek dan panjang. Sebut mereka menajamkan, Dekat, dan menajam. Dan ini adalah satu-satunya perbedaan antara opsi "Rendah" dan "Tinggi" dari efek ini di The Witcher 3.

Sekarang saatnya menggunakan rasio yang dihasilkan. Baris 8-9 hanya melakukan lerp(sharpenNear, sharpenFar, interpolationCoeff) . Untuk apa ini? Berkat ini, kami mendapatkan kecerahan berbeda di dekat Geralt dan menjauh darinya. Lihatlah:


Mungkin ini hampir tidak terlihat, tapi di sini kita diinterpolasi berdasarkan jarak mempertajam kecerahan di sebelah pemain (2.177151) dan efek kecerahannya sangat jauh (1.91303). Setelah perhitungan ini, kami menambahkan 1,0 ke kecerahan (baris 10). Mengapa ini dibutuhkan? Misalkan operasi lerp yang ditunjukkan di atas memberi kami 0,0. Setelah menambahkan 1,0, kami secara alami mendapatkan 1,0, dan ini adalah nilai yang tidak akan memengaruhi piksel saat melakukan penajaman. Baca lebih lanjut tentang ini di bawah ini.

Saat mengasah, kami tidak ingin memengaruhi langit. Ini dapat dicapai dengan menambahkan pemeriksaan bersyarat sederhana:

// sharpen
float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;


Dalam The Witcher 3, nilai kedalaman piksel langit adalah 1,0, jadi kami menggunakannya untuk mendapatkan semacam "filter biner" (fakta yang menarik: dalam hal ini langkah tidak akan bekerja dengan benar).

Sekarang kita dapat melipatgandakan kecerahan yang diinterpolasi dengan "filter langit":


Perkalian ini dilakukan pada baris 13.

Contoh kode shader:

// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;


Pixel Sampling Center


SV_Position memiliki aspek yang penting di sini: offset setengah piksel . Ternyata piksel ini di sudut kiri atas (0, 0) tidak memiliki koordinat (0, 0) dalam hal SV_Position.xy, tetapi (0,5, 0,5). Wow!

Di sini kita ingin mengambil sampel di tengah pixel, jadi mari kita lihat garis 14-16. Anda dapat menulisnya di HLSL:

// .
// "" SV_Position.xy.
float2 uvCenter = trunc( Input.Position.xy );

// ,
uvCenter += float2(0.5, 0.5);
uvCenter /= g_Viewport.xy


Dan kemudian, kami mencicipi tekstur warna input dari texcoords "uvCenter". Jangan khawatir, hasil pengambilan sampel akan sama dengan metode "normal" (SV_Position.xy / ViewportSize.xy).

Menajam atau tidak menajam


Keputusan untuk menggunakan mempertajam tergantung pada fSharpenAmount.

//
float3 colorCenter = TexColorBuffer.SampleLevel( samplerLinearClamp, uvCenter, 0 ).rgb;

//
float3 finalColor = colorCenter;

if ( fSharpenAmount > 0 )
{
// sharpening...
}

return float4( finalColor, 1 );


Pertajam


Sekarang saatnya untuk melihat bagian dalam algoritma itu sendiri.

Pada dasarnya, ia melakukan tindakan berikut:

- sampel empat kali tekstur warna input di sudut-sudut piksel,

- menambah sampel dan menghitung nilai rata-rata,

- menghitung perbedaan antara "center" dan "cornerAverage",

- Menemukan komponen absolut maksimum perbedaan,

- mengoreksi maks. abs komponen menggunakan skala + nilai bias,

- Menentukan besarnya efek menggunakan maks. abs komponen

- menghitung nilai kecerahan (luma) untuk "centerColor" dan "averageColor",

- Membagi colorCenter menjadi luma-nya,

- Menghitung nilai luma baru yang diinterpolasi berdasarkan besarnya efek,

- Mengalikan colorCenter dengan nilai luma baru.

Banyak pekerjaan, dan sulit bagi saya untuk mengetahuinya, karena saya belum pernah bereksperimen dengan mempertajam filter.

Mari kita mulai dengan pola pengambilan sampel. Seperti yang dapat Anda lihat dalam kode assembler, dilakukan empat pembacaan tekstur.

Ini akan ditampilkan dengan menggunakan contoh gambar piksel (tingkat keterampilan artis adalah ahli ):


Semua bacaan di shader menggunakan pengambilan sampel bilinear (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).

Offset dari pusat ke masing-masing sudut adalah (Β± 0,5, Β± 0,5), tergantung pada sudutnya.

Lihat bagaimana ini dapat diterapkan pada HLSL? Mari kita lihat:

float2 uvCorner;
float2 uvOffset = float2( 0.5, 0.5 ) / g_Viewport.xy; // remember about division!

float3 colorCorners = 0;

//
// -0,5, -0.5
uvCorner = uvCenter - uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// +0.5, -0.5
uvCorner = uvCenter + float2(uvOffset.x, -uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// -0.5, +0.5
uvCorner = uvCenter + float2(-uvOffset.x, uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// +0.5, +0.5
uvCorner = uvCenter + uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;


Jadi, sekarang keempat sampel dirangkum dalam variabel "colorCorners". Mari ikuti langkah-langkah ini:

//
float3 averageColorCorners = colorCorners / 4.0;

//
float3 diffColor = colorCenter - averageColorCorners;

// . . RGB-
float fDiffColorMaxComponent = max( abs(diffColor.x), max( abs(diffColor.y), abs(diffColor.z) ) );

//
float fDiffColorMaxComponentScaled = saturate( fDiffColorMaxComponent * sharpenLumScale + sharpenLumBias );

// .
// "1.0" - fSharpenIntensity 1.0.
float fPixelSharpenAmount = lerp(1.0, fSharpenAmount, fDiffColorMaxComponentScaled);

// "" .
float lumaCenter = dot( LUMINANCE_RGB, finalColor );
float lumaCornersAverage = dot( LUMINANCE_RGB, averageColorCorners );

// "centerColor"
float3 fColorBalanced = colorCenter / max( lumaCenter, 1e-4 );

//
float fPixelLuminance = lerp(lumaCornersAverage, lumaCenter, fPixelSharpenAmount);

//
finalColor = fColorBalanced * max(fPixelLuminance, 0.0);
}

return float4(finalColor, 1.0);


Pengenalan tepi dilakukan dengan menghitung maks. abs komponen perbedaan. Langkah cerdas! Lihat visualisasinya:


Visualisasi komponen absolut maksimum perbedaan.

Bagus Shader HLSL yang telah selesai tersedia di sini . Maaf untuk pemformatan yang sangat buruk. Anda dapat menggunakan program HLSLexplorer saya dan bereksperimen dengan kode tersebut.

Saya dengan senang hati dapat mengatakan bahwa kode di atas menciptakan kode assembler yang sama seperti di dalam game!

Untuk meringkas: The Witcher 3 ketajaman shader ditulis dengan sangat baik (perhatikan bahwa fPixelSharpenAmount lebih besar dari 1,0! Ini menarik ...). Selain itu, cara utama untuk mengubah kecerahan efek adalah kecerahan objek dekat / jauh. Dalam game ini, mereka bukan konstanta; Saya telah mengumpulkan beberapa contoh nilai:

Skellige:

menajamkan Dekatmenajamkan FarpertajamSkalaPertajamBiasBiaspertajamLumSkalamempertajamLumBias
rendah
tinggi2.01.80,025
-0,25
-13.33333
1.33333

Kaer Morhen:

menajamkan Dekat
menajamkan Far
pertajamSkala
PertajamBiasBias
pertajamLumSkala
mempertajamLumBias
rendah
0,57751
0,31303
0,06665
-0,33256
-1.0
2.0
tinggi
2.17751
1.91303
0,06665
-0,33256
-1.0
2.0

Bagian 7. Kecerahan rata-rata


Pengoperasian menghitung kecerahan rata-rata bingkai saat ini dapat ditemukan di hampir semua gim video modern. Nilai ini sering digunakan kemudian untuk efek adaptasi mata dan koreksi nada (lihat bagian sebelumnya pada pos). Dalam solusi sederhana, perhitungan kecerahan digunakan untuk, katakanlah, tekstur 512 2 , kemudian perhitungan level mip dan aplikasi yang terakhir. Ini biasanya berhasil, tetapi sangat membatasi kemungkinan. Solusi yang lebih kompleks menggunakan shader komputasi yang melakukan, misalnya, reduksi paralel .

Mari kita cari tahu bagaimana tim CD Projekt Red memecahkan masalah ini di The Witcher 3. Di bagian sebelumnya, saya sudah memeriksa koreksi nada dan adaptasi mata, jadi satu-satunya bagian puzzle yang tersisa adalah kecerahan rata-rata.

Untuk mulai dengan, perhitungan kecerahan rata-rata The Witcher 3 terdiri dari dua lintasan. Untuk lebih jelasnya, saya memutuskan untuk memecahnya menjadi beberapa bagian, dan pertama-tama kita melihat pass pertama - "distribusi kecerahan" (perhitungan histogram kecerahan).

Distribusi kecerahan


Dua lintasan ini cukup mudah ditemukan di setiap penganalisa bingkai. Ini adalah panggilan Pengiriman agar tepat sebelum melakukan adaptasi mata:


Mari kita lihat input untuk pass ini. Ia membutuhkan dua tekstur:

1) buffer warna HDR, skala yang direduksi menjadi 1/4 x 1/4 (misalnya, dari 1920x1080 menjadi 480x270),

2) Buffer kedalaman layar penuh


1/4 x 1/4 buffer warna HDR. Perhatikan trik rumit - buffer ini adalah bagian dari buffer yang lebih besar. Menggunakan kembali buffer adalah praktik yang baik.


Penyangga Kedalaman Layar Penuh

Mengapa memperkecil buffer warna? Saya pikir ini semua tentang kinerja.

Adapun output dari pass ini, itu adalah buffer terstruktur. 256 elemen masing-masing 4 byte.

Shader tidak memiliki informasi debug di sini, jadi anggap itu hanya penyangga nilai int yang tidak ditandatangani.

Penting: langkah pertama dalam menghitung kecerahan panggilan rata-rata ClearUnorderedAccessViewUint untuk mengatur ulang semua elemen buffer terstruktur ke nol.

Mari kita pelajari kode assembler dari shader komputasi (ini adalah shader komputasi pertama dalam seluruh analisis kami!)

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[3], immediateIndexed
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_uav_structured u0, 4
dcl_input vThreadGroupID.x
dcl_input vThreadIDInGroup.x
dcl_temps 6
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: store_structured g0.x, vThreadIDInGroup.x, l(0), l(0)
1: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
2: store_structured g0.x, r0.x, l(0), l(0)
3: store_structured g0.x, r0.y, l(0), l(0)
4: store_structured g0.x, r0.z, l(0), l(0)
5: sync_g_t
6: ftoi r1.x, cb0[2].z
7: mov r2.y, vThreadGroupID.x
8: mov r2.zw, l(0, 0, 0, 0)
9: mov r3.zw, l(0, 0, 0, 0)
10: mov r4.yw, l(0, 0, 0, 0)
11: mov r1.y, l(0)
12: loop
13: utof r1.z, r1.y
14: ge r1.z, r1.z, cb0[0].x
15: breakc_nz r1.z
16: iadd r2.x, r1.y, vThreadIDInGroup.x
17: utof r1.z, r2.x
18: lt r1.z, r1.z, cb0[0].x
19: if_nz r1.z
20: ld_indexable(texture2d)(float,float,float,float) r5.xyz, r2.xyzw, t0.xyzw
21: dp3 r1.z, r5.xyzx, l(0.212600, 0.715200, 0.072200, 0.000000)
22: imul null, r3.xy, r1.xxxx, r2.xyxx
23: ld_indexable(texture2d)(float,float,float,float) r1.w, r3.xyzw, t1.yzwx
24: eq r1.w, r1.w, cb0[2].w
25: and r1.w, r1.w, cb0[2].y
26: add r2.x, -r1.z, cb0[2].x
27: mad r1.z, r1.w, r2.x, r1.z
28: add r1.z, r1.z, l(1.000000)
29: log r1.z, r1.z
30: mul r1.z, r1.z, l(88.722839)
31: ftou r1.z, r1.z
32: umin r4.x, r1.z, l(255)
33: atomic_iadd g0, r4.xyxx, l(1)
34: endif
35: iadd r1.y, r1.y, l(64)
36: endloop
37: sync_g_t
38: ld_structured r1.x, vThreadIDInGroup.x, l(0), g0.xxxx
39: mov r4.z, vThreadIDInGroup.x
40: atomic_iadd u0, r4.zwzz, r1.x
41: ld_structured r1.x, r0.x, l(0), g0.xxxx
42: mov r0.w, l(0)
43: atomic_iadd u0, r0.xwxx, r1.x
44: ld_structured r0.x, r0.y, l(0), g0.xxxx
45: atomic_iadd u0, r0.ywyy, r0.x
46: ld_structured r0.x, r0.z, l(0), g0.xxxx
47: atomic_iadd u0, r0.zwzz, r0.x
48: ret


Dan buffer konstan:


Kita sudah tahu bahwa input pertama adalah buffer warna HDR. Dengan FullHD, resolusinya adalah 480x270. Mari kita lihat panggilan Pengiriman.

Pengiriman (270, 1, 1) - ini artinya kami menjalankan 270 grup utas. Sederhananya, kami menjalankan satu grup benang per baris buffer warna.


Setiap grup thread mengeksekusi satu baris buffer warna HDR

Sekarang kita memiliki konteks ini, mari kita coba mencari tahu apa yang shader lakukan.

Setiap grup utas memiliki 64 utas dalam arah X (dcl_thread_group 64, 1, 1), serta memori bersama, 256 elemen dengan masing-masing 4 byte (dcl_tgsm_structured g0, 4, 256).

Perhatikan bahwa dalam shader kita menggunakan SV_GroupThreadID (vThreadIDInGroup.x) [0-63] dan SV_GroupID (vThreadGroupID.x) [0-269].

1) Kita mulai dengan menetapkan semua elemen dari nilai nol memori yang dibagikan. Karena total memori berisi 256 elemen dan 64 utas per grup, ini dapat dengan mudah dilakukan dengan loop sederhana:

// - .
// 64 , 4 .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = 0;
}


2) Setelah itu, kami mengatur penghalang menggunakan GroupMemoryBarrierWithGroupSync (sync_g_t). Kami melakukan ini untuk memastikan bahwa semua utas di memori bersama grup diatur ulang ke nol sebelum melanjutkan ke langkah berikutnya.

3) Sekarang kita menjalankan loop, yang secara kasar dapat ditulis seperti ini:

// cb0_v0.x - . 1920x1080 1920/4 = 480;
float ViewportSizeX = cb0_v0.x;
[loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )
{
...


Ini sederhana untuk loop dengan selisih 64 (apakah Anda sudah mengerti mengapa?).

Langkah selanjutnya adalah menghitung posisi piksel yang dimuat.

Mari kita pikirkan.

Untuk koordinat Y, kita dapat menggunakan SV_GroupID.x karena kami meluncurkan 270 grup utas.

Untuk koordinat X, kita ... dapat memanfaatkan aliran grup saat ini! Mari kita coba melakukannya.

Karena ada 64 utas di setiap grup, solusi seperti itu akan mem-bypass semua piksel.

Pertimbangkan grup utas (0, 0, 0).

- Aliran (0, 0, 0) akan memproses piksel (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0), (320, 0), (384, 0), (448,0).

- Utas (1, 0, 0) akan memproses piksel (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0) 0), (449, 0) ...

- Aliran (63, 0, 0) akan memproses piksel (63, 0), (127, 0), (191, 0), (255, 0), (319, 0), (383, 0), (383, 0), (447, 0)

Dengan demikian, semua piksel akan diproses.

Kami juga perlu memastikan bahwa kami tidak memuat piksel dari luar buffer warna:

// X. Y GroupID.
uint CurrentPixelPositionX = PositionX + threadID;
uint CurrentPixelPositionY = groupID;
if ( CurrentPixelPositionX < ViewportSizeX )
{
// HDR- .
// HDR- , .
uint2 colorPos = uint2(CurrentPixelPositionX, CurrentPixelPositionY);
float3 color = texture0.Load( int3(colorPos, 0) ).rgb;
float luma = dot(color, LUMA_RGB);


Lihat? Sederhana saja!

Saya juga menghitung kecerahan (baris 21 dari kode assembler).

Hebat, kami sudah menghitung kecerahan dari piksel warna. Langkah selanjutnya adalah memuat (bukan sampel!) Nilai kedalaman yang sesuai.

Tapi di sini kita punya masalah, karena kami menghubungkan buffer kedalaman resolusi penuh. Apa yang harus dilakukan?

Ini sangat sederhana - cukup gandakan colorPos dengan konstanta (cb0_v2.z). Kami mengurangi buffer warna HDR empat kali. oleh karena itu nilainya 4!

const int iDepthTextureScale = (int) cb0_v2.z;
uint2 depthPos = iDepthTextureScale * colorPos;
float depth = texture1.Load( int3(depthPos, 0) ).x;


Sejauh ini bagus! Tapi ... kita harus antre 24-25 ...

24: eq r2.x, r2.x, cb0[2].w
25: and r2.x, r2.x, cb0[2].y


Jadi Pertama kita memiliki perbandingan kesetaraan floating point, hasilnya ditulis dalam r2.x, dan setelah itu pergi ... apa? Bitwise dan ?? Benarkah? Untuk nilai floating point? Apa-apaan ini ???

Masalah 'eq + dan'

Izinkan saya mengatakan bahwa bagi saya itu adalah bagian tersulit dari shader. Saya bahkan mencoba kombinasi asint / asfloat yang aneh ...

Dan jika Anda menggunakan pendekatan yang sedikit berbeda? Mari kita lakukan perbandingan float-float di HLSL.

float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y);
return test;
}


Dan inilah output dalam kode assembler:

0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, l(0x3f800000)
2: ret


Menarik, bukan? Saya tidak berharap melihat "dan" di sini.

0x3f800000 hanya 1,0f ... Itu logis karena kita mendapatkan 1,0 dan 0,0 sebaliknya jika perbandingan berhasil.

Tetapi bagaimana jika kita β€œmengganti” 1.0 dengan beberapa nilai lain? Misalnya, seperti ini:

float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y) ? cb0_v0.z : 0.0;
return test;
}


Kami mendapatkan hasil sebagai berikut:

0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, cb0[0].z
2: ret


Ha! Itu berhasil. Ini hanya keajaiban dari kompiler HLSL. Catatan: jika Anda mengganti 0,0 dengan sesuatu yang lain, maka Anda hanya mendapatkan movc.

Mari kita kembali ke shader komputasi. Langkah selanjutnya adalah memverifikasi bahwa kedalamannya sama dengan cb0_v2.w. Itu selalu sama dengan 0,0 - dengan kata lain, kami memeriksa apakah suatu piksel berada di bidang yang jauh (di langit). Jika demikian, maka kami menetapkan koefisien ini beberapa nilai, sekitar 0,5 (saya memeriksa beberapa frame).

Koefisien yang dihitung ini digunakan untuk menginterpolasi antara kecerahan warna dan kecerahan "langit" (nilai cb0_v2.x, yang sering kira-kira sama dengan 0,0). Saya berasumsi bahwa ini diperlukan untuk mengontrol pentingnya langit dalam menghitung kecerahan rata-rata. Biasanya kepentingannya berkurang. Ide yang sangat pintar.

// , ( ). , ,
// .
float value = (depth == cb0_v2.w) ? cb0_v2.y : 0.0;

// 'value' 0.0, lerp 'luma'. 'value'
// ( 0.50), luma . (cb0_v2.x 0.0).
float lumaOk = lerp( luma, cb0_v2.x, value );


Karena kita memiliki lumaOk, langkah selanjutnya adalah menghitung logaritma natural untuk membuat distribusi yang baik. Tapi tunggu, katakan saja lumaOk adalah 0,0. Kita tahu bahwa nilai log (0) tidak terdefinisi, jadi kami menambahkan 1,0 karena log (1) = 0,0.

Setelah itu, kami skala logaritma yang dihitung untuk 128 untuk mendistribusikannya dalam 256 sel. Sangat pintar!

Dan dari sini nilai ini diambil 88.722839. Ini adalah 128 * (2) .

Ini adalah cara HLSL menghitung logaritma.

Hanya ada satu fungsi dalam kode assembler HLSL yang menghitung logaritma: log , dan ia memiliki basis 2.

// , lumaOk 0.0.
// log(0) undefined
// log(1) = 0.
//
lumaOk = log(lumaOk + 1.0);

// 128
lumaOk *= 128;


Akhirnya, kami menghitung indeks sel dari kecerahan yang didistribusikan secara logaritmik dan menambahkan 1 ke sel yang sesuai dalam memori bersama.

// . Uint, 256 ,
// , .
uint uLuma = (uint) lumaOk;
uLuma = min(uLuma, 255);

// 1 .
InterlockedAdd( shared_data[uLuma], 1 );


Langkah selanjutnya lagi akan menetapkan penghalang untuk memastikan bahwa semua piksel di baris telah diproses.

Dan langkah terakhir adalah menambahkan nilai dari memori bersama ke buffer terstruktur. Ini dilakukan dengan cara yang sama, melalui loop sederhana:

// ,
GroupMemoryBarrierWithGroupSync();

// .
[unroll] for (uint idx = 0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;

uint data = shared_data[offset];
InterlockedAdd( g_buffer[offset], data );
}


Setelah semua 64 utas dalam grup utas mengisi data umum, masing-masing utas menambahkan 4 nilai ke buffer output.

Pertimbangkan buffer output. Mari kita pikirkan. Jumlah semua nilai dalam buffer sama dengan jumlah total piksel! (pada 480x270 = 129.600). Artinya, kita tahu berapa banyak piksel yang memiliki nilai kecerahan tertentu.

Jika Anda kurang berpengalaman dalam shading komputasi (seperti saya), maka pada awalnya mungkin tidak jelas, jadi bacalah posting beberapa kali lagi, ambil kertas dan pensil, dan cobalah untuk memahami konsep-konsep bahwa teknik ini dibangun.

Itu saja! Itulah cara The Witcher 3 menghitung histogram kecerahan. Secara pribadi, saya belajar banyak ketika menulis bagian ini. Selamat kepada orang-orang dari CD Projekt Red atas kerja luar biasa mereka!

Jika Anda tertarik dengan shader HLSL lengkap, maka tersedia di sini . Saya selalu berusaha untuk mendapatkan kode perakitan sedekat mungkin dengan permainan dan sangat senang bahwa saya berhasil lagi!

Perhitungan kecerahan rata-rata


Ini adalah bagian kedua dari analisis perhitungan kecerahan sedang dalam "The Witcher 3: Wild Hunt".

Sebelum kita berperang dengan shader komputasi lain, mari kita ulangi secara singkat apa yang terjadi di bagian terakhir: kita bekerja dengan penyangga warna HDR dengan skala hingga 1 / 4x1 / 4. Setelah lulus pertama, kami mendapat histogram kecerahan (buffer terstruktur dari 256 nilai integer yang tidak ditandatangani). Kami menghitung logaritma untuk kecerahan setiap piksel, mendistribusikannya lebih dari 256 sel dan meningkatkan nilai buffer terstruktur sebesar 1 per piksel. Karena ini, jumlah total semua nilai dalam 256 sel ini sama dengan jumlah piksel.


Contoh output dari pass pertama. Ada 256 elemen.

Misalnya, buffer layar penuh kami memiliki ukuran 1920x1080. Setelah melakukan zoom out, pass pertama menggunakan buffer 480x270. Jumlah dari semua 256 nilai dalam buffer akan sama dengan 480 * 270 = 129 600.

Setelah pengantar singkat ini, kami siap untuk melanjutkan ke langkah berikutnya: ke komputasi.

Kali ini hanya satu grup utas yang digunakan (Pengiriman (1, 1, 1)).

Mari kita lihat kode assembler dari shader komputasi:

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_uav_structured u0, 4
dcl_uav_typed_texture2d (float,float,float,float) u1
dcl_input vThreadIDInGroup.x
dcl_temps 4
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, vThreadIDInGroup.x, l(0), u0.xxxx
1: store_structured g0.x, vThreadIDInGroup.x, l(0), r0.x
2: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
3: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, r0.x, l(0), u0.xxxx
4: store_structured g0.x, r0.x, l(0), r0.w
5: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.y, l(0), u0.xxxx
6: store_structured g0.x, r0.y, l(0), r0.x
7: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.z, l(0), u0.xxxx
8: store_structured g0.x, r0.z, l(0), r0.x
9: sync_g_t
10: if_z vThreadIDInGroup.x
11: mul r0.x, cb0[0].y, cb0[0].x
12: ftou r0.x, r0.x
13: utof r0.y, r0.x
14: mul r0.yz, r0.yyyy, cb0[0].zzwz
15: ftoi r0.yz, r0.yyzy
16: iadd r0.x, r0.x, l(-1)
17: imax r0.y, r0.y, l(0)
18: imin r0.y, r0.x, r0.y
19: imax r0.z, r0.y, r0.z
20: imin r0.x, r0.x, r0.z
21: mov r1.z, l(-1)
22: mov r2.xyz, l(0, 0, 0, 0)
23: loop
24: breakc_nz r2.x
25: ld_structured r0.z, r2.z, l(0), g0.xxxx
26: iadd r3.x, r0.z, r2.y
27: ilt r0.z, r0.y, r3.x
28: iadd r3.y, r2.z, l(1)
29: mov r1.xy, r2.yzyy
30: mov r3.z, r2.x
31: movc r2.xyz, r0.zzzz, r1.zxyz, r3.zxyz
32: endloop
33: mov r0.w, l(-1)
34: mov r1.yz, r2.yyzy
35: mov r1.xw, l(0, 0, 0, 0)
36: loop
37: breakc_nz r1.x
38: ld_structured r2.x, r1.z, l(0), g0.xxxx
39: iadd r1.y, r1.y, r2.x
40: utof r2.x, r2.x
41: utof r2.w, r1.z
42: add r2.w, r2.w, l(0.500000)
43: mul r2.w, r2.w, l(0.011271)
44: exp r2.w, r2.w
45: add r2.w, r2.w, l(-1.000000)
46: mad r3.z, r2.x, r2.w, r1.w
47: ilt r2.x, r0.x, r1.y
48: iadd r2.w, -r2.y, r1.y
49: itof r2.w, r2.w
50: div r0.z, r3.z, r2.w
51: iadd r3.y, r1.z, l(1)
52: mov r0.y, r1.z
53: mov r3.w, r1.x
54: movc r1.xzw, r2.xxxx, r0.wwyz, r3.wwyz
55: endloop
56: store_uav_typed u1.xyzw, l(0, 0, 0, 0), r1.wwww
57: endif
58: ret


Ada satu buffer konstan:


Lihatlah kode assembler: dua UAV terpasang (u0: input buffer dari bagian pertama dan u1: tekstur output format 1x1 R32_FLOAT). Kita juga melihat bahwa ada 64 utas per grup dan 256 elemen memori grup 4-byte yang dibagi.

Kami mulai dengan mengisi memori bersama dengan data dari buffer input. Kami memiliki 64 utas, jadi Anda harus melakukan hampir sama seperti sebelumnya.

Untuk benar-benar yakin bahwa semua data telah dimuat untuk diproses lebih lanjut, setelah itu kami menempatkan penghalang.

// - .
// 64 , 4
// .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = g_buffer[offset];
}
// , ,
// .
GroupMemoryBarrierWithGroupSync();


Semua perhitungan dilakukan hanya dalam satu utas, semua yang lain hanya digunakan untuk memuat nilai dari buffer ke memori bersama.

Aliran "komputasi" memiliki indeks 0. Mengapa? Secara teoritis, kita dapat menggunakan aliran apa pun dari interval [0-63], tetapi berkat perbandingan dengan 0, kita dapat menghindari perbandingan integer-integer tambahan (instruksi ieq ).

Algoritma didasarkan pada indikasi interval piksel yang akan diperhitungkan dalam operasi.

Pada baris 11, kita mengalikan lebar * tinggi untuk mendapatkan jumlah total piksel dan mengalikannya dengan dua angka dari interval [0,0f-1.0f], yang menunjukkan awal dan akhir interval. Pembatasan lebih lanjut digunakan untuk memastikan bahwa 0 <= Start <= End <= totalPixels - 1 :

// 0.
[branch] if (threadID == 0)
{
//
uint totalPixels = cb0_v0.x * cb0_v0.y;

// (, , ),
// .
int pixelsToConsiderStart = totalPixels * cb0_v0.z;
int pixelsToConsiderEnd = totalPixels * cb0_v0.w;

int pixelsMinusOne = totalPixels - 1;

pixelsToConsiderStart = clamp( pixelsToConsiderStart, 0, pixelsMinusOne );
pixelsToConsiderEnd = clamp( pixelsToConsiderEnd, pixelsToConsiderStart, pixelsMinusOne );


Seperti yang Anda lihat, ada dua siklus di bawah ini. Masalah dengan mereka (atau dengan kode assembler mereka) adalah bahwa ada transisi bersyarat yang aneh di ujung loop. Sangat sulit bagi saya untuk menciptakannya kembali. Lihat juga baris 21. Mengapa ada "-1"? Saya akan jelaskan sedikit di bawah ini.

Tugas dari siklus pertama adalah untuk menjatuhkan pixelToConsiderStart dan memberi kami indeks sel buffer di mana pixelToConsiderStart +1 hadir (serta jumlah semua piksel dalam sel sebelumnya).

Katakanlah pixelToConsiderStart kira-kira sama dengan 30.000, dan di buffer ada 37.000 piksel dalam sel "nol" (ini terjadi di permainan di malam hari). Oleh karena itu, kami ingin memulai analisis kecerahan dengan kira-kira piksel 30001, yang ada di sel "nol". Dalam hal ini, kami segera keluar dari loop, mendapatkan indeks awal '0' dan nol piksel yang dibuang.

Lihatlah kode HLSL:

//
int numProcessedPixels = 0;

// [0-255]
int lumaValue = 0;

//
bool bExitLoop = false;

// - "pixelsToConsiderStart" .
// lumaValue, .
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];

// , lumaValue
int tempSum = numProcessedPixels + numPixels;

// , pixelsToConsiderStart, .
// , lumaValue.
// , pixelsToConsiderStart - "" , , .
[flatten]
if (tempSum > pixelsToConsiderStart)
{
bExitLoop = true;
}
else
{
numProcessedPixels = tempSum;
lumaValue++;
}
}


Angka misterius "-1" dari baris 21 kode assembler dikaitkan dengan kondisi Boolean untuk eksekusi loop (saya menemukan ini hampir secara tidak sengaja).

Setelah menerima jumlah piksel dari sel lumaValue dan lumaValue sendiri, kita dapat beralih ke siklus kedua.

Tugas siklus kedua adalah untuk menghitung pengaruh piksel dan kecerahan rata-rata.

Kita mulai dengan lumaValue yang dihitung di loop pertama.

float finalAvgLuminance = 0.0f;

//
uint numProcessedPixelStart = numProcessedPixels;

// - .
// , , lumaValue.
// [0-255], , , ,
// pixelsToConsiderEnd.
// .
bExitLoop = false;
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];

//
numProcessedPixels += numPixels;

// , [0-255] (uint)
uint encodedLumaUint = lumaValue;

//
float numberOfPixelsWithCurrentLuma = numPixels;

// , [0-255] (float)
float encodedLumaFloat = encodedLumaUint;


Pada tahap ini, kami memperoleh nilai kecerahan yang dikodekan dalam interval [0,0f-255.f].

Proses decoding cukup sederhana - Anda harus membalikkan perhitungan tahap penyandian.

Pengulangan singkat dari proses pengkodean:

float luma = dot( hdrPixelColor, float3(0.2126, 0.7152, 0.0722) );
...
float outLuma;

// log(0) undef, log(1) = 0
outLuma = luma + 1.0;

//
outLuma = log( outLuma );

// 128, log(1) * 128 = 0, log(2,71828) * 128 = 128, log(7,38905) * 128 = 256
outLuma = outLuma * 128

// uint
uint outLumaUint = min( (uint) outLuma, 255);


Untuk mendekode kecerahan, kami membalikkan proses pengkodean, misalnya, seperti ini:

// 0.5f ( , )
float fDecodedLuma = encodedLumaFloat + 0.5;

// :

// 128
fDecodedLuma /= 128.0;

// exp(x), log(x)
fDecodedLuma = exp(fDecodedLuma);

// 1.0
fDecodedLuma -= 1.0;


Kemudian kita menghitung distribusi dengan mengalikan jumlah piksel dengan kecerahan yang diberikan dengan kecerahan yang didekodekan, dan menjumlahkannya sampai kita sampai pada pemrosesan piksel ToTonsamaEnd piksel.

Setelah itu, kami membagi efek total pada jumlah piksel yang dianalisis.

Berikut adalah sisa dari loop (dan shader): Shader lengkap tersedia di sini . Ini sepenuhnya kompatibel dengan program HLSLexplorer saya , tanpanya saya tidak akan dapat secara efektif membuat kembali perhitungan kecerahan rata-rata di The Witcher 3 (dan semua efek lainnya juga!). Kesimpulannya, beberapa pemikiran. Dalam hal menghitung kecerahan rata-rata, shader ini sulit dibuat ulang. Alasan utama: 1) Pemeriksaan "pending" yang aneh pada pelaksanaan siklus, butuh waktu lebih banyak daripada yang saya pikir sebelumnya. 2) Masalah dengan debugging shader komputasi ini di RenderDoc (v. 1.2).

//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;

// () .
float tempTotalContribution = fCurrentLumaContribution + finalAvgLuminance;


[flatten]
if (numProcessedPixels > pixelsToConsiderEnd )
{
//
bExitLoop = true;

// , .
//
int diff = numProcessedPixels - numProcessedPixelStart;

//
finalAvgLuminance = tempTotalContribution / float(diff);
}
else
{
// lumaValue
finalAvgLuminance = tempTotalContribution;
lumaValue++;
}
}

//
g_avgLuminance[uint2(0,0)] = finalAvgLuminance;










Operasi "ld_structured_indexable" tidak sepenuhnya didukung, meskipun hasil membaca dari indeks 0 memberikan nilai yang benar, semua yang lain mengembalikan nol, itulah sebabnya siklus terus berlanjut tanpa batas.

Meskipun saya tidak dapat mencapai kode perakitan yang sama seperti aslinya (lihat screenshot di bawah untuk perbedaan), menggunakan RenderDoc saya bisa menyuntikkan shader ini ke dalam pipa - dan hasilnya sama!


Hasil pertempuran. Di sebelah kiri adalah shader saya, di sebelah kanan adalah kode assembler asli.

Bagian 8. Bulan dan fasanya


Pada bagian kedelapan artikel, saya memeriksa shader bulan dari The Witcher 3 (dan lebih khusus lagi, dari ekstensi Darah dan Anggur).

Bulan adalah elemen penting dari langit malam, dan itu bisa sangat sulit untuk membuatnya dapat dipercaya, tetapi bagi saya berjalan di malam hari di TW3 benar-benar menyenangkan.

Lihat saja adegan ini!


Sebelum kita mengambil pixel shader, saya akan mengatakan beberapa kata tentang nuansa rendering. Dari sudut pandang geometris, Bulan hanyalah sebuah bola (lihat di bawah), yang memiliki koordinat tekstur, vektor normal dan garis singgung. Vertex shader menghitung posisi di ruang dunia, serta vektor normals yang dinormalisasi, garis singgung dan garis singgung ke dua titik (menggunakan produk vektor), dikalikan dengan matriks dunia.

Untuk memastikan bahwa Bulan terletak sepenuhnya pada pesawat remote, bidang dan MinDepth MaxDepth struktur D3D11_VIEWPORT ditugaskan nilai 0.0 (trik yang sama yang digunakan untuk kubah langit). Bulan dirender segera setelah langit.


Bola yang digunakan untuk menggambar bulan.

Nah, semuanya, saya pikir, Anda bisa melanjutkan. Mari kita lihat pixel shader: Alasan utama saya memilih shader dari Blood and Wine adalah sederhana - lebih pendek. Pertama, kami menghitung offset untuk sampel tekstur. cb0 [0] .w digunakan sebagai offset di sepanjang sumbu X. Dengan trik sederhana ini kita dapat mensimulasikan rotasi bulan di sekitar porosnya.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[267], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_input_ps linear v1.w
dcl_input_ps linear v2.xyzw
dcl_input_ps linear v3.xy
dcl_input_ps linear v4.xy
dcl_output o0.xyzw
dcl_temps 3
0: mov r0.x, -cb0[0].w
1: mov r0.y, l(0)
2: add r0.xy, r0.xyxx, v2.xyxx
3: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, t0.xyzw, s0
4: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
5: log r0.w, r0.w
6: mul r0.w, r0.w, l(2.200000)
7: exp r0.w, r0.w
8: add r0.xyz, r0.xyzx, r0.xyzx
9: dp3 r1.x, r0.xyzx, r0.xyzx
10: rsq r1.x, r1.x
11: mul r0.xyz, r0.xyzx, r1.xxxx
12: mul r1.xy, r0.yyyy, v3.xyxx
13: mad r0.xy, v4.xyxx, r0.xxxx, r1.xyxx
14: mad r0.xy, v2.zwzz, r0.zzzz, r0.xyxx
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
17: sincos r1.x, r2.x, r0.z
18: mov r2.y, r1.x
19: dp2_sat r0.x, r0.xyxx, r2.xyxx
20: mul r0.xyz, r0.xxxx, cb12[266].xyzx
21: mul r0.xyz, r0.xyzx, r0.wwww
22: mul r0.xyz, r0.xyzx, cb2[2].xyzx
23: add_sat r0.w, -v1.w, l(1.000000)
24: mul r0.w, r0.w, cb2[2].w
25: mul o0.xyz, r0.wwww, r0.xyzx
26: mov o0.w, l(0)
27: ret









Contoh nilai dari buffer konstan.

Satu tekstur (1024x512) dilampirkan sebagai input. Peta normal dikodekan dalam saluran RGB, dan warna permukaan bulan di saluran alfa. Pandai!


Saluran alfa dari suatu tekstur adalah warna permukaan bulan.


Tekstur saluran RGB adalah peta normal.

Setelah menerima koordinat tekstur yang benar, kami mencicipi saluran RGBA. Kita perlu membongkar peta normal dan melakukan koreksi gamma pada warna permukaan. Saat ini, shader HLSL dapat ditulis seperti ini, misalnya: Langkah selanjutnya adalah melakukan penjilidan normal, tetapi hanya di komponen XY. (Dalam The Witcher 3, sumbu Z naik, dan seluruh saluran Z tekstur adalah 1.0). Kita dapat melakukannya dengan cara ini: Sekarang saatnya untuk bagian favorit saya dari shader ini. Lihat lagi pada baris 15-16: Apakah 0,033864 yang misterius ini? Pada awalnya, tampaknya itu tidak masuk akal, tetapi jika kita menghitung nilai kebalikannya, kita mendapatkan sekitar 29,53, yang sama dengan durasi bulan sinode.

float4 MoonPS(in InputStruct IN) : SV_Target0
{
// Texcoords
float2 uvOffsets = float2(-cb0_v0.w, 0.0);

// texcoords
float2 uv = IN.param2.xy + uvOffsets;

//
float4 sampledTexture = texture0.Sample( sampler0, uv);

// - -
float moonColorTex = pow(sampledTexture.a, 2.2 );

// [0,1] [-1,1].
// : sampledTexture.xyz * 2.0 - 1.0
float3 sampledNormal = normalize((sampledTexture.xyz - 0.5) * 2);




//
float3 Tangent = IN.param4.xyz;
float3 Normal = float3(IN.param2.zw, IN.param3.w);
float3 Bitangent = IN.param3.xyz;

// TBN
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);

// XY
// TBN float3x2: 3 , 2
float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;




15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)


dalam beberapa hari! Inilah yang saya perhatikan detail!

Kami dapat mengasumsikan bahwa cb0 [0] .y adalah jumlah hari yang telah berlalu selama bermain game. Deviasi tambahan digunakan di sini, digunakan sebagai offset di sepanjang sumbu x tekstur.

Setelah menerima koefisien ini, kami mengalikannya dengan 2 * Pi.

Kemudian menggunakan sincos kita menghitung vektor 2d lainnya.

Dengan menghitung produk skalar antara vektor normal dan vektor "bulan", satu fase bulan disimulasikan. Lihatlah tangkapan layar dengan berbagai fase bulan:

// .
// days/29.53 + bias.
float phase = cb0_v0.y * (1.0 / SYNODIC_MONTH_LENGTH) + cb0_v0.w;

// 2*PI. , 29.53
// sin/cos.
phase *= TWOPI;

// .
float outSin = 0.0;
float outCos = 0.0;
sincos(phase, outSin, outCos);

//
float lunarPhase = saturate( dot(vNormal, float2(outCos, outSin)) );






Langkah terakhir adalah melakukan serangkaian operasi multiplikasi untuk menghitung warna akhir. Anda mungkin tidak mengerti mengapa shader ini mengirimkan nilai alpha 0,0 ke output. Ini karena bulan dirender dengan blending diaktifkan:

// .

// cb12_v266.xyz , .
// (1.54, 2.82, 4.13)
float3 moonSurfaceGlowColor = cb12_v266.xyz;

float3 moonColor = lunarPhase * moonSurfaceGlowColor;
moonColor = moonColorTex * moonColor;

// cb_v2.xyz - , , , (1.0, 1.0, 1.0)
moonColor *= cb2_v2.xyz;

// , , . - .
// , ,
// .
float paramHorizon = saturate(1.0 - IN.param1.w);
paramHorizon *= cb2_v2.w;

moonColor *= paramHorizon;

//
return float4(moonColor, 0.0);





Pendekatan ini memungkinkan Anda untuk mendapatkan warna latar belakang (langit) jika shader ini kembali hitam.

Jika Anda tertarik pada shader lengkap, maka Anda dapat membawanya di sini . Ini memiliki buffer konstan besar dan harus sudah siap untuk injeksi ke RenderDoc bukan shader asli (cukup ganti nama "MoonPS" menjadi "EditedShaderPS").

Dan yang terakhir: Saya ingin berbagi hasil dengan Anda:

Di sebelah kiri adalah shader saya, di sebelah kanan adalah shader asli dari permainan.

Perbedaannya minimal dan tidak mempengaruhi hasil.


Seperti yang Anda lihat, shader ini cukup mudah dibuat ulang.

Bagian 9. G-buffer


Pada bagian ini, saya akan mengungkapkan beberapa detail gbuffer di The Witcher 3.

Kami akan menganggap bahwa Anda mengetahui dasar-dasar peneduhan yang ditangguhkan.

Pengulangan singkat: ide menunda bukan untuk menghitung semua pencahayaan jadi dan bayangan sekaligus, tetapi untuk membagi perhitungan menjadi dua tahap.

Pada yang pertama (lintasan geometri) kami mengisi GBuffer dengan data permukaan (posisi, normal, warna specular, dll ...), dan pada lintasan yang kedua (penerangan penerangan) kami menggabungkan semuanya dan menghitung pencahayaan.

Peneduhan ditangguhkan adalah pendekatan yang sangat populer karena memungkinkan Anda untuk menghitung dalam satu layar penuh lulus dengan teknik seperti peneduhan ubin , yang sangat meningkatkan kinerja.

Sederhananya, GBuffer adalah sekumpulan tekstur dengan properti geometri. Sangat penting untuk membuat struktur yang tepat untuk itu. Sebagai contoh dari kehidupan nyata, Anda dapat mempelajari teknologi rendering Crysis 3 .

Setelah pengantar singkat ini, mari kita lihat contoh frame dari The Witcher 3: Blood and Wine:


Salah satu dari banyak hotel di Toussent

Basic GBuffer terdiri dari tiga target render layar penuh dalam format DXGI_FORMAT_R8G8B8A8_UNORM dan buffer stensil kedalaman + dalam format DXGI_FORMAT_D24_UNORM_S8_UINT.

Berikut screenshot mereka:


Render Target 0 - saluran RGB, warna permukaan


Render Target 0 - saluran alpha. Jujur, saya tidak tahu apa informasi ini.


Render Target 1 - saluran RGB. Vektor normal dalam interval [0-1] direkam di sini.


Render Target 1 - saluran alpha. Tampak seperti reflektifitas!


Render Target 2 - saluran RGB. Tampak seperti warna specular!

Dalam adegan ini, saluran alfa berwarna hitam (tetapi kemudian digunakan).


Kedalaman penyangga. Perhatikan bahwa kedalaman terbalik digunakan di sini.


Buffer stensil digunakan untuk menandai jenis piksel tertentu (misalnya, kulit, vegetasi, dll.)

Ini bukan seluruh GBuffer. Pass pencahayaan juga menggunakan probe pencahayaan dan buffer lainnya, tetapi saya tidak akan membahasnya di artikel ini.

Sebelum melanjutkan ke bagian "utama" dari pos, saya akan memberikan pengamatan umum:

Pengamatan umum



1) Satu-satunya penyangga yang harus dibersihkan adalah penyangga kedalaman / stensil.

Jika Anda menganalisis tekstur yang disebutkan di atas dalam penganalisis bingkai yang baik, Anda akan sedikit terkejut, karena mereka tidak menggunakan panggilan "Hapus", dengan pengecualian Kedalaman / Stensil.

Pada kenyataannya, RenderTarget1 terlihat seperti ini (perhatikan piksel "buram" di pesawat jauh):


Ini adalah optimasi sederhana dan cerdas.

Pelajaran penting: Anda perlu menghabiskan sumber daya pada panggilan ClearRenderTargetView , jadi gunakan hanya saat diperlukan.

2) terbalik kedalaman - itu adalah dingin

di banyak artikel yang sudah ditulis tentang akurasi buffer mendalam dengan floating point. Witcher 3 menggunakan terbalik-z. Ini adalah pilihan alami untuk gim dunia terbuka dengan jarak render yang panjang.

Beralih ke DirectX tidak akan sulit:

a) Kami menghapus buffer kedalaman dengan menulis "0", bukan "1".

Dalam pendekatan tradisional, nilai jauh "1" digunakan untuk menghapus buffer kedalaman. Setelah flip kedalaman, nilai "jauh" baru menjadi 0, jadi Anda perlu mengubah semuanya.

b) Tukar batas dekat dan jauh ketika menghitung matriks proyeksi

c) Ubah pemeriksaan kedalaman dari "kurang" menjadi "lebih"

Untuk OpenGL, sedikit lebih banyak pekerjaan yang harus dilakukan (lihat artikel yang disebutkan di atas), tetapi itu sepadan.

3) Kami tidak menjaga posisi kami di dunia.

Ya, semuanya sangat sederhana. Dalam perjalanan pencahayaan, kita menciptakan kembali posisi di dunia dari kedalaman.

Pixel shader


Pada bagian ini, saya ingin menunjukkan pixel shader yang memasok data permukaan ke GBuffer.

Jadi sekarang kita sudah tahu cara menyimpan warna, normals dan specular.

Tentu saja, semuanya tidak sesederhana yang Anda bayangkan.

Masalah dengan pixel shader adalah ia memiliki banyak opsi. Mereka berbeda dalam jumlah tekstur yang ditransfer ke mereka dan jumlah parameter yang digunakan dari buffer konstan (mungkin dari buffer konstan menggambarkan materi).

Untuk analisis, saya memutuskan untuk menggunakan tong yang indah ini:


Barel heroik kami!

Harap sambut teksturnya:


Jadi kami memiliki Albedo, peta normal, dan warna specular. Kasing standar cantik.

Sebelum kita mulai, beberapa kata tentang input geometri:

Geometri ditransmisikan dengan posisi, texcoords, buffer normal dan tangen. Vertex

shader menghasilkan setidaknya texcoords, vektor tangen / normal / tangen dinormalisasi menjadi dua titik, yang sebelumnya dikalikan dengan matriks dunia. Untuk bahan yang lebih kompleks (misalnya, dengan dua peta difus atau dua peta normal), vertex shader dapat menampilkan data lain, tetapi saya ingin menunjukkan contoh sederhana di sini.

Pixel shader dalam kode assembler: Shader terdiri dari beberapa langkah. Saya akan menjelaskan setiap bagian utama dari shader ini secara terpisah.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[3], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t2
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 3
0: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, v1.xyxx, t1.xyzw, s0
1: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t0.xyzw, s0
2: add r1.w, r1.y, r1.x
3: add r1.w, r1.z, r1.w
4: mul r2.x, r1.w, l(0.333300)
5: add r2.y, l(-1.000000), cb4[1].x
6: mul r2.y, r2.y, l(0.500000)
7: mov_sat r2.z, r2.y
8: mad r1.w, r1.w, l(-0.666600), l(1.000000)
9: mad r1.w, r2.z, r1.w, r2.x
10: mul r2.xzw, r1.xxyz, cb4[0].xxyz
11: mul_sat r2.xzw, r2.xxzw, l(1.500000, 0.000000, 1.500000, 1.500000)
12: mul_sat r1.w, abs(r2.y), r1.w
13: add r2.xyz, -r1.xyzx, r2.xzwx
14: mad r1.xyz, r1.wwww, r2.xyzx, r1.xyzx
15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
21: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r1.xyz, v3.xyzx, r0.xxxx, r1.xyzx
27: mad r0.xyz, v2.xyzx, r0.zzzz, r1.xyzx
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
46: dp3 r0.w, r0.xyzx, r0.xyzx
47: rsq r0.w, r0.w
48: mul r0.xyz, r0.wwww, r0.xyzx
49: max r0.w, abs(r0.y), abs(r0.x)
50: max r0.w, r0.w, abs(r0.z)
51: lt r1.xy, abs(r0.zyzz), r0.wwww
52: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
53: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
54: lt r1.z, r1.y, r1.x
55: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
56: div r1.z, r1.y, r1.x
57: div r0.xyz, r0.xyzx, r0.wwww
58: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
59: mul r0.xyz, r0.wwww, r0.xyzx
60: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
61: mov o0.w, cb4[2].x
62: mov o2.w, l(0)
63: ret




Tapi pertama-tama, seperti biasa - tangkapan layar dengan nilai dari buffer konstan:


Albedo


Kami akan mulai dengan hal-hal yang kompleks. Ini bukan hanya "OutputColor.rgb = Texture.Sample (uv) .rgb"

Setelah mengambil sampel tekstur warna RGB (baris 1), 14 baris berikutnya adalah apa yang saya sebut "buffer pengurangan saturasi". Mari saya tunjukkan kode HLSL: Untuk sebagian besar objek, kode ini tidak melakukan apa-apa selain mengembalikan warna asli dari tekstur. Ini dicapai dengan nilai "material cbuffer" yang sesuai. cb4_v1.x memiliki nilai 1.0, yang mengembalikan mask 0,0 dan mengembalikan warna input dari instruksi lerp . Namun, ada beberapa pengecualian. DesaturationFactor terbesar yang saya temukan adalah 4.0 (tidak pernah kurang dari 1.0), dan desaturatedColor

float3 albedoColorFilter( in float3 color, in float desaturationFactor, in float3 desaturationValue )
{
float sumColorComponents = color.r + color.g + color.b;

float averageColorComponentValue = 0.3333 * sumColorComponents;
float oneMinusAverageColorComponentValue = 1.0 - averageColorComponentValue;

float factor = 0.5 * (desaturationFactor - 1.0);

float avgColorComponent = lerp(averageColorComponentValue, oneMinusAverageColorComponentValue, saturate(factor));
float3 desaturatedColor = saturate(color * desaturationValue * 1.5);

float mask = saturate( avgColorComponent * abs(factor) );

float3 finalColor = lerp( color, desaturatedColor, mask );
return finalColor;
}




Tergantung bahannya. Itu bisa seperti (0.2, 0.3, 0.4); Tidak ada aturan ketat. Tentu saja, saya tidak bisa menolak menerapkan ini dalam kerangka DX11 saya sendiri, dan di sini adalah hasil di mana semua nilai desaturatedColor sama dengan float3 (0,25, 0,3, 0,45)


desaturationFactor = 1.0 (tidak berpengaruh)


desaturationFactor = 2.0


desaturationFactor = 3.0


desaturationFactor = 4.0

Saya yakin ini hanya aplikasi parameter material, tetapi tidak dilakukan di akhir bagian albedo.

Baris 15-20 menambahkan sentuhan akhir: v0.z adalah output dari vertex shader, dan semuanya nol. Jangan lupakan itu, karena v0.z nantinya akan digunakan beberapa kali. Sepertinya itu semacam koefisien, dan seluruh kode tampak seperti albedo peredupan sedikit, tetapi karena v0.z adalah 0, warnanya tetap tidak berubah. HLSL: Mengenai RT0.a, seperti yang dapat kita lihat, ini diambil dari buffer konstanta material, tetapi karena shader tidak memiliki informasi debug, sulit untuk mengatakan apa itu. Mungkin tembus cahaya? Kami selesai dengan target render pertama!

15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx






/* ALBEDO */
// (?)
float3 albedoColor = albedoColorFilter( colorTex, cb4_v1.x, cb4_v0.rgb );
float albedoMaxComponent = getMaxComponent( albedoColor );

// ,
// "paramZ" 0
float paramZ = Input.out0.z; // , 0

// , 0.70 0.85
// lerp, .
float param = (albedoMaxComponent > 0.22) ? 0.70 : 0.85;
float mulParam = lerp(1, param, paramZ);

//
pout.RT0.rgb = albedoColor * mulParam;
pout.RT0.a = cb4_v2.x;






Normal


Mari kita mulai dengan membongkar peta normal, dan kemudian, seperti biasa, kita akan mengikat normals: Sejauh ini, tidak ada yang mengejutkan. Lihatlah baris 28-33: Kita dapat menuliskannya secara kasar sebagai berikut: Tidak yakin apakah itu benar untuk ditulis. Jika Anda tahu operasi matematika ini, beri tahu saya. Kami melihat bahwa pixel shader menggunakan SV_IsFrontFace.

/* */
float3 sampledNormal = ((normalTex.xyz - 0.5) * 2);

// TBN
float3 Tangent = Input.TangentW.xyz;
float3 Normal = Input.NormalW.xyz;
float3 Bitangent;
Bitangent.x = Input.out0.w;
Bitangent.yz = Input.out1.zw;

// ; , , normal-tbn
// 'mad' 'mov'
Bitangent = saturate(Bitangent);

float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
float3 normal = mul( sampledNormal, TBN );






28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif




[branch] if (bIsFrontFace <= 0)
{
float cosTheta = dot(Input.NormalW, normal);
float3 invNormal = cosTheta * Input.NormalW;
normal = normal - 2*invNormal;
}






Apa iniDokumentasi datang untuk membantu (saya ingin menulis "msdn", tetapi ...):

Menentukan apakah segitiga melihat kamera. Untuk garis dan titik, IsFrontFace benar. Pengecualian adalah garis yang diambil dari segitiga (mode wireframe), yang mengatur IsFrontFace mirip dengan rasterisasi sebuah segitiga dalam mode padat. Menulis untuk itu dapat dilakukan oleh geometri shader, dan membaca darinya dengan pixel shader.

Saya ingin memeriksanya sendiri. Dan pada kenyataannya, efeknya hanya terlihat dalam mode wireframe. Saya percaya potongan kode ini diperlukan untuk perhitungan normals yang benar (dan karenanya pencahayaan) dalam mode wireframe.

Berikut ini perbandingannya: baik warna bingkai adegan yang sudah selesai dengan trik on / off ini, maupun tekstur gbuffer [0-1] normals dengan trick on / off:


Warna adegan tanpa trik


Adegan warna dengan aksi


Normal [0-1] tidak ada trik


Normal [0-1] dengan trik

Pernahkah Anda memperhatikan bahwa setiap target render di GBuffer memiliki format R8G8B8A8_UNORM? Ini berarti ada 256 nilai yang mungkin per komponen. Apakah ini cukup untuk menyimpan normals?

Menyimpan normals berkualitas tinggi dengan byte yang cukup di Gbuffer adalah masalah yang diketahui, tetapi untungnya ada banyak bahan yang berbeda untuk dipelajari . Mungkin beberapa dari Anda sudah tahu teknik apa yang digunakan di sini. Saya harus mengatakan bahwa di seluruh bagian geometri ada satu tekstur tambahan yang melekat pada slot 13 ...:





Ha!The Witcher 3 menggunakan teknik yang disebut " Best Fit Normals ." Di sini saya tidak akan menjelaskannya secara detail (lihat presentasi). Itu diciptakan sekitar 2009-2010 oleh Crytek, dan karena CryEngine memiliki open source, BFN juga open source .

BFN memberi tekstur tampilan yang "kasar" pada normals.

Setelah penskalaan normalnya menggunakan BFN, kami mengode ulangnya dari interval [-1; 1] ke [0, 1].

Specular


Mari kita mulai dari baris 34 dan sampel tekstur specular: Seperti yang Anda lihat, ada filter "peredupan" yang kita tahu dari Albedo: Kami menghitung komponen dengan maks. nilai, dan kemudian menghitung warna "gelap" dan menyisipkannya dengan warna specular asli, mengambil parameter dari vertex shader ... yaitu 0, jadi pada output kita mendapatkan warna dari tekstur. HLSL:

34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx








/* SPECULAR */
float3 specularTex = texture2.Sample( samplerAnisoWrap, Texcoords ).rgb;

// , Albedo. . ,
// - "".
// paramZ 0,
// .
float specularMaxComponent = getMaxComponent( specularTex );
float3 specB = (specularMaxComponent > 0.2) ? specularTex : float3(0.12, 0.12, 0.12);
float3 finalSpec = lerp(specularTex, specB, paramZ);
pout.RT2.xyz = finalSpec;


Reflektivitas


Saya tidak tahu apakah nama ini cocok untuk parameter ini, karena saya tidak tahu bagaimana ini mempengaruhi jalannya pencahayaan. Faktanya adalah saluran alfa dari peta normal input berisi data tambahan:


Tekstur saluran alfa "peta normal".

Kode Assembler: Katakan halo kepada teman lama kita - v0.z! Artinya mirip dengan Albedo dan specular:

41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w




/* REFLECTIVITY */
float reflectivity = normalTex.a;
float reflectivity2 = (reflectivity < 0.33) ? (reflectivity * 0.95) : 0.33;

float finalReflectivity = lerp(reflectivity, reflectivity2, paramZ);
pout.RT1.a = finalReflectivity;


Hebat!Ini adalah akhir dari analisis versi pertama dari shader piksel.

Berikut ini adalah perbandingan shader saya (kiri) dengan yang asli (kanan):


Perbedaan-perbedaan ini tidak mempengaruhi perhitungan, jadi pekerjaan saya di sini selesai.

Pixel Shader: Albedo + Opsi Normal


Saya memutuskan untuk menunjukkan satu opsi lagi, sekarang hanya dengan albedo dan peta normal, tanpa tekstur specular. Kode assembler sedikit lebih panjang: Perbedaan antara ini dan opsi sebelumnya adalah sebagai berikut: a) baris 1, 19 : parameter interpolasi v0.z dikalikan dengan cb4 [0] .x dari buffer konstan, tetapi produk ini hanya digunakan untuk interpolasi albedo pada baris 19. Untuk output lain, nilai "normal" dari v0.z digunakan. b) baris 54-55 : o2.w sekarang diatur dalam kondisi bahwa (cb4 [7] .x> 0,0) Kita sudah mengenali pola ini "semacam perbandingan - DAN" dari perhitungan histogram kecerahan. Dapat ditulis seperti ini: c) baris 34-42 : perhitungan specular yang sama sekali berbeda.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 4
0: mul r0.x, v0.z, cb4[0].x
1: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, v1.xyxx, t1.xyzw, s0
2: sample_indexable(texture2d)(float,float,float,float) r0.yzw, v1.xyxx, t0.wxyz, s0
3: add r2.x, r0.z, r0.y
4: add r2.x, r0.w, r2.x
5: add r2.z, l(-1.000000), cb4[2].x
6: mul r2.yz, r2.xxzx, l(0.000000, 0.333300, 0.500000, 0.000000)
7: mov_sat r2.w, r2.z
8: mad r2.x, r2.x, l(-0.666600), l(1.000000)
9: mad r2.x, r2.w, r2.x, r2.y
10: mul r3.xyz, r0.yzwy, cb4[1].xyzx
11: mul_sat r3.xyz, r3.xyzx, l(1.500000, 1.500000, 1.500000, 0.000000)
12: mul_sat r2.x, abs(r2.z), r2.x
13: add r2.yzw, -r0.yyzw, r3.xxyz
14: mad r0.yzw, r2.xxxx, r2.yyzw, r0.yyzw
15: max r2.x, r0.w, r0.z
16: max r2.x, r0.y, r2.x
17: lt r2.x, l(0.220000), r2.x
18: movc r2.x, r2.x, l(-0.300000), l(-0.150000)
19: mad r0.x, r0.x, r2.x, l(1.000000)
20: mul o0.xyz, r0.xxxx, r0.yzwy
21: add r0.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r0.xyw, v3.xyxz, r0.xxxx, r1.xyxz
27: mad r0.xyz, v2.xyzx, r0.zzzz, r0.xywx
28: uge r0.w, l(0), v4.x
29: if_nz r0.w
30: dp3 r0.w, v2.xyzx, r0.xyzx
31: mul r1.xyz, r0.wwww, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
43: max r0.w, r1.z, r1.y
44: max r0.w, r0.w, r1.x
45: lt r0.w, l(0.200000), r0.w
46: movc r2.xyz, r0.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
47: add r2.xyz, -r1.xyzx, r2.xyzx
48: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
49: lt r0.w, r1.w, l(0.330000)
50: mul r1.x, r1.w, l(0.950000)
51: movc r0.w, r0.w, r1.x, l(0.330000)
52: add r0.w, -r1.w, r0.w
53: mad o1.w, v0.z, r0.w, r1.w
54: lt r0.w, l(0), cb4[7].x
55: and o2.w, r0.w, l(0.064706)
56: dp3 r0.w, r0.xyzx, r0.xyzx
57: rsq r0.w, r0.w
58: mul r0.xyz, r0.wwww, r0.xyzx
59: max r0.w, abs(r0.y), abs(r0.x)
60: max r0.w, r0.w, abs(r0.z)
61: lt r1.xy, abs(r0.zyzz), r0.wwww
62: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
63: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
64: lt r1.z, r1.y, r1.x
65: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
66: div r1.z, r1.y, r1.x
67: div r0.xyz, r0.xyzx, r0.wwww
68: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
69: mul r0.xyz, r0.wwww, r0.xyzx
70: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
71: mov o0.w, cb4[6].x
72: ret










pout.RT2.w = (cb4_v7.x > 0.0) ? (16.5/255.0) : 0.0;



Tidak ada tekstur specular di sini. Mari kita lihat kode assembler yang bertanggung jawab untuk bagian ini: Perhatikan bahwa kita digunakan di sini (1 - kemampuan tercermin). Untungnya, menulis ini di HLSL cukup sederhana: Saya akan menambahkan bahwa dalam versi ini buffer konstan dengan data material sedikit lebih besar. Di sini, nilai-nilai tambahan ini digunakan untuk meniru warna specular. Sisa shader sama dengan di versi sebelumnya. 72 baris kode assembler terlalu banyak untuk ditampilkan di WinMerge, jadi terima kata-kata saya untuk itu: kode saya ternyata hampir sama dengan yang asli. Atau Anda dapat mengunduh HLSLexplorer saya dan lihat sendiri!

34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx




float oneMinusReflectivity = 1.0 - normalTex.a;
float3 specularTex = pow(cb4_v3.rgb, 2.2);
oneMinusReflectivity = oneMinusReflectivity * cb4_v4.x + cb4_v5.x;
specularTex = saturate(specularTex * oneMinusReflectivity);
specularTex = pow(specularTex, 1.0/2.2);

// ...
float specularMaxComponent = getMaxComponent( specularTex );
...








Untuk meringkas


... dan jika Anda membacanya di sini, maka Anda mungkin ingin masuk lebih dalam.

Apa yang tampak sederhana dalam kehidupan nyata seringkali tidak demikian, dan transfer data ke gbuffer The Witcher 3 tidak terkecuali. Saya menunjukkan kepada Anda hanya versi paling sederhana dari pixel shader yang bertanggung jawab untuknya, dan juga memberikan pengamatan umum yang berhubungan dengan shading yang ditangguhkan secara umum.

Untuk sebagian besar pasien, ada dua opsi untuk pixel shaders di pastebin:

Opsi 1 - dengan tekstur specular

Opsi 2 - tanpa tekstur specular

Bagian 10. Tirai hujan di kejauhan


Pada bagian ini kita akan melihat efek atmosfer yang indah yang sangat saya sukai - tirai hujan / cahaya jauh di dekat cakrawala. Dalam permainan, mereka paling mudah untuk bertemu di Kepulauan Skellig.


Secara pribadi, saya sangat menyukai fenomena atmosfer ini dan ingin tahu bagaimana programer grafis CD Projekt Red menerapkannya. Mari kita cari tahu!

Berikut adalah dua tangkapan layar sebelum dan sesudah menerapkan tirai hujan:


Ke tirai hujan


Setelah tirai hujan

Geometri


Pertama, kita akan fokus pada geometri. Idenya adalah menggunakan silinder kecil:


Silinder di ruang lokal

Dari sudut pandang posisinya di ruang lokal, cukup kecil - posisinya ada di kisaran (0,0 - 1,0).

Sirkuit input untuk panggilan undian ini terlihat seperti ini ...


Berikut ini penting bagi kami di sini: Texcoords dan Instance_Transform.

Texcoords dibungkus cukup sederhana: U dari pangkalan atas dan bawah berada dalam interval [0,02777 - 1,02734]. V pada basis yang lebih rendah adalah 1.0, dan di atas - 0.0. Seperti yang Anda lihat, Anda dapat dengan mudah membuat mesh ini bahkan secara prosedural.

Setelah menerima silinder kecil ini di ruang lokal, kami mengalikannya dengan matriks dunia yang disediakan untuk setiap instance elemen input INSTANCE_TRANSFORM. Mari kita periksa nilai dari matriks ini:




Terlihat sangat menakutkan, bukan? Tapi jangan khawatir, kami akan menganalisis matriks ini dan melihat apa yang disembunyikannya! Hasilnya sangat menarik: Penting untuk mengetahui posisi kamera dalam bingkai khusus ini: (-116.5338, 234.8695, 2.09) Seperti yang Anda lihat, kami menskalakan silinder untuk membuatnya cukup besar di ruang dunia (di TW3 sumbu Z naik), memindahkannya relatif ke posisi kamera , dan berbalik. Beginilah bentuk silinder setelah konversi dengan vertex shader:

XMMATRIX mat( -227.7472, 159.8043, 374.0736, -116.4951,
-194.7577, -173.3836, -494.4982, 238.6908,
-14.16466, -185.4743, 784.564, -1.45565,
0.0, 0.0, 0.0, 1.0 );

mat = XMMatrixTranspose( mat );

XMVECTOR vScale;
XMVECTOR vRotateQuat;
XMVECTOR vTranslation;
XMMatrixDecompose( &vScale, &vRotateQuat, &vTranslation, mat );

// ...
XMMATRIX matRotate = XMMatrixRotationQuaternion( vRotateQuat );




vRotateQuat: (0.0924987569, -0.314900011, 0.883411944, -0.334462732)

vScale: (299.999969, 300.000000, 1000.00012)

vTranslation: (-116.495102, 238.690796, -1.45564997)









Silinder setelah konversi oleh vertex shader. Lihat bagaimana letaknya relatif terhadap piramida visibilitas.

Vertex shader


Input geometri dan vertex shader sangat tergantung satu sama lain.

Mari kita lihat lebih dekat kode assembler untuk vertex shader: Seiring dengan Texcoords (garis 0) yang lewat sederhana dan Instance_LOD_Params (baris 8), dua elemen lagi diperlukan untuk output: SV_Position (ini jelas) dan Tinggi (komponen .z) dari posisi di dunia. Ingat bahwa ruang lokal berada dalam kisaran [0-1]? Jadi, tepat sebelum menerapkan matriks dunia, shader vertex menggunakan skala dan deviasi untuk mengubah posisi lokal. Langkah cerdas! Dalam hal ini, skala = float3 (4, 4, 2), dan bias = float3 (-2, -2, -1). < Pola yang terlihat antara garis 9 dan 28 adalah perkalian dari dua matriks utama-baris. Mari kita lihat vertex shader yang sudah jadi pada HLSL:

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












cbuffer cbPerFrame : register (b1)
{
row_major float4x4 g_viewProjMatrix;
row_major float4x4 g_rainShaftsViewProjMatrix;
}

cbuffer cbPerObject : register (b2)
{
float4x4 g_mtxWorld;
float4 g_modelScale;
float4 g_modelBias;
}

struct VS_INPUT
{
float3 PositionW : POSITION;
float2 Texcoord : TEXCOORD;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float4 InstanceTransform0 : INSTANCE_TRANSFORM0;
float4 InstanceTransform1 : INSTANCE_TRANSFORM1;
float4 InstanceTransform2 : INSTANCE_TRANSFORM2;
float4 InstanceLODParams : INSTANCE_LOD_PARAMS;
};

struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;

float4 LODParams : LODParams;
float4 PositionH : SV_Position;
};

VS_OUTPUT RainShaftsVS( VS_INPUT Input )
{
VS_OUTPUT Output = (VS_OUTPUT)0;

//
Output.TexcoordAndZ.xy = Input.Texcoord;
Output.LODParams = Input.InstanceLODParams;

//
float3 meshScale = g_modelScale.xyz; // float3( 4, 4, 2 );
float3 meshBias = g_modelBias.xyz; // float3( -2, -2, -1 );
float3 PositionL = Input.PositionW * meshScale + meshBias;

// instanceWorld float4s:
float4x4 matInstanceWorld = float4x4(Input.InstanceTransform0, Input.InstanceTransform1,
Input.InstanceTransform2 , float4(0, 0, 0, 1) );

// (.z)
float4x4 matWorldInstanceLod = mul( g_rainShaftsViewProjMatrix, matInstanceWorld );
Output.TexcoordAndZ.z = mul( float4(PositionL, 1.0), transpose(matWorldInstanceLod) ).z;

// SV_Posiiton
float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );
Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );

return Output;
}


Perbandingan shader saya (kiri) dan asli (kanan):


Perbedaan tidak memengaruhi perhitungan. Saya menyuntikkan shader saya ke bingkai dan semuanya masih baik-baik saja!

Pixel shader


Akhirnya!Untuk memulai, saya akan menunjukkan kepada Anda input:

Dua tekstur digunakan di sini: tekstur noise dan buffer kedalaman:



Nilai dari buffer konstan:





Dan kode perakitan pixel shader: Wow! Jumlah yang cukup besar, tetapi pada kenyataannya, semuanya tidak terlalu buruk. Apa yang sedang terjadi di sini? Pertama, kami menghitung animasi UV menggunakan waktu yang telah berlalu dari cbuffer (cb0 [0] .x) dan skala / offset. Texcoords ini digunakan untuk mengambil sampel dari tekstur noise (baris 2). Setelah menerima nilai noise dari tekstur, kami menginterpolasi antara nilai min / maks (biasanya 0 dan 1). Kemudian kita melakukan penggandaan, misalnya, dengan koordinat tekstur V (ingat bahwa koordinat V berubah dari 1 menjadi 0?) - baris 5. Jadi, kami menghitung "topeng kecerahan" - tampilannya seperti ini:

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[8], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s15, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t15
dcl_input_ps linear v0.xyz
dcl_input_ps linear v1.w
dcl_input_ps_siv v2.xy, position
dcl_output o0.xyzw
dcl_temps 1
0: mul r0.xy, cb0[0].xxxx, cb4[5].xyxx
1: mad r0.xy, v0.xyxx, cb4[4].xyxx, r0.xyxx
2: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t0.xyzw, s0
3: add r0.y, -cb4[2].x, cb4[3].x
4: mad_sat r0.x, r0.x, r0.y, cb4[2].x
5: mul r0.x, r0.x, v0.y
6: mul r0.x, r0.x, v1.w
7: mul r0.x, r0.x, cb4[1].x
8: mul r0.yz, v2.xxyx, cb0[1].zzwz
9: sample_l(texture2d)(float,float,float,float) r0.y, r0.yzyy, t15.yxzw, s15, l(0)
10: mad r0.y, r0.y, cb12[22].x, cb12[22].y
11: mad r0.y, r0.y, cb12[21].x, cb12[21].y
12: max r0.y, r0.y, l(0.000100)
13: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
14: add r0.y, r0.y, -v0.z
15: mul_sat r0.y, r0.y, cb4[6].x
16: mul_sat r0.x, r0.y, r0.x
17: mad r0.y, cb0[7].y, r0.x, -r0.x
18: mad r0.x, cb4[7].x, r0.y, r0.x
19: mul r0.xyz, r0.xxxx, cb4[0].xyzx
20: log r0.xyz, r0.xyzx
21: mul r0.xyz, r0.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
22: exp r0.xyz, r0.xyzx
23: mul r0.xyz, r0.xyzx, cb2[2].xyzx
24: mul o0.xyz, r0.xyzx, cb2[2].wwww
25: mov o0.w, l(0)
26: ret













Perhatikan bahwa objek yang jauh (mercusuar, pegunungan ...) telah menghilang. Ini terjadi karena silinder melewati uji kedalaman - silinder tidak berada di bidang jauh dan ditarik di atas benda-benda ini:


Tes Kedalaman

Kami ingin mensimulasikan bahwa tirai hujan lebih jauh (tetapi tidak harus di pesawat jauh). Untuk melakukan ini, kita menghitung topeng lain, "topeng benda yang jauh."

Itu dihitung dengan rumus berikut:

farObjectsMask = saturate( (FrustumDepth - CylinderWorldSpaceHeight) * 0.001 );

(0,001 diambil dari buffer), yang memberi kita topeng yang diinginkan:


(Pada bagian tentang efek Sharpen, saya sudah secara dangkal menjelaskan bagaimana kedalaman piramida visibilitas diekstraksi dari buffer kedalaman.)

Secara pribadi, menurut saya efek ini dapat direalisasikan lebih murah tanpa menghitung ketinggian di ruang dunia dengan mengalikan kedalaman piramida visibilitas dengan jumlah yang lebih kecil, misalnya 0,0004.

Ketika kedua topeng dikalikan, yang terakhir diperoleh:


Setelah menerima topeng terakhir ini (baris 16), kami melakukan interpolasi lain, yang hampir tidak menghasilkan apa-apa (setidaknya dalam kasus yang diuji), dan kemudian mengalikan topeng akhir dengan warna tirai (baris 19), melakukan koreksi gamma (baris 20). -22) dan perkalian terakhir (23-24).

Pada akhirnya, kami mengembalikan warna dengan nilai alpha nol. Ini karena pencampuran diaktifkan pada lintasan ini:

FinalColor = SourceColor * 1.0 + (1.0 - SourceAlpha) * DestColor

Jika Anda tidak begitu memahami cara kerja pencampuran, berikut adalah penjelasan singkat:

SourceColor adalah output RGB dari pixel shader, dan DestColor adalah warna RGB saat ini dari piksel dalam target render. . Sejak SourceAlpha selalu sama untuk 0.0, persamaan di atas tereduksi menjadi: FinalColor = SourceColor + DestColor.

Sederhananya, di sini kami melakukan pencampuran aditif. Jika pixel shader kembali (0, 0, 0), maka warnanya akan tetap sama.

Berikut ini adalah kode HLSL yang telah selesai - Saya pikir setelah menjelaskannya akan jauh lebih mudah untuk dipahami: Saya dapat mengatakan dengan senang hati bahwa pixel shader saya membuat kode assembler yang sama seperti pada aslinya. Saya harap Anda menikmati artikel ini. Terima kasih sudah membaca!

struct VS_OUTPUT
{
float3 TexcoordAndWorldspaceHeight : Texcoord0;
float4 LODParams : LODParams; // float4(1,1,1,1)
float4 PositionH : SV_Position;
};

float getFrustumDepth( in float depth )
{
// from [1-0] to [0-1]
float d = depth * cb12_v22.x + cb12_v22.y;

// special coefficents
d = d * cb12_v21.x + cb12_v21.y;

// return frustum depth
return 1.0 / max(d, 1e-4);
}

float4 EditedShaderPS( in VS_OUTPUT Input ) : SV_Target0
{
// * Input from Vertex Shader
float2 InputUV = Input.TexcoordAndWorldspaceHeight.xy;
float WorldHeight = Input.TexcoordAndWorldspaceHeight.z;
float LODParam = Input.LODParams.w;

// * Inputs
float elapsedTime = cb0_v0.x;
float2 uvAnimation = cb4_v5.xy;
float2 uvScale = cb4_v4.xy;
float minValue = cb4_v2.x; // 0.0
float maxValue = cb4_v3.x; // 1.0
float3 shaftsColor = cb4_v0.rgb; // RGB( 147, 162, 173 )

float3 finalColorFilter = cb2_v2.rgb; // float3( 1.175, 1.296, 1.342 );
float finalEffectIntensity = cb2_v2.w;

float2 invViewportSize = cb0_v1.zw;

float depthScale = cb4_v6.x; // 0.001

// sample noise
float2 uvOffsets = elapsedTime * uvAnimation;
float2 uv = InputUV * uvScale + uvOffsets;
float disturb = texture0.Sample( sampler0, uv ).x;

// * Intensity mask
float intensity = saturate( lerp(minValue, maxValue, disturb) );
intensity *= InputUV.y; // transition from (0, 1)
intensity *= LODParam; // usually 1.0
intensity *= cb4_v1.x; // 1.0

// Sample depth
float2 ScreenUV = Input.PositionH.xy * invViewportSize;
float hardwareDepth = texture15.SampleLevel( sampler15, ScreenUV, 0 ).x;
float frustumDepth = getFrustumDepth( hardwareDepth );


// * Calculate mask covering distant objects behind cylinder.

// Seems that the input really is world-space height (.z component, see vertex shader)
float depth = frustumDepth - WorldHeight;
float distantObjectsMask = saturate( depth * depthScale );

// * calculate final mask
float finalEffectMask = saturate( intensity * distantObjectsMask );

// cb0_v7.y and cb4_v7.x are set to 1.0 so I didn't bother with naming them :)
float paramX = finalEffectMask;
float paramY = cb0_v7.y * finalEffectMask;
float effectAmount = lerp(paramX, paramY, cb4_v7.x);

// color of shafts comes from contant buffer
float3 effectColor = effectAmount * shaftsColor;

// gamma correction
effectColor = pow(effectColor, 2.2);

// final multiplications
effectColor *= finalColorFilter;
effectColor *= finalEffectIntensity;

// return with zero alpha 'cause the blending used here is:
// SourceColor * 1.0 + (1.0 - SrcAlpha) * DestColor
return float4( effectColor, 0.0 );
}




Source: https://habr.com/ru/post/id437100/


All Articles