OpenGL-基本的な知識の要約

画像レンダリングパイプライン

OpenGLでは、すべてが3D空間にあり、画面とウィンドウは2Dピクセル配列です。これにより、OpenGLの作業のほとんどは、3D座標を画面に合う2Dピクセルに変換します。3D座標を2D座標に変換するプロセスは、OpenGLグラフィックスレンダリングパイプラインです(グラフィックスパイプラインは、ほとんどがパイプラインとして変換され、実際には、さまざまな変更が処理されて最終的にに表示される、伝送パイプラインを通過する元のグラフィックスデータの束を指します。画面プロセス)管理。グラフィックスレンダリングパイプラインは、2つの主要部分に分けることができます。最初の部分は3D座標を2D座標に変換し、2番目の部分は2D座標を実際の色付きピクセルに変換します。
グラフィックスレンダリングパイプラインは、3D座標のセットを受け入れ、それらを画面上で色付きの2Dピクセルに変換します。グラフィックスレンダリングパイプラインはいくつかのステージに分割でき、各ステージは前のステージの出力を入力として受け取ります。これらのステージはすべて高度に専門化されており(すべて特定の機能があります)、簡単に並行して実行できます。今日のほとんどのグラフィックスカードには数千の小さな処理コアがあり、GPUの各(レンダリングパイプライン)ステージに対して独自の小さなプログラムを実行するため、グラフィックスレンダリングパイプラインでデータをすばやく処理できます。これらの小さなプログラムはシェーダーと呼ばれます。
ここに写真の説明を挿入

  • 頂点シェーダー:入力として単一の頂点を取ります。頂点シェーダーの主な目的は、3D座標を別の3D座標に変換することです(後で説明します)。頂点シェーダーを使用すると、頂点属性に対して基本的な処理を実行できます。
  • プリミティブアセンブリ:ステージは、頂点シェーダーによって出力されたすべての頂点を入力として受け取り(GL_POINTSの場合、それは頂点です)、すべてのポイントを指定されたプリミティブの形状にアセンブルします。このセクションの例では、は三角形です。
  • ジオメトリシェーダー:ジオメトリシェーダーは、入力としてプリミティブの形式で頂点のコレクションを受け取ります。新しい頂点を生成して他の形状を生成することにより、新しい(または他の)プリミティブを構築できます。この例では、別の三角形を生成します。
  • ラスタライズ段階:ここでは、プリミティブを最終画面の対応するピクセルにマップし、フラグメントシェーダーで使用するフラグメントを生成します。クリッピングは、フラグメントシェーダーが実行される前に実行されます。トリミングすると、ビューを超えたすべてのピクセルが破棄され、実行効率が向上します。
  • フラグメントシェーダー:主な目的は、すべてのOpenGLの高度な効果が生成されるピクセルの最終的な色を計算することです。通常、フラグメントシェーダーには、最終ピクセルの色を計算するために使用できる3Dシーンデータ(照明、影、明るい色など)が含まれています。
  • アルファテストとブレンディング(ブレンディング):対応するすべての色の値が決定された後、最終オブジェクトは最終段階に渡されます。これをアルファテストとブレンディング段階と呼びます。この段階で、フラグメントの対応する深度(およびステンシル)値が検出され(後述)、ピクセルが他のオブジェクトの前にあるか後ろにあるか、およびピクセルを破棄する必要があるかどうかを判断するために使用されます。このステージでは、アルファ値(アルファ値はオブジェクトの透明度を定義します)もチェックし、オブジェクトをブレンドします(ブレンド)。したがって、フラグメントシェーダーで1つのピクセルの出力色を計算しても、複数の三角形をレンダリングすると、最終的なピクセルの色が完全に異なる場合があります。

VAO、VBO、EBOの関係

ここに写真の説明を挿入

頂点属性フォーマット

ここに写真の説明を挿入

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

座標系

