OpenGLのライト1:基本色と照明

この記事では、学習、チュートリアルを参照することが推奨された学習の個人的な記録である https://learnopengl-cn.github.io/を
主に中国語の翻訳が提供する、著者JoeyDeVriesと品質のチュートリアル元に非常に感謝

コンテンツは、コメントを挿入、あなたはスキップすることができます

序文

照明(私たちは、簡単にOpenGLでカラー(色)を使用する方法を述べたが、非常に表面的な知識であり、今、私たちは色が何であるかのより詳細な議論をしたいし、また、光の研究のために、我々はまだ接触して)シーンの作成

カラー

まず、我々は現実の世界のすべての色が数値で表すことができる無限の色の現実の世界をシミュレートするために、限られた値を使用して、そうではないされていることを知っている必要がありますが、我々はまだ、数値によって表示するために色をたくさんすることができ、あなたはおそらくも、色と現実の違いを意識することはありません

色は、任意の色を組み合わせることができ、通常、3つだけの値を用いて、RGBと略記される3つの成分の赤(赤)、(緑)、緑色、および青色(ブルー)によってデジタル化することができます

例えば、取得するにはオレンジ(コーラル)の色を、私たちは色のベクトルを定義することができます。

glm::vec3 coral(1.0f, 0.5f, 0.31f);

私たちは、オブジェクトの色の色が対象ではありません、本当に(反射)の色は、他の言葉では、これらの色は、オブジェクトで吸収することができません(アブゾーブ)実際の生活の中で持っているが、それが反映され(拒否されます色)私たちは、オブジェクトを認識することができます色です

ホワイト太陽光が実際に見ることができます作られた多くの異なった色の組み合わせである、オレンジ色のオブジェクトで、私たちの白色光場合は、オブジェクトはすべてのサブ色でオレンジ色に白色光オレンジ色の追加を吸収し、図を参照してください、ではありませんオブジェクトがオレンジ色のように見えるように、それは、オレンジ色の光が私たちの目に反映され、吸収しました

これらの法律は、我々は、OpenGLの光源を作成するとき、我々はそれから、グラフィックスを計算し、白色太陽光に光源当分の間、光の色を与える必要があり、色が直接グラフィックス分野で使用された反射しました反射色?

限り、物体光源色の色値を乗算する(外積)が、これをオブジェクトの色によって反射されます。

glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);

我々は、玩具の色が見ることができる吸収する色の大部分の白色光源が、赤、緑、青の成分の独自の値に応じてその色も色の現実を示した反射の一定量を加えましたワークス

したがって、我々は、オブジェクトの色を定義することができている光源の各色成分からオブジェクトのサイズと反射


我々は緑色の光を使用する場合さて、次に何が起こるのだろうか?

glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);

それは見ることができ、そして何の赤と青の光を吸収又は反映するために、私たちのおもちゃをしないために。おもちゃは半分に緑色の光値を吸収し、それでもまた、緑の値の半分を反射し、おもちゃが今暗緑色のように見える、私たちはおもちゃを照らすために緑色の光を使用する場合は、緑色成分のみができることを見ることができます赤と青の反射との認識は、私たちによって知覚されるものではありません

これらの色の理論が十分である、のは、バーのシーンで実験を構築してみましょう

照明シーンを構築

次は広く、いくつかの興味深い視覚効果を作成し、実世界の照明効果をシミュレートするために色を使用します
、我々は今、光源を使用するので、我々は彼らがシーンにおける可視オブジェクトとして表示させたいと、少なくとも追加します照明効果をシミュレートするテスト・オブジェクト

まず、我々はそれが前に行うために使用されるオブジェクトがキャストされるような物体光(光をキャスト)、必要なボックスには
、我々は、3Dシーンで、光源の位置を表すために体を必要とし、シンプルさが、我々はまだへのキューブを使用しますこれは、光源を表します

頂点バッファオブジェクト(VBO)を記入し、頂点はポインタと今のものの他のいくつかの混乱についての設定はあなたのために非常に簡単であるべき属性が、私たちは、これらの手順を繰り返すことはしません、とあなたはまだ、これは非常に困難であると思われる場合、それが推奨されますあなたはその後を通して見て、以前のブログ

まあ開始

まず、我々は(この時間は、我々はテクスチャ座標を持っていないが)、コンテナの頂点位置が維持され、以前の頂点シェーダと比較してボックスを描画する頂点シェーダを必要としないので、新たな頂点シェーダコード我々は前に書かれたコードを簡素化します。

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

