learnopengl。 レッスン1.5-シェヌダヌ

前のレッスンでシェヌダヌに぀いお既に蚀及したした。 シェヌダヌは、グラフィックアクセラレヌタで実行される小さなプログラムです以降、より䞀般的な名前-GPUを䜿甚したす。 これらのプログラムは、グラフィックスパむプラむンの特定のセクションごずに実行されたす。 最も単玔な方法でシェヌダヌを蚘述する堎合、シェヌダヌは入力を出力に倉換するプログラムにすぎたせん。 シェヌダヌは通垞互いに分離されおおり、䞊蚘の入力ず出力を陀いお、シェヌダヌは盞互に通信メカニズムを持ちたせん。


前のレッスンでは、「サヌフェスシェヌダヌ」のトピックずその䜿甚方法に぀いお簡単に觊れたした。 このレッスンでは、シェヌダヌ、特にOpenGLシェヌダヌ蚀語OpenGLシェヌディング蚀語に぀いお詳しく芋おいきたす。



メニュヌ


  1. はじめに
    1. Opengl
    2. りィンドり䜜成
    3. こんにちはりィンドり
    4. こんにちはトラむアングル
    5. シェヌダヌ

GLSL


シェヌダヌ䞊蚘のように、シェヌダヌはプログラムですは、GLSL蚀語のようなCでプログラムされおいたす。 グラフィックスでの䜿甚に適合しおおり、ベクトルおよび行列を操䜜するための機胜を提䟛したす。


シェヌダヌの説明は、そのバヌゞョンの衚瀺で始たり、入力および出力倉数、グロヌバル倉数統䞀キヌワヌド、メむン関数のリストが続きたす。 関数mainは、シェヌダヌの開始点です。 この関数内では、入力デヌタを操䜜できたす。シェヌダヌの結果は出力倉数に配眮されたす。 統䞀されたキヌワヌドに泚意を払わないでください、埌でそれに戻りたす。


以䞋は、䞀般化されたシェヌダヌ構造です。


#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; void main() { // - , , ,  .. ... //       out_variable_name = weird_stuff_we_processed; } 

頂点シェヌダヌの入力倉数は、頂点属性ず呌ばれたす。 シェヌダヌに転送できる頂点の最倧数がありたす。そのような制限は、制限されたハヌドりェア機胜によっお課せられたす。 OpenGLは、少なくずも16 4個のコンポヌネント頂点を転送する機胜を保蚌したす。぀たり、少なくずも64個の倀をシェヌダヌに転送できたす。 ただし、この氎準を倧幅に匕き䞊げるコンピュヌティングデバむスがあるこずを考慮する䟡倀がありたす。 GL_MAX_VERTEX_ATTRIBS属性を調べるこずで、シェヌダヌに枡される入力頂点倉数の最倧数を芋぀けるこずができたす。


 GLint nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl; 

このコヌドを実行した結果、Figure> = 16が衚瀺されたす。


皮類


GLSLは、他のプログラミング蚀語ず同様に、倉数タむプの特定のリストを提䟛したす;これらには、int、float、double、uint、boolずいうプリミティブタむプが含たれたす。 GLSLは、ベクタヌずマトリックスの2぀のコンテナヌタむプも提䟛したす。


ベクトル


GLSLのベクタヌは、1〜4個の任意のプリミティブ型の倀を含むコンテナヌです。 コンテナのベクトル宣蚀は次のようになりたすnはベクトル芁玠の数です


vecnvec4などは、float型のn個の倀を含む暙準ベクトルです。
bvecnbvec4などは、ブヌル型のn個の倀を含むベクトルです
ivecn䟋えば、ivec4は、n個の敎数倀を含むベクトルです。
uvecnたずえば、uvecnは、笊号なし敎数型のn個の倀を含むベクトルです。
dvecndvecnなどは、n個のdouble倀を含むベクトルです。


ほずんどの堎合、暙準ベクトルvecnが䜿甚されたす。


ベクタヌコンテナの芁玠にアクセスするには、次の構文vec.x、vec.y、vec.z、vec.wを䜿甚したすこの堎合、最初から最埌たですべおの芁玠を順番に凊理したした。 ベクトルが色を衚す堎合はRGBAを繰り返し、ベクトルがテクスチャの座暙を衚す堎合はstpqを繰り返すこずもできたす。


PS XYZW、RGBA、STPQを介した1぀のベクタヌぞのアクセスが蚱可されたす。 このメモを行動の指針ずする必芁はありたせん。

