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. (, ). , , , ?:








All Articles