あなたのデータを更新することを忘れないでくださいと頂点は、一貫性のある新しい頂点シェーダでそれを作るためにポインタを属性(あなたはテクスチャデータを保持し、ポインタを属性することができますが、これがブログでそれらを使用することはありません)

我々は表現キューブランプ(光源)を作成する必要があるので、私たちはVAOのための特別なランプを作成する必要があるため

もちろん、我々はまた、我々は頻繁にするために属性データとポインタを頂点になる同じVAO、単純にそのいくつかの良いを行うためのモデル(モデル)変換行列が、次のチュートリアルを使用して照明やその他のオブジェクトを作ることができます変更は、我々はこれらの変化は、(我々は唯一のランプの頂点位置を気)ランプに影響を与えたくないので、私たちは新しいVAOランプを作成する必要があります

unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

このコードは、あなたのための非常に直感的であるべきであり、今、私たちは手段ライトボックスや照らされたオブジェクトを作成している、私たちはライン上のフラグメントシェーダを定義する必要があります。

#version 330 core
out vec4 FragColor;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    FragColor = vec4(lightColor * objectColor, 1.0);
}

このフラグメントシェーダに許容される色と光源の均一な変数からオブジェクトの色、我々は物体光(反射)色の色が乗算され

シェーダは理解が容易であるべきで、私たちは前に述べたサンゴへのオブジェクトの色を設定して、白色光源が配置されています。

// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor",  1.0f, 1.0f, 1.0f);

それは、我々は頂点またはフラグメントシェーダを変更したときに、ランプの位置や色が変わる、と指摘、これは我々が望むものではありません、我々は次のチュートリアルでライティング計算によって色の光を望んでいません(それは、より本物のソースのようなものだったように)結果が影響を受けるが、それは他のコンピューティングから分離することができると、私たちは明るい光を維持したい、色の変化は、他に影響されません

この目標を達成するために、我々はする必要がランプを描画するシェーダーの別のセットを作成することにより、他の光は、シェーダを変更したとき、それが影響を受けることができないことを保証し、頂点シェーダ、当社の現在の頂点シェーダは同じですあなたは今、直接頂点シェーダにライトに使用されることができます

ランプフラグメントシェーダのランプは、ランプの色が点灯したことを確認するために、一定の白定数を定義します。

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
}

我々は、オブジェクトを描画するとき、私たちはボックス(あるいは他のオブジェクト)を描画する光シェーダの定義を使用する必要があります。私たちは光を描きたい場合は、我々はチュートリアルの後に我々は徐々にゆっくりと、より現実的な効果を得ることができ、この光シェーダを更新する、ランプシェーダを使用します。

この光キューブを使用しての主な目的は、私たちは、シーン内の光源の特定の場所を知らせることです。それは視覚的な感覚を持っていない、我々は通常、シーン内の光源の位置を定義するが、これは唯一の場所です。実際のランプを示すために、我々は、キューブの光源が同じ位置にプロットされて表します。それはそれを描画するために我々は、シーン内の光の影響を受け、それが白の状態にあった、私たちの新しいフラグメントシェーダを使用します。

私たちは、グローバル宣言vec3シーンのワールド空間座標における光源の位置を表す変数を:

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

その後、我々はそれがそれほど明白ではない、それは少し縮小します、ここで明るみにシフトします:

model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));

ランプキューブの描画コードは次のようになります。

lampShader.use();
// 设置模型、视图和投影矩阵uniform
lampShader.setMat4("projection", projection);
lampShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // 小点的立方体
lampShader.setMat4("model", model);
// 绘制灯立方体对象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);

私たちは光のクリーンな実験サイトを持つことができるように、正しい位置にあなたのプログラム内のすべての上記のコードを入れてください。すべてがうまくいけば示すように、効果は(プルバックカメラ)を実行します

ライト財団

現実世界の光はので、私たちは、限られた計算能力を備えたコンピュータを完全にシミュレートすることができない持っている非常に複雑で、多くの要因によって影響を受けている単純化したモデル使用してOpenGLの照明現実のは、それらに対処するために近似されますより簡単に、そしてそれはほとんどのように思えます

これらのモデルは、モデルが呼ばれており、光の物理的特性の我々の理解の照明に基づいているPhongライティングモデル(モデル照明フォン):ライティングモデル風水主な構造は、3つのコンポーネントで構成され、環境(周囲)マン反射(拡散)ミラー(鏡面)