PPS䞀般に、むンデックスずむンデックス[]によるアクセス挔算子を䜿甚しお、ベクトルの繰り返しを犁止する人はいたせん。 https://en.wikibooks.org/wiki/GLSL_Programming/Vector_and_Matrix_Operations#Components

ベクトルから、ポむントを介しおデヌタにアクセスする堎合、1぀の倀だけでなく、 スりィズリングず呌ばれる次の構文を䜿甚しおベクトル党䜓を取埗できたす。


 vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy; 

新しいベクトルを䜜成するには、同じタむプたたはベクトルのリテラルを最倧4぀䜿甚できたす。ルヌルは1぀だけです。たずえば、必芁な芁玠の数を取埗する必芁がありたす。 3぀の芁玠ず1぀のリテラル。 たた、n個の芁玠のベクトルを䜜成するには、単䞀の倀を䜿甚できたす;この堎合、ベクトルのすべおの芁玠がこの倀を取りたす。 プリミティブ型の倉数の䜿甚も蚱可されおいたす。


 vec2 vect = vec2(0.5f, 0.7f); vec4 result = vec4(vect, 0.0f, 0.0f); vec4 otherResult = vec4(result.xyz, 1.0f); 

ベクトルは非垞に柔軟なデヌタ型であり、入力倉数および出力倉数ずしお䜿甚できるこずに気付くかもしれたせん。


むン倉数ずアりト倉数


シェヌダヌは小さなプログラムですが、ほずんどの堎合、それらはより倧きなものの䞀郚であるこずがわかっおいたす。そのため、GLSLには、凊理甚のデヌタを受け取っお呌び出し元に結果を枡すこずができる「シェヌダヌ」むンタヌフェむスを䜜成できる入出力倉数がありたす。 したがっお、各シェヌダヌは、キヌワヌドinおよびoutを䜿甚しお、入力倉数ず出力倉数を自身で決定できたす。


頂点シェヌダヌは、入力を受け入れなかった堎合、非垞に効率が悪くなりたす。 このシェヌダヌ自䜓は、頂点デヌタから盎接入力倀を取埗するずいう点で他のシェヌダヌずは異なりたす。 匕数の線成方法をOpenGLに䌝えるために、䜍眮メタデヌタを䜿甚しお、CPUの属性を構成できたす。 既にこのトリックを芋たした 'layoutlocation = 0。 頂点シェヌダヌには、匕数を頂点デヌタに接続できるように、远加の仕様が必芁です。


レむアりトlocation = 0を省略し、glGetAttributeLocationの呌び出しを䜿甚しお頂点属性の䜍眮を取埗できたす。

別の䟋倖は、フラグメントシェヌダヌにvec4出力が必芁であるずいうこずです。぀たり、フラグメントシェヌダヌは結果ずしおRGBA圢匏で色を提䟛する必芁がありたす。 これが行われない堎合、オブゞェクトは黒たたは癜で描画されたす。


したがっお、あるシェヌダヌから別のシェヌダヌに情報を転送するタスクに盎面しおいる堎合、発信シェヌダヌの受信シェヌダヌのin倉数ず同じタむプの倉数を定矩する必芁がありたす。 したがっお、倉数のタむプず名前が䞡偎で同じ堎合、OpenGLはこれらの倉数を接続し、シェヌダヌ間で情報を亀換する機䌚を䞎えたすこれはリンク段階で行われたす。 これを実際に瀺すために、頂点シェヌダヌがフラグメントシェヌダヌの色を提䟛するように、前のレッスンのシェヌダヌを倉曎したす。


頂点シェヌダヌ


 #version 330 core layout (location = 0) in vec3 position; //     0 out vec4 vertexColor; //      void main() { gl_Position = vec4(position, 1.0); //   vec3  vec4 vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); //      - . } 

フラグメントシェヌダヌ


 #version 330 core in vec4 vertexColor; //      (      ) out vec4 color; void main() { color = vertexColor; } 

これらの䟋では、頂点シェヌダヌでvertexColorずいう名前の4぀の芁玠の出力ベクトルず、vertexColorずいう同じベクトルを宣蚀したしたが、これはフラグメントシェヌダヌの入力ずしおのみです。 その結果、頂点シェヌダヌからの出力バヌテックスカラヌずフラグメントシェヌダヌからの入力バヌテックスカラヌが接続されたした。 なぜなら 頂点シェヌダヌのvertexColor倀を䞍透明なバヌガンディ濃い赀に蚭定し、シェヌダヌをオブゞェクトに適甚するず、バヌガンディ色になりたす。 次の図に結果を瀺したす。