OpenGLは、頂点シェーダーが実行されるたびに、表示されるすべての頂点が正規化されたデバイス座標(NDC)であることを期待しています。つまり、各頂点のx、y、z座標は、-1.0〜1.0である必要があり、この座標範囲を超える頂点は表示されません。通常、座標の範囲を自分で設定し、これらの座標を頂点シェーダーで標準化されたデバイス座標に変換します。次に、これらの標準化されたデバイス座標がラスタライザに渡され、画面上で2次元座標またはピクセルに変換されます。
座標を標準化されたデバイス座標に変換してから画面座標に変換するプロセスは、通常、パイプラインと同様に段階的に実行されます。パイプラインでは、オブジェクトの頂点が複数の座標系(座標系)に変換されてから、最終的に画面座標に変換されます。オブジェクトの座標をいくつかの中間座標系(中間座標系)に変換する利点は、これらの特定の座標系では、一部の操作または計算がより便利で簡単になることです。これはすぐに明らかになります。私たちにとって重要な5つの異なる座標系があります。
次の図は、プロセス全体と各変換プロセスの機能を示しています。
ここに写真の説明を挿入

座標系

  1. ローカル座標は、ローカル原点を基準にしたオブジェクトの座標とオブジェクトの始点の座標です。
  2. 次のステップは、ローカル座標をより広い空間範囲にあるワールド空間座標に変換することです。これらの座標は、世界のグローバルな原点を基準にしており、他のオブジェクトとともに世界の原点を基準にして配置されます。
  3. 次に、世界座標を観測空間座標に変換し、各座標がカメラまたは観測者の視点から観測されるようにします。
  4. 座標が観測空間に到達したら、クリッピング座標に投影する必要があります。クリッピング座標は-1.0から1.0の範囲で処理され、どの頂点が画面に表示されます。
  5. 最後に、トリミング座標を画面座標に変換し、ビューポート変換と呼ばれるプロセスを使用します。ビューポート変換は、-1.0〜1.0の範囲の座標を、glViewport関数で定義された座標範囲に変換します。最終的に変換された座標はラスタライザーに送信され、フラグメントに変換されます。

正射影と透視投影

ここに写真の説明を挿入
ここに写真の説明を挿入
上記の錐台は、幅、高さ、近距離(近距離)平面および遠距離(遠距離)平面によって指定される可視座標を定義します。ニアプレーンの前またはファープレーンの後ろに表示される座標はすべてクリップされます。各ベクトルのw成分は変更されないため、正投影錐台は錐台内のすべての座標を標準化されたデバイス座標に直接マッピングします。w成分が1.0に等しい場合、遠近法による除算ではこの座標は変更されません。
頂点座標の各コンポーネントは、そのwコンポーネントで除算され、オブザーバーから離れるほど、頂点座標は小さくなります。これが、wコンポーネントが非常に重要であるもう1つの理由です。これは、透視投影を実行するのに役立ちます。

変換を組み合わせる

上記の各ステップでは、変換行列(モデル行列、観測行列、射影行列)を作成します。頂点座標は、次のプロセスに従ってクリッピング座標に変換され
ます。Vクリップ= M投影∗ Mビュー∗ Mモデル∗ VローカルV_ {クリップ} = M_ {投影} * M_ {ビュー} * M_ {モデル} * V_ {ローカル}VC L I P=MP R O 、J 、E 、C 、T I O NMV私は電子ワットMM O D EのLVL O C A L
行列演算の順序が逆になっていることに注意してください(行列の乗算を右から左に読み取る必要があることに注意してください)。最終的な頂点は、頂点シェーダーのgl_Positionに割り当てる必要があり、OpenGLは自動的に遠近法の分割とクリッピングを実行します。

右側の座標系

慣例により、OpenGLは右手の座標系です。簡単に言うと、正のx軸は右側にあり、正のy軸は上を向いており、正のz軸は後ろを向いています。画面が3つの軸の中心にあり、正のz軸が画面を通過して自分に向かっているとします。座標系は次のように描画されます。
ここに写真の説明を挿入

右手座標系と呼ばれる理由を理解するには、次の手順に従います。

  • 正のy軸に沿って右腕を伸ばし、指を上に向けます。
  • 親指は右を向いています。
  • 人差し指を上に向けます。
  • 中点は90度下に曲がっています。

ビデオカメラ

カメラ/ビュースペースについて説明するときは、カメラの視点をシーンの原点として使用した場合のシーン内のすべての頂点の座標について説明します。観測マトリックスは、すべてのワールド座標をカメラの位置と方向を基準にして変換します。座標を観察します。 。カメラを定義するには、世界空間でのカメラの位置、観測方向、カメラの右を指すベクトル、およびカメラの上を指すベクトルが必要です。観察者の読者は、3つの単位軸が互いに垂直でカメラの原点を持つ座標系を実際に作成したことに気付いたかもしれません。
ここに写真の説明を挿入

LookAtマトリックス

