上記のこんにちは三角形の章では、シェーダーはプログラムを用いています。 これらのプログラムは、グラフィックスパイプラインの特定のセクションごとに実行されます。 基本的な意味では、シェーダーは入力を出力に変換するプログラムに過ぎません。 シェーダは、互いに通信することが許可されていないという点で非常に孤立したプログラムでもあります。
前の章では、シェーダの表面とそれらを適切に使用する方法に簡単に触れました。 ここでは、シェーダー、特にOpenGLシェーディング言語について、より一般的な方法で説明します。
- GLSL
- Types
- Vectors
- Ins and outs
- Uniforms
- より多くの属性!前の章では、VBOを埋め、頂点属性ポインタを設定し、それをすべてVAOに格納する方法を見ました。 今回は、頂点データにもカラーデータを追加したいと思います。 色データを3floatとして頂点配列に追加します。 三角形の各角に赤、緑、青の色をそれぞれ割り当てます。 float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
- 私たち自身のシェーダクラス
- ファイルからの読み取り
- 演習
GLSL
シェーダはC言語のようなGLSLで書かれています。 GLSLは、グラフィックスでの使用に合わせて調整され、特にベクトルと行列の操作を対象とした便利な機能が含まれています。
シェーダは常にバージョン宣言で始まり、その後に入力変数と出力変数のリスト、ユニフォーム、およびその主な関数が続きます。 各シェーダーのエントリポイントは、入力変数を処理し、その結果を出力変数に出力するmain関数にあります。 あなたは制服が何であるかわからない場合は心配しないでください、我々はすぐにそれらに取得します。
シェーダは、通常、次の構造を持っています。
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
頂点シェーダについて具体的に話しているとき、各入力変数は頂点属性とも呼ばれます。 ハードウェアによって制限された宣言が許可されている頂点属性の最大数があります。 OpenGLでは、少なくとも16個の4コンポーネントの頂点属性が利用可能であることが保証されていますが、一部のハードウェアでは、GL_MAX_VERTEX_ATTRIBSを照会して取得で:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
これは、多くの場合、ほとんどの目的のために十分である必要があります16
の最小値を返します。
Types
GLSLには、他のプログラミング言語と同様に、どのような種類の変数を使用するかを指定するためのデータ型があります。 GLSLには、Cのような言語から知っているデフォルトの基本型のほとんどがあります:int
float
double
uint
bool
。 GLSLには、多く使用する2つのコンテナタイプ、つまりvectors
matrices
もあります。 行列については後の章で説明します。
Vectors
GLSLのベクトルは、前述の基本型のいずれかの1,2,3または4コンポーネントコンテナです。 それらは次の形式を取ることができます(n
はコンポーネントの数を表します)。
vecn
n
浮動小数点数。bvecn
n
ブール値のベクトル。ivecn
n
整数のベクトル。uvecn
n
符号なし整数のベクトル。dvecn
n
vecn
vecn
を使用します。ベクトルのコンポーネントは、
vec.x
x
.x
.y
.z
.w
それぞれの第一、第二、第三および第四のコンポーネ GLSLでは、色にrgba
stpq
を使用して同じコンポーネントにアクセスすることもできます。vectorデータ型は、swizzlingと呼ばれる興味深い柔軟なコンポーネント選択を可能にします。 Swizzlingでは、次のような構文を使用できます: たとえば、
vec2
.z
コンポーネントにアクセスすることはできません。 また、必要な引数の数を減らす、異なるベクターコンストラクタ呼び出しに引数としてベクトルを渡すことができます。vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
ベクトルは、 本を通して、あなたは私たちが創造的にベクトルを管理する方法の例をたくさん見るでしょう。
Ins and outs
シェーダは、それ自体で素敵な小さなプログラムですが、それらは全体の一部であり、そのため、個々のシェーダに入力と出力を持ち、ものを動 GLSLは、その目的のために特別に
in
out
キーワードを定義しました。 各シェーダは、これらのキーワードを使用して入力と出力を指定することができ、出力変数が渡される次のシェーダ段階の入力変数と一致する場合はどこでも 頂点シェーダとフラグメントシェーダは少し異なります。頂点シェーダは何らかの形の入力を受け取る必要があります。 頂点シェーダーは、頂点データから直接入力を受け取るという点で、入力が異なります。 頂点データの編成方法を定義するために、CPU上で頂点属性を構成できるように、位置メタデータを使用して入力変数を指定します。 前の章では、これを
layout (location = 0)
layout (location = 0)
指定子を省略して、glGetAttribLocationを介してOpenGLコードの属性位置を照会することもできますが、頂点シェーダで設定することをお勧めします。 理解しやすく、あなた(およびOpenGL)の作業を節約できます。もう一つの例外は、フラグメントシェーダが最終的な出力カラーを生成する必要があるため、フラグメントシェーダには
vec4
カラー出力変数が必 フラグメントシェーダで出力色を指定しなかった場合、それらのフラグメントのカラーバッファ出力は未定義になります(通常、OpenGLは黒または白のいずしたがって、あるシェーダから別のシェーダにデータを送信する場合は、送信シェーダで出力を宣言し、受信シェーダで同様の入力を宣言する必要があります。 型と名前が両側で等しい場合、OpenGLはこれらの変数をリンクし、シェーダ間でデータを送信することができます(これはプログラムオブジェクトをリンクす これが実際にどのように機能するかを示すために、前の章のシェーダを変更して、頂点シェーダがフラグメントシェーダの色を決定できるようにします。
頂点シェーダー
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
頂点シェーダーで設定した出力としてvertexColor変数を宣言し、同様のvertexColor入力をフラグメントシェーダーで宣言していることがわかります。 両方とも同じ型と名前を持つため、フラグメントシェーダのvertexColorは頂点シェーダのvertexColorにリンクされます。 頂点シェーダーでは色を暗赤色に設定しているため、結果として得られるフラグメントも暗赤色にする必要があります。 次の図は、出力を示しています: p>
そこに行く! 頂点シェーダーからフラグメントシェーダに値を送信することができました。 アプリケーションからフラグメントシェーダに色を送ることができるかどうかを見てみましょう!
Uniforms
Uniformsは、CPU上のアプリケーションからGPU上のシェーダーにデータを渡す別の方法です。 しかし、制服は頂点属性と比較してわずかに異なります。 まず第一に、制服はグローバルです。 つまり、uniform変数はシェーダープログラムオブジェクトごとに一意であり、シェーダープログラムの任意の段階で任意のシェーダーからアクセスできます。 次に、uniform値をどのように設定しても、uniformはリセットまたは更新されるまで値を保持します。
GLSLでuniformを宣言するには、
uniform
キーワードをタイプと名前を持つシェーダーに追加します。 その時点から、シェーダで新しく宣言されたuniformを使用することができます。 今回は、制服を介して三角形の色を設定できるかどうかを見てみましょう:#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
フラグメントシェーダでuniform
vec4
ourColorを宣言し、フラグメントの出力色をこのuniform値の内容に設定しました。 Uniformsはグローバル変数なので、任意のシェーダーステージで定義できるので、フラグメントシェーダに何かを得るために頂点シェーダーを再度通過する必要はありません。 頂点シェーダーでこのユニフォームを使用していないので、そこで定義する必要はありません。GLSLコードのどこにも使用されていないuniformを宣言すると、コンパイラはコンパイルされたバージョンから変数を静かに削除します。 uniformにはまだデータを追加していないので、試してみましょう。 まず、シェーダー内のuniform属性のインデックス/位置を見つける必要があります。 Uniformのインデックス/位置を取得したら、その値を更新できます。 フラグメントシェーダに単一の色を渡すのではなく、時間の経過とともに徐々に色を変更して、物事をスパイスしましょう:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
まず、glfwGetTime()を使用して実行時間を秒単位で取得します。 次に、sin関数を使用して
0.0
1.0
の範囲の色を変更し、結果をgreenValueに格納します。 次に、glGetUniformLocationを使用してourColor uniformの場所を照会します。 シェーダープログラムとuniformの名前(場所を取得したい)をquery関数に提供します。 GlGetUniformLocationが-1
を返す場合、場所が見つかりませんでした。 最後に、関数gluniform4fを使用して一様な値を設定できます。 ただし、uniformを更新するには、glUseProgramを呼び出して最初にプログラムを使用する必要があります。OpenGLはそのコアにCライブラリであるため、関数のオーバーロードのネイティブサポートを持っていないので、OpenGLは必要な型ごとに新しい関数を定義します。glUniformはこれの完璧な例です。 この関数には、設定するユニフォームのタイプに特定の後置が必要です。 可能な接尾辞のいくつかは次のとおりです。
f
float
int
を期待しています。ui
unsigned int
を期待しています。3f
float
を期待しています。fv
float
ベクトル/配列を期待しています。
OpenGLのオプションを設定したいときはいつでも、あなたのタイプに対応するオーバーロードされた関数を選択するだけです。 私たちの場合、制服の4つの浮動小数点数を個別に設定して、gluniform4fを介してデータを渡します(
fv
バージョンを使用することもできます)。均一な変数の値を設定する方法がわかったので、それらをレンダリングに使用できます。 色を徐々に変化させたい場合は、フレームごとにこのユニフォームを更新したいと考えています。 したがって、greenValueを計算し、各レンダリング反復を均一に更新します。
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
コードは、前のコードの比較的簡単な適応です。 今回は、三角形を描画する前に、フレームごとに一様な値を更新します。 ユニフォームを正しく更新すると、三角形の色が緑色から黒色に徐々に変化し、緑色に戻ることがわかります。 p>
あなたが立ち往生している場合は、ここでソースコードをチェックしてください。
ご覧のように、uniformsは、フレームごとに変更される属性を設定したり、アプリケーションとシェーダーの間でデータを交換したりするのに便利なツールですが、各頂点に色を設定したい場合はどうすればよいですか? その場合、頂点がある数のユニフォームを宣言する必要があります。 より良い解決策は、私たちが今やろうとしていることである頂点属性に多くのデータを含めることです。
より多くの属性!前の章では、VBOを埋め、頂点属性ポインタを設定し、それをすべてVAOに格納する方法を見ました。 今回は、頂点データにもカラーデータを追加したいと思います。 色データを3
float
として頂点配列に追加します。 三角形の各角に赤、緑、青の色をそれぞれ割り当てます。float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
頂点シェーダに送信するデータが増えたので、頂点シェーダを調整して、頂点属性入力とし レイアウト指定子を使用してaColor属性の位置を1に設定することに注意してください。
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
フラグメントの色にuniformを使用しなくなった頂点属性ポインターを構成します。 VBOのメモリ内の更新されたデータは、次のようになります:
現在のレイアウトを知ることで、glVertexAttribPointerで頂点フォーマットを更新することができます。
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
glVertexAttribPointerの最初のいくつかの引数は比較的簡単です。 今回は、属性location
1
3
float
sであり、値を正規化しません。二つの頂点属性があるので、ストライド値を再計算する必要があります。 データ配列内の次の属性値(例えば、位置ベクトルの次の
x
6
float
float
24
バイト)。
また、今回はオフセットを指定する必要があります。 各頂点について、位置頂点属性が最初であるため、0
のオフセットを宣言します。 Color属性は位置データの後から開始されるため、オフセットは3 * sizeof(float)
12
バイト)になります。アプリケーションを実行すると、次の画像が表示されます。
立ち往生している場合は、ここでソースコードを確認してくださ
画像は、私たちが今見ている巨大なカラーパレットではなく、3色しか提供していないので、あなたが期待するものではないかもしれません。 これはすべて、フラグメントシェーダのフラグメント補間と呼ばれるものの結果です。 三角形をレンダリングする場合、ラスタライズ段階では、通常、最初に指定された頂点よりも多くのフラグメントが生成されます。 次に、ラスタライザは、三角形の形状上の位置に基づいて、それらの各フラグメントの位置を決定します。
これらの位置に基づいて、フラグメントシェーダのすべての入力変数を補間します。 たとえば、上の点が緑色で下の点が青色である線があるとします。 フラグメントシェーダが行の70%
の位置の周りにあるフラグメントで実行される場合、結果の色入力属性は緑と青の線形結合になります。 これはまさに三角形で起こったことです。3つの頂点と3つの色があり、三角形のピクセルから判断すると、おそらく約50000個のフラグメントが含まれており、フラグメントシェーダはそれらのピク あなたが色をよく見てみると、それはすべて理にかなっていることがわかります:赤から青は最初に紫になり、次に青になります。 フラグメント補間は、フラグメントシェーダのすべての入力アトリビュートに適用されます。
私たち自身のシェーダクラス
シェーダの作成、コンパイル、管理は非常に面倒です。 シェーダーの最後の仕上げとして、ディスクからシェーダーを読み取り、コンパイルしてリンクし、エラーをチェックし、使いやすいshaderクラスを構築することで、私たちの生活を少し楽にするつもりです。 これはまた、これまでに学んだ知識の一部を有用な抽象オブジェクトにどのようにカプセル化できるかについての少しのアイデアを提供します。
私たちは、主に学習目的と移植性のために、ヘッダファイルに完全にシェーダクラスを作成します。 まず、必要なインクルードを追加し、クラス構造を定義することから始めましょう。
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
ヘッダーファイルの先頭にいくつかのプリプロセッサディレクティブを使用しました。 これらの小さなコード行を使用すると、複数のファイルにシェーダーヘッダーが含まれていても、このヘッダーファイルがまだ含まれていない場合にのみ、コ これにより、リンクの競合が防止されます。
shaderクラスは、シェーダープログラムのIDを保持します。 そのコンストラクタには、頂点シェーダーとフラグメントシェーダのソースコードのファイルパスが必要で、ディスク上に単純なテキストファイルとして保 少し余分を追加するには、我々はまた、私たちの生活を少し楽にするために、いくつかのユーティリティ関数を追加します。.. 関数は、均一な場所を照会し、その値を設定します。
ファイルからの読み取り
C++filestreamsを使用して、ファイルからいくつかの
string
オブジェクトにコンテンツを読み取ります。Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
次に、シェーダをコンパイルしてリンクする必要があります。 コンパイル/リンクが失敗した場合は、コンパイル時のエラーを出力することに注意してください。 これはデバッグ時に非常に便利です(最終的にはこれらのエラーログが必要になります)。
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
use関数は簡単です: 同様に、ユニフォームセッター関数のいずれかについて:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
そこには完成したシェーダクラスがあります。 ここでは、頂点とフラグメントシェーダーのソースコードを
shader.vs
shader.fs
.vs
.fs
非常に直感的です。新しく作成したshaderクラスを使用して、ここでソースコードを見つけることができます。 シェーダーファイルのパスをクリックすると、シェーダーのソースコードを見つけることができます。
演習
- 三角形が逆さまになるように頂点シェーダを調整します:解決策。
- ユニフォームを介して水平オフセットを指定し、このオフセット値を使用して頂点シェーダの画面の右側に三角形を移動します。
out
キーワードを使用して頂点位置をフラグメントシェーダに出力し、フラグメントの色をこの頂点位置に等しく設定します(頂点位置の値 次の質問に答えてみてください:なぜ私たちの三角形の左下が黒いのですか?:解決策。