結果


以䞊です。 頂点シェヌダヌからの倀がフラグメントシェヌダヌによっお取埗されるようにしたした。 さらに、アプリケヌションからシェヌダヌに情報を転送する方法を怜蚎したす。


制服


ナニフォヌムフォヌムず呌びたすは、CPUで実行されおいるアプリケヌションからGPUで実行されおいるシェヌダヌに情報を転送する別の方法です。 圢状は、頂点属性ずはわずかに異なりたす。 たず、フォヌムはグロヌバルです。 GLSLのグロヌバル倉数ずは、次のこずを意味したす。グロヌバル倉数は各シェヌダヌプログラムに察しお䞀意であり、すべおのシェヌダヌはこのプログラムのどの段階でもそれにアクセスできたす。 2番目フォヌムの倀は、リセットたたは曎新されるたで保持されたす。


GLSLは、unifrom倉数指定子を䜿甚しおフォヌムを宣蚀したす。 フォヌムが宣蚀された埌、シェヌダヌで䜿甚できたす。 シェむプを䜿甚しお䞉角圢の色を蚭定する方法を芋おみたしょう。


 #version 330 core out vec4 color; uniform vec4 ourColor; //        OpenGL. void main() { color = ourColor; } 

フラグメントシェヌダヌで4芁玠ベクトル型のoutColorフォヌム倉数を宣蚀し、それを䜿甚しおフラグメントシェヌダヌの出力倀を蚭定したした。 なぜなら フォヌムはグロヌバル倉数なので、その宣蚀はどのシェヌダヌでも実行できたす。぀たり、頂点シェヌダヌからフラグメントシェヌダヌに䜕かを転送する必芁はありたせん。 したがっお、頂点シェヌダヌでフォヌムを宣蚀したせん。なぜなら、 そこでは䜿甚したせん。


フォヌムを宣蚀しおシェヌダヌプログラムで䜿甚しない堎合、コンパむラヌはそれをサむレントに削陀するため、゚ラヌが発生する可胜性があるため、この情報に留意しおください。


珟時点では、フォヌムには有甚なデヌタが含たれおいたせん。 そこに入れなかったので、やりたしょう。 たず、シェヌダヌで必芁なフォヌム属性のむンデックス、぀たり堎所を芋぀ける必芁がありたす。 属性むンデックスの倀を受け取ったら、そこに必芁なデヌタを収容できたす。 この関数の機胜を明確に瀺すために、色を時々倉曎したす。


 GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); 

たず、glfwGetTimeを呌び出しおランタむムを数秒で取埗したす。 その埌、sin関数を䜿甚しお倀を0.0から1.0に倉曎し、結果をgreenValue倉数に曞き蟌みたす。


glGetUniformLocationを䜿甚しおourColorフォヌムのむンデックスを芁求した埌。 この関数は2぀の匕数を取りたす。シェヌダヌプログラムの倉数ず、このプログラム内で定矩されたフォヌムの名前です。 glGetUniformLocationが-1を返した堎合、同じ名前のフォヌムが芋぀からなかったこずを意味したす。 最埌のステップは、glUniform4f関数を䜿甚しおourColorフォヌムの倀を蚭定するこずです。 フォヌムむンデックスの怜玢には、事前にglUseProgramを呌び出す必芁はありたせんが、フォヌムの倀を曎新するには、最初にglUseProgramを呌び出す必芁がありたす。


OpenGLは関数のオヌバヌロヌドがないC蚀語を䜿甚しお実装されおいるため、異なる匕数で関数を呌び出すこずはできたせんが、OpenGLは関数の接尟蟞によっお決定される各デヌタ型の関数を定矩したす。 以䞋はいく぀かの接尟蟞です


f関数はfloat匕数を取りたす。
i関数はint匕数を取りたす。
ui関数はunsigned int匕数を取りたす。
3f関数は、float型の3぀の匕数を取りたす。
fv関数は、匕数ずしおfloatからベクトルを受け取りたす。


したがっお、オヌバヌロヌドされた関数を䜿甚する代わりに、関数の接尟蟞で瀺されるように、実装が特定の匕数セット甚に蚭蚈された関数を䜿甚する必芁がありたす。 䞊蚘の䟋では、float型の4぀の匕数の凊理に特化した関数glUniform ...を䜿甚したため、関数の完党な名前はglUniform4f4f-float型の4぀の匕数でした。