行列を使用する利点の1つは、3つの相互に垂直な(または非線形の)軸を使用して座標空間を定義する場合、これらの3つの軸と平行移動ベクトルを使用して行列を作成でき、この行列を使用できることです。乗算するには任意のベクトルを使用して、その座標空間に変換します。これはまさにLookAtマトリックスが行うことです。3つの相互に垂直な軸とカメラ空間を定義する位置座標があるので、独自のLookAtマトリックスを作成できます。
ここに写真の説明を挿入
ここで、Rは右ベクトル、Uは上ベクトル、Dは上ベクトルです。方向ベクトルです。Pはカメラ位置ベクトルです。最終的には世界を自分の動きの反対方向に変換したいので、位置ベクトルは反対であることに注意してください。このLookAt行列を観測行列として使用すると、すべての世界座標を定義したばかりの観測空間に効率的に変換できます。LookAtマトリックスは、その名前が示すように、特定のターゲットを調べる観測マトリックスを作成します。

幸い、GLMはすでにこのサポートを提供しています。私たちがしなければならないのは、カメラの位置、ターゲットの位置、およびワールド空間のアップベクトルを表すベクトル(正しいベクトルを計算するために使用するアップベクトル)を定義することだけです。次に、GLMはLookAt行列を作成します。これは、観測行列として使用できます。

オイラーポイント

オイラー角は、3D空間の任意の回転を表すことができる3つの値であり、18世紀にレオンハルトオイラーによって提案されました。オイラー角には、ピッチ、ヨー、ロールの3種類があります。次の図はその意味を示しています。
ここに写真の説明を挿入

用語集リスト

  • OpenGL:関数のレイアウトと出力を定義するグラフィックAPIの正式な仕様
  • GLAD:すべての(最新の)OpenGL関数を使用できるように、すべてのOpenGL関数ポインターをロードおよび設定するために使用される拡張ロードライブラリ。
  • ビューポート:レンダリングする必要のあるウィンドウ
  • グラフィックスパイプライン:ピクセルとしてレンダリングされる前の頂点のプロセス全体
  • シェーダー:グラフィックカード上で実行される小さなプログラム。グラフィックパイプラインの多くの段階で、カスタムシェーダーを使用して元の関数を置き換えることができます。
  • 正規化されたデバイス座標(NDC):クリッピングと遠近法の分割後に頂点が最終的​​にクリッピング座標系でレンダリングされる座標系。位置がNDC -1.0〜1.0未満のすべての頂点は破棄されず、表示されません。
  • 頂点バッファオブジェクト(VBO):ビデオメモリを呼び出し、グラフィックカードで使用するためにすべての頂点データを格納するバッファオブジェクト
  • 頂点配列オブジェクト(VAO):ストレージバッファーと頂点属性の状態
  • インデックスバッファオブジェクト(要素バッファオブジェクト、EBO):インデックス付き図面のインデックスを格納するバッファオブジェクト
  • ユニフォーム:特殊なタイプのGLSL変数であり、Quanjude(シェーダープログラム内のすべてのシェーダーがユニフォーム変数にアクセスできます)であり、一度設定するだけで済みます。
  • テクスチャ:オブジェクトをラップして、細かい視覚効果を与えるフィーチャタイプの画像。
  • テクスチャラッピング:テクスチャ頂点が範囲(0、1)を超えたときにOpenGLがテクスチャをサンプリングする方法を指定するモードを定義します
  • テクスチャフィルタリング:複数のテクスチャの選択肢がある場合、通常はテクスチャが拡大されている場合に、OpenGLがテクスチャをサンプリングする方法を指定するモードを定義します
  • ミップマップ:保存されている資料の一部の縮小版、および資料の適切なサイズが、観察者からの距離に応じて使用されます。
  • stb_image.h:画像読み込みライブラリ。
  • テクスチャユニット:テクスチャを異なるテクスチャユニットにバインドすることにより、同じオブジェクトに複数のテクスチャをレンダリングできるようにします。
  • ベクトル:空間内の方向や位置を定義する数学的エンティティ。
  • 行列:長方形配列の数式。
  • GLM:OpenGL用に構築された数学ライブラリ。
  • ローカルスペース:オブジェクトの初期スペース。すべての座標は、オブジェクトの原点を基準にしています。
  • ワールドスペース:すべての座標は、グローバル原点を基準にしています。
  • ビュースペース:すべての座標はカメラの視点から観察されます。
  • クリップスペース:すべての座標はカメラの視点から観察されますが、スペースは投影を使用します。このスペースは、頂点シェーダーの出力としての頂点座標の最後のスペースである必要があります。OpenGLが残りの処理を行います(トリミング/パースペクティブ分割)。
  • 画面スペース:すべての座標は画面の観点から観察されます。座標の範囲は、0から画面の幅/高さまでです。
  • LookAtマトリックス:ある位置からターゲットを観察しているユーザーに応じて、すべての座標が回転または平行移動される座標系を作成する特殊なタイプの観察マトリックス。
  • オイラー角:これらの3つの値を使用して任意の3D方向を構築できるように、ヨー、ピッチ、およびロールとして定義されます。