1565256379160

  • 環境光(アンビエント照明):でも、夜には、多くの場合、そこにいくつかの明るい(月、遠くの光)が世界に残っているので、オブジェクトが完全に暗くなることはほとんどありません、この状況をシミュレートするために、私たちは環境を使用します一定の光、それは常に「色」をオブジェクトになります
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact),它是冯氏光照模型中视觉上最显著的分量,物体的某一部分越是正对着光源,它就会越亮
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点,镜面光照的颜色相比于物体的颜色会更倾向于光的颜色

为了创建较为真实和丰富的视觉场景,我们希望模拟至少这三种光照分量,现在我们将以最简单的一个开始:环境光照

环境光照 Ambient Lighting

首先你要知道,生活中的光通常都不是来自于同一个光源,而是来自于我们周围分散的很多光源,很多光源并不显而易见,生活中的光可以向很多方向发散并反弹,从而到达较远的点

总的来说,光能够在其它的表面上反射,从而对一个物体产生间接的影响,考虑到以上情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂,所以,我们现在会先使用一个简化的全局照明模型,即环境光照

我们之前使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光,那么把环境光照添加到场景里也差不多:我们用光的颜色乘以一个很小的常量(环境因子),再乘以物体的颜色,然后将最终结果作为片段的颜色:

// 片段着色器
void main()
{
    float ambientStrength = 0.2;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}

现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了,这个物体非常暗,但由于应用了环境光照(光源立方体没受影响是因为我们对它使用了另一个着色器),也不是完全黑的

如你所见环境光照本身不能提供特别强的效果,但是加入漫反射光照就能开始对物体产生显著的视觉影响了

漫反射光照 Diffuse Lighting

漫反射光照简单来说就是,使物体上与光线方向越接近的片段能从光源处获得更多的亮度

我们需要测量这个光线是以什么角度接触到这个片段的,如果光线垂直于物体表面,这束光对物体的影响会达到最大化(也就是最亮)

为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),这两个向量之间的角度很容易就能够通过点乘计算出来(两个单位向量的夹角越小,它们点乘的结果越倾向于1,当两个向量的夹角为90度的时候,点乘会变为0)

点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,这样不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同

现在我们知道了,计算漫反射光照需要:

  • 法向量:一个垂直于顶点表面的向量
  • 定向的光线:作为光源的位置片段的位置之间向量差的方向向量,为了计算这个光线,我们需要光的位置向量和片段的位置向量

法向量(Normal Vector)

法向量是一个垂直于顶点表面的(单位)向量,由于顶点本身并没有表面(它只是空间中一个独立的点),我们要利用它周围的顶点来计算出这个顶点的表面

我们要用到一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中,更新后的顶点数据数组如下:

float vertices[] = {
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
     0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
    -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f, 

    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,

    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,
    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,

     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
     0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,

    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f
};

这些法向量真的是垂直于立方体各个平面的表面的

然后更新:

//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//法向量属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

由于我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

现在我们已经向每个顶点添加了一个法向量并更新了顶点着色器,我们还要更新顶点属性指针,注意,灯使用同样的顶点数组作为它的顶点数据,然而灯的着色器并没有使用新添加的法向量,我们不需要更新灯的着色器或者是属性的配置,但是至少要修改一下顶点属性指针来适应新的顶点数组的大小:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把步长参数改成float大小的6倍就行了

虽然对灯的着色器使用不能完全利用的顶点数据看起来不是那么高效,但这些顶点数据已经从箱子对象载入后开始就储存在GPU的内存里了,所以我们并不需要储存新数据到GPU内存中,对比给灯专门分配一个新的VBO更高效

所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器:

// 顶点着色器
out vec3 Normal; //新增

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal; //新增
}

在片段着色器中也要加入定义相应的输入变量:

// 片段着色器
in vec3 Normal;

我们现在对每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量

光线方向向量

光源的位置向量

由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform:

// 片段着色器
uniform vec3 lightPos;

然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量作为光源位置:

// 渲染循环内
lightingShader.setVec3("lightPos", lightPos);
片段的位置向量

最后还需要片段的位置,我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标,这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标:

// 顶点着色器
out vec3 FragPos;  
out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

最后,在片段着色器中添加相应的输入变量

// 片段着色器
in vec3 FragPos;

现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了

计算漫反射光照

我们的第一件事是计算光线的方向向量,也就是是光源位置向量片段位置向量之间的向量差,同时我们要把结果转化为单位向量:

// 片段着色器
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);

当计算光照时,我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向,所以,几乎所有的计算都使用单位向量完成,这样可以简化大部分的计算(比如点乘)

下一步,我们对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响,结果值再乘以光的颜色,得到漫反射分量