フォヌムに倀を蚭定する方法がわかったので、レンダリングプロセスでそれらを䜿甚できたす。 時間の経過ずずもに色を倉曎したい堎合は、レンダリングサむクルの反埩ごずに圢状倀を曎新する必芁がありたす蚀い換えれば、各フレヌムで色が倉曎されたす。そうでない堎合、色を1回だけ蚭定するず䞉角圢は同じ色になりたす。 以䞋の䟋では、䞉角圢の新しい色が蚈算され、レンダリングサむクルの各反埩で曎新されたす。


 while(!glfwWindowShouldClose(window)) { //   glfwPollEvents(); //  //    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); //    glUseProgram(shaderProgram); //    GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); //   glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); } 

コヌドは以前䜿甚したものず非垞に䌌おいたすが、ルヌプ内で実行し、各反埩でフォヌムの倀を倉曎したす。 すべおが正しければ、䞉角圢の色が緑から黒に、たたはその逆に倉化するこずがわかりたすはっきりしない堎合は、正匊波の画像を芋぀けたす。



このような奇跡を起こすプログラムの完党な゜ヌスコヌドはここにありたす 。


既にお気付きのように、フォヌムはシェヌダヌずプログラムの間でデヌタを亀換するための非垞に䟿利な方法です。 しかし、各頂点に色を蚭定したい堎合はどうでしょうか これを行うには、頂点の数だけ図圢を宣蚀する必芁がありたす。 最も成功する解決策は、頂点属性を䜿甚するこずです。これを次に瀺したす。


属性神ぞのより倚くの属性!!!


前のレッスンでは、VBOにデヌタを入力する方法、頂点属性ぞのポむンタヌを構成する方法、およびすべおをVAOに保存する方法を芋たした。 次に、頂点デヌタに色情報を远加する必芁がありたす。 これを行うには、3぀のfloat芁玠のベクトルを䜜成したす。 䞉角圢の各頂点にそれぞれ赀、緑、青の色を割り圓おたす。


 GLfloat vertices[] = { //  //  0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, //    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, //    0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f //   }; 

これで、頂点シェヌダヌに送信する倚くの情報が埗られたので、頂点ず色の䞡方を受け取るようにシェヌダヌを線集する必芁がありたす。 色の堎所を1に蚭定しおいるこずに泚意しおください。


 #version 330 core layout (location = 0) in vec3 position; //       0 layout (location = 1) in vec3 color; //       1 out vec3 ourColor; //      void main() { gl_Position = vec4(position, 1.0); ourColor = color; //   ,     } 

ourColorフォヌムは䞍芁になりたしたが、出力パラメヌタヌourColorは倀をフラグメントシェヌダヌに枡すのに圹立ちたす。


 #version 330 core in vec3 ourColor; out vec4 color; void main() { color = vec4(ourColor, 1.0f); } 

なぜなら 新しい頂点パラメヌタヌを远加し、VBOメモリを曎新したため、頂点属性ポむンタヌを構成する必芁がありたす。 VBOメモリ内の曎新されたデヌタは次のずおりです。


Vboデヌタ


珟圚のスキヌムがわかっおいるので 、 glVertexAttribPointer関数を䜿甚しお頂点圢匏を曎新できたす。


 //    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0); //    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat))); glEnableVertexAttribArray(1); 

glVertexAttribPointer関数の最初のいく぀かの属性は非垞に単玔です。 この䟋では、䜍眮1の頂点属性を䜿甚したす。色はフリヌトタむプの3぀の倀で構成されおおり、正芏化は必芁ありたせん。


なぜなら ここで、シェヌダヌの2぀の属性を䜿甚し、ステップを再蚈算する必芁がありたす。 次のシェヌダヌアトリビュヌト次のx頂点ベクトルにアクセスするには、6個のフロヌト芁玠を右に移動する必芁がありたす。3個は頂点ベクトル、3個は色ベクトルです。 ぀たり 6回右に移動したす。 右偎に24バむト。


次に、シフトを把握したした。 最初は、頂点座暙を持぀ベクトルです。 RGBカラヌ倀を持぀ベクトルは、座暙を持぀ベクトルの埌にありたす。 3 * sizeofGLfloat= 12バむトの埌。


プログラムを実行するず、次の結果が衚瀺されたす。


結果


この奇跡を生み出す完党な゜ヌスコヌドはここにありたす 


結果ずしお衚瀺されるパレットではなく3色のみを蚭定しおいるため、結果は完了した䜜業に察応しおいないように芋える堎合がありたす。 この結果は、フラグメントシェヌダヌのフラグメント補間によっお取埗されたす。 ラスタラむズの段階で䞉角圢を描画するず、シェヌダヌ匕数ずしお䜿甚する頂点だけでなく、より倚くの領域が埗られたす。 ラスタラむザは、ポリゎン䞊の䜍眮に基づいおこれらの領域の䜍眮を決定したす。 この䜍眮に基づいお、フラグメントシェヌダヌのすべおの匕数が補間されたす。 片方の端が緑で、もう䞀方の端が青である単玔な線があるずしたす。 フラグメントシェヌダヌがほが䞭倮にある領域を凊理する堎合、この領域の色は、緑色がラむンで䜿甚される色の50に等しくなるように遞択され、したがっお、青色は青色の50に等しくなりたす。 これが䞉角圢で起こるこずです。 3぀の色ず3぀のピヌクがあり、それぞれに色が蚭定されおいたす。 よく芋るず、赀が青に移動するず最初に玫に倉わるこずがわかりたす。 フラグメント補間は、フラグメントシェヌダヌのすべおの属性に適甚されたす。


倧衆ぞのOOP シェヌダヌクラスを䜜成する


シェヌダヌをコンパむルしお、シェヌダヌを構成できるようにするコヌドは、非垞に面倒です。 したがっお、ディスクからシェヌダヌを読み取り、コンパむルし、リンクし、゚ラヌをチェックし、そしおもちろん、シンプルで玠晎らしいむンタヌフェヌスを持぀クラスを曞くこずで、私たちの生掻を少し楜にしたしょう。 このようにしお、OOPはクラスのメ゜ッド内にこのカオスをすべおカプセル化するのに圹立ちたす。


むンタヌフェむスの宣蚀からクラスの開発を開始し、新しく䜜成された芋出しに必芁なすべおのincludeディレクティブを蚘述したす。 結果は次のようになりたす。


 #ifndef SHADER_H #define SHADER_H #include <string> #include <fstream> #include <sstream> #include <iostream> #include <GL/glew.h>; //  glew  ,       OpenGL class Shader { public: //   GLuint Program; //      Shader(const GLchar* vertexPath, const GLchar* fragmentPath); //   void Use(); }; #endif 

さあ、ifndefを䜿甚しおディレクティブを定矩し、includeディレクティブの再垰的な実行を避けたしょう。 このヒントはOpenGLではなく、䞀般的なC ++プログラミングに適甚されたす。


そしお、クラスはその識別子を保存したす。 シェヌダヌコンストラクタヌは、プレヌンテキストで衚される頂点シェヌダヌずフラグメントシェヌダヌを含むファむルぞのパスを含む文字の配列぀たり、テキスト、およびクラスのコンテキストでは、シェヌダヌの゜ヌスコヌドを持぀ファむルぞのパスを蚀う方が適切ですの匕数ずしお匕数を取りたす。 たた、シェヌダヌクラスを䜿甚する利点を明確に瀺すナヌティリティ関数Useを远加したす。


シェヌダヌファむルを読み取りたす。 読み取りには、暙準のC ++ストリヌムを䜿甚しお、結果を次の行に入れたす。


 Shader(const GLchar* vertexPath, const GLchar* fragmentPath) { // 1.      filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ,  ifstream     vShaderFile.exceptions(std::ifstream::badbit); fShaderFile.exceptions(std::ifstream::badbit); try { //   vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; //     vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); //   vShaderFile.close(); fShaderFile.close(); //     GLchar vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const GLchar* vShaderCode = vertexCode.c_str(); const GLchar* fShaderCode = fragmentCode.c_str(); [...] 

( , , . , 
 ):


 // 2.   GLuint vertex, fragment; GLint success; GLchar infoLog[512]; //   vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); //    -   glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; }; //     [...] //   this->Program = glCreateProgram(); glAttachShader(this->Program, vertex); glAttachShader(this->Program, fragment); glLinkProgram(this->Program); //   -   glGetProgramiv(this->Program, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(this->Program, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } //  ,          . glDeleteShader(vertex); glDeleteShader(fragment); 

Use:


 void Use() { glUseProgram(this->Program); } 

:


 Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag"); ... while(...) { ourShader.Use(); glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), 1.0f); DrawStuff(); } 

shader.vs, shader.frag. , , , , .


, ,


:


1. , : .


2. : .


3. (, ). , , , ?:



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


All Articles