サンプルコードの解釈

これは、マウス、キーボード、ホイールを使用して3次元空間を移動し、10個のボックスを観察するコードであり、コードの解釈が添付されています。

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "../../util/stb_image.h"

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include "../../util/shader_s.h"

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// 屏幕大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// camera
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); // 世界空间中一个指向摄像机位置的向量
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); // 摄像机的方向,指向Z负轴
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); // 摄像机的上向量

bool firstMouse = true;
float yaw = -90.0f;	// 偏航角 yaw is initialized to -90.0 degrees since a yaw of 0.0 results in a direction vector pointing to the right so we initially rotate a bit to the left.
float pitch = 0.0f; // 俯仰角
float lastX = 800.0f / 2.0;
float lastY = 600.0 / 2.0;
float fov = 45.0f; // Field of view 视野的大小

// timing 计算时间差,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值,结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间
float deltaTime = 0.0f;	// time between current frame and last frame
float lastFrame = 0.0f;

int main()
{
    // glfw: 初始化配置
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 主版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 次版本
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心模式

#ifdef __APPLE__ //苹果系统使用
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw 创建窗口
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    // 创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。
    glfwMakeContextCurrent(window);
    // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数:当窗口被第一次显示的时候framebuffer_size_callback也会被调用。对于视网膜(Retina)显示屏,width和height都会明显比原输入值更高一点。
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback); // 鼠标位置的回调
    glfwSetScrollCallback(window, scroll_callback); // 鼠标滚轮的回调

    // 告诉 GLFW 捕捉鼠标
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers 加载所有的OpenGL函数指针,这样确保不同的平台函数是一样的
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 配置全局OpenGl状态
    // -----------------------------
    glEnable(GL_DEPTH_TEST); // 启动深度测试,这样有Z缓冲,控制3D物体的显示与遮挡

    // 构建编译着色器程序,这里使用了自定义的着色器类
    // ------------------------------------
    Shader ourShader("shader/7.3.camera.vs", "shader/7.3.camera.fs");

    // 设置顶点数组(和buffers)和配置顶点属性
    // ------------------------------------------------------------------
    float vertices[] = {
            // 顶点坐标x,y,z,纹理坐标x1,y1
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f,

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

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

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

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

            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            -0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f
    };
    // 我们的立方体在世界空间的坐标
    glm::vec3 cubePositions[] = {
            glm::vec3( 0.0f, 0.0f, 0.0f),
            glm::vec3( 2.0f, 5.0f, -15.0f),
            glm::vec3(-1.5f, -2.2f, -2.5f),
            glm::vec3(-3.8f, -2.0f, -12.3f),
            glm::vec3( 2.4f, -0.4f, -3.5f),
            glm::vec3(-1.7f, 3.0f, -7.5f),
            glm::vec3( 1.3f, -2.0f, -2.5f),
            glm::vec3( 1.5f, 2.0f, -2.5f),
            glm::vec3( 1.5f, 0.2f, -1.5f),
            glm::vec3(-1.3f, 1.0f, -1.5f)
    };
    unsigned int VBO, VAO; // 定义顶点缓冲对象和顶点数组对象,还有个EBO是索引缓冲对象,在用两个三角形画长方形定义绘制顺序的时候有用到
    glGenVertexArrays(1, &VAO); // 使用函数生成VAO,第一个是生成数量,第二个是对象索引
    glGenBuffers(1, &VBO); // 使用函数生成VBO

    // 要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO); // 使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
    // 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO),glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中
    // 三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //** 设置顶点属性指针
    // 位置属性,第一个参数是顶点着色器layout的值,第二个参数是顶点属性大小3,第三个参数是指定数据的类型,第四个是否标准化,第五个是步长,连续顶点属性组之间的间隔,最后一个是位置数据在缓冲中起始位置的偏移量(offset)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); // 以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的
    // 纹理坐标属性2个
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);


    // 加载和创建一个纹理
    // -------------------------
    unsigned int texture1, texture2; // 纹理是使用ID引用的
    // texture 1
    // ---------
    glGenTextures(1, &texture1); // 第一个参数是生成纹理的数量,然后储存在第二个参数unsigned int数组中,这里就是单独的unsigned int。
    glBindTexture(GL_TEXTURE_2D, texture1); // 绑定到GL_TEXTURE_2D上,这时,任何调用都会用来配置texture1
    // 设置纹理环绕参数,纹理超出边界如何显示
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // 设置纹理滤波参数,放大或缩小时如何处理
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 加载图片,创建纹理和生成多级渐远纹理
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true); // tell stb_image.h 在加载纹理时绕y轴翻转
    unsigned char *data = stbi_load("resources/container.jpg", &width, &height, &nrChannels, 0);
    if (data)
    {
        // 使用前面载入的图片数据生成一个纹理,第一个参数指定了纹理目标,设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理
        // 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
        // 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
        // 第四个和第五个参数设置最终的纹理的宽度和高度
        // 下个参数应该总是被设为0(历史遗留的问题)。
        // 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
        // 最后一个参数是真正的图像数据, 当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,
        // 如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data); // 生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个很好的习惯。
    // texture 2
    // ---------
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
    // set the texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load image, create texture and generate mipmaps
    data = stbi_load("resources/awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    // tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
    // 告诉opengl对于每个sampler哪个纹理单元的所属(只需要被做一次)
    // 我们还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面:
    // -------------------------------------------------------------------------------------------
    ourShader.use(); // 别忘记在激活着色器前先设置uniform!
    ourShader.setInt("texture1", 0); //设置纹理属于哪个纹理单元GL_TEXTURE0,GL_TEXTURE1
    ourShader.setInt("texture2", 1);


    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic 每帧的逻辑
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 使用对应颜色清除颜色和深度缓冲

        // 将纹理绑定到对应的纹理单元
        // 使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。
        // 一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。
        // 通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
        // 激活纹理单元之后,接下来的glBindTexture函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活,所以我们在前面的例子里当我们使用glBindTexture的时候,无需激活任何纹理单元。
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2);

        // 确保在调用任何glUniform之前激活shader
        ourShader.use();

        // 传递投影矩阵到shader中(注意这个例子中它可能每帧都改变)
        glm::mat4 projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        ourShader.setMat4("projection", projection);

        // camera/view transformation 摄像机视角变换
        glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        ourShader.setMat4("view", view);

        // render boxes 渲染盒子
        // 任何随后的顶点属性调用都会储存在这个VAO中,这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
        // 这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
        // 一个顶点数组对象会储存以下这些内容:
        // - glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
        // - 通过glVertexAttribPointer设置的顶点属性配置。
        // - 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
        glBindVertexArray(VAO); //当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了
        for (unsigned int i = 0; i < 10; i++)
        {
            // 在绘制之前为每个物体计算模型矩阵然后传递到shader
            glm::mat4 model = glm::mat4(1.0f); // 首先确保初始化为单位矩阵
            model = glm::translate(model, cubePositions[i]);
            float angle = 20.0f * i;
            model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
            ourShader.setMat4("model", model);

            glDrawArrays(GL_TRIANGLES, 0, 36);
        }

        // glfw:使用的双缓冲所以交换buffers然后获取IO事件(keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

// float cameraSpeed = 2.5 * deltaTime;
    float cameraSpeed = 10 * deltaTime;
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        cameraPos += cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        cameraPos -= cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; //反过来因为y坐标从下到上
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.1f; // 根据自己喜好调整
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw += xoffset;
    pitch += yoffset;

    // make sure that when pitch is out of bounds, screen doesn't get flipped
    if (pitch > 89.0f)
        pitch = 89.0f;
    if (pitch < -89.0f)
        pitch = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    fov -= (float)yoffset;
    if (fov < 1.0f)
        fov = 1.0f;
    if (fov > 45.0f)
        fov = 45.0f;
}

リソースリファレンス

LearnOpenGL

おすすめ

転載: blog.csdn.net/u012457196/article/details/106586466