float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
//两个向量之间的角度越大,漫反射分量就会越小

但是,如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致漫反射分量变为负数,为此,我们使用max函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数(负数颜色的光照是没有定义的,所以最好避免它,除非你是那种古怪的艺术家)

现在我们有了环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色

vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

好了,终于可以运行试试看了:

现在有了漫反射光照,立方体看起来就真的像个立方体了

完整着色器代码:

//片段着色器
#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 FragColor;

uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;

void main()
{
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;

    float ambientStrength = 0.2;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = (ambient + diffuse) * objectColor;
    FragColor = vec4(result, 1.0);
}
//顶点着色器
#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;  
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

最后一件事

现在我们已经把法向量从顶点着色器传到了片段着色器,可是,目前片段着色器里的计算都是在世界空间坐标中进行的,那我们是不是应该把法向量也转换为世界空间坐标?对,但如果你直接用它乘以模型矩阵就可能会出现下图的情况

img

为什么呢?

首先,法向量只是一个方向向量,方向向量不能表达空间中的特定位置,同时,法向量没有齐次坐标(顶点位置中的w分量),也就是说位移操作不应该影响到法向量

对于法向量,我们只希望对它实施缩放和旋转变换

因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分:只选用模型矩阵左上角3×3的矩阵,或者把法向量的w分量设置为0再乘以4×4矩阵

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了,因此,我们不能用这样的模型矩阵来变换法向量

修复的方法是使用一个为法向量专门定制的模型矩阵,这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响

法线矩阵被定义为「模型矩阵左上角 的逆矩阵 的转置矩阵」,如果你不明白这是什么意思,别担心,我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix),注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵

现在,在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3的法向量

// Normal = aNormal;
Normal = mat3(transpose(inverse(model))) * aNormal;

在漫反射光照部分,光照表现没有出现问题,是因为我们没有对物体本身执行任何缩放操作

即使是对于着色器来说,逆矩阵也是一个开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点都进行这样的处理,当然我们用作学习目这样做是可以的,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出法线矩阵,然后通过uniform把值传递给着色器(像模型矩阵一样)

镜面光照 Specular Lighting

如果你还没晕,我们就再把镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整

和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的

镜面光照是基于光的反射特性,如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化

img

我们通过反射法向量周围光的方向来计算反射向量,然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大

它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光

观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它,之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量

我们选择在世界空间进行光照计算,但是大多数人趋向于在观察空间进行光照计算,在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置,可是我发现在学习的时候在世界空间中计算光照更符合直觉,如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量都用观察矩阵进行变换(记得也要改变法线矩阵)

为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者),所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:

//片段着色器
uniform vec3 viewPos;
//渲染循环
lightingShader.setVec3("viewPos", camera.Position);

现在我们已经获得所有需要的变量,可以计算高光强度了

首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响

float specularStrength = 0.5;

如果我们把它设置为1.0f,我们会得到一个非常亮的镜面光分量,这对于一个珊瑚色的立方体来说有点太多了(之后我们再讨论如何合理设置这些光照强度,以及它们是如何影响物体的)

下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

需要注意的是我们对lightDir向量进行了取反,reflect函数要求第一个向量是光源指向片段位置的向量,但是lightDir当前正好相反,是从片段指向光源(由先前我们计算lightDir向量时,减法的顺序决定),为了保证我们得到正确的reflect向量,我们通过对lightDir向量取反来获得相反的方向
第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

剩下要做的是计算镜面分量

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂,这个32是高光的反光度(Shininess),一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小

img

我们不希望镜面成分过于显眼,所以我们把指数保持为32,剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

我们现在为冯氏光照计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面:

源码:

//片段着色器

#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 FragColor;

uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;
uniform vec3 viewPos;

float specularStrength = 0.5;

void main()
{
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;

    float ambientStrength = 0.2;
    vec3 ambient = ambientStrength * lightColor;

    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;

    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
}
//顶点着色器

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;  
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = mat3(transpose(inverse(model))) * aNormal;
    FragPos = vec3(model * vec4(aPos, 1.0));
}

在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型,在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的,结果就是这种光照看起来不会非常真实,除非使用了大量顶点

img

在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)

由于插值,这种光照看起来有点逊色,而冯氏着色能产生更平滑的光照效果

现在你应该能够看到着色器的强大之处了,只用很少的信息,着色器就能计算出光照如何影响到所有物体的片段颜色,之后我们还会更深入的研究光照模型

おすすめ

転載: www.cnblogs.com/zhxmdefj/p/11360712.html
おすすめ